Compare commits

...

24 Commits

Author SHA1 Message Date
2770ae1ccc fix: now supports video / gif baners 2026-06-12 12:15:31 +03:00
Macsim
c3390a5887 Merge pull request #315 from MuRuLOSE/update-submodules_d33e49b6967d97ab3dc83dc74e03537a26defd62
Update of repositories 2026-06-12 08:37:40
2026-06-12 11:59:03 +03:00
github-actions[bot]
2ff79975c2 Updated modules.json after parse 2026-06-12 08:37:15 2026-06-12 08:37:15 +00:00
github-actions[bot]
ab03f6ed94 Added and updated repositories 2026-06-12 08:36:40 2026-06-12 08:36:40 +00:00
d33e49b696 fix 2026-06-12 11:34:55 +03:00
04cc1dc4b3 New developer 2026-06-12 11:24:33 +03:00
6b6afb7493 fix: Ported fix from stable, in python version below 3.12 you can't use " with f-strings 2026-06-08 09:46:37 +03:00
Macsim
2ed246b9ad Merge pull request #301 from MuRuLOSE/update-submodules_d279789b37a939b3d9ececce6b4d0e1992293c23
Update of repositories 2026-05-31 02:48:09
2026-06-01 00:23:40 +03:00
github-actions[bot]
837784206f Updated modules.json after parse 2026-05-31 02:47:46 2026-05-31 02:47:46 +00:00
github-actions[bot]
811beb2b74 Added and updated repositories 2026-05-31 02:47:15 2026-05-31 02:47:16 +00:00
Zahar Vanilovv
d279789b37 Merge pull request #273 from MuRuLOSE/update-submodules_63944822139e8b6869fe2250ad8c650e9db06765
Update of repositories 2026-05-03 02:11:45
2026-05-03 05:14:12 +03:00
github-actions[bot]
74dfe4caf8 Updated modules.json after parse 2026-05-03 02:11:26 2026-05-03 02:11:26 +00:00
github-actions[bot]
18b8247e21 Added and updated repositories 2026-05-03 02:10:53 2026-05-03 02:10:53 +00:00
6394482213 fix: banners now will be visible no matter what 2026-04-25 09:23:54 +03:00
43a0e578c1 fix: 1
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 21:58:27 +03:00
3112e6d4bf python-magic is shit so its replaced with filetype due to magic requires its own binary in order to work 2026-04-24 21:43:29 +03:00
18bb817239 fix: if server does not return Content-Type in header we will try determine MIME-type offline
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 21:37:39 +03:00
24f1983e2a fix: not awaited update function 2026-04-24 21:16:36 +03:00
74638c14d0 feat: now you can see version if module updated
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 20:56:58 +03:00
Macsim
db62a5aee1 Merge pull request #264 from MuRuLOSE/update-submodules_49956ba865b533dca03b344d048f96465a6be62e
Update of repositories 2026-04-24 17:42:48
2026-04-24 20:43:54 +03:00
github-actions[bot]
4d2a2899f4 Updated modules.json after parse 2026-04-24 17:42:27 2026-04-24 17:42:27 +00:00
github-actions[bot]
2a27c56d83 Added and updated repositories 2026-04-24 17:41:57 2026-04-24 17:41:57 +00:00
49956ba865 version bump 2026-04-24 20:33:45 +03:00
f0d2a28105 removed H.modules
feat: added force index update

Co-authored-by: Copilot <copilot@github.com>
2026-04-24 20:10:52 +03:00
197 changed files with 81885 additions and 92556 deletions

View File

@@ -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
}
]

View File

@@ -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}">&#8204;</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()

View File

@@ -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 [[

View File

@@ -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"])

View File

@@ -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=[]
)
)

View File

@@ -2,3 +2,4 @@ akinator
FHeta
BSR
SCD
LFSecurity

141
Limoka.py
View File

@@ -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))

View File

@@ -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,
},
{

View 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"))

View File

@@ -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>",
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
ChatCopy.py
Gemini.py
GiftFinder.py
MaillingChatGT99.py
NekoEditorMod.py

View 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)

View 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

View File

@@ -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"
}
]
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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 &lt;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>

File diff suppressed because it is too large Load Diff

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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())

View 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)

View 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],
)

Some files were not shown because too many files have changed in this diff Show More