diff --git a/Fixyres/FModules/BSR.py b/Fixyres/FModules/BSR.py index 5b32f16..004e8c4 100644 --- a/Fixyres/FModules/BSR.py +++ b/Fixyres/FModules/BSR.py @@ -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 } ] diff --git a/Fixyres/FModules/FHeta.py b/Fixyres/FModules/FHeta.py index c50e0a2..9ab280d 100644 --- a/Fixyres/FModules/FHeta.py +++ b/Fixyres/FModules/FHeta.py @@ -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"{character}{name} {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"{{{name}}} {description}".strip() + character = self.main.get_prefix() + display_name = name + + row = f"{character}{display_name} {description}".strip() extra = f"{self.main.strings[more].format(remaining=len(items) - index)}" 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')} {self.main.strings[title]}:\n
{chr(10).join(lines)}
" - + 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(" 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'' + 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')} {self.strings['list']}" 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')} {self.strings['list']}" 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')} {self.strings['noquery'].format(prefix=f'@{self.inline.bot_username} ')}", + "message": f"{self.ui.emoji('error')} {self.strings['noquery'].format(prefix=f'@{self._inline_mgr.bot_username} ')}", "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() diff --git a/Fixyres/FModules/FSecurity.py b/Fixyres/FModules/FSecurity.py index f7c8c0a..9c56f07 100644 --- a/Fixyres/FModules/FSecurity.py +++ b/Fixyres/FModules/FSecurity.py @@ -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' ({utils.escape_html(link)})' if link else "" if state == "unavailable": - return f'{self.strings("unavailable").format(link_part)}\n{self.strings("continue")}' + return f'{self.strings["unavailable"].format(link_part)}\n{self.strings["continue"]}' if state == "suspicious": - return f'{self.strings("suspicious").format(link_part)}\n
{reason}
\n{self.strings("continue")}' - return f'{self.strings("blocked").format(link_part)}\n
{reason}
' + return f'{self.strings["suspicious"].format(link_part)}\n
{reason}
\n{self.strings["continue"]}' + return f'{self.strings["blocked"].format(link_part)}\n
{reason}
' def buttons(self, task): return [[ diff --git a/Fixyres/FModules/SCD.py b/Fixyres/FModules/SCD.py index 4c0fc33..99a76c9 100644 --- a/Fixyres/FModules/SCD.py +++ b/Fixyres/FModules/SCD.py @@ -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"]) diff --git a/Fixyres/FModules/akinator.py b/Fixyres/FModules/akinator.py index 952e12b..33f3d66 100644 --- a/Fixyres/FModules/akinator.py +++ b/Fixyres/FModules/akinator.py @@ -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=[] - ) + ) diff --git a/Fixyres/FModules/full.txt b/Fixyres/FModules/full.txt index e1d67ec..d8b8a5d 100644 --- a/Fixyres/FModules/full.txt +++ b/Fixyres/FModules/full.txt @@ -2,3 +2,4 @@ akinator FHeta BSR SCD +LFSecurity diff --git a/Midga3/Heroku-modules/wordle.py b/Midga3/Heroku-modules/wordle.py new file mode 100644 index 0000000..4a75357 --- /dev/null +++ b/Midga3/Heroku-modules/wordle.py @@ -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")) \ No newline at end of file diff --git a/Ruslan-Isaev/modules/financemod.py b/Ruslan-Isaev/modules/financemod.py index f87c3bc..c0019e5 100644 --- a/Ruslan-Isaev/modules/financemod.py +++ b/Ruslan-Isaev/modules/financemod.py @@ -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": "💰", - "ETH": "💰", - "SOL": "💰", - "TON": "💰", - "USDT": "💰", - "XRP": "💰", - "USDC": "💰", - "ADA": "💰", - "DOGE": "💰", - "TRX": "💰", - "AVAX": "💰", - "LTC": "💰", - "BCH": "💰", - "ATOM": "💰", - "XLM": "💰", - "SHIB": "💰", - "UNI": "💰", - "XMR": "💰", - "LINK": "💰", - "ETC": "💰", - "SUI": "💰", - "NEAR": "💰", - "VET": "💰", - "FIL": "💰", - "XTZ": "💰", - "ALGO": "💰", + "BTC": "💰", + "ETH": "💰", + "SOL": "💰", + "TON": "💰", + "USDT": "💰", + "XRP": "💰", + "USDC": "💰", + "ADA": "💰", + "DOGE": "💰", + "TRX": "💰", + "AVAX": "💰", + "LTC": "💰", + "BCH": "💰", + "ATOM": "💰", + "XLM": "💰", + "SHIB": "💰", + "UNI": "💰", + "XMR": "💰", + "LINK": "💰", + "ETC": "💰", + "SUI": "💰", + "NEAR": "💰", + "VET": "💰", + "FIL": "💰", + "XTZ": "💰", + "ALGO": "💰", "THETA": "💰", - "FTM": "💰", - "XDAI": "💰", - "RUNE": "💰", - "DOT": "💰", + "FTM": "💰", + "XDAI": "💰", + "RUNE": "💰", + "DOT": "💰", } -_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": ( - "💵 Курс валюты с сайта ЦБ(РФ)\n" - "Актуально на {}\n\n
{}
" - ), - "valute_specific": ( - "💵 Курс валюты с сайта ЦБ(РФ)\n" - "Актуально на {}\n\n{}" - ), - "valute_not_found": "🚫 Валюта {} не найдена", - "crypto_description": "<кол-во> <код> - курс крипты\n<кол-во> - список", - "crypto_no_args": "💎 Курсы криптовалют\n\n
{}
", - "crypto_specific": "💎 Курс криптовалюты\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"💵 Курс валюты · ЦБ РФ\n" + f"Актуально на {date}\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"
{chr(10).join(lines)}
", + ) + + # Первый аргумент: число или код валюты? + 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)}") \ No newline at end of file + if code: + if code not in rates: + return await utils.answer(message, f"🚫 Валюта {code} не найдена") + 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"
{chr(10).join(lines)}
", + ) + + @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"💎 Курсы криптовалют · {self.config['crypto_currency']}\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"🚫 Крипта {symbol} не найдена") + 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"
{chr(10).join(lines)}
", + ) diff --git a/SenkoGuardian/SenModules/ChatCopy.py b/SenkoGuardian/SenModules/ChatCopy.py index d132857..abe3c48 100644 --- a/SenkoGuardian/SenModules/ChatCopy.py +++ b/SenkoGuardian/SenModules/ChatCopy.py @@ -919,7 +919,7 @@ class ChatCopy(loader.Module): idx = next((i for i, t in enumerate(self.task_queue) if t.get('tid') == tid), None) if idx is not None: self.task_queue[idx]['status'] = 'running' - self.task_queue[idx]['start_time'] = self._now() + self.task_queue[idx]['start_time'] = time.time() self.current_task_index = idx if tid: self.active_dumps[tid] = { @@ -2074,6 +2074,18 @@ class ChatCopy(loader.Module): btns = [[{"text": "🔙 К списку", "callback": self._panel_tasks}]] await call.edit(text, reply_markup=btns) + @staticmethod + def _ms(obj): + if isinstance(obj, dict): + return {k: ChatCopy._ms(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [ChatCopy._ms(v) for v in obj] + if isinstance(obj, datetime): + return obj.timestamp() + if isinstance(obj, (int, float, str, bool)) or obj is None: + return obj + return str(obj) + def _save_tasks(self): """Saves the current task queue to DB, including live progress from active_dumps.""" tasks_to_save = [] @@ -2086,7 +2098,7 @@ class ChatCopy(loader.Module): live = self.active_dumps[tid] snapshot['current'] = live.get('current', snapshot.get('current', 0)) snapshot['total_msgs'] = live.get('total_estimated', snapshot.get('total_msgs', 0)) - tasks_to_save.append(snapshot) + tasks_to_save.append(self._ms(snapshot)) self.db.set("ChatCopy", "persistent_queue", tasks_to_save) async def _action_task(self, call, tid, action): # вот эта хрень держит все что находится в панели, лучше не трогать diff --git a/SenkoGuardian/SenModules/full.txt b/SenkoGuardian/SenModules/full.txt new file mode 100644 index 0000000..1d68301 --- /dev/null +++ b/SenkoGuardian/SenModules/full.txt @@ -0,0 +1,5 @@ +ChatCopy.py +Gemini.py +GiftFinder.py +MaillingChatGT99.py +NekoEditorMod.py \ No newline at end of file diff --git a/archquise/q.mods/.gitignore b/archquise/q.mods/.gitignore index f8e3f5a..a3417d9 100644 --- a/archquise/q.mods/.gitignore +++ b/archquise/q.mods/.gitignore @@ -4,4 +4,6 @@ .ruff_cache ruff.log ruff.log.2 -ruff.toml \ No newline at end of file +ruff.toml +# Heroku files +heroku/ diff --git a/archquise/q.mods/QNotes.py b/archquise/q.mods/QNotes.py new file mode 100644 index 0000000..7f48a99 --- /dev/null +++ b/archquise/q.mods/QNotes.py @@ -0,0 +1,438 @@ +__version__ = (1, 1, 6) + +# █▀▀▄ █▀▄▀█ █▀█ █▀▄ █▀ +# ▀▀▀█ ▄ █ ▀ █ █▄█ █▄▀ ▄█ + +# #### Copyright (c) 2026 Archquise ##### + +# 💬 Contact: https://t.me/archquise +# 🔒 Licensed under the GNU AGPLv3. +# 📄 LICENSE: https://raw.githubusercontent.com/archquise/Q.Mods/main/LICENSE +# --------------------------------------------------------------------------------- +# Name: QNotes +# Description: A notes module that just works +# Author: @quise_m +# --------------------------------------------------------------------------------- +# meta developer: @quise_m +# meta banner: https://raw.githubusercontent.com/archquise/qmods_meta/main/qnotes.png +# --------------------------------------------------------------------------------- + +import asyncio +import logging +import re +from datetime import date +from typing import cast + +from herokutl.tl.functions.users import GetUsersRequest +from herokutl.tl.types import InputUserSelf + +from .. import loader, utils + +logger = logging.getLogger(__name__) + + +@loader.tds +class QNotes(loader.Module): + """A notes module that just works\nUsage: #notetag in any chat""" + + strings = { + "name": "QNotes", + "topic_desc": "Stores your notes content\nUsage: #notetag in any chat", + "wrongargs": " Wrong arguments. Check command usage.", + "not_exist": "There is no such note!", + "no_reply": "No reply! Reply to the message, which text will become a note.", + "already_exists": "Seems like note with the same tag already exists. Overwrite?", + "show_note_inline": "
#{}
\n\n
{}
", + "notelist": "Note list:", + "msg_not_found_inline": "Message with this note wasn't found. Probably, it was been removed. Note has been removed from the database.", + "remnote_inline": "🗑 Remove", + "close_inline": "❌ Close", + "yes": "✔️ Yes", + "no": "❌ No", + "true": "yes", + "false": "no", + "saved": "Note saved!", + "removed": "Note removed!", + "nonotes": "You don't have any notes!", + "privacy_switch": "Determines whose data will be used by the my_* placeholders\n\nTrue - the account that is issuing the note\nFalse - the account on which the userbot is running", + "note_prefix": "The prefix used to call up notes", + "placeholders": """ + Available placeholders: + + about the account on which userbot is installed: + {my_id} - ID + @{my_username} - username, tag + {my_phone} - phone number + {my_premium} - premium status (yes/no) + + about reply author: + {reply_id} - ID + {reply_name} - name + {reply_surname} - surname + {reply_fullname} - full name (name + surname (if specified)) + @{reply_username} - username, tag + {reply_phone} - phone number (if not hidden) + {reply_premium} - premium status (yes/no) + + general: + {today} - current date + """, + } + + strings_ru = { + "_cls_doc": "Модуль для заметок, который просто работает\nИспользование: #тегзаметки в любом чате", + "topic_desc": "Хранит содержимое ваших заметок\nИспользование: #тегзаметки в любом чате", + "wrongargs": " Неверные аргументы. Проверьте использование команды.", + "no_reply": "Нет реплая! Ответьте на сообщение, текст которого станет заметкой.", + "not_exist": "Такой заметки не найдено!", + "already_exists": "Кажется, заметка с таким тегом уже существует. Перезаписать?", + "show_note_inline": "
#{}
\n\n
{}
", + "notelist": "Список заметок:", + "msg_not_found_inline": "Сообщение с этой заметкой не было найдено. Вероятно, оно было удалено. Заметка очищена из базы данных.", + "remnote_inline": "🗑 Удалить", + "close_inline": "❌ Закрыть", + "yes": "✔️ Да", + "no": "❌ Нет", + "saved": "Заметка сохранена!", + "removed": "Заметка удалена!", + "true": "да", + "false": "нет", + "nonotes": "Нет заметок!", + "privacy_switch": "Влияет на то, чьи данные будут использовать my_* плейсхолдеры\n\nTrue - аккаунта, который вызывает заметку\nFalse - аккаунта на котором стоит юзербот", + "note_prefix": "Префикс, с которым вызываются заметки", + "placeholders": """ + Доступные плейсхолдеры: + + об аккаунте, на котором стоит юзербот: + {my_id} - айди + @{my_username} - юзернейм, тег + {my_phone} - номер телефона + {my_premium} - статус премиум (да/нет) + + об авторе реплая: + {reply_id} - айди + {reply_name} - имя + {reply_surname} - фамилия + {reply_fullname} - полное имя (имя + фамилия (если указана)) + @{reply_username} - юзернейм, тег + {reply_phone} - номер телефона (если не скрыт) + {reply_premium} - статус премиум (да/нет) + + общее: + {today} - текущая дата + """, + } + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "privacy_switch", + True, + lambda: self.strings["privacy_switch"], + validator=loader.validators.Boolean(), # type: ignore + ), + loader.ConfigValue( + "note_prefix", + "#", + lambda: self.strings["note_prefix"], + validator=loader.validators.RegExp(r"^\S+$"), # type: ignore + ), + ) + + async def client_ready(self, client, db): # type: ignore + self._content_channel_id = await utils.wait_for_content_channel(self._db) + self._notes_topic = await utils.asset_forum_topic( + client=self._client, + db=self._db, + peer=self._content_channel_id, # type: ignore + title="QNotes | Storage", + description=self.strings["topic_desc"], + icon_emoji_id=5272001961326049733, + ) + + self.my_phone = (await self._client(GetUsersRequest(id=[InputUserSelf()])))[ + 0 + ].phone + + self.placeholders = { + "my_phone": self.my_phone, + "my_username": self._client.heroku_me.username, + "my_id": self.tg_id, + "my_premium": self.strings["true"] + if self._client.heroku_me.premium + else self.strings["false"], + } + + self._notemap = cast(dict, self.pointer("notemap", default={})) + + async def _ask_overwrite(self, message): + + loop = asyncio.get_running_loop() + future = loop.create_future() + + form = await self.inline.form( + self.strings["already_exists"], + message=message, + reply_markup=[ + [ + { + "text": self.strings["yes"], + "callback": ( + lambda call, flag: ( + future.set_result(flag) if not future.done() else None + ) + ), + "args": (True,), + }, + { + "text": self.strings["no"], + "callback": ( + lambda call, flag: ( + future.set_result(flag) if not future.done() else None + ) + ), + "args": (False,), + }, + ] + ], + ) + + try: + async with asyncio.timeout(15): + overwrite_answer = await future + except TimeoutError: + await form.delete() # type: ignore + return False, message + + if not overwrite_answer: + await form.delete() # type: ignore + return False, form + + return True, form + + async def _show_note_inline(self, call, note, page=0): + async def _remnote(call, notetag, note_msg): + await note_msg.delete() + self._notemap.pop(notetag, None) + + await call.edit(self.strings["removed"]) + + note_msg = await self._client.get_messages( + self._content_channel_id, ids=note[1] + ) + + if not note_msg: + self._notemap.pop(note[0], None) + + await call.edit( + self.strings["msg_not_found_inline"], + reply_markup=[ + {"text": "⬅️ Назад", "callback": self._list_page, "args": (page,)}, + {"text": self.strings["close_inline"], "action": "close"}, + ], + ) + return + + await call.edit( + self.strings["show_note_inline"].format(note[0], note_msg.text), # type: ignore + reply_markup=[ + [ + {"text": "⬅️ Назад", "callback": self._list_page, "args": (page,)}, + { + "text": self.strings["remnote_inline"], + "callback": _remnote, + "args": (note[0], note_msg), + }, + ], + [{"text": self.strings["close_inline"], "action": "close"}], + ], + ) + + def _build_list_markup(self, page: int): + items = list(self._notemap.items()) + total = -(-len(items) // 3) + page = max(0, min(page, total - 1)) + rows = [ + [ + { + "text": notetag, + "callback": self._show_note_inline, + "args": ([notetag, msg_id], page), + } + ] + for notetag, msg_id in items[page * 3 : (page + 1) * 3] + ] + return ( + rows + + self.inline.build_pagination( + callback=self._list_page, # type: ignore + total_pages=total, + current_page=page + 1, + ) + + [[{"text": self.strings["close_inline"], "action": "close"}]] + ) + + async def _list_page(self, call, page): + await call.edit( + text=self.strings["notelist"], reply_markup=self._build_list_markup(page) + ) + + @loader.command( + ru_doc="Сохраняет заметку под тегом | Пример: .qnsave заметка", + en_doc="Saves note by tag | Example: .qnsave note", + ) + async def qnsave(self, message) -> None: + args = utils.get_args(message) + if not args: + await utils.answer(message, self.strings["wrongargs"]) + return + + current_message = message + + if not (reply := await message.get_reply_message()): + await utils.answer(message, self.strings["no_reply"]) + return + try: + if args[0].strip() in self._notemap: + need_overwrite, msg = await self._ask_overwrite(message) + if not need_overwrite: + return + old_note_message = await self._client.get_messages( + self._content_channel_id, + ids=self._notemap[args[0].strip()], + ) + old_note_message and await old_note_message.delete() # type: ignore + current_message = msg + + note_message = await self._client.send_message( + self._content_channel_id, + reply.text, + reply_to=self._notes_topic.id, + file=reply.media, + ) + self._notemap[args[0].strip()] = note_message.id + + except Exception as e: + await utils.answer(current_message, f"Произошла ошибка: {e}") + logger.exception("Произошла ошибка при сохранении заметки!") + return + await utils.answer(current_message, self.strings["saved"]) + + @loader.command( + ru_doc="Удаляет заметку по тегу | Пример: .qnrem заметка", + en_doc="Removes note by tag | Example: .qnrem note", + ) + async def qnrem(self, message) -> None: + args = utils.get_args(message) + if not args: + await utils.answer(message, self.strings["wrongargs"]) + return + + if args[0] not in self._notemap or not ( + note_message := await self._client.get_messages( + self._content_channel_id, + ids=self._notemap[args[0]], + ) + ): + await utils.answer(message, self.strings["not_exist"]) + return + + await note_message.delete() # type: ignore + self._notemap.pop(args[0], None) + + await utils.answer(message, self.strings["removed"]) + + @loader.command( + ru_doc="Выводит список всех заметок и позволяет управлять ими", + en_doc="Shows note list and allows managing them", + ) + async def qnlist(self, message) -> None: + if self._notemap: + await self.inline.form( + text=self.strings["notelist"], + reply_markup=self._build_list_markup(0), + message=message, + ) + return + await utils.answer(message, self.strings["nonotes"]) + + @loader.command( + ru_doc="Выводит список доступных плейсхолдеров", + en_doc="Displays a list of available placeholders", + ) + async def qnp(self, message) -> None: + await utils.answer(message, self.strings["placeholders"]) + + @loader.watcher() + async def _note_watcher(self, message): + if not message.text.startswith(prefix := self.config["note_prefix"]) or not ( + await self._client.dispatcher.security.check(message, self._note_watcher) + ): + return + + notetag = message.text.split(prefix, maxsplit=1)[1] + + if notetag in self._notemap: + if not ( + note_message := await self._client.get_messages( + self._content_channel_id, + ids=self._notemap[notetag], + ) + ): + self._notemap.pop(notetag, None) + return + notetext = note_message.text or "" # type: ignore + if re.search(r"\{\w+\}", notetext): + if ( + not self.config["privacy_switch"] + or message.sender_id == self._client.heroku_me.id + ): + placeholders = {**self.placeholders} + else: + message_author_entity = await self._client.get_entity( + message.sender_id + ) + placeholders = { + "my_phone": ( + await self._client(GetUsersRequest(id=[message.sender_id])) + )[0].phone, + "my_username": message_author_entity.username, + "my_id": message.sender_id, + "my_premium": self.strings["true"] + if message_author_entity.premium + else self.strings["false"], + } + + if reply_msg := await message.get_reply_message(): + reply_user = await self._client.get_entity(reply_msg.sender_id) + placeholders = { + **placeholders, + "reply_id": reply_user.id, + "reply_fullname": " ".join( + filter(None, [reply_user.first_name, reply_user.last_name]) + ), + "reply_name": reply_user.first_name, + "reply_surname": reply_user.last_name, + "reply_phone": ( + await self._client(GetUsersRequest(id=[reply_user.id])) + )[0].phone, + "reply_username": reply_user.username, + "reply_premium": self.strings["true"] + if reply_user.premium + else self.strings["false"], + } + + placeholders = placeholders | {"today": date.today()} + + def replacer(match): + key = match.group(1) + if key not in placeholders or not placeholders[key]: + return match.group(0) + return utils.escape_html(str(placeholders[key])) + + notetext = re.sub(r"\{(\w+)\}", replacer, notetext) + if media := note_message.media: # type: ignore + await utils.answer_file(message, media, notetext) # type: ignore + else: + await utils.answer(message, notetext) + return diff --git a/archquise/q.mods/face.py b/archquise/q.mods/face.py index b5c0160..0a01f9b 100644 --- a/archquise/q.mods/face.py +++ b/archquise/q.mods/face.py @@ -59,7 +59,7 @@ class FaceMod(loader.Module): en_doc="Random kaomoji", ) async def rfacecmd(self, message) -> None: # noqa: D102, ANN001 - await utils.answer(message, self.strings("loading")) + await utils.answer(message, self.strings["loading"]) url = "https://files.archquise.ru/kaomoji.txt" @@ -72,7 +72,7 @@ class FaceMod(loader.Module): kaomoji = random.choice(kaomoji_list) # noqa: S311 await utils.answer( message, - self.strings("random_face").format(kaomoji), + self.strings["random_face"].format(kaomoji), ) else: - await utils.answer(message, self.strings("error")) + await utils.answer(message, self.strings["error"]) diff --git a/archquise/q.mods/shortener.py b/archquise/q.mods/shortener.py index 6d4de4e..3b52972 100644 --- a/archquise/q.mods/shortener.py +++ b/archquise/q.mods/shortener.py @@ -113,24 +113,24 @@ class ShortenerMod(loader.Module): async def shortencmd(self, message): # noqa: ANN001, ANN201 """Shorten URL using bit.ly API.""" if self.config["token"] is None: - await utils.answer(message, self.strings("no_api")) + await utils.answer(message, self.strings["no_api"]) return args = utils.get_args_raw(message) if not args: - await utils.answer(message, self.strings("no_args")) + await utils.answer(message, self.strings["no_args"]) return if not self._validate_url(args): - await utils.answer(message, self.strings("invalid_url")) + await utils.answer(message, self.strings["invalid_url"]) return try: short_url = await self.shorten_url(url=args, token=self.config["token"]) - await utils.answer(message, self.strings("shortencmd").format(c=short_url)) + await utils.answer(message, self.strings["shortencmd"].format(c=short_url)) except Exception as e: logger.exception("Error shortening URL!") - await utils.answer(message, self.strings("api_error").format(error=str(e))) + await utils.answer(message, self.strings["api_error"].format(error=str(e))) @loader.command( ru_doc="Посмотреть статистику ссылки через bit.ly (ссылка без https:// | Доступно только на платных аккаунтах)", @@ -139,22 +139,22 @@ class ShortenerMod(loader.Module): async def statclcmd(self, message): # noqa: ANN001, ANN201 """Get click statistics for shortened URL.""" if self.config["token"] is None: - await utils.answer(message, self.strings("no_api")) + await utils.answer(message, self.strings["no_api"]) return args = utils.get_args_raw(message) if not args: - await utils.answer(message, self.strings("no_args")) + await utils.answer(message, self.strings["no_args"]) return try: if not args.startswith("bit.ly/"): - await utils.answer(message, self.strings("invalid_url")) + await utils.answer(message, self.strings["invalid_url"]) return clicks = await self.get_bitlink_stats( bitlink=args, token=self.config["token"] ) - await utils.answer(message, self.strings("statclcmd").format(c=clicks)) + await utils.answer(message, self.strings["statclcmd"].format(c=clicks)) except Exception as e: logger.exception("Error getting statistics!") - await utils.answer(message, self.strings("api_error").format(error=str(e))) + await utils.answer(message, self.strings["api_error"].format(error=str(e))) diff --git a/coddrago/modules/full.txt b/coddrago/modules/full.txt index d889594..3032c51 100644 --- a/coddrago/modules/full.txt +++ b/coddrago/modules/full.txt @@ -22,4 +22,4 @@ chatmodule stats tagwatcher hardspam -YaMusic +YaMusic \ No newline at end of file diff --git a/fiksofficial/python-modules/full.txt b/fiksofficial/python-modules/full.txt index 5855795..febee3e 100644 --- a/fiksofficial/python-modules/full.txt +++ b/fiksofficial/python-modules/full.txt @@ -27,4 +27,5 @@ github stream placeholders+ PyInstall -IwaAnimation \ No newline at end of file +IwaAnimation +lateban \ No newline at end of file diff --git a/fiksofficial/python-modules/lateban.py b/fiksofficial/python-modules/lateban.py new file mode 100644 index 0000000..1ea1e1a --- /dev/null +++ b/fiksofficial/python-modules/lateban.py @@ -0,0 +1,320 @@ +# ______ ___ ___ _ _ +# ____ | ___ \ | \/ | | | | | +# / __ \| |_/ / _| . . | ___ __| |_ _| | ___ +# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \ +# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/ +# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___| +# \____/ __/ | +# |___/ + +# На модуль распространяется лицензия "GNU General Public License v3.0" +# https://github.com/all-licenses/GNU-General-Public-License-v3.0 + +# meta developer: @pymodule + +import asyncio +import logging +from datetime import datetime, timezone + +from herokutl.tl.functions.channels import ( + EditBannedRequest, + GetParticipantsRequest, +) +from herokutl.tl.types import ( + ChatBannedRights, + ChannelParticipantsSearch, + MessageService, + MessageActionChatAddUser, + MessageActionChatJoinedByLink, + MessageActionChatJoinedByRequest, +) + +from .. import loader, utils + +logger = logging.getLogger(__name__) + +_BAN = ChatBannedRights(until_date=None, view_messages=True) + +@loader.tds +class LateBanMod(loader.Module): + """Ban all members who joined the chat after a specified date/time""" + + strings = { + "name": "LateBan", + "no_args": ( + "❌ Specify date/time:\n" + ".lateban DD.MM.YYYY\n" + ".lateban DD.MM.YYYY HH:MM\n" + ".lateban HH:MM — today" + ), + "bad_date": ( + "❌ Invalid format. Use DD.MM.YYYY, " + "DD.MM.YYYY HH:MM or HH:MM" + ), + "not_chat": "❌ Only works in supergroups", + "no_rights": "❌ No permission to ban members", + "scanning": "🔍 Scanning members who joined after {dt}...", + "confirm": ( + "⚠️ Found {count} members who joined after {dt}.\n\n" + "Confirm ban:" + ), + "btn_ban": "✅ Ban {count} members", + "btn_cancel": "❌ Cancel", + "banning": "⏳ Banning {count} members...", + "progress": "⏳ Banned {done}/{total}...", + "done": ( + "✅ Banned: {banned}\n" + "Skipped (errors/bots): {skipped}\n" + "Service messages deleted: {deleted}" + ), + "nobody": "✅ No members found who joined after {dt}.", + } + + strings_ru = { + "name": "LateBan", + "_cls_doc": "Заблокируйте всех участников, присоединившихся к чату после указанной даты/времени.", + "no_args": ( + "❌ Укажи дату/время:\n" + ".lateban DD.MM.YYYY\n" + ".lateban DD.MM.YYYY HH:MM\n" + ".lateban HH:MM" + ), + "bad_date": ( + "❌ Неверный формат. Используй DD.MM.YYYY, " + "DD.MM.YYYY HH:MM или HH:MM" + ), + "not_chat": "❌ Команда работает только в супергруппах", + "no_rights": "❌ Нет прав на бан участников", + "scanning": "🔍 Сканирую участников, вступивших после {dt}...", + "confirm": ( + "⚠️ Найдено {count} участников, вступивших после {dt}.\n\n" + "Подтверди бан:" + ), + "btn_ban": "✅ Забанить {count} участников", + "btn_cancel": "❌ Отмена", + "banning": "⏳ Баню {count} участников...", + "progress": "⏳ Забанено {done}/{total}...", + "done": ( + "✅ Забанено: {banned}\n" + "Пропущено (ошибки/боты): {skipped}\n" + "Удалено сервисных сообщений: {deleted}" + ), + "nobody": "✅ Участников, вступивших после {dt}, не найдено.", + } + + async def client_ready(self): + pass + + @loader.command(ru_doc=" - Забанить всех, кто присоединился после определённой даты/времени.") + async def latebancmd(self, message): + """ — ban all who joined after this date/time""" + args = utils.get_args_raw(message).strip() + if not args: + return await utils.answer(message, self.strings["no_args"]) + + cutoff = _parse_dt(args) + if cutoff is None: + return await utils.answer(message, self.strings["bad_date"]) + + chat = await message.get_chat() + if not getattr(chat, "megagroup", False) and not getattr(chat, "gigagroup", False): + return await utils.answer(message, self.strings["not_chat"]) + + me = await self._client.get_me() + perms = await self._client.get_permissions(chat, me) + if not getattr(perms, "ban_users", False): + return await utils.answer(message, self.strings["no_rights"]) + + dt_str = cutoff.strftime("%d.%m.%Y %H:%M") + await utils.answer(message, self.strings["scanning"].format(dt=dt_str)) + + targets = await self._collect_targets(chat, cutoff, me.id) + + if not targets: + return await utils.answer(message, self.strings["nobody"].format(dt=dt_str)) + + await self.inline.form( + message=message, + text=self.strings["confirm"].format(count=len(targets), dt=dt_str), + reply_markup=[[ + { + "text": self.strings["btn_ban"].format(count=len(targets)), + "callback": self._do_ban, + "args": (chat, targets, dt_str, cutoff), + }, + { + "text": self.strings["btn_cancel"], + "callback": self._cancel, + }, + ]], + force_me=True, + ) + + async def _collect_targets(self, chat, cutoff: datetime, my_id: int) -> list: + targets = [] + offset = 0 + limit = 200 + + while True: + res = await self._client(GetParticipantsRequest( + channel=chat, + filter=ChannelParticipantsSearch(""), + offset=offset, + limit=limit, + hash=0, + )) + if not res.users: + break + + users_map = {u.id: u for u in res.users} + + for p in res.participants: + joined = getattr(p, "date", None) + if joined is None: + continue + if joined.tzinfo is None: + joined = joined.replace(tzinfo=timezone.utc) + if joined <= cutoff: + continue + + uid = p.user_id + user = users_map.get(uid) + if not user or user.id == my_id: + continue + if getattr(user, "bot", False): + continue + if p.__class__.__name__ in ("ChannelParticipantAdmin", "ChannelParticipantCreator"): + continue + + targets.append(uid) + + if len(res.participants) < limit: + break + offset += limit + await asyncio.sleep(0.3) + + return targets + + async def _do_ban(self, call, chat, targets: list, dt_str: str, cutoff: datetime): + await call.edit(self.strings["banning"].format(count=len(targets))) + + banned = 0 + skipped = 0 + banned_ids = set() + + for i, uid in enumerate(targets, 1): + try: + await self._client(EditBannedRequest(chat, uid, _BAN)) + banned += 1 + banned_ids.add(uid) + except Exception as e: + logger.warning("LateBan: skip %s — %s", uid, e) + skipped += 1 + + if i % 10 == 0: + try: + await call.edit( + self.strings["progress"].format(done=i, total=len(targets)) + ) + except Exception: + pass + + await asyncio.sleep(0.4) + + deleted = await self._delete_join_messages(chat, banned_ids, cutoff) + + await call.edit(self.strings["done"].format( + banned=banned, skipped=skipped, deleted=deleted + )) + + async def _delete_join_messages( + self, chat, banned_ids: set, cutoff: datetime + ) -> int: + _JOIN_ACTIONS = ( + MessageActionChatAddUser, + MessageActionChatJoinedByLink, + MessageActionChatJoinedByRequest, + ) + to_delete = [] + + try: + async for msg in self._client.iter_messages( + chat, + filter=MessageService, + reverse=False, + limit=None, + offset_date=None, + ): + ts = msg.date + if ts.tzinfo is None: + ts = ts.replace(tzinfo=timezone.utc) + if ts < cutoff: + break + + action = getattr(msg, "action", None) + if not isinstance(action, _JOIN_ACTIONS): + continue + + if isinstance(action, MessageActionChatAddUser): + if any(uid in banned_ids for uid in action.users): + to_delete.append(msg.id) + else: + sender_id = getattr(msg, "from_id", None) + if sender_id is not None: + uid = getattr(sender_id, "user_id", None) + if uid in banned_ids: + to_delete.append(msg.id) + + except Exception as e: + logger.warning("LateBan: failed to scan service messages — %s", e) + return 0 + + deleted = 0 + for chunk in _chunks(to_delete, 100): + try: + await self._client.delete_messages(chat, chunk) + deleted += len(chunk) + except Exception as e: + logger.warning("LateBan: delete chunk failed — %s", e) + await asyncio.sleep(0.2) + + return deleted + + async def _cancel(self, call): + await call.delete() + +def _parse_dt(raw: str) -> datetime | None: + """ + Supported formats: + DD.MM.YYYY → 00:00 UTC + DD.MM.YYYY HH:MM → HH:MM UTC + HH:MM → today HH:MM UTC + """ + raw = raw.strip() + today = datetime.now(timezone.utc).date() + + try: + return datetime.strptime(raw, "%d.%m.%Y %H:%M").replace(tzinfo=timezone.utc) + except ValueError: + pass + + try: + return datetime.strptime(raw, "%d.%m.%Y").replace(tzinfo=timezone.utc) + except ValueError: + pass + + try: + t = datetime.strptime(raw, "%H:%M").time() + return datetime( + today.year, today.month, today.day, + t.hour, t.minute, tzinfo=timezone.utc, + ) + except ValueError: + pass + + return None + + +def _chunks(lst: list, n: int): + for i in range(0, len(lst), n): + yield lst[i:i + n] diff --git a/radiocycle/Modules/LastFm.py b/radiocycle/Modules/LastFm.py index 90ad508..9bdfc64 100644 --- a/radiocycle/Modules/LastFm.py +++ b/radiocycle/Modules/LastFm.py @@ -6,11 +6,6 @@ # |_|\_\___| |_| |_|\___/ \__,_|___/ # @ke_mods # ======================================= -# -# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) -# -------------------------------------- -# https://creativecommons.org/licenses/by-nd/4.0/legalcode -# ======================================= # meta developer: @ke_mods diff --git a/radiocycle/Modules/Neofetch.py b/radiocycle/Modules/Neofetch.py index 3301c1e..5fc11a3 100644 --- a/radiocycle/Modules/Neofetch.py +++ b/radiocycle/Modules/Neofetch.py @@ -6,11 +6,6 @@ # |_|\_\___| |_| |_|\___/ \__,_|___/ # @ke_mods # ======================================= -# -# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) -# -------------------------------------- -# https://creativecommons.org/licenses/by-nd/4.0/legalcode -# ======================================= # meta developer: @ke_mods @@ -44,5 +39,5 @@ class NeofetchMod(loader.Module): await utils.answer(message, f"
{utils.escape_html(output)}
") except FileNotFoundError: - await utils.answer(message, self.strings("not_installed")) + await utils.answer(message, self.strings["not_installed"]) diff --git a/radiocycle/Modules/PicToStories.py b/radiocycle/Modules/PicToStories.py index b244a2c..7d848b1 100644 --- a/radiocycle/Modules/PicToStories.py +++ b/radiocycle/Modules/PicToStories.py @@ -6,11 +6,6 @@ # |_|\_\___| |_| |_|\___/ \__,_|___/ # @ke_mods # ======================================= -# -# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) -# -------------------------------------- -# https://creativecommons.org/licenses/by-nd/4.0/legalcode -# ======================================= # meta developer: @ke_mods # requires: pillow @@ -95,17 +90,17 @@ class PicToStoriesMod(loader.Module): args = utils.get_args_raw(message) reply = await message.get_reply_message() if not reply or not reply.media: - await utils.answer(message, self.strings("no_rep")) + await utils.answer(message, self.strings["no_rep"]) return try: image_bytes = await reply.download_media(file=bytes) img = Image.open(io.BytesIO(image_bytes)) except Exception as e: - await utils.answer(message, self.strings("err").format(e)) + await utils.answer(message, self.strings["err"].format(e)) return - await utils.answer(message, self.strings("work")) + await utils.answer(message, self.strings["work"]) w, h = img.size curr_ratio = w / h @@ -208,4 +203,4 @@ class PicToStoriesMod(loader.Module): ) ) - await utils.answer(message, self.strings("done")) \ No newline at end of file + await utils.answer(message, self.strings["done"]) diff --git a/radiocycle/Modules/RandomAnimePic.py b/radiocycle/Modules/RandomAnimePic.py index f609d7e..e310af0 100644 --- a/radiocycle/Modules/RandomAnimePic.py +++ b/radiocycle/Modules/RandomAnimePic.py @@ -6,11 +6,6 @@ # |_|\_\___| |_| |_|\___/ \__,_|___/ # @ke_mods # ======================================= -# -# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) -# -------------------------------------- -# https://creativecommons.org/licenses/by-nd/4.0/legalcode -# ======================================= # meta developer: @ke_mods # requires: pillow @@ -55,23 +50,13 @@ class RandomAnimePicMod(loader.Module): IMAGES_API_URL = "https://api.nekosapi.com/v4/images" CATEGORIES_SCAN_LIMIT = 500 - def __init__(self): - self.config = loader.ModuleConfig( - loader.ConfigValue( - "category", - "", - "Category", - validator=loader.validators.String(), - ), - ) - @loader.command(ru_doc="- получить рандомную аниме-картинку 👀") async def rapiccmd(self, message): """- fetch random anime-pic 👀""" - await utils.answer(message, self.strings("loading")) + await utils.answer(message, self.strings["loading"]) try: - category = self.config["category"].strip() + category = await utils.get_args_raw().strip() def fetch_image(): params = {"limit": 1, "rating": ["safe"]} @@ -111,7 +96,7 @@ class RandomAnimePicMod(loader.Module): url, file = await asyncio.to_thread(fetch_image) await utils.answer( message, - self.strings("img").format(url), + self.strings["img"].format(url), file=file ) @@ -120,12 +105,12 @@ class RandomAnimePicMod(loader.Module): "Error fetching random anime pic: %s", traceback.format_exc(), ) - await utils.answer(message, self.strings("error")) + await utils.answer(message, self.strings["error"]) @loader.command(ru_doc="- получить список категорий из API 👀") async def racategoriescmd(self, message): """- fetch categories from api 👀""" - await utils.answer(message, self.strings("categories_loading")) + await utils.answer(message, self.strings["categories_loading"]) try: def fetch_categories() -> list[str]: @@ -162,15 +147,15 @@ class RandomAnimePicMod(loader.Module): categories = await asyncio.to_thread(fetch_categories) if not categories: - await utils.answer(message, self.strings("no_categories")) + await utils.answer(message, self.strings["no_categories"]) return - formatted_categories = "\n".join( + formatted_categories = ", ".join( f"{category}" for category in categories ) await utils.answer( message, - self.strings("categories").format(formatted_categories), + self.strings["categories"].format(formatted_categories), ) except Exception: @@ -178,4 +163,4 @@ class RandomAnimePicMod(loader.Module): "Error fetching categories: %s", traceback.format_exc(), ) - await utils.answer(message, self.strings("error")) \ No newline at end of file + await utils.answer(message, self.strings["error"]) diff --git a/radiocycle/Modules/SpotifyMod.py b/radiocycle/Modules/SpotifyMod.py index d1ea339..af176c6 100644 --- a/radiocycle/Modules/SpotifyMod.py +++ b/radiocycle/Modules/SpotifyMod.py @@ -16,11 +16,6 @@ # @ke_mods # ======================================= # -# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) -# -------------------------------------- -# https://creativecommons.org/licenses/by-nd/4.0/legalcode -# ======================================= -# # meta developer: @ke_mods # requires: telethon spotipy pillow requests yt-dlp curl_cffi # scope: ffmpeg @@ -39,6 +34,7 @@ import traceback import os from types import FunctionType +import random import requests import spotipy from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont, ImageOps @@ -61,7 +57,9 @@ class Banners: progress: int, track_cover: bytes, font, - blur + blur, + album_title: str = "", + meta_info: str = "", ): self.title = title self.artists = ", ".join(artists) if isinstance(artists, list) else artists @@ -70,6 +68,8 @@ class Banners: self.track_cover = track_cover self.font_url = font self.blur_intensity = blur + self.album_title = album_title + self.meta_info = meta_info def _get_font(self, size, font_bytes): return ImageFont.truetype(io.BytesIO(font_bytes), size) @@ -237,6 +237,164 @@ class Banners: by.name = "banner.png" return by + # Ultra banner from YaMusic by @codrago_m + def ultra(self) -> io.BytesIO: + WIDTH, HEIGHT = 2560, 1220 + + font_bytes = requests.get(self.font_url).content + + def get_font(size): + try: + return ImageFont.truetype(io.BytesIO(font_bytes), size) + except Exception: + return ImageFont.load_default() + + try: + original_cover = Image.open(io.BytesIO(self.track_cover)).convert("RGBA") + except Exception: + original_cover = Image.new("RGBA", (1000, 1000), "black") + + dominant_color_img = original_cover.resize((1, 1), Image.Resampling.LANCZOS) + dominant_color = dominant_color_img.getpixel((0, 0)) + + r, g, b, a = dominant_color + brightness = (r * 299 + g * 587 + b * 114) / 1000 + if brightness < 60: + r = min(255, r + 60) + g = min(255, g + 60) + b = min(255, b + 60) + dominant_color = (r, g, b, 255) + + background = original_cover.copy() + bg_w, bg_h = background.size + + target_ratio = WIDTH / HEIGHT + current_ratio = bg_w / bg_h + + if current_ratio > target_ratio: + new_w = int(bg_h * target_ratio) + offset = (bg_w - new_w) // 2 + background = background.crop((offset, 0, offset + new_w, bg_h)) + else: + new_h = int(bg_w / target_ratio) + offset = (bg_h - new_h) // 2 + background = background.crop((0, offset, bg_w, offset + new_h)) + + background = background.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS) + + if self.blur_intensity > 0: + background = background.filter(ImageFilter.GaussianBlur(radius=self.blur_intensity)) + + dark_overlay = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 180)) + background = Image.alpha_composite(background, dark_overlay) + + cover_size = 500 + cover_x = (WIDTH - cover_size) // 2 + cover_y = 160 + + glow_layer = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0)) + draw_glow = ImageDraw.Draw(glow_layer) + + glow_rect_size = 620 + g_x = (WIDTH - glow_rect_size) // 2 + g_y = cover_y + (cover_size - glow_rect_size) // 2 + + draw_glow.rounded_rectangle( + (g_x, g_y, g_x + glow_rect_size, g_y + glow_rect_size), + radius=50, + fill=dominant_color, + ) + + glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(radius=60)) + glow_layer = ImageEnhance.Brightness(glow_layer).enhance(1.4) + glow_layer = ImageEnhance.Color(glow_layer).enhance(1.2) + + background = Image.alpha_composite(background, glow_layer) + + cover_img = original_cover.resize((cover_size, cover_size), Image.Resampling.LANCZOS) + + mask = Image.new("L", (cover_size, cover_size), 0) + draw_mask = ImageDraw.Draw(mask) + draw_mask.rounded_rectangle((0, 0, cover_size, cover_size), radius=45, fill=255) + + background.paste(cover_img, (cover_x, cover_y), mask) + + draw = ImageDraw.Draw(background) + center_x = WIDTH // 2 + current_y = cover_y + cover_size + 130 + + def draw_text_shadow(text, pos, font, fill="white", anchor="ms"): + x, y = pos + draw.text((x + 2, y + 2), text, font=font, fill=(0, 0, 0, 240), anchor=anchor) + draw.text((x, y), text, font=font, fill=fill, anchor=anchor) + + font_title = get_font(100) + title_text = self.title if len(self.title) <= 30 else self.title[:30] + "..." + draw_text_shadow(title_text.upper(), (center_x, current_y), font_title) + + current_y += 85 + + font_artist = get_font(65) + artist_text = self.artists if len(self.artists) <= 45 else self.artists[:45] + "..." + draw_text_shadow(artist_text.upper(), (center_x, current_y), font_artist, fill=(255, 255, 255, 240)) + + current_y += 80 + + bar_width = 800 + font_time = get_font(40) + + bar_start_x = center_x - (bar_width // 2) + bar_end_x = center_x + (bar_width // 2) + bar_y = current_y + + total_time_str = f"{self.duration // 1000 // 60:02d}:{(self.duration // 1000) % 60:02d}" + cur_time_str = f"{self.progress // 1000 // 60:02d}:{(self.progress // 1000) % 60:02d}" + + draw_text_shadow(cur_time_str, (bar_start_x - 30, bar_y), font_time, anchor="rm") + draw_text_shadow(total_time_str, (bar_end_x + 30, bar_y), font_time, anchor="lm") + + old_state = random.getstate() + random.seed(self.title + str(self.duration)) + + num_bars = 65 + bar_spacing = bar_width / num_bars + bar_w = max(4, int(bar_spacing * 0.5)) + max_h, min_h = 50, 6 + + active_bars = int(num_bars * (self.progress / self.duration)) if self.duration > 0 else 0 + + for i in range(num_bars): + base_h = random.randint(min_h, max_h) + edge_factor = 1.0 - abs((i - num_bars / 2) / (num_bars / 2)) + h = max(min_h, int(base_h * 0.4 + max_h * edge_factor * 0.6)) + x_center = bar_start_x + i * bar_spacing + color = (255, 255, 255, 255) if i < active_bars else (80, 80, 80, 100) + draw.rounded_rectangle( + (x_center - bar_w / 2, bar_y - h / 2, x_center + bar_w / 2, bar_y + h / 2), + radius=int(bar_w / 2), + fill=color, + ) + + random.setstate(old_state) + + current_y += 80 + + if self.album_title: + font_album = get_font(50) + album_text = self.album_title if len(self.album_title) <= 50 else self.album_title[:50] + "..." + draw_text_shadow(album_text, (center_x, current_y), font_album, fill=(230, 230, 230)) + current_y += 60 + + if self.meta_info: + font_meta = get_font(40) + draw_text_shadow(self.meta_info, (center_x, current_y), font_meta, fill=(210, 210, 210)) + + by = io.BytesIO() + background.save(by, format="PNG") + by.seek(0) + by.name = "banner.png" + return by + @loader.tds class SpotifyMod(loader.Module): """Card with the currently playing track on Spotify.""" @@ -349,9 +507,6 @@ class SpotifyMod(loader.Module): " Invalid track number." " Please search first or provide a valid number from the list." ), - "device_list": ( - "📄 Available devices:\n{}" - ), "no_devices_found": ( " No devices found." ), @@ -359,10 +514,6 @@ class SpotifyMod(loader.Module): " Playback transferred to" " {}." ), - "invalid_device_id": ( - " Invalid device ID." - " Use .sdevice to see available devices." - ), "autobio": ( "🎧 Spotify autobio {}" ), @@ -379,6 +530,7 @@ class SpotifyMod(loader.Module): "playlist_created": " Playlist {} created.", "playlist_deleted": " Playlist {} deleted.", "no_playlist_name": " Please specify a playlist name.", + "device_select": "📄 Select playback device:", } strings_ru = { @@ -478,9 +630,6 @@ class SpotifyMod(loader.Module): " Некорректный номер трека." " Сначала выполните поиск или укажите правильный номер из списка." ), - "device_list": ( - "📄 Доступные устройства:\n{}" - ), "no_devices_found": ( " Устройства не найдены." ), @@ -488,10 +637,6 @@ class SpotifyMod(loader.Module): " Воспроизведение переключено на" " {}." ), - "invalid_device_id": ( - " Некорректный ID устройства." - " Используйте .sdevice , чтобы увидеть доступные устройства." - ), "autobio": ( "🎧 Обновление био" " включено {}" @@ -509,6 +654,7 @@ class SpotifyMod(loader.Module): "playlist_created": " Плейлист {} создан.", "playlist_deleted": " Плейлист {} удален.", "no_playlist_name": " Пожалуйста, укажите название плейлиста.", + "device_select": "📄 Выберите устройство для воспроизведения:", } def __init__(self): @@ -569,7 +715,7 @@ class SpotifyMod(loader.Module): "banner_version", "horizontal", lambda: "Banner version", - validator=loader.validators.Choice(["horizontal", "vertical"]), + validator=loader.validators.Choice(["horizontal", "vertical", "ultra"]), ), loader.ConfigValue( "blur_intensity", @@ -589,12 +735,11 @@ class SpotifyMod(loader.Module): try: self.sp = spotipy.Spotify(auth=access_token) + return True except Exception: self.sp = None return False - return True - async def client_ready(self, client, db): self.font_ready = asyncio.Event() @@ -628,8 +773,6 @@ class SpotifyMod(loader.Module): return await func(*args, **kwargs) except Exception as e: error_msg = str(e) - logger.error(f"Error in {func.__name__}: {error_msg}") - if "NO_ACTIVE_DEVICE" in error_msg: user_error = "No active device" elif "PREMIUM_REQUIRED" in error_msg: @@ -697,8 +840,8 @@ class SpotifyMod(loader.Module): await asyncio.sleep(getattr(e, "seconds", 30) + 1) except asyncio.CancelledError: break - except Exception as e: - logger.exception("autobio error: %s", e) + except Exception: + pass await asyncio.sleep(self.config.get("BIO_UPDATE_DELAY", 30)) @@ -754,20 +897,17 @@ class SpotifyMod(loader.Module): reply_to_id=None, ) -> bool: dl_dir = os.path.join(os.getcwd(), "spotifymod") - if not os.path.exists(dl_dir): - os.makedirs(dl_dir, exist_ok=True) + os.makedirs(dl_dir, exist_ok=True) for f in os.listdir(dl_dir): - try: + with contextlib.suppress(Exception): os.remove(os.path.join(dl_dir, f)) - except Exception: - pass - success = False if caption is None: - safe_track = utils.escape_html(track_name or "Unknown") - safe_artists = utils.escape_html(artists or "Unknown Artist") - caption = self.strings("download_success").format(safe_track, safe_artists) + caption = self.strings["download_success"].format( + utils.escape_html(track_name or "Unknown"), + utils.escape_html(artists or "Unknown Artist"), + ) async def send_text(text: str) -> bool: if target is None: @@ -789,91 +929,60 @@ class SpotifyMod(loader.Module): if target is None: return False if isinstance(target, int): - await self._client.send_file( - target, - file_path, - caption=caption, - reply_to=reply_to_id, - ) + await self._client.send_file(target, file_path, caption=caption, reply_to=reply_to_id) return True try: await utils.answer(target, caption, file=file_path) return True - except Exception: + except Exception as e: + logger.error("SpotifyMod send_file fallback: %s", e, exc_info=True) chat_id = self._get_chat_id(target) if chat_id is None: return False - await self._client.send_file( - chat_id, - file_path, - caption=caption, - reply_to=reply_to_id, - ) + await self._client.send_file(chat_id, file_path, caption=caption, reply_to=reply_to_id) return True + success = False try: squery = query.replace('"', '').replace("'", "") - cookies = self.config["cookies_path"] - - if cookies: - cmd = ( - f'{self.config["ytdlp_path"]} -x --impersonate="" --cookies {cookies} --audio-format mp3 --add-metadata ' - f'--audio-quality 0 -o "{dl_dir}/%(title)s [%(id)s].%(ext)s" ' - f'"ytsearch1:{squery}"' - ) - else: - cmd = ( - f'{self.config["ytdlp_path"]} -x --impersonate="" --audio-format mp3 --add-metadata ' - f'--audio-quality 0 -o "{dl_dir}/%(title)s [%(id)s].%(ext)s" ' - f'"ytsearch1:{squery}"' - ) + ytdlp_flags = '-x --audio-format mp3 --audio-quality 0 --add-metadata --format "bestaudio/best" --no-playlist' + cookies_flag = f"--cookies {cookies} " if cookies else "" + cmd = ( + f'{self.config["ytdlp_path"]} {ytdlp_flags} {cookies_flag}' + f'-o "{dl_dir}/%(title)s [%(id)s].%(ext)s" ' + f'"ytsearch1:{squery}"' + ) proc = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) _, stderr = await proc.communicate() - if proc.returncode and log_context: - err_text = stderr.decode(errors="ignore").strip() if stderr else "" - err_text = err_text[-400:] if err_text else "yt-dlp failed" - logger.error("Search download failed (%s): %s", log_context, err_text) + + if proc.returncode: + err_text = stderr.decode(errors="ignore").strip() if stderr else "yt-dlp failed" + logger.error("SpotifyMod: yt-dlp code %s for %r: %s", proc.returncode, log_context or query, err_text[-400:]) files = [f for f in os.listdir(dl_dir) if f.endswith(".mp3")] - if files: - first = files[0] - target_file = os.path.join(dl_dir, first) - success = await send_file(target_file) + success = await send_file(os.path.join(dl_dir, files[0])) if not success: - if log_context: - logger.error( - "Search download send failed (%s). target=%s chat_id=%s", - log_context, - type(target).__name__, - self._get_chat_id(target), - ) - await send_text(self.strings("dl_err")) + logger.error("SpotifyMod: failed to send %r (target=%s)", log_context or query, type(target).__name__) + await send_text(self.strings["dl_err"]) else: - if log_context: - logger.error("Search download produced no files (%s)", log_context) - await send_text(self.strings("snowt_failed")) + logger.error("SpotifyMod: yt-dlp produced no mp3 for %r", log_context or query) + await send_text(self.strings["snowt_failed"]) except Exception as e: - if log_context: - logger.exception("Search download error (%s)", log_context) - else: - logger.error(e) - await send_text(self.strings("dl_err")) + logger.error("Download track error (%s): %s", log_context or "no context", e, exc_info=True) + await send_text(self.strings["dl_err"]) finally: - if os.path.exists(dl_dir): - for f in os.listdir(dl_dir): - try: - os.remove(os.path.join(dl_dir, f)) - except Exception: - pass + for f in os.listdir(dl_dir): + with contextlib.suppress(Exception): + os.remove(os.path.join(dl_dir, f)) return success @@ -937,7 +1046,7 @@ class SpotifyMod(loader.Module): await call.answer() with contextlib.suppress(Exception): - await call.edit(self.strings("downloading_track").lstrip(), reply_markup=None) + await call.edit(self.strings["downloading_track"].lstrip(), reply_markup=None) target_message = getattr(call, "message", None) if reply_to_id is None: @@ -951,9 +1060,9 @@ class SpotifyMod(loader.Module): chat_id = self._get_chat_id(call) if chat_id is None and target_message is None: - logger.error("Inline download missing chat_id (%s - %s)", track_name, artists) + pass with contextlib.suppress(Exception): - await call.edit(self.strings("dl_err"), reply_markup=None) + await call.edit(self.strings["dl_err"], reply_markup=None) return target = chat_id if chat_id is not None else target_message @@ -972,14 +1081,14 @@ class SpotifyMod(loader.Module): await call.delete() else: with contextlib.suppress(Exception): - await call.edit(self.strings("dl_err"), reply_markup=None) + await call.edit(self.strings["dl_err"], reply_markup=None) async def _inline_search_tracks(self, query): if not self.get("acs_tkn", False) or not self.sp: return { "title": "Auth required", "description": "Run .sauth", - "message": self.strings("need_auth"), + "message": self.strings["need_auth"], } query_text = (query.args or "").strip() @@ -987,7 +1096,7 @@ class SpotifyMod(loader.Module): return { "title": "No query", "description": "Provide search query", - "message": self.strings("no_search_query"), + "message": self.strings["no_search_query"], } try: @@ -1001,7 +1110,7 @@ class SpotifyMod(loader.Module): return { "title": "Search error", "description": "Try again", - "message": self.strings("err").format( + "message": self.strings["err"].format( utils.escape_html(str(e)[:50]) ), } @@ -1010,7 +1119,7 @@ class SpotifyMod(loader.Module): return { "title": "No results", "description": self._short_text(query_text, limit=60), - "message": self.strings("no_tracks_found").format( + "message": self.strings["no_tracks_found"].format( utils.escape_html(query_text) ), } @@ -1029,7 +1138,7 @@ class SpotifyMod(loader.Module): { "title": self._short_text(track_name, limit=60), "description": self._short_text(artists, limit=60) if artists else "", - "message": f"{self.strings('downloading_track').lstrip()}\nspdl_{store_id}_{i}", + "message": f'{self.strings["downloading_track"].lstrip()}\nspdl_{store_id}_{i}', "thumb": thumb, } ) @@ -1056,22 +1165,22 @@ class SpotifyMod(loader.Module): """| .spla - ➕ Add current track to playlist (use number from .splaylists | .spls)""" args = utils.get_args_raw(message) if not args or not args.isdigit(): - await utils.answer(message, self.strings("invalid_playlist_index")) + await utils.answer(message, self.strings["invalid_playlist_index"]) return index = int(args) - 1 playlists = self.get("last_playlists", []) if not playlists: - await utils.answer(message, self.strings("no_cached_playlists")) + await utils.answer(message, self.strings["no_cached_playlists"]) return if index < 0 or index >= len(playlists): - await utils.answer(message, self.strings("invalid_playlist_index")) + await utils.answer(message, self.strings["invalid_playlist_index"]) return current = self.sp.current_playback() if not current or not current.get("item"): - await utils.answer(message, self.strings("no_music")) + await utils.answer(message, self.strings["no_music"]) return track_uri = current["item"]["uri"] @@ -1083,7 +1192,7 @@ class SpotifyMod(loader.Module): playlist_name = playlists[index]["name"] self.sp.playlist_add_items(playlist_id, [track_uri]) - await utils.answer(message, self.strings("added_to_playlist").format(utils.escape_html(full_track_name), utils.escape_html(playlist_name))) + await utils.answer(message, self.strings["added_to_playlist"].format(utils.escape_html(full_track_name), utils.escape_html(playlist_name))) @error_handler @tokenized @@ -1095,22 +1204,22 @@ class SpotifyMod(loader.Module): """| .splr - ➖ Remove current track from playlist (use number from .splaylists | .spls)""" args = utils.get_args_raw(message) if not args or not args.isdigit(): - await utils.answer(message, self.strings("invalid_playlist_index")) + await utils.answer(message, self.strings["invalid_playlist_index"]) return index = int(args) - 1 playlists = self.get("last_playlists", []) if not playlists: - await utils.answer(message, self.strings("no_cached_playlists")) + await utils.answer(message, self.strings["no_cached_playlists"]) return if index < 0 or index >= len(playlists): - await utils.answer(message, self.strings("invalid_playlist_index")) + await utils.answer(message, self.strings["invalid_playlist_index"]) return current = self.sp.current_playback() if not current or not current.get("item"): - await utils.answer(message, self.strings("no_music")) + await utils.answer(message, self.strings["no_music"]) return track_uri = current["item"]["uri"] @@ -1122,7 +1231,7 @@ class SpotifyMod(loader.Module): playlist_name = playlists[index]["name"] self.sp.playlist_remove_all_occurrences_of_items(playlist_id, [track_uri]) - await utils.answer(message, self.strings("removed_from_playlist").format(utils.escape_html(full_track_name), utils.escape_html(playlist_name))) + await utils.answer(message, self.strings["removed_from_playlist"].format(utils.escape_html(full_track_name), utils.escape_html(playlist_name))) @error_handler @tokenized @@ -1134,12 +1243,12 @@ class SpotifyMod(loader.Module): """| .splc - 🆕 Create a new playlist""" name = utils.get_args_raw(message) if not name: - await utils.answer(message, self.strings("no_playlist_name")) + await utils.answer(message, self.strings["no_playlist_name"]) return user_id = self.sp.me()["id"] self.sp.user_playlist_create(user_id, name) - await utils.answer(message, self.strings("playlist_created").format(utils.escape_html(name))) + await utils.answer(message, self.strings["playlist_created"].format(utils.escape_html(name))) @error_handler @tokenized @@ -1151,24 +1260,24 @@ class SpotifyMod(loader.Module): """| .spld - 🗑 Delete playlist (use number from .splaylists | .spls)""" args = utils.get_args_raw(message) if not args or not args.isdigit(): - await utils.answer(message, self.strings("invalid_playlist_index")) + await utils.answer(message, self.strings["invalid_playlist_index"]) return index = int(args) - 1 playlists = self.get("last_playlists", []) if not playlists: - await utils.answer(message, self.strings("no_cached_playlists")) + await utils.answer(message, self.strings["no_cached_playlists"]) return if index < 0 or index >= len(playlists): - await utils.answer(message, self.strings("invalid_playlist_index")) + await utils.answer(message, self.strings["invalid_playlist_index"]) return playlist_id = playlists[index]["id"] playlist_name = playlists[index]["name"] self.sp.current_user_unfollow_playlist(playlist_id) - await utils.answer(message, self.strings("playlist_deleted").format(utils.escape_html(playlist_name))) + await utils.answer(message, self.strings["playlist_deleted"].format(utils.escape_html(playlist_name))) @error_handler @tokenized @@ -1196,9 +1305,9 @@ class SpotifyMod(loader.Module): playlist_list_text += f"{i + 1}. {name} ({count} tracks)\n" if playlist_list_text == "": - await utils.answer(message, self.strings("no_playlists")) + await utils.answer(message, self.strings["no_playlists"]) else: - await utils.answer(message, self.strings("playlists_list").format(playlist_list_text)) + await utils.answer(message, self.strings["playlists_list"].format(playlist_list_text)) @error_handler @tokenized @@ -1208,7 +1317,7 @@ class SpotifyMod(loader.Module): async def sbiocmd(self, message): """- ℹ️ Toggle streaming playback in bio""" if not getattr(self, "sp", None): - await utils.answer(message, self.strings("need_auth")) + await utils.answer(message, self.strings["need_auth"]) return state = not self.get("autobio", False) @@ -1227,7 +1336,7 @@ class SpotifyMod(loader.Module): await utils.answer( message, - self.strings("autobio").format("on" if state else "off"), + self.strings["autobio"].format("on" if state else "off"), ) @error_handler @@ -1240,64 +1349,63 @@ class SpotifyMod(loader.Module): """| .sv - 🔊 Change playback volume. .svolume | .sv <0-100>""" args = utils.get_args_raw(message) if args == "": - await utils.answer(message, self.strings("no_volume_arg")) + await utils.answer(message, self.strings["no_volume_arg"]) else: try: volume_percent = int(args) if 0 <= volume_percent <= 100: self.sp.volume(volume_percent) - await utils.answer(message, self.strings("volume_changed").format(volume_percent)) + await utils.answer(message, self.strings["volume_changed"].format(volume_percent)) else: - await utils.answer(message, self.strings("volume_invalid")) + await utils.answer(message, self.strings["volume_invalid"]) except ValueError: - await utils.answer(message, self.strings("volume_invalid")) + await utils.answer(message, self.strings["volume_invalid"]) @error_handler @tokenized @loader.command( - ru_doc=( - "| .sd - 🎵 Выбрать устройство для воспроизведения. Например: .sdevice или .sdevice | .sd для вывода списка устройств" - ), + ru_doc="| .sd - 🎵 Выбрать устройство для воспроизведения", alias="sd" ) async def sdevicecmd(self, message: Message): - """| .sd - 🎵 Set preferred playback device. Usage: .sdevice or .sdevice | .sd to list devices""" - args = utils.get_args_raw(message) + """| .sd - 🎵 Select playback device""" devices = self.sp.devices()["devices"] + if not devices: + await utils.answer(message, self.strings["no_devices_found"]) + return - if args == "": - if not devices: - await utils.answer(message, self.strings("no_devices_found")) - else: - device_list_text = "" - for i, device in enumerate(devices): - is_active = "(active)" if device["is_active"] else "" - device_list_text += ( - f"{i+1}. {device['name']}" - f" ({device['type']}) {is_active}\n" - ) - await utils.answer(message, self.strings("device_list").format(device_list_text.strip())) - else: - device_id = None + async def _switch(call, device_id: str, device_name: str): + with contextlib.suppress(Exception): + await call.answer() try: - device_number = int(args) - if 0 < device_number <= len(devices): - device_id = devices[device_number - 1]["id"] - device_name = devices[device_number - 1]["name"] - else: - await utils.answer(message, self.strings("invalid_device_id")) - return - except ValueError: - found_device = next((d for d in devices if d["id"] == args.strip()), None) - if found_device: - device_id = found_device["id"] - device_name = found_device["name"] - else: - await utils.answer(message, self.strings("invalid_device_id")) - return + self.sp.transfer_playback(device_id=device_id) + with contextlib.suppress(Exception): + await call.edit( + self.strings["device_changed"].format(utils.escape_html(device_name)), + reply_markup=None, + ) + except Exception as e: + with contextlib.suppress(Exception): + await call.edit( + self.strings["err"].format(utils.escape_html(str(e)[:80])), + reply_markup=None, + ) - self.sp.transfer_playback(device_id=device_id) - await utils.answer(message, self.strings("device_changed").format(device_name)) + keyboard = [] + for device in devices: + active_mark = "> " if device["is_active"] else "" + label = f"{active_mark}{device['name']} ({device['type'].lower()})" + keyboard.append([{ + "text": label, + "callback": _switch, + "args": (device["id"], device["name"]), + }]) + + await self.inline.form( + self.strings["device_select"], + message=message, + reply_markup=keyboard, + ) @error_handler @tokenized @@ -1307,7 +1415,7 @@ class SpotifyMod(loader.Module): async def srepeatcmd(self, message: Message): """- 💫 Repeat""" self.sp.repeat("track") - await utils.answer(message, self.strings("on-repeat")) + await utils.answer(message, self.strings["on-repeat"]) @error_handler @tokenized @@ -1317,7 +1425,7 @@ class SpotifyMod(loader.Module): async def sderepeatcmd(self, message: Message): """- ✋ Stop repeat""" self.sp.repeat("context") - await utils.answer(message, self.strings("off-repeat")) + await utils.answer(message, self.strings["off-repeat"]) @error_handler @tokenized @@ -1327,7 +1435,7 @@ class SpotifyMod(loader.Module): async def snextcmd(self, message: Message): """- 👉 Next track""" self.sp.next_track() - await utils.answer(message, self.strings("skipped")) + await utils.answer(message, self.strings["skipped"]) @error_handler @tokenized @@ -1337,7 +1445,7 @@ class SpotifyMod(loader.Module): async def sresumecmd(self, message: Message): """- 🤚 Resume""" self.sp.start_playback() - await utils.answer(message, self.strings("playing")) + await utils.answer(message, self.strings["playing"]) @error_handler @tokenized @@ -1347,7 +1455,7 @@ class SpotifyMod(loader.Module): async def spausecmd(self, message: Message): """- 🤚 Pause""" self.sp.pause_playback() - await utils.answer(message, self.strings("paused")) + await utils.answer(message, self.strings["paused"]) @error_handler @tokenized @@ -1357,7 +1465,7 @@ class SpotifyMod(loader.Module): async def sbackcmd(self, message: Message): """- ⏮ Previous track""" self.sp.previous_track() - await utils.answer(message, self.strings("back")) + await utils.answer(message, self.strings["back"]) @error_handler @tokenized @@ -1367,7 +1475,7 @@ class SpotifyMod(loader.Module): async def sbegincmd(self, message: Message): """- ⏪ Restart track""" self.sp.seek_track(0) - await utils.answer(message, self.strings("restarted")) + await utils.answer(message, self.strings["restarted"]) @error_handler @tokenized @@ -1378,7 +1486,7 @@ class SpotifyMod(loader.Module): """- ❤️ Like current track""" cupl = self.sp.current_playback() self.sp.current_user_saved_tracks_add([cupl["item"]["id"]]) - await utils.answer(message, self.strings("liked")) + await utils.answer(message, self.strings["liked"]) @error_handler @tokenized @@ -1389,7 +1497,7 @@ class SpotifyMod(loader.Module): """- 💔 Unlike current track""" cupl = self.sp.current_playback() self.sp.current_user_saved_tracks_delete([cupl["item"]["id"]]) - await utils.answer(message, self.strings("unlike")) + await utils.answer(message, self.strings["unlike"]) @error_handler @loader.command( @@ -1398,12 +1506,12 @@ class SpotifyMod(loader.Module): async def sauthcmd(self, message: Message): """- Get authorization link""" if self.get("acs_tkn", False) and not self.sp: - await utils.answer(message, self.strings("already_authed")) + await utils.answer(message, self.strings["already_authed"]) else: self.sp_auth.get_authorize_url() await utils.answer( message, - self.strings("auth").format(self.sp_auth.get_authorize_url()), + self.strings["auth"].format(self.sp_auth.get_authorize_url()), ) @error_handler @@ -1416,7 +1524,7 @@ class SpotifyMod(loader.Module): code = self.sp_auth.parse_auth_response_url(url) self.set("acs_tkn", self.sp_auth.get_access_token(code, True, False)) self._init_spotify_client() - await utils.answer(message, self.strings("authed")) + await utils.answer(message, self.strings["authed"]) @error_handler @loader.command( @@ -1426,7 +1534,7 @@ class SpotifyMod(loader.Module): """- Log out of account""" self.set("acs_tkn", None) self.sp = None - await utils.answer(message, self.strings("deauth")) + await utils.answer(message, self.strings["deauth"]) @error_handler @tokenized @@ -1442,7 +1550,7 @@ class SpotifyMod(loader.Module): ) self.set("NextRefresh", time.time() + 45 * 60) self._init_spotify_client() - await utils.answer(message, self.strings("authed")) + await utils.answer(message, self.strings["authed"]) @error_handler @tokenized @@ -1454,7 +1562,7 @@ class SpotifyMod(loader.Module): """| .sn - 🎧 View current track card.""" current_playback = self.sp.current_playback() if not current_playback or not current_playback.get("is_playing", False): - await utils.answer(message, self.strings("no_music")) + await utils.answer(message, self.strings["no_music"]) return track = current_playback["item"]["name"] @@ -1515,7 +1623,7 @@ class SpotifyMod(loader.Module): if self.config["show_banner"]: cover_url = current_playback["item"]["album"]["images"][0]["url"] - tmp_msg = await utils.answer(message, text + self.strings("uploading_banner")) + tmp_msg = await utils.answer(message, text + self.strings["uploading_banner"]) banners = Banners( title=track, @@ -1525,9 +1633,14 @@ class SpotifyMod(loader.Module): track_cover=requests.get(cover_url).content, font=self.config["font"], blur=self.config["blur_intensity"], + album_title=album_name, + meta_info="Spotify", ) - - if self.config["banner_version"] == "vertical": + + version = self.config["banner_version"] + if version == "ultra": + file = banners.ultra() + elif version == "vertical": file = banners.vertical() else: file = banners.horizontal() @@ -1546,7 +1659,7 @@ class SpotifyMod(loader.Module): """| .snt - 🎧 Download current track.""" current_playback = self.sp.current_playback() if not current_playback or not current_playback.get("is_playing", False): - await utils.answer(message, self.strings("no_music")) + await utils.answer(message, self.strings["no_music"]) return track = current_playback["item"]["name"] @@ -1603,9 +1716,16 @@ class SpotifyMod(loader.Module): text = self.config["custom_text"].format(**data) - msg = await utils.answer(message, text + self.strings("downloading_track")) - - await self._download_track(msg, f"{artists} {track}", caption=text) + msg = await utils.answer(message, text + self.strings["downloading_track"]) + + await self._download_track( + msg, + f"{artists} {track}", + caption=text, + track_name=track, + artists=artists, + log_context=f"{track} - {artists}", + ) @error_handler @tokenized @@ -1617,7 +1737,7 @@ class SpotifyMod(loader.Module): """| .sq - 🔍 Search for tracks.""" args = utils.get_args_raw(message) if not args: - await utils.answer(message, self.strings("no_search_query")) + await utils.answer(message, self.strings["no_search_query"]) return search_results = self.get("last_search_results", []) @@ -1630,7 +1750,7 @@ class SpotifyMod(loader.Module): if is_selection: track_number = int(args) - msg = await utils.answer(message, self.strings("downloading_track")) + msg = await utils.answer(message, self.strings["downloading_track"]) track_info = search_results[track_number - 1] track_name, artists = self._track_info(track_info) reply_to_id = self._reply_id(message) @@ -1659,7 +1779,7 @@ class SpotifyMod(loader.Module): ) if not results or not results["tracks"]["items"]: - await utils.answer(message, self.strings("no_tracks_found").format(args)) + await utils.answer(message, self.strings["no_tracks_found"].format(args)) return tracks = results["tracks"]["items"] @@ -1668,7 +1788,7 @@ class SpotifyMod(loader.Module): reply_to_id = self._reply_id(message) await self.inline.form( - self.strings("search_results_inline").format( + self.strings["search_results_inline"].format( count=len(tracks), query=utils.escape_html(args), ), @@ -1717,17 +1837,22 @@ class SpotifyMod(loader.Module): next_refresh = self.get("NextRefresh") if not next_refresh or next_refresh < time.time(): + acs_tkn = self.get("acs_tkn") + if not acs_tkn or not acs_tkn.get("refresh_token"): + self.set("NextRefresh", time.time() + 300) + return try: - self.set( - "acs_tkn", - self.sp_auth.refresh_access_token(self.get("acs_tkn")["refresh_token"]), - ) + new_token = self.sp_auth.refresh_access_token(acs_tkn["refresh_token"]) + self.set("acs_tkn", new_token) self.set("NextRefresh", time.time() + 45 * 60) - self.sp = spotipy.Spotify(auth=self.get("acs_tkn")["access_token"]) + if new_token and new_token.get("access_token"): + self.sp = spotipy.Spotify(auth=new_token["access_token"]) + logger.debug("Token refreshed successfully") except Exception as e: - logger.error(f"Spotify watcher error: {e}") + logger.error("Token refresh error: %s", e, exc_info=True) if "Refresh token revoked" in str(e): + logger.warning("Refresh token revoked, re-authenticating") refresh_token = await self.invoke("stokrefresh", "", self.inline.bot.id) await refresh_token.delete() else: - self.set("NextRefresh", time.time() + 300) + self.set("NextRefresh", time.time() + 300) \ No newline at end of file diff --git a/radiocycle/Modules/UnbanAll.py b/radiocycle/Modules/UnbanAll.py index 922fc3d..3aa69d6 100644 --- a/radiocycle/Modules/UnbanAll.py +++ b/radiocycle/Modules/UnbanAll.py @@ -6,11 +6,6 @@ # |_|\_\___| |_| |_|\___/ \__,_|___/ # @ke_mods # ======================================= -# -# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) -# -------------------------------------- -# https://creativecommons.org/licenses/by-nd/4.0/legalcode -# ======================================= # meta developer: @ke_mods @@ -43,10 +38,10 @@ class UnbanAllMod(loader.Module): chat = await message.get_chat() if not chat.admin_rights and not chat.creator: - await utils.answer(message, self.strings("no_rights")) + await utils.answer(message, self.strings["no_rights"]) return - await utils.answer(message, self.strings("unban_in_process")) + await utils.answer(message, self.strings["unban_in_process"]) no_banned = True @@ -64,11 +59,11 @@ class UnbanAllMod(loader.Module): )) except Exception as e: - await utils.answer(message, self.strings("error_occured").format(user.id, e)) + await utils.answer(message, self.strings["error_occured"].format(user.id, e)) pass if no_banned: - await utils.answer(message, self.strings("no_banned")) + await utils.answer(message, self.strings["no_banned"]) return - await utils.answer(message, self.strings("success")) + await utils.answer(message, self.strings["success"]) diff --git a/radiocycle/Modules/voicetotext.py b/radiocycle/Modules/voicetotext.py index 0e4cfd5..c95866b 100644 --- a/radiocycle/Modules/voicetotext.py +++ b/radiocycle/Modules/voicetotext.py @@ -6,11 +6,6 @@ # |_|\_\___| |_| |_|\___/ \__,_|___/ # @ke_mods # ======================================= -# -# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) -# -------------------------------------- -# https://creativecommons.org/licenses/by-nd/4.0/legalcode -# ======================================= # meta developer: @ke_mods # scope: ffmpeg diff --git a/yummy1gay/limoka/yg_quotes.py b/yummy1gay/limoka/yg_quotes.py index 28f89ac..a9ebb39 100644 --- a/yummy1gay/limoka/yg_quotes.py +++ b/yummy1gay/limoka/yg_quotes.py @@ -1,4 +1,4 @@ -__version__ = (1, 2, 0, 0) +__version__ = (1, 3, 0, 0) # This file is a part of Hikka Userbot! # This product includes software developed by t.me/Fl1yd and t.me/spypm. @@ -19,6 +19,10 @@ __version__ = (1, 2, 0, 0) # - Added: Proxy for users from RF # - Fixed: Correct reply author resolving for forwarded messages +# Changelog v1.3: +# - Added: Message grouping for consecutive messages from the same user (hides avatar/name) +# - Changed: Replaced RU endpoint logic with direct proxy support via module config + # █▄█ █░█ █▀▄▀█ █▀▄▀█ █▄█   █▀▄▀█ █▀█ █▀▄ █▀ # ░█░ █▄█ █░▀░█ █░▀░█ ░█░   █░▀░█ █▄█ █▄▀ ▄█ @@ -155,9 +159,10 @@ class Dick: return None @staticmethod - async def post(url: str, data: dict): + async def post(url: str, data: dict, proxy: Optional[str] = None): try: - return await utils.run_sync(requests.post, url, json=data, timeout=30) + px = {"http": proxy, "https": proxy} if proxy else None + return await utils.run_sync(requests.post, url, json=data, timeout=30, proxies=px) except Exception: return None @@ -199,12 +204,8 @@ class Quotes(loader.Module): loader.ConfigValue("endpoint","https://kok.gay/gayotes/generate", lambda:"URL API-эндпоинта (можешь поднять локально - github.com/yummy1gay/quote-api)", validator=loader.validators.Link()), - loader.ConfigValue("use_rf_proxy", False, - lambda:'Включает прокси для РФ, если основной эндпоинт возвращает ошибку "Нетворк еррорь", и при этом сервер с юзерботом находится в России или ты сам сидишь в России с ограниченным доступом к зарубежным ресурсам (Termux / UserLAnd)', - validator=loader.validators.Boolean()), - loader.ConfigValue("rf_endpoint", "https://ru.kok.gay/gayotes/generate", - lambda:"URL API-эндпоинта для РФ", - validator=loader.validators.Link())) + loader.ConfigValue("proxy", "", + lambda:"Прокси для обхода блокировок (например: http://user:pass@ip:port). Оставь пустым, если не нужно.")) async def client_ready(self, client, db): self.client=client; self.db=db @@ -236,11 +237,11 @@ class Quotes(loader.Module): "format": "webp" if not doc else "png", "type": self.config["type"]} await utils.answer(st,self.strings["api_processing"]) - endpoint=self.config['rf_endpoint'] if self.config['use_rf_proxy'] else self.config['endpoint'] - r=await Dick.post(f"{endpoint}.webp",pay) + prx = self.config["proxy"] if self.config["proxy"] else None + r=await Dick.post(f"{self.config['endpoint']}.webp",pay,proxy=prx) if not r or r.status_code!=200: - try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй включить use_rf_proxy в конфиге)" - except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй включить use_rf_proxy в конфиге)" + try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй указать прокси в конфиге)" + except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй указать прокси в конфиге)" return await utils.answer(st,self.strings["api_error"].format(err)) buf=io.BytesIO(r.content); buf.name="YgQuote"+(".png" if doc else ".webp") @@ -270,11 +271,11 @@ class Quotes(loader.Module): "format": "webp","type":self.config["type"]} await utils.answer(st,self.strings["api_processing"]) - endpoint=self.config['rf_endpoint'] if self.config['use_rf_proxy'] else self.config['endpoint'] - r=await Dick.post(f"{endpoint}.webp",dickk) + prx = self.config["proxy"] if self.config["proxy"] else None + r=await Dick.post(f"{self.config['endpoint']}.webp",dickk,proxy=prx) if not r or r.status_code!=200: - try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй включить use_rf_proxy в конфиге)" - except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй включить use_rf_proxy в конфиге)" + try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй указать прокси в конфиге)" + except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй указать прокси в конфиге)" return await utils.answer(st,self.strings["api_error"].format(err)) buf=io.BytesIO(r.content); buf.name="YgQuote.webp" @@ -290,12 +291,18 @@ class Quotes(loader.Module): return None out: List[dict]=[] + prev_sender_id = None + for mm in lst: try: u=await self.who(mm) if not u: continue + current_sender_id = getattr(u,"id",0) + + is_chained = (current_sender_id == prev_sender_id) if current_sender_id else False name=telethon.utils.get_display_name(u); f,l=Dick.split(name) - ava=await Dick.ava(self.client,getattr(u,"id",0)) if getattr(u,"id",None) else None + + ava = await Dick.ava(self.client,current_sender_id) if (not is_chained and current_sender_id) else None rb=None try: @@ -315,10 +322,16 @@ class Quotes(loader.Module): txt=mm.raw_text or ""; ad=Dick.desc(mm) if ad: txt=f"{txt}\n\n{ad}" if txt else ad - item={"from":{"id":getattr(u,"id", 0),"first_name":getattr(u,"first_name","") or f,"last_name":getattr(u,"last_name","") or l, - "username":getattr(u,"username",None),"name":name,"photo":{"url":ava} if ava else {}}, - "text":txt,"entities":Dick.ents(mm.entities),"avatar":True} - + if is_chained: + item={"from":{"id":current_sender_id,"name":""}, + "text":txt,"entities":Dick.ents(mm.entities),"avatar":False} + else: + item={"from":{"id":current_sender_id,"first_name":getattr(u,"first_name","") or f,"last_name":getattr(u,"last_name","") or l, + "username":getattr(u,"username",None),"name":name,"photo":{"url":ava} if ava else {}}, + "text":txt,"entities":Dick.ents(mm.entities),"avatar":True} + + es=getattr(u,"emoji_status",None) + if getattr(es,"document_id",None): item["from"]["emoji_status"]=str(es.document_id) try: if mm.voice: a = next((a for a in mm.voice.attributes or [] @@ -327,11 +340,10 @@ class Quotes(loader.Module): except Exception: pass if med: item["voice" if "voice" in med else "media"] = med.get("voice", med) - - es=getattr(u,"emoji_status",None) - if getattr(es,"document_id",None): item["from"]["emoji_status"]=str(es.document_id) if rb: item["replyMessage"]=rb out.append(item) + + prev_sender_id = current_sender_id except Exception: continue return out @@ -378,6 +390,8 @@ class Quotes(loader.Module): return await self.fake(f"{getattr(u,'id','')} {args}", None) out: List[dict]=[] + prev_sender_id = None + for part in args.split("; "): try: rb=None @@ -388,22 +402,32 @@ class Quotes(loader.Module): if not u1: continue txt1, ents1 = html.parse(t1) if t1 else ("", []) + + current_sender_id = u1.id + is_chained = (current_sender_id == prev_sender_id) name=telethon.utils.get_display_name(u1); f,l=Dick.split(name) - ava=await Dick.ava(self.client,u1.id) + + ava = await Dick.ava(self.client,u1.id) if not is_chained else None if u2: txt2, ents2 = html.parse(t2) if t2 else ("", []) name2=telethon.utils.get_display_name(u2); ava2=await Dick.ava(self.client,u2.id) rb={"name":name2,"text":txt2,"entities":Dick.ents(ents2),"chatId":u2.id,"from":{"name":name2,"photo":{"url":ava2} if ava2 else {}}} - msg={"from":{"id":u1.id,"first_name":getattr(u1,"first_name","") or f,"last_name":getattr(u1,"last_name","") or l, - "username":getattr(u1,"username",None),"name":name,"photo":{"url":ava} if ava else {}}, - "text":txt1,"entities":Dick.ents(ents1), "avatar":True} - - es=getattr(u1,"emoji_status",None) - if getattr(es,"document_id",None): msg["from"]["emoji_status"]=str(es.document_id) + if is_chained: + msg={"from":{"id":current_sender_id,"name":""}, + "text":txt1,"entities":Dick.ents(ents1), "avatar":False} + else: + msg={"from":{"id":current_sender_id,"first_name":getattr(u1,"first_name","") or f,"last_name":getattr(u1,"last_name","") or l, + "username":getattr(u1,"username",None),"name":name,"photo":{"url":ava} if ava else {}}, + "text":txt1,"entities":Dick.ents(ents1), "avatar":True} + es=getattr(u1,"emoji_status",None) + if getattr(es,"document_id",None): msg["from"]["emoji_status"]=str(es.document_id) + if rb: msg["replyMessage"]=rb out.append(msg) + + prev_sender_id = current_sender_id except Exception: continue return out \ No newline at end of file