Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
62afd03da1 Updated modules.json after parse 2026-04-25 01:51:33 2026-04-25 01:51:33 +00:00
31 changed files with 66642 additions and 69060 deletions

View File

@@ -42,7 +42,6 @@ async def to_code(n: int) -> str:
n_shifted //= 31 n_shifted //= 31
return "X" + "".join(reversed(res)) return "X" + "".join(reversed(res))
@loader.tds @loader.tds
class BSR(loader.Module): class BSR(loader.Module):
'''Module for finding nearby game rooms in BrawlStars.''' '''Module for finding nearby game rooms in BrawlStars.'''
@@ -140,7 +139,7 @@ class BSR(loader.Module):
'''(room code/link) (previous) (next) - find rooms.''' '''(room code/link) (previous) (next) - find rooms.'''
args = utils.get_args_raw(message).split() args = utils.get_args_raw(message).split()
if not args: 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] raw_input = args[0]
before = 0 before = 0
@@ -162,13 +161,13 @@ class BSR(loader.Module):
nxt = max(0, min(nxt, 5000)) nxt = max(0, min(nxt, 5000))
if before == 0 and nxt == 0: 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) clean_tag = await extract_code(raw_input)
base_id = await to_id(clean_tag) base_id = await to_id(clean_tag)
if base_id == 0: 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) 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) kb = self.build_keyboard(base_id, before, nxt, page, total_pages, clean_tag)
@@ -206,10 +205,10 @@ class BSR(loader.Module):
blocks = [] blocks = []
if prev_list: 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: 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) res = "\n\n".join(blocks)
if not res.strip(): if not res.strip():
@@ -221,7 +220,7 @@ class BSR(loader.Module):
kb = [ kb = [
[ [
{ {
"text": self.strings["btn_target"], "text": self.strings("btn_target"),
"copy": clean_tag "copy": clean_tag
} }
] ]

View File

@@ -19,19 +19,15 @@ import re
import sys import sys
import uuid import uuid
import importlib import importlib
from contextlib import suppress
from typing import Optional, Dict, List, Union, Tuple, Any from typing import Optional, Dict, List, Union, Tuple, Any
from urllib.parse import unquote from urllib.parse import unquote
from importlib.machinery import ModuleSpec from importlib.machinery import ModuleSpec
import telethon
from .. import loader, utils from .. import loader, utils
from ..types import CoreOverwriteError from ..types import CoreOverwriteError
from herokutl.tl.functions.contacts import UnblockRequest 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: class FHetaAPI:
@@ -74,14 +70,14 @@ class FHetaAPI:
return {} return {}
except Exception: except Exception:
return {} return {}
class MInstaller: class MInstaller:
async def execute(self, plugin: 'loader.Module', url: str) -> Tuple[str, List[str]]: async def execute(self, plugin: 'loader.Module', url: str) -> Tuple[str, List[str]]:
try: try:
code = await plugin._storage.fetch(url, auth=plugin.config.get("basic_auth")) code = await plugin._storage.fetch(url, auth=plugin.config.get("basic_auth"))
except Exception: except Exception:
return "error",[] return "error", []
for step in range(5): for step in range(5):
state = await self.load(plugin, code, url, step) state = await self.load(plugin, code, url, step)
@@ -89,10 +85,10 @@ class MInstaller:
if state == "success": if state == "success":
if plugin.fully_loaded: if plugin.fully_loaded:
plugin.update_modules_in_db() plugin.update_modules_in_db()
return "success",[] return "success", []
if state == "overwrite": if state == "overwrite":
return "overwrite",[] return "overwrite", []
if isinstance(state, list): if isinstance(state, list):
return "dependency", state return "dependency", state
@@ -102,37 +98,35 @@ class MInstaller:
await asyncio.sleep(0.5) 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]]: async def load(self, plugin: 'loader.Module', code: str, origin: str, step: int) -> Union[str, List[str]]:
if step == 0: if step == 0:
try: try:
raw_pip = loader.VALID_PIP_PACKAGES.search(code) dependencies = list(filter(
if raw_pip: lambda requirement: not requirement.startswith(("-", "_", ".")),
dependencies = [ map(lambda raw: raw.strip().rstrip(','), loader.VALID_PIP_PACKAGES.search(code)[1].split())
dep.strip() for dep in raw_pip[1].replace(',', ' ').split() ))
if dep.strip() and not dep.strip().startswith(("-", "_", "."))
] if dependencies:
if not await plugin.install_requirements(dependencies):
if dependencies: return dependencies
await plugin.install_requirements(dependencies) importlib.invalidate_caches()
importlib.invalidate_caches() return "retry"
return "retry"
except Exception: except Exception:
pass pass
try: try:
raw_apt = loader.VALID_APT_PACKAGES.search(code) packages = list(filter(
if raw_apt: lambda requirement: not requirement.startswith(("-", "_", ".")),
packages = [ map(lambda raw: raw.strip().rstrip(','), loader.VALID_APT_PACKAGES.search(code)[1].split())
pkg.strip() for pkg in raw_apt[1].replace(',', ' ').split() ))
if pkg.strip() and not pkg.strip().startswith(("-", "_", "."))
] if packages:
if not await plugin.install_packages(packages):
if packages: return packages
await plugin.install_packages(packages) importlib.invalidate_caches()
importlib.invalidate_caches() return "retry"
return "retry"
except Exception: except Exception:
pass pass
@@ -179,8 +173,8 @@ class MInstaller:
finally: finally:
if instance and sys.exc_info()[0] is not None: if instance and sys.exc_info()[0] is not None:
await plugin.allmodules.unload_module(instance.__class__.__name__) with suppress(Exception):
if instance in plugin.allmodules.modules: await plugin.allmodules.unload_module(instance.__class__.__name__)
plugin.allmodules.modules.remove(instance) plugin.allmodules.modules.remove(instance)
@@ -232,17 +226,11 @@ class FHetaUI:
description = utils.escape_html(description).split('\n')[0] if description else "" description = utils.escape_html(description).split('\n')[0] if description else ""
name = utils.escape_html(item.get("name", "")) name = utils.escape_html(item.get("name", ""))
if item.get('inline'): if kind == "cmd":
character = '@' + self.main.inline.bot_username + ' ' character = '@' + self.main.inline.bot_username + ' ' if item.get('inline') else self.main.get_prefix()
display_name = name row = f"<code>{character}{name}</code> {description}".strip()
elif kind == "ph":
character = ""
display_name = f"{{{name}}}"
else: else:
character = self.main.get_prefix() row = f"<code>{{{name}}}</code> {description}".strip()
display_name = name
row = f"<code>{character}{display_name}</code> {description}".strip()
extra = f"<i>{self.main.strings[more].format(remaining=len(items) - index)}</i>" extra = f"<i>{self.main.strings[more].format(remaining=len(items) - index)}</i>"
test = "\n".join(lines + [row, extra]) test = "\n".join(lines + [row, extra])
@@ -254,7 +242,7 @@ class FHetaUI:
lines.append(row) lines.append(row)
return f"\n\n{self.emoji('command' if kind == 'cmd' else 'placeholder')} <b>{self.main.strings[title]}:</b>\n<blockquote expandable>{chr(10).join(lines)}</blockquote>" return f"\n\n{self.emoji('command' if kind == 'cmd' else 'placeholder')} <b>{self.main.strings[title]}:</b>\n<blockquote expandable>{chr(10).join(lines)}</blockquote>"
def buttons(self, link: str, stats: Dict[str, Any], index: int, modules: Optional[List[Dict[str, Any]]] = None, query: str = "") -> List[List[Dict[str, Any]]]: 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 = [] buttons = []
decoded = unquote(link.replace('%20', '___SPACE___')).replace('___SPACE___', '%20') decoded = unquote(link.replace('%20', '___SPACE___')).replace('___SPACE___', '%20')
@@ -374,7 +362,7 @@ class FHeta(loader.Module):
"counter": "{idx}/{total}", "counter": "{idx}/{total}",
"code": "Код", "code": "Код",
"success": "✔ Модуль успешно установлен!", "success": "✔ Модуль успешно установлен!",
"error": "✘ Ошибка, возможно, модуль сломан!", "error": "✘ Ошибка, возможно, модуль поломан!",
"overwrite": "✘ Ошибка, модуль пытался перезаписать встроенный модуль!", "overwrite": "✘ Ошибка, модуль пытался перезаписать встроенный модуль!",
"dependency": "✘ Ошибка установки зависимостей! {deps}", "dependency": "✘ Ошибка установки зависимостей! {deps}",
"docdevs": "Использовать только модули от официальных разработчиков Heroku при поиске?", "docdevs": "Использовать только модули от официальных разработчиков Heroku при поиске?",
@@ -428,7 +416,7 @@ class FHeta(loader.Module):
"search": "{query} сұрауы бойынша іздеу...", "search": "{query} сұрауы бойынша іздеу...",
"noquery": "Сіз іздеу сұрауын енгізбедіңіз, мысал: {prefix}fheta сіздің сұрауыңыз", "noquery": "Сіз іздеу сұрауын енгізбедіңіз, мысал: {prefix}fheta сіздің сұрауыңыз",
"notfound": "{query} сұрауы бойынша ештеңе табылмады.", "notfound": "{query} сұрауы бойынша ештеңе табылмады.",
"toolong": "Сіздің сұрауыңыз тым үлкен, оны 168 таңбаға до қысқартыңыз.", "toolong": "Сіздің сұрауыңыз тым үлкен, оны 168 таңбаға дейін қысқартыңыз.",
"added": "✔ Бағалау қосылды!", "added": "✔ Бағалау қосылды!",
"changed": "✔ Бағалау өзгертілді!", "changed": "✔ Бағалау өзгертілді!",
"deleted": "✔ Бағалау жойылды!", "deleted": "✔ Бағалау жойылды!",
@@ -465,7 +453,7 @@ class FHeta(loader.Module):
"added": "✔ Reyting qo'shildi!", "added": "✔ Reyting qo'shildi!",
"changed": "✔ Reyting o'zgartirildi!", "changed": "✔ Reyting o'zgartirildi!",
"deleted": "✔ Reyting o'chirildi!", "deleted": "✔ Reyting o'chirildi!",
"prompt": "Qidirish o'rniga so'rov kiritish.", "prompt": "Qidirish uchun so'rov kiriting.",
"hint": "Nomi, buyruq, tavsif, muallif.", "hint": "Nomi, buyruq, tavsif, muallif.",
"retry": "Boshqa so'rovni sinab ko'ring.", "retry": "Boshqa so'rovni sinab ko'ring.",
"query": "So'rov", "query": "So'rov",
@@ -477,7 +465,7 @@ class FHeta(loader.Module):
"overwrite": "✘ Xatolik, modul o'rnatilgan modulni qayta yozishga harakat qildi!", "overwrite": "✘ Xatolik, modul o'rnatilgan modulni qayta yozishga harakat qildi!",
"dependency": "✘ Bog'liqliklarni o'rnatish xatosi! {deps}", "dependency": "✘ Bog'liqliklarni o'rnatish xatosi! {deps}",
"docdevs": "Qidiruv paytida faqat rasmiy Heroku ishlab chiquvchilarining modullaridan foydalanish kerakmi?", "docdevs": "Qidiruv paytida faqat rasmiy Heroku ishlab chiquvchilarining modullaridan foydalanish kerakmi?",
"doctheme": "Emojilar uchun mavзу.", "doctheme": "Emojilar uchun mavzu.",
"channel": "Bu FHeta-dagi barcha yangilanishlari bo'lgan kanal!" "channel": "Bu FHeta-dagi barcha yangilanishlari bo'lgan kanal!"
} }
@@ -542,9 +530,9 @@ class FHeta(loader.Module):
"error": "✘ Fehler, vielleicht ist das Modul kaputt!", "error": "✘ Fehler, vielleicht ist das Modul kaputt!",
"overwrite": "✘ Fehler, Modul hat versucht, das integrierte Modul zu überschreiben!", "overwrite": "✘ Fehler, Modul hat versucht, das integrierte Modul zu überschreiben!",
"dependency": "✘ Fehler bei der Installation von Abhängigkeiten! {deps}", "dependency": "✘ Fehler bei der Installation von Abhängigkeiten! {deps}",
"docdevs": "Nur Module von offiziellen Heroku-Entwicklern bei की खोज में उपयोग करें?", "docdevs": "Nur Module von offiziellen Heroku-Entwicklern bei der Suche verwenden?",
"doctheme": "Theма для эмодзи.", "doctheme": "Thema für Emojis.",
"channel": "Dies ist der Kanal with all updates in FHeta!" "channel": "Dies ist der Kanal mit allen Updates in FHeta!"
} }
strings_jp = { strings_jp = {
@@ -572,8 +560,8 @@ class FHeta(loader.Module):
"counter": "{idx}/{total}", "counter": "{idx}/{total}",
"code": "コード", "code": "コード",
"success": "✔ モジュールが正常にインストールされました!", "success": "✔ モジュールが正常にインストールされました!",
"error": "✘ エラー, モジュールが壊れている可能性があります!", "error": "✘ エラーモジュールが壊れている可能性があります!",
"overwrite": "✘ エラー, モジュールが組み込みモジュールを上書きしようとしました!", "overwrite": "✘ エラーモジュールが組み込みモジュールを上書きしようとしました!",
"dependency": "✘ 依存関係のインストールエラー! {deps}", "dependency": "✘ 依存関係のインストールエラー! {deps}",
"docdevs": "検索時に公式Heroku開発者のモジュールのみを使用しますか", "docdevs": "検索時に公式Heroku開発者のモジュールのみを使用しますか",
"doctheme": "絵文字のテーマ。", "doctheme": "絵文字のテーマ。",
@@ -643,13 +631,13 @@ class FHeta(loader.Module):
loader.ConfigValue( loader.ConfigValue(
"only_official_developers", "only_official_developers",
False, False,
lambda: self.strings["docdevs"], lambda: self.strings("docdevs"),
validator=loader.validators.Boolean() validator=loader.validators.Boolean()
), ),
loader.ConfigValue( loader.ConfigValue(
"theme", "theme",
"default", "default",
lambda: self.strings["doctheme"], lambda: self.strings("doctheme"),
validator=loader.validators.Choice(["default", "winter", "summer", "spring", "autumn"]) validator=loader.validators.Choice(["default", "winter", "summer", "spring", "autumn"])
) )
) )
@@ -657,31 +645,13 @@ class FHeta(loader.Module):
async def on_unload(self) -> None: async def on_unload(self) -> None:
if hasattr(self, "api") and self.api.session and not self.api.session.closed: if hasattr(self, "api") and self.api.session and not self.api.session.closed:
await self.api.session.close() await self.api.session.close()
@property
def _inline_mgr(self):
if hasattr(self, "_raw_inline_cache") and self._raw_inline_cache:
return self._raw_inline_cache
am_attr = "seludomlla"[::-1]
allmodules = getattr(self, am_attr, None)
if allmodules:
for cmd in getattr(allmodules, "commands", {}).values():
mod = getattr(cmd, "__self__", None)
if mod and getattr(mod, "__origin__", "").startswith("<core"):
real_allmodules = getattr(mod, am_attr, None)
if real_allmodules:
self._raw_inline_cache = getattr(real_allmodules, "inline", None)
if self._raw_inline_cache:
return self._raw_inline_cache
return self._raw_inline_cache
async def client_ready(self, client: 'telethon.TelegramClient', database: 'loader.Database') -> None: async def client_ready(self, client: 'telethon.TelegramClient', database: 'loader.Database') -> None:
await client(UnblockRequest("@FHeta_robot")) try:
await utils.dnd(client, "@FHeta_robot", archive=True) await client(UnblockRequest("@FHeta_robot"))
await utils.dnd(client, "@FHeta_robot", archive=True)
except Exception:
pass
self.identifier = (await client.get_me()).id self.identifier = (await client.get_me()).id
self.token = database.get("FHeta", "token") self.token = database.get("FHeta", "token")
@@ -692,99 +662,93 @@ class FHeta(loader.Module):
await self.request_join( await self.request_join(
"NFHeta_Updates", "NFHeta_Updates",
f"{self.ui.emoji('channel')} {self.strings['channel']}" f"{self.ui.emoji('channel')} {self.strings('channel')}"
) )
self.api.token = self.token self.api.token = self.token
self._is_telethon = hasattr(self._inline_mgr, "_bot_client")
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
if self._is_telethon: router = router or self.inline
if hasattr(self._inline_mgr, "register_bot_update_handler"): dispatcher = getattr(router, "_dp", getattr(router, "dp", getattr(router, "router", None)))
async def telethon_chosen_handler(event: Any) -> None: self.bot = getattr(router, "_bot", getattr(router, "bot", getattr(self.inline, "bot", None)))
if isinstance(event, telethon.tl.types.UpdateBotInlineSend):
if event.id.startswith("fh_"): if dispatcher:
class MockCallback: if not getattr(dispatcher, "_fpatched", False):
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: async def fmiddleware(handler: Any, event: Any, data: Any) -> Any:
module = self.lookup("FHeta") try:
if module and event.result_id.startswith("fh_"): module = self.lookup("FHeta")
await module.click(event)
return None if module and getattr(event, "result_id", "").startswith("fh_"):
await module.click(event)
return None
except Exception:
pass
return await handler(event, data) return await handler(event, data)
dispatcher.chosen_inline_result.middleware(fmiddleware) try:
dispatcher._fpatched = True dispatcher.chosen_inline_result.middleware(fmiddleware)
dispatcher._fpatched = True
except Exception:
pass
if self.token and not await self.api.fetch("validatetkn", user_id=str(self.identifier)): if self.token and not await self.api.fetch("validatetkn", user_id=str(self.identifier)):
self.token = None self.token = None
self.api.token = None self.api.token = None
if not self.token: if not self.token:
async with client.conversation("@FHeta_robot") as conversation: try:
await conversation.send_message('/token') async with client.conversation("@FHeta_robot") as conversation:
self.token = (await conversation.get_response(timeout=5)).text.strip() await conversation.send_message('/token')
database.set("FHeta", "token", self.token) self.token = (await conversation.get_response(timeout=5)).text.strip()
self.api.token = self.token database.set("FHeta", "token", self.token)
self.api.token = self.token
except Exception:
pass
asyncio.create_task(self.sync()) asyncio.create_task(self.sync())
async def sync(self): async def sync(self):
ll = None ll = None
while True: while True:
cl = self.strings["lang"] try:
if cl != ll: cl = self.strings["lang"]
await self.api.send("dataset", user_id=self.identifier, lang=cl) if cl != ll:
ll = cl await self.api.send("dataset", user_id=self.identifier, lang=cl)
ll = cl
except Exception:
pass
await asyncio.sleep(1) await asyncio.sleep(1)
async def answer(self, callback: Any, text: Optional[str] = None, alert: bool = False) -> None: async def answer(self, callback: Union[CallbackQuery, ChosenInlineResult], text: Optional[str] = None, alert: bool = False) -> None:
if not hasattr(callback, "answer"): try:
return if text:
await callback.answer(text=text or "", show_alert=alert) await callback.answer(text, show_alert=alert)
else:
await callback.answer()
except Exception:
pass
async def edit(self, target: Any, text: str, buttons: List[List[Dict[str, Any]]], banner: Optional[str] = None) -> None: 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:
markup = self._inline_mgr.generate_markup(buttons) try:
if self._is_telethon:
if banner and banner not in text:
text = f'<a href="{banner}">&#8204;</a>' + text
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) 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)
if not self.bot:
return
arguments = { arguments = {
"text": text, "text": text,
"reply_markup": markup, "reply_markup": markup,
@@ -792,53 +756,64 @@ class FHeta(loader.Module):
"parse_mode": "HTML" "parse_mode": "HTML"
} }
if hasattr(target, "inline_message_id") and target.inline_message_id: inline = target if isinstance(target, str) else getattr(target, "inline_message_id", None)
arguments["inline_message_id"] = target.inline_message_id
if inline:
arguments["inline_message_id"] = inline
else: else:
arguments["chat_id"] = target.message.chat.id message = getattr(target, "message", target)
arguments["message_id"] = target.message.message_id 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
await self._inline_mgr.bot.edit_message_text(**arguments) await self.bot.edit_message_text(**arguments)
except Exception:
pass
async def click(self, callback: Any) -> None: async def click(self, callback: ChosenInlineResult) -> None:
result_id = callback.result_id try:
if not result_id.startswith("fh_"): if not getattr(callback, "result_id", "").startswith("fh_"):
return return
parts = callback.result_id.split("_")
if len(parts) != 3:
return
queryid = parts[1]
index = int(parts[2])
parts = result_id.split("_") cache = getattr(self.inline, "fheta_cache", {})
if len(parts) != 3: saved = cache.get(queryid, {})
return query = saved.get("query", "")
modules = saved.get("mods", [])
queryid = parts[1] if not modules or index >= len(modules):
index = int(parts[2]) return
if not hasattr(self._inline_mgr, "fheta_cache"): data = modules[index]
return text = self.ui.format(data, query, index+1, len(modules), True)
buttons = self.ui.buttons(data.get("install", ""), data, index, None, query)
saved = self._inline_mgr.fheta_cache.get(queryid, {}) await self.edit(callback, text, buttons, data.get("banner"))
query = saved.get("query", "") except Exception:
modules = saved.get("mods", []) pass
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: Any, index: int, modules: List[Dict[str, Any]], query: str) -> None: async def show(self, callback: Union[CallbackQuery, ChosenInlineResult], index: int, modules: List[Dict[str, Any]], query: str) -> None:
await self.answer(callback) await self.answer(callback)
text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>" text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>"
await self.edit(callback, text, self.ui.pagination(modules, query, 0, index)) await self.edit(callback, text, self.ui.pagination(modules, query, 0, index))
async def page(self, callback: Any, current: int, modules: List[Dict[str, Any]], query: str, index: int) -> None: async def page(self, callback: Union[CallbackQuery, ChosenInlineResult], current: int, modules: List[Dict[str, Any]], query: str, index: int) -> None:
await self.answer(callback) await self.answer(callback)
text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>" text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>"
await self.edit(callback, text, self.ui.pagination(modules, query, current, index)) await self.edit(callback, text, self.ui.pagination(modules, query, current, index))
async def navigate(self, callback: Any, index: int, modules: List[Dict[str, Any]], query: str = "") -> None: async def navigate(self, callback: Union[CallbackQuery, ChosenInlineResult], index: int, modules: List[Dict[str, Any]], query: str = "") -> None:
await self.answer(callback) await self.answer(callback)
if 0 <= index < len(modules): if 0 <= index < len(modules):
data = modules[index] data = modules[index]
@@ -846,7 +821,7 @@ class FHeta(loader.Module):
buttons = self.ui.buttons(data.get('install', ''), data, index, modules, query) buttons = self.ui.buttons(data.get('install', ''), data, index, modules, query)
await self.edit(callback, text, buttons, data.get("banner")) await self.edit(callback, text, buttons, data.get("banner"))
async def rate(self, callback: Any, link: str, action: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None: 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:
response = await self.api.send(f"rate/{self.identifier}/{link}/{action}") response = await self.api.send(f"rate/{self.identifier}/{link}/{action}")
request = await self.api.send("get", payload=[unquote(link)]) request = await self.api.send("get", payload=[unquote(link)])
@@ -855,7 +830,10 @@ class FHeta(loader.Module):
if modules and index < len(modules): if modules and index < len(modules):
modules[index].update(stats) modules[index].update(stats)
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")) try:
await callback.edit(reply_markup=self.ui.buttons(link, stats, index, modules, query))
except Exception:
pass
if response and response.get("status"): if response and response.get("status"):
status = response.get("status") status = response.get("status")
@@ -869,18 +847,21 @@ class FHeta(loader.Module):
text = "" text = ""
await self.answer(callback, text, True) await self.answer(callback, text, True)
async def install(self, callback: Any, link: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None: async def install(self, callback: Union[CallbackQuery, ChosenInlineResult], link: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
state, dependencies = await self.installer.execute(self.lookup("loader"), link) state, dependencies = await self.installer.execute(self.lookup("loader"), link)
if state == "success": try:
await self.answer(callback, self.strings["success"], True) if state == "success":
elif state == "dependency": await self.answer(callback, self.strings["success"], True)
formatted = f"({','.join(dependencies[:5])})" if dependencies else "" elif state == "dependency":
await self.answer(callback, self.strings["dependency"].format(deps=formatted), True) formatted = f"({','.join(dependencies[:5])})" if dependencies else ""
elif state == "overwrite": await self.answer(callback, self.strings["dependency"].format(deps=formatted), True)
await self.answer(callback, self.strings["overwrite"], True) elif state == "overwrite":
else: await self.answer(callback, self.strings["overwrite"], True)
await self.answer(callback, self.strings["error"], True) else:
await self.answer(callback, self.strings["error"], True)
except Exception:
pass
@loader.inline_handler( @loader.inline_handler(
ru_doc="(запрос) - поиск модулей.", ru_doc="(запрос) - поиск модулей.",
@@ -899,7 +880,7 @@ class FHeta(loader.Module):
return { return {
"title": self.strings["prompt"], "title": self.strings["prompt"],
"description": self.strings["hint"], "description": self.strings["hint"],
"message": f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=f'<code>@{self._inline_mgr.bot_username} ')}</code></b>", "message": f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=f'<code>@{self.inline.bot_username} ')}</code></b>",
"thumb": "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/magnifying_glass.png" "thumb": "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/magnifying_glass.png"
} }
@@ -922,13 +903,13 @@ class FHeta(loader.Module):
} }
queryid = str(uuid.uuid4())[:8] queryid = str(uuid.uuid4())[:8]
if not hasattr(self._inline_mgr, "fheta_cache"): if not hasattr(self.inline, "fheta_cache"):
self._inline_mgr.fheta_cache = {} self.inline.fheta_cache = {}
if len(self._inline_mgr.fheta_cache) >= 50: if len(self.inline.fheta_cache) >= 50:
self._inline_mgr.fheta_cache.pop(next(iter(self._inline_mgr.fheta_cache))) self.inline.fheta_cache.pop(next(iter(self.inline.fheta_cache)))
self._inline_mgr.fheta_cache[queryid] = {"query": query, "mods": modules} self.inline.fheta_cache[queryid] = {"query": query, "mods": modules}
results = [] results = []
@@ -937,38 +918,22 @@ class FHeta(loader.Module):
if isinstance(description, dict): if isinstance(description, dict):
description = description.get(self.strings["lang"]) or description.get("doc") or next(iter(description.values()), "") description = description.get(self.strings["lang"]) or description.get("doc") or next(iter(description.values()), "")
markup = self._inline_mgr.generate_markup(self.ui.buttons(data.get("install", ""), data, index, None, query)) markup = None
try:
markup = self.inline.generate_markup(self.ui.buttons(data.get("install", ""), data, index, None, query))
except Exception:
pass
if self._is_telethon: results.append(InlineQueryResultArticle(
thumb_url = data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png" id=f"fh_{queryid}_{index}",
thumb = self._inline_mgr._web_document(thumb_url) title=utils.escape_html(data.get("name", "")),
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
results.append( thumbnail_url=data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png",
await event.builder.article( input_message_content=InputTextMessageContent(message_text="", parse_mode="HTML"),
id=f"fh_{queryid}_{index}", reply_markup=markup
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
))
if self._is_telethon: await event.inline_query.answer(results, cache_time=0)
await event.answer(results, cache_time=0)
elif InlineQueryResultArticle is not Any:
await event.inline_query.answer(results, cache_time=0)
@loader.command( @loader.command(
ru_doc="(запрос) - поиск модулей.", ru_doc="(запрос) - поиск модулей.",
@@ -998,7 +963,7 @@ class FHeta(loader.Module):
data = modules[0] data = modules[0]
buttons = self.ui.buttons(data.get("install", ""), data, 0, modules, query) buttons = self.ui.buttons(data.get("install", ""), data, 0, modules, query)
form = await self._inline_mgr.form("", message, reply_markup=buttons, silent=True) form = await self.inline.form("", message, reply_markup=buttons, silent=True)
text = self.ui.format(data, query, 1, len(modules)) text = self.ui.format(data, query, 1, len(modules))
await self.edit(form, text, buttons, data.get("banner")) await self.edit(form, text, buttons, data.get("banner"))
@@ -1010,17 +975,20 @@ class FHeta(loader.Module):
if not url.startswith("https://api.fixyres.com/module/"): if not url.startswith("https://api.fixyres.com/module/"):
return return
state, dependencies = await self.installer.execute(self.lookup("loader"), url) try:
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("")
await asyncio.sleep(1) if state == "success":
await reply.delete() reply = await message.respond("")
await message.delete() 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

View File

@@ -112,13 +112,13 @@ class FSecurity(loader.Module):
loader.ConfigValue( loader.ConfigValue(
"strict_mode", "strict_mode",
False, False,
lambda: self.strings["strict_mode_doc"], lambda: self.strings("strict_mode_doc"),
validator=loader.validators.Boolean(), validator=loader.validators.Boolean(),
), ),
loader.ConfigValue( loader.ConfigValue(
"nvidia_api_key", "nvidia_api_key",
"", "",
lambda: self.strings["nvidia_api_key_doc"], lambda: self.strings("nvidia_api_key_doc"),
validator=loader.validators.Hidden(), validator=loader.validators.Hidden(),
) )
) )
@@ -386,10 +386,10 @@ class FSecurity(loader.Module):
def format(self, state, reason="", link=""): def format(self, state, reason="", link=""):
link_part = f' (<code>{utils.escape_html(link)}</code>)' if link else "" link_part = f' (<code>{utils.escape_html(link)}</code>)' if link else ""
if state == "unavailable": if state == "unavailable":
return f'<b>{self.strings["unavailable"].format(link_part)}</b>\n<b>{self.strings["continue"]}</b>' return f'<b>{self.strings("unavailable").format(link_part)}</b>\n<b>{self.strings("continue")}</b>'
if state == "suspicious": if state == "suspicious":
return f'<b>{self.strings["suspicious"].format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>\n<b>{self.strings["continue"]}</b>' return f'<b>{self.strings("suspicious").format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>\n<b>{self.strings("continue")}</b>'
return f'<b>{self.strings["blocked"].format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>' return f'<b>{self.strings("blocked").format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>'
def buttons(self, task): def buttons(self, task):
return [[ return [[

View File

@@ -8,7 +8,6 @@ __version__ = (1, 0, 0)
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/SCD/banner.png # meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/SCD/banner.png
# meta developer: @NFModules # meta developer: @NFModules
# meta fhsdesc: SoundCloud, Music, Music downloader, Downloader
# requires: curl_cffi # requires: curl_cffi
@@ -106,15 +105,15 @@ class SCD(loader.Module):
'''(link) - download a song from SoundCloud.''' '''(link) - download a song from SoundCloud.'''
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args: 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 return
m = re.search(r"(https?://(?:[a-zA-Z0-9-]+\.)?soundcloud\.com/[^\s]+)", args) m = re.search(r"(https?://(?:[a-zA-Z0-9-]+\.)?soundcloud\.com/[^\s]+)", args)
if not m: if not m:
await utils.answer(message, self.strings["not_found"]) await utils.answer(message, self.strings("not_found"))
return return
msg = await utils.answer(message, self.strings["downloading"]) msg = await utils.answer(message, self.strings("downloading"))
try: try:
async with requests.AsyncSession(impersonate="chrome120") as ses: async with requests.AsyncSession(impersonate="chrome120") as ses:
@@ -195,4 +194,4 @@ class SCD(loader.Module):
await msg.delete() await msg.delete()
except: except:
await utils.answer(msg, self.strings["not_found"]) await utils.answer(msg, self.strings("not_found"))

View File

@@ -308,7 +308,7 @@ class Akinator(loader.Module):
loader.ConfigValue( loader.ConfigValue(
"child_mode", "child_mode",
False, False,
lambda: self.strings["child_mode"], lambda: self.strings("child_mode"),
validator=loader.validators.Boolean() validator=loader.validators.Boolean()
) )
) )
@@ -339,17 +339,17 @@ class Akinator(loader.Module):
async def akinator(self, message): async def akinator(self, message):
'''- start the game.''' '''- start the game.'''
try: try:
aki = AsyncAki(self.strings["lang"], self.config["child_mode"]) aki = AsyncAki(self.strings("lang"), self.config["child_mode"])
await aki.start() await aki.start()
self.games.setdefault(message.chat_id, {})[message.id] = aki self.games.setdefault(message.chat_id, {})[message.id] = aki
await self.inline.form( await self.inline.form(
text=self.strings["text"], text=self.strings("text"),
message=message, message=message,
photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/banner.png", photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/banner.png",
reply_markup={ reply_markup={
"text": self.strings["start"], "text": self.strings("start"),
"callback": self._cb, "callback": self._cb,
"args": (message,) "args": (message,)
} }
@@ -369,12 +369,12 @@ class Akinator(loader.Module):
question = aki.q question = aki.q
markup = [[ markup = [[
{"text": self.strings["yes"], "callback": self._ans, "args": (0, message)}, {"text": self.strings("yes"), "callback": self._ans, "args": (0, message)},
{"text": self.strings["no"], "callback": self._ans, "args": (1, message)}, {"text": self.strings("no"), "callback": self._ans, "args": (1, message)},
{"text": self.strings["idk"], "callback": self._ans, "args": (2, 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"), "callback": self._ans, "args": (3, message)},
{"text": self.strings["probably_not"], "callback": self._ans, "args": (4, message)} {"text": self.strings("probably_not"), "callback": self._ans, "args": (4, message)}
] ]
] ]
@@ -393,13 +393,13 @@ class Akinator(loader.Module):
desc = aki.desc desc = aki.desc
if desc: if desc:
text = self.strings["this_is"].format(name=name, description=desc) text = self.strings("this_is").format(name=name, description=desc)
else: else:
text = self.strings["this_is_no_desc"].format(name=name) text = self.strings("this_is_no_desc").format(name=name)
markup = [[ markup = [[
{"text": self.strings["yes"], "callback": self._fin, "args": (True, message, text, aki.photo)}, {"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("not_right"), "callback": self._rej, "args": (message,)}
] ]
] ]
@@ -450,7 +450,7 @@ class Akinator(loader.Module):
await call.edit(text, photo=photo, reply_markup=[]) await call.edit(text, photo=photo, reply_markup=[])
else: else:
await call.edit( await call.edit(
self.strings["failed"], self.strings("failed"),
photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/idk.png", photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/idk.png",
reply_markup=[] reply_markup=[]
) )

View File

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

View File

@@ -37,7 +37,7 @@ from .. import utils, loader
from ..types import BotInlineCall, InlineCall from ..types import BotInlineCall, InlineCall
logger = logging.getLogger("Limoka") logger = logging.getLogger("Limoka")
__version__ = (1, 5, 5) __version__ = (1, 5, 4)
def _parse_version_from_source(source: str): def _parse_version_from_source(source: str):
@@ -846,83 +846,29 @@ class Limoka(loader.Module):
logger.error(f"Skipping unsafe rmtree for {folder}") logger.error(f"Skipping unsafe rmtree for {folder}")
async def _validate_url(self, url: str) -> Optional[str]: async def _validate_url(self, url: str) -> Optional[str]:
logger.debug(f"_validate_url called with: {url}") if not url or url in self._invalid_banners:
if not url:
logger.warning("_validate_url: URL is empty, returning None")
return None return None
if url in self._invalid_banners:
logger.debug(f"_validate_url: URL already in invalid_banners: {url}, returning None")
return None
# Headers to mimic a browser request
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try: try:
logger.debug(f"_validate_url: Starting validation for {url}")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
ct = None async with session.head(
response_status = None url, timeout=5, allow_redirects=True
) as response:
# Try HEAD first (more efficient) if response.status != 200:
try: self._invalid_banners.add(url)
logger.debug(f"_validate_url: Attempting HEAD request for {url}") return None
async with session.head( ct = response.headers.get("Content-Type", "").lower()
url, timeout=5, allow_redirects=True, headers=headers mime = None
) as response: if ct.startswith("image/"):
response_status = response.status return url
logger.debug(f"_validate_url: HEAD request returned status {response.status} for {url}") if not ct: # Some servers don't respond to HEAD requests with Content-Type, so instead we will try guess mime from content
if response.status == 200: async with session.get(url, timeout=5) as get_response:
ct = response.headers.get("Content-Type", "").lower() data = await get_response.read(2048)
logger.debug(f"_validate_url: Content-Type from HEAD: '{ct}' for {url}") mime = filetype.guess_mime(data, mime=True)
except (aiohttp.ClientError, asyncio.TimeoutError) as head_error: if mime and mime.startswith("image/"):
logger.debug(f"_validate_url: HEAD failed ({type(head_error).__name__}), will try GET for {url}") return url
# If HEAD didn't work or returned non-200, try GET
if ct is None:
max_retries = 2
for attempt in range(max_retries):
try:
async with session.get(
url, timeout=10, headers=headers, allow_redirects=True
) as response:
if response.status != 200:
self._invalid_banners.add(url)
return None
ct = response.headers.get("Content-Type", "").lower()
# Try to get MIME if Content-Type is missing
if not ct:
try:
data = await response.content.read(2048)
mime = filetype.guess_mime(data, mime=True)
if mime and mime.startswith("image/"):
return url
else:
self._invalid_banners.add(url)
return None
except Exception as mime_error:
logger.error(f"_validate_url: Error reading content for MIME detection: {mime_error}")
break # Success, exit retry loop
except (aiohttp.ClientError, asyncio.TimeoutError) as get_error:
if attempt < max_retries - 1:
await asyncio.sleep(1) # Wait before retry
else:
self._invalid_banners.add(url)
return None
# Check Content-Type from successful request
if ct and ct.startswith("image/"):
return url
elif ct:
self._invalid_banners.add(url) self._invalid_banners.add(url)
return None return None
else: except Exception:
self._invalid_banners.add(url)
return None
except Exception as e:
if url: if url:
self._invalid_banners.add(url) self._invalid_banners.add(url)
return None return None

View File

@@ -760,7 +760,7 @@ class Limoka(loader.Module):
), ),
}, },
{ {
"text": f"{self.strings['body_page']} {page_body + 1}/{len(body_pages)}", "text": f"{self.strings["body_page"]} {page_body + 1}/{len(body_pages)}",
"callback": self._inline_void, "callback": self._inline_void,
}, },
{ {

View File

@@ -1,157 +0,0 @@
# Midga3
# I AM NOT AFFICIATED WITH WORDLE
# meta developer: @midga3_modules
import requests
import random
import logging
from .. import loader, utils
from herokutl.tl.types import Message
__verison__ = (0, 1, 1)
logger = logging.getLogger(__name__)
@loader.tds
class wordle(loader.Module):
"""Wordle!"""
strings = {
"name": "Wordle",
"loading": "Loading...",
"language": "Language of the wordle",
"have_a_good_game": "Have a good game! Try to guess the 5 letter word in {}",
"attempts_left": "WRONG! Attempts left: {}",
"gg": "GG! YOU DIDN'T GUESS THE WORD {}",
"win": "GG! YOU WON! THE WORD WAS {}",
"already_playing": "ALREADY PLAYING! type .stopwordle to stop the current game",
"length": "Must be 5 letters",
"no_game": "No game is currently running",
"ok": "Game stopped",
"ad": "I tried to Guess word {}. Check out my result:\n{}",
"real_word": "This word is not in the word list"
}
strings_ru ={
"name": "Wordle",
"loading": "Загрузка...",
"language": "Язык вордла",
"have_a_good_game": "Хорошей игры! Попытайтесь угадать слово из 5 букв на {}",
"attempts_left": "НВЕВЕРНО! Осталось попыток: {}",
"gg": "ГГ! ВЫ НЕ УГАДАЛИ СЛОВО {}",
"win": "ГГ! ВЫ ВЫИГРАЛИ! СЛОВО БЫЛО {}",
"already_playing": "УЖЕ ИГРАЕТЕ! напишите .stopwordle чтобы остановить текущую игру",
"length": "Должно содержать 5 букв",
"no_game": "Сейчас нет активной игры",
"ok": "Игра остановлена",
"ad": "Я попытался угадать слово {}. Чекайте мой результат:\n{}",
"real_word": "Такого слова нет в списке слов"
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"language",
"en",
self.strings["language"],
validator=loader.validators.Choice(["en", "ru"])
),
)
async def handler(self, call, data):
guess = data.upper()
word = self._db.get("wordle", "word", "")
attempts = self._db.get("wordle", "attempts", 0)
buttons = self._db.get("wordle", "buttons", [])
markup = buttons + [[{"text":"Введите слово","input":self.strings("length"),"handler": self.handler}]]
if len(guess) != 5:
await call.edit(self.strings("length"), reply_markup=markup)
return
if guess not in self._db.get("wordle", "words", []):
await call.edit(self.strings("real_word"), reply_markup=markup)
return
buttons2 = []
for i in range(5):
if guess[i] == word[i]:
buttons2.append({"text": guess[i], "data": "custom/data", "style": "success"})
elif guess[i] in word:
buttons2.append({"text": guess[i], "data": "custom/data", "style": "primary"})
else:
buttons2.append({"text": guess[i], "data": "custom/data", "style": "danger"})
buttons.append(buttons2)
if guess == word:
self._db.set("wordle", "buttons", buttons)
self._db.set("wordle", "now_playing", False)
result = ""
for btn in buttons:
for b in btn:
if b["style"] == "success":
result += "🟩"
elif b["style"] == "primary":
result += "🟨"
else:
result += ""
result += "\n"
buttons.append([{"text":"Поделится резултатом","copy":self.strings("ad").format(word, result)}])
await call.edit(f"{self.strings('win').format(word)}", reply_markup=buttons)
return
self._db.set("wordle", "buttons", buttons)
attempts -= 1
self._db.set("wordle", "attempts", attempts)
if attempts == 0:
result = ""
for btn in buttons:
for b in btn:
if b["style"] == "success":
result += "🟩"
elif b["style"] == "primary":
result += "🟨"
else:
result += ""
result += "\n"
buttons.append([{"text":"Поделится резултатом","copy":self.strings("ad").format(word, result)}])
await call.edit(f"{self.strings('gg').format(word)}", reply_markup=buttons)
self._db.set("wordle", "now_playing", False)
else:
await call.edit(f"{self.strings('attempts_left').format(attempts)}", reply_markup=markup)
@loader.command()
async def wordle(self, message: Message):
"""Play wordle!"""
await utils.answer(message, self.strings("loading"))
if self._db.get("wordle", "now_playing", False):
await utils.answer(message, self.strings("already_playing"))
return
args = utils.get_args(message)
if args and args[0].lower():
if args[0].lower() == "--no-sec":
self._db.set("wordle", "nosec", True)
return
else:
self._db.set("wordle", "nosec", False)
try:
response = requests.get(f"https://raw.githubusercontent.com/mimimishka449/Worlde/refs/heads/main/words_{self.config['language']}.txt")
if response.status_code == 200:
words = response.text.splitlines()
word = random.choice(words).upper()
self._db.set("wordle", "now_playing", True)
self._db.set("wordle", "attempts", 6)
self._db.set("wordle", "word", word)
self._db.set("wordle", "words", words)
self._db.set("wordle", "buttons", [])
await self.inline.form(self.strings("have_a_good_game").format("английском" if self.config['language'] == "en" else "русском"), message, reply_markup=[[{"text":"Введите слово","input":self.strings("length"),"handler": self.handler}]])
else:
await utils.answer(message, "Error fetching wordle data.")
except Exception as e:
logger.exception(f"Error: {e}")
await utils.answer(message, "An error occurred while fetching wordle data.")
@loader.command()
async def stopwordle(self, message: Message):
"""Stop the wordle game."""
if not self._db.get("wordle", "now_playing", False):
await utils.answer(message, self.strings("no_game"))
return
self._db.set("wordle", "now_playing", False)
await utils.answer(message, self.strings("ok"))

View File

@@ -1,122 +1,150 @@
# meta developer: @matubuntu #meta developer: @matubuntu
import requests, bs4
import time
from datetime import datetime from datetime import datetime
import aiohttp
from .. import loader, utils from .. import loader, utils
import lxml
# requires: lxml requests bs4
_FLAGS = { _FLAGS = {
"AUD": "🇦🇺", "AZN": "🇦🇿", "GBP": "🇬🇧", "AMD": "🇦🇲", "AUD": "🇦🇺",
"BYN": "🇧🇾", "BGN": "🇧🇬", "BRL": "🇧🇷", "HUF": "🇭🇺", "AZN": "🇦🇿",
"VND": "🇻🇳", "HKD": "🇭🇰", "GEL": "🇬🇪", "DKK": "🇩🇰", "GBP": "🇬🇧",
"AED": "🇦🇪", "USD": "🇺🇸", "EUR": "🇪🇺", "EGP": "🇪🇬", "AMD": "🇦🇲",
"INR": "🇮🇳", "IDR": "🇮🇩", "KZT": "🇰🇿", "CAD": "🇨🇦", "BYN": "🇧🇾",
"QAR": "🇶🇦", "KGS": "🇰🇬", "CNY": "🇨🇳", "MDL": "🇲🇩", "BGN": "🇧🇬",
"NZD": "🇳🇿", "NOK": "🇳🇴", "PLN": "🇵🇱", "RON": "🇷🇴", "BRL": "🇧🇷",
"SGD": "🇸🇬", "TJS": "🇹🇯", "THB": "🇹🇭", "TRY": "🇹🇷", "HUF": "🇭🇺",
"TMT": "🇹🇲", "UZS": "🇺🇿", "UAH": "🇺🇦", "CZK": "🇨🇿", "VND": "🇻🇳",
"SEK": "🇸🇪", "CHF": "🇨🇭", "RSD": "🇷🇸", "ZAR": "🇿🇦", "HKD": "🇭🇰",
"KRW": "🇰🇷", "JPY": "🇯🇵", "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 = { _CRYPTO_EMOJIS = {
"BTC": "<emoji document_id=5289519973285257969>💰</emoji>", "BTC": "<emoji document_id=5289519973285257969>💰</emoji>",
"ETH": "<emoji document_id=5287735049301550386>💰</emoji>", "ETH": "<emoji document_id=5287735049301550386>💰</emoji>",
"SOL": "<emoji document_id=5251712673258697260>💰</emoji>", "SOL": "<emoji document_id=5251712673258697260>💰</emoji>",
"TON": "<emoji document_id=5289648693455119919>💰</emoji>", "TON": "<emoji document_id=5289648693455119919>💰</emoji>",
"USDT": "<emoji document_id=5289904548951911168>💰</emoji>", "USDT": "<emoji document_id=5289904548951911168>💰</emoji>",
"XRP": "<emoji document_id=5373312921214401986>💰</emoji>", "XRP": "<emoji document_id=5373312921214401986>💰</emoji>",
"USDC": "<emoji document_id=5372958453268497353>💰</emoji>", "USDC": "<emoji document_id=5372958453268497353>💰</emoji>",
"ADA": "<emoji document_id=5373076801092338046>💰</emoji>", "ADA": "<emoji document_id=5373076801092338046>💰</emoji>",
"DOGE": "<emoji document_id=5375192042420842380>💰</emoji>", "DOGE": "<emoji document_id=5375192042420842380>💰</emoji>",
"TRX": "<emoji document_id=5375187081733616165>💰</emoji>", "TRX": "<emoji document_id=5375187081733616165>💰</emoji>",
"AVAX": "<emoji document_id=5375311275007947936>💰</emoji>", "AVAX": "<emoji document_id=5375311275007947936>💰</emoji>",
"LTC": "<emoji document_id=5373035462032113888>💰</emoji>", "LTC": "<emoji document_id=5373035462032113888>💰</emoji>",
"BCH": "<emoji document_id=5375596920397903962>💰</emoji>", "BCH": "<emoji document_id=5375596920397903962>💰</emoji>",
"ATOM": "<emoji document_id=5375468745688889977>💰</emoji>", "ATOM": "<emoji document_id=5375468745688889977>💰</emoji>",
"XLM": "<emoji document_id=5372823290647690288>💰</emoji>", "XLM": "<emoji document_id=5372823290647690288>💰</emoji>",
"SHIB": "<emoji document_id=5375231036428924778>💰</emoji>", "SHIB": "<emoji document_id=5375231036428924778>💰</emoji>",
"UNI": "<emoji document_id=5372953110329180525>💰</emoji>", "UNI": "<emoji document_id=5372953110329180525>💰</emoji>",
"XMR": "<emoji document_id=5375507073977038661>💰</emoji>", "XMR": "<emoji document_id=5375507073977038661>💰</emoji>",
"LINK": "<emoji document_id=5375149651093633217>💰</emoji>", "LINK": "<emoji document_id=5375149651093633217>💰</emoji>",
"ETC": "<emoji document_id=5375543306321146693>💰</emoji>", "ETC": "<emoji document_id=5375543306321146693>💰</emoji>",
"SUI": "<emoji document_id=5391002164929772708>💰</emoji>", "SUI": "<emoji document_id=5391002164929772708>💰</emoji>",
"NEAR": "<emoji document_id=5391181990915487346>💰</emoji>", "NEAR": "<emoji document_id=5391181990915487346>💰</emoji>",
"VET": "<emoji document_id=5391091302681033446>💰</emoji>", "VET": "<emoji document_id=5391091302681033446>💰</emoji>",
"FIL": "<emoji document_id=5373117173784919811>💰</emoji>", "FIL": "<emoji document_id=5373117173784919811>💰</emoji>",
"XTZ": "<emoji document_id=5390985478981829698>💰</emoji>", "XTZ": "<emoji document_id=5390985478981829698>💰</emoji>",
"ALGO": "<emoji document_id=5391337713544738420>💰</emoji>", "ALGO": "<emoji document_id=5391337713544738420>💰</emoji>",
"THETA": "<emoji document_id=5391256014676833736>💰</emoji>", "THETA": "<emoji document_id=5391256014676833736>💰</emoji>",
"FTM": "<emoji document_id=5393179395521263785>💰</emoji>", "FTM": "<emoji document_id=5393179395521263785>💰</emoji>",
"XDAI": "<emoji document_id=5391325992578988886>💰</emoji>", "XDAI": "<emoji document_id=5391325992578988886>💰</emoji>",
"RUNE": "<emoji document_id=5391347570494684983>💰</emoji>", "RUNE": "<emoji document_id=5391347570494684983>💰</emoji>",
"DOT": "<emoji document_id=5375224568208177973>💰</emoji>", "DOT": "<emoji document_id=5375224568208177973>💰</emoji>",
} }
_CRYPTO_NAMES = { _CRYPTO_LIST = {
"BTC": "Bitcoin", "ETH": "Ethereum", "XMR": "Monero", "BTC": "Bitcoin",
"LTC": "Litecoin", "XRP": "XRP", "ADA": "Cardano", "ETH": "Ethereum",
"DOGE": "Dogecoin", "SOL": "Solana", "DOT": "Polkadot", "XMR": "Monero",
"USDT": "Tether", "TON": "Toncoin", "USDC": "USD Coin", "LTC": "Litecoin",
"TRX": "TRON", "AVAX": "Avalanche", "BCH": "Bitcoin Cash", "XRP": "XRP",
"ATOM": "Cosmos", "XLM": "Stellar", "SHIB": "Shiba Inu", "ADA": "Cardano",
"UNI": "Uniswap", "LINK": "Chainlink", "ETC": "Ethereum Classic", "DOGE": "Dogecoin",
"SUI": "Sui", "NEAR": "NEAR Protocol", "VET": "VeChain", "SOL": "Solana",
"FIL": "Filecoin", "XTZ": "Tezos", "ALGO": "Algorand", "DOT": "Polkadot",
"THETA": "Theta Network", "FTM": "Fantom", "XDAI": "xDai", "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", "RUNE": "THORChain",
} }
_CBR_URL = "https://www.cbr.ru/scripts/XML_daily.asp" def _fmt_num(v, d=3):
_CRYPTO_URL = "https://api.coinlore.net/api/tickers/?limit=100" p = f"{v:,.{d}f}".replace(",", " ").split(".")
i = p[0]
CACHE_TTL = 300 # seconds d = p[1].rstrip("0") if len(p) > 1 else ""
return f"{i},{d}" if d else i
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 @loader.tds
class FinanceMod(loader.Module): class FinanceMod(loader.Module):
"""Курсы валют (ЦБ РФ) и криптовалют (CoinLore)""" strings = {
"name": "FinanceMod",
strings = {"name": "FinanceMod"} "valute_description": "<кол-во> <код> - курс валюты\n<кол-во> - список",
"valute_no_args": (
"💵 <b>Курс валюты с сайта </b><a href='https://www.cbr.ru/'>ЦБ(РФ)</a>\n"
"<b>Актуально на</b> <i>{}</i>\n\n<blockquote expandable>{}</blockquote>"
),
"valute_specific": (
"💵 <b>Курс валюты с сайта </b><a href='https://www.cbr.ru/'>ЦБ(РФ)</a>\n"
"<b>Актуально на</b> <i>{}</i>\n\n{}"
),
"valute_not_found": "🚫 Валюта {} не найдена",
"crypto_description": "<кол-во> <код> - курс крипты\n<кол-во> - список",
"crypto_no_args": "💎 <b>Курсы криптовалют</b>\n\n<blockquote expandable>{}</blockquote>",
"crypto_specific": "💎 <b>Курс криптовалюты</b>\n\n{}",
"crypto_not_found": "🚫 Криптовалюта {} не найдена",
"error": "🚫 Ошибка получения данных",
}
def __init__(self): def __init__(self):
self.config = loader.ModuleConfig( self.config = loader.ModuleConfig(
@@ -124,194 +152,149 @@ class FinanceMod(loader.Module):
"crypto_currency", "crypto_currency",
"USD", "USD",
lambda: "Валюта для отображения крипты (USD, RUB, EUR)", 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)
# ──────────────────────────── HTTP helpers ──────────────────────────── async def _get_curr_data(self):
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: try:
raw = await self._fetch(_CBR_URL) r = requests.get("https://www.cbr.ru/scripts/XML_daily.asp")
date, rates = _parse_cbr_xml(raw) s = bs4.BeautifulSoup(r.content, 'xml')
self._cbr_cache = (now, date, rates) d = datetime.strptime(s.ValCurs['Date'], "%d.%m.%Y").strftime("%d.%m.%Y")
return date, rates return d, s.find_all('Valute')
except Exception: except:
if self._cbr_cache: return None, None
return self._cbr_cache[1], self._cbr_cache[2]
return None, {}
# ──────────────────────────── Crypto data ───────────────────────────── async def _get_rates(self):
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: try:
js = await self._fetch(_CRYPTO_URL, as_json=True) r = requests.get("https://www.cbr.ru/scripts/XML_daily.asp")
data = js.get("data", []) s = bs4.BeautifulSoup(r.content, 'xml')
self._crypto_cache = (now, data) rt = {'USD': None, 'EUR': None}
return data for v in s.find_all('Valute'):
except Exception: if v.CharCode.text in ['USD', 'EUR']:
return self._crypto_cache[1] if self._crypto_cache else [] 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
# ──────────────────────────── Formatters ────────────────────────────── 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} руб."
def _fmt_valute(self, code: str, info: dict, amount: float = 1.0) -> str: async def _get_crypto(self):
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: try:
price_usd = float(coin["price_usd"]) return requests.get("https://api.coinlore.net/api/tickers/").json().get('data', [])
except (KeyError, ValueError, TypeError): except:
return "" return None
currency = self.config["crypto_currency"] async def _fmt_crypto(self, c, a=1):
if currency == "RUB": r = await self._get_rates()
usd_rate = rates.get("USD", {}).get("rub") if not r:
if not usd_rate: return "🚫 Ошибка получения курсов валют"
return "" cr = self.config["crypto_currency"]
price = price_usd * usd_rate try:
sign = "" p = float(c['price_usd'])
elif currency == "EUR": except:
usd_rate = rates.get("USD", {}).get("rub") return "🚫 Ошибка данных криптовалюты"
eur_rate = rates.get("EUR", {}).get("rub") if cr == "RUB":
if not usd_rate or not eur_rate: if not r['USD']:
return "" return "🚫 Курс USD не найден"
price = price_usd * (usd_rate / eur_rate) p *= r['USD']
sign = "" elif cr == "EUR":
else: if not r['EUR_USD']:
price = price_usd return "🚫 Курс EUR/USD не рассчитан"
sign = "$" 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}"
total = price * amount @loader.command()
emoji = _CRYPTO_EMOJIS.get(symbol, "💠") async def valutecmd(self, m):
name = _CRYPTO_NAMES.get(symbol, coin.get("name", symbol)) """[count] [usd, eur, ...]"""
return f"{emoji} [{_fmt_num(amount, 0)}] {name} ({symbol}) — {_fmt_num(total, 3)}{sign}" a = utils.get_args(m)
d, v = await self._get_curr_data()
# ──────────────────────────── Commands ──────────────────────────────── if not d or not v:
return await utils.answer(m, self.strings["error"])
@loader.command(ru_doc="[кол-во] [код] — курс валюты по ЦБ РФ") if len(a) == 0:
async def valutecmd(self, message): l = []
"""[amount] [code] — exchange rates from CBR""" for x in v:
args = utils.get_args(message) if (n := await self._fmt_curr(x)):
date, rates = await self._cbr_data() l.append(n)
await utils.answer(m, self.strings["valute_no_args"].format(d, "\n".join(l)))
if not rates: elif len(a) == 1:
return await utils.answer(message, "🚫 Не удалось получить данные ЦБ РФ")
header = (
f"💵 <b>Курс валюты</b> · <a href='https://www.cbr.ru/'>ЦБ РФ</a>\n"
f"<b>Актуально на</b> <i>{date}</i>\n\n"
)
# .valute — список всех, кол-во = 1
if not args:
lines = [self._fmt_valute(c, i) for c, i in rates.items()]
return await utils.answer(
message,
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
)
# Первый аргумент: число или код валюты?
amount = 1.0
code = None
arg0 = args[0].upper()
if len(args) >= 2:
# .valute 100 USD
try: try:
amount = float(args[0].replace(",", ".")) am = float(a[0])
except ValueError: l = []
return await utils.answer(message, "🚫 Некорректное число") for x in v:
code = args[1].upper() if (n := await self._fmt_curr(x, am)):
else: l.append(n)
# .valute USD или .valute 100 await utils.answer(m, self.strings["valute_no_args"].format(d, "\n".join(l)))
except:
await utils.answer(m, "🚫 Некорректное число")
elif len(a) == 2:
try: try:
amount = float(arg0.replace(",", ".")) am = float(a[0])
# число без кода — список с умножением c = a[1].upper()
except ValueError: for x in v:
code = arg0 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, "🚫 Некорректное число")
if code: @loader.command()
if code not in rates: async def cryptocmd(self, m):
return await utils.answer(message, f"🚫 Валюта <b>{code}</b> не найдена") """[count] [ton, btc, ...]"""
line = self._fmt_valute(code, rates[code], amount) a = utils.get_args(m)
return await utils.answer(message, header + line) c = await self._get_crypto()
if not c:
# список с кол-вом return await utils.answer(m, self.strings["error"])
lines = [self._fmt_valute(c, i, amount) for c, i in rates.items()] try:
await utils.answer( if len(a) == 0:
message, f = [x for x in c if x['symbol'].upper() in _CRYPTO_LIST]
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>", l = []
) for x in f:
if (n := await self._fmt_crypto(x)):
@loader.command(ru_doc="[кол-во] [код] — курс крипты") l.append(n)
async def cryptocmd(self, message): await utils.answer(m, self.strings["crypto_no_args"].format("\n".join(l)))
"""[amount] [symbol] — crypto rates from CoinLore""" elif len(a) == 1:
args = utils.get_args(message) am = float(a[0])
coins = await self._crypto_data() f = [x for x in c if x['symbol'].upper() in _CRYPTO_LIST]
_, rates = await self._cbr_data() l = []
for x in f:
if not coins: if (n := await self._fmt_crypto(x, am)):
return await utils.answer(message, "🚫 Не удалось получить данные крипты") l.append(n)
await utils.answer(m, self.strings["crypto_no_args"].format("\n".join(l)))
header = f"💎 <b>Курсы криптовалют</b> · <i>{self.config['crypto_currency']}</i>\n\n" elif len(a) == 2:
am = float(a[0])
amount = 1.0 t = a[1].upper()
symbol = None f = False
for x in c:
if not args: if x['symbol'].upper() == t:
pass # список, amount=1 if (n := await self._fmt_crypto(x, am)):
elif len(args) == 1: f = True
try: await utils.answer(m, self.strings["crypto_specific"].format(n))
amount = float(args[0].replace(",", ".")) break
except ValueError: if not f:
symbol = args[0].upper() await utils.answer(m, self.strings["crypto_not_found"].format(t))
else: except ValueError:
try: await utils.answer(m, "🚫 Некорректное число")
amount = float(args[0].replace(",", ".")) except Exception as e:
except ValueError: await utils.answer(m, f"🚫 Ошибка: {str(e)}")
return await utils.answer(message, "🚫 Некорректное число")
symbol = args[1].upper()
if symbol:
coin = next((c for c in coins if c["symbol"].upper() == symbol), None)
if not coin:
return await utils.answer(message, f"🚫 Крипта <b>{symbol}</b> не найдена")
line = self._fmt_crypto(coin, rates, amount)
if not line:
return await utils.answer(message, "🚫 Ошибка форматирования")
return await utils.answer(message, header + line)
# список только известных монет
known = {c["symbol"].upper(): c for c in coins if c["symbol"].upper() in _CRYPTO_NAMES}
# сортируем по порядку _CRYPTO_NAMES
lines = []
for sym in _CRYPTO_NAMES:
if sym in known:
line = self._fmt_crypto(known[sym], rates, amount)
if line:
lines.append(line)
await utils.answer(
message,
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -4,6 +4,4 @@
.ruff_cache .ruff_cache
ruff.log ruff.log
ruff.log.2 ruff.log.2
ruff.toml ruff.toml
# Heroku files
heroku/

View File

@@ -1,438 +0,0 @@
__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": "<emoji document_id=5980953710157632545>❌</emoji> <b>Wrong arguments. Check command usage.</b>",
"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": "<blockquote>#{}</blockquote>\n\n<blockquote>{}</blockquote>",
"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": """
<b>Available placeholders</b>:
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": "<emoji document_id=5980953710157632545>❌</emoji> <b>Неверные аргументы. Проверьте использование команды.</b>",
"no_reply": "Нет реплая! Ответьте на сообщение, текст которого станет заметкой.",
"not_exist": "Такой заметки не найдено!",
"already_exists": "Кажется, заметка с таким тегом уже существует. Перезаписать?",
"show_note_inline": "<blockquote>#{}</blockquote>\n\n<blockquote>{}</blockquote>",
"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": """
<b>Доступные плейсхолдеры</b>:
об аккаунте, на котором стоит юзербот:
{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

View File

@@ -59,7 +59,7 @@ class FaceMod(loader.Module):
en_doc="Random kaomoji", en_doc="Random kaomoji",
) )
async def rfacecmd(self, message) -> None: # noqa: D102, ANN001 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" url = "https://files.archquise.ru/kaomoji.txt"
@@ -72,7 +72,7 @@ class FaceMod(loader.Module):
kaomoji = random.choice(kaomoji_list) # noqa: S311 kaomoji = random.choice(kaomoji_list) # noqa: S311
await utils.answer( await utils.answer(
message, message,
self.strings["random_face"].format(kaomoji), self.strings("random_face").format(kaomoji),
) )
else: else:
await utils.answer(message, self.strings["error"]) await utils.answer(message, self.strings("error"))

View File

@@ -113,24 +113,24 @@ class ShortenerMod(loader.Module):
async def shortencmd(self, message): # noqa: ANN001, ANN201 async def shortencmd(self, message): # noqa: ANN001, ANN201
"""Shorten URL using bit.ly API.""" """Shorten URL using bit.ly API."""
if self.config["token"] is None: if self.config["token"] is None:
await utils.answer(message, self.strings["no_api"]) await utils.answer(message, self.strings("no_api"))
return return
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args: if not args:
await utils.answer(message, self.strings["no_args"]) await utils.answer(message, self.strings("no_args"))
return return
if not self._validate_url(args): if not self._validate_url(args):
await utils.answer(message, self.strings["invalid_url"]) await utils.answer(message, self.strings("invalid_url"))
return return
try: try:
short_url = await self.shorten_url(url=args, token=self.config["token"]) 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: except Exception as e:
logger.exception("Error shortening URL!") 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( @loader.command(
ru_doc="Посмотреть статистику ссылки через bit.ly (ссылка без https:// | Доступно только на платных аккаунтах)", ru_doc="Посмотреть статистику ссылки через bit.ly (ссылка без https:// | Доступно только на платных аккаунтах)",
@@ -139,22 +139,22 @@ class ShortenerMod(loader.Module):
async def statclcmd(self, message): # noqa: ANN001, ANN201 async def statclcmd(self, message): # noqa: ANN001, ANN201
"""Get click statistics for shortened URL.""" """Get click statistics for shortened URL."""
if self.config["token"] is None: if self.config["token"] is None:
await utils.answer(message, self.strings["no_api"]) await utils.answer(message, self.strings("no_api"))
return return
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args: if not args:
await utils.answer(message, self.strings["no_args"]) await utils.answer(message, self.strings("no_args"))
return return
try: try:
if not args.startswith("bit.ly/"): if not args.startswith("bit.ly/"):
await utils.answer(message, self.strings["invalid_url"]) await utils.answer(message, self.strings("invalid_url"))
return return
clicks = await self.get_bitlink_stats( clicks = await self.get_bitlink_stats(
bitlink=args, token=self.config["token"] 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: except Exception as e:
logger.exception("Error getting statistics!") 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)))

View File

@@ -22,4 +22,4 @@ chatmodule
stats stats
tagwatcher tagwatcher
hardspam hardspam
YaMusic YaMusic

View File

@@ -21,7 +21,6 @@ import random
import string import string
import asyncio import asyncio
import logging import logging
import re
from PIL import Image, UnidentifiedImageError from PIL import Image, UnidentifiedImageError
from telethon.tl.functions.stickers import CreateStickerSetRequest from telethon.tl.functions.stickers import CreateStickerSetRequest
@@ -39,12 +38,11 @@ except AttributeError:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
STATIC_STICKER_LIMIT = 120
EMOJI_LIMIT = 200
async def process_to_webp(input_path: str, output_path: str, size: int = 512) -> bool: async def process_to_webp(input_path: str, output_path: str, size: int = 512) -> bool:
try: try:
is_video = input_path.lower().endswith((".mp4", ".webm", ".mov")) or b"ftyp" in open(input_path, "rb").read(32) is_video = input_path.lower().endswith(('.mp4', '.webm', '.mov')) or b'ftyp' in open(input_path, 'rb').read(32)
if is_video: if is_video:
cap = cv2.VideoCapture(input_path) cap = cv2.VideoCapture(input_path)
success, frame = cap.read() success, frame = cap.read()
@@ -89,7 +87,7 @@ async def process_to_webp(input_path: str, output_path: str, size: int = 512) ->
async def process_to_png(input_path: str, output_path: str, size: int = 100) -> bool: async def process_to_png(input_path: str, output_path: str, size: int = 100) -> bool:
try: try:
is_video = input_path.lower().endswith((".mp4", ".webm", ".mov")) or b"ftyp" in open(input_path, "rb").read(32) is_video = input_path.lower().endswith(('.mp4', '.webm', '.mov')) or b'ftyp' in open(input_path, 'rb').read(32)
if is_video: if is_video:
cap = cv2.VideoCapture(input_path) cap = cv2.VideoCapture(input_path)
success, frame = cap.read() success, frame = cap.read()
@@ -139,10 +137,8 @@ class CreatePacks(loader.Module):
"processing": "<b>[CreatePacks]</b> Collecting avatars of participants...", "processing": "<b>[CreatePacks]</b> Collecting avatars of participants...",
"no_avatars": "<b>[CreatePacks]</b> No members with avatars", "no_avatars": "<b>[CreatePacks]</b> No members with avatars",
"no_valid": "<b>[CreatePacks]</b> Could not process any avatars", "no_valid": "<b>[CreatePacks]</b> Could not process any avatars",
"done_pack": "<b>[CreatePacks]</b> Sticker pack is ready:\n<b>[CreatePacks]</b> Open: <a href=\'https://t.me/addstickers/{}\\'>here</a>", "done_pack": "<b>[CreatePacks]</b> Sticker pack is ready:\n<b>[CreatePacks]</b> Open: <a href='https://t.me/addstickers/{}'>here</a>",
"done_packs": "<b>[CreatePacks]</b> Sticker packs are ready:\n{}", "done_emoji_pack": "<b>[CreatePacks]</b> Emoji pack is ready:\n<b>[CreatePacks]</b> Open: <a href='https://t.me/addstickers/{}'>here</a>",
"done_emoji_pack": "<b>[CreatePacks]</b> Emoji pack is ready:\n<b>[CreatePacks]</b> Open: <a href=\'https://t.me/addstickers/{}\\''>here</a>",
"done_emoji_packs": "<b>[CreatePacks]</b> Emoji packs are ready:\n{}",
"already": "<b>[CreatePacks]</b> A sticker pack with this name already exists.", "already": "<b>[CreatePacks]</b> A sticker pack with this name already exists.",
"emoji_processing": "<b>[CreatePacks]</b> Creating emoji pack from avatars...", "emoji_processing": "<b>[CreatePacks]</b> Creating emoji pack from avatars...",
"emoji_no_emoji": "<b>[CreatePacks]</b> No emoji specified — using", "emoji_no_emoji": "<b>[CreatePacks]</b> No emoji specified — using",
@@ -153,10 +149,8 @@ class CreatePacks(loader.Module):
"processing": "<b>[CreatePacks]</b> Собираю аватарки участников...", "processing": "<b>[CreatePacks]</b> Собираю аватарки участников...",
"no_avatars": "<b>[CreatePacks]</b> Нет участников с аватарками", "no_avatars": "<b>[CreatePacks]</b> Нет участников с аватарками",
"no_valid": "<b>[CreatePacks]</b> Не удалось обработать ни одну аватарку", "no_valid": "<b>[CreatePacks]</b> Не удалось обработать ни одну аватарку",
"done_pack": "<b>[CreatePacks]</b> Стикерпак готов:\n<b>[CreatePacks]</b> Открыть: <a href=\'https://t.me/addstickers/{}\\''>здесь</a>", "done_pack": "<b>[CreatePacks]</b> Стикерпак готов:\n<b>[CreatePacks]</b> Открыть: <a href='https://t.me/addstickers/{}'>здесь</a>",
"done_packs": "<b>[CreatePacks]</b> Стикерпаки готовы:\n{}", "done_emoji_pack": "<b>[CreatePacks]</b> Эмодзи-пак готов:\n<b>[CreatePacks]</b> Открыть: <a href='https://t.me/addstickers/{}'>здесь</a>",
"done_emoji_pack": "<b>[CreatePacks]</b> Эмодзи-пак готов:\n<b>[CreatePacks]</b> Открыть: <a href=\'https://t.me/addstickers/{}\\''>здесь</a>",
"done_emoji_packs": "<b>[CreatePacks]</b> Эмодзи-паки готовы:\n{}",
"already": "<b>[CreatePacks]</b> Стикерпак с таким именем уже существует", "already": "<b>[CreatePacks]</b> Стикерпак с таким именем уже существует",
"emoji_processing": "<b>[CreatePacks]</b> Создаю эмодзи-пак из аватаров...", "emoji_processing": "<b>[CreatePacks]</b> Создаю эмодзи-пак из аватаров...",
"emoji_no_emoji": "<b>[CreatePacks]</b> Эмодзи не указан — используется", "emoji_no_emoji": "<b>[CreatePacks]</b> Эмодзи не указан — используется",
@@ -175,6 +169,10 @@ class CreatePacks(loader.Module):
if len(users) >= 100: if len(users) >= 100:
break break
if not users:
shutil.rmtree(tmp_dir, ignore_errors=True)
return [], tmp_dir
processed = [] processed = []
process_func = process_to_webp if format == "webp" else process_to_png process_func = process_to_webp if format == "webp" else process_to_png
@@ -226,43 +224,6 @@ class CreatePacks(loader.Module):
return processed, tmp_dir return processed, tmp_dir
async def _create_sticker_pack(self, message, stickers_to_add, is_emoji_pack: bool, pack_number: int = 1, emoji: str = "🖼️"):
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10))
short_name = f"pack_{random_str}_by_fcreate"
chat = await message.get_chat()
chat_title = getattr(chat, 'title', 'Chat')
title_prefix = "Ava" if not is_emoji_pack else "Emoji"
full_title = f"{chat_title} {title_prefix} #{pack_number}"
try:
await self._client(CreateStickerSetRequest(
user_id="me",
title=full_title,
short_name=short_name,
stickers=stickers_to_add,
emojis=is_emoji_pack
))
return short_name, full_title
except PackShortNameOccupiedError:
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12))
short_name = f"pack_{random_str}_by_fcreate"
try:
await self._client(CreateStickerSetRequest(
user_id="me",
title=full_title,
short_name=short_name,
stickers=stickers_to_add,
emojis=is_emoji_pack
))
return short_name, full_title
except:
return "already_exists", None
except Exception as e:
logger.error(f"Error creating pack: {e}")
return None, None
@loader.command( @loader.command(
ru_doc="- Создать стикерпак из аватаров в группе", ru_doc="- Создать стикерпак из аватаров в группе",
only_groups=True only_groups=True
@@ -275,7 +236,11 @@ class CreatePacks(loader.Module):
if not files: if not files:
return await message.edit(self.strings("no_avatars")) return await message.edit(self.strings("no_avatars"))
all_stickers = [] tag = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
short_name = f"f{abs(message.chat_id)}_{tag}_by_fcreateavatars"
title = f"AvaPack {tag}"
stickers = []
for path in files: for path in files:
try: try:
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
@@ -283,42 +248,42 @@ class CreatePacks(loader.Module):
msg = await self._client.send_file("me", file, force_document=True) msg = await self._client.send_file("me", file, force_document=True)
doc = msg.document doc = msg.document
await self._client.delete_messages("me", msg.id) await self._client.delete_messages("me", msg.id)
all_stickers.append(InputStickerSetItem( stickers.append(InputStickerSetItem(
document=InputDocument(doc.id, doc.access_hash, doc.file_reference), document=InputDocument(doc.id, doc.access_hash, doc.file_reference),
emoji="🖼️" emoji="🖼️"
)) ))
except Exception as e: except Exception as e:
logger.error(f"Sticker loading error {path}: {e}") logger.error(f"Sticker loading error {path}: {e}")
continue continue
if not all_stickers: if not stickers:
shutil.rmtree(tmp_dir, ignore_errors=True) shutil.rmtree(tmp_dir, ignore_errors=True)
return await message.edit(self.strings("no_valid")) return await message.edit(self.strings("no_valid"))
created_packs_links = [] try:
pack_number = 1 await self._client(CreateStickerSetRequest(
for i in range(0, len(all_stickers), STATIC_STICKER_LIMIT): user_id="me",
current_pack_stickers = all_stickers[i : i + STATIC_STICKER_LIMIT] title=title,
short_name, full_title = await self._create_sticker_pack(message, current_pack_stickers, False, pack_number) short_name=short_name,
if short_name == "already_exists": stickers=stickers
await message.edit(self.strings("already")) ))
shutil.rmtree(tmp_dir, ignore_errors=True) await message.edit(self.strings("done_pack").format(short_name))
return except PackShortNameOccupiedError:
elif short_name: await message.edit(self.strings("already"))
created_packs_links.append(f"<a href=\'https://t.me/addstickers/{short_name}\\''>{full_title}</a>") except Exception as e:
pack_number += 1 error_details = f"❌ Ошибка создания стикерпака:\n<code>{type(e).__name__}: {e}</code>\n"
error_details += f"Пак: {short_name}\nСтикеров: {len(stickers)}\n"
if created_packs_links: if files:
if len(created_packs_links) == 1: error_details += f"Последний файл: {files[-1]}\n"
# Extract short name for the single link format try:
sn = created_packs_links[0].split('/')[-1].split("'")[0] error_details += f"Размер: {Image.open(files[-1]).size}\n"
await message.edit(self.strings("done_pack").format(sn)) error_details += f"Вес: {os.path.getsize(files[-1])} байт"
else: except:
await message.edit(self.strings("done_packs").format("\n".join(created_packs_links))) pass
else: await message.edit(error_details)
await message.edit(self.strings("no_valid")) logger.exception("Error creating sticker pack")
finally:
shutil.rmtree(tmp_dir, ignore_errors=True) shutil.rmtree(tmp_dir, ignore_errors=True)
@loader.command( @loader.command(
ru_doc="[эмодзи] - Создать эмодзи-пак из всех аватаров", ru_doc="[эмодзи] - Создать эмодзи-пак из всех аватаров",
@@ -338,7 +303,11 @@ class CreatePacks(loader.Module):
if not files: if not files:
return await message.edit(self.strings("no_avatars")) return await message.edit(self.strings("no_avatars"))
all_emojis = [] tag = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
short_name = f"f{abs(message.chat_id)}_{tag}_by_fcreateemojis"
title = f"EmojiPack {tag}"
stickers = []
for path in files: for path in files:
try: try:
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
@@ -346,7 +315,7 @@ class CreatePacks(loader.Module):
msg = await self._client.send_file("me", file, force_document=True) msg = await self._client.send_file("me", file, force_document=True)
doc = msg.document doc = msg.document
await self._client.delete_messages("me", msg.id) await self._client.delete_messages("me", msg.id)
all_emojis.append(InputStickerSetItem( stickers.append(InputStickerSetItem(
document=InputDocument(doc.id, doc.access_hash, doc.file_reference), document=InputDocument(doc.id, doc.access_hash, doc.file_reference),
emoji=emoji emoji=emoji
)) ))
@@ -354,30 +323,32 @@ class CreatePacks(loader.Module):
logger.error(f"Error loading emoji {path}: {e}") logger.error(f"Error loading emoji {path}: {e}")
continue continue
if not all_emojis: if not stickers:
shutil.rmtree(tmp_dir, ignore_errors=True) shutil.rmtree(tmp_dir, ignore_errors=True)
return await message.edit(self.strings("no_valid")) return await message.edit(self.strings("no_valid"))
created_packs_links = [] try:
pack_number = 1 await self._client(CreateStickerSetRequest(
for i in range(0, len(all_emojis), EMOJI_LIMIT): user_id="me",
current_pack_emojis = all_emojis[i : i + EMOJI_LIMIT] title=title,
short_name, full_title = await self._create_sticker_pack(message, current_pack_emojis, True, pack_number, emoji) short_name=short_name,
if short_name == "already_exists": stickers=stickers,
await message.edit(self.strings("already")) emojis=True
shutil.rmtree(tmp_dir, ignore_errors=True) ))
return await message.edit(self.strings("done_emoji_pack").format(short_name))
elif short_name: except PackShortNameOccupiedError:
created_packs_links.append(f"<a href=\'https://t.me/addstickers/{short_name}\\''>{full_title}</a>") await message.edit(self.strings("already"))
pack_number += 1 except Exception as e:
error_details = f"❌ Ошибка создания эмодзи-пака:\n<code>{type(e).__name__}: {e}</code>\n"
if created_packs_links: error_details += f"Пак: {short_name}\nСмайликов: {len(stickers)}\n"
if len(created_packs_links) == 1: if files:
sn = created_packs_links[0].split('/')[-1].split("'")[0] error_details += f"Последний файл: {files[-1]}\n"
await message.edit(self.strings("done_emoji_pack").format(sn)) try:
else: error_details += f"Размер: {Image.open(files[-1]).size}\n"
await message.edit(self.strings("done_emoji_packs").format("\n".join(created_packs_links))) error_details += f"Вес: {os.path.getsize(files[-1])} байт"
else: except:
await message.edit(self.strings("no_valid")) pass
await message.edit(error_details)
shutil.rmtree(tmp_dir, ignore_errors=True) logger.exception("Error creating emoji pack")
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)

View File

@@ -27,5 +27,4 @@ github
stream stream
placeholders+ placeholders+
PyInstall PyInstall
IwaAnimation IwaAnimation
lateban

View File

@@ -1,320 +0,0 @@
# ______ ___ ___ _ _
# ____ | ___ \ | \/ | | | | |
# / __ \| |_/ / _| . . | ___ __| |_ _| | ___
# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \
# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/
# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___|
# \____/ __/ |
# |___/
# На модуль распространяется лицензия "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"
"<code>.lateban DD.MM.YYYY</code>\n"
"<code>.lateban DD.MM.YYYY HH:MM</code>\n"
"<code>.lateban HH:MM</code> — today"
),
"bad_date": (
"❌ Invalid format. Use <code>DD.MM.YYYY</code>, "
"<code>DD.MM.YYYY HH:MM</code> or <code>HH:MM</code>"
),
"not_chat": "❌ Only works in supergroups",
"no_rights": "❌ No permission to ban members",
"scanning": "🔍 Scanning members who joined after <b>{dt}</b>...",
"confirm": (
"⚠️ Found <b>{count}</b> members who joined after <b>{dt}</b>.\n\n"
"Confirm ban:"
),
"btn_ban": "✅ Ban {count} members",
"btn_cancel": "❌ Cancel",
"banning": "⏳ Banning {count} members...",
"progress": "⏳ Banned {done}/{total}...",
"done": (
"✅ Banned: <b>{banned}</b>\n"
"Skipped (errors/bots): <b>{skipped}</b>\n"
"Service messages deleted: <b>{deleted}</b>"
),
"nobody": "✅ No members found who joined after <b>{dt}</b>.",
}
strings_ru = {
"name": "LateBan",
"_cls_doc": "Заблокируйте всех участников, присоединившихся к чату после указанной даты/времени.",
"no_args": (
"❌ Укажи дату/время:\n"
"<code>.lateban DD.MM.YYYY</code>\n"
"<code>.lateban DD.MM.YYYY HH:MM</code>\n"
"<code>.lateban HH:MM</code>"
),
"bad_date": (
"❌ Неверный формат. Используй <code>DD.MM.YYYY</code>, "
"<code>DD.MM.YYYY HH:MM</code> или <code>HH:MM</code>"
),
"not_chat": "❌ Команда работает только в супергруппах",
"no_rights": "❌ Нет прав на бан участников",
"scanning": "🔍 Сканирую участников, вступивших после <b>{dt}</b>...",
"confirm": (
"⚠️ Найдено <b>{count}</b> участников, вступивших после <b>{dt}</b>.\n\n"
"Подтверди бан:"
),
"btn_ban": "✅ Забанить {count} участников",
"btn_cancel": "❌ Отмена",
"banning": "⏳ Баню {count} участников...",
"progress": "⏳ Забанено {done}/{total}...",
"done": (
"✅ Забанено: <b>{banned}</b>\n"
"Пропущено (ошибки/боты): <b>{skipped}</b>\n"
"Удалено сервисных сообщений: <b>{deleted}</b>"
),
"nobody": "✅ Участников, вступивших после <b>{dt}</b>, не найдено.",
}
async def client_ready(self):
pass
@loader.command(ru_doc="<DD.MM.YYYY [HH:MM] | HH:MM> - Забанить всех, кто присоединился после определённой даты/времени.")
async def latebancmd(self, message):
"""<DD.MM.YYYY [HH:MM] | HH:MM> — 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]

View File

@@ -57,12 +57,6 @@
"gifts":[ "gifts":[
{"id": 5969796561943660080, "emoji": "🧸", "name": "Пасхальный мишка", "price": 50} {"id": 5969796561943660080, "emoji": "🧸", "name": "Пасхальный мишка", "price": 50}
] ]
},
"may_1th": {
"name": "🛠 1 Мая",
"gifts":[
{"id": 6026193266406327981, "emoji": "🧸", "name": "1 Мая мишка", "price": 50}
]
} }
} }
} }

131128
modules.json

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,11 @@
# |_|\_\___| |_| |_|\___/ \__,_|___/ # |_|\_\___| |_| |_|\___/ \__,_|___/
# @ke_mods # @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 # meta developer: @ke_mods

View File

@@ -6,6 +6,11 @@
# |_|\_\___| |_| |_|\___/ \__,_|___/ # |_|\_\___| |_| |_|\___/ \__,_|___/
# @ke_mods # @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 # meta developer: @ke_mods
@@ -39,5 +44,5 @@ class NeofetchMod(loader.Module):
await utils.answer(message, f"<pre>{utils.escape_html(output)}</pre>") await utils.answer(message, f"<pre>{utils.escape_html(output)}</pre>")
except FileNotFoundError: except FileNotFoundError:
await utils.answer(message, self.strings["not_installed"]) await utils.answer(message, self.strings("not_installed"))

View File

@@ -6,6 +6,11 @@
# |_|\_\___| |_| |_|\___/ \__,_|___/ # |_|\_\___| |_| |_|\___/ \__,_|___/
# @ke_mods # @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 # meta developer: @ke_mods
# requires: pillow # requires: pillow
@@ -90,17 +95,17 @@ class PicToStoriesMod(loader.Module):
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
reply = await message.get_reply_message() reply = await message.get_reply_message()
if not reply or not reply.media: if not reply or not reply.media:
await utils.answer(message, self.strings["no_rep"]) await utils.answer(message, self.strings("no_rep"))
return return
try: try:
image_bytes = await reply.download_media(file=bytes) image_bytes = await reply.download_media(file=bytes)
img = Image.open(io.BytesIO(image_bytes)) img = Image.open(io.BytesIO(image_bytes))
except Exception as e: except Exception as e:
await utils.answer(message, self.strings["err"].format(e)) await utils.answer(message, self.strings("err").format(e))
return return
await utils.answer(message, self.strings["work"]) await utils.answer(message, self.strings("work"))
w, h = img.size w, h = img.size
curr_ratio = w / h curr_ratio = w / h
@@ -203,4 +208,4 @@ class PicToStoriesMod(loader.Module):
) )
) )
await utils.answer(message, self.strings["done"]) await utils.answer(message, self.strings("done"))

View File

@@ -6,6 +6,11 @@
# |_|\_\___| |_| |_|\___/ \__,_|___/ # |_|\_\___| |_| |_|\___/ \__,_|___/
# @ke_mods # @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 # meta developer: @ke_mods
# requires: pillow # requires: pillow
@@ -50,13 +55,23 @@ class RandomAnimePicMod(loader.Module):
IMAGES_API_URL = "https://api.nekosapi.com/v4/images" IMAGES_API_URL = "https://api.nekosapi.com/v4/images"
CATEGORIES_SCAN_LIMIT = 500 CATEGORIES_SCAN_LIMIT = 500
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"category",
"",
"Category",
validator=loader.validators.String(),
),
)
@loader.command(ru_doc="- получить рандомную аниме-картинку 👀") @loader.command(ru_doc="- получить рандомную аниме-картинку 👀")
async def rapiccmd(self, message): async def rapiccmd(self, message):
"""- fetch random anime-pic 👀""" """- fetch random anime-pic 👀"""
await utils.answer(message, self.strings["loading"]) await utils.answer(message, self.strings("loading"))
try: try:
category = await utils.get_args_raw().strip() category = self.config["category"].strip()
def fetch_image(): def fetch_image():
params = {"limit": 1, "rating": ["safe"]} params = {"limit": 1, "rating": ["safe"]}
@@ -96,7 +111,7 @@ class RandomAnimePicMod(loader.Module):
url, file = await asyncio.to_thread(fetch_image) url, file = await asyncio.to_thread(fetch_image)
await utils.answer( await utils.answer(
message, message,
self.strings["img"].format(url), self.strings("img").format(url),
file=file file=file
) )
@@ -105,12 +120,12 @@ class RandomAnimePicMod(loader.Module):
"Error fetching random anime pic: %s", "Error fetching random anime pic: %s",
traceback.format_exc(), traceback.format_exc(),
) )
await utils.answer(message, self.strings["error"]) await utils.answer(message, self.strings("error"))
@loader.command(ru_doc="- получить список категорий из API 👀") @loader.command(ru_doc="- получить список категорий из API 👀")
async def racategoriescmd(self, message): async def racategoriescmd(self, message):
"""- fetch categories from api 👀""" """- fetch categories from api 👀"""
await utils.answer(message, self.strings["categories_loading"]) await utils.answer(message, self.strings("categories_loading"))
try: try:
def fetch_categories() -> list[str]: def fetch_categories() -> list[str]:
@@ -147,15 +162,15 @@ class RandomAnimePicMod(loader.Module):
categories = await asyncio.to_thread(fetch_categories) categories = await asyncio.to_thread(fetch_categories)
if not categories: if not categories:
await utils.answer(message, self.strings["no_categories"]) await utils.answer(message, self.strings("no_categories"))
return return
formatted_categories = ", ".join( formatted_categories = "\n".join(
f"<code>{category}</code>" for category in categories f"<code>{category}</code>" for category in categories
) )
await utils.answer( await utils.answer(
message, message,
self.strings["categories"].format(formatted_categories), self.strings("categories").format(formatted_categories),
) )
except Exception: except Exception:
@@ -163,4 +178,4 @@ class RandomAnimePicMod(loader.Module):
"Error fetching categories: %s", "Error fetching categories: %s",
traceback.format_exc(), traceback.format_exc(),
) )
await utils.answer(message, self.strings["error"]) await utils.answer(message, self.strings("error"))

View File

@@ -16,6 +16,11 @@
# @ke_mods # @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 # meta developer: @ke_mods
# requires: telethon spotipy pillow requests yt-dlp curl_cffi # requires: telethon spotipy pillow requests yt-dlp curl_cffi
# scope: ffmpeg # scope: ffmpeg
@@ -34,7 +39,6 @@ import traceback
import os import os
from types import FunctionType from types import FunctionType
import random
import requests import requests
import spotipy import spotipy
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont, ImageOps from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont, ImageOps
@@ -57,9 +61,7 @@ class Banners:
progress: int, progress: int,
track_cover: bytes, track_cover: bytes,
font, font,
blur, blur
album_title: str = "",
meta_info: str = "",
): ):
self.title = title self.title = title
self.artists = ", ".join(artists) if isinstance(artists, list) else artists self.artists = ", ".join(artists) if isinstance(artists, list) else artists
@@ -68,8 +70,6 @@ class Banners:
self.track_cover = track_cover self.track_cover = track_cover
self.font_url = font self.font_url = font
self.blur_intensity = blur self.blur_intensity = blur
self.album_title = album_title
self.meta_info = meta_info
def _get_font(self, size, font_bytes): def _get_font(self, size, font_bytes):
return ImageFont.truetype(io.BytesIO(font_bytes), size) return ImageFont.truetype(io.BytesIO(font_bytes), size)
@@ -237,164 +237,6 @@ class Banners:
by.name = "banner.png" by.name = "banner.png"
return by 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 @loader.tds
class SpotifyMod(loader.Module): class SpotifyMod(loader.Module):
"""Card with the currently playing track on Spotify.""" """Card with the currently playing track on Spotify."""
@@ -507,6 +349,9 @@ class SpotifyMod(loader.Module):
"<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Invalid track number." "<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Invalid track number."
" Please search first or provide a valid number from the list.</b>" " Please search first or provide a valid number from the list.</b>"
), ),
"device_list": (
"<tg-emoji emoji-id=5956561916573782596>📄</tg-emoji> <b>Available devices:</b>\n{}"
),
"no_devices_found": ( "no_devices_found": (
"<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>No devices found.</b>" "<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>No devices found.</b>"
), ),
@@ -514,6 +359,10 @@ class SpotifyMod(loader.Module):
"<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Playback transferred to" "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Playback transferred to"
" {}.</b>" " {}.</b>"
), ),
"invalid_device_id": (
"<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Invalid device ID."
" Use</b> <code>.sdevice</code> <b>to see available devices.</b>"
),
"autobio": ( "autobio": (
"<tg-emoji emoji-id=6319076999105087378>🎧</tg-emoji> <b>Spotify autobio {}</b>" "<tg-emoji emoji-id=6319076999105087378>🎧</tg-emoji> <b>Spotify autobio {}</b>"
), ),
@@ -530,7 +379,6 @@ class SpotifyMod(loader.Module):
"playlist_created": "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Playlist {} created.</b>", "playlist_created": "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Playlist {} created.</b>",
"playlist_deleted": "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Playlist {} deleted.</b>", "playlist_deleted": "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Playlist {} deleted.</b>",
"no_playlist_name": "<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Please specify a playlist name.</b>", "no_playlist_name": "<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Please specify a playlist name.</b>",
"device_select": "<tg-emoji emoji-id=5956561916573782596>📄</tg-emoji> <b>Select playback device:</b>",
} }
strings_ru = { strings_ru = {
@@ -630,6 +478,9 @@ class SpotifyMod(loader.Module):
"<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Некорректный номер трека." "<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Некорректный номер трека."
" Сначала выполните поиск или укажите правильный номер из списка.</b>" " Сначала выполните поиск или укажите правильный номер из списка.</b>"
), ),
"device_list": (
"<tg-emoji emoji-id=5956561916573782596>📄</tg-emoji> <b>Доступные устройства:</b>\n{}"
),
"no_devices_found": ( "no_devices_found": (
"<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Устройства не найдены.</b>" "<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Устройства не найдены.</b>"
), ),
@@ -637,6 +488,10 @@ class SpotifyMod(loader.Module):
"<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Воспроизведение переключено на" "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Воспроизведение переключено на"
" {}.</b>" " {}.</b>"
), ),
"invalid_device_id": (
"<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Некорректный ID устройства."
" Используйте</b> <code>.sdevice</code> <b>, чтобы увидеть доступные устройства.</b>"
),
"autobio": ( "autobio": (
"<tg-emoji emoji-id=6319076999105087378>🎧</tg-emoji> <b>Обновление био" "<tg-emoji emoji-id=6319076999105087378>🎧</tg-emoji> <b>Обновление био"
" включено {}</b>" " включено {}</b>"
@@ -654,7 +509,6 @@ class SpotifyMod(loader.Module):
"playlist_created": "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Плейлист {} создан.</b>", "playlist_created": "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Плейлист {} создан.</b>",
"playlist_deleted": "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Плейлист {} удален.</b>", "playlist_deleted": "<tg-emoji emoji-id=5776375003280838798>✅</tg-emoji> <b>Плейлист {} удален.</b>",
"no_playlist_name": "<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Пожалуйста, укажите название плейлиста.</b>", "no_playlist_name": "<tg-emoji emoji-id=5778527486270770928>❌</tg-emoji> <b>Пожалуйста, укажите название плейлиста.</b>",
"device_select": "<tg-emoji emoji-id=5956561916573782596>📄</tg-emoji> <b>Выберите устройство для воспроизведения:</b>",
} }
def __init__(self): def __init__(self):
@@ -715,7 +569,7 @@ class SpotifyMod(loader.Module):
"banner_version", "banner_version",
"horizontal", "horizontal",
lambda: "Banner version", lambda: "Banner version",
validator=loader.validators.Choice(["horizontal", "vertical", "ultra"]), validator=loader.validators.Choice(["horizontal", "vertical"]),
), ),
loader.ConfigValue( loader.ConfigValue(
"blur_intensity", "blur_intensity",
@@ -735,11 +589,12 @@ class SpotifyMod(loader.Module):
try: try:
self.sp = spotipy.Spotify(auth=access_token) self.sp = spotipy.Spotify(auth=access_token)
return True
except Exception: except Exception:
self.sp = None self.sp = None
return False return False
return True
async def client_ready(self, client, db): async def client_ready(self, client, db):
self.font_ready = asyncio.Event() self.font_ready = asyncio.Event()
@@ -773,6 +628,8 @@ class SpotifyMod(loader.Module):
return await func(*args, **kwargs) return await func(*args, **kwargs)
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
logger.error(f"Error in {func.__name__}: {error_msg}")
if "NO_ACTIVE_DEVICE" in error_msg: if "NO_ACTIVE_DEVICE" in error_msg:
user_error = "No active device" user_error = "No active device"
elif "PREMIUM_REQUIRED" in error_msg: elif "PREMIUM_REQUIRED" in error_msg:
@@ -840,8 +697,8 @@ class SpotifyMod(loader.Module):
await asyncio.sleep(getattr(e, "seconds", 30) + 1) await asyncio.sleep(getattr(e, "seconds", 30) + 1)
except asyncio.CancelledError: except asyncio.CancelledError:
break break
except Exception: except Exception as e:
pass logger.exception("autobio error: %s", e)
await asyncio.sleep(self.config.get("BIO_UPDATE_DELAY", 30)) await asyncio.sleep(self.config.get("BIO_UPDATE_DELAY", 30))
@@ -897,17 +754,20 @@ class SpotifyMod(loader.Module):
reply_to_id=None, reply_to_id=None,
) -> bool: ) -> bool:
dl_dir = os.path.join(os.getcwd(), "spotifymod") dl_dir = os.path.join(os.getcwd(), "spotifymod")
os.makedirs(dl_dir, exist_ok=True) if not os.path.exists(dl_dir):
os.makedirs(dl_dir, exist_ok=True)
for f in os.listdir(dl_dir): for f in os.listdir(dl_dir):
with contextlib.suppress(Exception): try:
os.remove(os.path.join(dl_dir, f)) os.remove(os.path.join(dl_dir, f))
except Exception:
pass
success = False
if caption is None: if caption is None:
caption = self.strings["download_success"].format( safe_track = utils.escape_html(track_name or "Unknown")
utils.escape_html(track_name or "Unknown"), safe_artists = utils.escape_html(artists or "Unknown Artist")
utils.escape_html(artists or "Unknown Artist"), caption = self.strings("download_success").format(safe_track, safe_artists)
)
async def send_text(text: str) -> bool: async def send_text(text: str) -> bool:
if target is None: if target is None:
@@ -929,60 +789,91 @@ class SpotifyMod(loader.Module):
if target is None: if target is None:
return False return False
if isinstance(target, int): 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 return True
try: try:
await utils.answer(target, caption, file=file_path) await utils.answer(target, caption, file=file_path)
return True return True
except Exception as e: except Exception:
logger.error("SpotifyMod send_file fallback: %s", e, exc_info=True)
chat_id = self._get_chat_id(target) chat_id = self._get_chat_id(target)
if chat_id is None: if chat_id is None:
return False 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 return True
success = False
try: try:
squery = query.replace('"', '').replace("'", "") squery = query.replace('"', '').replace("'", "")
cookies = self.config["cookies_path"] cookies = self.config["cookies_path"]
ytdlp_flags = '-x --audio-format mp3 --audio-quality 0 --add-metadata --format "bestaudio/best" --no-playlist'
cookies_flag = f"--cookies {cookies} " if cookies else "" if cookies:
cmd = ( cmd = (
f'{self.config["ytdlp_path"]} {ytdlp_flags} {cookies_flag}' f'{self.config["ytdlp_path"]} -x --impersonate="" --cookies {cookies} --audio-format mp3 --add-metadata '
f'-o "{dl_dir}/%(title)s [%(id)s].%(ext)s" ' f'--audio-quality 0 -o "{dl_dir}/%(title)s [%(id)s].%(ext)s" '
f'"ytsearch1:{squery}"' 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}"'
)
proc = await asyncio.create_subprocess_shell( proc = await asyncio.create_subprocess_shell(
cmd, cmd,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
) )
_, stderr = await proc.communicate() _, stderr = await proc.communicate()
if proc.returncode and log_context:
if proc.returncode: err_text = stderr.decode(errors="ignore").strip() if stderr else ""
err_text = stderr.decode(errors="ignore").strip() if stderr else "yt-dlp failed" err_text = err_text[-400:] if err_text else "yt-dlp failed"
logger.error("SpotifyMod: yt-dlp code %s for %r: %s", proc.returncode, log_context or query, err_text[-400:]) logger.error("Search download failed (%s): %s", log_context, err_text)
files = [f for f in os.listdir(dl_dir) if f.endswith(".mp3")] files = [f for f in os.listdir(dl_dir) if f.endswith(".mp3")]
if files: if files:
success = await send_file(os.path.join(dl_dir, files[0])) first = files[0]
target_file = os.path.join(dl_dir, first)
success = await send_file(target_file)
if not success: if not success:
logger.error("SpotifyMod: failed to send %r (target=%s)", log_context or query, type(target).__name__) if log_context:
await send_text(self.strings["dl_err"]) 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"))
else: else:
logger.error("SpotifyMod: yt-dlp produced no mp3 for %r", log_context or query) if log_context:
await send_text(self.strings["snowt_failed"]) logger.error("Search download produced no files (%s)", log_context)
await send_text(self.strings("snowt_failed"))
except Exception as e: except Exception as e:
logger.error("Download track error (%s): %s", log_context or "no context", e, exc_info=True) if log_context:
await send_text(self.strings["dl_err"]) logger.exception("Search download error (%s)", log_context)
else:
logger.error(e)
await send_text(self.strings("dl_err"))
finally: finally:
for f in os.listdir(dl_dir): if os.path.exists(dl_dir):
with contextlib.suppress(Exception): for f in os.listdir(dl_dir):
os.remove(os.path.join(dl_dir, f)) try:
os.remove(os.path.join(dl_dir, f))
except Exception:
pass
return success return success
@@ -1046,7 +937,7 @@ class SpotifyMod(loader.Module):
await call.answer() await call.answer()
with contextlib.suppress(Exception): 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) target_message = getattr(call, "message", None)
if reply_to_id is None: if reply_to_id is None:
@@ -1060,9 +951,9 @@ class SpotifyMod(loader.Module):
chat_id = self._get_chat_id(call) chat_id = self._get_chat_id(call)
if chat_id is None and target_message is None: if chat_id is None and target_message is None:
pass logger.error("Inline download missing chat_id (%s - %s)", track_name, artists)
with contextlib.suppress(Exception): 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 return
target = chat_id if chat_id is not None else target_message target = chat_id if chat_id is not None else target_message
@@ -1081,14 +972,14 @@ class SpotifyMod(loader.Module):
await call.delete() await call.delete()
else: else:
with contextlib.suppress(Exception): 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): async def _inline_search_tracks(self, query):
if not self.get("acs_tkn", False) or not self.sp: if not self.get("acs_tkn", False) or not self.sp:
return { return {
"title": "Auth required", "title": "Auth required",
"description": "Run .sauth", "description": "Run .sauth",
"message": self.strings["need_auth"], "message": self.strings("need_auth"),
} }
query_text = (query.args or "").strip() query_text = (query.args or "").strip()
@@ -1096,7 +987,7 @@ class SpotifyMod(loader.Module):
return { return {
"title": "No query", "title": "No query",
"description": "Provide search query", "description": "Provide search query",
"message": self.strings["no_search_query"], "message": self.strings("no_search_query"),
} }
try: try:
@@ -1110,7 +1001,7 @@ class SpotifyMod(loader.Module):
return { return {
"title": "Search error", "title": "Search error",
"description": "Try again", "description": "Try again",
"message": self.strings["err"].format( "message": self.strings("err").format(
utils.escape_html(str(e)[:50]) utils.escape_html(str(e)[:50])
), ),
} }
@@ -1119,7 +1010,7 @@ class SpotifyMod(loader.Module):
return { return {
"title": "No results", "title": "No results",
"description": self._short_text(query_text, limit=60), "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) utils.escape_html(query_text)
), ),
} }
@@ -1138,7 +1029,7 @@ class SpotifyMod(loader.Module):
{ {
"title": self._short_text(track_name, limit=60), "title": self._short_text(track_name, limit=60),
"description": self._short_text(artists, limit=60) if artists else "", "description": self._short_text(artists, limit=60) if artists else "",
"message": f'{self.strings["downloading_track"].lstrip()}\n<i>spdl_{store_id}_{i}</i>', "message": f"{self.strings('downloading_track').lstrip()}\n<i>spdl_{store_id}_{i}</i>",
"thumb": thumb, "thumb": thumb,
} }
) )
@@ -1165,22 +1056,22 @@ class SpotifyMod(loader.Module):
"""| .spla - Add current track to playlist (use number from .splaylists | .spls)""" """| .spla - Add current track to playlist (use number from .splaylists | .spls)"""
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args or not args.isdigit(): 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 return
index = int(args) - 1 index = int(args) - 1
playlists = self.get("last_playlists", []) playlists = self.get("last_playlists", [])
if not playlists: if not playlists:
await utils.answer(message, self.strings["no_cached_playlists"]) await utils.answer(message, self.strings("no_cached_playlists"))
return return
if index < 0 or index >= len(playlists): 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 return
current = self.sp.current_playback() current = self.sp.current_playback()
if not current or not current.get("item"): 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 return
track_uri = current["item"]["uri"] track_uri = current["item"]["uri"]
@@ -1192,7 +1083,7 @@ class SpotifyMod(loader.Module):
playlist_name = playlists[index]["name"] playlist_name = playlists[index]["name"]
self.sp.playlist_add_items(playlist_id, [track_uri]) 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 @error_handler
@tokenized @tokenized
@@ -1204,22 +1095,22 @@ class SpotifyMod(loader.Module):
"""| .splr - Remove current track from playlist (use number from .splaylists | .spls)""" """| .splr - Remove current track from playlist (use number from .splaylists | .spls)"""
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args or not args.isdigit(): 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 return
index = int(args) - 1 index = int(args) - 1
playlists = self.get("last_playlists", []) playlists = self.get("last_playlists", [])
if not playlists: if not playlists:
await utils.answer(message, self.strings["no_cached_playlists"]) await utils.answer(message, self.strings("no_cached_playlists"))
return return
if index < 0 or index >= len(playlists): 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 return
current = self.sp.current_playback() current = self.sp.current_playback()
if not current or not current.get("item"): 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 return
track_uri = current["item"]["uri"] track_uri = current["item"]["uri"]
@@ -1231,7 +1122,7 @@ class SpotifyMod(loader.Module):
playlist_name = playlists[index]["name"] playlist_name = playlists[index]["name"]
self.sp.playlist_remove_all_occurrences_of_items(playlist_id, [track_uri]) 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 @error_handler
@tokenized @tokenized
@@ -1243,12 +1134,12 @@ class SpotifyMod(loader.Module):
"""| .splc - 🆕 Create a new playlist""" """| .splc - 🆕 Create a new playlist"""
name = utils.get_args_raw(message) name = utils.get_args_raw(message)
if not name: if not name:
await utils.answer(message, self.strings["no_playlist_name"]) await utils.answer(message, self.strings("no_playlist_name"))
return return
user_id = self.sp.me()["id"] user_id = self.sp.me()["id"]
self.sp.user_playlist_create(user_id, name) 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 @error_handler
@tokenized @tokenized
@@ -1260,24 +1151,24 @@ class SpotifyMod(loader.Module):
"""| .spld - 🗑 Delete playlist (use number from .splaylists | .spls)""" """| .spld - 🗑 Delete playlist (use number from .splaylists | .spls)"""
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args or not args.isdigit(): 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 return
index = int(args) - 1 index = int(args) - 1
playlists = self.get("last_playlists", []) playlists = self.get("last_playlists", [])
if not playlists: if not playlists:
await utils.answer(message, self.strings["no_cached_playlists"]) await utils.answer(message, self.strings("no_cached_playlists"))
return return
if index < 0 or index >= len(playlists): 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 return
playlist_id = playlists[index]["id"] playlist_id = playlists[index]["id"]
playlist_name = playlists[index]["name"] playlist_name = playlists[index]["name"]
self.sp.current_user_unfollow_playlist(playlist_id) 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 @error_handler
@tokenized @tokenized
@@ -1305,9 +1196,9 @@ class SpotifyMod(loader.Module):
playlist_list_text += f"<b>{i + 1}.</b> <a href='{url}'>{name}</a> ({count} tracks)\n" playlist_list_text += f"<b>{i + 1}.</b> <a href='{url}'>{name}</a> ({count} tracks)\n"
if playlist_list_text == "": if playlist_list_text == "":
await utils.answer(message, self.strings["no_playlists"]) await utils.answer(message, self.strings("no_playlists"))
else: 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 @error_handler
@tokenized @tokenized
@@ -1317,7 +1208,7 @@ class SpotifyMod(loader.Module):
async def sbiocmd(self, message): async def sbiocmd(self, message):
"""- Toggle streaming playback in bio""" """- Toggle streaming playback in bio"""
if not getattr(self, "sp", None): if not getattr(self, "sp", None):
await utils.answer(message, self.strings["need_auth"]) await utils.answer(message, self.strings("need_auth"))
return return
state = not self.get("autobio", False) state = not self.get("autobio", False)
@@ -1336,7 +1227,7 @@ class SpotifyMod(loader.Module):
await utils.answer( await utils.answer(
message, message,
self.strings["autobio"].format("on" if state else "off"), self.strings("autobio").format("on" if state else "off"),
) )
@error_handler @error_handler
@@ -1349,63 +1240,64 @@ class SpotifyMod(loader.Module):
"""| .sv - 🔊 Change playback volume. .svolume | .sv <0-100>""" """| .sv - 🔊 Change playback volume. .svolume | .sv <0-100>"""
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if args == "": if args == "":
await utils.answer(message, self.strings["no_volume_arg"]) await utils.answer(message, self.strings("no_volume_arg"))
else: else:
try: try:
volume_percent = int(args) volume_percent = int(args)
if 0 <= volume_percent <= 100: if 0 <= volume_percent <= 100:
self.sp.volume(volume_percent) 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: else:
await utils.answer(message, self.strings["volume_invalid"]) await utils.answer(message, self.strings("volume_invalid"))
except ValueError: except ValueError:
await utils.answer(message, self.strings["volume_invalid"]) await utils.answer(message, self.strings("volume_invalid"))
@error_handler @error_handler
@tokenized @tokenized
@loader.command( @loader.command(
ru_doc="| .sd - 🎵 Выбрать устройство для воспроизведения", ru_doc=(
"| .sd - 🎵 Выбрать устройство для воспроизведения. Например: .sdevice <ID устройства>или .sdevice | .sd для вывода списка устройств"
),
alias="sd" alias="sd"
) )
async def sdevicecmd(self, message: Message): async def sdevicecmd(self, message: Message):
"""| .sd - 🎵 Select playback device""" """| .sd - 🎵 Set preferred playback device. Usage: .sdevice <device_id> or .sdevice | .sd to list devices"""
args = utils.get_args_raw(message)
devices = self.sp.devices()["devices"] devices = self.sp.devices()["devices"]
if not devices:
await utils.answer(message, self.strings["no_devices_found"])
return
async def _switch(call, device_id: str, device_name: str): if args == "":
with contextlib.suppress(Exception): if not devices:
await call.answer() 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"<b>{i+1}.</b> {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
try: try:
self.sp.transfer_playback(device_id=device_id) device_number = int(args)
with contextlib.suppress(Exception): if 0 < device_number <= len(devices):
await call.edit( device_id = devices[device_number - 1]["id"]
self.strings["device_changed"].format(utils.escape_html(device_name)), device_name = devices[device_number - 1]["name"]
reply_markup=None, else:
) await utils.answer(message, self.strings("invalid_device_id"))
except Exception as e: return
with contextlib.suppress(Exception): except ValueError:
await call.edit( found_device = next((d for d in devices if d["id"] == args.strip()), None)
self.strings["err"].format(utils.escape_html(str(e)[:80])), if found_device:
reply_markup=None, device_id = found_device["id"]
) device_name = found_device["name"]
else:
await utils.answer(message, self.strings("invalid_device_id"))
return
keyboard = [] self.sp.transfer_playback(device_id=device_id)
for device in devices: await utils.answer(message, self.strings("device_changed").format(device_name))
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 @error_handler
@tokenized @tokenized
@@ -1415,7 +1307,7 @@ class SpotifyMod(loader.Module):
async def srepeatcmd(self, message: Message): async def srepeatcmd(self, message: Message):
"""- 💫 Repeat""" """- 💫 Repeat"""
self.sp.repeat("track") self.sp.repeat("track")
await utils.answer(message, self.strings["on-repeat"]) await utils.answer(message, self.strings("on-repeat"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1425,7 +1317,7 @@ class SpotifyMod(loader.Module):
async def sderepeatcmd(self, message: Message): async def sderepeatcmd(self, message: Message):
"""- ✋ Stop repeat""" """- ✋ Stop repeat"""
self.sp.repeat("context") self.sp.repeat("context")
await utils.answer(message, self.strings["off-repeat"]) await utils.answer(message, self.strings("off-repeat"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1435,7 +1327,7 @@ class SpotifyMod(loader.Module):
async def snextcmd(self, message: Message): async def snextcmd(self, message: Message):
"""- 👉 Next track""" """- 👉 Next track"""
self.sp.next_track() self.sp.next_track()
await utils.answer(message, self.strings["skipped"]) await utils.answer(message, self.strings("skipped"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1445,7 +1337,7 @@ class SpotifyMod(loader.Module):
async def sresumecmd(self, message: Message): async def sresumecmd(self, message: Message):
"""- 🤚 Resume""" """- 🤚 Resume"""
self.sp.start_playback() self.sp.start_playback()
await utils.answer(message, self.strings["playing"]) await utils.answer(message, self.strings("playing"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1455,7 +1347,7 @@ class SpotifyMod(loader.Module):
async def spausecmd(self, message: Message): async def spausecmd(self, message: Message):
"""- 🤚 Pause""" """- 🤚 Pause"""
self.sp.pause_playback() self.sp.pause_playback()
await utils.answer(message, self.strings["paused"]) await utils.answer(message, self.strings("paused"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1465,7 +1357,7 @@ class SpotifyMod(loader.Module):
async def sbackcmd(self, message: Message): async def sbackcmd(self, message: Message):
"""- ⏮ Previous track""" """- ⏮ Previous track"""
self.sp.previous_track() self.sp.previous_track()
await utils.answer(message, self.strings["back"]) await utils.answer(message, self.strings("back"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1475,7 +1367,7 @@ class SpotifyMod(loader.Module):
async def sbegincmd(self, message: Message): async def sbegincmd(self, message: Message):
"""- ⏪ Restart track""" """- ⏪ Restart track"""
self.sp.seek_track(0) self.sp.seek_track(0)
await utils.answer(message, self.strings["restarted"]) await utils.answer(message, self.strings("restarted"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1486,7 +1378,7 @@ class SpotifyMod(loader.Module):
"""- ❤️ Like current track""" """- ❤️ Like current track"""
cupl = self.sp.current_playback() cupl = self.sp.current_playback()
self.sp.current_user_saved_tracks_add([cupl["item"]["id"]]) 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 @error_handler
@tokenized @tokenized
@@ -1497,7 +1389,7 @@ class SpotifyMod(loader.Module):
"""- 💔 Unlike current track""" """- 💔 Unlike current track"""
cupl = self.sp.current_playback() cupl = self.sp.current_playback()
self.sp.current_user_saved_tracks_delete([cupl["item"]["id"]]) 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 @error_handler
@loader.command( @loader.command(
@@ -1506,12 +1398,12 @@ class SpotifyMod(loader.Module):
async def sauthcmd(self, message: Message): async def sauthcmd(self, message: Message):
"""- Get authorization link""" """- Get authorization link"""
if self.get("acs_tkn", False) and not self.sp: 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: else:
self.sp_auth.get_authorize_url() self.sp_auth.get_authorize_url()
await utils.answer( await utils.answer(
message, message,
self.strings["auth"].format(self.sp_auth.get_authorize_url()), self.strings("auth").format(self.sp_auth.get_authorize_url()),
) )
@error_handler @error_handler
@@ -1524,7 +1416,7 @@ class SpotifyMod(loader.Module):
code = self.sp_auth.parse_auth_response_url(url) code = self.sp_auth.parse_auth_response_url(url)
self.set("acs_tkn", self.sp_auth.get_access_token(code, True, False)) self.set("acs_tkn", self.sp_auth.get_access_token(code, True, False))
self._init_spotify_client() self._init_spotify_client()
await utils.answer(message, self.strings["authed"]) await utils.answer(message, self.strings("authed"))
@error_handler @error_handler
@loader.command( @loader.command(
@@ -1534,7 +1426,7 @@ class SpotifyMod(loader.Module):
"""- Log out of account""" """- Log out of account"""
self.set("acs_tkn", None) self.set("acs_tkn", None)
self.sp = None self.sp = None
await utils.answer(message, self.strings["deauth"]) await utils.answer(message, self.strings("deauth"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1550,7 +1442,7 @@ class SpotifyMod(loader.Module):
) )
self.set("NextRefresh", time.time() + 45 * 60) self.set("NextRefresh", time.time() + 45 * 60)
self._init_spotify_client() self._init_spotify_client()
await utils.answer(message, self.strings["authed"]) await utils.answer(message, self.strings("authed"))
@error_handler @error_handler
@tokenized @tokenized
@@ -1562,7 +1454,7 @@ class SpotifyMod(loader.Module):
"""| .sn - 🎧 View current track card.""" """| .sn - 🎧 View current track card."""
current_playback = self.sp.current_playback() current_playback = self.sp.current_playback()
if not current_playback or not current_playback.get("is_playing", False): 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 return
track = current_playback["item"]["name"] track = current_playback["item"]["name"]
@@ -1623,7 +1515,7 @@ class SpotifyMod(loader.Module):
if self.config["show_banner"]: if self.config["show_banner"]:
cover_url = current_playback["item"]["album"]["images"][0]["url"] 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( banners = Banners(
title=track, title=track,
@@ -1633,14 +1525,9 @@ class SpotifyMod(loader.Module):
track_cover=requests.get(cover_url).content, track_cover=requests.get(cover_url).content,
font=self.config["font"], font=self.config["font"],
blur=self.config["blur_intensity"], blur=self.config["blur_intensity"],
album_title=album_name,
meta_info="Spotify",
) )
version = self.config["banner_version"] if self.config["banner_version"] == "vertical":
if version == "ultra":
file = banners.ultra()
elif version == "vertical":
file = banners.vertical() file = banners.vertical()
else: else:
file = banners.horizontal() file = banners.horizontal()
@@ -1659,7 +1546,7 @@ class SpotifyMod(loader.Module):
"""| .snt - 🎧 Download current track.""" """| .snt - 🎧 Download current track."""
current_playback = self.sp.current_playback() current_playback = self.sp.current_playback()
if not current_playback or not current_playback.get("is_playing", False): 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 return
track = current_playback["item"]["name"] track = current_playback["item"]["name"]
@@ -1716,16 +1603,9 @@ class SpotifyMod(loader.Module):
text = self.config["custom_text"].format(**data) text = self.config["custom_text"].format(**data)
msg = await utils.answer(message, text + self.strings["downloading_track"]) msg = await utils.answer(message, text + self.strings("downloading_track"))
await self._download_track( await self._download_track(msg, f"{artists} {track}", caption=text)
msg,
f"{artists} {track}",
caption=text,
track_name=track,
artists=artists,
log_context=f"{track} - {artists}",
)
@error_handler @error_handler
@tokenized @tokenized
@@ -1737,7 +1617,7 @@ class SpotifyMod(loader.Module):
"""| .sq - 🔍 Search for tracks.""" """| .sq - 🔍 Search for tracks."""
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args: if not args:
await utils.answer(message, self.strings["no_search_query"]) await utils.answer(message, self.strings("no_search_query"))
return return
search_results = self.get("last_search_results", []) search_results = self.get("last_search_results", [])
@@ -1750,7 +1630,7 @@ class SpotifyMod(loader.Module):
if is_selection: if is_selection:
track_number = int(args) 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_info = search_results[track_number - 1]
track_name, artists = self._track_info(track_info) track_name, artists = self._track_info(track_info)
reply_to_id = self._reply_id(message) reply_to_id = self._reply_id(message)
@@ -1779,7 +1659,7 @@ class SpotifyMod(loader.Module):
) )
if not results or not results["tracks"]["items"]: 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 return
tracks = results["tracks"]["items"] tracks = results["tracks"]["items"]
@@ -1788,7 +1668,7 @@ class SpotifyMod(loader.Module):
reply_to_id = self._reply_id(message) reply_to_id = self._reply_id(message)
await self.inline.form( await self.inline.form(
self.strings["search_results_inline"].format( self.strings("search_results_inline").format(
count=len(tracks), count=len(tracks),
query=utils.escape_html(args), query=utils.escape_html(args),
), ),
@@ -1837,22 +1717,17 @@ class SpotifyMod(loader.Module):
next_refresh = self.get("NextRefresh") next_refresh = self.get("NextRefresh")
if not next_refresh or next_refresh < time.time(): 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: try:
new_token = self.sp_auth.refresh_access_token(acs_tkn["refresh_token"]) self.set(
self.set("acs_tkn", new_token) "acs_tkn",
self.sp_auth.refresh_access_token(self.get("acs_tkn")["refresh_token"]),
)
self.set("NextRefresh", time.time() + 45 * 60) self.set("NextRefresh", time.time() + 45 * 60)
if new_token and new_token.get("access_token"): self.sp = spotipy.Spotify(auth=self.get("acs_tkn")["access_token"])
self.sp = spotipy.Spotify(auth=new_token["access_token"])
logger.debug("Token refreshed successfully")
except Exception as e: except Exception as e:
logger.error("Token refresh error: %s", e, exc_info=True) logger.error(f"Spotify watcher error: {e}")
if "Refresh token revoked" in str(e): if "Refresh token revoked" in str(e):
logger.warning("Refresh token revoked, re-authenticating")
refresh_token = await self.invoke("stokrefresh", "", self.inline.bot.id) refresh_token = await self.invoke("stokrefresh", "", self.inline.bot.id)
await refresh_token.delete() await refresh_token.delete()
else: else:
self.set("NextRefresh", time.time() + 300) self.set("NextRefresh", time.time() + 300)

View File

@@ -6,6 +6,11 @@
# |_|\_\___| |_| |_|\___/ \__,_|___/ # |_|\_\___| |_| |_|\___/ \__,_|___/
# @ke_mods # @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 # meta developer: @ke_mods
@@ -38,10 +43,10 @@ class UnbanAllMod(loader.Module):
chat = await message.get_chat() chat = await message.get_chat()
if not chat.admin_rights and not chat.creator: 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 return
await utils.answer(message, self.strings["unban_in_process"]) await utils.answer(message, self.strings("unban_in_process"))
no_banned = True no_banned = True
@@ -59,11 +64,11 @@ class UnbanAllMod(loader.Module):
)) ))
except Exception as e: 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 pass
if no_banned: if no_banned:
await utils.answer(message, self.strings["no_banned"]) await utils.answer(message, self.strings("no_banned"))
return return
await utils.answer(message, self.strings["success"]) await utils.answer(message, self.strings("success"))

View File

@@ -6,6 +6,11 @@
# |_|\_\___| |_| |_|\___/ \__,_|___/ # |_|\_\___| |_| |_|\___/ \__,_|___/
# @ke_mods # @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 # meta developer: @ke_mods
# scope: ffmpeg # scope: ffmpeg

View File

@@ -1,4 +1,4 @@
__version__ = (1, 3, 0, 0) __version__ = (1, 2, 0, 0)
# This file is a part of Hikka Userbot! # This file is a part of Hikka Userbot!
# This product includes software developed by t.me/Fl1yd and t.me/spypm. # This product includes software developed by t.me/Fl1yd and t.me/spypm.
@@ -19,10 +19,6 @@ __version__ = (1, 3, 0, 0)
# - Added: Proxy for users from RF # - Added: Proxy for users from RF
# - Fixed: Correct reply author resolving for forwarded messages # - 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
# █▄█ █░█ █▀▄▀█ █▀▄▀█ █▄█   █▀▄▀█ █▀█ █▀▄ █▀ # █▄█ █░█ █▀▄▀█ █▀▄▀█ █▄█   █▀▄▀█ █▀█ █▀▄ █▀
# ░█░ █▄█ █░▀░█ █░▀░█ ░█░   █░▀░█ █▄█ █▄▀ ▄█ # ░█░ █▄█ █░▀░█ █░▀░█ ░█░   █░▀░█ █▄█ █▄▀ ▄█
@@ -159,10 +155,9 @@ class Dick:
return None return None
@staticmethod @staticmethod
async def post(url: str, data: dict, proxy: Optional[str] = None): async def post(url: str, data: dict):
try: try:
px = {"http": proxy, "https": proxy} if proxy else None return await utils.run_sync(requests.post, url, json=data, timeout=30)
return await utils.run_sync(requests.post, url, json=data, timeout=30, proxies=px)
except Exception: except Exception:
return None return None
@@ -204,8 +199,12 @@ class Quotes(loader.Module):
loader.ConfigValue("endpoint","https://kok.gay/gayotes/generate", loader.ConfigValue("endpoint","https://kok.gay/gayotes/generate",
lambda:"URL API-эндпоинта (можешь поднять локально - github.com/yummy1gay/quote-api)", lambda:"URL API-эндпоинта (можешь поднять локально - github.com/yummy1gay/quote-api)",
validator=loader.validators.Link()), validator=loader.validators.Link()),
loader.ConfigValue("proxy", "", loader.ConfigValue("use_rf_proxy", False,
lambda:"Прокси для обхода блокировок (например: http://user:pass@ip:port). Оставь пустым, если не нужно.")) lambda:'Включает прокси для РФ, если основной эндпоинт возвращает ошибку "Нетворк еррорь", и при этом сервер с юзерботом находится в России или ты сам сидишь в России с ограниченным доступом к зарубежным ресурсам (Termux / UserLAnd)',
validator=loader.validators.Boolean()),
loader.ConfigValue("rf_endpoint", "https://ru.kok.gay/gayotes/generate",
lambda:"URL API-эндпоинта для РФ",
validator=loader.validators.Link()))
async def client_ready(self, client, db): async def client_ready(self, client, db):
self.client=client; self.db=db self.client=client; self.db=db
@@ -237,11 +236,11 @@ class Quotes(loader.Module):
"format": "webp" if not doc else "png", "type": self.config["type"]} "format": "webp" if not doc else "png", "type": self.config["type"]}
await utils.answer(st,self.strings["api_processing"]) await utils.answer(st,self.strings["api_processing"])
prx = self.config["proxy"] if self.config["proxy"] else None endpoint=self.config['rf_endpoint'] if self.config['use_rf_proxy'] else self.config['endpoint']
r=await Dick.post(f"{self.config['endpoint']}.webp",pay,proxy=prx) r=await Dick.post(f"{endpoint}.webp",pay)
if not r or r.status_code!=200: if not r or r.status_code!=200:
try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй указать прокси в конфиге)" try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй включить <code>use_rf_proxy</code> в конфиге)"
except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй указать прокси в конфиге)" except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй включить <code>use_rf_proxy</code> в конфиге)"
return await utils.answer(st,self.strings["api_error"].format(err)) return await utils.answer(st,self.strings["api_error"].format(err))
buf=io.BytesIO(r.content); buf.name="YgQuote"+(".png" if doc else ".webp") buf=io.BytesIO(r.content); buf.name="YgQuote"+(".png" if doc else ".webp")
@@ -271,11 +270,11 @@ class Quotes(loader.Module):
"format": "webp","type":self.config["type"]} "format": "webp","type":self.config["type"]}
await utils.answer(st,self.strings["api_processing"]) await utils.answer(st,self.strings["api_processing"])
prx = self.config["proxy"] if self.config["proxy"] else None endpoint=self.config['rf_endpoint'] if self.config['use_rf_proxy'] else self.config['endpoint']
r=await Dick.post(f"{self.config['endpoint']}.webp",dickk,proxy=prx) r=await Dick.post(f"{endpoint}.webp",dickk)
if not r or r.status_code!=200: if not r or r.status_code!=200:
try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй указать прокси в конфиге)" try: err=r.json().get("error",f"HTTP {r.status_code}") if r else "Нетворк еррорь (попробуй включить <code>use_rf_proxy</code> в конфиге)"
except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй указать прокси в конфиге)" except Exception: err=f"HTTP {r.status_code}" if r else "Нетворк еррорь (попробуй включить <code>use_rf_proxy</code> в конфиге)"
return await utils.answer(st,self.strings["api_error"].format(err)) return await utils.answer(st,self.strings["api_error"].format(err))
buf=io.BytesIO(r.content); buf.name="YgQuote.webp" buf=io.BytesIO(r.content); buf.name="YgQuote.webp"
@@ -291,18 +290,12 @@ class Quotes(loader.Module):
return None return None
out: List[dict]=[] out: List[dict]=[]
prev_sender_id = None
for mm in lst: for mm in lst:
try: try:
u=await self.who(mm) u=await self.who(mm)
if not u: continue 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) 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 rb=None
try: try:
@@ -322,16 +315,10 @@ class Quotes(loader.Module):
txt=mm.raw_text or ""; ad=Dick.desc(mm) txt=mm.raw_text or ""; ad=Dick.desc(mm)
if ad: txt=f"{txt}\n\n{ad}" if txt else ad if ad: txt=f"{txt}\n\n{ad}" if txt else ad
if is_chained: item={"from":{"id":getattr(u,"id", 0),"first_name":getattr(u,"first_name","") or f,"last_name":getattr(u,"last_name","") or l,
item={"from":{"id":current_sender_id,"name":""}, "username":getattr(u,"username",None),"name":name,"photo":{"url":ava} if ava else {}},
"text":txt,"entities":Dick.ents(mm.entities),"avatar":False} "text":txt,"entities":Dick.ents(mm.entities),"avatar":True}
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: try:
if mm.voice: if mm.voice:
a = next((a for a in mm.voice.attributes or [] a = next((a for a in mm.voice.attributes or []
@@ -340,10 +327,11 @@ class Quotes(loader.Module):
except Exception: pass except Exception: pass
if med: item["voice" if "voice" in med else "media"] = med.get("voice", med) 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 if rb: item["replyMessage"]=rb
out.append(item) out.append(item)
prev_sender_id = current_sender_id
except Exception: continue except Exception: continue
return out return out
@@ -390,8 +378,6 @@ class Quotes(loader.Module):
return await self.fake(f"{getattr(u,'id','')} {args}", None) return await self.fake(f"{getattr(u,'id','')} {args}", None)
out: List[dict]=[] out: List[dict]=[]
prev_sender_id = None
for part in args.split("; "): for part in args.split("; "):
try: try:
rb=None rb=None
@@ -402,32 +388,22 @@ class Quotes(loader.Module):
if not u1: continue if not u1: continue
txt1, ents1 = html.parse(t1) if t1 else ("", []) 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) 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: if u2:
txt2, ents2 = html.parse(t2) if t2 else ("", []) txt2, ents2 = html.parse(t2) if t2 else ("", [])
name2=telethon.utils.get_display_name(u2); ava2=await Dick.ava(self.client,u2.id) 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 {}}} rb={"name":name2,"text":txt2,"entities":Dick.ents(ents2),"chatId":u2.id,"from":{"name":name2,"photo":{"url":ava2} if ava2 else {}}}
if is_chained: msg={"from":{"id":u1.id,"first_name":getattr(u1,"first_name","") or f,"last_name":getattr(u1,"last_name","") or l,
msg={"from":{"id":current_sender_id,"name":""}, "username":getattr(u1,"username",None),"name":name,"photo":{"url":ava} if ava else {}},
"text":txt1,"entities":Dick.ents(ents1), "avatar":False} "text":txt1,"entities":Dick.ents(ents1), "avatar":True}
else:
msg={"from":{"id":current_sender_id,"first_name":getattr(u1,"first_name","") or f,"last_name":getattr(u1,"last_name","") or l, es=getattr(u1,"emoji_status",None)
"username":getattr(u1,"username",None),"name":name,"photo":{"url":ava} if ava else {}}, if getattr(es,"document_id",None): msg["from"]["emoji_status"]=str(es.document_id)
"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 if rb: msg["replyMessage"]=rb
out.append(msg) out.append(msg)
prev_sender_id = current_sender_id
except Exception: continue except Exception: continue
return out return out