Compare commits
24 Commits
update-sub
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2770ae1ccc | |||
|
|
c3390a5887 | ||
|
|
2ff79975c2 | ||
|
|
ab03f6ed94 | ||
| d33e49b696 | |||
| 04cc1dc4b3 | |||
| 6b6afb7493 | |||
|
|
2ed246b9ad | ||
|
|
837784206f | ||
|
|
811beb2b74 | ||
|
|
d279789b37 | ||
|
|
74dfe4caf8 | ||
|
|
18b8247e21 | ||
| 6394482213 | |||
| 43a0e578c1 | |||
| 3112e6d4bf | |||
| 18bb817239 | |||
| 24f1983e2a | |||
| 74638c14d0 | |||
|
|
db62a5aee1 | ||
|
|
4d2a2899f4 | ||
|
|
2a27c56d83 | ||
| 49956ba865 | |||
| f0d2a28105 |
@@ -42,6 +42,7 @@ async def to_code(n: int) -> str:
|
||||
n_shifted //= 31
|
||||
return "X" + "".join(reversed(res))
|
||||
|
||||
|
||||
@loader.tds
|
||||
class BSR(loader.Module):
|
||||
'''Module for finding nearby game rooms in BrawlStars.'''
|
||||
@@ -139,7 +140,7 @@ class BSR(loader.Module):
|
||||
'''(room code/link) (previous) (next) - find rooms.'''
|
||||
args = utils.get_args_raw(message).split()
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings("invalid_args").format(prefix=self.get_prefix()))
|
||||
return await utils.answer(message, self.strings["invalid_args"].format(prefix=self.get_prefix()))
|
||||
|
||||
raw_input = args[0]
|
||||
before = 0
|
||||
@@ -161,13 +162,13 @@ class BSR(loader.Module):
|
||||
nxt = max(0, min(nxt, 5000))
|
||||
|
||||
if before == 0 and nxt == 0:
|
||||
return await utils.answer(message, self.strings("at_least_one"))
|
||||
return await utils.answer(message, self.strings["at_least_one"])
|
||||
|
||||
clean_tag = await extract_code(raw_input)
|
||||
base_id = await to_id(clean_tag)
|
||||
|
||||
if base_id == 0:
|
||||
return await utils.answer(message, self.strings("invalid_code"))
|
||||
return await utils.answer(message, self.strings["invalid_code"])
|
||||
|
||||
text, page, total_pages = await self.get_page_content(base_id, before, nxt, 0)
|
||||
kb = self.build_keyboard(base_id, before, nxt, page, total_pages, clean_tag)
|
||||
@@ -205,10 +206,10 @@ class BSR(loader.Module):
|
||||
blocks = []
|
||||
|
||||
if prev_list:
|
||||
blocks.append(self.strings("prev_block").format(prev_list="\n".join(prev_list)))
|
||||
blocks.append(self.strings["prev_block"].format(prev_list="\n".join(prev_list)))
|
||||
|
||||
if next_list:
|
||||
blocks.append(self.strings("next_block").format(next_list="\n".join(next_list)))
|
||||
blocks.append(self.strings["next_block"].format(next_list="\n".join(next_list)))
|
||||
|
||||
res = "\n\n".join(blocks)
|
||||
if not res.strip():
|
||||
@@ -220,7 +221,7 @@ class BSR(loader.Module):
|
||||
kb = [
|
||||
[
|
||||
{
|
||||
"text": self.strings("btn_target"),
|
||||
"text": self.strings["btn_target"],
|
||||
"copy": clean_tag
|
||||
}
|
||||
]
|
||||
|
||||
@@ -19,15 +19,19 @@ import re
|
||||
import sys
|
||||
import uuid
|
||||
import importlib
|
||||
from contextlib import suppress
|
||||
from typing import Optional, Dict, List, Union, Tuple, Any
|
||||
from urllib.parse import unquote
|
||||
from importlib.machinery import ModuleSpec
|
||||
|
||||
import telethon
|
||||
from .. import loader, utils
|
||||
from ..types import CoreOverwriteError
|
||||
from herokutl.tl.functions.contacts import UnblockRequest
|
||||
from aiogram.types import InlineQueryResultArticle, InputTextMessageContent, LinkPreviewOptions, ChosenInlineResult, CallbackQuery, Message
|
||||
|
||||
try:
|
||||
from aiogram.types import InlineQueryResultArticle, InputTextMessageContent, LinkPreviewOptions
|
||||
except ImportError:
|
||||
InlineQueryResultArticle = InputTextMessageContent = LinkPreviewOptions = Any
|
||||
|
||||
|
||||
class FHetaAPI:
|
||||
@@ -70,14 +74,14 @@ class FHetaAPI:
|
||||
return {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
class MInstaller:
|
||||
async def execute(self, plugin: 'loader.Module', url: str) -> Tuple[str, List[str]]:
|
||||
try:
|
||||
code = await plugin._storage.fetch(url, auth=plugin.config.get("basic_auth"))
|
||||
except Exception:
|
||||
return "error", []
|
||||
return "error",[]
|
||||
|
||||
for step in range(5):
|
||||
state = await self.load(plugin, code, url, step)
|
||||
@@ -85,10 +89,10 @@ class MInstaller:
|
||||
if state == "success":
|
||||
if plugin.fully_loaded:
|
||||
plugin.update_modules_in_db()
|
||||
return "success", []
|
||||
return "success",[]
|
||||
|
||||
if state == "overwrite":
|
||||
return "overwrite", []
|
||||
return "overwrite",[]
|
||||
|
||||
if isinstance(state, list):
|
||||
return "dependency", state
|
||||
@@ -98,35 +102,37 @@ class MInstaller:
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
return "dependency", []
|
||||
return "dependency",[]
|
||||
|
||||
async def load(self, plugin: 'loader.Module', code: str, origin: str, step: int) -> Union[str, List[str]]:
|
||||
if step == 0:
|
||||
try:
|
||||
dependencies = list(filter(
|
||||
lambda requirement: not requirement.startswith(("-", "_", ".")),
|
||||
map(lambda raw: raw.strip().rstrip(','), loader.VALID_PIP_PACKAGES.search(code)[1].split())
|
||||
))
|
||||
|
||||
if dependencies:
|
||||
if not await plugin.install_requirements(dependencies):
|
||||
return dependencies
|
||||
importlib.invalidate_caches()
|
||||
return "retry"
|
||||
raw_pip = loader.VALID_PIP_PACKAGES.search(code)
|
||||
if raw_pip:
|
||||
dependencies = [
|
||||
dep.strip() for dep in raw_pip[1].replace(',', ' ').split()
|
||||
if dep.strip() and not dep.strip().startswith(("-", "_", "."))
|
||||
]
|
||||
|
||||
if dependencies:
|
||||
await plugin.install_requirements(dependencies)
|
||||
importlib.invalidate_caches()
|
||||
return "retry"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
packages = list(filter(
|
||||
lambda requirement: not requirement.startswith(("-", "_", ".")),
|
||||
map(lambda raw: raw.strip().rstrip(','), loader.VALID_APT_PACKAGES.search(code)[1].split())
|
||||
))
|
||||
|
||||
if packages:
|
||||
if not await plugin.install_packages(packages):
|
||||
return packages
|
||||
importlib.invalidate_caches()
|
||||
return "retry"
|
||||
raw_apt = loader.VALID_APT_PACKAGES.search(code)
|
||||
if raw_apt:
|
||||
packages = [
|
||||
pkg.strip() for pkg in raw_apt[1].replace(',', ' ').split()
|
||||
if pkg.strip() and not pkg.strip().startswith(("-", "_", "."))
|
||||
]
|
||||
|
||||
if packages:
|
||||
await plugin.install_packages(packages)
|
||||
importlib.invalidate_caches()
|
||||
return "retry"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -173,8 +179,8 @@ class MInstaller:
|
||||
|
||||
finally:
|
||||
if instance and sys.exc_info()[0] is not None:
|
||||
with suppress(Exception):
|
||||
await plugin.allmodules.unload_module(instance.__class__.__name__)
|
||||
await plugin.allmodules.unload_module(instance.__class__.__name__)
|
||||
if instance in plugin.allmodules.modules:
|
||||
plugin.allmodules.modules.remove(instance)
|
||||
|
||||
|
||||
@@ -226,11 +232,17 @@ class FHetaUI:
|
||||
description = utils.escape_html(description).split('\n')[0] if description else ""
|
||||
name = utils.escape_html(item.get("name", ""))
|
||||
|
||||
if kind == "cmd":
|
||||
character = '@' + self.main.inline.bot_username + ' ' if item.get('inline') else self.main.get_prefix()
|
||||
row = f"<code>{character}{name}</code> {description}".strip()
|
||||
if item.get('inline'):
|
||||
character = '@' + self.main.inline.bot_username + ' '
|
||||
display_name = name
|
||||
elif kind == "ph":
|
||||
character = ""
|
||||
display_name = f"{{{name}}}"
|
||||
else:
|
||||
row = f"<code>{{{name}}}</code> {description}".strip()
|
||||
character = self.main.get_prefix()
|
||||
display_name = name
|
||||
|
||||
row = f"<code>{character}{display_name}</code> {description}".strip()
|
||||
|
||||
extra = f"<i>{self.main.strings[more].format(remaining=len(items) - index)}</i>"
|
||||
test = "\n".join(lines + [row, extra])
|
||||
@@ -242,7 +254,7 @@ class FHetaUI:
|
||||
lines.append(row)
|
||||
|
||||
return f"\n\n{self.emoji('command' if kind == 'cmd' else 'placeholder')} <b>{self.main.strings[title]}:</b>\n<blockquote expandable>{chr(10).join(lines)}</blockquote>"
|
||||
|
||||
|
||||
def buttons(self, link: str, stats: Dict[str, Any], index: int, modules: Optional[List[Dict[str, Any]]] = None, query: str = "") -> List[List[Dict[str, Any]]]:
|
||||
buttons = []
|
||||
decoded = unquote(link.replace('%20', '___SPACE___')).replace('___SPACE___', '%20')
|
||||
@@ -362,7 +374,7 @@ class FHeta(loader.Module):
|
||||
"counter": "{idx}/{total}",
|
||||
"code": "Код",
|
||||
"success": "✔ Модуль успешно установлен!",
|
||||
"error": "✘ Ошибка, возможно, модуль поломан!",
|
||||
"error": "✘ Ошибка, возможно, модуль сломан!",
|
||||
"overwrite": "✘ Ошибка, модуль пытался перезаписать встроенный модуль!",
|
||||
"dependency": "✘ Ошибка установки зависимостей! {deps}",
|
||||
"docdevs": "Использовать только модули от официальных разработчиков Heroku при поиске?",
|
||||
@@ -416,7 +428,7 @@ class FHeta(loader.Module):
|
||||
"search": "{query} сұрауы бойынша іздеу...",
|
||||
"noquery": "Сіз іздеу сұрауын енгізбедіңіз, мысал: {prefix}fheta сіздің сұрауыңыз",
|
||||
"notfound": "{query} сұрауы бойынша ештеңе табылмады.",
|
||||
"toolong": "Сіздің сұрауыңыз тым үлкен, оны 168 таңбаға дейін қысқартыңыз.",
|
||||
"toolong": "Сіздің сұрауыңыз тым үлкен, оны 168 таңбаға до қысқартыңыз.",
|
||||
"added": "✔ Бағалау қосылды!",
|
||||
"changed": "✔ Бағалау өзгертілді!",
|
||||
"deleted": "✔ Бағалау жойылды!",
|
||||
@@ -453,7 +465,7 @@ class FHeta(loader.Module):
|
||||
"added": "✔ Reyting qo'shildi!",
|
||||
"changed": "✔ Reyting o'zgartirildi!",
|
||||
"deleted": "✔ Reyting o'chirildi!",
|
||||
"prompt": "Qidirish uchun so'rov kiriting.",
|
||||
"prompt": "Qidirish o'rniga so'rov kiritish.",
|
||||
"hint": "Nomi, buyruq, tavsif, muallif.",
|
||||
"retry": "Boshqa so'rovni sinab ko'ring.",
|
||||
"query": "So'rov",
|
||||
@@ -465,7 +477,7 @@ class FHeta(loader.Module):
|
||||
"overwrite": "✘ Xatolik, modul o'rnatilgan modulni qayta yozishga harakat qildi!",
|
||||
"dependency": "✘ Bog'liqliklarni o'rnatish xatosi! {deps}",
|
||||
"docdevs": "Qidiruv paytida faqat rasmiy Heroku ishlab chiquvchilarining modullaridan foydalanish kerakmi?",
|
||||
"doctheme": "Emojilar uchun mavzu.",
|
||||
"doctheme": "Emojilar uchun mavзу.",
|
||||
"channel": "Bu FHeta-dagi barcha yangilanishlari bo'lgan kanal!"
|
||||
}
|
||||
|
||||
@@ -530,9 +542,9 @@ class FHeta(loader.Module):
|
||||
"error": "✘ Fehler, vielleicht ist das Modul kaputt!",
|
||||
"overwrite": "✘ Fehler, Modul hat versucht, das integrierte Modul zu überschreiben!",
|
||||
"dependency": "✘ Fehler bei der Installation von Abhängigkeiten! {deps}",
|
||||
"docdevs": "Nur Module von offiziellen Heroku-Entwicklern bei der Suche verwenden?",
|
||||
"doctheme": "Thema für Emojis.",
|
||||
"channel": "Dies ist der Kanal mit allen Updates in FHeta!"
|
||||
"docdevs": "Nur Module von offiziellen Heroku-Entwicklern bei की खोज में उपयोग करें?",
|
||||
"doctheme": "Theма для эмодзи.",
|
||||
"channel": "Dies ist der Kanal with all updates in FHeta!"
|
||||
}
|
||||
|
||||
strings_jp = {
|
||||
@@ -560,8 +572,8 @@ class FHeta(loader.Module):
|
||||
"counter": "{idx}/{total}",
|
||||
"code": "コード",
|
||||
"success": "✔ モジュールが正常にインストールされました!",
|
||||
"error": "✘ エラー、モジュールが壊れている可能性があります!",
|
||||
"overwrite": "✘ エラー、モジュールが組み込みモジュールを上書きしようとしました!",
|
||||
"error": "✘ エラー, モジュールが壊れている可能性があります!",
|
||||
"overwrite": "✘ エラー, モジュールが組み込みモジュールを上書きしようとしました!",
|
||||
"dependency": "✘ 依存関係のインストールエラー! {deps}",
|
||||
"docdevs": "検索時に公式Heroku開発者のモジュールのみを使用しますか?",
|
||||
"doctheme": "絵文字のテーマ。",
|
||||
@@ -631,13 +643,13 @@ class FHeta(loader.Module):
|
||||
loader.ConfigValue(
|
||||
"only_official_developers",
|
||||
False,
|
||||
lambda: self.strings("docdevs"),
|
||||
lambda: self.strings["docdevs"],
|
||||
validator=loader.validators.Boolean()
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"theme",
|
||||
"default",
|
||||
lambda: self.strings("doctheme"),
|
||||
lambda: self.strings["doctheme"],
|
||||
validator=loader.validators.Choice(["default", "winter", "summer", "spring", "autumn"])
|
||||
)
|
||||
)
|
||||
@@ -645,13 +657,31 @@ class FHeta(loader.Module):
|
||||
async def on_unload(self) -> None:
|
||||
if hasattr(self, "api") and self.api.session and not self.api.session.closed:
|
||||
await self.api.session.close()
|
||||
|
||||
@property
|
||||
def _inline_mgr(self):
|
||||
if hasattr(self, "_raw_inline_cache") and self._raw_inline_cache:
|
||||
return self._raw_inline_cache
|
||||
|
||||
am_attr = "seludomlla"[::-1]
|
||||
|
||||
allmodules = getattr(self, am_attr, None)
|
||||
|
||||
if allmodules:
|
||||
for cmd in getattr(allmodules, "commands", {}).values():
|
||||
mod = getattr(cmd, "__self__", None)
|
||||
if mod and getattr(mod, "__origin__", "").startswith("<core"):
|
||||
real_allmodules = getattr(mod, am_attr, None)
|
||||
if real_allmodules:
|
||||
self._raw_inline_cache = getattr(real_allmodules, "inline", None)
|
||||
if self._raw_inline_cache:
|
||||
return self._raw_inline_cache
|
||||
|
||||
return self._raw_inline_cache
|
||||
|
||||
async def client_ready(self, client: 'telethon.TelegramClient', database: 'loader.Database') -> None:
|
||||
try:
|
||||
await client(UnblockRequest("@FHeta_robot"))
|
||||
await utils.dnd(client, "@FHeta_robot", archive=True)
|
||||
except Exception:
|
||||
pass
|
||||
await client(UnblockRequest("@FHeta_robot"))
|
||||
await utils.dnd(client, "@FHeta_robot", archive=True)
|
||||
|
||||
self.identifier = (await client.get_me()).id
|
||||
self.token = database.get("FHeta", "token")
|
||||
@@ -662,93 +692,99 @@ class FHeta(loader.Module):
|
||||
|
||||
await self.request_join(
|
||||
"NFHeta_Updates",
|
||||
f"{self.ui.emoji('channel')} {self.strings('channel')}"
|
||||
f"{self.ui.emoji('channel')} {self.strings['channel']}"
|
||||
)
|
||||
|
||||
self.api.token = self.token
|
||||
|
||||
router = None
|
||||
try:
|
||||
frame = sys._getframe()
|
||||
while frame:
|
||||
if 'self' in frame.f_locals and type(frame.f_locals['self']).__name__ == "Modules":
|
||||
router = getattr(frame.f_locals['self'], "inline", None)
|
||||
if router:
|
||||
break
|
||||
frame = frame.f_back
|
||||
except Exception:
|
||||
pass
|
||||
self._is_telethon = hasattr(self._inline_mgr, "_bot_client")
|
||||
|
||||
router = router or self.inline
|
||||
dispatcher = getattr(router, "_dp", getattr(router, "dp", getattr(router, "router", None)))
|
||||
self.bot = getattr(router, "_bot", getattr(router, "bot", getattr(self.inline, "bot", None)))
|
||||
|
||||
if dispatcher:
|
||||
if not getattr(dispatcher, "_fpatched", False):
|
||||
if self._is_telethon:
|
||||
if hasattr(self._inline_mgr, "register_bot_update_handler"):
|
||||
async def telethon_chosen_handler(event: Any) -> None:
|
||||
if isinstance(event, telethon.tl.types.UpdateBotInlineSend):
|
||||
if event.id.startswith("fh_"):
|
||||
class MockCallback:
|
||||
result_id = event.id
|
||||
inline_message_id = event.msg_id
|
||||
await self.click(MockCallback())
|
||||
|
||||
self._inline_mgr.register_bot_update_handler("fheta_chosen", "chosen_inline_result", telethon_chosen_handler)
|
||||
else:
|
||||
bot_client = self._inline_mgr._bot_client
|
||||
if not hasattr(bot_client, "_fpatched"):
|
||||
@bot_client.on(telethon.events.Raw)
|
||||
async def telethon_raw_handler(event: Any) -> None:
|
||||
if isinstance(event, telethon.tl.types.UpdateBotInlineSend):
|
||||
if event.id.startswith("fh_"):
|
||||
class MockCallback:
|
||||
result_id = event.id
|
||||
inline_message_id = event.msg_id
|
||||
await self.lookup("FHeta").click(MockCallback())
|
||||
bot_client._fpatched = True
|
||||
|
||||
elif hasattr(self._inline_mgr, "_dp"):
|
||||
dispatcher = self._inline_mgr._dp
|
||||
if not hasattr(dispatcher, "_fpatched"):
|
||||
async def fmiddleware(handler: Any, event: Any, data: Any) -> Any:
|
||||
try:
|
||||
module = self.lookup("FHeta")
|
||||
|
||||
if module and getattr(event, "result_id", "").startswith("fh_"):
|
||||
await module.click(event)
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
module = self.lookup("FHeta")
|
||||
if module and event.result_id.startswith("fh_"):
|
||||
await module.click(event)
|
||||
return None
|
||||
return await handler(event, data)
|
||||
|
||||
try:
|
||||
dispatcher.chosen_inline_result.middleware(fmiddleware)
|
||||
dispatcher._fpatched = True
|
||||
except Exception:
|
||||
pass
|
||||
dispatcher.chosen_inline_result.middleware(fmiddleware)
|
||||
dispatcher._fpatched = True
|
||||
|
||||
if self.token and not await self.api.fetch("validatetkn", user_id=str(self.identifier)):
|
||||
self.token = None
|
||||
self.api.token = None
|
||||
|
||||
if not self.token:
|
||||
try:
|
||||
async with client.conversation("@FHeta_robot") as conversation:
|
||||
await conversation.send_message('/token')
|
||||
self.token = (await conversation.get_response(timeout=5)).text.strip()
|
||||
database.set("FHeta", "token", self.token)
|
||||
self.api.token = self.token
|
||||
except Exception:
|
||||
pass
|
||||
async with client.conversation("@FHeta_robot") as conversation:
|
||||
await conversation.send_message('/token')
|
||||
self.token = (await conversation.get_response(timeout=5)).text.strip()
|
||||
database.set("FHeta", "token", self.token)
|
||||
self.api.token = self.token
|
||||
|
||||
asyncio.create_task(self.sync())
|
||||
|
||||
async def sync(self):
|
||||
ll = None
|
||||
while True:
|
||||
try:
|
||||
cl = self.strings["lang"]
|
||||
if cl != ll:
|
||||
await self.api.send("dataset", user_id=self.identifier, lang=cl)
|
||||
ll = cl
|
||||
except Exception:
|
||||
pass
|
||||
cl = self.strings["lang"]
|
||||
if cl != ll:
|
||||
await self.api.send("dataset", user_id=self.identifier, lang=cl)
|
||||
ll = cl
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def answer(self, callback: Union[CallbackQuery, ChosenInlineResult], text: Optional[str] = None, alert: bool = False) -> None:
|
||||
try:
|
||||
if text:
|
||||
await callback.answer(text, show_alert=alert)
|
||||
else:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
pass
|
||||
async def answer(self, callback: Any, text: Optional[str] = None, alert: bool = False) -> None:
|
||||
if not hasattr(callback, "answer"):
|
||||
return
|
||||
await callback.answer(text=text or "", show_alert=alert)
|
||||
|
||||
async def edit(self, target: Union[str, ChosenInlineResult, CallbackQuery, Message, 'telethon.types.Message'], text: str, buttons: List[List[Dict[str, Any]]], banner: Optional[str] = None) -> None:
|
||||
try:
|
||||
options = LinkPreviewOptions(url=banner, show_above_text=True, prefer_large_media=True) if banner else LinkPreviewOptions(is_disabled=True)
|
||||
markup = self.inline.generate_markup(buttons)
|
||||
async def edit(self, target: Any, text: str, buttons: List[List[Dict[str, Any]]], banner: Optional[str] = None) -> None:
|
||||
markup = self._inline_mgr.generate_markup(buttons)
|
||||
|
||||
if self._is_telethon:
|
||||
if banner and banner not in text:
|
||||
text = f'<a href="{banner}">‌</a>' + text
|
||||
|
||||
if not self.bot:
|
||||
return
|
||||
bot_client = self._inline_mgr._bot_client
|
||||
|
||||
inline_msg_id = target.inline_message_id if hasattr(target, "inline_message_id") else None
|
||||
|
||||
await bot_client.edit_message(
|
||||
inline_msg_id or target.chat_id,
|
||||
None if inline_msg_id else target.message_id,
|
||||
text,
|
||||
parse_mode="HTML",
|
||||
buttons=markup,
|
||||
link_preview=banner is not None,
|
||||
invert_media=True
|
||||
)
|
||||
|
||||
elif InlineQueryResultArticle is not Any:
|
||||
options = LinkPreviewOptions(url=banner, show_above_text=True, prefer_large_media=True) if banner else LinkPreviewOptions(is_disabled=True)
|
||||
arguments = {
|
||||
"text": text,
|
||||
"reply_markup": markup,
|
||||
@@ -756,64 +792,53 @@ class FHeta(loader.Module):
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
|
||||
inline = target if isinstance(target, str) else getattr(target, "inline_message_id", None)
|
||||
|
||||
if inline:
|
||||
arguments["inline_message_id"] = inline
|
||||
if hasattr(target, "inline_message_id") and target.inline_message_id:
|
||||
arguments["inline_message_id"] = target.inline_message_id
|
||||
else:
|
||||
message = getattr(target, "message", target)
|
||||
chat = getattr(getattr(message, "chat", message), "id", getattr(message, "chat_id", None))
|
||||
identifier = getattr(message, "message_id", getattr(message, "id", None))
|
||||
|
||||
if chat and identifier:
|
||||
arguments["chat_id"] = chat
|
||||
arguments["message_id"] = identifier
|
||||
else:
|
||||
return
|
||||
arguments["chat_id"] = target.message.chat.id
|
||||
arguments["message_id"] = target.message.message_id
|
||||
|
||||
await self.bot.edit_message_text(**arguments)
|
||||
except Exception:
|
||||
pass
|
||||
await self._inline_mgr.bot.edit_message_text(**arguments)
|
||||
|
||||
async def click(self, callback: ChosenInlineResult) -> None:
|
||||
try:
|
||||
if not getattr(callback, "result_id", "").startswith("fh_"):
|
||||
return
|
||||
|
||||
parts = callback.result_id.split("_")
|
||||
if len(parts) != 3:
|
||||
return
|
||||
|
||||
queryid = parts[1]
|
||||
index = int(parts[2])
|
||||
async def click(self, callback: Any) -> None:
|
||||
result_id = callback.result_id
|
||||
if not result_id.startswith("fh_"):
|
||||
return
|
||||
|
||||
cache = getattr(self.inline, "fheta_cache", {})
|
||||
saved = cache.get(queryid, {})
|
||||
query = saved.get("query", "")
|
||||
modules = saved.get("mods", [])
|
||||
parts = result_id.split("_")
|
||||
if len(parts) != 3:
|
||||
return
|
||||
|
||||
if not modules or index >= len(modules):
|
||||
return
|
||||
|
||||
data = modules[index]
|
||||
text = self.ui.format(data, query, index+1, len(modules), True)
|
||||
buttons = self.ui.buttons(data.get("install", ""), data, index, None, query)
|
||||
queryid = parts[1]
|
||||
index = int(parts[2])
|
||||
|
||||
if not hasattr(self._inline_mgr, "fheta_cache"):
|
||||
return
|
||||
|
||||
await self.edit(callback, text, buttons, data.get("banner"))
|
||||
except Exception:
|
||||
pass
|
||||
saved = self._inline_mgr.fheta_cache.get(queryid, {})
|
||||
query = saved.get("query", "")
|
||||
modules = saved.get("mods", [])
|
||||
|
||||
if not modules or index >= len(modules):
|
||||
return
|
||||
|
||||
data = modules[index]
|
||||
text = self.ui.format(data, query, index+1, len(modules), True)
|
||||
buttons = self.ui.buttons(data.get("install", ""), data, index, None, query)
|
||||
|
||||
await self.edit(callback, text, buttons, data.get("banner"))
|
||||
|
||||
async def show(self, callback: Union[CallbackQuery, ChosenInlineResult], index: int, modules: List[Dict[str, Any]], query: str) -> None:
|
||||
async def show(self, callback: Any, index: int, modules: List[Dict[str, Any]], query: str) -> None:
|
||||
await self.answer(callback)
|
||||
text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>"
|
||||
await self.edit(callback, text, self.ui.pagination(modules, query, 0, index))
|
||||
|
||||
async def page(self, callback: Union[CallbackQuery, ChosenInlineResult], current: int, modules: List[Dict[str, Any]], query: str, index: int) -> None:
|
||||
async def page(self, callback: Any, current: int, modules: List[Dict[str, Any]], query: str, index: int) -> None:
|
||||
await self.answer(callback)
|
||||
text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>"
|
||||
await self.edit(callback, text, self.ui.pagination(modules, query, current, index))
|
||||
|
||||
async def navigate(self, callback: Union[CallbackQuery, ChosenInlineResult], index: int, modules: List[Dict[str, Any]], query: str = "") -> None:
|
||||
async def navigate(self, callback: Any, index: int, modules: List[Dict[str, Any]], query: str = "") -> None:
|
||||
await self.answer(callback)
|
||||
if 0 <= index < len(modules):
|
||||
data = modules[index]
|
||||
@@ -821,7 +846,7 @@ class FHeta(loader.Module):
|
||||
buttons = self.ui.buttons(data.get('install', ''), data, index, modules, query)
|
||||
await self.edit(callback, text, buttons, data.get("banner"))
|
||||
|
||||
async def rate(self, callback: Union[CallbackQuery, ChosenInlineResult, Message, 'telethon.types.Message'], link: str, action: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
|
||||
async def rate(self, callback: Any, link: str, action: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
|
||||
response = await self.api.send(f"rate/{self.identifier}/{link}/{action}")
|
||||
|
||||
request = await self.api.send("get", payload=[unquote(link)])
|
||||
@@ -830,10 +855,7 @@ class FHeta(loader.Module):
|
||||
if modules and index < len(modules):
|
||||
modules[index].update(stats)
|
||||
|
||||
try:
|
||||
await callback.edit(reply_markup=self.ui.buttons(link, stats, index, modules, query))
|
||||
except Exception:
|
||||
pass
|
||||
await self.edit(callback, self.ui.format(modules[index], query, index + 1, len(modules)), self.ui.buttons(link, stats, index, modules, query), modules[index].get("banner"))
|
||||
|
||||
if response and response.get("status"):
|
||||
status = response.get("status")
|
||||
@@ -847,21 +869,18 @@ class FHeta(loader.Module):
|
||||
text = ""
|
||||
await self.answer(callback, text, True)
|
||||
|
||||
async def install(self, callback: Union[CallbackQuery, ChosenInlineResult], link: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
|
||||
async def install(self, callback: Any, link: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
|
||||
state, dependencies = await self.installer.execute(self.lookup("loader"), link)
|
||||
|
||||
try:
|
||||
if state == "success":
|
||||
await self.answer(callback, self.strings["success"], True)
|
||||
elif state == "dependency":
|
||||
formatted = f"({','.join(dependencies[:5])})" if dependencies else ""
|
||||
await self.answer(callback, self.strings["dependency"].format(deps=formatted), True)
|
||||
elif state == "overwrite":
|
||||
await self.answer(callback, self.strings["overwrite"], True)
|
||||
else:
|
||||
await self.answer(callback, self.strings["error"], True)
|
||||
except Exception:
|
||||
pass
|
||||
if state == "success":
|
||||
await self.answer(callback, self.strings["success"], True)
|
||||
elif state == "dependency":
|
||||
formatted = f"({','.join(dependencies[:5])})" if dependencies else ""
|
||||
await self.answer(callback, self.strings["dependency"].format(deps=formatted), True)
|
||||
elif state == "overwrite":
|
||||
await self.answer(callback, self.strings["overwrite"], True)
|
||||
else:
|
||||
await self.answer(callback, self.strings["error"], True)
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="(запрос) - поиск модулей.",
|
||||
@@ -880,7 +899,7 @@ class FHeta(loader.Module):
|
||||
return {
|
||||
"title": self.strings["prompt"],
|
||||
"description": self.strings["hint"],
|
||||
"message": f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=f'<code>@{self.inline.bot_username} ')}</code></b>",
|
||||
"message": f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=f'<code>@{self._inline_mgr.bot_username} ')}</code></b>",
|
||||
"thumb": "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/magnifying_glass.png"
|
||||
}
|
||||
|
||||
@@ -903,13 +922,13 @@ class FHeta(loader.Module):
|
||||
}
|
||||
|
||||
queryid = str(uuid.uuid4())[:8]
|
||||
if not hasattr(self.inline, "fheta_cache"):
|
||||
self.inline.fheta_cache = {}
|
||||
if not hasattr(self._inline_mgr, "fheta_cache"):
|
||||
self._inline_mgr.fheta_cache = {}
|
||||
|
||||
if len(self.inline.fheta_cache) >= 50:
|
||||
self.inline.fheta_cache.pop(next(iter(self.inline.fheta_cache)))
|
||||
if len(self._inline_mgr.fheta_cache) >= 50:
|
||||
self._inline_mgr.fheta_cache.pop(next(iter(self._inline_mgr.fheta_cache)))
|
||||
|
||||
self.inline.fheta_cache[queryid] = {"query": query, "mods": modules}
|
||||
self._inline_mgr.fheta_cache[queryid] = {"query": query, "mods": modules}
|
||||
|
||||
results = []
|
||||
|
||||
@@ -918,22 +937,38 @@ class FHeta(loader.Module):
|
||||
if isinstance(description, dict):
|
||||
description = description.get(self.strings["lang"]) or description.get("doc") or next(iter(description.values()), "")
|
||||
|
||||
markup = None
|
||||
try:
|
||||
markup = self.inline.generate_markup(self.ui.buttons(data.get("install", ""), data, index, None, query))
|
||||
except Exception:
|
||||
pass
|
||||
markup = self._inline_mgr.generate_markup(self.ui.buttons(data.get("install", ""), data, index, None, query))
|
||||
|
||||
results.append(InlineQueryResultArticle(
|
||||
id=f"fh_{queryid}_{index}",
|
||||
title=utils.escape_html(data.get("name", "")),
|
||||
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
|
||||
thumbnail_url=data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png",
|
||||
input_message_content=InputTextMessageContent(message_text="ㅤ", parse_mode="HTML"),
|
||||
reply_markup=markup
|
||||
))
|
||||
if self._is_telethon:
|
||||
thumb_url = data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png"
|
||||
thumb = self._inline_mgr._web_document(thumb_url)
|
||||
|
||||
results.append(
|
||||
await event.builder.article(
|
||||
id=f"fh_{queryid}_{index}",
|
||||
title=utils.escape_html(data.get("name", "")),
|
||||
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
|
||||
thumb=thumb,
|
||||
text="ㅤ",
|
||||
parse_mode="HTML",
|
||||
buttons=markup,
|
||||
link_preview=False
|
||||
)
|
||||
)
|
||||
elif InlineQueryResultArticle is not Any:
|
||||
results.append(InlineQueryResultArticle(
|
||||
id=f"fh_{queryid}_{index}",
|
||||
title=utils.escape_html(data.get("name", "")),
|
||||
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
|
||||
thumbnail_url=data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png",
|
||||
input_message_content=InputTextMessageContent(message_text="ㅤ", parse_mode="HTML"),
|
||||
reply_markup=markup
|
||||
))
|
||||
|
||||
await event.inline_query.answer(results, cache_time=0)
|
||||
if self._is_telethon:
|
||||
await event.answer(results, cache_time=0)
|
||||
elif InlineQueryResultArticle is not Any:
|
||||
await event.inline_query.answer(results, cache_time=0)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="(запрос) - поиск модулей.",
|
||||
@@ -963,7 +998,7 @@ class FHeta(loader.Module):
|
||||
|
||||
data = modules[0]
|
||||
buttons = self.ui.buttons(data.get("install", ""), data, 0, modules, query)
|
||||
form = await self.inline.form("ㅤ", message, reply_markup=buttons, silent=True)
|
||||
form = await self._inline_mgr.form("ㅤ", message, reply_markup=buttons, silent=True)
|
||||
text = self.ui.format(data, query, 1, len(modules))
|
||||
|
||||
await self.edit(form, text, buttons, data.get("banner"))
|
||||
@@ -975,20 +1010,17 @@ class FHeta(loader.Module):
|
||||
if not url.startswith("https://api.fixyres.com/module/"):
|
||||
return
|
||||
|
||||
try:
|
||||
state, dependencies = await self.installer.execute(self.lookup("loader"), url)
|
||||
state, dependencies = await self.installer.execute(self.lookup("loader"), url)
|
||||
|
||||
if state == "success":
|
||||
reply = await message.respond("✅")
|
||||
elif state == "dependency":
|
||||
reply = await message.respond(f"📋{','.join(dependencies[:5])}" if dependencies else "📋")
|
||||
elif state == "overwrite":
|
||||
reply = await message.respond("😨")
|
||||
else:
|
||||
reply = await message.respond("❌")
|
||||
|
||||
if state == "success":
|
||||
reply = await message.respond("✅")
|
||||
elif state == "dependency":
|
||||
reply = await message.respond(f"📋{','.join(dependencies[:5])}" if dependencies else "📋")
|
||||
elif state == "overwrite":
|
||||
reply = await message.respond("😨")
|
||||
else:
|
||||
reply = await message.respond("❌")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await reply.delete()
|
||||
await message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(1)
|
||||
await reply.delete()
|
||||
await message.delete()
|
||||
|
||||
@@ -112,13 +112,13 @@ class FSecurity(loader.Module):
|
||||
loader.ConfigValue(
|
||||
"strict_mode",
|
||||
False,
|
||||
lambda: self.strings("strict_mode_doc"),
|
||||
lambda: self.strings["strict_mode_doc"],
|
||||
validator=loader.validators.Boolean(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"nvidia_api_key",
|
||||
"",
|
||||
lambda: self.strings("nvidia_api_key_doc"),
|
||||
lambda: self.strings["nvidia_api_key_doc"],
|
||||
validator=loader.validators.Hidden(),
|
||||
)
|
||||
)
|
||||
@@ -386,10 +386,10 @@ class FSecurity(loader.Module):
|
||||
def format(self, state, reason="", link=""):
|
||||
link_part = f' (<code>{utils.escape_html(link)}</code>)' if link else ""
|
||||
if state == "unavailable":
|
||||
return f'<b>{self.strings("unavailable").format(link_part)}</b>\n<b>{self.strings("continue")}</b>'
|
||||
return f'<b>{self.strings["unavailable"].format(link_part)}</b>\n<b>{self.strings["continue"]}</b>'
|
||||
if state == "suspicious":
|
||||
return f'<b>{self.strings("suspicious").format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>\n<b>{self.strings("continue")}</b>'
|
||||
return f'<b>{self.strings("blocked").format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>'
|
||||
return f'<b>{self.strings["suspicious"].format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>\n<b>{self.strings["continue"]}</b>'
|
||||
return f'<b>{self.strings["blocked"].format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>'
|
||||
|
||||
def buttons(self, task):
|
||||
return [[
|
||||
|
||||
@@ -8,6 +8,7 @@ __version__ = (1, 0, 0)
|
||||
|
||||
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/SCD/banner.png
|
||||
# meta developer: @NFModules
|
||||
# meta fhsdesc: SoundCloud, Music, Music downloader, Downloader
|
||||
|
||||
# requires: curl_cffi
|
||||
|
||||
@@ -105,15 +106,15 @@ class SCD(loader.Module):
|
||||
'''(link) - download a song from SoundCloud.'''
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("no_args").format(prefix=self.get_prefix()))
|
||||
await utils.answer(message, self.strings["no_args"].format(prefix=self.get_prefix()))
|
||||
return
|
||||
|
||||
m = re.search(r"(https?://(?:[a-zA-Z0-9-]+\.)?soundcloud\.com/[^\s]+)", args)
|
||||
if not m:
|
||||
await utils.answer(message, self.strings("not_found"))
|
||||
await utils.answer(message, self.strings["not_found"])
|
||||
return
|
||||
|
||||
msg = await utils.answer(message, self.strings("downloading"))
|
||||
msg = await utils.answer(message, self.strings["downloading"])
|
||||
|
||||
try:
|
||||
async with requests.AsyncSession(impersonate="chrome120") as ses:
|
||||
@@ -194,4 +195,4 @@ class SCD(loader.Module):
|
||||
await msg.delete()
|
||||
|
||||
except:
|
||||
await utils.answer(msg, self.strings("not_found"))
|
||||
await utils.answer(msg, self.strings["not_found"])
|
||||
|
||||
@@ -308,7 +308,7 @@ class Akinator(loader.Module):
|
||||
loader.ConfigValue(
|
||||
"child_mode",
|
||||
False,
|
||||
lambda: self.strings("child_mode"),
|
||||
lambda: self.strings["child_mode"],
|
||||
validator=loader.validators.Boolean()
|
||||
)
|
||||
)
|
||||
@@ -339,17 +339,17 @@ class Akinator(loader.Module):
|
||||
async def akinator(self, message):
|
||||
'''- start the game.'''
|
||||
try:
|
||||
aki = AsyncAki(self.strings("lang"), self.config["child_mode"])
|
||||
aki = AsyncAki(self.strings["lang"], self.config["child_mode"])
|
||||
await aki.start()
|
||||
|
||||
self.games.setdefault(message.chat_id, {})[message.id] = aki
|
||||
|
||||
await self.inline.form(
|
||||
text=self.strings("text"),
|
||||
text=self.strings["text"],
|
||||
message=message,
|
||||
photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/banner.png",
|
||||
reply_markup={
|
||||
"text": self.strings("start"),
|
||||
"text": self.strings["start"],
|
||||
"callback": self._cb,
|
||||
"args": (message,)
|
||||
}
|
||||
@@ -369,12 +369,12 @@ class Akinator(loader.Module):
|
||||
question = aki.q
|
||||
|
||||
markup = [[
|
||||
{"text": self.strings("yes"), "callback": self._ans, "args": (0, message)},
|
||||
{"text": self.strings("no"), "callback": self._ans, "args": (1, message)},
|
||||
{"text": self.strings("idk"), "callback": self._ans, "args": (2, message)}
|
||||
{"text": self.strings["yes"], "callback": self._ans, "args": (0, message)},
|
||||
{"text": self.strings["no"], "callback": self._ans, "args": (1, message)},
|
||||
{"text": self.strings["idk"], "callback": self._ans, "args": (2, message)}
|
||||
],[
|
||||
{"text": self.strings("probably"), "callback": self._ans, "args": (3, message)},
|
||||
{"text": self.strings("probably_not"), "callback": self._ans, "args": (4, message)}
|
||||
{"text": self.strings["probably"], "callback": self._ans, "args": (3, message)},
|
||||
{"text": self.strings["probably_not"], "callback": self._ans, "args": (4, message)}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -393,13 +393,13 @@ class Akinator(loader.Module):
|
||||
desc = aki.desc
|
||||
|
||||
if desc:
|
||||
text = self.strings("this_is").format(name=name, description=desc)
|
||||
text = self.strings["this_is"].format(name=name, description=desc)
|
||||
else:
|
||||
text = self.strings("this_is_no_desc").format(name=name)
|
||||
text = self.strings["this_is_no_desc"].format(name=name)
|
||||
|
||||
markup = [[
|
||||
{"text": self.strings("yes"), "callback": self._fin, "args": (True, message, text, aki.photo)},
|
||||
{"text": self.strings("not_right"), "callback": self._rej, "args": (message,)}
|
||||
{"text": self.strings["yes"], "callback": self._fin, "args": (True, message, text, aki.photo)},
|
||||
{"text": self.strings["not_right"], "callback": self._rej, "args": (message,)}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -450,7 +450,7 @@ class Akinator(loader.Module):
|
||||
await call.edit(text, photo=photo, reply_markup=[])
|
||||
else:
|
||||
await call.edit(
|
||||
self.strings("failed"),
|
||||
self.strings["failed"],
|
||||
photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/idk.png",
|
||||
reply_markup=[]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,3 +2,4 @@ akinator
|
||||
FHeta
|
||||
BSR
|
||||
SCD
|
||||
LFSecurity
|
||||
|
||||
141
Limoka.py
@@ -1,5 +1,5 @@
|
||||
# meta developer: @limokanews
|
||||
# requires: whoosh cryptography
|
||||
# requires: whoosh cryptography filetype
|
||||
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
@@ -24,21 +24,20 @@ from telethon.errors.rpcerrorlist import WebpageMediaEmptyError
|
||||
from telethon import TelegramClient
|
||||
from telethon.errors.rpcerrorlist import YouBlockedUserError
|
||||
from telethon import functions
|
||||
import filetype
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
import ast
|
||||
|
||||
try:
|
||||
from aiogram.utils.exceptions import BadRequest
|
||||
except ImportError:
|
||||
from aiogram.exceptions import TelegramBadRequest as BadRequest
|
||||
|
||||
from aiogram.exceptions import TelegramBadRequest as BadRequest
|
||||
|
||||
from .. import utils, loader
|
||||
from ..types import BotInlineCall, InlineCall
|
||||
|
||||
logger = logging.getLogger("Limoka")
|
||||
__version__ = (1, 5, 1)
|
||||
__version__ = (1, 5, 6)
|
||||
|
||||
|
||||
def _parse_version_from_source(source: str):
|
||||
@@ -489,8 +488,12 @@ class Limoka(loader.Module):
|
||||
"New Limoka Version {version} already available. Please update for better performance, bug fixes, and new features.\n"
|
||||
"Press the button below to update the module."
|
||||
),
|
||||
"no_updates_available": "No updates available. You are using the latest version of Limoka.",
|
||||
"module_update_available": "Notification about module update has been sent, check @{bot}.",
|
||||
"no_updates_available": "<blockquote>❌ No updates available. You are using the latest version of Limoka.</blockquote>",
|
||||
"module_update_available": "<blockquote>🔔 Notification about module update has been sent, check @{bot}.</blockquote>",
|
||||
"index_update_started": "<blockquote>🔄 Limoka module index update has started. This may take a few minutes. Please wait...</blockquote>",
|
||||
"index_update_failed": "<blockquote>❌ Failed to update Limoka module index. Please try again later. If the error persists, report to developers</blockquote>",
|
||||
"index_update_success": "<blockquote>✅ Limoka module index updated successfully!</blockquote>",
|
||||
"update_check_started": "<blockquote>🔍 Checking for Limoka updates...</blockquote>",
|
||||
}
|
||||
strings_ru = {
|
||||
"name": "Limoka",
|
||||
@@ -612,8 +615,12 @@ class Limoka(loader.Module):
|
||||
"Новая версия Limoka {version} уже доступна. Пожалуйста, обновитесь для лучшей производительности, исправления багов и новых функций.\n"
|
||||
"Нажмите кнопку ниже, чтобы обновить модуль."
|
||||
),
|
||||
"no_updates_available": "Нет доступных обновлений. У вас установлена последняя версия Limoka.",
|
||||
"module_update_available": "Уведомление об обновлении модуля было отправлено, проверьте @{bot}.",
|
||||
"no_updates_available": "<blockquote>❌ Нет доступных обновлений. У вас установлена последняя версия Limoka.</blockquote>",
|
||||
"module_update_available": "<blockquote>🔔 Уведомление об обновлении модуля было отправлено, проверьте @{bot}.</blockquote>",
|
||||
"index_update_started": "<blockquote>🔄 Обновление индекса модулей Limoka началось. Это может занять несколько минут. Пожалуйста, подождите...</blockquote>",
|
||||
"index_update_failed": "<blockquote>❌ Не удалось обновить индекс модулей Limoka. Пожалуйста, попробуйте снова позже. Если ошибка сохраняется, сообщите разработчикам</blockquote>",
|
||||
"index_update_success": "<blockquote>✅ Индекс модулей Limoka успешно обновлен!</blockquote>",
|
||||
"update_check_started": "<blockquote>🔍 Проверка обновлений Limoka...</blockquote>",
|
||||
"_cls_doc": "Модули теперь в одном месте с простым и удобным поиском!",
|
||||
}
|
||||
|
||||
@@ -798,7 +805,7 @@ class Limoka(loader.Module):
|
||||
@loader.loop(interval=3600)
|
||||
async def _update_modules_loop(self):
|
||||
"""Periodically update modules list and rebuild index."""
|
||||
raw_modules = await self.api.fetch_json(self._base_url, "modules.json")
|
||||
await self.api.fetch_json(self._base_url, "modules.json")
|
||||
self.modules = self.repository.apply_newbie_filter(
|
||||
self.config.get("filter_newbies_modules", False)
|
||||
)
|
||||
@@ -839,21 +846,85 @@ class Limoka(loader.Module):
|
||||
logger.error(f"Skipping unsafe rmtree for {folder}")
|
||||
|
||||
async def _validate_url(self, url: str) -> Optional[str]:
|
||||
if not url or url in self._invalid_banners:
|
||||
logger.debug(f"_validate_url called with: {url}")
|
||||
if not url:
|
||||
logger.warning("_validate_url: URL is empty, returning None")
|
||||
return None
|
||||
if url in self._invalid_banners:
|
||||
logger.debug(f"_validate_url: URL already in invalid_banners: {url}, returning None")
|
||||
return None
|
||||
|
||||
# Headers to mimic a browser request
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
}
|
||||
|
||||
try:
|
||||
logger.debug(f"_validate_url: Starting validation for {url}")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.head(
|
||||
url, timeout=5, allow_redirects=True
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
ct = response.headers.get("Content-Type", "").lower()
|
||||
if not ct.startswith("image/"):
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
ct = None
|
||||
response_status = None
|
||||
|
||||
# Try HEAD first (more efficient)
|
||||
try:
|
||||
logger.debug(f"_validate_url: Attempting HEAD request for {url}")
|
||||
async with session.head(
|
||||
url, timeout=5, allow_redirects=True, headers=headers
|
||||
) as response:
|
||||
response_status = response.status
|
||||
logger.debug(f"_validate_url: HEAD request returned status {response.status} for {url}")
|
||||
if response.status == 200:
|
||||
ct = response.headers.get("Content-Type", "").lower()
|
||||
logger.debug(f"_validate_url: Content-Type from HEAD: '{ct}' for {url}")
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as head_error:
|
||||
logger.debug(f"_validate_url: HEAD failed ({type(head_error).__name__}), will try GET for {url}")
|
||||
|
||||
def _is_supported_media(content_type: str) -> bool:
|
||||
return content_type.startswith("image/") or content_type.startswith("video/mp4")
|
||||
|
||||
# If HEAD didn't work or returned non-200, try GET
|
||||
if ct is None:
|
||||
max_retries = 2
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
async with session.get(
|
||||
url, timeout=10, headers=headers, allow_redirects=True
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
ct = response.headers.get("Content-Type", "").lower()
|
||||
|
||||
# Try to get MIME if Content-Type is missing
|
||||
if not ct:
|
||||
try:
|
||||
data = await response.content.read(2048)
|
||||
mime = filetype.guess_mime(data, mime=True)
|
||||
if mime and _is_supported_media(mime):
|
||||
return url
|
||||
else:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
except Exception as mime_error:
|
||||
logger.error(f"_validate_url: Error reading content for MIME detection: {mime_error}")
|
||||
break # Success, exit retry loop
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as get_error:
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(1) # Wait before retry
|
||||
else:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
|
||||
# Check Content-Type from successful request
|
||||
if ct and _is_supported_media(ct):
|
||||
return url
|
||||
elif ct:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
else:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
if url:
|
||||
self._invalid_banners.add(url)
|
||||
@@ -918,8 +989,6 @@ class Limoka(loader.Module):
|
||||
def _build_navigation_markup(self, session: Dict[str, Any]) -> list:
|
||||
result = session["results"]
|
||||
index = session["current_index"]
|
||||
query = session["query"]
|
||||
filters = session["filters"]
|
||||
|
||||
page = index + 1
|
||||
markup = [
|
||||
@@ -973,8 +1042,6 @@ class Limoka(loader.Module):
|
||||
) -> list:
|
||||
result = session["results"]
|
||||
index = session["current_index"]
|
||||
query = session["query"]
|
||||
filters = session["filters"]
|
||||
|
||||
markup = []
|
||||
if len(body_pages) > 1:
|
||||
@@ -1352,6 +1419,7 @@ class Limoka(loader.Module):
|
||||
result = searcher.search()
|
||||
except Exception:
|
||||
await call.edit(self.strings["?"], reply_markup=[])
|
||||
|
||||
return
|
||||
if not result:
|
||||
markup = (
|
||||
@@ -1705,7 +1773,8 @@ class Limoka(loader.Module):
|
||||
)
|
||||
try:
|
||||
result = SearchIndex(args.lower(), self.ix).search()
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(f"Error occurred while searching: {e}")
|
||||
return await utils.answer(message, self.strings["?"])
|
||||
if not result:
|
||||
return await utils.answer(message, self.strings["404"].format(query=args))
|
||||
@@ -1724,9 +1793,23 @@ class Limoka(loader.Module):
|
||||
message, module_info, module_path, display_session, 0
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="Проверить наличие обновлений модуля")
|
||||
@loader.command(ru_doc="— Обновить индекс ")
|
||||
async def updateindex(self, message: Message):
|
||||
"""— Update search index"""
|
||||
await utils.answer(message, self.strings["index_update_started"])
|
||||
try:
|
||||
await self._update_index()
|
||||
except Exception as e:
|
||||
logger.exception(f"Error updating index: {e}")
|
||||
await utils.answer(message, self.strings["index_update_failed"])
|
||||
else:
|
||||
await utils.answer(message, self.strings["index_update_success"])
|
||||
|
||||
@loader.command(ru_doc="— Проверить наличие обновлений модуля")
|
||||
async def limokaupdatecmd(self, message: Message):
|
||||
"""Check for module updates"""
|
||||
"""— Check for module updates"""
|
||||
await utils.answer(message, self.strings["checking_for_updates"])
|
||||
|
||||
is_update_available = await self.check_for_module_update()
|
||||
if is_update_available:
|
||||
await utils.answer(message, self.strings["module_update_available"].format(bot=self._self_bot_username))
|
||||
|
||||
@@ -523,6 +523,10 @@ class Limoka(loader.Module):
|
||||
async def _validate_url(self, url: str) -> Optional[str]:
|
||||
if not url or url in self._invalid_banners:
|
||||
return None
|
||||
|
||||
def _is_supported_media(content_type: str) -> bool:
|
||||
return content_type.startswith("image/") or content_type.startswith("video/mp4")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.head(
|
||||
@@ -532,7 +536,7 @@ class Limoka(loader.Module):
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
ct = response.headers.get("Content-Type", "").lower()
|
||||
if not ct.startswith("image/"):
|
||||
if not _is_supported_media(ct):
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
return url
|
||||
@@ -760,7 +764,7 @@ class Limoka(loader.Module):
|
||||
),
|
||||
},
|
||||
{
|
||||
"text": f"{self.strings["body_page"]} {page_body + 1}/{len(body_pages)}",
|
||||
"text": f"{self.strings['body_page']} {page_body + 1}/{len(body_pages)}",
|
||||
"callback": self._inline_void,
|
||||
},
|
||||
{
|
||||
|
||||
157
Midga3/Heroku-modules/wordle.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# Midga3
|
||||
|
||||
# I AM NOT AFFICIATED WITH WORDLE
|
||||
|
||||
# meta developer: @midga3_modules
|
||||
|
||||
import requests
|
||||
import random
|
||||
import logging
|
||||
from .. import loader, utils
|
||||
from herokutl.tl.types import Message
|
||||
__verison__ = (0, 1, 1)
|
||||
logger = logging.getLogger(__name__)
|
||||
@loader.tds
|
||||
class wordle(loader.Module):
|
||||
"""Wordle!"""
|
||||
strings = {
|
||||
"name": "Wordle",
|
||||
"loading": "Loading...",
|
||||
"language": "Language of the wordle",
|
||||
"have_a_good_game": "Have a good game! Try to guess the 5 letter word in {}",
|
||||
"attempts_left": "WRONG! Attempts left: {}",
|
||||
"gg": "GG! YOU DIDN'T GUESS THE WORD {}",
|
||||
"win": "GG! YOU WON! THE WORD WAS {}",
|
||||
"already_playing": "ALREADY PLAYING! type .stopwordle to stop the current game",
|
||||
"length": "Must be 5 letters",
|
||||
"no_game": "No game is currently running",
|
||||
"ok": "Game stopped",
|
||||
"ad": "I tried to Guess word {}. Check out my result:\n{}",
|
||||
"real_word": "This word is not in the word list"
|
||||
}
|
||||
strings_ru ={
|
||||
"name": "Wordle",
|
||||
"loading": "Загрузка...",
|
||||
"language": "Язык вордла",
|
||||
"have_a_good_game": "Хорошей игры! Попытайтесь угадать слово из 5 букв на {}",
|
||||
"attempts_left": "НВЕВЕРНО! Осталось попыток: {}",
|
||||
"gg": "ГГ! ВЫ НЕ УГАДАЛИ СЛОВО {}",
|
||||
"win": "ГГ! ВЫ ВЫИГРАЛИ! СЛОВО БЫЛО {}",
|
||||
"already_playing": "УЖЕ ИГРАЕТЕ! напишите .stopwordle чтобы остановить текущую игру",
|
||||
"length": "Должно содержать 5 букв",
|
||||
"no_game": "Сейчас нет активной игры",
|
||||
"ok": "Игра остановлена",
|
||||
"ad": "Я попытался угадать слово {}. Чекайте мой результат:\n{}",
|
||||
"real_word": "Такого слова нет в списке слов"
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"language",
|
||||
"en",
|
||||
self.strings["language"],
|
||||
validator=loader.validators.Choice(["en", "ru"])
|
||||
),
|
||||
)
|
||||
async def handler(self, call, data):
|
||||
guess = data.upper()
|
||||
word = self._db.get("wordle", "word", "")
|
||||
attempts = self._db.get("wordle", "attempts", 0)
|
||||
buttons = self._db.get("wordle", "buttons", [])
|
||||
markup = buttons + [[{"text":"Введите слово","input":self.strings("length"),"handler": self.handler}]]
|
||||
|
||||
if len(guess) != 5:
|
||||
await call.edit(self.strings("length"), reply_markup=markup)
|
||||
return
|
||||
|
||||
if guess not in self._db.get("wordle", "words", []):
|
||||
await call.edit(self.strings("real_word"), reply_markup=markup)
|
||||
return
|
||||
buttons2 = []
|
||||
for i in range(5):
|
||||
if guess[i] == word[i]:
|
||||
buttons2.append({"text": guess[i], "data": "custom/data", "style": "success"})
|
||||
elif guess[i] in word:
|
||||
buttons2.append({"text": guess[i], "data": "custom/data", "style": "primary"})
|
||||
else:
|
||||
buttons2.append({"text": guess[i], "data": "custom/data", "style": "danger"})
|
||||
|
||||
buttons.append(buttons2)
|
||||
|
||||
if guess == word:
|
||||
self._db.set("wordle", "buttons", buttons)
|
||||
self._db.set("wordle", "now_playing", False)
|
||||
result = ""
|
||||
for btn in buttons:
|
||||
for b in btn:
|
||||
if b["style"] == "success":
|
||||
result += "🟩"
|
||||
elif b["style"] == "primary":
|
||||
result += "🟨"
|
||||
else:
|
||||
result += "⬛"
|
||||
result += "\n"
|
||||
buttons.append([{"text":"Поделится резултатом","copy":self.strings("ad").format(word, result)}])
|
||||
await call.edit(f"{self.strings('win').format(word)}", reply_markup=buttons)
|
||||
return
|
||||
|
||||
self._db.set("wordle", "buttons", buttons)
|
||||
attempts -= 1
|
||||
self._db.set("wordle", "attempts", attempts)
|
||||
|
||||
if attempts == 0:
|
||||
result = ""
|
||||
for btn in buttons:
|
||||
for b in btn:
|
||||
if b["style"] == "success":
|
||||
result += "🟩"
|
||||
elif b["style"] == "primary":
|
||||
result += "🟨"
|
||||
else:
|
||||
result += "⬛"
|
||||
result += "\n"
|
||||
buttons.append([{"text":"Поделится резултатом","copy":self.strings("ad").format(word, result)}])
|
||||
await call.edit(f"{self.strings('gg').format(word)}", reply_markup=buttons)
|
||||
self._db.set("wordle", "now_playing", False)
|
||||
else:
|
||||
await call.edit(f"{self.strings('attempts_left').format(attempts)}", reply_markup=markup)
|
||||
|
||||
@loader.command()
|
||||
async def wordle(self, message: Message):
|
||||
"""Play wordle!"""
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
if self._db.get("wordle", "now_playing", False):
|
||||
await utils.answer(message, self.strings("already_playing"))
|
||||
return
|
||||
args = utils.get_args(message)
|
||||
if args and args[0].lower():
|
||||
if args[0].lower() == "--no-sec":
|
||||
self._db.set("wordle", "nosec", True)
|
||||
return
|
||||
else:
|
||||
self._db.set("wordle", "nosec", False)
|
||||
try:
|
||||
response = requests.get(f"https://raw.githubusercontent.com/mimimishka449/Worlde/refs/heads/main/words_{self.config['language']}.txt")
|
||||
if response.status_code == 200:
|
||||
words = response.text.splitlines()
|
||||
word = random.choice(words).upper()
|
||||
self._db.set("wordle", "now_playing", True)
|
||||
self._db.set("wordle", "attempts", 6)
|
||||
self._db.set("wordle", "word", word)
|
||||
self._db.set("wordle", "words", words)
|
||||
self._db.set("wordle", "buttons", [])
|
||||
await self.inline.form(self.strings("have_a_good_game").format("английском" if self.config['language'] == "en" else "русском"), message, reply_markup=[[{"text":"Введите слово","input":self.strings("length"),"handler": self.handler}]])
|
||||
else:
|
||||
await utils.answer(message, "Error fetching wordle data.")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error: {e}")
|
||||
await utils.answer(message, "An error occurred while fetching wordle data.")
|
||||
@loader.command()
|
||||
async def stopwordle(self, message: Message):
|
||||
"""Stop the wordle game."""
|
||||
if not self._db.get("wordle", "now_playing", False):
|
||||
await utils.answer(message, self.strings("no_game"))
|
||||
return
|
||||
self._db.set("wordle", "now_playing", False)
|
||||
await utils.answer(message, self.strings("ok"))
|
||||
@@ -1,150 +1,122 @@
|
||||
#meta developer: @matubuntu
|
||||
import requests, bs4
|
||||
from datetime import datetime
|
||||
from .. import loader, utils
|
||||
import lxml
|
||||
# meta developer: @matubuntu
|
||||
|
||||
# requires: lxml requests bs4
|
||||
import time
|
||||
from datetime import datetime
|
||||
import aiohttp
|
||||
from .. import loader, utils
|
||||
|
||||
_FLAGS = {
|
||||
"AUD": "🇦🇺",
|
||||
"AZN": "🇦🇿",
|
||||
"GBP": "🇬🇧",
|
||||
"AMD": "🇦🇲",
|
||||
"BYN": "🇧🇾",
|
||||
"BGN": "🇧🇬",
|
||||
"BRL": "🇧🇷",
|
||||
"HUF": "🇭🇺",
|
||||
"VND": "🇻🇳",
|
||||
"HKD": "🇭🇰",
|
||||
"GEL": "🇬🇪",
|
||||
"DKK": "🇩🇰",
|
||||
"AED": "🇦🇪",
|
||||
"USD": "🇺🇸",
|
||||
"EUR": "🇪🇺",
|
||||
"EGP": "🇪🇬",
|
||||
"INR": "🇮🇳",
|
||||
"IDR": "🇮🇩",
|
||||
"KZT": "🇰🇿",
|
||||
"CAD": "🇨🇦",
|
||||
"QAR": "🇶🇦",
|
||||
"KGS": "🇰🇬",
|
||||
"CNY": "🇨🇳",
|
||||
"MDL": "🇲🇩",
|
||||
"NZD": "🇳🇿",
|
||||
"NOK": "🇳🇴",
|
||||
"PLN": "🇵🇱",
|
||||
"RON": "🇷🇴",
|
||||
"SGD": "🇸🇬",
|
||||
"TJS": "🇹🇯",
|
||||
"THB": "🇹🇭",
|
||||
"TRY": "🇹🇷",
|
||||
"TMT": "🇹🇲",
|
||||
"UZS": "🇺🇿",
|
||||
"UAH": "🇺🇦",
|
||||
"CZK": "🇨🇿",
|
||||
"SEK": "🇸🇪",
|
||||
"CHF": "🇨🇭",
|
||||
"RSD": "🇷🇸",
|
||||
"ZAR": "🇿🇦",
|
||||
"KRW": "🇰🇷",
|
||||
"JPY": "🇯🇵",
|
||||
"AUD": "🇦🇺", "AZN": "🇦🇿", "GBP": "🇬🇧", "AMD": "🇦🇲",
|
||||
"BYN": "🇧🇾", "BGN": "🇧🇬", "BRL": "🇧🇷", "HUF": "🇭🇺",
|
||||
"VND": "🇻🇳", "HKD": "🇭🇰", "GEL": "🇬🇪", "DKK": "🇩🇰",
|
||||
"AED": "🇦🇪", "USD": "🇺🇸", "EUR": "🇪🇺", "EGP": "🇪🇬",
|
||||
"INR": "🇮🇳", "IDR": "🇮🇩", "KZT": "🇰🇿", "CAD": "🇨🇦",
|
||||
"QAR": "🇶🇦", "KGS": "🇰🇬", "CNY": "🇨🇳", "MDL": "🇲🇩",
|
||||
"NZD": "🇳🇿", "NOK": "🇳🇴", "PLN": "🇵🇱", "RON": "🇷🇴",
|
||||
"SGD": "🇸🇬", "TJS": "🇹🇯", "THB": "🇹🇭", "TRY": "🇹🇷",
|
||||
"TMT": "🇹🇲", "UZS": "🇺🇿", "UAH": "🇺🇦", "CZK": "🇨🇿",
|
||||
"SEK": "🇸🇪", "CHF": "🇨🇭", "RSD": "🇷🇸", "ZAR": "🇿🇦",
|
||||
"KRW": "🇰🇷", "JPY": "🇯🇵",
|
||||
}
|
||||
|
||||
_CRYPTO_EMOJIS = {
|
||||
"BTC": "<emoji document_id=5289519973285257969>💰</emoji>",
|
||||
"ETH": "<emoji document_id=5287735049301550386>💰</emoji>",
|
||||
"SOL": "<emoji document_id=5251712673258697260>💰</emoji>",
|
||||
"TON": "<emoji document_id=5289648693455119919>💰</emoji>",
|
||||
"USDT": "<emoji document_id=5289904548951911168>💰</emoji>",
|
||||
"XRP": "<emoji document_id=5373312921214401986>💰</emoji>",
|
||||
"USDC": "<emoji document_id=5372958453268497353>💰</emoji>",
|
||||
"ADA": "<emoji document_id=5373076801092338046>💰</emoji>",
|
||||
"DOGE": "<emoji document_id=5375192042420842380>💰</emoji>",
|
||||
"TRX": "<emoji document_id=5375187081733616165>💰</emoji>",
|
||||
"AVAX": "<emoji document_id=5375311275007947936>💰</emoji>",
|
||||
"LTC": "<emoji document_id=5373035462032113888>💰</emoji>",
|
||||
"BCH": "<emoji document_id=5375596920397903962>💰</emoji>",
|
||||
"ATOM": "<emoji document_id=5375468745688889977>💰</emoji>",
|
||||
"XLM": "<emoji document_id=5372823290647690288>💰</emoji>",
|
||||
"SHIB": "<emoji document_id=5375231036428924778>💰</emoji>",
|
||||
"UNI": "<emoji document_id=5372953110329180525>💰</emoji>",
|
||||
"XMR": "<emoji document_id=5375507073977038661>💰</emoji>",
|
||||
"LINK": "<emoji document_id=5375149651093633217>💰</emoji>",
|
||||
"ETC": "<emoji document_id=5375543306321146693>💰</emoji>",
|
||||
"SUI": "<emoji document_id=5391002164929772708>💰</emoji>",
|
||||
"NEAR": "<emoji document_id=5391181990915487346>💰</emoji>",
|
||||
"VET": "<emoji document_id=5391091302681033446>💰</emoji>",
|
||||
"FIL": "<emoji document_id=5373117173784919811>💰</emoji>",
|
||||
"XTZ": "<emoji document_id=5390985478981829698>💰</emoji>",
|
||||
"ALGO": "<emoji document_id=5391337713544738420>💰</emoji>",
|
||||
"BTC": "<emoji document_id=5289519973285257969>💰</emoji>",
|
||||
"ETH": "<emoji document_id=5287735049301550386>💰</emoji>",
|
||||
"SOL": "<emoji document_id=5251712673258697260>💰</emoji>",
|
||||
"TON": "<emoji document_id=5289648693455119919>💰</emoji>",
|
||||
"USDT": "<emoji document_id=5289904548951911168>💰</emoji>",
|
||||
"XRP": "<emoji document_id=5373312921214401986>💰</emoji>",
|
||||
"USDC": "<emoji document_id=5372958453268497353>💰</emoji>",
|
||||
"ADA": "<emoji document_id=5373076801092338046>💰</emoji>",
|
||||
"DOGE": "<emoji document_id=5375192042420842380>💰</emoji>",
|
||||
"TRX": "<emoji document_id=5375187081733616165>💰</emoji>",
|
||||
"AVAX": "<emoji document_id=5375311275007947936>💰</emoji>",
|
||||
"LTC": "<emoji document_id=5373035462032113888>💰</emoji>",
|
||||
"BCH": "<emoji document_id=5375596920397903962>💰</emoji>",
|
||||
"ATOM": "<emoji document_id=5375468745688889977>💰</emoji>",
|
||||
"XLM": "<emoji document_id=5372823290647690288>💰</emoji>",
|
||||
"SHIB": "<emoji document_id=5375231036428924778>💰</emoji>",
|
||||
"UNI": "<emoji document_id=5372953110329180525>💰</emoji>",
|
||||
"XMR": "<emoji document_id=5375507073977038661>💰</emoji>",
|
||||
"LINK": "<emoji document_id=5375149651093633217>💰</emoji>",
|
||||
"ETC": "<emoji document_id=5375543306321146693>💰</emoji>",
|
||||
"SUI": "<emoji document_id=5391002164929772708>💰</emoji>",
|
||||
"NEAR": "<emoji document_id=5391181990915487346>💰</emoji>",
|
||||
"VET": "<emoji document_id=5391091302681033446>💰</emoji>",
|
||||
"FIL": "<emoji document_id=5373117173784919811>💰</emoji>",
|
||||
"XTZ": "<emoji document_id=5390985478981829698>💰</emoji>",
|
||||
"ALGO": "<emoji document_id=5391337713544738420>💰</emoji>",
|
||||
"THETA": "<emoji document_id=5391256014676833736>💰</emoji>",
|
||||
"FTM": "<emoji document_id=5393179395521263785>💰</emoji>",
|
||||
"XDAI": "<emoji document_id=5391325992578988886>💰</emoji>",
|
||||
"RUNE": "<emoji document_id=5391347570494684983>💰</emoji>",
|
||||
"DOT": "<emoji document_id=5375224568208177973>💰</emoji>",
|
||||
"FTM": "<emoji document_id=5393179395521263785>💰</emoji>",
|
||||
"XDAI": "<emoji document_id=5391325992578988886>💰</emoji>",
|
||||
"RUNE": "<emoji document_id=5391347570494684983>💰</emoji>",
|
||||
"DOT": "<emoji document_id=5375224568208177973>💰</emoji>",
|
||||
}
|
||||
|
||||
_CRYPTO_LIST = {
|
||||
"BTC": "Bitcoin",
|
||||
"ETH": "Ethereum",
|
||||
"XMR": "Monero",
|
||||
"LTC": "Litecoin",
|
||||
"XRP": "XRP",
|
||||
"ADA": "Cardano",
|
||||
"DOGE": "Dogecoin",
|
||||
"SOL": "Solana",
|
||||
"DOT": "Polkadot",
|
||||
"USDT": "Tether",
|
||||
"TON": "Toncoin",
|
||||
"USDC": "USD Coin",
|
||||
"TRX": "TRON",
|
||||
"AVAX": "Avalanche",
|
||||
"BCH": "Bitcoin Cash",
|
||||
"ATOM": "Cosmos",
|
||||
"XLM": "Stellar",
|
||||
"SHIB": "Shiba Inu",
|
||||
"UNI": "Uniswap",
|
||||
"LINK": "Chainlink",
|
||||
"ETC": "Ethereum Classic",
|
||||
"SUI": "Sui",
|
||||
"NEAR": "NEAR Protocol",
|
||||
"VET": "VeChain",
|
||||
"FIL": "Filecoin",
|
||||
"XTZ": "Tezos",
|
||||
"ALGO": "Algorand",
|
||||
"THETA": "Theta Network",
|
||||
"FTM": "Fantom",
|
||||
"XDAI": "xDai",
|
||||
_CRYPTO_NAMES = {
|
||||
"BTC": "Bitcoin", "ETH": "Ethereum", "XMR": "Monero",
|
||||
"LTC": "Litecoin", "XRP": "XRP", "ADA": "Cardano",
|
||||
"DOGE": "Dogecoin", "SOL": "Solana", "DOT": "Polkadot",
|
||||
"USDT": "Tether", "TON": "Toncoin", "USDC": "USD Coin",
|
||||
"TRX": "TRON", "AVAX": "Avalanche", "BCH": "Bitcoin Cash",
|
||||
"ATOM": "Cosmos", "XLM": "Stellar", "SHIB": "Shiba Inu",
|
||||
"UNI": "Uniswap", "LINK": "Chainlink", "ETC": "Ethereum Classic",
|
||||
"SUI": "Sui", "NEAR": "NEAR Protocol", "VET": "VeChain",
|
||||
"FIL": "Filecoin", "XTZ": "Tezos", "ALGO": "Algorand",
|
||||
"THETA": "Theta Network", "FTM": "Fantom", "XDAI": "xDai",
|
||||
"RUNE": "THORChain",
|
||||
}
|
||||
|
||||
def _fmt_num(v, d=3):
|
||||
p = f"{v:,.{d}f}".replace(",", " ").split(".")
|
||||
i = p[0]
|
||||
d = p[1].rstrip("0") if len(p) > 1 else ""
|
||||
return f"{i},{d}" if d else i
|
||||
_CBR_URL = "https://www.cbr.ru/scripts/XML_daily.asp"
|
||||
_CRYPTO_URL = "https://api.coinlore.net/api/tickers/?limit=100"
|
||||
|
||||
CACHE_TTL = 300 # seconds
|
||||
|
||||
|
||||
def _fmt_num(value: float, decimals: int = 3) -> str:
|
||||
if decimals == 0:
|
||||
return f"{int(value):,}".replace(",", " ")
|
||||
rounded = round(value, decimals)
|
||||
int_part = int(rounded)
|
||||
dec_part = str(rounded - int_part)[2:2 + decimals].rstrip("0")
|
||||
int_str = f"{int_part:,}".replace(",", " ")
|
||||
return f"{int_str},{dec_part}" if dec_part else int_str
|
||||
|
||||
|
||||
def _parse_cbr_xml(xml_bytes: bytes) -> tuple[str | None, dict]:
|
||||
"""Parse CBR XML without bs4/lxml — pure stdlib ElementTree."""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
root = ET.fromstring(xml_bytes)
|
||||
date_str = root.attrib.get("Date", "")
|
||||
try:
|
||||
date = datetime.strptime(date_str, "%d.%m.%Y").strftime("%d.%m.%Y")
|
||||
except ValueError:
|
||||
date = date_str
|
||||
|
||||
rates: dict[str, dict] = {}
|
||||
for valute in root.findall("Valute"):
|
||||
code = valute.findtext("CharCode", "").strip()
|
||||
if not code or code == "XDR":
|
||||
continue
|
||||
try:
|
||||
nominal = float(valute.findtext("Nominal", "1").replace(",", "."))
|
||||
value = float(valute.findtext("Value", "0").replace(",", "."))
|
||||
except ValueError:
|
||||
continue
|
||||
rates[code] = {
|
||||
"name": valute.findtext("Name", code).strip(),
|
||||
"nominal": nominal,
|
||||
"rub": value / nominal,
|
||||
}
|
||||
return date, rates
|
||||
|
||||
|
||||
@loader.tds
|
||||
class FinanceMod(loader.Module):
|
||||
strings = {
|
||||
"name": "FinanceMod",
|
||||
"valute_description": "<кол-во> <код> - курс валюты\n<кол-во> - список",
|
||||
"valute_no_args": (
|
||||
"💵 <b>Курс валюты с сайта </b><a href='https://www.cbr.ru/'>ЦБ(РФ)</a>\n"
|
||||
"<b>Актуально на</b> <i>{}</i>\n\n<blockquote expandable>{}</blockquote>"
|
||||
),
|
||||
"valute_specific": (
|
||||
"💵 <b>Курс валюты с сайта </b><a href='https://www.cbr.ru/'>ЦБ(РФ)</a>\n"
|
||||
"<b>Актуально на</b> <i>{}</i>\n\n{}"
|
||||
),
|
||||
"valute_not_found": "🚫 Валюта {} не найдена",
|
||||
"crypto_description": "<кол-во> <код> - курс крипты\n<кол-во> - список",
|
||||
"crypto_no_args": "💎 <b>Курсы криптовалют</b>\n\n<blockquote expandable>{}</blockquote>",
|
||||
"crypto_specific": "💎 <b>Курс криптовалюты</b>\n\n{}",
|
||||
"crypto_not_found": "🚫 Криптовалюта {} не найдена",
|
||||
"error": "🚫 Ошибка получения данных",
|
||||
}
|
||||
"""Курсы валют (ЦБ РФ) и криптовалют (CoinLore)"""
|
||||
|
||||
strings = {"name": "FinanceMod"}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
@@ -152,149 +124,194 @@ class FinanceMod(loader.Module):
|
||||
"crypto_currency",
|
||||
"USD",
|
||||
lambda: "Валюта для отображения крипты (USD, RUB, EUR)",
|
||||
validator=loader.validators.Choice(["USD", "RUB", "EUR"])
|
||||
validator=loader.validators.Choice(["USD", "RUB", "EUR"]),
|
||||
)
|
||||
)
|
||||
# Simple in-process cache
|
||||
self._cbr_cache: tuple[float, str, dict] | None = None # (ts, date, rates)
|
||||
self._crypto_cache: tuple[float, list] | None = None # (ts, data)
|
||||
|
||||
async def _get_curr_data(self):
|
||||
# ──────────────────────────── HTTP helpers ────────────────────────────
|
||||
|
||||
async def _fetch(self, url: str, *, as_json: bool = False):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json() if as_json else await resp.read()
|
||||
|
||||
# ──────────────────────────── CBR data ────────────────────────────────
|
||||
|
||||
async def _cbr_data(self) -> tuple[str | None, dict]:
|
||||
now = time.monotonic()
|
||||
if self._cbr_cache and now - self._cbr_cache[0] < CACHE_TTL:
|
||||
return self._cbr_cache[1], self._cbr_cache[2]
|
||||
try:
|
||||
r = requests.get("https://www.cbr.ru/scripts/XML_daily.asp")
|
||||
s = bs4.BeautifulSoup(r.content, 'xml')
|
||||
d = datetime.strptime(s.ValCurs['Date'], "%d.%m.%Y").strftime("%d.%m.%Y")
|
||||
return d, s.find_all('Valute')
|
||||
except:
|
||||
return None, None
|
||||
raw = await self._fetch(_CBR_URL)
|
||||
date, rates = _parse_cbr_xml(raw)
|
||||
self._cbr_cache = (now, date, rates)
|
||||
return date, rates
|
||||
except Exception:
|
||||
if self._cbr_cache:
|
||||
return self._cbr_cache[1], self._cbr_cache[2]
|
||||
return None, {}
|
||||
|
||||
async def _get_rates(self):
|
||||
# ──────────────────────────── Crypto data ─────────────────────────────
|
||||
|
||||
async def _crypto_data(self) -> list:
|
||||
now = time.monotonic()
|
||||
if self._crypto_cache and now - self._crypto_cache[0] < CACHE_TTL:
|
||||
return self._crypto_cache[1]
|
||||
try:
|
||||
r = requests.get("https://www.cbr.ru/scripts/XML_daily.asp")
|
||||
s = bs4.BeautifulSoup(r.content, 'xml')
|
||||
rt = {'USD': None, 'EUR': None}
|
||||
for v in s.find_all('Valute'):
|
||||
if v.CharCode.text in ['USD', 'EUR']:
|
||||
n = float(v.Nominal.text.replace(',', '.'))
|
||||
vl = float(v.Value.text.replace(',', '.'))
|
||||
rt[v.CharCode.text] = vl / n
|
||||
if rt['USD'] and rt['EUR']:
|
||||
rt['EUR_USD'] = rt['USD'] / rt['EUR']
|
||||
else:
|
||||
rt['EUR_USD'] = None
|
||||
return rt
|
||||
except:
|
||||
return None
|
||||
js = await self._fetch(_CRYPTO_URL, as_json=True)
|
||||
data = js.get("data", [])
|
||||
self._crypto_cache = (now, data)
|
||||
return data
|
||||
except Exception:
|
||||
return self._crypto_cache[1] if self._crypto_cache else []
|
||||
|
||||
async def _fmt_curr(self, v, a=1):
|
||||
if v.CharCode.text == "XDR":
|
||||
return None
|
||||
c = v.CharCode.text
|
||||
n = v.Name.text
|
||||
v = float(v.Value.text.replace(',', '.')) / float(v.Nominal.text.replace(',', '.'))
|
||||
t = v * a
|
||||
ts = _fmt_num(t, 3)
|
||||
return f"{_FLAGS.get(c, '🏳')} [{a}] {n} ({c}) - {ts} руб."
|
||||
# ──────────────────────────── Formatters ──────────────────────────────
|
||||
|
||||
async def _get_crypto(self):
|
||||
def _fmt_valute(self, code: str, info: dict, amount: float = 1.0) -> str:
|
||||
total = info["rub"] * amount
|
||||
flag = _FLAGS.get(code, "🏳")
|
||||
return f"{flag} [{_fmt_num(amount, 0)}] {info['name']} ({code}) — {_fmt_num(total, 3)} ₽"
|
||||
|
||||
def _fmt_crypto(self, coin: dict, rates: dict, amount: float = 1.0) -> str:
|
||||
symbol = coin["symbol"].upper()
|
||||
try:
|
||||
return requests.get("https://api.coinlore.net/api/tickers/").json().get('data', [])
|
||||
except:
|
||||
return None
|
||||
price_usd = float(coin["price_usd"])
|
||||
except (KeyError, ValueError, TypeError):
|
||||
return ""
|
||||
|
||||
async def _fmt_crypto(self, c, a=1):
|
||||
r = await self._get_rates()
|
||||
if not r:
|
||||
return "🚫 Ошибка получения курсов валют"
|
||||
cr = self.config["crypto_currency"]
|
||||
try:
|
||||
p = float(c['price_usd'])
|
||||
except:
|
||||
return "🚫 Ошибка данных криптовалюты"
|
||||
if cr == "RUB":
|
||||
if not r['USD']:
|
||||
return "🚫 Курс USD не найден"
|
||||
p *= r['USD']
|
||||
elif cr == "EUR":
|
||||
if not r['EUR_USD']:
|
||||
return "🚫 Курс EUR/USD не рассчитан"
|
||||
p *= r['EUR_USD']
|
||||
t = p * a
|
||||
ts = _fmt_num(t)
|
||||
s = c['symbol'].upper()
|
||||
e = _CRYPTO_EMOJIS.get(s, "💠")
|
||||
n = _CRYPTO_LIST.get(s, c['name'])
|
||||
cs = {"USD": "$", "RUB": "₽", "EUR": "€"}.get(cr, "$")
|
||||
return f"{e} [{a}] {n} ({s}) - {ts}{cs}"
|
||||
currency = self.config["crypto_currency"]
|
||||
if currency == "RUB":
|
||||
usd_rate = rates.get("USD", {}).get("rub")
|
||||
if not usd_rate:
|
||||
return ""
|
||||
price = price_usd * usd_rate
|
||||
sign = "₽"
|
||||
elif currency == "EUR":
|
||||
usd_rate = rates.get("USD", {}).get("rub")
|
||||
eur_rate = rates.get("EUR", {}).get("rub")
|
||||
if not usd_rate or not eur_rate:
|
||||
return ""
|
||||
price = price_usd * (usd_rate / eur_rate)
|
||||
sign = "€"
|
||||
else:
|
||||
price = price_usd
|
||||
sign = "$"
|
||||
|
||||
@loader.command()
|
||||
async def valutecmd(self, m):
|
||||
"""[count] [usd, eur, ...]"""
|
||||
a = utils.get_args(m)
|
||||
d, v = await self._get_curr_data()
|
||||
if not d or not v:
|
||||
return await utils.answer(m, self.strings["error"])
|
||||
if len(a) == 0:
|
||||
l = []
|
||||
for x in v:
|
||||
if (n := await self._fmt_curr(x)):
|
||||
l.append(n)
|
||||
await utils.answer(m, self.strings["valute_no_args"].format(d, "\n".join(l)))
|
||||
elif len(a) == 1:
|
||||
total = price * amount
|
||||
emoji = _CRYPTO_EMOJIS.get(symbol, "💠")
|
||||
name = _CRYPTO_NAMES.get(symbol, coin.get("name", symbol))
|
||||
return f"{emoji} [{_fmt_num(amount, 0)}] {name} ({symbol}) — {_fmt_num(total, 3)}{sign}"
|
||||
|
||||
# ──────────────────────────── Commands ────────────────────────────────
|
||||
|
||||
@loader.command(ru_doc="[кол-во] [код] — курс валюты по ЦБ РФ")
|
||||
async def valutecmd(self, message):
|
||||
"""[amount] [code] — exchange rates from CBR"""
|
||||
args = utils.get_args(message)
|
||||
date, rates = await self._cbr_data()
|
||||
|
||||
if not rates:
|
||||
return await utils.answer(message, "🚫 Не удалось получить данные ЦБ РФ")
|
||||
|
||||
header = (
|
||||
f"💵 <b>Курс валюты</b> · <a href='https://www.cbr.ru/'>ЦБ РФ</a>\n"
|
||||
f"<b>Актуально на</b> <i>{date}</i>\n\n"
|
||||
)
|
||||
|
||||
# .valute — список всех, кол-во = 1
|
||||
if not args:
|
||||
lines = [self._fmt_valute(c, i) for c, i in rates.items()]
|
||||
return await utils.answer(
|
||||
message,
|
||||
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
|
||||
)
|
||||
|
||||
# Первый аргумент: число или код валюты?
|
||||
amount = 1.0
|
||||
code = None
|
||||
arg0 = args[0].upper()
|
||||
|
||||
if len(args) >= 2:
|
||||
# .valute 100 USD
|
||||
try:
|
||||
am = float(a[0])
|
||||
l = []
|
||||
for x in v:
|
||||
if (n := await self._fmt_curr(x, am)):
|
||||
l.append(n)
|
||||
await utils.answer(m, self.strings["valute_no_args"].format(d, "\n".join(l)))
|
||||
except:
|
||||
await utils.answer(m, "🚫 Некорректное число")
|
||||
elif len(a) == 2:
|
||||
amount = float(args[0].replace(",", "."))
|
||||
except ValueError:
|
||||
return await utils.answer(message, "🚫 Некорректное число")
|
||||
code = args[1].upper()
|
||||
else:
|
||||
# .valute USD или .valute 100
|
||||
try:
|
||||
am = float(a[0])
|
||||
c = a[1].upper()
|
||||
for x in v:
|
||||
if x.CharCode.text == c:
|
||||
if (n := await self._fmt_curr(x, am)):
|
||||
return await utils.answer(m, self.strings["valute_specific"].format(d, n))
|
||||
await utils.answer(m, self.strings["valute_not_found"].format(c))
|
||||
except:
|
||||
await utils.answer(m, "🚫 Некорректное число")
|
||||
amount = float(arg0.replace(",", "."))
|
||||
# число без кода — список с умножением
|
||||
except ValueError:
|
||||
code = arg0
|
||||
|
||||
@loader.command()
|
||||
async def cryptocmd(self, m):
|
||||
"""[count] [ton, btc, ...]"""
|
||||
a = utils.get_args(m)
|
||||
c = await self._get_crypto()
|
||||
if not c:
|
||||
return await utils.answer(m, self.strings["error"])
|
||||
try:
|
||||
if len(a) == 0:
|
||||
f = [x for x in c if x['symbol'].upper() in _CRYPTO_LIST]
|
||||
l = []
|
||||
for x in f:
|
||||
if (n := await self._fmt_crypto(x)):
|
||||
l.append(n)
|
||||
await utils.answer(m, self.strings["crypto_no_args"].format("\n".join(l)))
|
||||
elif len(a) == 1:
|
||||
am = float(a[0])
|
||||
f = [x for x in c if x['symbol'].upper() in _CRYPTO_LIST]
|
||||
l = []
|
||||
for x in f:
|
||||
if (n := await self._fmt_crypto(x, am)):
|
||||
l.append(n)
|
||||
await utils.answer(m, self.strings["crypto_no_args"].format("\n".join(l)))
|
||||
elif len(a) == 2:
|
||||
am = float(a[0])
|
||||
t = a[1].upper()
|
||||
f = False
|
||||
for x in c:
|
||||
if x['symbol'].upper() == t:
|
||||
if (n := await self._fmt_crypto(x, am)):
|
||||
f = True
|
||||
await utils.answer(m, self.strings["crypto_specific"].format(n))
|
||||
break
|
||||
if not f:
|
||||
await utils.answer(m, self.strings["crypto_not_found"].format(t))
|
||||
except ValueError:
|
||||
await utils.answer(m, "🚫 Некорректное число")
|
||||
except Exception as e:
|
||||
await utils.answer(m, f"🚫 Ошибка: {str(e)}")
|
||||
if code:
|
||||
if code not in rates:
|
||||
return await utils.answer(message, f"🚫 Валюта <b>{code}</b> не найдена")
|
||||
line = self._fmt_valute(code, rates[code], amount)
|
||||
return await utils.answer(message, header + line)
|
||||
|
||||
# список с кол-вом
|
||||
lines = [self._fmt_valute(c, i, amount) for c, i in rates.items()]
|
||||
await utils.answer(
|
||||
message,
|
||||
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="[кол-во] [код] — курс крипты")
|
||||
async def cryptocmd(self, message):
|
||||
"""[amount] [symbol] — crypto rates from CoinLore"""
|
||||
args = utils.get_args(message)
|
||||
coins = await self._crypto_data()
|
||||
_, rates = await self._cbr_data()
|
||||
|
||||
if not coins:
|
||||
return await utils.answer(message, "🚫 Не удалось получить данные крипты")
|
||||
|
||||
header = f"💎 <b>Курсы криптовалют</b> · <i>{self.config['crypto_currency']}</i>\n\n"
|
||||
|
||||
amount = 1.0
|
||||
symbol = None
|
||||
|
||||
if not args:
|
||||
pass # список, amount=1
|
||||
elif len(args) == 1:
|
||||
try:
|
||||
amount = float(args[0].replace(",", "."))
|
||||
except ValueError:
|
||||
symbol = args[0].upper()
|
||||
else:
|
||||
try:
|
||||
amount = float(args[0].replace(",", "."))
|
||||
except ValueError:
|
||||
return await utils.answer(message, "🚫 Некорректное число")
|
||||
symbol = args[1].upper()
|
||||
|
||||
if symbol:
|
||||
coin = next((c for c in coins if c["symbol"].upper() == symbol), None)
|
||||
if not coin:
|
||||
return await utils.answer(message, f"🚫 Крипта <b>{symbol}</b> не найдена")
|
||||
line = self._fmt_crypto(coin, rates, amount)
|
||||
if not line:
|
||||
return await utils.answer(message, "🚫 Ошибка форматирования")
|
||||
return await utils.answer(message, header + line)
|
||||
|
||||
# список только известных монет
|
||||
known = {c["symbol"].upper(): c for c in coins if c["symbol"].upper() in _CRYPTO_NAMES}
|
||||
# сортируем по порядку _CRYPTO_NAMES
|
||||
lines = []
|
||||
for sym in _CRYPTO_NAMES:
|
||||
if sym in known:
|
||||
line = self._fmt_crypto(known[sym], rates, amount)
|
||||
if line:
|
||||
lines.append(line)
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
|
||||
)
|
||||
|
||||
5
SenkoGuardian/SenModules/full.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
ChatCopy.py
|
||||
Gemini.py
|
||||
GiftFinder.py
|
||||
MaillingChatGT99.py
|
||||
NekoEditorMod.py
|
||||
97
SunnexGB/Heroku-Modules/ASCII.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# requires: Pillow numpy
|
||||
# meta developer: @SunnexGB
|
||||
# meta banner: https://i.pinimg.com/control1/1200x/24/8d/40/248d40b6afa5bd3c3764556b50635691.jpg
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
import io
|
||||
import logging
|
||||
from herokutl.types import Message
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class ASCII(loader.Module):
|
||||
"""Convert images to braille ASCII"""
|
||||
|
||||
strings = {
|
||||
"name": "ASCII",
|
||||
"no_lib": "<tg-emoji emoji-id=5447385112612208213>🚫</tg-emoji> | <b>Library not loaded</b>",
|
||||
"no_image": "<tg-emoji emoji-id=5447381715293074599>⚠️</tg-emoji> | <b>Reply to image</b>",
|
||||
"processing": "<tg-emoji emoji-id=5445373981290952548>®️</tg-emoji> | <b>Processing...</b>",
|
||||
"empty": "<tg-emoji emoji-id=5287613115180006030>🤬</tg-emoji> | <b>Empty result</b>",
|
||||
"result": "<pre>{art}</pre>",
|
||||
"Failed_to_load_library": "Failed to load library",
|
||||
"Conversion_error": "Conversion error",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Конвертирует картинку в braille ASCII",
|
||||
"no_lib": "<tg-emoji emoji-id=5447385112612208213>🚫</tg-emoji> | <b>Библиотека не была загружена</b>",
|
||||
"no_image": "<tg-emoji emoji-id=5447381715293074599>⚠️</tg-emoji> | <b>Ответьте на картинку</b>",
|
||||
"processing": "<tg-emoji emoji-id=5445373981290952548>®️</tg-emoji> | <b>Обработка...</b>",
|
||||
"empty": "<tg-emoji emoji-id=5287613115180006030>🤬</tg-emoji> | <b>Пустой результат</b>",
|
||||
"result": "<pre>{art}</pre>",
|
||||
"Failed_to_load_library": "Не удалось загрузить библиотеку",
|
||||
"Conversion_error": "Ошибка конвертации",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue("width", 50),
|
||||
loader.ConfigValue("threshold", 0.65),
|
||||
loader.ConfigValue("contrast", 2.0),
|
||||
loader.ConfigValue("chars", 464),
|
||||
loader.ConfigValue("invert", False),
|
||||
)
|
||||
self.lib = None
|
||||
|
||||
async def client_ready(self):
|
||||
try:
|
||||
self.lib = await self.import_lib("https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/refs/heads/main/Assets/ASCII/ascii-lib.py", suspend_on_error=True)
|
||||
except Exception:
|
||||
logger.exception(self.strings["Failed_to_load_library"])
|
||||
self.lib = None
|
||||
|
||||
@loader.command(ru_doc="- Отрисовать ASCII-ART (аргумент -f, отправляет файлом)")
|
||||
async def dotcmd(self, message: Message):
|
||||
"""- Draw ASCII-ART (argument -f, sends as a file)"""
|
||||
if not self.lib:
|
||||
return await utils.answer(message, self.strings["no_lib"])
|
||||
args = utils.get_args_raw(message)
|
||||
force_file = "-f" in args.lower()
|
||||
reply = await message.get_reply_message() or message
|
||||
if not reply or not (
|
||||
reply.photo
|
||||
or (
|
||||
reply.document
|
||||
and str(getattr(reply.document, "mime_type", "")).startswith("image/")
|
||||
)
|
||||
):
|
||||
return await utils.answer(message, self.strings["no_image"])
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
image_bytes = await reply.download_media(bytes)
|
||||
art = self.lib.convert(
|
||||
image_bytes,
|
||||
width=self.config["width"],
|
||||
threshold=self.config["threshold"],
|
||||
contrast_boost=self.config["contrast"],
|
||||
invert=self.config["invert"],
|
||||
target_chars=self.config["chars"],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(self.strings["Conversion_error"])
|
||||
return await utils.answer(msg, f"<pre>{e}</pre>")
|
||||
if not art or not art.strip():
|
||||
return await utils.answer(msg, self.strings["empty"])
|
||||
formatted_art = self.strings("result").format(art=art)
|
||||
if force_file or len(formatted_art) > 4096:
|
||||
file = io.BytesIO(art.encode("utf-8"))
|
||||
file.name = "ascii.txt"
|
||||
await message.client.send_file(message.peer_id, file)
|
||||
await msg.delete()
|
||||
else:
|
||||
await utils.answer(msg, formatted_art)
|
||||
110
SunnexGB/Heroku-Modules/Assets/ASCII/ascii-lib.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# requires: Pillow numpy
|
||||
# Дикие оправдания по поводу именно этого ассета а точнее кода в нем,честно я не знаю что сказать была попытка переписать JS на Py и как бы особых проблем не было,
|
||||
# до момента пост-обработки на помощь я позвал Claude и он не решил мою проблему от слова совсем,так как в целом я своего рода призираю пилоу,а модуль мне хотелось
|
||||
# написать я примерно вайб-кодил около 50 минут и я уверен из за этого будет возможно много проблем,в итоге благодаря немного копанию в коде,я нашел проблему и уже
|
||||
# начал ее решать,НО я опять же вообще не понимал как сделать то что мне нужно,в интернете были сюрсы но будто бы тот или иной мне не подходили? Я не знаю почему я
|
||||
# дропнул эту идею. Потом я стал искать в JS-е что там вообще можно сделать,в итоге я там импортировал модель какую то блядскую не нужную и опять впустую время
|
||||
# потратил,думал что тут определено есть решение и снова пошел к ии,вывод опятьь 0 помощи,я не знаю почему я так вцепился лишь в 1 идею.Как бы я мог упростить все,
|
||||
# даже наверное просто попросив какую то флагмен ии написать модуль и переписать его,но я уже на тот момент по моему мнению сделал много и не хотел ни каким образом
|
||||
# оставлять это,поэтому через время я нашел сайты которые в целом давали возможность настраивать фильтр,была переделана логика(в целом ее переделал на 60 процентов
|
||||
# клод,я просто убирал мусор который он испражнял.И вот дальше точно бред я убил более дня на решение проблем которые были решены мной,но результат мне не нравился
|
||||
# И ОПЯТЬ я пошел просить помощи у гугла,потом понял что возможно даже будет легко(по факту легко,но я ленивый) пока искал,мне перехотелось и я уже потом пытался
|
||||
# сделать режимы в модуле,что оказалось ужасом ведь они работали,но при возможности гармонично вписать их в код были конфликты И Я В ОЧЕРЕДНОЙ РАЗ ПОШЕЛ К ИИ,спойлер
|
||||
# он не смог написать лучше чем я,в итоге я отбросил эту идею и думаю в целом никак больше не апдейтать модуль по крупному.
|
||||
# Да это были оправдания,но зато какие!
|
||||
import io
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFilter, ImageEnhance, ImageOps
|
||||
from .. import loader
|
||||
|
||||
BASE = 0x2800
|
||||
INVERT_MAP = {chr(BASE + c): chr(BASE + (c ^ 0xFF)) for c in range(256)}
|
||||
|
||||
|
||||
class AsciiLib(loader.Library):
|
||||
developer = "@SunnexGB"
|
||||
|
||||
def resize(self, img):
|
||||
if img.width > 768:
|
||||
img = img.resize((768, int(img.height * 768 / img.width)), Image.LANCZOS)
|
||||
w = img.width - img.width % 4
|
||||
h = img.height - img.height % 4
|
||||
if w != img.width or h != img.height:
|
||||
img = img.resize((w, h), Image.LANCZOS)
|
||||
return img
|
||||
|
||||
def mode(self, img, threshold, contrast):
|
||||
gray = img.convert("L")
|
||||
edges = ImageOps.invert(gray.filter(ImageFilter.FIND_EDGES))
|
||||
contrast_img = ImageEnhance.Contrast(img).enhance(contrast).convert("L")
|
||||
e = np.array(edges, dtype=np.float32) / 255.0
|
||||
c = np.array(contrast_img, dtype=np.float32) / 255.0
|
||||
blended = Image.fromarray((e * c * 255).astype(np.uint8), "L")
|
||||
t = int(threshold * 255)
|
||||
processed = blended.point(lambda p: 255 if p > t else 0, "L")
|
||||
return processed, t
|
||||
|
||||
def braille(self, img, threshold, width):
|
||||
cw = width * 2
|
||||
o = -(-round(cw * img.height / img.width) // 4)
|
||||
ch = 4 * o
|
||||
px = np.array(img.resize((cw, ch), Image.LANCZOS).convert("L"))
|
||||
order = [(0,0),(1,0),(2,0),(0,1),(1,1),(2,1),(3,0),(3,1)]
|
||||
rows = []
|
||||
for rs in range(0, ch, 4):
|
||||
line = []
|
||||
for cs in range(0, cw, 2):
|
||||
grays = [
|
||||
int(px[rs+dy, cs+dx]) if (rs+dy < ch and cs+dx < cw) else 255
|
||||
for dy, dx in order
|
||||
]
|
||||
bits = list(reversed([1 if g < threshold else 0 for g in grays]))
|
||||
code = int("".join(str(b) for b in bits), 2)
|
||||
line.append(chr(BASE + code))
|
||||
rows.append("".join(line))
|
||||
return rows
|
||||
|
||||
def trim(self, lines):
|
||||
blank = "\u2800"
|
||||
while lines and all(c == blank for c in lines[0]):
|
||||
lines = lines[1:]
|
||||
while lines and all(c == blank for c in lines[-1]):
|
||||
lines = lines[:-1]
|
||||
if not lines:
|
||||
return lines
|
||||
left = min(next((i for i,c in enumerate(r) if c!=blank), len(r)) for r in lines)
|
||||
right = min(next((i for i,c in enumerate(reversed(r)) if c!=blank), len(r)) for r in lines)
|
||||
return [r[left: len(r)-right if right else len(r)] for r in lines]
|
||||
|
||||
def invert(self, lines):
|
||||
return ["".join(INVERT_MAP.get(c,c) for c in l) for l in lines]
|
||||
|
||||
def fit(self, img, threshold, chars, width):
|
||||
lo, hi = 5, 200
|
||||
best = ""
|
||||
for _ in range(14):
|
||||
mid = (lo + hi)//2
|
||||
lines = self.trim(self.braille(img, threshold, mid))
|
||||
art = "\n".join(lines)
|
||||
if len(art) <= chars:
|
||||
best = art
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid - 1
|
||||
return best
|
||||
|
||||
def convert(self, data, width=50, threshold=0.65, contrast_boost=2.0, invert=False, target_chars=0):
|
||||
buf = io.BytesIO(data)
|
||||
img = Image.open(buf)
|
||||
img.load()
|
||||
buf.close()
|
||||
img = img.convert("RGB")
|
||||
img = self.resize(img)
|
||||
processed, t = self.mode(img, threshold, contrast_boost)
|
||||
if target_chars > 0:
|
||||
art = self.fit(processed, t, target_chars, width)
|
||||
else:
|
||||
art = "\n".join(self.trim(self.braille(processed, t, width)))
|
||||
if invert and art:
|
||||
art = "\n".join(self.invert(art.split("\n")))
|
||||
return art
|
||||
@@ -0,0 +1,832 @@
|
||||
{
|
||||
"prologue": [
|
||||
{
|
||||
"type": "label",
|
||||
"name": "prologue"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_1",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_1",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_1.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Мне опять снился сон."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "<i>Этот</i> сон..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Каждую ночь одно и то же."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но наутро, как обычно, всё забудется."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может быть, оно и к лучшему..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Останутся только туманные воспоминания о приоткрытых, словно приглашающих куда-то воротах, рядом с которыми в камне застыли два пионера."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А ещё странная девочка...{w} которая постоянно спрашивает:"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "anim_prolog1_off",
|
||||
"action": "load_asset",
|
||||
"location": "anim/anim_prolog1_off",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/anim_prolog1_off.gif?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "dialogue",
|
||||
"char_id": "dreamgirl",
|
||||
"character": "...",
|
||||
"text": "Ты пойдёшь со мной?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Пойду?.."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но куда?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И зачем?.."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Да и где я вообще нахожусь?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Конечно, случись всё на самом деле, наяву, стоило бы непременно испугаться."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Как же иначе!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но это – всего лишь сон.{w} Тот самый, который я вижу каждую ночь."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А ведь всё это неспроста!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Необязательно знать <i>где</i> и <i>почему</i>, чтобы понять – что-то происходит."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Нечто, отчаянно требующее моего внимания."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь всё окружающее меня здесь – реально!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Реально настолько, насколько реальны вещи в моей квартире; я бы мог открыть ворота, услышать скрип петель, смахнуть рукой осыпающуюся ржавчину, потянуть носом свежий прохладный воздух и поёжиться от холода."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Мог бы, но для этого надо сдвинуться с места, сделать шаг, пошевелить рукой..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А ведь это сон – я понимаю, но что дальше, что изменит моё <i>понимание</i>?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь здесь – словно по ту сторону потрескавшегося экрана старого телевизора, который из последних сил борется с помехами и силится показать зрителям всё, не упустив ни малейшей детали."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но вот картинка теряет чёткость...{w} Наверное, скоро просыпаться."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может быть, спросить у неё что-то?{w} У девочки."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Как же её зовут..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Например про звёзды..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Хотя почему про звёзды?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Можно же спросить про ворота!{w} Да, про ворота!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Вот она удивится."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Или лучше про букву <i>ё</i>."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Хорошая была буква..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Как будто её больше нет!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И какое отношение буквы, ворота и звёзды имеют к этому месту?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь если мне каждую ночь снится <i>этот</i> сон, который потом всё равно забудется, надо искать разгадку здесь и сейчас!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А вот, если присмотреться, можно увидеть Магелланово Облако..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Словно попал в южное полушарие!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Во сне всегда больше волнуют мелочи: неестественный цвет травы, невозможная кривизна прямых или своё перекошенное отражение – а реальная опасность, готовая оборвать всё здесь и сейчас, кажется пустяком."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Естественно, ведь <i>здесь</i> нельзя умереть."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я точно знаю – я делал это сотни раз."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Но если нельзя умереть, нет смысла жить?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Надо будет спросить у девочки: она местная – должна знать!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Да, именно!{w} Спросить, например, про сову."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Больно уж птица странная..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А впрочем, неважно..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "..."
|
||||
},
|
||||
{
|
||||
"type": "dialogue",
|
||||
"char_id": "dreamgirl",
|
||||
"character": "...",
|
||||
"text": "Ты пойдёшь со мной?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И каждый раз надо отвечать."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Иначе никак, иначе сон не закончится, а я – не проснусь."
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"id": "prologue_choice_1"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Каждый раз так сложно решить, что же ответить."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Где я, что я здесь делаю, кто она такая?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И почему от ответа на этот вопрос зависит так много в моей жизни?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Или не зависит?.."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь это просто сон..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Просто сон..."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "black",
|
||||
"action": "load_asset",
|
||||
"location": "bg/black",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/black.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "1_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_1",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_1.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "2_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_2",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_2.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "3_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_3",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_3.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "4_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_4",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_4.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "5_prologue",
|
||||
"action": "load_asset",
|
||||
"location": "cg/p_kb_5",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/cg/p_kb_5.png?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Экран монитора смотрел на меня словно живой."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Иногда мне правда казалось, что он обладает сознанием, своими мыслями и желаниями, стремлениями; умеет чувствовать, любить и страдать."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Словно в наших отношениях инструмент не он – неодушевлённый кусок пластика и текстолита, – а я."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Наверное, в этом есть доля правды, ведь компьютер на 90% обеспечивает моё общение с внешним миром."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Анонимные имиджборды, иногда какие-то чаты, редко – аська или джаббер, ещё реже – форумы."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А людей, сидящих по ту сторону сетевого кабеля, попросту не существует!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Все они – всего лишь плод его больной фантазии, ошибка в программном коде или баг ядра, зажившего собственной жизнью."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_15",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_15",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_15.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_3",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_3",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_3.png?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_4",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_4",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_4.png?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Если посмотреть со стороны на моё существование, то такие мысли покажутся не столь уж бредовыми, а какой-нибудь психолог наверняка поставит мне кучу заумных диагнозов и, возможно, выпишет направление в жёлтый дом."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_5",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_5",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_5.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_14",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_14",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_14.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_11",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_11",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_11.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Маленькая квартирка без следов какого бы то ни было ремонта или даже подобия порядка, и вечно одинаковый вид из окна на серый, день и ночь куда-то бегущий мегаполис, – вот условия моей жизни."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_2",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_2",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Конечно, всё начиналось не так..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я родился, пошёл в школу, закончил её – всё как у людей."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Поступил в институт, где кое-как промучился полтора курса."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Работал на паре-тройке разных работ.{w} Иногда даже и неплохо, иногда даже получая за это достойные деньги."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Однако всё это казалось чужим, словно списанным с биографии другого человека."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я не ощущал полноту жизни – она словно зациклилась и продолжала идти по кругу.{w} Как в фильме «День сурка»."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Только у меня не было выбора, как именно провести этот день, и каждый раз всё повторялось по одной и той же схеме.{w} Схеме пустоты, уныния и отчаяния."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Последние несколько лет я просто целыми днями сидел за компьютером."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Иногда подворачивались какие-то халтурки, иногда помогали родители."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "В общем, на жизнь хватало."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Это и немудрено, ведь потребности у меня небольшие."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "На улицу я практически не выхожу, а всё моё общение с людьми сводится к интернет-переписке с <i>анонимами</i>, у которых нет ни реального имени, ни пола, ни возраста."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Короче говоря, достаточно типичная жизнь достаточно типичного асоциального человека своего времени.{w} Этакий Обломов XXI века."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может быть, маститый писатель напишет обо мне роман, который станет классикой современной литературы.{w} Или напишу я сам…"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Впрочем нет, что себя обманывать – уже не раз пытался, но меня не хватало даже на короткий рассказ."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Изучал я и множество других вещей."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Рисовать – не дано от природы.{w} Программирование – надоело.{w} Иностранные языки – долго и скучно…"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Любил я разве что читать, но даже при этом никогда бы не назвал себя эрудированным человеком."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Возможно, я был асом в просмотре аниме и гроссмейстером неумелых шуточек в интернете."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Плати мне за это деньги, я бы обрадовался (да и заработал неплохо), но вряд ли так просто можно заполнить пустоту в душе."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "semen_room_window",
|
||||
"action": "load_asset",
|
||||
"location": "bg/semen_room_window",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/semen_room_window.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Сегодня очередной типичный день моей типичной жизни типичного неудачника."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "И именно сегодня мне нужно ехать на встречу институтских товарищей."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "По правде говоря, совершенно не хотелось."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Да и какой смысл, если вместе с ними я отучился всего ничего?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Однако меня всё же уговорил друг, бывший одногруппник, один из немногих, с кем я поддерживал контакт не только в интернете."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_1",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_1",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_1.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_2",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_2",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_2.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_3",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_3",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_3.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_4",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_4",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_4.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_5",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_5",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_5.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_6",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_6",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_6.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_8",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_8",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_8.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_7",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_7",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_7.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "bus_stop",
|
||||
"action": "load_asset",
|
||||
"location": "bg/bus_stop",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/bus_stop.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Вечер. Мороз.{w} Остановка и ожидание автобуса."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я никогда не любил зиму.{w} Впрочем, и жаркое лето – тоже не моя стихия."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Просто не вижу смысла выделять какое-то одно время года – не столь важно, какая погода на улице, если ты целыми днями сидишь дома."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Автобус сегодня задерживался так сильно, что я уже был готов плюнуть на всё и потратить последнюю пару сотен на такси (совсем не ехать мне почему-то в голову не пришло)."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "В мозгу, как всегда, роились миллионы мыслей, из которых совершенно невозможно выудить хотя бы одну стоящую."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Такую, которую можно закончить, привести в порядок, облечь в форму идеи и претворить в жизнь."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может быть, заняться бизнесом?{w} Но откуда я возьму деньги?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Или пойти опять работать в офис?{w} Нет уж!"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Может, стоит попробовать фриланс?{w} Да что я умею, и кому я нужен…"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "prolog_2",
|
||||
"action": "load_asset",
|
||||
"location": "anim/prolog_2",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/prolog_2.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Вдруг мне вспомнилось детство…{w} Или скорее юношество – 15-17 лет."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Почему именно это время?{w} Не знаю."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Наверное, потому что тогда всё было проще."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Было проще принимать такие сложные сейчас и такие простые тогда решения."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Проснувшись с утра, я чётко знал, как пройдёт мой день, а выходных ждал с нетерпением – смогу отдохнуть, заняться любимыми делами: компьютер, футбол, встречи с друзьями."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "А потом, когда наступит новая неделя, вновь примусь за учёбу."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь раньше не возникало этих мучительных вопросов «зачем», «кому это надо», «что изменится, если я это сделаю» или «что не изменится»."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Простой поток жизни, такой привычный для любого нормального человека и такой чуждый для меня теперешнего."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Время беззаботного детства…{w} Тогда же я и встретил свою первую любовь."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Стёрлись из памяти её внешность, характер."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Как строчка из профиля в социальной сети осталось лишь имя, да те чувства, которые захлёстывали меня, когда я был с ней.{w} Теплота, нежность, желание заботиться, защитить…"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Жаль, что это продолжалось так недолго."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Сейчас я уже с трудом могу себе представить что-то подобное."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Наверное, и хочется познакомиться с девушкой, только не знаю, как начать диалог, о чём вообще с ней говорить, чем её заинтересовать."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Да и подходящих девушек я давно не встречал.{w} Хотя где мне их встретить…"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_9",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_9",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_9.jpg"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Звук работающего двигателя вернул меня к реальности."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Подъехал автобус."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "«Какой-то он не такой» – мелькнула мысль."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Впрочем, какая разница – по этому маршруту ходит только 410-ый."
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_10",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_10",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_10.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_11",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_11",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_11.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "anim",
|
||||
"name": "intro_13",
|
||||
"action": "load_asset",
|
||||
"location": "anim/intro_13",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/anim/intro_13.jpg?raw=true",
|
||||
"duration": null
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "intro_xx",
|
||||
"action": "load_asset",
|
||||
"location": "bg/intro_xx",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/intro_xx.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Огни пролетают мимо, их холодный свет словно зажигает внутри давно погасшие чувства."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Или не зажигает, а просто пробуждает…"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь «они» уже давно живут во мне, то затихая, то просыпаясь вновь."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Какая-то очень известная мелодия играла в радиоприёмнике у водителя.{w} Но я её не слушал."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я смотрел в запотевшее окно автобуса на проезжающие мимо машины."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Ведь люди куда-то спешат, ведь им что-то нужно, и, погружённые в свои дела, они не задумываются о вопросах, мучающих меня."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Наверное, у них тоже есть свои серьёзные проблемы, а может, им живётся куда легче."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Знать наверняка нельзя, так как все люди разные.{w} Или не разные?"
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Бывает, поступки человека легко предсказуемы, но, пытаясь заглянуть к нему в душу, видишь лишь непроглядную тьму."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "..."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Автобус приближался к центру, и мои мысли прервал яркий свет огней большого города."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Сотни рекламных вывесок, тысячи машин, миллионы людей."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Я смотрел на это светопреставление, и мне почему-то безумно захотелось спать."
|
||||
},
|
||||
{
|
||||
"type": "narration",
|
||||
"text": "Глаза закрылись всего на полсекунды и…"
|
||||
},
|
||||
{
|
||||
"type": "scene",
|
||||
"kind": "bg",
|
||||
"name": "black",
|
||||
"action": "load_asset",
|
||||
"location": "bg/black",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/images/1920/bg/int_bus_black.jpg?raw=true"
|
||||
},
|
||||
{
|
||||
"type": "opening",
|
||||
"kind": "opening",
|
||||
"name": "opening",
|
||||
"action": "load_asset",
|
||||
"location": "opening/opening",
|
||||
"raw_url": "https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Everlasting_Summer/opening/opening.mp4?raw=true"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"prologue_choice_1": {
|
||||
"question": "Иначе никак, иначе сон не закончится, а я – не проснусь. — что выбрать?",
|
||||
"chapter": "prologue",
|
||||
"options": {
|
||||
"Да, я пойду с тобой": {
|
||||
"effects": {}
|
||||
},
|
||||
"Нет, я останусь здесь": {
|
||||
"effects": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"endings": {
|
||||
"labels": [
|
||||
"main_good_ending",
|
||||
"main_bad_ending",
|
||||
"sl_good_ending",
|
||||
"sl_bad_ending",
|
||||
"dv_good_ending",
|
||||
"dv_bad_ending",
|
||||
"un_good_ending",
|
||||
"un_bad_ending",
|
||||
"us_good_ending",
|
||||
"us_bad_ending",
|
||||
"mi_ending",
|
||||
"uv_ending",
|
||||
"harem_ending"
|
||||
],
|
||||
"routes": {
|
||||
"sl": {
|
||||
"good": "sl_good_ending",
|
||||
"bad": "sl_bad_ending",
|
||||
"point_key": "sl_points"
|
||||
},
|
||||
"dv": {
|
||||
"good": "dv_good_ending",
|
||||
"bad": "dv_bad_ending",
|
||||
"point_key": "dv_points"
|
||||
},
|
||||
"un": {
|
||||
"good": "un_good_ending",
|
||||
"bad": "un_bad_ending",
|
||||
"point_key": "un_points"
|
||||
},
|
||||
"us": {
|
||||
"good": "us_good_ending",
|
||||
"bad": "us_bad_ending",
|
||||
"point_key": "us_points"
|
||||
},
|
||||
"mi": {
|
||||
"single": "mi_ending",
|
||||
"point_key": "mi_points"
|
||||
},
|
||||
"uv": {
|
||||
"single": "uv_ending",
|
||||
"point_key": "uv_points"
|
||||
}
|
||||
},
|
||||
"fallback": "main_bad_ending"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.2 MiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 394 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 519 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 569 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 46 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/1.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/10.png
Normal file
|
After Width: | Height: | Size: 317 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/2.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/3.png
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/4.png
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/5.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/6.png
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/7.png
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/8.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/9.png
Normal file
|
After Width: | Height: | Size: 313 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/Hangman/full_hp.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
1488
SunnexGB/Heroku-Modules/Assets/Hangman/words.txt
Normal file
400
SunnexGB/Heroku-Modules/Assets/Mikuru/cultural_words_en.txt
Normal file
@@ -0,0 +1,400 @@
|
||||
2g1c
|
||||
2 girls 1 cup
|
||||
acrotomophilia
|
||||
alabama hot pocket
|
||||
alaskan pipeline
|
||||
anal
|
||||
anilingus
|
||||
anus
|
||||
apeshit
|
||||
arsehole
|
||||
ass
|
||||
asshole
|
||||
assmunch
|
||||
auto erotic
|
||||
autoerotic
|
||||
babeland
|
||||
baby batter
|
||||
baby juice
|
||||
ball gag
|
||||
ball gravy
|
||||
ball kicking
|
||||
ball licking
|
||||
ball sack
|
||||
ball sucking
|
||||
bangbros
|
||||
bangbus
|
||||
bareback
|
||||
barely legal
|
||||
barenaked
|
||||
bastard
|
||||
bastardo
|
||||
bastinado
|
||||
bbw
|
||||
bdsm
|
||||
beaner
|
||||
beaners
|
||||
beaver cleaver
|
||||
beaver lips
|
||||
beastiality
|
||||
bestiality
|
||||
big black
|
||||
big breasts
|
||||
big knockers
|
||||
big tits
|
||||
bimbos
|
||||
birdlock
|
||||
bitch
|
||||
bitches
|
||||
black cock
|
||||
blonde action
|
||||
blonde on blonde action
|
||||
blowjob
|
||||
blow job
|
||||
blow your load
|
||||
blue waffle
|
||||
blumpkin
|
||||
bollocks
|
||||
bondage
|
||||
boner
|
||||
boob
|
||||
boobs
|
||||
booty call
|
||||
brown showers
|
||||
brunette action
|
||||
bukkake
|
||||
bulldyke
|
||||
bullet vibe
|
||||
bullshit
|
||||
bung hole
|
||||
bunghole
|
||||
busty
|
||||
butt
|
||||
buttcheeks
|
||||
butthole
|
||||
camel toe
|
||||
camgirl
|
||||
camslut
|
||||
camwhore
|
||||
carpet muncher
|
||||
carpetmuncher
|
||||
chocolate rosebuds
|
||||
cialis
|
||||
circlejerk
|
||||
cleveland steamer
|
||||
clit
|
||||
clitoris
|
||||
clover clamps
|
||||
clusterfuck
|
||||
cock
|
||||
cocks
|
||||
coprolagnia
|
||||
coprophilia
|
||||
cornhole
|
||||
coon
|
||||
coons
|
||||
creampie
|
||||
cum
|
||||
cumming
|
||||
cumshot
|
||||
cumshots
|
||||
cunnilingus
|
||||
cunt
|
||||
darkie
|
||||
date rape
|
||||
daterape
|
||||
deep throat
|
||||
deepthroat
|
||||
dendrophilia
|
||||
dick
|
||||
dildo
|
||||
dingleberry
|
||||
dingleberries
|
||||
dirty pillows
|
||||
dirty sanchez
|
||||
doggie style
|
||||
doggiestyle
|
||||
doggy style
|
||||
doggystyle
|
||||
dog style
|
||||
dolcett
|
||||
domination
|
||||
dominatrix
|
||||
dommes
|
||||
donkey punch
|
||||
double dong
|
||||
double penetration
|
||||
dp action
|
||||
dry hump
|
||||
dvda
|
||||
eat my ass
|
||||
ecchi
|
||||
ejaculation
|
||||
erotic
|
||||
erotism
|
||||
escort
|
||||
eunuch
|
||||
fag
|
||||
faggot
|
||||
fecal
|
||||
felch
|
||||
fellatio
|
||||
feltch
|
||||
female squirting
|
||||
femdom
|
||||
figging
|
||||
fingerbang
|
||||
fingering
|
||||
fisting
|
||||
foot fetish
|
||||
footjob
|
||||
frotting
|
||||
fuck
|
||||
fuck buttons
|
||||
fuckin
|
||||
fucking
|
||||
fucktards
|
||||
fudge packer
|
||||
fudgepacker
|
||||
futanari
|
||||
gangbang
|
||||
gang bang
|
||||
gay sex
|
||||
genitals
|
||||
giant cock
|
||||
girl on
|
||||
girl on top
|
||||
girls gone wild
|
||||
goatcx
|
||||
goatse
|
||||
god damn
|
||||
gokkun
|
||||
golden shower
|
||||
goodpoop
|
||||
goo girl
|
||||
goregasm
|
||||
grope
|
||||
group sex
|
||||
g-spot
|
||||
guro
|
||||
hand job
|
||||
handjob
|
||||
hard core
|
||||
hardcore
|
||||
hentai
|
||||
homoerotic
|
||||
honkey
|
||||
hooker
|
||||
horny
|
||||
hot carl
|
||||
hot chick
|
||||
how to kill
|
||||
how to murder
|
||||
huge fat
|
||||
humping
|
||||
incest
|
||||
intercourse
|
||||
jack off
|
||||
jail bait
|
||||
jailbait
|
||||
jelly donut
|
||||
jerk off
|
||||
jigaboo
|
||||
jiggaboo
|
||||
jiggerboo
|
||||
jizz
|
||||
juggs
|
||||
kike
|
||||
kinbaku
|
||||
kinkster
|
||||
kinky
|
||||
knobbing
|
||||
leather restraint
|
||||
leather straight jacket
|
||||
lemon party
|
||||
livesex
|
||||
lolita
|
||||
lovemaking
|
||||
make me come
|
||||
male squirting
|
||||
masturbate
|
||||
masturbating
|
||||
masturbation
|
||||
menage a trois
|
||||
milf
|
||||
missionary position
|
||||
mong
|
||||
motherfucker
|
||||
mound of venus
|
||||
mr hands
|
||||
muff diver
|
||||
muffdiving
|
||||
nambla
|
||||
nawashi
|
||||
negro
|
||||
neonazi
|
||||
nigga
|
||||
nigger
|
||||
nig nog
|
||||
nimphomania
|
||||
nipple
|
||||
nipples
|
||||
nsfw
|
||||
nsfw images
|
||||
nude
|
||||
nudity
|
||||
nutten
|
||||
nympho
|
||||
nymphomania
|
||||
octopussy
|
||||
omorashi
|
||||
one cup two girls
|
||||
one guy one jar
|
||||
orgasm
|
||||
orgy
|
||||
paedophile
|
||||
paki
|
||||
panties
|
||||
panty
|
||||
pedobear
|
||||
pedophile
|
||||
pegging
|
||||
penis
|
||||
phone sex
|
||||
piece of shit
|
||||
pikey
|
||||
pissing
|
||||
piss pig
|
||||
pisspig
|
||||
playboy
|
||||
pleasure chest
|
||||
pole smoker
|
||||
ponyplay
|
||||
poof
|
||||
poon
|
||||
poontang
|
||||
punany
|
||||
poop chute
|
||||
poopchute
|
||||
porn
|
||||
porno
|
||||
pornography
|
||||
prince albert piercing
|
||||
pthc
|
||||
pubes
|
||||
pussy
|
||||
queaf
|
||||
queef
|
||||
quim
|
||||
raghead
|
||||
raging boner
|
||||
rape
|
||||
raping
|
||||
rapist
|
||||
rectum
|
||||
reverse cowgirl
|
||||
rimjob
|
||||
rimming
|
||||
rosy palm
|
||||
rosy palm and her 5 sisters
|
||||
rusty trombone
|
||||
sadism
|
||||
santorum
|
||||
scat
|
||||
schlong
|
||||
scissoring
|
||||
semen
|
||||
sex
|
||||
sexcam
|
||||
sexo
|
||||
sexy
|
||||
sexual
|
||||
sexually
|
||||
sexuality
|
||||
shaved beaver
|
||||
shaved pussy
|
||||
shemale
|
||||
shibari
|
||||
shit
|
||||
shitblimp
|
||||
shitty
|
||||
shota
|
||||
shrimping
|
||||
skeet
|
||||
slanteye
|
||||
slut
|
||||
s&m
|
||||
smut
|
||||
snatch
|
||||
snowballing
|
||||
sodomize
|
||||
sodomy
|
||||
spastic
|
||||
spic
|
||||
splooge
|
||||
splooge moose
|
||||
spooge
|
||||
spread legs
|
||||
spunk
|
||||
strap on
|
||||
strapon
|
||||
strappado
|
||||
strip club
|
||||
style doggy
|
||||
suck
|
||||
sucks
|
||||
suicide girls
|
||||
sultry women
|
||||
swastika
|
||||
swinger
|
||||
tainted love
|
||||
taste my
|
||||
tea bagging
|
||||
threesome
|
||||
throating
|
||||
thumbzilla
|
||||
tied up
|
||||
tight white
|
||||
tit
|
||||
tits
|
||||
titties
|
||||
titty
|
||||
tongue in a
|
||||
topless
|
||||
tosser
|
||||
towelhead
|
||||
tranny
|
||||
tribadism
|
||||
tub girl
|
||||
tubgirl
|
||||
tushy
|
||||
twat
|
||||
twink
|
||||
twinkie
|
||||
two girls one cup
|
||||
undressing
|
||||
upskirt
|
||||
urethra play
|
||||
urophilia
|
||||
vagina
|
||||
venus mound
|
||||
viagra
|
||||
vibrator
|
||||
violet wand
|
||||
vorarephilia
|
||||
voyeur
|
||||
voyeurweb
|
||||
voyuer
|
||||
vulva
|
||||
wank
|
||||
wetback
|
||||
wet dream
|
||||
white power
|
||||
whore
|
||||
worldsex
|
||||
wrapping men
|
||||
wrinkled starfish
|
||||
yaoi
|
||||
yellow showers
|
||||
yiffy
|
||||
zoophilia
|
||||
2187
SunnexGB/Heroku-Modules/Assets/Mikuru/cultural_words_ru.txt
Normal file
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/icons/first.png
Normal file
|
After Width: | Height: | Size: 609 B |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/icons/last.png
Normal file
|
After Width: | Height: | Size: 609 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="30" height="22" viewBox="0 0 30 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24.5455 5.5L19.0909 11H23.1818C23.1818 15.5512 19.5136 19.25 15 19.25C13.6227 19.25 12.3136 18.9062 11.1818 18.2875L9.19091 20.295C10.8682 21.3675 12.8591 22 15 22C21.0273 22 25.9091 17.0775 25.9091 11H30L24.5455 5.5ZM6.81818 11C6.81818 6.44875 10.4864 2.75 15 2.75C16.3773 2.75 17.6864 3.09375 18.8182 3.7125L20.8091 1.705C19.1318 0.6325 17.1409 0 15 0C8.97273 0 4.09091 4.9225 4.09091 11H0L5.45455 16.5L10.9091 11H6.81818Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 554 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="23" height="25" viewBox="0 0 23 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6389 10.05H17.3611M7.91667 5.65H17.3611M21.25 17.75V6.75C21.25 3.45 19.5833 1.25 15.6944 1.25H6.80555C2.91667 1.25 1.25 3.45 1.25 6.75V17.75C1.25 21.05 2.91667 23.25 6.80555 23.25H15.6944C19.5833 23.25 21.25 21.05 21.25 17.75ZM15.1389 23.25V14.604C15.1389 14.12 14.5611 13.878 14.2056 14.197L11.6278 16.551C11.4167 16.749 11.0833 16.749 10.8722 16.551L8.29448 14.197C7.93893 13.878 7.36112 14.12 7.36112 14.604V23.25H15.1389Z" stroke="white" stroke-width="2.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 579 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.3333 7.33333C16.2381 7.33333 14.6667 5.76191 14.6667 3.66667C14.6667 1.57143 16.2381 0 18.3333 0C20.4286 0 22 1.57143 22 3.66667C22 5.76191 20.4286 7.33333 18.3333 7.33333ZM3.66667 14.6667C1.57143 14.6667 0 13.0952 0 11C0 8.90476 1.57143 7.33333 3.66667 7.33333C5.76191 7.33333 7.33333 8.90476 7.33333 11C7.33333 13.0952 5.76191 14.6667 3.66667 14.6667ZM18.3333 22C16.2381 22 14.6667 20.4286 14.6667 18.3333C14.6667 16.2381 16.2381 14.6667 18.3333 14.6667C20.4286 14.6667 22 16.2381 22 18.3333C22 20.4286 20.4286 22 18.3333 22ZM4.1381 10.0676L18.8048 17.401L17.8724 19.2762L3.20571 11.9429L4.1381 10.0676ZM18.8048 4.59905L4.1381 11.9324L3.20571 10.0571L17.8724 2.72381L18.8048 4.59905Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 817 B |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/icons/next.png
Normal file
|
After Width: | Height: | Size: 539 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5 23.5L15.3223 18.5362C15.8683 17.8294 16.8801 17.7008 17.5867 18.247L20.5257 20.5603C21.2323 21.1064 22.2441 20.9779 22.7902 20.2872L26.5 15.5M13.75 36.5H24.25C33 36.5 36.5 33 36.5 24.25V13.75C36.5 5 33 1.5 24.25 1.5H13.75C5 1.5 1.5 5 1.5 13.75V24.25C1.5 33 5 36.5 13.75 36.5Z" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/icons/prev.png
Normal file
|
After Width: | Height: | Size: 520 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="61" height="54" viewBox="0 0 61 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.65352 50.6608C7.65352 48.8166 9.11479 47.3216 10.9173 47.3216H50.0828C51.8854 47.3216 53.3466 48.8166 53.3466 50.6608C53.3466 52.505 51.8854 54 50.0828 54H10.9173C9.11479 54 7.65352 52.505 7.65352 50.6608Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.5764 2.93407C27.4001 -0.978025 33.5996 -0.978025 37.4235 2.93407L45.8 11.504C46.7935 12.5204 48.3108 12.7723 49.5674 12.1295L51.887 10.943C56.8965 8.38037 62.4336 13.2937 60.6626 18.7299L54.6667 37.1327C53.3341 41.2232 49.5925 43.9824 45.378 43.9824H15.6221C11.4076 43.9824 7.66588 41.2232 6.33315 37.1327L0.337435 18.7299C-1.43373 13.2937 4.10349 8.38037 9.11308 10.943L11.4325 12.1295C12.689 12.7723 14.2066 12.5204 15.2 11.504L23.5764 2.93407Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 893 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="45" height="84" viewBox="0 0 45 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.2576 0.680354L22.5645 0.764917C22.704 0.811776 22.8388 0.867879 22.969 0.933227L23.4432 1.10181C24.2984 1.48492 24.7861 1.78787 24.9063 2.01065C24.9155 2.03847 25.0131 2.09906 25.1989 2.19241C25.394 2.31359 25.5008 2.402 25.5192 2.45765C25.5932 2.58762 25.5501 2.87923 25.39 3.33248C25.3243 3.4619 25.2538 3.62373 25.1786 3.81795L25.1079 4.02609L25.0516 4.13702C24.4029 5.69064 23.5858 7.43811 22.6004 9.37945C22.2905 10.0359 21.84 10.9234 21.2489 12.0419L18.025 18.3928L17.786 18.7948L17.5889 19.1831C17.2794 19.7469 17.1291 20.0659 17.1381 20.14C17.1472 20.2141 17.3421 20.3585 17.7231 20.573L17.96 20.7268L18.183 20.8388C18.3503 20.9228 18.4571 20.9881 18.5035 21.0346L18.8099 21.2581L19.1302 21.5233C19.3343 21.7186 19.4407 21.8765 19.4496 21.997C19.4582 22.1637 19.3128 22.4503 19.0132 22.8567L18.8166 23.106L18.6341 23.3276C18.4937 23.5123 18.3908 23.6462 18.3253 23.7293L17.7627 24.6997C17.547 25.0787 17.3553 25.2724 17.1877 25.281C17.1597 25.2902 16.7785 25.1451 16.044 24.8459C15.2909 24.5372 14.84 24.3456 14.6913 24.2709C14.1987 24.0467 13.5573 23.7478 12.7672 23.3742L12.307 23.1779L11.8888 22.9678C11.582 22.8369 11.345 22.7295 11.1777 22.6455L10.6479 22.3794L10.4668 22.2537L10.2717 22.1556C9.99285 22.0156 9.66287 21.8615 9.28173 21.6933C9.26314 21.6839 9.21198 21.6698 9.12824 21.651L8.86323 21.5527C8.62141 21.4776 8.4728 21.3798 8.4174 21.2592C8.39899 21.2035 8.42752 21.0554 8.50296 20.8149L8.61586 20.5235L8.71495 20.1905C8.90339 19.6354 9.04914 19.2794 9.15219 19.1223C9.33033 18.8174 9.53635 18.5264 9.77025 18.2494C10.0509 17.9263 10.2797 17.7512 10.4568 17.7241C10.6152 17.6877 10.8154 17.707 11.0572 17.782L11.3642 17.8666L11.6292 17.9649L11.9919 18.0775L12.3127 18.2038C12.5265 18.288 12.6847 18.321 12.7872 18.3029C12.8152 18.2937 12.9272 18.2339 13.1232 18.1236L13.1795 18.0126L13.1798 17.9432C13.1707 17.869 13.1756 17.8135 13.1944 17.7765C13.2693 17.6749 13.3348 17.5918 13.3909 17.5272C13.4377 17.481 13.4797 17.4441 13.5171 17.4165C13.601 17.389 13.6524 17.3568 13.6711 17.3198C13.7274 17.2089 13.798 17.0239 13.8828 16.7649L14.0098 16.4458L14.1509 16.099L14.7562 14.9065C15.0751 14.3242 15.291 13.8989 15.4038 13.6308C15.526 13.3441 15.7418 12.942 16.0511 12.4245L16.4447 11.7869C16.6041 11.5189 16.7448 11.2647 16.8668 11.0244L17.444 9.88729C17.5191 9.73938 17.6504 9.50369 17.8379 9.18023L18.0491 8.76422L18.2881 8.36223C18.654 7.68747 18.8278 7.29911 18.8096 7.19715C18.7728 7.08585 18.6052 7.07131 18.3069 7.15351C18.2044 7.17163 18.0972 7.199 17.9853 7.23562L17.7755 7.30427C17.2629 7.39489 16.9556 7.40296 16.8535 7.32846C16.7699 7.26329 16.664 6.96647 16.5357 6.43799C16.4993 6.23406 16.472 6.05796 16.454 5.90969L16.385 5.70101L16.2881 5.47832L16.3311 5.18671C16.3595 5.08493 16.3691 4.99697 16.3601 4.92283L16.3195 4.58922L16.2651 4.21386C16.2384 3.89882 16.2861 3.62112 16.4081 3.38077C16.5864 3.02948 17.3425 2.57392 18.6764 2.0141C18.9469 1.89474 19.2408 1.75232 19.5581 1.58683L19.9501 1.36605L20.328 1.17301C20.9158 0.888157 21.438 0.709572 21.8946 0.637252C21.9785 0.609792 22.0623 0.605489 22.146 0.624342L22.2576 0.680354Z" fill="white"/>
|
||||
<path d="M23.9091 76.2443C24.5784 76.534 25.4198 76.8754 26.4333 77.2684L27.7305 77.7458C28.2605 77.9424 28.7301 78.1203 29.1392 78.2793C29.2508 78.3353 29.4088 78.3915 29.6135 78.4479L30.0878 78.6165C30.4691 78.7384 30.706 78.869 30.7986 79.0083C30.882 79.1198 30.895 79.3468 30.8378 79.6893C30.7808 79.9855 30.6864 80.3093 30.5546 80.6608C30.5263 80.7625 30.4794 80.8318 30.4141 80.8686L30.2739 81.007L30.2173 81.1874L30.1886 81.3818C30.1507 81.5484 30.0944 81.6593 30.0197 81.7146L29.8794 81.853C29.8327 81.8992 29.8046 81.9315 29.7952 81.95C29.7483 82.0424 29.72 82.1442 29.7102 82.2553L29.6674 82.4774C29.6387 82.6718 29.6009 82.8153 29.554 82.9077C29.5164 82.9817 29.451 83.0416 29.3577 83.0876C29.3204 83.1152 29.283 83.1429 29.2456 83.1705C29.1895 83.2351 29.1287 83.309 29.0632 83.3921C29.0538 83.4106 29.0443 83.4522 29.0347 83.517L29.0201 83.6837C28.9916 83.8318 28.9074 83.9287 28.7675 83.9745C28.5717 84.0386 28.2972 83.9819 27.9441 83.8045L27.4143 83.5385C27.2007 83.408 27.0148 83.3146 26.8567 83.2584C26.5127 83.1089 26.0711 82.9219 25.5319 82.6975L24.8765 82.4032L24.1792 82.1225C23.4819 81.8419 22.6173 81.4773 21.5854 81.0287L20.2747 80.44C19.7169 80.2062 19.2381 80.0006 18.8384 79.823C18.6897 79.7484 18.4665 79.6595 18.1689 79.5564L17.7645 79.3881L17.3739 79.2616C16.7138 78.9996 16.3328 78.7851 16.231 78.618C16.2032 78.5808 16.199 78.4743 16.2183 78.2983C16.2191 78.1131 16.2337 77.9464 16.2622 77.7983C16.2907 77.6502 16.3426 77.4791 16.4179 77.2848L16.6296 76.7299C16.7897 76.2767 16.8843 75.9065 16.9134 75.6195L17.392 74.6765C17.5707 74.2326 17.6928 73.9691 17.7583 73.886C17.8612 73.7753 18.1039 73.6187 18.4865 73.4164L18.7245 73.2923L18.9903 73.1822L19.2283 73.0581L19.4521 72.9617C19.6947 72.8515 20.049 72.7278 20.5152 72.5907L21.4803 72.2749C21.5082 72.2658 21.5503 72.2289 21.6064 72.1643L21.6346 72.1088L21.7047 72.0396L21.8585 72.0124L22.0263 71.9575C22.1941 71.9026 22.334 71.8568 22.4459 71.8202L22.9077 71.5997L23.3836 71.3514C23.7848 71.1585 24.1114 70.9977 24.3633 70.869L24.839 70.6902L25.2867 70.4974L25.4968 70.3593L25.7209 70.1935C25.9171 70.0368 26.0758 69.9309 26.1971 69.8758C26.3092 69.7928 26.4678 69.7101 26.673 69.6275L27.1349 69.407C27.3962 69.2598 27.7089 69.0573 28.0731 68.7993L28.9835 68.1777C29.6466 67.6986 30.2444 67.2331 30.777 66.7813L31.0854 66.5186L31.4219 66.2003C31.6742 65.979 31.8752 65.7667 32.025 65.5635C32.4181 65.0649 32.5224 64.5836 32.338 64.1198C32.3196 64.0641 32.2732 64.0176 32.1989 63.9803L32.1014 63.8965L32.0319 63.8268L32.0043 63.7433L32.0045 63.6738C32.0047 63.6275 31.9816 63.5811 31.9352 63.5346C31.5267 63.2366 31.0616 63.0959 30.5401 63.1124C30.3538 63.1116 30.1675 63.1341 29.9811 63.1796L29.7293 63.262L29.5336 63.3029C29.3751 63.3394 29.2167 63.3758 29.0582 63.4122C28.853 63.4948 28.6758 63.5682 28.5266 63.6325C28.4986 63.6416 28.4565 63.6785 28.4004 63.7431L28.3444 63.7846L28.2742 63.8538C28.2463 63.8629 28.1857 63.8673 28.0926 63.867C27.9995 63.8666 27.9342 63.8803 27.8969 63.9079L27.4767 64.1841C27.2901 64.276 27.1224 64.3078 26.9735 64.2794C26.7223 64.2229 26.5179 64.097 26.3604 63.9019L26.1936 63.6789L26.0548 63.47C25.4059 62.7264 24.9472 62.1364 24.6789 61.7C24.6697 61.6722 24.6326 61.6304 24.5676 61.5746C24.5119 61.5466 24.4701 61.514 24.4423 61.4768L24.3457 61.1847C24.2905 61.0177 24.2911 60.8556 24.3476 60.6984C24.4041 60.5411 24.8104 60.2231 25.5666 59.7444L26.2948 59.2749C26.5563 59.0814 26.7291 58.9709 26.813 58.9434C26.9343 58.8883 27.1579 58.8383 27.4841 58.7932L27.8755 58.7114L28.3089 58.6158L28.6586 58.4782L29.0222 58.3824C29.423 58.2821 29.7725 58.214 30.0706 58.1781C30.406 58.1609 30.7599 58.153 31.1323 58.1544C31.57 58.1654 31.9749 58.1948 32.3473 58.2426C32.5241 58.2618 32.9008 58.3698 33.4774 58.5665C33.9795 58.7723 34.4861 58.9919 34.9974 59.2255C35.555 59.5056 35.9499 59.7387 36.1819 59.9248L36.1817 59.9943L36.2093 60.0778C36.2183 60.1519 36.2321 60.1936 36.2507 60.203L36.696 60.6354C36.8723 60.7936 37.0113 60.9562 37.1131 61.1234C37.5022 61.6436 37.7414 62.367 37.8309 63.2937C37.903 63.8868 37.8774 64.4887 37.7539 65.0996C37.6113 65.8401 37.3291 66.5568 36.9073 67.2499C36.8324 67.3515 36.7342 67.4762 36.6125 67.6239L36.2757 68.0116L35.3637 69.0501C34.9989 69.447 34.6763 69.7838 34.3959 70.0606C33.4428 70.8813 32.5462 71.5447 31.7061 72.0509L31.5661 72.1198L31.454 72.2027C31.3699 72.2765 31.2719 72.3317 31.1601 72.3683C30.5723 72.6532 29.7604 73.0807 28.7245 73.6509C28.4446 73.7888 28.062 73.9911 27.5767 74.2578L26.359 74.8644C25.3698 75.3885 24.5532 75.8485 23.9091 76.2443Z" fill="white"/>
|
||||
<path d="M39.4239 0L45 2.80102L5.57613 80.4516L0 77.6506L39.4239 0Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="40" height="58" viewBox="0 0 40 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 34.4L8.35315 33.0659C12.5974 32.1747 16.9968 32.5988 21.0155 34.2866C25.3704 36.1156 30.1633 36.4571 34.7137 35.2627L35.265 35.1179C36.8724 34.6962 38 33.1796 38 31.4403V11.5492C38 9.44166 36.1139 7.89526 34.1665 8.4064C29.972 9.50749 25.5535 9.19261 21.5393 7.50657L21.0155 7.28663C16.9968 5.59878 12.5974 5.17466 8.35315 6.06596L2 7.40011V34.4Z" fill="white"/>
|
||||
<path d="M2 56V34.4M2 34.4L8.35315 33.0659C12.5974 32.1747 16.9968 32.5988 21.0155 34.2866C25.3704 36.1156 30.1633 36.4571 34.7137 35.2627L35.265 35.1179C36.8724 34.6962 38 33.1796 38 31.4403V11.5492C38 9.44166 36.1139 7.89526 34.1665 8.4064C29.972 9.50749 25.5535 9.19261 21.5393 7.50657L21.0155 7.28663C16.9968 5.59878 12.5974 5.17466 8.35315 6.06596L2 7.40011V34.4ZM2 34.4V2" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 914 B |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/mr_granstandercleang.otf
Normal file
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/other/bg.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/other/noise.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-bishop.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-king.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-knight.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-pawn.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-queen.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/black-rook.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-bishop.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-king.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-knight.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-pawn.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-queen.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
SunnexGB/Heroku-Modules/Assets/NoChess/pieces/white-rook.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
113
SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/index.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<title>NoChess</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="web_chess">
|
||||
<div class="pgn_moves_board">
|
||||
<div class="move_board_bg"></div>
|
||||
<div class="moves_list">
|
||||
<div class="moves_list_mobile">
|
||||
</div>
|
||||
</div>
|
||||
<div class="moves_control_buttons">
|
||||
<div class="move_btn first_move_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/first.png" alt="first">
|
||||
</div>
|
||||
<div class="move_btn prev_move_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/prev.png" alt="prev">
|
||||
</div>
|
||||
<div class="move_btn next_move_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/next.png" alt="next">
|
||||
</div>
|
||||
<div class="move_btn last_move_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/last.png" alt="last">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="board-wrapper">
|
||||
<canvas id="chessBoard"></canvas>
|
||||
</div>
|
||||
<div class="main_board">
|
||||
<div class="main_board_bg"></div>
|
||||
<div class="player_block player_black">
|
||||
<div class="player_name_black">guest-acc</div>
|
||||
<div class="player_avatar avatar_black"></div>
|
||||
</div>
|
||||
<div class="divider_line top_line">
|
||||
<svg width="270" height="4" viewBox="0 0 270 4">
|
||||
<path d="M75 0H95V4H75V0Z" fill="white" />
|
||||
<path d="M50 0H70V4H50V0Z" fill="white" />
|
||||
<path d="M25 0H45V4H25V0Z" fill="white" />
|
||||
<path d="M0 0H20V4H0V0Z" fill="white" />
|
||||
<path d="M100 0H120V4H100V0Z" fill="white" />
|
||||
<path d="M125 0H145V4H125V0Z" fill="white" />
|
||||
<path d="M150 0H170V4H150V0Z" fill="white" />
|
||||
<path d="M175 0H195V4H175V0Z" fill="white" />
|
||||
<path d="M200 0H220V4H200V0Z" fill="white" />
|
||||
<path d="M225 0H245V4H225V0Z" fill="white" />
|
||||
<path d="M250 0H270V4H250V0Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="timer timer_black">--:--</div>
|
||||
<div class="board_menu">
|
||||
<div class="menu_btn" id="flip_board_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-flip.svg" alt="flip" />
|
||||
</div>
|
||||
<div class="menu_btn" id="history_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-history.svg" alt="history" />
|
||||
</div>
|
||||
<div class="menu_btn" id="share_btn">
|
||||
<img src="https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/icons/menu-share.svg" alt="share" />
|
||||
</div>
|
||||
<div class="menu_btn menu_more_btn" id="more_btn" aria-label="toggle menu">
|
||||
<span class="more_dots" aria-hidden="true">
|
||||
<span class="more_dot more_dot_top"></span>
|
||||
<span class="more_dot more_dot_mid"></span>
|
||||
<span class="more_dot more_dot_bottom"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timer timer_white">--:--</div>
|
||||
<div class="divider_line bottom_line">
|
||||
<svg width="270" height="4" viewBox="0 0 270 4">
|
||||
<path d="M75 0H95V4H75V0Z" fill="white" />
|
||||
<path d="M50 0H70V4H50V0Z" fill="white" />
|
||||
<path d="M25 0H45V4H25V0Z" fill="white" />
|
||||
<path d="M0 0H20V4H0V0Z" fill="white" />
|
||||
<path d="M100 0H120V4H100V0Z" fill="white" />
|
||||
<path d="M125 0H145V4H125V0Z" fill="white" />
|
||||
<path d="M150 0H170V4H150V0Z" fill="white" />
|
||||
<path d="M175 0H195V4H175V0Z" fill="white" />
|
||||
<path d="M200 0H220V4H200V0Z" fill="white" />
|
||||
<path d="M225 0H245V4H225V0Z" fill="white" />
|
||||
<path d="M250 0H270V4H250V0Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="player_block player_white">
|
||||
<div class="player_avatar avatar_white"></div>
|
||||
<div class="player_name_white">Sunnex <3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="submenu_overlay" id="submenu_overlay"></div>
|
||||
<div class="submenu_panel" id="history_panel">
|
||||
<div class="submenu_title">History</div>
|
||||
<div class="history_games" id="history_games"></div>
|
||||
</div>
|
||||
<div class="submenu_panel" id="share_panel">
|
||||
<div class="submenu_title">Share PGN</div>
|
||||
<textarea id="share_pgn_text"></textarea>
|
||||
<div class="share_actions">
|
||||
<button id="load_pgn_btn" type="button">Load PGN</button>
|
||||
<button id="copy_pgn_btn" type="button">Copy PGN</button>
|
||||
</div>
|
||||
<div id="share_status"></div>
|
||||
</div>
|
||||
<script src="javascript.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1675
SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/javascript.js
Normal file
967
SunnexGB/Heroku-Modules/Assets/NoChess/raw_assets/style.css
Normal file
@@ -0,0 +1,967 @@
|
||||
@font-face {
|
||||
font-family: 'mr_GranstanderCleanG';
|
||||
src: url('https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/NoChess/mr_granstandercleang.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1A1224;
|
||||
background-image: url('bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 100vh;
|
||||
font-family: 'mr_GranstanderCleanG', Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.web_chess {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 573px) and (max-width: 1562px) {
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.web_chess {
|
||||
width: 1562px;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: 0;
|
||||
--desktop-scale: min(calc(100vw / 1562px), calc(100vh / 856px), 1);
|
||||
transform-origin: center center;
|
||||
transform: translate(-50%, -50%) scale(var(--desktop-scale));
|
||||
}
|
||||
}
|
||||
|
||||
.board-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#chessBoard {
|
||||
width: 856px;
|
||||
height: 856px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pgn_moves_board {
|
||||
width: 333px;
|
||||
height: 856px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.move_board_bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #1E1E1E;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.move_board_bg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 80px;
|
||||
height: 48px;
|
||||
background: linear-gradient(180deg, #00000000 0%, #00000040 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pgn_moves_board.has_scroll .move_board_bg::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.moves_list {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 80px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.moves_list_mobile {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.moves_list_mobile::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.move_row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 1fr;
|
||||
align-items: center;
|
||||
min-height: 52px;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.move_number {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.move_white,
|
||||
.move_black {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.current_move {
|
||||
background: #B7B7B71A;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.move_white,
|
||||
.move_black {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.move_white:empty,
|
||||
.move_black:empty {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.moves_control_buttons {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.move_btn {
|
||||
width: 75px;
|
||||
height: 37px;
|
||||
background: #2D2D2D;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 3.7px -1px #00000040;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.move_btn img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.move_btn:hover {
|
||||
background: #3A3A3A;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.move_btn:active {
|
||||
background: #252525;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.main_board {
|
||||
width: 333px;
|
||||
height: 856px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main_board_bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #00000054;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.player_block {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player_black {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.player_white {
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black {
|
||||
top: auto;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white {
|
||||
top: 20px;
|
||||
bottom: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black .player_avatar {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black .player_name_black {
|
||||
order: 2;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white .player_name_white {
|
||||
order: 1;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white .player_avatar {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.player_avatar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 189px;
|
||||
height: 189px;
|
||||
aspect-ratio: 1 / 1;
|
||||
margin: 0 auto;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border: 5px solid #FFFFFF;
|
||||
border-radius: 50%;
|
||||
clip-path: circle(50% at 50% 50%);
|
||||
}
|
||||
|
||||
.avatar_black {
|
||||
background-image: url('https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg');
|
||||
}
|
||||
|
||||
.avatar_white {
|
||||
background-image: url('https://i.pinimg.com/736x/6e/0a/0c/6e0a0cf688b30ba9de81b81bb32e49f9.jpg');
|
||||
}
|
||||
|
||||
.player_name_black {
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.player_name_white {
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.divider_line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.top_line {
|
||||
top: 260px;
|
||||
}
|
||||
|
||||
.bottom_line {
|
||||
bottom: 260px;
|
||||
}
|
||||
|
||||
.divider_line svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.timer {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 139px;
|
||||
height: 41px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #00000054;
|
||||
border-radius: 15px;
|
||||
color: #FFFFFF;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.timer_black {
|
||||
top: 280px;
|
||||
}
|
||||
|
||||
.timer_white {
|
||||
bottom: 280px;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .timer_black {
|
||||
top: auto;
|
||||
bottom: 280px;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .timer_white {
|
||||
top: 280px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.board_menu {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.menu_btn {
|
||||
width: 50px;
|
||||
height: 42px;
|
||||
background: #00000054;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.menu_btn:hover {
|
||||
background: #FFFFFF1A;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.menu_btn:active {
|
||||
background: #00000080;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.menu_btn img {
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.menu_more_btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.more_dots {
|
||||
position: relative;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.more_dot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #FFFFFF;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.more_dot_top {
|
||||
transform: translate(-50%, calc(-50% - var(--dot-gap, 10px)));
|
||||
transition: transform 0.32s ease;
|
||||
}
|
||||
|
||||
.more_dot_mid {
|
||||
transition: transform 0.32s ease;
|
||||
}
|
||||
|
||||
.more_dot_bottom {
|
||||
transform: translate(-50%, calc(-50% + var(--dot-gap, 10px)));
|
||||
transition: transform 0.32s ease;
|
||||
}
|
||||
|
||||
.menu_more_btn.active .more_dot_top {
|
||||
transform: translate(calc(-50% - var(--dot-gap, 10px)), -50%);
|
||||
}
|
||||
|
||||
.menu_more_btn.active .more_dot_bottom {
|
||||
transform: translate(calc(-50% + var(--dot-gap, 10px)), -50%);
|
||||
}
|
||||
|
||||
.web_chess.mobile_menu_open .menu_more_btn .more_dot_mid {
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
|
||||
.submenu_overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #00000080;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.submenu_overlay.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.submenu_panel {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.98);
|
||||
width: min(540px, 92vw);
|
||||
max-height: 80vh;
|
||||
background: #1e1e1e;
|
||||
border-radius: 15px;
|
||||
border: 1px solid #FFFFFF14;
|
||||
padding: 18px;
|
||||
color: #FFFFFF;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.submenu_panel.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.submenu_title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.history_games {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.history_games.empty {
|
||||
min-height: 120px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.history_empty {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history_game_btn {
|
||||
width: 100%;
|
||||
background: #2d2d2d;
|
||||
color: #FFFFFF;
|
||||
border: 1px solid #FFFFFF14;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history_game_btn:hover {
|
||||
background: #383838;
|
||||
}
|
||||
|
||||
#share_pgn_text {
|
||||
width: 100%;
|
||||
min-height: 170px;
|
||||
max-height: 55vh;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #FFFFFF1A;
|
||||
background: #121212;
|
||||
color: #FFFFFF;
|
||||
padding: 10px;
|
||||
resize: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.share_actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#load_pgn_btn,
|
||||
#copy_pgn_btn {
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
background: #2d2d2d;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
#load_pgn_btn {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
#load_pgn_btn:hover,
|
||||
#copy_pgn_btn:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
#share_status {
|
||||
margin-top: 10px;
|
||||
min-height: 20px;
|
||||
color: #d9d9d9;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
#share_status.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
#share_status.error {
|
||||
color: #ff8f8f;
|
||||
}
|
||||
|
||||
@media (max-width: 572px) {
|
||||
body {
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: clamp(72px, 17.7vw, 101px);
|
||||
background: linear-gradient(180deg, #00000000 0%, #0000008C 100%);
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.web_chess {
|
||||
--mobile-layout-width: 750px;
|
||||
--mobile-board-size: 750px;
|
||||
--mobile-board-top: 233px;
|
||||
--mobile-player-gap: clamp(24px, 5vw, 32px);
|
||||
--mobile-panel-height: clamp(64px, 17.13vw, 98px);
|
||||
--mobile-timer-height: clamp(30px, 7.17vw, 41px);
|
||||
--mobile-pgn-space: clamp(360px, 86vw, 492px);
|
||||
--mobile-layout-height: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-pgn-space));
|
||||
--mobile-fit-scale: min(1, calc((100vw - 2px) / var(--mobile-layout-width)), calc((100dvh - 2px) / var(--mobile-layout-height)));
|
||||
width: var(--mobile-layout-width);
|
||||
height: var(--mobile-layout-height);
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
display: block;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
transform-origin: top center;
|
||||
transform: translateX(-50%) scale(var(--mobile-fit-scale));
|
||||
}
|
||||
|
||||
.web_chess::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.board-wrapper {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: var(--mobile-board-top);
|
||||
transform: translateX(-50%);
|
||||
width: var(--mobile-board-size);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#chessBoard {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.main_board {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.main_board_bg,
|
||||
.divider_line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.board_menu {
|
||||
top: 4.2vw;
|
||||
right: 3.1vw;
|
||||
left: auto;
|
||||
transform: none;
|
||||
gap: 2.2vw;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.board_menu .menu_btn:not(.menu_more_btn) {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.28s ease, opacity 0.28s ease, background 0.2s;
|
||||
}
|
||||
|
||||
.web_chess:not(.mobile_menu_open) .board_menu .menu_btn:not(.menu_more_btn) {
|
||||
opacity: 0;
|
||||
transform: translateX(11vw) scale(0.86);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.web_chess.mobile_menu_open .board_menu .menu_btn:not(.menu_more_btn) {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.menu_btn {
|
||||
width: 50px;
|
||||
height: 42px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.menu_btn img {
|
||||
width: 28px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.menu_more_btn {
|
||||
--dot-gap: 14px;
|
||||
display: flex;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.menu_more_btn .more_dots {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.menu_more_btn .more_dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.player_block {
|
||||
width: calc(100% - 4.2vw);
|
||||
left: 2.1vw;
|
||||
height: var(--mobile-panel-height);
|
||||
min-height: 64px;
|
||||
border-radius: clamp(10px, 2.6vw, 15px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(10px, 2.8vw, 18px);
|
||||
padding: 0 calc(clamp(96px, 24.3vw, 139px) + max(6vw, 22px)) 0 max(2.4vw, 12px);
|
||||
text-align: left;
|
||||
background: #00000054;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.player_black {
|
||||
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height));
|
||||
}
|
||||
|
||||
.player_black .player_avatar {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.player_black .player_name_black {
|
||||
order: 2;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.player_white {
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap));
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.player_avatar {
|
||||
width: min(13.99vw, 80px);
|
||||
height: min(13.99vw, 80px);
|
||||
margin: 0;
|
||||
border-width: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.player_name_black,
|
||||
.player_name_white {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
max-width: min(39vw, 220px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: clamp(14px, 3.8vw, 20px);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.timer {
|
||||
left: auto;
|
||||
right: 6vw;
|
||||
transform: none;
|
||||
width: clamp(96px, 24.3vw, 139px);
|
||||
height: var(--mobile-timer-height);
|
||||
border-radius: 15px;
|
||||
font-size: clamp(12px, 3.8vw, 20px);
|
||||
background: #00000066;
|
||||
color: #FFFFFF;
|
||||
text-shadow: 0 1px 1px #0000004D;
|
||||
z-index: 7;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.timer_black {
|
||||
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
|
||||
}
|
||||
|
||||
.timer_white {
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.pgn_moves_board {
|
||||
position: absolute;
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + clamp(24px, 5vw, 32px) + var(--mobile-panel-height) + clamp(18px, 4vw, 28px));
|
||||
left: 50%;
|
||||
width: calc(100% - clamp(10px, 2.7vw, 20px));
|
||||
height: clamp(180px, 56vw, 320px);
|
||||
z-index: 3;
|
||||
opacity: 1;
|
||||
transform: translateX(-50%);
|
||||
transition: opacity 0.24s ease;
|
||||
}
|
||||
|
||||
.move_board_bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moves_list {
|
||||
top: 0;
|
||||
bottom: clamp(66px, 14vw, 84px);
|
||||
left: 50%;
|
||||
right: auto;
|
||||
width: clamp(260px, 44vw, 360px);
|
||||
transform: translateX(-50%);
|
||||
padding: 0;
|
||||
transition: opacity 0.24s ease;
|
||||
}
|
||||
|
||||
.moves_list::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.moves_list_mobile {
|
||||
height: 140%;
|
||||
gap: 0.6vw;
|
||||
pointer-events: auto;
|
||||
padding-right: 0;
|
||||
align-items: center;
|
||||
transform: translateY(clamp(10px, 2.2vh, 18px));
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.moves_list_mobile::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.move_row {
|
||||
min-height: 9vw;
|
||||
font-size: clamp(18px, 4.2vw, 24px);
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
grid-template-columns: auto auto auto;
|
||||
column-gap: clamp(10px, 2.8vw, 16px);
|
||||
}
|
||||
|
||||
.move_number {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.move_white,
|
||||
.move_black {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
justify-self: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.current_move {
|
||||
min-height: 0;
|
||||
width: max-content;
|
||||
padding: clamp(4px, 1vw, 8px) clamp(6px, 1.4vw, 10px);
|
||||
}
|
||||
|
||||
.moves_control_buttons {
|
||||
left: 2.6vw;
|
||||
right: 2.6vw;
|
||||
bottom: 0;
|
||||
width: auto;
|
||||
justify-content: flex-end;
|
||||
gap: 1.5vw;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.move_btn {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: #00000054;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.menu_btn,
|
||||
.move_btn {
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
-webkit-tap-highlight-color: #00000000;
|
||||
}
|
||||
|
||||
.menu_btn:hover {
|
||||
background: #FFFFFF1A;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.move_btn:hover {
|
||||
background-color: #00000054;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.menu_btn:active {
|
||||
background: #00000080;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.menu_more_btn:hover {
|
||||
background-color: #00000054;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.menu_more_btn:active {
|
||||
background-color: #00000054;
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.move_btn:active {
|
||||
background-color: #00000054;
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.move_btn img {
|
||||
width: 46%;
|
||||
height: 46%;
|
||||
}
|
||||
|
||||
.first_move_btn {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.last_move_btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.web_chess:not(.mobile_moves_open) .moves_list {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black {
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap));
|
||||
bottom: auto;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white {
|
||||
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height));
|
||||
bottom: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white .player_avatar {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_white .player_name_white {
|
||||
order: 2;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .player_black .player_name_black {
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .timer_black {
|
||||
top: calc(var(--mobile-board-top) + var(--mobile-board-size) + var(--mobile-player-gap) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.web_chess.board_flipped .timer_white {
|
||||
top: calc(var(--mobile-board-top) - var(--mobile-player-gap) - var(--mobile-panel-height) + (var(--mobile-panel-height) - var(--mobile-timer-height)) / 2);
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
1782
SunnexGB/Heroku-Modules/Assets/XOCheat/Opening_book.txt
Normal file
BIN
SunnexGB/Heroku-Modules/Assets/tmp_assets/chalkboardse.ttf
Normal file
323
SunnexGB/Heroku-Modules/DevStats.py
Normal file
@@ -0,0 +1,323 @@
|
||||
# meta developer: @H_SunMods
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/7c43eb05-4387-48f8-bbb2-20c5fad2f85f.jpg
|
||||
# current ver
|
||||
__version__ = (1, 0, 1)
|
||||
|
||||
from .. import loader, utils
|
||||
from herokutl.types import Message
|
||||
from ..types import InlineCall
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import math
|
||||
|
||||
FHETA_URL = "https://api.fixyres.com/grates"
|
||||
VECTOR_URL = "https://vector-three-sooty.vercel.app/api/devstats"
|
||||
VECTOR_TOPMOD_URL = "https://vector-three-sooty.vercel.app/api/usertopmod?users="
|
||||
|
||||
@loader.tds
|
||||
class DevStats(loader.Module):
|
||||
"""developers stats module"""
|
||||
|
||||
strings = {
|
||||
"name": "DevStats",
|
||||
"loading": "<b>Loading...</b>",
|
||||
"no_data": "<b>Failed to fetch data. Try again later.</b>",
|
||||
"dev_header": "<b><i>Most popular developers:</i></b>\n\n",
|
||||
"devtop_not_found": "<b>Your not found.</b>",
|
||||
"topmod_not_found": "<b>No modules found.</b>",
|
||||
"no_usernames": "<b>No usernames configured.</b> Set them in <code>.fcfg DevStats usernames @username</code>",
|
||||
"select_page": "<b>Select page:</b>",
|
||||
"btn_prev": "◄",
|
||||
"btn_next": "►",
|
||||
"btn_back": "Back",
|
||||
"btn_close": "Close",
|
||||
"like_singl": "like",
|
||||
"just_likes": "likes",
|
||||
"just_dislikes": "dislikes",
|
||||
"devtop_desc": "Your rank in developer leaderboard",
|
||||
"topmod_desc": "Your most popular module and its rank",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль статистики разработчиков",
|
||||
"loading": "<b>Загрузка...</b>",
|
||||
"no_data": "<b>Не удалось получить данные. Попробуйте позже.</b>",
|
||||
"dev_header": "<b><i>Самые популярные разработчики:</i></b>\n\n",
|
||||
"devtop_not_found": "<b>Вы не были найдены.</b>",
|
||||
"topmod_not_found": "<b>Модули не найдены.</b>",
|
||||
"no_usernames": "<b>Юзернеймы не настроены.</b> Укажите в <code>.fcfg DevStats usernames @username</code>",
|
||||
"select_page": "<b>Выберите страницу:</b>",
|
||||
"btn_prev": "◄",
|
||||
"btn_next": "►",
|
||||
"btn_back": "Назад",
|
||||
"btn_close": "Закрыть",
|
||||
"like_singl": "Лайк",
|
||||
"just_likes": "Лайков",
|
||||
"just_dislikes": "Дизлайков",
|
||||
"devtop_desc": "Ваше место в рейтинге разработчиков",
|
||||
"topmod_desc": "Ваш самый популярный модуль и его место в топе",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"provider",
|
||||
"multi",
|
||||
"Data source: multi (fheta + vector combined) | fheta | vector",
|
||||
validator=loader.validators.Choice(["multi", "fheta", "vector"]),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"display_mode",
|
||||
"likes",
|
||||
"Display mode: likes | both",
|
||||
validator=loader.validators.Choice(["likes", "both"]),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"usernames",
|
||||
[],
|
||||
"Your usernames with @ for placeholders",
|
||||
validator=loader.validators.Series(loader.validators.String()),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"excluded_authors",
|
||||
["unknown"],
|
||||
"Authors to exclude from leaderboard",
|
||||
validator=loader.validators.Series(loader.validators.String()),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"rank1_emoji",
|
||||
"<tg-emoji emoji-id=5429387335626145566>👑</tg-emoji>",
|
||||
"Emoji for rank №1",
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"rank2_emoji",
|
||||
"<tg-emoji emoji-id=5429351167706547656>🌟</tg-emoji>",
|
||||
"Emoji for rank №2",
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"rank3_emoji",
|
||||
"<tg-emoji emoji-id=5429365839314830135>✨</tg-emoji>",
|
||||
"Emoji for rank №3",
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
utils.register_placeholder("devtop", self.placeholder_devtop, self.strings("devtop_desc"))
|
||||
utils.register_placeholder("topmod", self.placeholder_topmod, self.strings("topmod_desc"))
|
||||
|
||||
async def request_api(self, url: str, token: str = None):
|
||||
headers = {"Authorization": token} if token else {}
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=aiohttp.ClientTimeout(total=15),
|
||||
) as resp:
|
||||
return await resp.json() if resp.status == 200 else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def aggregate_devs(self, data: dict) -> list:
|
||||
excluded = {u.lower() for u in self.config["excluded_authors"]}
|
||||
devs = {}
|
||||
items = data.items() if isinstance(data, dict) else (
|
||||
(e.get("url", i), e) for i, e in enumerate(data)
|
||||
)
|
||||
for _, info in items:
|
||||
author = info.get("author", "").lstrip("@")
|
||||
if not author or author.lower() in excluded:
|
||||
continue
|
||||
if author not in devs:
|
||||
devs[author] = {"likes": 0, "dislikes": 0}
|
||||
devs[author]["likes"] += int(info.get("likes", 0) or 0)
|
||||
devs[author]["dislikes"] += int(info.get("dislikes", 0) or 0)
|
||||
return sorted(devs.items(), key=lambda x: x[1]["likes"], reverse=True)
|
||||
|
||||
def aggregate_vector(self, data: list) -> list:
|
||||
excluded = {u.lower() for u in self.config["excluded_authors"]}
|
||||
devs = {}
|
||||
for entry in data:
|
||||
author = entry.get("author", "").lstrip("@")
|
||||
if not author or author.lower() in excluded:
|
||||
continue
|
||||
if author not in devs:
|
||||
devs[author] = {"likes": 0, "dislikes": 0}
|
||||
devs[author]["likes"] += int(entry.get("likes", 0) or 0)
|
||||
devs[author]["dislikes"] += int(entry.get("dislikes", 0) or 0)
|
||||
return sorted(devs.items(), key=lambda x: x[1]["likes"], reverse=True)
|
||||
|
||||
def merge_sources(self, fheta_devs: list, vector_devs: list) -> list:
|
||||
merged = {}
|
||||
for username, stats in fheta_devs:
|
||||
merged[username.lower()] = {"name": username, "likes": stats["likes"], "dislikes": stats["dislikes"]}
|
||||
for username, stats in vector_devs:
|
||||
key = username.lower()
|
||||
if key in merged:
|
||||
merged[key]["likes"] += stats["likes"]
|
||||
merged[key]["dislikes"] += stats["dislikes"]
|
||||
else:
|
||||
merged[key] = {"name": username, "likes": stats["likes"], "dislikes": stats["dislikes"]}
|
||||
result = [(v["name"], {"likes": v["likes"], "dislikes": v["dislikes"]}) for v in merged.values()]
|
||||
return sorted(result, key=lambda x: x[1]["likes"], reverse=True)
|
||||
|
||||
async def fetch_sorted_devs(self) -> list:
|
||||
provider = self.config["provider"]
|
||||
if provider == "fheta":
|
||||
data = await self.request_api(FHETA_URL)
|
||||
return self.aggregate_devs(data) if data else []
|
||||
if provider == "vector":
|
||||
data = await self.request_api(VECTOR_URL)
|
||||
return self.aggregate_vector(data) if isinstance(data, list) else []
|
||||
# multi
|
||||
fheta_data, vector_data = await asyncio.gather(
|
||||
self.request_api(FHETA_URL),
|
||||
self.request_api(VECTOR_URL),
|
||||
)
|
||||
fheta_devs = self.aggregate_devs(fheta_data) if fheta_data else []
|
||||
vector_devs = self.aggregate_vector(vector_data) if isinstance(vector_data, list) else []
|
||||
if not fheta_devs and not vector_devs:
|
||||
return []
|
||||
return self.merge_sources(fheta_devs, vector_devs)
|
||||
|
||||
def extract_module_name(self, key: str) -> str:
|
||||
return key.strip().split("/")[-1].removesuffix(".py")
|
||||
|
||||
def format_stats(self, likes: int, dislikes: int) -> str:
|
||||
mode = self.config["display_mode"]
|
||||
lw = self.strings["like_singl"] if likes == 1 else self.strings["just_likes"]
|
||||
if mode == "both":
|
||||
return f"({likes} {lw} | {dislikes} {self.strings['just_dislikes']})"
|
||||
return f"({likes} {lw})"
|
||||
|
||||
def dev_entry(self, rank: int, username: str, likes: int, dislikes: int) -> str:
|
||||
stats = self.format_stats(likes, dislikes)
|
||||
emoji = self.config[f"rank{rank}_emoji"] if rank <= 3 else ""
|
||||
safe = utils.escape_html(username)
|
||||
if emoji:
|
||||
return f"{rank}. @{safe} <b>{stats} | </b>{emoji}\n"
|
||||
return f"{rank}. @{safe} <b>{stats}</b>\n"
|
||||
|
||||
def kb_dev_page(self, sorted_devs: list, page: int) -> str:
|
||||
start = page * 10
|
||||
text = self.strings["dev_header"]
|
||||
for i, (username, stats) in enumerate(sorted_devs[start:start + 10]):
|
||||
rank = start + i + 1
|
||||
text += self.dev_entry(rank, username, stats["likes"], stats["dislikes"])
|
||||
return text
|
||||
|
||||
def nav_markup(self, page: int, total: int, on_prev, on_next, on_page) -> list:
|
||||
return [
|
||||
[
|
||||
{"text": self.strings["btn_prev"], "callback": on_prev},
|
||||
{"text": f"{page + 1}/{total}", "callback": on_page},
|
||||
{"text": self.strings["btn_next"], "callback": on_next},
|
||||
],
|
||||
[{"text": self.strings["btn_close"], "action": "close"}],
|
||||
]
|
||||
|
||||
def page_selector_markup(self, total: int, page_cb_factory, on_back) -> list:
|
||||
buttons, row = [], []
|
||||
for p in range(total):
|
||||
row.append({"text": str(p + 1), "callback": page_cb_factory(p)})
|
||||
if len(row) == 5:
|
||||
buttons.append(row)
|
||||
row = []
|
||||
if row:
|
||||
buttons.append(row)
|
||||
buttons.append([{"text": self.strings["btn_back"], "callback": on_back}])
|
||||
return buttons
|
||||
|
||||
async def placeholder_devtop(self) -> str:
|
||||
usernames = {u.lstrip("@").lower() for u in self.config["usernames"]}
|
||||
if not usernames:
|
||||
return self.strings["no_usernames"]
|
||||
sorted_devs = await self.fetch_sorted_devs()
|
||||
if not sorted_devs:
|
||||
return self.strings["no_data"]
|
||||
for rank, (username, _) in enumerate(sorted_devs, 1):
|
||||
if username.lower() in usernames:
|
||||
return f"{rank}"
|
||||
return self.strings["devtop_not_found"]
|
||||
|
||||
async def placeholder_topmod(self) -> str:
|
||||
usernames = {u.lstrip("@").lower() for u in self.config["usernames"]}
|
||||
if not usernames:
|
||||
return self.strings["no_usernames"]
|
||||
|
||||
provider = self.config["provider"]
|
||||
joined_usernames = ",".join(sorted(usernames))
|
||||
|
||||
if provider in {"vector", "multi"}:
|
||||
data = await self.request_api(f"{VECTOR_TOPMOD_URL}{joined_usernames}")
|
||||
if isinstance(data, dict) and data.get("name") and data.get("rank"):
|
||||
return f"{data['name']} ({data['rank']})"
|
||||
if provider == "vector":
|
||||
return self.strings["topmod_not_found"] if data else self.strings["no_data"]
|
||||
|
||||
data = await self.request_api(FHETA_URL)
|
||||
if not data:
|
||||
return self.strings["no_data"]
|
||||
all_sorted = sorted(
|
||||
[(self.extract_module_name(k), v) for k, v in data.items()],
|
||||
key=lambda x: int(x[1].get("likes", 0) or 0),
|
||||
reverse=True,
|
||||
)
|
||||
user_mods = [
|
||||
(name, val)
|
||||
for name, val in all_sorted
|
||||
if val.get("author", "").lstrip("@").lower() in usernames
|
||||
]
|
||||
if not user_mods:
|
||||
return self.strings["topmod_not_found"]
|
||||
top_name = user_mods[0][0]
|
||||
global_rank = next(
|
||||
(i + 1 for i, (name, _) in enumerate(all_sorted) if name == top_name),
|
||||
None,
|
||||
)
|
||||
return (
|
||||
f"{top_name} ({global_rank})"
|
||||
if global_rank
|
||||
else self.strings["topmod_not_found"]
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="Статистика топ разработчиков")
|
||||
async def devstats(self, message: Message):
|
||||
"""Top Developers statistics"""
|
||||
await utils.answer(message, self.strings["loading"])
|
||||
sorted_devs = await self.fetch_sorted_devs()
|
||||
if not sorted_devs:
|
||||
return await utils.answer(message, self.strings["no_data"])
|
||||
total_pages = max(1, math.ceil(len(sorted_devs) / 10))
|
||||
state = {"page": 0}
|
||||
|
||||
def markup():
|
||||
return self.nav_markup(state["page"], total_pages, on_prev, on_next, on_page)
|
||||
|
||||
def render():
|
||||
return self.kb_dev_page(sorted_devs, state["page"])
|
||||
|
||||
async def on_prev(call: InlineCall):
|
||||
state["page"] = max(0, state["page"] - 1)
|
||||
await call.edit(render(), reply_markup=markup())
|
||||
|
||||
async def on_next(call: InlineCall):
|
||||
state["page"] = min(total_pages - 1, state["page"] + 1)
|
||||
await call.edit(render(), reply_markup=markup())
|
||||
|
||||
async def on_page(call: InlineCall):
|
||||
await call.edit(
|
||||
self.strings["select_page"],
|
||||
reply_markup=self.page_selector_markup(total_pages, make_page_cb, on_back),
|
||||
)
|
||||
|
||||
def make_page_cb(p):
|
||||
async def go(call: InlineCall):
|
||||
state["page"] = p
|
||||
await call.edit(render(), reply_markup=markup())
|
||||
return go
|
||||
|
||||
async def on_back(call: InlineCall):
|
||||
await call.edit(render(), reply_markup=markup())
|
||||
|
||||
await utils.answer(message, render(), reply_markup=markup())
|
||||
168
SunnexGB/Heroku-Modules/ForkCircles.py
Normal file
@@ -0,0 +1,168 @@
|
||||
# requires: python-ffmpeg
|
||||
# meta developer: @SunnexGB
|
||||
# meta pic: https://r2.fakecrime.bio/uploads/ef6d3ed1-6378-4bc4-aaad-d2bdeeaa4bbd.jpg
|
||||
# meta banner: https://r2.fakecrime.bio/uploads/ef6d3ed1-6378-4bc4-aaad-d2bdeeaa4bbd.jpg
|
||||
|
||||
# Note
|
||||
# This is a fork module from @KeyZenD.
|
||||
# Here is a link to the original module: https://github.com/KeyZenD/modules/blob/master/Circles.py
|
||||
|
||||
from .. import loader, utils
|
||||
from PIL import Image, ImageDraw, ImageOps, ImageFilter
|
||||
import io
|
||||
from telethon.tl.types import DocumentAttributeFilename
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
|
||||
@loader.tds
|
||||
class ForkCircles(loader.Module):
|
||||
"""rounds everything - reply to message"""
|
||||
strings = {
|
||||
"name": "ForkCircles",
|
||||
"processing_image": "<b>Processing image</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"processing_video": "<b>Processing video</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"no_reply": "<b><emoji document_id=5260249440450520061>🤚</emoji>|reply to image/sticker or video/gif!</b>",
|
||||
"download": "<b>downloading</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"ffprobe_failed": "<b><emoji document_id=5260249440450520061>🤚</emoji>|error`ffmpeg is installed?</b>",
|
||||
"ffmpeg_failed": "<b><emoji document_id=5260249440450520061>🤚</emoji>|ffmpeg error`:</b> {error}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Округляет всё - ответом на сообщение",
|
||||
"processing_image": "<b>Обработка изображения</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"processing_video": "<b>Обработка видео</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"no_reply": "<b><emoji document_id=5260249440450520061>🤚</emoji>|ответьте на изображение/стикер или видео/gif!</b>",
|
||||
"download": "<b>Скачивание</b><emoji document_id=5427181942934088912>💬</emoji>",
|
||||
"ffprobe_failed": "<b><emoji document_id=5260249440450520061>🤚</emoji>|еррорь ffmpeg установил?</b>",
|
||||
"ffmpeg_failed": "<b><emoji document_id=5260249440450520061>🤚</emoji>|ffmpeg еррорь:</b> {error}",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.name = self.strings['name']
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
|
||||
@loader.sudo
|
||||
async def roundcmd(self, message):
|
||||
"""<Reply to image/sticker or video/gif>"""
|
||||
reply = None
|
||||
if message.is_reply:
|
||||
reply = await message.get_reply_message()
|
||||
data = await check_media(reply)
|
||||
if isinstance(data, bool):
|
||||
await utils.answer(message, self.strings['no_reply'])
|
||||
return
|
||||
else:
|
||||
await utils.answer(message, self.strings['no_reply'])
|
||||
return
|
||||
data, type = data
|
||||
if type == "img":
|
||||
await message.edit(self.strings['processing_image'])
|
||||
img = io.BytesIO()
|
||||
bytes = await message.client.download_file(data, img)
|
||||
im = Image.open(img)
|
||||
w, h = im.size
|
||||
img = Image.new("RGBA", (w,h), (0,0,0,0))
|
||||
img.paste(im, (0, 0))
|
||||
m = min(w, h)
|
||||
img = img.crop(((w-m)//2, (h-m)//2, (w+m)//2, (h+m)//2))
|
||||
w, h = img.size
|
||||
mask = Image.new('L', (w, h), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.ellipse((10, 10, w-10, h-10), fill=255)
|
||||
mask = mask.filter(ImageFilter.GaussianBlur(2))
|
||||
img = ImageOps.fit(img, (w, h))
|
||||
img.putalpha(mask)
|
||||
im = io.BytesIO()
|
||||
im.name = "img.webp"
|
||||
img.save(im)
|
||||
im.seek(0)
|
||||
await message.client.send_file(message.to_id, im, reply_to=reply)
|
||||
else:
|
||||
await message.edit(self.strings['processing_video'])
|
||||
await message.client.download_file(data, "video.mp4")
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
|
||||
'-show_entries', 'stream=width,height', '-of', 'json', 'video.mp4'
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
return
|
||||
info = json.loads(proc.stdout or '{}')
|
||||
streams = info.get('streams', [])
|
||||
if not streams:
|
||||
return
|
||||
w = int(streams[0].get('width', 0))
|
||||
h = int(streams[0].get('height', 0))
|
||||
m = min(w, h)
|
||||
x = (w - m) // 2
|
||||
y = (h - m) // 2
|
||||
await message.edit(self.strings['download'])
|
||||
crop_filter = f"crop={m}:{m}:{x}:{y}"
|
||||
is_gif = getattr(reply, 'gif', False) or False
|
||||
if is_gif:
|
||||
cmd = [
|
||||
'ffmpeg', '-y', '-i', 'video.mp4',
|
||||
'-vf', crop_filter,
|
||||
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23',
|
||||
'-pix_fmt', 'yuv420p', '-an',
|
||||
'result.mp4'
|
||||
]
|
||||
else:
|
||||
cmd = [
|
||||
'ffmpeg', '-y', '-i', 'video.mp4',
|
||||
'-vf', crop_filter,
|
||||
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23',
|
||||
'-c:a', 'aac', '-strict', '-2',
|
||||
'result.mp4'
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
err = proc.stderr or ''
|
||||
lines = [l for l in err.splitlines() if l.strip()]
|
||||
filtered = []
|
||||
for l in lines:
|
||||
low = l.lower()
|
||||
if low.startswith('ffmpeg version') or low.startswith('built with') or low.startswith('configuration:'):
|
||||
continue
|
||||
filtered.append(l)
|
||||
if not filtered:
|
||||
safe = err[:300]
|
||||
else:
|
||||
safe = '\n'.join(filtered[-6:])
|
||||
await utils.answer(message, self.strings['ffmpeg_failed'].format(error=safe))
|
||||
return
|
||||
await message.client.send_file(message.to_id, 'result.mp4', video_note=(not is_gif), reply_to=reply)
|
||||
finally:
|
||||
if os.path.exists('video.mp4'):
|
||||
os.remove('video.mp4')
|
||||
if os.path.exists('result.mp4'):
|
||||
os.remove('result.mp4')
|
||||
await message.delete()
|
||||
|
||||
|
||||
async def check_media(reply):
|
||||
type = "img"
|
||||
if reply and reply.media:
|
||||
if reply.photo:
|
||||
data = reply.photo
|
||||
elif reply.document:
|
||||
if DocumentAttributeFilename(file_name='AnimatedSticker.tgs') in reply.media.document.attributes:
|
||||
return False
|
||||
if reply.gif or reply.video:
|
||||
type = "vid"
|
||||
if reply.audio or reply.voice:
|
||||
return False
|
||||
data = reply.media.document
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
if not data or data is None:
|
||||
return False
|
||||
else:
|
||||
return (data, type)
|
||||
171
SunnexGB/Heroku-Modules/Hangman.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# meta developer: @H_SunMods
|
||||
# meta pic: https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png
|
||||
# meta banner: https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png
|
||||
# meta fhsdesc: Game, Игра, Hangman, Висилица
|
||||
# крутой баннер да?
|
||||
|
||||
#current version
|
||||
__version__ = ("d", "i", "e")
|
||||
|
||||
import random
|
||||
import aiohttp
|
||||
from .. import loader, utils
|
||||
from herokutl.types import Message
|
||||
from ..types import InlineCall
|
||||
|
||||
words = "https://github.com/SunnexGB/Heroku-Modules/raw/refs/heads/main/Assets/Hangman/words.txt"
|
||||
|
||||
@loader.tds
|
||||
class Hangman(loader.Module):
|
||||
"""Висилица"""
|
||||
|
||||
strings = {
|
||||
"name": "Hangman",
|
||||
"caption": "<b>{word}</b>",
|
||||
"won": "<b>{word}</b>",
|
||||
"over": "Игра окончена. Слово было: <b>{word}</b>",
|
||||
"already": "<b>Сосиски свои ебаные убрал от этой буквы!</b>",
|
||||
}
|
||||
|
||||
HangmanLives = [
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/full_hp.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/1.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/2.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/3.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/4.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/5.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/6.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/7.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/8.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/9.png",
|
||||
"https://raw.githubusercontent.com/SunnexGB/Heroku-Modules/main/Assets/Hangman/10.png",
|
||||
]
|
||||
|
||||
async def client_ready(self):
|
||||
await self.load_words()
|
||||
|
||||
async def load_words(self):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(words) as resp:
|
||||
resp.raise_for_status()
|
||||
text = await resp.text()
|
||||
self.words = [
|
||||
word.strip().upper()
|
||||
for word in text.splitlines()
|
||||
if word.strip()
|
||||
]
|
||||
except Exception:
|
||||
self.words = ["СЛЕНДЕРМЕН", "КАЗИНО", "АЗАРТ"]
|
||||
|
||||
def field_w_letters(self, word, guessed):
|
||||
return " ".join(l if l in guessed else "_" for l in word)
|
||||
|
||||
def caption(self, state):
|
||||
return self.strings["caption"].format(
|
||||
word=self.field_w_letters(state["word"], state["guessed"]),
|
||||
)
|
||||
|
||||
def russian_latters(self, state, chat_id):
|
||||
guessed = state["guessed"]
|
||||
wrong = state["wrong"]
|
||||
return [
|
||||
[
|
||||
{"text": "А", "callback": self.on_letter, "args": (chat_id, "А"), **({"style": "success"} if "А" in guessed else {"style": "danger"} if "А" in wrong else {})},
|
||||
{"text": "Б", "callback": self.on_letter, "args": (chat_id, "Б"), **({"style": "success"} if "Б" in guessed else {"style": "danger"} if "Б" in wrong else {})},
|
||||
{"text": "В", "callback": self.on_letter, "args": (chat_id, "В"), **({"style": "success"} if "В" in guessed else {"style": "danger"} if "В" in wrong else {})},
|
||||
{"text": "Г", "callback": self.on_letter, "args": (chat_id, "Г"), **({"style": "success"} if "Г" in guessed else {"style": "danger"} if "Г" in wrong else {})},
|
||||
{"text": "Д", "callback": self.on_letter, "args": (chat_id, "Д"), **({"style": "success"} if "Д" in guessed else {"style": "danger"} if "Д" in wrong else {})},
|
||||
{"text": "Е", "callback": self.on_letter, "args": (chat_id, "Е"), **({"style": "success"} if "Е" in guessed else {"style": "danger"} if "Е" in wrong else {})},
|
||||
{"text": "Ё", "callback": self.on_letter, "args": (chat_id, "Ё"), **({"style": "success"} if "Ё" in guessed else {"style": "danger"} if "Ё" in wrong else {})},
|
||||
{"text": "Ж", "callback": self.on_letter, "args": (chat_id, "Ж"), **({"style": "success"} if "Ж" in guessed else {"style": "danger"} if "Ж" in wrong else {})},
|
||||
],
|
||||
[
|
||||
{"text": "З", "callback": self.on_letter, "args": (chat_id, "З"), **({"style": "success"} if "З" in guessed else {"style": "danger"} if "З" in wrong else {})},
|
||||
{"text": "И", "callback": self.on_letter, "args": (chat_id, "И"), **({"style": "success"} if "И" in guessed else {"style": "danger"} if "И" in wrong else {})},
|
||||
{"text": "Й", "callback": self.on_letter, "args": (chat_id, "Й"), **({"style": "success"} if "Й" in guessed else {"style": "danger"} if "Й" in wrong else {})},
|
||||
{"text": "К", "callback": self.on_letter, "args": (chat_id, "К"), **({"style": "success"} if "К" in guessed else {"style": "danger"} if "К" in wrong else {})},
|
||||
{"text": "Л", "callback": self.on_letter, "args": (chat_id, "Л"), **({"style": "success"} if "Л" in guessed else {"style": "danger"} if "Л" in wrong else {})},
|
||||
{"text": "М", "callback": self.on_letter, "args": (chat_id, "М"), **({"style": "success"} if "М" in guessed else {"style": "danger"} if "М" in wrong else {})},
|
||||
{"text": "Н", "callback": self.on_letter, "args": (chat_id, "Н"), **({"style": "success"} if "Н" in guessed else {"style": "danger"} if "Н" in wrong else {})},
|
||||
{"text": "О", "callback": self.on_letter, "args": (chat_id, "О"), **({"style": "success"} if "О" in guessed else {"style": "danger"} if "О" in wrong else {})},
|
||||
],
|
||||
[
|
||||
{"text": "П", "callback": self.on_letter, "args": (chat_id, "П"), **({"style": "success"} if "П" in guessed else {"style": "danger"} if "П" in wrong else {})},
|
||||
{"text": "Р", "callback": self.on_letter, "args": (chat_id, "Р"), **({"style": "success"} if "Р" in guessed else {"style": "danger"} if "Р" in wrong else {})},
|
||||
{"text": "С", "callback": self.on_letter, "args": (chat_id, "С"), **({"style": "success"} if "С" in guessed else {"style": "danger"} if "С" in wrong else {})},
|
||||
{"text": "Т", "callback": self.on_letter, "args": (chat_id, "Т"), **({"style": "success"} if "Т" in guessed else {"style": "danger"} if "Т" in wrong else {})},
|
||||
{"text": "У", "callback": self.on_letter, "args": (chat_id, "У"), **({"style": "success"} if "У" in guessed else {"style": "danger"} if "У" in wrong else {})},
|
||||
{"text": "Ф", "callback": self.on_letter, "args": (chat_id, "Ф"), **({"style": "success"} if "Ф" in guessed else {"style": "danger"} if "Ф" in wrong else {})},
|
||||
{"text": "Х", "callback": self.on_letter, "args": (chat_id, "Х"), **({"style": "success"} if "Х" in guessed else {"style": "danger"} if "Х" in wrong else {})},
|
||||
{"text": "Ц", "callback": self.on_letter, "args": (chat_id, "Ц"), **({"style": "success"} if "Ц" in guessed else {"style": "danger"} if "Ц" in wrong else {})},
|
||||
],
|
||||
[
|
||||
{"text": "Ч", "callback": self.on_letter, "args": (chat_id, "Ч"), **({"style": "success"} if "Ч" in guessed else {"style": "danger"} if "Ч" in wrong else {})},
|
||||
{"text": "Ш", "callback": self.on_letter, "args": (chat_id, "Ш"), **({"style": "success"} if "Ш" in guessed else {"style": "danger"} if "Ш" in wrong else {})},
|
||||
{"text": "Щ", "callback": self.on_letter, "args": (chat_id, "Щ"), **({"style": "success"} if "Щ" in guessed else {"style": "danger"} if "Щ" in wrong else {})},
|
||||
{"text": "Ъ", "callback": self.on_letter, "args": (chat_id, "Ъ"), **({"style": "success"} if "Ъ" in guessed else {"style": "danger"} if "Ъ" in wrong else {})},
|
||||
{"text": "Ь", "callback": self.on_letter, "args": (chat_id, "Ь"), **({"style": "success"} if "Ь" in guessed else {"style": "danger"} if "Ь" in wrong else {})},
|
||||
{"text": "Ы", "callback": self.on_letter, "args": (chat_id, "Ы"), **({"style": "success"} if "Ы" in guessed else {"style": "danger"} if "Ы" in wrong else {})},
|
||||
{"text": "Э", "callback": self.on_letter, "args": (chat_id, "Э"), **({"style": "success"} if "Э" in guessed else {"style": "danger"} if "Э" in wrong else {})},
|
||||
{"text": "Ю", "callback": self.on_letter, "args": (chat_id, "Ю"), **({"style": "success"} if "Ю" in guessed else {"style": "danger"} if "Ю" in wrong else {})},
|
||||
],
|
||||
[
|
||||
{"text": "Я", "callback": self.on_letter, "args": (chat_id, "Я"), **({"style": "success"} if "Я" in guessed else {"style": "danger"} if "Я" in wrong else {})},
|
||||
],
|
||||
]
|
||||
|
||||
async def on_letter(self, call: InlineCall, chat_id: int, letter: str):
|
||||
letter = letter.upper()
|
||||
state = self.get(f"pidor_{chat_id}", None)
|
||||
|
||||
if letter in state["guessed"] or letter in state["wrong"]:
|
||||
await call.answer(self.strings["already"], show_alert=True)
|
||||
return
|
||||
|
||||
if letter in state["word"]:
|
||||
state["guessed"].append(letter)
|
||||
self.set(f"pidor_{chat_id}", state)
|
||||
|
||||
if "_" not in self.field_w_letters(state["word"], state["guessed"]):
|
||||
self.set(f"pidor_{chat_id}", None)
|
||||
await call.edit(self.strings["won"].format(word=state["word"]))
|
||||
return
|
||||
|
||||
await call.edit(self.caption(state), reply_markup=self.russian_latters(state, chat_id))
|
||||
|
||||
else:
|
||||
state["wrong"].append(letter)
|
||||
self.set(f"pidor_{chat_id}", state)
|
||||
|
||||
wrong_count = len(state["wrong"])
|
||||
stage = min(wrong_count, len(self.HangmanLives) - 1)
|
||||
|
||||
if wrong_count >= 10:
|
||||
self.set(f"pidor_{chat_id}", None)
|
||||
await call.edit(
|
||||
self.strings["over"].format(word=state["word"]),
|
||||
photo=self.HangmanLives[stage],
|
||||
)
|
||||
return
|
||||
|
||||
await call.edit(
|
||||
self.caption(state),
|
||||
reply_markup=self.russian_latters(state, chat_id),
|
||||
photo=self.HangmanLives[stage],
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="(.oleg) Начать висилицу", alias="oleg")
|
||||
async def hangman(self, message: Message):
|
||||
"""(.oleg) Start hangman game"""
|
||||
chat_id = message.chat_id
|
||||
word = random.choice(self.words)
|
||||
state = {"word": word, "guessed": [], "wrong": []}
|
||||
self.set(f"pidor_{chat_id}", state)
|
||||
|
||||
await self.inline.form(
|
||||
message=message,
|
||||
text=self.caption(state),
|
||||
reply_markup=self.russian_latters(state, chat_id),
|
||||
photo=self.HangmanLives[0],
|
||||
)
|
||||