diff --git a/KorenbZla/HikkaModules/Send.py b/KorenbZla/HikkaModules/Send.py index 71f8155..794cd66 100644 --- a/KorenbZla/HikkaModules/Send.py +++ b/KorenbZla/HikkaModules/Send.py @@ -23,10 +23,11 @@ # meta pic: https://i.postimg.cc/Hx3Zm8rB/logo.png # meta banner: https://te.legra.ph/file/55fa6eebae860a359ac27.jpg -__version__ = (1, 2, 0) +__version__ = (1, 3, 2) -from .. import loader, utils -from telethon.tl.types import Message # type: ignore +from .. import loader, utils # type: ignore +from telethon.tl import types # type: ignore +from telethon.tl.types import Message, InputDocument # type: ignore @loader.tds class SendMod(loader.Module): @@ -104,19 +105,68 @@ class SendMod(loader.Module): @loader.command( - ru_doc="[text] - Написать сообщение в закрытую тему", - uz_doc="[text] - Yopiq mavzuga xabar yozing", - de_doc="[text] - Schreiben Sie eine Nachricht zu einem geschlossenen Thema", - es_doc="[text] - Escribir un mensaje a un tema cerrado", + ru_doc="[text or reply(media/file/sticker) or coordinates (, )] - Написать сообщение в закрытую тему", + uz_doc="[text or reply(media/file/sticker) or coordinates (, )] - Yopiq mavzuga xabar yozing", + de_doc="[text or reply(media/file/sticker) or coordinates (, )] - Schreiben Sie eine Nachricht zu einem geschlossenen Thema", + es_doc="[text or reply(media/file/sticker) or coordinates (, )] - Escribir un mensaje a un tema cerrado", ) - async def sendclosedtopic(self, message: Message): - """[text] - Write a message to a closed topic""" - if not utils.get_args_raw(message): - await utils.answer(message, self.strings["error_send"]) + async def sendclosedtopic(self, message: Message): + """[text or reply(media/file/sticker) or coordinates (, )] - Write a message to a closed topic""" + args = utils.get_args_raw(message) + message_text = args if args else "" + reply = await message.get_reply_message() + + media = None + temp_file = None + + if reply and reply.media: + doc = getattr(reply.media, "document", None) + + if doc and any(a.__class__.__name__ == "DocumentAttributeSticker" for a in doc.attributes): + media = InputDocument( + id=doc.id, + access_hash=doc.access_hash, + file_reference=doc.file_reference + ) + message_text = "" + + elif doc and doc.mime_type == "image/webp": + temp_file = await reply.download_media() + media = temp_file + else: + media = reply.media else: - text2 = f"{utils.get_args_raw(message)}" - await message.delete() - await message.reply(text2) + media = message.media + + if message_text and "," in message_text: + lat_str, long_str = message_text.split(",", 1) + try: + gps_x = float(lat_str.strip()) + gps_y = float(long_str.strip()) + if -90 <= gps_x <= 90 and -180 <= gps_y <= 180: + geo_point = types.InputGeoPoint(lat=gps_x, long=gps_y) + media = types.InputMediaGeoPoint(geo_point) + message_text = "" + except ValueError: + pass + + if not message_text and not media: + await utils.answer(message, self.strings["error_send_2"]) + return + + await message.delete() + await message.reply( + message_text, + file=media if media else None, + parse_mode="html" + ) + + if temp_file: + import os + try: + os.remove(temp_file) + except: + pass @loader.command( ru_doc="[@UserName] [text or replay] - Написать сообщение в личные сообщения", diff --git a/Ruslan-Isaev/modules/youtube-loader.py b/Ruslan-Isaev/modules/youtube-loader.py index 66c11f9..35b59ca 100644 --- a/Ruslan-Isaev/modules/youtube-loader.py +++ b/Ruslan-Isaev/modules/youtube-loader.py @@ -1,4 +1,4 @@ -__version__ = (1, 1, 0) +version = (1, 4, 0) # meta developer: @RUIS_VlP, @RoKrz # requires: yt_dlp @@ -7,51 +7,190 @@ import yt_dlp import uuid import os import re +import tempfile from .. import loader, utils -def extract_youtube_link(text): +def extract_video_link(text): + """Извлекает видео с сайтов""" if not text: return None - match = re.search(r"(https?://)?(www\.)?(youtube\.com|youtu\.be)/[^\s]+", text) - return match.group(0) if match else None + video_sites_patterns = [ + # YouTube + r"(https?://)?(www\.)?(youtube\.com|youtu\.be)/[^\s]+", + # Social Media + r"(https?://)?(www\.)?(tiktok\.com|vt\.tiktok\.com|vm\.tiktok\.com)/[^\s]+", + r"(https?://)?(www\.)?instagram\.com/(p|reel|tv)/[^\s]+", + r"(https?://)?(www\.)?(twitter\.com|x\.com)/[^\s]+/status/[^\s]+", + r"(https?://)?(www\.)?facebook\.com/[^\s]+/videos/[^\s]+", + r"(https?://)?(www\.)?reddit\.com/r/[^\s]+/comments/[^\s]+", -async def download_video(url): + # Video Platforms + r"(https?://)?(www\.)?vimeo\.com/[^\s]+", + r"(https?://)?(www\.)?dailymotion\.com/video/[^\s]+", + r"(https?://)?(www\.)?twitch\.tv/(videos/|clip/|[^/]+$)[^\s]*", + r"(https?://)?(www\.)?streamable\.com/[^\s]+", + + # News & Media + r"(https?://)?(www\.)?bbc\.co\.uk/iplayer/[^\s]+", + r"(https?://)?(www\.)?cnn\.com/videos/[^\s]+", + r"(https?://)?(www\.)?reuters\.com/video/[^\s]+", + + # Educational + r"(https?://)?(www\.)?coursera\.org/learn/[^\s]+", + r"(https?://)?(www\.)?udemy\.com/course/[^\s]+", + r"(https?://)?(www\.)?khanacademy\.org/[^\s]+", + + # Russian platforms + r"(https?://)?(www\.)?rutube\.ru/video/[^\s]+", + r"(https?://)?(www\.)?vk\.com/(video|clip)[^\s]+", + r"(https?://)?(www\.)?ok\.ru/video/[^\s]+", + + # Other popular platforms + r"(https?://)?(www\.)?pornhub\.com/view_video\.php\?viewkey=[^\s]+", + r"(https?://)?(www\.)?xvideos\.com/video[^\s]+", + r"(https?://)?(www\.)?soundcloud\.com/[^\s]+", + r"(https?://)?(www\.)?bandcamp\.com/track/[^\s]+", + r"(https?://)?(www\.)?mixcloud\.com/[^\s]+", + + # Live streaming + r"(https?://)?(www\.)?periscope\.tv/[^\s]+", + r"(https?://)?(www\.)?ustream\.tv/[^\s]+", + + # International + r"(https?://)?(www\.)?bilibili\.com/video/[^\s]+", + r"(https?://)?(www\.)?niconico\.jp/watch/[^\s]+", + r"(https?://)?(www\.)?youku\.com/v_show/[^\s]+", + + # Generic fallback for other video URLs + r"https?://[^\s]+\.(mp4|webm|avi|mkv|mov|flv|m4v)", + ] + for pattern in video_sites_patterns: + match = re.search(pattern, text, re.IGNORECASE) + if match: + return match.group(0) + general_url_pattern = r"https?://[^\s]+" + match = re.search(general_url_pattern, text) + if match: + url = match.group(0) + excluded_domains = [ + 'google.com', 'yandex.ru', 'wikipedia.org', 'github.com', + 'stackoverflow.com', 'reddit.com/r/', 'amazon.com' + ] + if not any(domain in url.lower() for domain in excluded_domains): + return url + return None +async def download_video(url, cookies_text=None, youtube_client="default", custom_user_agent=None): output_dir = utils.get_base_dir() random_uuid = str(uuid.uuid4()) os.makedirs(output_dir, exist_ok=True) - ydl_opts = { - 'format': 'best', - 'outtmpl': os.path.join(output_dir, f'{random_uuid}.%(ext)s'), - 'noplaylist': True, - } - - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - info_dict = ydl.extract_info(url, download=True) - video_ext = info_dict.get('ext', None) - file_path = os.path.join(output_dir, f"{random_uuid}.{video_ext}") - title = info_dict.get('title', None) - - return file_path, title + formats_to_try = [ + 'best[ext=mp4]', + 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]', + 'bestvideo+bestaudio/best', + 'best', + 'best*', + 'bestvideo+bestaudio', + 'best[height<=1080]', + 'best[height<=720]', + 'worst', + 'worst*', + ] + cookies_file = None + if cookies_text and cookies_text.strip(): + cleaned_cookies = cookies_text.strip() + if cleaned_cookies.startswith('"') or cleaned_cookies.startswith("'"): + cleaned_cookies = cleaned_cookies[1:] + if cleaned_cookies.endswith('"') or cleaned_cookies.endswith("'"): + cleaned_cookies = cleaned_cookies[:-1] + cookies_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8') + cookies_file.write(cleaned_cookies) + cookies_file.close() + try: + is_youtube = 'youtube.com' in url.lower() or 'youtu.be' in url.lower() + for format_option in formats_to_try: + ydl_opts = { + 'format': format_option, + 'outtmpl': os.path.join(output_dir, f'{random_uuid}.%(ext)s'), + 'noplaylist': True, + 'merge_output_format': 'mp4', + } + if cookies_file: + ydl_opts['cookiefile'] = cookies_file.name + if custom_user_agent: + ydl_opts['http_headers'] = {'User-Agent': custom_user_agent} + if is_youtube and youtube_client != "default": + ydl_opts['extractor_args'] = {'youtube': {'player_client': [youtube_client]}} + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info_dict = ydl.extract_info(url, download=True) + video_ext = info_dict.get('ext', None) + file_path = os.path.join(output_dir, f"{random_uuid}.{video_ext}") + title = info_dict.get('title', None) + channel = info_dict.get('uploader', None) + return file_path, title, channel + except Exception as e: + if "Requested format is not available" in str(e) or "No video formats found" in str(e): + continue + else: + raise e + raise Exception("Не удалось скачать видео ни в одном формате") + finally: + if cookies_file: + try: + os.unlink(cookies_file.name) + except: + pass def convert_markdown_to_html(template: str, link: str) -> str: return re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1', template).replace("{link}", link) - - @loader.tds class YouTube_DLDMod(loader.Module): - """Помогает скачивать видео с YouTube""" - + """Помогает скачивать видео с YouTube, TikTok и др.""" strings = { "name": "YouTube-DLD", - "no_link": "❌ Пожалуйста, укажите ссылку на YouTube либо ответьте на сообщение с ней.", + "no_link": "❌ Пожалуйста, укажите ссылку на видео либо ответьте на сообщение с ней.", "default_downloading": "📥 Начинаю загрузку видео.\n\nℹ️ Это может занять до 5 минут, в зависимости от длины и качества видео.", "default_error": "❌ Ошибка!\n\n{}", "default_response": "🎥 Вот [ваше видео]({link})!\n\n{title}", - } + "default_channel": "📺 Канал: {channel}", + "cookies_error": "🍪 YouTube требует аутентификацию!\n\n❌ Ошибка: Sign in to confirm you're not a bot\n\nВозможные причины:\n▫️ YouTube детектит запросы без аутентификации\n▫️ IP сервера может быть заблокирован YouTube\n▫️ Видео требует подтверждения возраста/входа\n\nРешения (попробуй по порядку):\n\n1️⃣ Смени YouTube клиент:\n• Открой .cfg YouTube-DLD\n• Попробуй разные значения youtube_client:\n - mweb (мобильная веб-версия, часто работает)\n - android (Android приложение, сработало на сервере во Франции)\n - ios (iOS приложение)\n - tv_embedded (встроенный ТВ плеер)\n\n2️⃣ Добавь куки (для пользователей из проблемных регионов):\n• Открой НОВОЕ приватное окно (в браузере ctrl+shift+N) и залогинься на YouTube\n• Перейди на https://www.youtube.com/robots.txt в ТОЙ же вкладке\n• Cookie-Editor (расширение) → Export → Netscape format\n• СРАЗУ закрой приватное окно\n• Вставь куки в youtube_cookies (БЕЗ кавычек)", + "supported_sites": """🎥 Поддерживаемые сайты: +🔴 YouTube — youtube.com, youtu.be +🎵 TikTok — tiktok.com, vt.tiktok.com, vm.tiktok.com +📸 Instagram — instagram.com (посты, reels, IGTV) +🐦 X (Twitter) — x.com, twitter.com +👥 Facebook — facebook.com (видео) +🎬 Vimeo — vimeo.com +📺 Twitch — twitch.tv (стримы, клипы, VOD) +🤖 Reddit — reddit.com (видео из постов) +⚡ Dailymotion — dailymotion.com + +🇷🇺 Российские: +▫️ RuTube — rutube.ru +▫️ ВКонтакте — vk.com (видео) +▫️ Одноклассники — ok.ru + +📚 Образовательные: +▫️ Coursera — coursera.org +▫️ Udemy — udemy.com +▫️ Khan Academy — khanacademy.org + +🌍 Международные: +▫️ Bilibili — bilibili.com +▫️ NicoNico — niconico.jp +▫️ BBC iPlayer — bbc.co.uk/iplayer + +🎵 Аудио: +▫️ SoundCloud — soundcloud.com +▫️ Bandcamp — bandcamp.com +▫️ Mixcloud — mixcloud.com + +И многие другие платформы...""" + } def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue( @@ -75,32 +214,84 @@ class YouTube_DLDMod(loader.Module): self.strings["default_response"], "Ответ после загрузки. (используй {link} для ссылки и {title} для названия видео)" ), + loader.ConfigValue( + "show_channel", + True, + "Показывать название канала?", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "channel_text", + self.strings["default_channel"], + "Текст для отображения канала. (используй {channel} для имени канала)" + ), + loader.ConfigValue( + "youtube_cookies", + "", + "🍪 Куки YouTube в формате Netscape (опционально)\n\n" + "⚠️ ВНИМАНИЕ: Риск бана аккаунта! Используй тестовый аккаунт.\n\n" + "Как получить:\n" + "1. НОВОЕ приватное окно (Ctrl+Shift+N) → залогинься на YouTube\n" + "2. Перейди на https://www.youtube.com/robots.txt в той же вкладке\n" + "3. Cookie-Editor (расширение) → Export → Netscape format\n" + "4. СРАЗУ закрой приватное окно\n" + "5. Вставь текст сюда (БЕЗ кавычек)", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "youtube_client", + "mweb", + "📱 YouTube клиент для обхода блокировок\n\n" + "Доступные варианты:\n" + "• default - стандартный (может не работать)\n" + "• mweb - мобильная веб-версия\n" + "• android - Android приложение (рекомендуется)\n" + "• ios - iOS приложение\n" + "• tv_embedded - встроенный ТВ плеер\n\n" + "Если видео не скачивается, попробуй другой клиент!", + validator=loader.validators.Choice(["default", "mweb", "android", "ios", "tv_embedded"]), + ), + loader.ConfigValue( + "custom_user_agent", + "", + "🌐 Кастомный User-Agent (опционально)\n\n" + "Можно указать User-Agent браузера для обхода некоторых блокировок.\n" + "Например:\n" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\n\n" + "Оставь пустым для использования стандартного.", + validator=loader.validators.String(), + ), ) @loader.command() - async def dlvideo(self, message): - """<ссылка> или ответ на сообщение со ссылкой — скачивает видео с YouTube""" + async def dvlist(self, message): + """Показать список всех поддерживаемых сайтов""" + await utils.answer(message, self.strings["supported_sites"]) + @loader.command() + async def dlvideo(self, message): + """<ссылка> или ответ на сообщение со ссылкой — скачивает видео с поддерживаемых платформ""" args = utils.get_args_raw(message) reply = await message.get_reply_message() - - link = extract_youtube_link(args) if args else None + link = extract_video_link(args) if args else None if not link and reply: - link = extract_youtube_link(reply.raw_text) - + link = extract_video_link(reply.raw_text) if not link: await utils.answer(message, self.strings["no_link"]) return - await utils.answer(message, self.config["downloading_text"]) - try: - video, title = await download_video(link) - + cookies_text = self.config["youtube_cookies"].strip() if self.config["youtube_cookies"] else None + youtube_client = self.config["youtube_client"] + user_agent = self.config["custom_user_agent"].strip() if self.config["custom_user_agent"] else None + video, title, channel = await download_video(link, cookies_text, youtube_client, user_agent) if self.config["show_link"]: caption_template = self.config["response_text"] caption = convert_markdown_to_html(caption_template, link) caption = caption.replace("{title}", title or "") + if self.config["show_channel"] and channel: + channel_text = self.config["channel_text"].replace("{channel}", channel) + caption += f"\n\n{channel_text}" else: caption = title or "Готово!" @@ -108,9 +299,10 @@ class YouTube_DLDMod(loader.Module): message, video, caption=caption, - parse_mode="HTML" + parse_mode="HTML", + reply_to=reply or message, + silent=True ) - try: await message.delete() except: @@ -120,9 +312,14 @@ class YouTube_DLDMod(loader.Module): except: pass except Exception as e: - error_msg = self.config["error_text"].format(e) - await utils.answer(message, error_msg) + error_str = str(e) + if "Sign in to confirm you're not a bot" in error_str or "Use --cookies" in error_str: + await utils.answer(message, self.strings["cookies_error"]) + else: + error_msg = self.config["error_text"].format(e) + await utils.answer(message, error_msg) try: - os.remove(video) + if 'video' in locals(): + os.remove(video) except: pass diff --git a/SenkoGuardian/SenModules/Gemini.py b/SenkoGuardian/SenModules/Gemini.py index 6901a26..4fa283a 100644 --- a/SenkoGuardian/SenModules/Gemini.py +++ b/SenkoGuardian/SenModules/Gemini.py @@ -3,9 +3,9 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT -__version__ = (5, 6, 0) # Емае, ну это уже слишком +__version__ = (5, 7, 0) #перепешите на меня квартиру пж -#фырфырфырфырфырфыр +#ладно # meta developer: @SenkoGuardianModules @@ -23,23 +23,24 @@ import random import socket import asyncio import logging -import aiohttp import tempfile +import httpx +from datetime import datetime from markdown_it import MarkdownIt import pytz -# New SDK -from google import genai as google_genai_sdk + +# New SDK Check try: - from google.genai import types as new_types - NEW_SDK_AVAILABLE = True + from google import genai + from google.genai import types + import google.api_core.exceptions as google_exceptions + GOOGLE_AVAILABLE = True except ImportError: - NEW_SDK_AVAILABLE = False -# Old SDK -import google.ai.generativelanguage as glm -import google.generativeai as old_genai -from google.generativeai import types as old_types -from telethon import types -from telethon.tl.types import Message, DocumentAttributeFilename + GOOGLE_AVAILABLE = False + google_exceptions = None + +from telethon import types as tg_types +from telethon.tl.types import Message, DocumentAttributeFilename, DocumentAttributeSticker from telethon.utils import get_display_name, get_peer_id from telethon.errors.rpcerrorlist import ( MessageTooLongError, @@ -48,12 +49,6 @@ from telethon.errors.rpcerrorlist import ( ChannelPrivateError ) -try: - import google.api_core.exceptions as google_exceptions - GOOGLE_AVAILABLE = True -except ImportError: - GOOGLE_AVAILABLE = False - from .. import loader, utils from ..inline.types import InlineCall @@ -65,19 +60,10 @@ DB_IMPERSONATION_KEY = "gemini_impersonation_chats" GEMINI_TIMEOUT = 840 MAX_FFMPEG_SIZE = 90 * 1024 * 1024 -# requires: google-generativeai google-genai google-api-core pytz markdown_it_py -# рекомендованные версии: google-generativeai==0.8.5 google-genai==1.52.0 google-api-core==2.28.1 - -logger = logging.getLogger(__name__) - -DB_HISTORY_KEY = "gemini_conversations_v4" -DB_GAUTO_HISTORY_KEY = "gemini_gauto_conversations_v1" -DB_IMPERSONATION_KEY = "gemini_impersonation_chats" -GEMINI_TIMEOUT = 840 -MAX_FFMPEG_SIZE = 90 * 1024 * 1024 +# requires: google-genai google-api-core pytz markdown_it_py class Gemini(loader.Module): - """Модуль для работы с Google Gemini AI.(стабильная память и поддержка video/image/audio)""" + """Модуль для работы с Google Gemini AI (New SDK). Поддержка видео/фото/аудио и контекста пользователей.""" strings = { "name": "Gemini", "cfg_api_key_doc": "API ключи Google Gemini, разделенные запятой. Будут скрыты.", @@ -147,7 +133,7 @@ class Gemini(loader.Module): "gmodel_img_warn": "⚠️ Текущая модель ({}) не может генерировать изображения(или не доступна по API).\nРекомендуем: gemini-2.5-flash-image", "gme_chat_not_found": "🚫 Не удалось найти чат для экспорта: {}", "gme_sent_to_saved": "💾 История экспортирована в избранное.", - "new_sdk_missing": "⚠️ Для работы поиска (Grounding) нужна библиотека google-genai.\nВыполните: pip install google-genai", + "new_sdk_missing": "⚠️ Для работы модуля нужна библиотека google-genai.\nВыполните: pip install google-genai", "gprompt_usage": "ℹ️ Использование:\n.gprompt <текст> — установить промпт.\n.gprompt -c — очистить.\nИли ответьте на .txt файл.", "gprompt_updated": "✅ Системный промпт обновлен!\nДлина: {} симв.", "gprompt_cleared": "🗑 Системный промпт очищен.", @@ -155,6 +141,8 @@ class Gemini(loader.Module): "gprompt_file_error": "❗️ Ошибка чтения файла: {}", "gprompt_file_too_big": "❗️ Файл слишком большой (лимит 1 МБ).", "gprompt_not_text": "❗️ Это не похоже на текстовый файл.(txt)", + "gmodel_no_models": "⚠️ Не удалось получить список моделей.", + "gmodel_list_error": "❗️ Ошибка получения списка: {}", } TEXT_MIME_TYPES = { "text/plain", "text/markdown", "text/html", "text/css", "text/csv", @@ -163,10 +151,7 @@ class Gemini(loader.Module): } def __init__(self): self.config = loader.ModuleConfig( - loader.ConfigValue( - "api_key", "", self.strings["cfg_api_key_doc"], - validator=loader.validators.Hidden() - ), + loader.ConfigValue("api_key", "", self.strings["cfg_api_key_doc"], validator=loader.validators.Hidden()), loader.ConfigValue("model_name", "gemini-2.5-flash", self.strings["cfg_model_name_doc"]), loader.ConfigValue("interactive_buttons", True, self.strings["cfg_buttons_doc"], validator=loader.validators.Boolean()), loader.ConfigValue("system_instruction", "", self.strings["cfg_system_instruction_doc"], validator=loader.validators.String()), @@ -182,8 +167,7 @@ class Gemini(loader.Module): "Правила:\n- Отвечай кратко и по делу.\n- Используй неформальный язык, сленг.\n- Не отвечай на каждое сообщение.\n- На медиа (стикер, фото) реагируй как человек ('лол', 'ору', 'жиза').\n- Не используй префиксы и кавычки.\n\n" "ИСТОРИЯ ЧАТА:\n{chat_history}\n\n{my_name}:" ), - self.strings["cfg_impersonation_prompt_doc"], - validator=loader.validators.String(), + self.strings["cfg_impersonation_prompt_doc"], validator=loader.validators.String() ), loader.ConfigValue("impersonation_history_limit", 20, self.strings["cfg_impersonation_history_limit_doc"], validator=loader.validators.Integer(minimum=5, maximum=100)), loader.ConfigValue("impersonation_reply_chance", 0.25, self.strings["cfg_impersonation_reply_chance_doc"], validator=loader.validators.Float(minimum=0.0, maximum=1.0)), @@ -203,7 +187,7 @@ class Gemini(loader.Module): self.db = db self.me = await client.get_me() if not GOOGLE_AVAILABLE: - logger.error("Gemini: Google API libraries are not available. Please install required dependencies.") + logger.error("Gemini: 'google-genai' library missing! pip install google-genai") return api_key_str = self.config["api_key"] self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else [] @@ -211,52 +195,54 @@ class Gemini(loader.Module): self.conversations = self._load_history_from_db(DB_HISTORY_KEY) self.gauto_conversations = self._load_history_from_db(DB_GAUTO_HISTORY_KEY) self.impersonation_chats = set(self.db.get(self.strings["name"], DB_IMPERSONATION_KEY, [])) - self.safety_settings = [{"category": c, "threshold": "BLOCK_NONE"} for c in ["HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT"]] - self._configure_proxy() if not self.api_keys: - logger.warning("Gemini: API ключ(и) не настроен(ы)!") + logger.warning("Gemini: API ключи не настроены.") async def _prepare_parts(self, message: Message, custom_text: str=None): - final_parts, warnings=[], [] - prompt_text_chunks=[] - user_args=custom_text if custom_text is not None else utils.get_args_raw(message) - reply=await message.get_reply_message() + final_parts, warnings = [], [] + prompt_text_chunks = [] + user_args = custom_text if custom_text is not None else utils.get_args_raw(message) + reply = await message.get_reply_message() if reply and getattr(reply, "text", None): try: - reply_sender=await reply.get_sender() - reply_author_name=get_display_name(reply_sender) if reply_sender else "Unknown" + reply_sender = await reply.get_sender() + reply_author_name = get_display_name(reply_sender) if reply_sender else "Unknown" prompt_text_chunks.append(f"{reply_author_name}: {reply.text}") - except Exception: prompt_text_chunks.append(f"Ответ на: {reply.text}") + except Exception: + prompt_text_chunks.append(f"Ответ на: {reply.text}") try: - current_sender=await message.get_sender() - current_user_name=get_display_name(current_sender) if current_sender else "User" + current_sender = await message.get_sender() + current_user_name = get_display_name(current_sender) if current_sender else "User" prompt_text_chunks.append(f"{current_user_name}: {user_args or ''}") - except Exception: prompt_text_chunks.append(f"Запрос: {user_args or ''}") + except Exception: + prompt_text_chunks.append(f"Запрос: {user_args or ''}") media_source = message if message.media or message.sticker else reply has_media = bool(media_source and (media_source.media or media_source.sticker)) if has_media: if media_source.sticker and hasattr(media_source.sticker, 'mime_type') and media_source.sticker.mime_type=='application/x-tgsticker': - alt_text=next((attr.alt for attr in media_source.sticker.attributes if isinstance(attr, types.DocumentAttributeSticker)), "?") - prompt_text_chunks.append(f"[Отправлен анимированный стикер: {alt_text}]") + alt_text = next((attr.alt for attr in media_source.sticker.attributes if isinstance(attr, DocumentAttributeSticker)), "?") + prompt_text_chunks.append(f"[Анимированный стикер: {alt_text}]") else: media, mime_type, filename = media_source.media, "application/octet-stream", "file" - if media_source.photo: mime_type="image/jpeg" + if media_source.photo: + mime_type = "image/jpeg" elif hasattr(media_source, "document") and media_source.document: - mime_type=getattr(media_source.document, "mime_type", mime_type) - doc_attr=next((attr for attr in media_source.document.attributes if isinstance(attr, DocumentAttributeFilename)), None) - if doc_attr: filename=doc_attr.file_name + mime_type = getattr(media_source.document, "mime_type", mime_type) + doc_attr = next((attr for attr in media_source.document.attributes if isinstance(attr, DocumentAttributeFilename)), None) + if doc_attr: filename = doc_attr.file_name + async def get_bytes(m): + bio = io.BytesIO() + await self.client.download_media(m, bio) + return bio.getvalue() if mime_type.startswith("image/"): try: - byte_io=io.BytesIO() - await self.client.download_media(media, byte_io) - final_parts.append(glm.Part(inline_data=glm.Blob(mime_type=mime_type, data=byte_io.getvalue()))) + data = await get_bytes(media) + final_parts.append(types.Part(inline_data=types.Blob(mime_type=mime_type, data=data))) except Exception as e: warnings.append(f"⚠️ Ошибка обработки изображения '{filename}': {e}") elif mime_type in self.TEXT_MIME_TYPES or filename.split('.')[-1] in ('txt', 'py', 'js', 'json', 'md', 'html', 'css', 'sh'): try: - byte_io=io.BytesIO() - await self.client.download_media(media, byte_io) - byte_io.seek(0) - file_content=byte_io.read().decode('utf-8') + data = await get_bytes(media) + file_content = data.decode('utf-8') prompt_text_chunks.insert(0, f"[Содержимое файла '{filename}']: \n```\n{file_content}\n```") except Exception as e: warnings.append(f"⚠️ Ошибка чтения файла '{filename}': {e}") elif mime_type.startswith("audio/"): @@ -265,207 +251,178 @@ class Gemini(loader.Module): with tempfile.NamedTemporaryFile(suffix=f".{filename.split('.')[-1]}", delete=False) as temp_in: input_path = temp_in.name await self.client.download_media(media, input_path) if os.path.getsize(input_path) > MAX_FFMPEG_SIZE: - warnings.append(f"⚠️ Аудиофайл '{filename}' слишком большой для конвертации (> {MAX_FFMPEG_SIZE // 1024 // 1024} МБ)."); raise StopIteration + warnings.append(f"⚠️ Аудиофайл '{filename}' слишком большой."); raise StopIteration with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_out: output_path = temp_out.name - ffmpeg_cmd = ["ffmpeg", "-y", "-i", input_path, "-c:a", "libmp3lame", "-q:a", "2", output_path] - process_ffmpeg = await asyncio.create_subprocess_exec(*ffmpeg_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - _, stderr = await process_ffmpeg.communicate() - if process_ffmpeg.returncode != 0: - stderr_str = stderr.decode() - warnings.append(f"⚠️ Ошибка FFmpeg (аудио):\nНе удалось конвертировать '{filename}'. Детали:\n{utils.escape_html(stderr_str)}") - raise StopIteration + proc = await asyncio.create_subprocess_exec("ffmpeg", "-y", "-i", input_path, "-c:a", "libmp3lame", "-q:a", "2", output_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + await proc.communicate() with open(output_path, "rb") as f: - final_parts.append(glm.Part(inline_data=glm.Blob(mime_type="audio/mpeg", data=f.read()))) + final_parts.append(types.Part(inline_data=types.Blob(mime_type="audio/mpeg", data=f.read()))) except StopIteration: pass - except Exception as e: warnings.append(f"⚠️ Критическая ошибка при обработке аудио '{filename}': {e}") + except Exception as e: warnings.append(f"⚠️ Ошибка обработки аудио: {e}") finally: if input_path and os.path.exists(input_path): os.remove(input_path) if output_path and os.path.exists(output_path): os.remove(output_path) elif mime_type.startswith("video/"): input_path, output_path = None, None try: - with tempfile.NamedTemporaryFile(suffix=f".{filename.split('.')[-1]}", delete=False) as temp_in: input_path=temp_in.name + with tempfile.NamedTemporaryFile(suffix=f".{filename.split('.')[-1]}", delete=False) as temp_in: input_path = temp_in.name await self.client.download_media(media, input_path) if os.path.getsize(input_path) > MAX_FFMPEG_SIZE: - warnings.append(f"⚠️ Медиафайл '{filename}' слишком большой для конвертации (> {MAX_FFMPEG_SIZE // 1024 // 1024} МБ)."); raise StopIteration - ffprobe_cmd = ["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=codec_type", "-of", "default=noprint_wrappers=1:nokey=1", input_path] - process_probe = await asyncio.create_subprocess_exec(*ffprobe_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - stdout, _ = await process_probe.communicate() + warnings.append(f"⚠️ Медиафайл '{filename}' слишком большой."); raise StopIteration + proc_probe = await asyncio.create_subprocess_exec("ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=codec_type", "-of", "default=noprint_wrappers=1:nokey=1", input_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + stdout, _ = await proc_probe.communicate() has_audio = bool(stdout.strip()) with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_out: output_path = temp_out.name - ffmpeg_cmd = ["ffmpeg", "-y", "-i", input_path] + cmd = ["ffmpeg", "-y", "-i", input_path] maps = ["-map", "0:v:0"] if not has_audio: - ffmpeg_cmd.extend(["-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100"]) + cmd.extend(["-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100"]) maps.extend(["-map", "1:a:0"]) else: maps.extend(["-map", "0:a:0?"]) - ffmpeg_cmd.extend([*maps, "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-c:v", "libx264", "-c:a", "aac", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-shortest", output_path]) - process_ffmpeg = await asyncio.create_subprocess_exec(*ffmpeg_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - _, stderr = await process_ffmpeg.communicate() - if process_ffmpeg.returncode != 0: - stderr_str = stderr.decode() - warnings.append(f"⚠️ Ошибка FFmpeg:\nНе удалось конвертировать '{filename}'. Детали:\n{utils.escape_html(stderr_str)}") - raise StopIteration + cmd.extend([*maps, "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-c:v", "libx264", "-c:a", "aac", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-shortest", output_path]) + proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + await proc.communicate() with open(output_path, "rb") as f: - final_parts.append(glm.Part(inline_data=glm.Blob(mime_type="video/mp4", data=f.read()))) + final_parts.append(types.Part(inline_data=types.Blob(mime_type="video/mp4", data=f.read()))) except StopIteration: pass - except Exception as e: warnings.append(f"⚠️ Критическая ошибка при обработке медиа '{filename}': {e}") + except Exception as e: warnings.append(f"⚠️ Ошибка обработки видео: {e}") finally: if input_path and os.path.exists(input_path): os.remove(input_path) if output_path and os.path.exists(output_path): os.remove(output_path) if not user_args and has_media and not final_parts and not any("[Содержимое файла" in chunk for chunk in prompt_text_chunks): prompt_text_chunks.append(self.strings["media_reply_placeholder"]) - full_prompt_text="\n".join(chunk for chunk in prompt_text_chunks if chunk and chunk.strip()).strip() + full_prompt_text = "\n".join(chunk for chunk in prompt_text_chunks if chunk and chunk.strip()).strip() if full_prompt_text: - final_parts.insert(0, glm.Part(text=full_prompt_text)) + final_parts.insert(0, types.Part(text=full_prompt_text)) return final_parts, warnings async def _send_to_gemini(self, message, parts: list, regeneration: bool=False, call: InlineCall=None, status_msg=None, chat_id_override: int=None, impersonation_mode: bool=False, use_url_context: bool=False, display_prompt: str=None): - msg_obj=None + msg_obj = None if regeneration: - chat_id=chat_id_override; base_message_id=message - try: msg_obj=await self.client.get_messages(chat_id, ids=base_message_id) - except Exception: msg_obj=None + chat_id = chat_id_override; base_message_id = message + try: msg_obj = await self.client.get_messages(chat_id, ids=base_message_id) + except Exception: msg_obj = None else: - chat_id=utils.get_chat_id(message); base_message_id=message.id; msg_obj=message + chat_id = utils.get_chat_id(message); base_message_id = message.id; msg_obj = message api_key_str = self.config["api_key"] self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else [] - try: - if not self.api_keys: - if not impersonation_mode and status_msg: await utils.answer(status_msg, self.strings['no_api_key']) - return None if impersonation_mode else "" - api_key = self.api_keys[self.current_api_key_index] - if regeneration: - current_turn_parts, request_text_for_display = self.last_requests.get(f"{chat_id}:{base_message_id}", (parts, "[регенерация]")) + if not self.api_keys: + if not impersonation_mode and status_msg: await utils.answer(status_msg, self.strings['no_api_key']) + return None if impersonation_mode else "" + if regeneration: + current_turn_parts, request_text_for_display = self.last_requests.get(f"{chat_id}:{base_message_id}", (parts, "[регенерация]")) + else: + current_turn_parts = parts + request_text_for_display = display_prompt or (self.strings["media_reply_placeholder"] if any(getattr(p, 'inline_data', None) for p in parts) else "") + self.last_requests[f"{chat_id}:{base_message_id}"] = (current_turn_parts, request_text_for_display) + result_text = "" + last_error = None + was_successful = False + search_icon = "" + max_retries = len(self.api_keys) + if impersonation_mode: + my_name = get_display_name(self.me) + chat_history_text = await self._get_recent_chat_text(chat_id) + sys_instruct = self.config["impersonation_prompt"].format(my_name=my_name, chat_history=chat_history_text) + else: + sys_val = self.config["system_instruction"] + sys_instruct = (sys_val.strip() if isinstance(sys_val, str) else "") or None + contents = [] + raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode) + if regeneration and raw_hist: raw_hist = raw_hist[:-2] + for item in raw_hist: + contents.append(types.Content( + role=item['role'], + parts=[types.Part(text=item['content'])] + )) + request_parts = list(current_turn_parts) + if not impersonation_mode: + try: user_timezone = pytz.timezone(self.config["timezone"]) + except pytz.UnknownTimeZoneError: user_timezone = pytz.utc + now = datetime.now(user_timezone) + time_note = f"[System note: Current time is {now.strftime('%Y-%m-%d %H:%M:%S %Z')}]" + if request_parts and getattr(request_parts[0], 'text', None): + request_parts[0] = types.Part(text=f"{time_note}\n\n{request_parts[0].text}") else: - current_turn_parts = parts - request_text_for_display = display_prompt or (self.strings["media_reply_placeholder"] if any("inline_data" in str(p) for p in parts) else "") - self.last_requests[f"{chat_id}:{base_message_id}"] = (current_turn_parts, request_text_for_display) - has_media = False - for p in current_turn_parts: - if hasattr(p, 'inline_data') and p.inline_data: - has_media = True; break - should_use_new_sdk = (self.config["google_search"] or use_url_context) and NEW_SDK_AVAILABLE and not has_media - result_text = ""; was_successful = False; search_icon = "" - if should_use_new_sdk: - try: - client = google_genai_sdk.Client(api_key=api_key); sys_instruct = None - if impersonation_mode: - my_name = get_display_name(self.me); chat_history_text = await self._get_recent_chat_text(chat_id) - sys_instruct = self.config["impersonation_prompt"].format(my_name=my_name, chat_history=chat_history_text) - else: - sys_val = self.config["system_instruction"]; sys_instruct = (sys_val.strip() if isinstance(sys_val, str) else "") or None - new_contents = []; raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode) - if regeneration and raw_hist: raw_hist = raw_hist[:-2] - for item in raw_hist: - new_contents.append(new_types.Content(role=item['role'], parts=[new_types.Part(text=item['content'])])) - new_parts = [] - for p in current_turn_parts: - if hasattr(p, 'text'): new_parts.append(new_types.Part(text=p.text)) - new_contents.append(new_types.Content(role="user", parts=new_parts)) - tools = [] - if self.config["google_search"] or use_url_context: - tools.append(new_types.Tool(google_search=new_types.GoogleSearch())) - config_new = new_types.GenerateContentConfig(tools=tools if tools else None, temperature=self.config["temperature"], system_instruction=sys_instruct) - response = await asyncio.to_thread(client.models.generate_content, model=self.config["model_name"], contents=new_contents, config=config_new) - if response.text: - result_text = response.text; was_successful = True - if self.config["google_search"]: search_icon = " 🌐" - else: result_text = "⚠️ Пустой ответ от модели (возможно, блокировка безопасности)." - except Exception as e: - logger.error(f"New SDK Error: {e}") - if "quota" in str(e).lower(): raise e - was_successful = False - if not was_successful: - if (self.config["google_search"] or use_url_context) and not NEW_SDK_AVAILABLE and not has_media: - if status_msg: await utils.answer(status_msg, self.strings["new_sdk_missing"]) - return None - tools_list = [] - if use_url_context and not has_media: - try: tools_list.append(old_types.Tool(url_context=old_types.UrlContext())) - except AttributeError: pass - system_instruction_to_use = None; api_history_content = [] - if impersonation_mode: - my_name = get_display_name(self.me); chat_history_text = await self._get_recent_chat_text(chat_id) - system_instruction_to_use = self.config["impersonation_prompt"].format(my_name=my_name, chat_history=chat_history_text) - raw_history = self._get_structured_history(chat_id, gauto=True) - api_history_content = [glm.Content(role=e["role"], parts=[glm.Part(text=e['content'])]) for e in raw_history] + request_parts.insert(0, types.Part(text=time_note)) + contents.append(types.Content(role="user", parts=request_parts)) + tools = [] + if self.config["google_search"] or use_url_context: + tools.append(types.Tool(google_search=types.GoogleSearch())) + gen_config = types.GenerateContentConfig( + temperature=self.config["temperature"], + system_instruction=sys_instruct, + tools=tools if tools else None, + safety_settings=[ + types.SafetySetting(category=cat, threshold="BLOCK_NONE") + for cat in ["HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT"] + ] + ) + proxy_config = self._get_proxy_config() + for i in range(max_retries): + current_idx = (self.current_api_key_index + i) % max_retries + api_key = self.api_keys[current_idx] + try: + http_opts = None + if proxy_config: + http_opts = types.HttpOptions(async_client_args={"proxies": proxy_config}) + + client = genai.Client(api_key=api_key, http_options=http_opts) + response = await client.aio.models.generate_content( + model=self.config["model_name"], + contents=contents, + config=gen_config + ) + + if response.text: + result_text = response.text + was_successful = True + if self.config["google_search"]: search_icon = " 🌐" + self.current_api_key_index = current_idx + break else: - system_instruction_val = self.config["system_instruction"] - system_instruction_to_use = (system_instruction_val.strip() if isinstance(system_instruction_val, str) else "") or None - raw_history = self._get_structured_history(chat_id, gauto=False) - if regeneration: raw_history = raw_history[:-2] - api_history_content = [glm.Content(role=e["role"], parts=[glm.Part(text=e['content'])]) for e in raw_history] - full_request_content = list(api_history_content) - if not impersonation_mode: - from datetime import datetime - try: user_timezone = pytz.timezone(self.config["timezone"]) - except pytz.UnknownTimeZoneError: user_timezone = pytz.utc - now = datetime.now(user_timezone); time_str = now.strftime("%Y-%m-%d %H:%M:%S %Z"); time_note = f"[System note: Current time is {time_str}]" - request_content_parts = list(current_turn_parts) - if request_content_parts and hasattr(request_content_parts[0], 'text'): - original_text = request_content_parts[0].text - request_content_parts[0] = glm.Part(text=f"{time_note}\n\n{original_text}") - else: request_content_parts.insert(0, glm.Part(text=time_note)) - else: request_content_parts = current_turn_parts - if request_content_parts: full_request_content.append(glm.Content(role="user", parts=request_content_parts)) - if not full_request_content and not system_instruction_to_use: - if not impersonation_mode and status_msg: await utils.answer(status_msg, self.strings["no_prompt_or_media"]) - return None if impersonation_mode else "" - error_to_report = None; max_retries = len(self.api_keys) - generation_cfg = old_types.GenerationConfig(temperature=self.config["temperature"]) - for i in range(max_retries): - current_key_index = (self.current_api_key_index + i) % max_retries - api_key = self.api_keys[current_key_index] - try: - old_genai.configure(api_key=api_key) - sanitized_model_name = self.config["model_name"].lower().replace(" ", "-") - model = old_genai.GenerativeModel(sanitized_model_name, safety_settings=self.safety_settings, system_instruction=system_instruction_to_use) - api_response = await asyncio.wait_for(model.generate_content_async(full_request_content, tools=tools_list or None, generation_config=generation_cfg), timeout=GEMINI_TIMEOUT) - response = api_response; self.current_api_key_index = current_key_index; break - except google_exceptions.GoogleAPIError as e: - msg = str(e) - if "quota" in msg.lower() or "exceeded" in msg.lower(): - if max_retries == 1: error_to_report = e; break - logger.warning(f"Ключ №{current_key_index + 1} исчерпал квоту.") - if i == max_retries - 1: error_to_report = RuntimeError("Все ключи исчерпали квоту."); continue - else: error_to_report = e; break - except Exception as e: error_to_report = e; break - if error_to_report: raise error_to_report - if response is None: raise RuntimeError("Не удалось получить ответ.") - try: - if response.prompt_feedback.block_reason: result_text = f"🚫 Запрос заблокирован.\nПричина: {response.prompt_feedback.block_reason.name}." - except AttributeError: pass - if not result_text: - try: - result_text = re.sub(r"]*>", "", response.text); was_successful = True - except ValueError: result_text = "❗️ Gemini не смог сгенерировать ответ (возможно, только safety ratings)." - if was_successful and self._is_memory_enabled(str(chat_id)): + raise ValueError("Empty response (Safety?)") + except Exception as e: + err_str = str(e).lower() + if "quota" in err_str or "exhausted" in err_str or "429" in err_str: + if i == max_retries - 1: last_error = RuntimeError(f"Keys exhausted. Last: {e}") + continue + else: + last_error = e + break + try: + if not was_successful: + raise last_error or RuntimeError("Unknown generation error") + if self._is_memory_enabled(str(chat_id)): self._update_history(chat_id, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode) - if impersonation_mode: return result_text if was_successful else None - hist_len_pairs = len(self._get_structured_history(chat_id, gauto=False)) // 2 - limit = self.config["max_history_length"] - mem_indicator = self.strings["memory_status_unlimited"].format(hist_len_pairs) if limit <= 0 else self.strings["memory_status"].format(hist_len_pairs, limit) - question_html = f"
{utils.escape_html(request_text_for_display[:200])}
" + if impersonation_mode: return result_text + hist_len = len(self._get_structured_history(chat_id)) // 2 + mem_ind = self.strings["memory_status"].format(hist_len, self.config["max_history_length"]) + if self.config["max_history_length"] <= 0: + mem_ind = self.strings["memory_status_unlimited"].format(hist_len) response_html = self._markdown_to_html(result_text) formatted_body = self._format_response_with_smart_separation(response_html) - header = f"{mem_indicator}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}{search_icon}\n" - text_to_send = f"{header}{formatted_body}" + question_html = f"
{utils.escape_html(request_text_for_display[:200])}
" + text_to_send = f"{mem_ind}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}{search_icon}\n{formatted_body}" buttons = self._get_inline_buttons(chat_id, base_message_id) if self.config["interactive_buttons"] else None if len(text_to_send) > 4096: file_content = (f"Вопрос: {display_prompt}\n\n════════════════════\n\nОтвет Gemini:\n{result_text}") - file = io.BytesIO(file_content.encode("utf-8")); file.name = "Gemini_response.txt" + file = io.BytesIO(file_content.encode("utf-8")) + file.name = "Gemini_response.txt" if call: - await call.answer("Ответ длинный, отправляю файлом...", show_alert=False); await self.client.send_file(call.chat_id, file, caption=self.strings["response_too_long"], reply_to=call.message_id); await call.edit(f"✅ {self.strings['response_too_long']}", reply_markup=None) + await call.answer("Ответ длинный, отправляю файлом...", show_alert=False) + await self.client.send_file(call.chat_id, file, caption=self.strings["response_too_long"], reply_to=call.message_id) + await call.edit(f"✅ {self.strings['response_too_long']}", reply_markup=None) elif status_msg: - await status_msg.delete(); await self.client.send_file(chat_id, file, caption=self.strings["response_too_long"], reply_to=base_message_id) + await status_msg.delete() + await self.client.send_file(chat_id, file, caption=self.strings["response_too_long"], reply_to=base_message_id) else: if call: await call.edit(text_to_send, reply_markup=buttons) elif status_msg: await utils.answer(status_msg, text_to_send, reply_markup=buttons) except Exception as e: error_text = self._handle_error(e) - if impersonation_mode: logger.error(f"Gauto | Ошибка: {error_text}") + if impersonation_mode: logger.error(f"Gauto error: {error_text}") elif call: await call.edit(error_text, reply_markup=None) elif status_msg: await utils.answer(status_msg, error_text) return None if impersonation_mode else "" @@ -473,66 +430,55 @@ class Gemini(loader.Module): @loader.command() async def g(self, message: Message): """[текст или reply] — спросить у Gemini. Может анализировать ссылки.""" - clean_args=utils.get_args_raw(message) - reply=await message.get_reply_message() - use_url_context=False - text_to_check=clean_args + clean_args = utils.get_args_raw(message) + reply = await message.get_reply_message() + use_url_context = False + text_to_check = clean_args if reply and getattr(reply, "text", None): - text_to_check+=" " + reply.text - if re.search(r'https?://\S+', text_to_check): use_url_context=True - status_msg=await utils.answer(message, self.strings["processing"]) + text_to_check += " " + reply.text + if re.search(r'https?://\S+', text_to_check): use_url_context = True + status_msg = await utils.answer(message, self.strings["processing"]) status_msg = await self.client.get_messages(status_msg.chat_id, ids=status_msg.id) - parts, warnings=await self._prepare_parts(message, custom_text=clean_args) + parts, warnings = await self._prepare_parts(message, custom_text=clean_args) if warnings and status_msg: - warning_text="\n".join(warnings) - try: await status_msg.edit(f"{status_msg.text}\n\n{warning_text}") - except MessageTooLongError: await message.reply(warning_text) + try: await status_msg.edit(f"{status_msg.text}\n\n" + "\n".join(warnings)) + except: pass if not parts: - err_msg=self.strings["no_prompt_or_media"] - if status_msg: await utils.answer(status_msg, err_msg) + if status_msg: await utils.answer(status_msg, self.strings["no_prompt_or_media"]) return - await self._send_to_gemini(message=message, parts=parts, status_msg=status_msg, use_url_context=use_url_context, display_prompt=clean_args or None) + await self._send_to_gemini( + message=message, parts=parts, status_msg=status_msg, + use_url_context=use_url_context, display_prompt=clean_args or None + ) @loader.command() async def gch(self, message: Message): """<[id чата]> <кол-во> <вопрос> - Проанализировать историю чата.""" args_str = utils.get_args_raw(message) - if not args_str: - return await utils.answer(message, self.strings["gch_usage"]) + if not args_str: return await utils.answer(message, self.strings["gch_usage"]) parts = args_str.split() target_chat_id = utils.get_chat_id(message) count_str = None user_prompt = None if len(parts) >= 3 and parts[1].isdigit(): try: - entity_str = parts[0] - entity = await self.client.get_entity(int(entity_str) if entity_str.lstrip('-').isdigit() else entity_str) + entity = await self.client.get_entity(int(parts[0]) if parts[0].lstrip('-').isdigit() else parts[0]) target_chat_id = entity.id count_str = parts[1] user_prompt = " ".join(parts[2:]) - except Exception: - pass + except: pass if user_prompt is None: if len(parts) >= 2 and parts[0].isdigit(): count_str = parts[0] user_prompt = " ".join(parts[1:]) - else: - return await utils.answer(message, self.strings["gch_usage"]) - if not user_prompt or not count_str: - return await utils.answer(message, self.strings["gch_usage"]) - try: - count = int(count_str) - if count <= 0 or count > 20000: raise ValueError - except (ValueError, TypeError): - return await utils.answer(message, self.strings["gch_invalid_args"].format(f"Количество сообщений должно быть числом от 1 до 20000. Вы ввели: {utils.escape_html(count_str)}")) + else: return await utils.answer(message, self.strings["gch_usage"]) + try: count = int(count_str) + except: return await utils.answer(message, "❗️ Кол-во должно быть числом.") status_msg = await utils.answer(message, self.strings["gch_processing"].format(count)) - status_msg = await self.client.get_messages(status_msg.chat_id, ids=status_msg.id) try: entity = await self.client.get_entity(target_chat_id) chat_name = utils.escape_html(get_display_name(entity)) chat_log = await self._get_recent_chat_text(target_chat_id, count=count, skip_last=False) - except (ValueError, TypeError, ChatAdminRequiredError, UserNotParticipantError, ChannelPrivateError) as e: - return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e.__class__.__name__)) except Exception as e: return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e)) full_prompt = ( @@ -542,50 +488,33 @@ class Gemini(loader.Module): f"ИСТОРИЯ ЧАТА:\n---\n{chat_log}\n---" ) try: - response = None - error_to_report = None - max_retries = len(self.api_keys) - if not max_retries: - await utils.answer(status_msg, self.strings['no_api_key']); return - for i in range(max_retries): - current_key_index = (self.current_api_key_index + i) % max_retries - api_key = self.api_keys[current_key_index] + response_text = None + proxy_config = self._get_proxy_config() + http_opts = types.HttpOptions(async_client_args={"proxies": proxy_config}) if proxy_config else None + for i in range(len(self.api_keys)): + key = self.api_keys[(self.current_api_key_index + i) % len(self.api_keys)] try: - old_genai.configure(api_key=api_key) - sanitized_model_name = self.config["model_name"].lower().replace(" ", "-") - generation_cfg = old_types.GenerationConfig( - temperature=self.config["temperature"] + client = genai.Client(api_key=key, http_options=http_opts) + resp = await client.aio.models.generate_content( + model=self.config["model_name"], + contents=full_prompt, + config=types.GenerateContentConfig(safety_settings=[types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE")]) ) - model = old_genai.GenerativeModel(sanitized_model_name, safety_settings=self.safety_settings) - api_response = await asyncio.wait_for(model.generate_content_async(full_prompt, generation_config=generation_cfg), timeout=GEMINI_TIMEOUT) - response = api_response - self.current_api_key_index = current_key_index - break - except google_exceptions.GoogleAPIError as e: - msg = str(e) - if "quota" in msg.lower() or "exceeded" in msg.lower(): - if max_retries == 1: error_to_report = e; break - logger.warning(f"Ключ Gemini API №{current_key_index + 1} исчерпал квоту. Пробую следующий.") - if i == max_retries - 1: error_to_report = RuntimeError("Все ключи исчерпали квоту.") - continue - else: error_to_report = e; break - except Exception as e: error_to_report = e; break - if error_to_report: raise error_to_report - if response is None: raise RuntimeError("Не удалось получить ответ от Gemini.") - result_text = re.sub(r"]*>", "", response.text) - header = self.strings["gch_result_caption_from_chat"].format(count, chat_name) if target_chat_id != utils.get_chat_id(message) else self.strings["gch_result_caption"].format(count) - question_html = f"
{utils.escape_html(user_prompt)}
" - response_html = self._markdown_to_html(result_text) - formatted_body = self._format_response_with_smart_separation(response_html) - text_to_send = (f"{header}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}\n{formatted_body}") - if len(text_to_send) > 4096: - file_content = (f"Вопрос: {user_prompt}\n\n════════════════════\n\nОтвет Gemini на анализ чата '{chat_name}':\n{result_text}") - file = io.BytesIO(file_content.encode("utf-8")) - file.name = f"analysis_{target_chat_id}.txt" + if resp.text: + response_text = resp.text + self.current_api_key_index = (self.current_api_key_index + i) % len(self.api_keys) + break + except: continue + if not response_text: raise RuntimeError("Failed to generate (all keys dead).") + header = self.strings["gch_result_caption_from_chat"].format(count, chat_name) + resp_html = self._markdown_to_html(response_text) + text = f"{header}\n\n{self.strings['question_prefix']}\n
{utils.escape_html(user_prompt)}
\n\n{self.strings['response_prefix']}\n{self._format_response_with_smart_separation(resp_html)}" + if len(text) > 4096: + f = io.BytesIO(response_text.encode('utf-8')); f.name = "analysis.txt" await status_msg.delete() - await message.reply(file=file, caption=f"📝 {header}") + await message.reply(file=f, caption=f"📝 {header}") else: - await utils.answer(status_msg, text_to_send) + await utils.answer(status_msg, text) except Exception as e: await utils.answer(status_msg, self._handle_error(e)) @@ -597,148 +526,115 @@ class Gemini(loader.Module): if args == "-c": self.config["system_instruction"] = "" return await utils.answer(message, self.strings["gprompt_cleared"]) - new_prompt = None + new_p = None if reply and reply.file: if reply.file.size > 1024 * 1024: return await utils.answer(message, self.strings["gprompt_file_too_big"]) try: - file_data = await self.client.download_file(reply.media, bytes) - try: - new_prompt = file_data.decode("utf-8") - except UnicodeDecodeError: - return await utils.answer(message, self.strings["gprompt_not_text"]) - except Exception as e: - return await utils.answer(message, self.strings["gprompt_file_error"].format(e)) - elif args: - new_prompt = args - if new_prompt is not None: - self.config["system_instruction"] = new_prompt - return await utils.answer(message, self.strings["gprompt_updated"].format(len(new_prompt))) - current_prompt = self.config["system_instruction"] - if not current_prompt: - return await utils.answer(message, self.strings["gprompt_usage"]) - if len(current_prompt) > 4000: - file = io.BytesIO(current_prompt.encode("utf-8")) - file.name = "system_instruction.txt" + data = await self.client.download_file(reply.media, bytes) + try: new_p = data.decode("utf-8") + except UnicodeDecodeError: return await utils.answer(message, self.strings["gprompt_not_text"]) + except Exception as e: return await utils.answer(message, self.strings["gprompt_file_error"].format(e)) + elif args: new_p = args + if new_p: + self.config["system_instruction"] = new_p + return await utils.answer(message, self.strings["gprompt_updated"].format(len(new_p))) + cur = self.config["system_instruction"] + if not cur: return await utils.answer(message, self.strings["gprompt_usage"]) + if len(cur) > 4000: + file = io.BytesIO(cur.encode("utf-8")); file.name = "system_instruction.txt" await utils.answer(message, self.strings["gprompt_current"], file=file) else: - await utils.answer(message, f"{self.strings['gprompt_current']}\n{utils.escape_html(current_prompt)}") + await utils.answer(message, f"{self.strings['gprompt_current']}\n{utils.escape_html(cur)}") @loader.command() async def gauto(self, message: Message): """ — Вкл/выкл авто-ответ в чате.""" args = utils.get_args_raw(message).split() - if not args: - await utils.answer(message, self.strings["auto_mode_usage"]) - return + if not args: return await utils.answer(message, self.strings["auto_mode_usage"]) chat_id = utils.get_chat_id(message) - state_arg = args[0].lower() - target_chat_id = None - action = None - if len(args) == 1: - if state_arg in ("on", "off"): - target_chat_id = chat_id - action = state_arg - elif len(args) == 2: + state = args[0].lower() + target = chat_id + if len(args) == 2: try: - entity = await self.client.get_entity(args[0]) - target_chat_id = entity.id - action = args[1].lower() - except Exception: - await utils.answer(message, self.strings["gauto_chat_not_found"].format(utils.escape_html(args[0]))) - return - if action == "on": - self.impersonation_chats.add(target_chat_id) + e = await self.client.get_entity(args[0]) + target = e.id + state = args[1].lower() + except: return await utils.answer(message, self.strings["gauto_chat_not_found"].format(args[0])) + if state == "on": + self.impersonation_chats.add(target) self.db.set(self.strings["name"], DB_IMPERSONATION_KEY, list(self.impersonation_chats)) - if target_chat_id == chat_id: - await utils.answer(message, self.strings["auto_mode_on"].format(int(self.config["impersonation_reply_chance"] * 100))) - else: - await utils.answer(message, self.strings["gauto_state_updated"].format(f"{target_chat_id}", self.strings["gauto_enabled"])) - elif action == "off": - self.impersonation_chats.discard(target_chat_id) + txt = self.strings["auto_mode_on"].format(int(self.config["impersonation_reply_chance"]*100)) if target==chat_id else self.strings["gauto_state_updated"].format(f"{target}", self.strings["gauto_enabled"]) + await utils.answer(message, txt) + elif state == "off": + self.impersonation_chats.discard(target) self.db.set(self.strings["name"], DB_IMPERSONATION_KEY, list(self.impersonation_chats)) - if target_chat_id == chat_id: - await utils.answer(message, self.strings["auto_mode_off"]) - else: - await utils.answer(message, self.strings["gauto_state_updated"].format(f"{target_chat_id}", self.strings["gauto_disabled"])) - else: - await utils.answer(message, self.strings["auto_mode_usage"]) + txt = self.strings["auto_mode_off"] if target==chat_id else self.strings["gauto_state_updated"].format(f"{target}", self.strings["gauto_disabled"]) + await utils.answer(message, txt) + else: await utils.answer(message, self.strings["auto_mode_usage"]) @loader.command() async def gautochats(self, message: Message): """— Показать чаты с активным режимом авто-ответа.""" - if not self.impersonation_chats: - await utils.answer(message, self.strings["no_auto_mode_chats"]) - return - out=[self.strings["auto_mode_chats_title"].format(len(self.impersonation_chats))] - for chat_id in self.impersonation_chats: + if not self.impersonation_chats: return await utils.answer(message, self.strings["no_auto_mode_chats"]) + out = [self.strings["auto_mode_chats_title"].format(len(self.impersonation_chats))] + for cid in self.impersonation_chats: try: - entity=await self.client.get_entity(chat_id) - name=utils.escape_html(get_display_name(entity)) - out.append(self.strings["memory_chat_line"].format(name, chat_id)) - except Exception: - out.append(self.strings["memory_chat_line"].format("Неизвестный чат", chat_id)) + e = await self.client.get_entity(cid) + name = utils.escape_html(get_display_name(e)) + out.append(self.strings["memory_chat_line"].format(name, cid)) + except: out.append(self.strings["memory_chat_line"].format("Неизвестный чат", cid)) await utils.answer(message, "\n".join(out)) @loader.command() async def gclear(self, message: Message): """[auto] — очистить память в чате. auto для памяти gauto.""" - args=utils.get_args_raw(message) - chat_id=utils.get_chat_id(message) - if args=="auto": + args = utils.get_args_raw(message) + chat_id = utils.get_chat_id(message) + if args == "auto": if str(chat_id) in self.gauto_conversations: self._clear_history(chat_id, gauto=True) await utils.answer(message, self.strings["memory_cleared_gauto"]) - else: - await utils.answer(message, self.strings["no_gauto_memory_to_clear"]) + else: await utils.answer(message, self.strings["no_gauto_memory_to_clear"]) elif not args: if str(chat_id) in self.conversations: - self._clear_history(chat_id, gauto=False) + self._clear_history(chat_id) await utils.answer(message, self.strings["memory_cleared"]) - else: - await utils.answer(message, self.strings["no_memory_to_clear"]) + else: await utils.answer(message, self.strings["no_memory_to_clear"]) else: await utils.answer(message, self.strings["gclear_usage"]) @loader.command() async def gmemdel(self, message: Message): """[N] — удалить последние N пар сообщений из памяти.""" - args=utils.get_args_raw(message) - try: n=int(args) if args else 1 - except Exception: n=1 - chat_id=utils.get_chat_id(message) - hist=self._get_structured_history(chat_id) - elements_to_remove=n*2 - if n > 0 and len(hist) >= elements_to_remove: - hist=hist[:-elements_to_remove] - self.conversations[str(chat_id)]=hist + try: n = int(utils.get_args_raw(message) or 1) + except: n = 1 + cid = utils.get_chat_id(message) + hist = self._get_structured_history(cid) + if n > 0 and len(hist) >= n*2: + self.conversations[str(cid)] = hist[:-n*2] self._save_history_sync() await utils.answer(message, f"🧹 Удалено последних {n} пар сообщений из памяти.") - else: - await utils.answer(message, "Недостаточно истории для удаления.") + else: await utils.answer(message, "Недостаточно истории для удаления.") @loader.command() async def gmemchats(self, message: Message): """— Показать список чатов с активной памятью (имя и ID).""" - if not self.conversations: - await utils.answer(message, self.strings["no_memory_found"]); return - out=[self.strings["memory_chats_title"].format(len(self.conversations))] - shown=set() - for chat_id_str in list(self.conversations.keys()): - if not chat_id_str or not str(chat_id_str).lstrip('-').isdigit(): - del self.conversations[chat_id_str] - continue - chat_id=int(chat_id_str) + if not self.conversations: return await utils.answer(message, self.strings["no_memory_found"]) + out = [self.strings["memory_chats_title"].format(len(self.conversations))] + shown = set() + for cid in list(self.conversations.keys()): + if not str(cid).lstrip('-').isdigit(): continue + chat_id = int(cid) if chat_id in shown: continue shown.add(chat_id) try: - entity=await self.client.get_entity(chat_id) - name=get_display_name(entity) - except Exception: name=f"Unknown ({chat_id})" + e = await self.client.get_entity(chat_id) + name = get_display_name(e) + except: name = f"Unknown ({chat_id})" out.append(self.strings["memory_chat_line"].format(name, chat_id)) self._save_history_sync() - if len(out)==1: - await utils.answer(message, self.strings["no_memory_found"]); return + if len(out) == 1: return await utils.answer(message, self.strings["no_memory_found"]) await utils.answer(message, "\n".join(out)) @loader.command() @@ -746,415 +642,357 @@ class Gemini(loader.Module): """[] [auto] [-s] — \n[из id/@юза чата] экспорт. -s в избранное.""" args = utils.get_args_raw(message).split() save_to_self = "-s" in args - if save_to_self: - args.remove("-s") - gauto_mode = "auto" in args - if gauto_mode: - args.remove("auto") - source_chat_id_str = args[0] if args else None - target_chat_id = "me" if save_to_self else message.chat_id - if source_chat_id_str: - try: - entity = await self.client.get_entity( - int(source_chat_id_str) - if source_chat_id_str.lstrip("-").isdigit() - else source_chat_id_str - ) - source_chat_id = entity.id - except Exception: - await utils.answer( - message, - self.strings["gme_chat_not_found"].format( - utils.escape_html(source_chat_id_str) - ), - ) - return - else: - source_chat_id = utils.get_chat_id(message) - hist = self._get_structured_history(source_chat_id, gauto=gauto_mode) - if not hist: - await utils.answer(message, "История для экспорта пуста.") - return - user_ids = {e.get("user_id") for e in hist if e.get("role") == "user" and e.get("user_id")} - user_names = {None: None} - for uid in user_ids: - if not uid: - continue - try: - entity = await self.client.get_entity(uid) - user_names[uid] = get_display_name(entity) - except Exception: - user_names[uid] = f"Deleted Account ({uid})" + if save_to_self: args.remove("-s") + gauto = "auto" in args + if gauto: args.remove("auto") + src_id = int(args[0]) if args and args[0].lstrip('-').isdigit() else utils.get_chat_id(message) + hist = self._get_structured_history(src_id, gauto=gauto) + if not hist: return await utils.answer(message, "История для экспорта пуста.") import json - def make_serializable(entry): - entry = dict(entry) - user_id = entry.get("user_id") - if user_id: - entry["user_name"] = user_names.get(user_id) - if hasattr(user_id, "user_id"): - entry["user_id"] = user_id.user_id - elif isinstance(user_id, (int, str)): - entry["user_id"] = user_id - elif user_id is not None: - entry["user_id"] = str(user_id) - else: - entry["user_id"] = None - if "message_id" in entry and entry["message_id"] is not None: - try: - entry["message_id"] = int(entry["message_id"]) - except (ValueError, TypeError): - entry["message_id"] = None - return entry - serializable_hist = [make_serializable(e) for e in hist] - data = json.dumps(serializable_hist, ensure_ascii=False, indent=2) - file_suffix = "gauto_history" if gauto_mode else "history" - file = io.BytesIO(data.encode("utf-8")) - file.name = f"gemini_{file_suffix}_{source_chat_id}.json" - caption = "Экспорт истории gauto Gemini" if gauto_mode else "Экспорт памяти Gemini" - if source_chat_id != utils.get_chat_id(message): - caption += f" из чата {source_chat_id}" - await self.client.send_file( - target_chat_id, - file, - caption=caption, - reply_to=message.id if target_chat_id == message.chat_id else None, - ) - if save_to_self: - await utils.answer(message, self.strings["gme_sent_to_saved"]) - elif source_chat_id_str: - await message.delete() + data = json.dumps(hist, ensure_ascii=False, indent=2) + f = io.BytesIO(data.encode('utf-8')) + f.name = f"gemini_{'gauto_' if gauto else ''}{src_id}.json" + dest = "me" if save_to_self else message.chat_id + cap = "Экспорт истории gauto Gemini" if gauto else "Экспорт памяти Gemini" + if src_id != utils.get_chat_id(message): cap += f" из чата {src_id}" + await self.client.send_file(dest, f, caption=cap) + if save_to_self: await utils.answer(message, self.strings["gme_sent_to_saved"]) + elif args: await message.delete() @loader.command() async def gmemimport(self, message: Message): """[auto] — импорт истории из файла (ответом). auto для gauto.""" - reply=await message.get_reply_message() + reply = await message.get_reply_message() if not reply or not reply.document: return await utils.answer(message, "Ответьте на json-файл с памятью.") - args=utils.get_args_raw(message) - gauto_mode=args=="auto" - file=io.BytesIO() - await self.client.download_media(reply, file) - file.seek(0) - MAX_IMPORT_SIZE=6 * 1024 * 1024 - if file.getbuffer().nbytes > MAX_IMPORT_SIZE: return await utils.answer(message, f"Файл слишком большой (>{MAX_IMPORT_SIZE // (1024*1024)} МБ).") - import json + gauto = "auto" in utils.get_args_raw(message) + try: - hist=json.load(file) - if not isinstance(hist, list): raise ValueError("Файл не содержит список истории.") - new_hist=[] - for e in hist: - if not isinstance(e, dict) or "role" not in e or "content" not in e: raise ValueError("Некорректная структура памяти.") - entry={"role": e["role"], "type": e.get("type", "text"), "content": e["content"], "date": e.get("date")} - if e["role"]=="user": - entry["user_id"]=e.get("user_id") - entry["message_id"]=e.get("message_id") - new_hist.append(entry) - chat_id=utils.get_chat_id(message) - conversations=self.gauto_conversations if gauto_mode else self.conversations - conversations[str(chat_id)]=new_hist - self._save_history_sync(gauto=gauto_mode) + f = await self.client.download_media(reply, bytes) + import json + hist = json.loads(f) + if not isinstance(hist, list): raise ValueError + + cid = utils.get_chat_id(message) + target = self.gauto_conversations if gauto else self.conversations + target[str(cid)] = hist + self._save_history_sync(gauto) await utils.answer(message, "Память успешно импортирована.") - except Exception as e: - await utils.answer(message, f"Ошибка импорта: {e}") + except Exception as e: await utils.answer(message, f"Ошибка импорта: {e}") @loader.command() async def gmemfind(self, message: Message): """[слово] — Поиск по истории текущего чата по ключевому слову или фразе.""" - args=utils.get_args_raw(message) - if not args: return await utils.answer(message, "Укажите слово для поиска.") - chat_id=utils.get_chat_id(message) - hist=self._get_structured_history(chat_id) - found=[f"{e['role']}: {e.get('content','')[:200]}" for e in hist if args.lower() in str(e.get("content", "")).lower()] + q = utils.get_args_raw(message).lower() + if not q: return await utils.answer(message, "Укажите слово для поиска.") + cid = utils.get_chat_id(message) + hist = self._get_structured_history(cid) + found = [f"{e['role']}: {e.get('content','')[:200]}" for e in hist if q in str(e.get('content','')).lower()] if not found: await utils.answer(message, "Ничего не найдено.") else: await utils.answer(message, "\n\n".join(found[:10])) @loader.command() async def gmemoff(self, message: Message): """— Отключить память в этом чате""" - chat_id=utils.get_chat_id(message) - self.memory_disabled_chats.add(str(chat_id)) + self.memory_disabled_chats.add(str(utils.get_chat_id(message))) await utils.answer(message, "Память в этом чате отключена.") @loader.command() async def gmemon(self, message: Message): """— Включить память в этом чате""" - chat_id=utils.get_chat_id(message) - self.memory_disabled_chats.discard(str(chat_id)) + self.memory_disabled_chats.discard(str(utils.get_chat_id(message))) await utils.answer(message, "Память в этом чате включена.") @loader.command() async def gmemshow(self, message: Message): """[auto] — Показать память чата (до 20 последних запросов). auto для gauto.""" - args=utils.get_args_raw(message) - gauto_mode=args=="auto" - chat_id=utils.get_chat_id(message) - hist=self._get_structured_history(chat_id, gauto=gauto_mode) + gauto = "auto" in utils.get_args_raw(message) + cid = utils.get_chat_id(message) + hist = self._get_structured_history(cid, gauto=gauto) if not hist: return await utils.answer(message, "Память пуста.") - out=[] + out = [] for e in hist[-40:]: - role=e.get('role') - content=utils.escape_html(str(e.get('content',''))[:300]) - if role=='user': out.append(f"{content}") - elif role=='model': out.append(f"Gemini: {content}") - text="
" + "\n".join(out) + "
" - await utils.answer(message, text) + role = e.get('role') + content = utils.escape_html(str(e.get('content',''))[:300]) + if role == 'user': out.append(f"{content}") + elif role == 'model': out.append(f"Gemini: {content}") + await utils.answer(message, "
" + "\n".join(out) + "
") @loader.command() async def gmodel(self, message: Message): """[model или пусто] — Узнать/сменить модель. -s — список доступных моделей в файле.""" args = utils.get_args_raw(message).strip().lower() if '-s' in args: - if not self.api_keys: - await utils.answer(message, self.strings['no_api_key']) - return - status_msg = await utils.answer(message, self.strings["processing"]) + if not self.api_keys: return await utils.answer(message, self.strings['no_api_key']) + sts = await utils.answer(message, self.strings["processing"]) try: - api_key = self.api_keys[self.current_api_key_index] - old_genai.configure(api_key=api_key) - models_list = [] - for model_obj in old_genai.list_models(): - model_name = model_obj.name - display_name = model_obj.display_name or "Неизвестно" - methods = ", ".join(model_obj.supported_generation_methods) if model_obj.supported_generation_methods else "Нет" - img_support = self.strings["gmodel_img_support"] if 'predict' in model_obj.supported_generation_methods or 'generateContent' in model_obj.supported_generation_methods else self.strings["gmodel_no_support"] - models_list.append(f"• {model_name} — {display_name} ({img_support})") - if not models_list: - await utils.answer(status_msg, self.strings["gmodel_no_models"]) - return - text = self.strings["gmodel_list_title"] + "\n" + "\n".join(models_list) - file = io.BytesIO(text.encode("utf-8")) - file.name = "models_list.txt" - await self.client.send_file( - message.chat_id, - file=file, - caption="📋 Список доступных моделей Gemini", - reply_to=message.id - ) - except Exception as e: - await utils.answer(status_msg, self.strings["gmodel_list_error"].format(self._handle_error(e))) - return - if not args: - await utils.answer(message, f"Текущая модель: {self.config['model_name']}") + client = genai.Client(api_key=self.api_keys[0]) + models = await asyncio.to_thread(client.models.list) + txt = "\n".join([f"• {m.name.split('/')[-1]} ({m.display_name})" for m in models]) + f = io.BytesIO((self.strings["gmodel_list_title"] + "\n" + txt).encode('utf-8')) + f.name = "models_list.txt" + await self.client.send_file(message.chat_id, file=f, caption="📋 Список доступных моделей", reply_to=message.id) + await sts.delete() + except Exception as e: await utils.answer(sts, self.strings["gmodel_list_error"].format(self._handle_error(e))) return + + if not args: return await utils.answer(message, f"Текущая модель: {self.config['model_name']}") self.config["model_name"] = args await utils.answer(message, f"Модель Gemini установлена: {args}") @loader.command() async def gres(self, message: Message): """[auto] — Очистить ВСЮ память. auto для всей памяти gauto.""" - args=utils.get_args_raw(message) - if args=="auto": + if utils.get_args_raw(message) == "auto": if not self.gauto_conversations: return await utils.answer(message, self.strings["no_gauto_memory_to_fully_clear"]) - num_chats=len(self.gauto_conversations) + n = len(self.gauto_conversations) self.gauto_conversations.clear() - self._save_history_sync(gauto=True) - await utils.answer(message, self.strings["gauto_memory_fully_cleared"].format(num_chats)) - elif not args: - if not self.conversations: return await utils.answer(message, self.strings["no_memory_to_fully_clear"]) - num_chats=len(self.conversations) - self.conversations.clear() - self._save_history_sync(gauto=False) - await utils.answer(message, self.strings["memory_fully_cleared"].format(num_chats)) + self._save_history_sync(True) + await utils.answer(message, self.strings["gauto_memory_fully_cleared"].format(n)) else: - await utils.answer(message, self.strings["gres_usage"]) - - def _configure_proxy(self): - for var in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"]: os.environ.pop(var, None) - if self.config["proxy"]: - os.environ["http_proxy"]=self.config["proxy"] - os.environ["https_proxy"]=self.config["proxy"] + if not self.conversations: return await utils.answer(message, self.strings["no_memory_to_fully_clear"]) + n = len(self.conversations) + self.conversations.clear() + self._save_history_sync(False) + await utils.answer(message, self.strings["memory_fully_cleared"].format(n)) @loader.watcher(only_incoming=True, ignore_edited=True) async def watcher(self, message: Message): - if not isinstance(message, types.Message) or not hasattr(message, 'chat_id'): - return - chat_id = utils.get_chat_id(message) - if chat_id not in self.impersonation_chats: - return - if message.is_private and not self.config["gauto_in_pm"]: - return - is_from_self_user = isinstance(message.from_id, types.PeerUser) and message.from_id.user_id == self.me.id - if message.out or is_from_self_user: - return - if message.text and message.text.startswith(self.get_prefix()): - return + if not hasattr(message, 'chat_id'): return + cid = utils.get_chat_id(message) + if cid not in self.impersonation_chats: return + if message.is_private and not self.config["gauto_in_pm"]: return + if message.out or (isinstance(message.from_id, tg_types.PeerUser) and message.from_id.user_id == self.me.id): return sender = await message.get_sender() - is_sender_a_bot = isinstance(sender, types.User) and sender.bot - if not sender or is_sender_a_bot: - return - if random.random() > self.config["impersonation_reply_chance"]: - return + if isinstance(sender, tg_types.User) and sender.bot: return + if random.random() > self.config["impersonation_reply_chance"]: return parts, warnings = await self._prepare_parts(message) - if warnings: - logger.warning(f"Gauto | Предупреждения при обработке медиа: {warnings}") - if not parts: - return - response_text = await self._send_to_gemini(message=message, parts=parts, impersonation_mode=True) - if response_text and response_text.strip(): - clean_text = response_text.strip() - reaction_delay = random.uniform(2.0, 10.0) - await asyncio.sleep(reaction_delay) - try: - await self.client.send_read_acknowledge(message.chat_id, message=message) - except Exception: - pass - char_count = len(clean_text) - typing_speed = random.uniform(0.1, 0.25) - typing_duration = char_count * typing_speed - typing_duration = max(1.5, min(typing_duration, 25.0)) - async with message.client.action(message.chat_id, "typing"): - await asyncio.sleep(typing_duration) - await message.reply(clean_text) + if warnings: logger.warning(f"Gauto warn: {warnings}") + if not parts: return + resp = await self._send_to_gemini(message=message, parts=parts, impersonation_mode=True) + if resp and resp.strip(): + cln = resp.strip() + await asyncio.sleep(random.uniform(2, 8)) + try: await self.client.send_read_acknowledge(cid, message=message) + except: pass + async with message.client.action(cid, "typing"): + await asyncio.sleep(min(25.0, max(1.5, len(cln) * random.uniform(0.1, 0.25)))) + await message.reply(cln) - def _load_history_from_db(self, db_key: str) -> dict: - raw_conversations=self.db.get(self.strings["name"], db_key, {}) - if not isinstance(raw_conversations, dict): - logger.warning(f"Gemini: БД для ключа '{db_key}' повреждена, сбрасываю.") - raw_conversations={}; self.db.set(self.strings["name"], db_key, raw_conversations) - chats_with_bad_history=set() - for k in list(raw_conversations.keys()): - v=raw_conversations[k] - if not isinstance(v, list): - chats_with_bad_history.add(k) - raw_conversations[k]=[] - else: - filtered, bad_found=[], False - for e in v: - if isinstance(e, dict) and "role" in e and "content" in e: filtered.append(e) - else: bad_found=True - if bad_found: chats_with_bad_history.add(k) - raw_conversations[k]=filtered - if chats_with_bad_history: logger.warning(f"Gemini ({db_key}): Некорректная структура памяти в {len(chats_with_bad_history)} чатах. Некорректные записи пропущены.") - return raw_conversations + def _get_proxy_config(self): + p = self.config["proxy"] + return {"http://": p, "https://": p} if p else None def _save_history_sync(self, gauto: bool=False): if getattr(self, "_db_broken", False): return - conversations_to_save, db_key=(self.gauto_conversations, DB_GAUTO_HISTORY_KEY) if gauto else (self.conversations, DB_HISTORY_KEY) - try: self.db.set(self.strings["name"], db_key, conversations_to_save) - except Exception as e: - logger.error(f"Ошибка сохранения истории Gemini (gauto={gauto}): {e}") - self._db_broken=True + data, key = (self.gauto_conversations, DB_GAUTO_HISTORY_KEY) if gauto else (self.conversations, DB_HISTORY_KEY) + try: self.db.set(self.strings["name"], key, data) + except: self._db_broken = True - def _get_structured_history(self, chat_id: int, gauto: bool=False) -> list: - conversations=self.gauto_conversations if gauto else self.conversations - hist=conversations.get(str(chat_id), []) - if not isinstance(hist, list): - logger.warning(f"Память для чата {chat_id} (gauto={gauto}) повреждена, сбрасываю.") - hist=[] - conversations[str(chat_id)]=hist - self._save_history_sync(gauto) - return hist + def _load_history_from_db(self, key): + d = self.db.get(self.strings["name"], key, {}) + return d if isinstance(d, dict) else {} + + def _get_structured_history(self, cid, gauto=False): + d = self.gauto_conversations if gauto else self.conversations + if str(cid) not in d: d[str(cid)] = [] + return d[str(cid)] def _update_history(self, chat_id: int, user_parts: list, model_response: str, regeneration: bool = False, message: Message = None, gauto: bool = False): if not self._is_memory_enabled(str(chat_id)): return history = self._get_structured_history(chat_id, gauto) - now = int(asyncio.get_event_loop().time()) + import time + now = int(time.time()) user_id = self.me.id - if message: - try: - peer_id = get_peer_id(message) - if peer_id: - user_id = peer_id - except (TypeError, ValueError): - pass + user_name = get_display_name(self.me) message_id = getattr(message, "id", None) + + if message: + if message.sender_id: + user_id = message.sender_id + if message.sender: + user_name = get_display_name(message.sender) user_text = " ".join([p.text for p in user_parts if hasattr(p, "text") and p.text]) or "[ответ на медиа]" - if regeneration: + if regeneration and history: for i in range(len(history) - 1, -1, -1): if history[i].get("role") == "model": - history[i].update({"content": model_response, "date": now}) + history[i].update({ + "content": model_response, + "date": now + }) break else: - history.extend([ - {"role": "user", "type": "text", "content": user_text, "date": now, "user_id": user_id, "message_id": message_id}, - {"role": "model", "type": "text", "content": model_response, "date": now}, - ]) - max_len = self.config["max_history_length"] - if max_len > 0 and len(history) > max_len * 2: - history = history[-(max_len * 2):] - conversations = self.gauto_conversations if gauto else self.conversations - conversations[str(chat_id)] = history + user_entry = { + "role": "user", + "type": "text", + "content": user_text, + "date": now, + "user_id": user_id, + "message_id": message_id, + "user_name": user_name + } + model_entry = { + "role": "model", + "type": "text", + "content": model_response, + "date": now, + "user_id": None + } + + history.extend([user_entry, model_entry]) + limit = self.config["max_history_length"] + if limit > 0 and len(history) > limit * 2: + history = history[-(limit * 2):] + target = self.gauto_conversations if gauto else self.conversations + target[str(chat_id)] = history self._save_history_sync(gauto) - def _clear_history(self, chat_id: int, gauto: bool=False): - conversations=self.gauto_conversations if gauto else self.conversations - if str(chat_id) in conversations: - del conversations[str(chat_id)] + def _clear_history(self, cid, gauto=False): + d = self.gauto_conversations if gauto else self.conversations + if str(cid) in d: + del d[str(cid)] self._save_history_sync(gauto) + def _is_memory_enabled(self, cid): return cid not in self.memory_disabled_chats + + def _markdown_to_html(self, text): + text = re.sub(r"^(#+)\s+(.*)", lambda m: f"{m.group(2)}", text, flags=re.M) + text = re.sub(r"^([ \t]*)[-*+]\s+", r"\1• ", text, flags=re.M) + md = MarkdownIt("commonmark", {"html": True, "linkify": True}).enable("strikethrough") + html = md.render(text) + def fmt_code(m): + lang = utils.escape_html(m.group(1).strip()) if m.group(1) else "" + return f'
{utils.escape_html(m.group(2).strip())}
' if lang else f'
{utils.escape_html(m.group(2).strip())}
' + html = re.sub(r"```(\w+)?\n([\s\S]+?)\n```", fmt_code, html) + html = re.sub(r"

(

[\s\S]*?
)

", r"\1", html, flags=re.DOTALL) + return html.replace("

", "").replace("

", "\n").strip() + + def _format_response_with_smart_separation(self, text): + parts = re.split(r"([\s\S]*?)", text, flags=re.DOTALL) + out = [] + for i, p in enumerate(parts): + if not p or p.isspace(): continue + if i % 2 == 1: out.append(p.strip()) + else: out.append(f"
{p.strip()}
") + return "\n".join(out) + + def _get_inline_buttons(self, cid, mid): + return [[ + {"text": self.strings["btn_clear"], "callback": self._clear_callback, "args": (cid,)}, + {"text": self.strings["btn_regenerate"], "callback": self._regenerate_callback, "args": (mid, cid)} + ]] + + async def _clear_callback(self, call: InlineCall, cid): + self._clear_history(cid, gauto=False) + await call.edit(self.strings["memory_cleared"], reply_markup=None) + + async def _regenerate_callback(self, call: InlineCall, mid, cid): + key = f"{cid}:{mid}" + if key not in self.last_requests: return await call.answer(self.strings["no_last_request"], show_alert=True) + parts, disp = self.last_requests[key] + use_url_context = bool(re.search(r'https?://\S+', disp or "")) + await self._send_to_gemini(mid, parts, regeneration=True, call=call, chat_id_override=cid, display_prompt=disp, use_url_context=use_url_context) + + async def _get_recent_chat_text(self, cid, count=None, skip_last=False): + lim = (count or self.config["impersonation_history_limit"]) + (1 if skip_last else 0) + lines = [] + try: + msgs = await self.client.get_messages(cid, limit=lim) + if skip_last and msgs: msgs = msgs[1:] + for m in msgs: + if not m or (not m.text and not m.media): continue + name = get_display_name(await m.get_sender()) or "Unknown" + txt = m.text or ("[Media]" if m.media else "") + if m.sticker: + alt = next((a.alt for a in m.sticker.attributes if isinstance(a, DocumentAttributeSticker)), "?") + txt += f" [Стикер: {alt}]" + elif m.photo: txt += " [Фото]" + elif m.document and not hasattr(m.media, "webpage"): txt += " [Файл]" + if txt.strip(): lines.append(f"{name}: {txt.strip()}") + except: pass + return "\n".join(reversed(lines)) + def _handle_error(self, e: Exception) -> str: logger.exception("Gemini execution error") if isinstance(e, asyncio.TimeoutError): return self.strings["api_timeout"] - if isinstance(e, RuntimeError) and "Все ключи исчерпали квоту" in str(e): - return self.strings["all_keys_exhausted"].format(len(self.api_keys)) - if isinstance(e, google_exceptions.GoogleAPIError): - msg = str(e) - if "quota" in msg.lower() or "exceeded" in msg.lower(): - model_name = self.config.get("model_name", "unknown") - model_name_match = re.search(r'key: "model"\s+value: "([^"]+)"', msg) - if model_name_match: - model_name = model_name_match.group(1) - return ( - f"❗️ Превышен лимит Google Gemini API для модели {utils.escape_html(model_name)}." - "\n\nЧаще всего это происходит на бесплатном тарифе. Вы можете:\n" - "• Подождать, пока лимит сбросится (обычно раз в сутки).\n" - "• Проверить свой тарифный план в Google AI Studio.\n" - "• Узнать больше о лимитах здесь.\n\n" - f"Детали ошибки:\n{utils.escape_html(msg)}" - ) - if "500 An internal error has occurred" in msg: - return ( - "❗️ Ошибка 500 от Google API.\n" - "Это значит, что формат медиа (файл или еще что то) который ты отправил, не поддерживается.\n" - "Такое случается, по такой причине:\n " - "• Если формат файла в принципе не поддерживается Gemini/Гуглом.\n " - "• Временный сбой на серверах Google. Попробуйте повторить запрос позже." - ) - if "User location is not supported for the API use" in msg or "location is not supported" in msg: - return ( - '❗️ В данном регионе Gemini API не доступен.\n' - 'Скачайте VPN (для пк/тел) или поставьте прокси (платный/бесплатный).\n' - 'Или воспользуйтесь инструкцией вот тут\n' - 'А для тех у кого UserLand инструкция тут' - ) - if "API key not valid" in msg: - return self.strings["invalid_api_key"] - if "blocked" in msg.lower(): - return self.strings["blocked_error"].format(utils.escape_html(msg)) - return self.strings["api_error"].format(utils.escape_html(msg)) - if isinstance(e, (OSError, aiohttp.ClientError, socket.timeout)): - return "❗️ Сетевая ошибка:\n{}".format(utils.escape_html(str(e))) msg = str(e) - if "No API_KEY or ADC found" in msg or "GOOGLE_API_KEY environment variable" in msg or "genai.configure(api_key" in msg: - return self.strings["no_api_key"] - return self.strings["generic_error"].format(utils.escape_html(str(e))) + if "quota" in msg.lower() or "exhausted" in msg.lower() or "429" in msg: + model = self.config.get("model_name", "unknown") + return ( + f"❗️ Превышен лимит Google Gemini API для модели {utils.escape_html(model)}." + "\n\nЧаще всего это происходит на бесплатном тарифе. Вы можете:\n" + "• Подождать, пока лимит сбросится (обычно раз в сутки).\n" + "• Проверить свой тарифный план в Google AI Studio.\n" + "• Узнать больше о лимитах здесь.\n\n" + f"Детали ошибки:\n{utils.escape_html(msg)}" + ) + if "location" in msg.lower() or "not supported" in msg.lower(): + return ( + '❗️ В данном регионе Gemini API не доступен.\n' + 'Скачайте VPN (для пк/тел) или поставьте прокси (платный/бесплатный).\n' + 'Или воспользуйтесь инструкцией вот тут\n' + 'А для тех у кого UserLand инструкция тут' + ) + if "key" in msg.lower() and "valid" in msg.lower(): + return self.strings["invalid_api_key"] + if "blocked" in msg.lower(): + return self.strings["blocked_error"].format(utils.escape_html(msg)) + if "500" in msg: + return ( + "❗️ Ошибка 500 от Google API.\n" + "Это значит, что формат медиа (файл или еще что то) который ты отправил, не поддерживается.\n" + "Такое случается, по такой причине:\n " + "• Если формат файла в принципе не поддерживается Gemini/Гуглом.\n " + "• Временный сбой на серверах Google. Попробуйте повторить запрос позже." + ) + return self.strings["api_error"].format(utils.escape_html(msg)) def _markdown_to_html(self, text: str) -> str: - def heading_replacer(match): level=len(match.group(1)); title=match.group(2).strip(); indent=" " * (level - 1); return f"{indent}{title}" - text=re.sub(r"^(#+)\s+(.*)", heading_replacer, text, flags=re.MULTILINE) - def list_replacer(match): indent=match.group(1); return f"{indent}• " - text=re.sub(r"^([ \t]*)[-*+]\s+", list_replacer, text, flags=re.MULTILINE) - md=MarkdownIt("commonmark", {"html": True, "linkify": True}); md.enable("strikethrough"); md.disable("hr"); md.disable("heading"); md.disable("list") - html_text=md.render(text) + def heading_replacer(match): + level = len(match.group(1)) + title = match.group(2).strip() + indent = " " * (level - 1) + return f"{indent}{title}" + text = re.sub(r"^(#+)\s+(.*)", heading_replacer, text, flags=re.MULTILINE) + def list_replacer(match): + indent = match.group(1) + return f"{indent}• " + text = re.sub(r"^([ \t]*)[-*+]\s+", list_replacer, text, flags=re.MULTILINE) + md = MarkdownIt("commonmark", {"html": True, "linkify": True}) + md.enable("strikethrough") + md.disable("hr") + md.disable("heading") + md.disable("list") + html_text = md.render(text) def format_code(match): - lang=utils.escape_html(match.group(1).strip()); code=utils.escape_html(match.group(2).strip()) + lang = utils.escape_html(match.group(1).strip()) + code = utils.escape_html(match.group(2).strip()) return f'
{code}
' if lang else f'
{code}
' - html_text=re.sub(r"```(.*?)\n([\s\S]+?)\n```", format_code, html_text) - html_text=re.sub(r"

(

[\s\S]*?
)

", r"\1", html_text, flags=re.DOTALL) - html_text=html_text.replace("

", "").replace("

", "\n").strip() + html_text = re.sub(r"```(.*?)\n([\s\S]+?)\n```", format_code, html_text) + html_text = re.sub(r"

(

[\s\S]*?
)

", r"\1", html_text, flags=re.DOTALL) + html_text = html_text.replace("

", "").replace("

", "\n").strip() return html_text def _format_response_with_smart_separation(self, text: str) -> str: - pattern=r"([\s\S]*?)"; parts=re.split(pattern, text, flags=re.DOTALL); result_parts=[] + pattern = r"([\s\S]*?)" + parts = re.split(pattern, text, flags=re.DOTALL) + result_parts = [] for i, part in enumerate(parts): if not part or part.isspace(): continue - if i % 2==1: result_parts.append(part.strip()) + if i % 2 == 1: + result_parts.append(part.strip()) else: - stripped_part=part.strip() - if stripped_part: result_parts.append(f'
{stripped_part}
') + stripped_part = part.strip() + if stripped_part: + result_parts.append(f'
{stripped_part}
') return "\n".join(result_parts) - def _get_inline_buttons(self, chat_id, base_message_id): return [[{"text": self.strings["btn_clear"], "callback": self._clear_callback, "args": (chat_id,)}, {"text": self.strings["btn_regenerate"], "callback": self._regenerate_callback, "args": (base_message_id, chat_id)}]] + + def _get_inline_buttons(self, chat_id, base_message_id): + return [[ + {"text": self.strings["btn_clear"], "callback": self._clear_callback, "args": (chat_id,)}, + {"text": self.strings["btn_regenerate"], "callback": self._regenerate_callback, "args": (base_message_id, chat_id)} + ]] async def _safe_del_msg(self, msg, delay=1): await asyncio.sleep(delay) @@ -1163,13 +1001,24 @@ class Gemini(loader.Module): async def _clear_callback(self, call: InlineCall, chat_id: int): self._clear_history(chat_id, gauto=False) - await call.edit(self.strings["memory_cleared"], reply_markup=None) + await call.edit(self.strings["memory_cleared"], reply_markup=None) async def _regenerate_callback(self, call: InlineCall, original_message_id: int, chat_id: int): - key=f"{chat_id}:{original_message_id}"; last_request_tuple=self.last_requests.get(key) - if not last_request_tuple: return await call.answer(self.strings["no_last_request"], show_alert=True) - last_parts, display_prompt=last_request_tuple; use_url_context=bool(re.search(r'https?://\S+', display_prompt or "")) - await self._send_to_gemini(message=original_message_id, parts=last_parts, regeneration=True, call=call, chat_id_override=chat_id, use_url_context=use_url_context, display_prompt=display_prompt) + key = f"{chat_id}:{original_message_id}" + last_request_tuple = self.last_requests.get(key) + if not last_request_tuple: + return await call.answer(self.strings["no_last_request"], show_alert=True) + last_parts, display_prompt = last_request_tuple + use_url_context = bool(re.search(r'https?://\S+', display_prompt or "")) + await self._send_to_gemini( + message=original_message_id, + parts=last_parts, + regeneration=True, + call=call, + chat_id_override=chat_id, + use_url_context=use_url_context, + display_prompt=display_prompt + ) async def _get_recent_chat_text(self, chat_id: int, count: int = None, skip_last: bool = False) -> str: history_limit = count or self.config["impersonation_history_limit"] @@ -1180,20 +1029,20 @@ class Gemini(loader.Module): if skip_last and messages: messages = messages[1:] for msg in messages: - if not msg: - continue + if not msg: continue if not msg.text and not msg.sticker and not msg.photo and not (msg.media and not hasattr(msg.media, "webpage")): continue sender = await msg.get_sender() sender_name = get_display_name(sender) if sender else "Unknown" text_content = msg.text or "" if msg.sticker and hasattr(msg.sticker, 'attributes'): - alt_text = next((attr.alt for attr in msg.sticker.attributes if isinstance(attr, types.DocumentAttributeSticker)), None) + alt_text = next((attr.alt for attr in msg.sticker.attributes if isinstance(attr, DocumentAttributeSticker)), None) text_content += f" [Стикер: {alt_text or '?'}]" elif msg.photo: text_content += " [Фото]" elif msg.document and not hasattr(msg.media, "webpage"): text_content += " [Файл]" + if text_content.strip(): chat_history_lines.append(f"{sender_name}: {text_content.strip()}") except Exception as e: diff --git a/coddrago/modules/chatmodule.py b/coddrago/modules/chatmodule.py new file mode 100644 index 0000000..21b2109 --- /dev/null +++ b/coddrago/modules/chatmodule.py @@ -0,0 +1,999 @@ +# meta developer: @codrago_m +# scope: disable_onload_docs +# packurl: https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/translations/chatmodule.yml + +import logging +import typing +from datetime import datetime, timedelta, timezone + +from telethon.tl import types +from telethon.tl.functions import channels, messages + +from .. import loader, utils + +logger = logging.getLogger("ChatModule") + + +@loader.tds +class ChatModuleMod(loader.Module): + strings = { + "name": "ChatModule", + } + + async def client_ready(self, client, db): + self._client = client + self._db = db + self.xdlib = await self.import_lib( + "https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/libs/xdlib.py", + suspend_on_error=True, + ) + + @loader.command(ru_doc="[reply] - Узнать ID") + async def id(self, message): + """[reply] - Get the ID""" + ids = [self.strings["my_id"].format(id=self.tg_id)] + if message.is_private: + ids.append(self.strings["user_id"].format(id=message.to_id.user_id)) + return await utils.answer(message, "\n".join(ids)) + ids.append(self.strings["chat_id"].format(id=message.chat_id)) + reply = await message.get_reply_message() + if ( + reply + and not getattr(reply, "is_private") + and not getattr(reply, "sender_id") == self.tg_id + ): + user_id = (await reply.get_sender()).id + ids.append(self.strings["user_id"].format(id=user_id)) + return await utils.answer(message, "\n".join(ids)) + + @loader.command( + ru_doc="[reply/-u username/id] - Посмотреть права администратора пользователя", + ) + @loader.tag("no_pm") + async def rights(self, message): + """[reply/-u username/id] - Check user's admin rights""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + reply = await message.get_reply_message() + user = opts.get("u") or opts.get("user") or (reply.sender_id if reply else None) + if not user: + return await utils.answer(message, self.strings["no_user"]) + rights = await self.xdlib.chat.get_rights(message.chat, user) + + participant = rights.participant + user = await self._client.get_entity(user) + if not hasattr(participant, "admin_rights"): + return await utils.answer( + message, self.strings["not_an_admin"].format(user=user.first_name) + ) + if participant.admin_rights: + can_do = [] + rights = participant.to_dict().get("admin_rights") + for right, is_permitted in rights.items(): + if right == "_": + continue + if is_permitted: + can_do.append(right) + promoter = ( + await self._client.get_entity(participant.promoted_by) + if hasattr(participant, "promoted_by") + else None + ) + return await utils.answer( + message, + self.strings["admin_rights"].format( + rights="\n".join( + [ + f" {self.strings[right]}" + for right in can_do + ] + ), + promoter_id=promoter.id if promoter else 0, + promoter_name=( + promoter.first_name if promoter else self.strings["no"] + ), + name=user.first_name, + ), + ) + return await utils.answer( + message, self.strings["not_an_admin"].format(user=user.first_name) + ) + + @loader.command( + ru_doc="Покинуть чат", + ) + @loader.tag("no_pm") + async def leave(self, message): + """Leave chat""" + await message.delete() + await self._client(channels.LeaveChannelRequest((await message.get_chat()).id)) + + @loader.command( + ru_doc="[a[1-100] b[1-100]] | [reply] Удалить сообщения", + ) + async def d(self, message): + """[a[1-100] b[1-100]] | [reply] - Delete messages""" + await self.xdlib.messages.delete_messages(message) + + @loader.command(ru_doc="[reply] - Закрепить сообщение") + @loader.tag("only_reply") + async def pin(self, message): + """[reply] - Pin a message""" + reply = await message.get_reply_message() + try: + await reply.pin(notify=True, pm_oneside=False) + except Exception as e: + logger.error(str(e)) + return await utils.answer(message, self.strings["pin_failed"]) + await utils.answer(message, self.strings["pinned"]) + + @loader.command(ru_doc="Открепить сообщение") + @loader.tag("only_reply") + async def unpin(self, message): + """Unpin a message""" + reply = await message.get_reply_message() + try: + await reply.unpin() + except Exception as e: + logger.error(str(e)) + return await utils.answer(message, self.strings["unpin_failed"]) + await utils.answer(message, self.strings["unpinned"]) + + @loader.command(ru_doc="[-c id] Удаляет группу/канал") + async def dgc(self, message): + """[-c id] Delete chat/channel""" + args = utils.get_args(message) + opts = self.xdlib.parse.opts(args) + chat_id = opts.get("c") or opts.get("chat") + if chat_id: + chat = await self._client.get_entity(chat_id) + if isinstance(chat, types.Channel): + await self._client(channels.DeleteChannelRequest(chat.id)) + elif isinstance(chat, types.Chat): + await self._client(messages.DeleteChatRequest(chat.id)) + else: + return await utils.answer(message, self.strings["failed_to_delete"]) + return await utils.answer(message, self.strings["successful_delete"]) + if isinstance(message.chat, types.Channel): + await self._client(channels.DeleteChannelRequest(message.chat)) + elif isinstance(message.chat, types.Chat): + await self._client(messages.DeleteChatRequest(message.chat)) + else: + return await utils.answer(message, self.strings["failed_to_delete"]) + return + + @loader.command(ru_doc="Очищает группу/канал от удаленных аккаунтов") + @loader.tag("no_pm") + async def flush(self, message): + """Removes deleted accounts from the chat/channel""" + chat = await message.get_chat() + + if not getattr(chat, "admin_rights", False) and not getattr( + getattr(chat, "admin_rights", None), "ban_users", False + ): + return await utils.answer(message, self.strings["no_rights"]) + + deleted = await self.xdlib.chat.get_deleted(chat) + if not deleted: + return await utils.answer(message, self.strings["no_deleted_accounts"]) + for to_delete in deleted: + try: + await self._client.kick_participant(chat, to_delete) + except Exception as e: + logger.error(str(e)) + return await utils.answer(message, self.strings["error"]) + return await utils.answer(message, self.strings["kicked_deleted_accounts"]) + + @loader.command(ru_doc="Показывает админов в группе/канале") + @loader.tag("no_pm") + async def admins(self, message): + """Shows the admins in the chat/channel""" + admins = await self.xdlib.chat.get_admins(message.chat, True) + creator = await self.xdlib.chat.get_creator(message.chat) + return await utils.answer( + message, + self.strings["admin_list"].format( + id=creator.id if creator else 0, + name=creator.first_name if creator else self.strings["no"], + admins_count=len(admins) or 0, + admins=( + "\n".join( + f" {admin.first_name} [{admin.id}] / {admin.participant.rank}" + for admin in admins + ) + if admins + else f"\n{self.strings['no_admins_in_chat']}" + ), + ), + ) + + @loader.command(ru_doc="Показывает ботов в группе/канале") + @loader.tag("no_pm") + async def bots(self, message): + """Shows the bots in the chat/channel""" + bots = await self.xdlib.chat.get_bots(message.chat) + if not bots: + return await utils.answer(message, self.strings["no_bots_in_chat"]) + await utils.answer( + message, + self.strings["bot_list"].format( + count=len(bots), + bots="\n".join( + [ + f" {bot.first_name} [{bot.id}]" + for bot in bots + ] + ), + ), + ) + + @loader.command(ru_doc="Показывает простых участников чата/канала") + @loader.tag("no_pm") + async def users(self, message): + """Shows the users in the chat/channel""" + users = await self.xdlib.chat.get_members(message.chat) + if not users: + return await utils.answer(message, self.strings["no_user_in_chat"]) + await utils.answer( + message, + self.strings["user_list"].format( + count=len(users), + users="\n".join( + [ + f" {user.first_name} [{user.id}]" + for user in users + ] + ), + ), + ) + + @loader.command(ru_doc="[-u] [-t] [-r] Забанить участника") + @loader.tag("no_pm") + async def ban(self, message): + """[-u] [-t] [-r] Ban a participant temporarily or permanently""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + reason = opts.get("r") + reply = await message.get_reply_message() + user = opts.get("u") or (reply.sender_id if reply else None) + user = await self._client.get_entity(user) if user else None + strings = [] + if not user: + return await utils.answer(message, self.strings["no_user"]) + + seconds = self.xdlib.parse.time(opts.get("t")) if opts.get("t") else None + until_date = ( + (datetime.now(timezone.utc) + timedelta(seconds=seconds)) + if seconds + else None + ) + time_info = f" {self.xdlib.format.time(seconds)}" if seconds else None + try: + await self._client.edit_permissions( + message.chat, + user, + until_date=until_date, + view_messages=False, + ) + except Exception as e: + logger.error(str(e)) + return await utils.answer(message, self.strings["error"]) + strings.append( + self.strings["user_is_banned"].format( + id=user.id, + name=( + getattr(user, "first_name") + if hasattr(user, "first_name") + else getattr(user, "title") + ), + time_info=time_info or self.strings["forever"], + ) + ) + + if reason: + strings.append(self.strings["reason"].format(reason=reason)) + return await utils.answer(message, "\n".join(strings)) + + @loader.command(ru_doc="Разбанить пользователя") + @loader.tag("no_pm") + async def unban(self, message): + """[-u] Unban a user""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + reply = await message.get_reply_message() + user = opts.get("u") or (reply.sender_id if reply else None) + user = await self._client.get_entity(user) if user else None + if not user: + return await utils.answer(message, self.strings["no_user"]) + try: + await self._client.edit_permissions(message.chat, user, view_messages=True) + except Exception as e: + logger.error(str(e)) + return await utils.answer(message, self.strings["error"]) + return await utils.answer( + message, + self.strings["user_is_unbanned"].format( + id=user.id, + name=( + getattr(user, "first_name") + if hasattr(user, "first_name") + else getattr(user, "title") + ), + ), + ) + + @loader.command(ru_doc="[-u] [-r] Кикнуть участника") + @loader.tag("no_pm") + async def kick(self, message): + """[-u] [-r] Kick a participant""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + reason = opts.get("r") + reply = await message.get_reply_message() + user = opts.get("u") or (reply.sender_id if reply else None) + user = await self._client.get_entity(user) if user else None + strings = [] + if not user: + return await utils.answer(message, self.strings["no_user"]) + try: + await self._client.kick_participant(message.chat, user) + except Exception as e: + logging.error(str(e)) + return await utils.answer(message, self.strings["error"]) + strings.append( + self.strings["user_is_kicked"].format( + id=user.id, + name=( + getattr(user, "first_name") + if hasattr(user, "first_name") + else getattr(user, "title") + ), + ) + ) + if reason: + strings.append(self.strings["reason"].format(reason=reason)) + return await utils.answer(message, "\n".join(strings)) + + @loader.command(ru_doc="[-u] [-t] [-r] Замутить участника") + @loader.tag("no_pm") + async def mute(self, message): + """[-u] [-t] [-r] Mute a participant temporarily or permanently""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + reason = opts.get("r") + reply = await message.get_reply_message() + user = opts.get("u") or (reply.sender_id if reply else None) + user = await self._client.get_entity(user) if user else None + strings = [] + if not user: + return await utils.answer(message, self.strings["no_user"]) + + seconds = self.xdlib.parse.time(opts.get("t")) if opts.get("t") else None + until_date = ( + (datetime.now(timezone.utc) + timedelta(seconds=seconds)) + if seconds + else None + ) + time_info = f" {self.xdlib.format.time(seconds)}" if seconds else None + try: + await self._client.edit_permissions( + message.chat, + user, + until_date=until_date, + send_messages=False, + ) + except Exception as e: + logger.error(str(e)) + return await utils.answer(message, self.strings["error"]) + strings.append( + self.strings["user_is_muted"].format( + id=user.id, + name=( + getattr(user, "first_name") + if hasattr(user, "first_name") + else getattr(user, "title") + ), + time_info=time_info or self.strings["forever"], + ) + ) + + if reason: + strings.append(self.strings["reason"].format(reason=reason)) + return await utils.answer(message, "\n".join(strings)) + + @loader.command(ru_doc="Размутить участника") + @loader.tag("no_pm") + async def unmute(self, message): + """Unmute a participant""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + reply = await message.get_reply_message() + user = opts.get("u") or (reply.sender_id if reply else None) + user = await self._client.get_entity(user) if user else None + if not user: + return await utils.answer(message, self.strings["no_user"]) + try: + await self._client.edit_permissions(message.chat, user, send_messages=True) + except Exception as e: + logger.error(str(e)) + return await utils.answer(message, self.strings["error"]) + return await utils.answer( + message, + self.strings["user_is_unmuted"].format( + id=user.id, + name=( + getattr(user, "first_name") + if hasattr(user, "first_name") + else getattr(user, "title") + ), + ), + ) + + @loader.command( + ru_doc="[-g|--group name] [-c|--channel name] - Создать группу/канал" + ) + async def create(self, message): + """[-g|--group name] [-c|--channel name] - Create group/channel""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + group_name = opts.get("g") or opts.get("group") + channel_name = opts.get("c") or opts.get("channel") + if channel_name: + result = await self._client( + channels.CreateChannelRequest( + title=channel_name, broadcast=True, about="" + ) + ) + chat = await self.xdlib.chat.get_info(result.chats[0]) + return await utils.answer( + message, + self.strings["channel_created"].format( + link=chat.get("link"), title=channel_name + ), + ) + if group_name: + result = await self._client( + channels.CreateChannelRequest( + title=group_name, megagroup=True, about="" + ) + ) + chat = await self.xdlib.chat.get_info(result.chats[0]) + return await utils.answer( + message, + self.strings["group_created"].format( + link=chat.get("link"), title=group_name + ), + ) + return await utils.answer(message, self.strings["invalid_args"]) + + @loader.command( + ru_doc="Отключает звук и архивирует чат", + ) + async def dnd(self, message): + """Mutes and archives the current chat""" + dnd = await utils.dnd(self._client, await message.get_chat()) + if dnd: + return await utils.answer(message, self.strings["dnd"]) + else: + return await utils.answer(message, self.strings["dnd_failed"]) + + @loader.command( + ru_doc="-u username/id - Пригласить пользователя в чат (-b пригласить инлайн бота)" + ) + async def invite(self, message): + """-u username/id - Invite a user to the chat (use -b to invite the inline bot)""" + args = utils.get_args(message) + opts = self.xdlib.parse.opts(args) + if opts.get("b") or opts.get("bot"): + invited = await self.xdlib.chat.invite_bot(self._client, message.chat) + entity = await self._client.get_entity(self.inline.bot_id) + if invited: + return await utils.answer( + message, + self.strings["user_invited"].format( + user=entity.first_name, id=entity.id + ), + ) + return await utils.answer(message, self.strings["user_not_invited"]) + reply = await message.get_reply_message() + user = opts.get("u") or opts.get("user") or (reply.sender_id if reply else None) + if not user: + return await utils.answer(message, self.strings["no_user"]) + entity = await self._client.get_entity(user) + invited = await self.xdlib.chat.invite_user(message.chat, user) + if invited: + return await utils.answer( + message, + self.strings["user_invited"].format( + user=entity.first_name, id=entity.id + ), + ) + return await utils.answer(message, self.strings["user_not_invited"]) + + @loader.command(ru_doc="[-i] Получить информацию о сущности") + async def inspect(self, message): + """[-i] Get the info about the entity""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + reply = await message.get_reply_message() + target = ( + opts.get("i") + or (reply.sender if reply else await message.get_chat()) + or None + ) + if not target: + return await utils.answer(message, self.strings["no_user"]) + ent = await self._client.get_entity(target) + if isinstance(ent, types.Channel): + try: + chatinfo = await self.xdlib.chat.get_info(ent) + photo = chatinfo.get("chat_photo") + photo = photo if not isinstance(photo, types.PhotoEmpty) else None + return await utils.answer( + message, + self.strings["chatinfo"].format( + id=chatinfo.get("id"), + title=chatinfo.get("title"), + about=chatinfo.get("about") or self.strings["no"], + admins_count=chatinfo.get("admins_count"), + online_count=chatinfo.get("online_count"), + participants_count=chatinfo.get("participants_count"), + kicked_count=chatinfo.get("kicked_count"), + slowmode_seconds=( + self.xdlib.format.time(chatinfo.get("slowmode_seconds")) + if chatinfo.get("slowmode_seconds") + else self.strings["no"] + ), + call=( + self.strings["yes"] + if chatinfo.get("call") + else self.strings["no"] + ), + ttl_period=( + self.xdlib.format.time(chatinfo.get("ttl_period")) + if chatinfo.get("ttl_period") + else self.strings["no"] + ), + requests_pending=chatinfo.get("requests_pending"), + recent_requesters=", ".join( + [ + f"{user}" + for user in chatinfo.get("recent_requesters") + ] + ) + or self.strings["no"], + linked_chat_id=chatinfo.get("linked_chat_id") + or self.strings["no"], + antispam=( + self.strings["yes"] + if chatinfo.get("antispam") + else self.strings["no"] + ), + participants_hidden=( + self.strings["yes"] + if chatinfo.get("participants_hidden") + else self.strings["no"] + ), + link=chatinfo.get("link") or self.strings["no"], + is_forum=( + self.strings["yes"] + if chatinfo.get("is_forum") + else self.strings["no"] + ), + type_of=( + self.strings["type_group"] + if chatinfo.get("is_group") + else ( + self.strings["type_channel"] + if chatinfo.get("is_channel") + else self.strings["type_unknown"] + ) + ), + ), + file=( + types.InputMediaPhoto( + types.InputPhoto( + photo.id, photo.access_hash, photo.file_reference + ) + ) + if photo + else None + ), + ) + except Exception as e: + logger.error(str(e)) + return await utils.answer(message, self.strings["error"]) + if isinstance(ent, types.User): + try: + userinfo = await self.xdlib.user.get_info(ent) + photo = userinfo.get("profile_photo") + working_hours = ( + userinfo.get("business_work_hours").weekly_open + if userinfo.get("business_work_hours") + else 0 + ) + weekdays = [ + self.strings["monday"], + self.strings["tuesday"], + self.strings["wednesday"], + self.strings["thursday"], + self.strings["friday"], + self.strings["saturday"], + self.strings["sunday"], + ] + personal_channel = userinfo.get("personal_channel") + working_hours_output = [] + if working_hours: + for item in working_hours: + day_index = item.start_minute // (24 * 60) + day = weekdays[day_index] + + start = self.xdlib.parse.minutes_to_hhmm(item.start_minute) + end = self.xdlib.parse.minutes_to_hhmm(item.end_minute) + working_hours_output.append(f"{day}: {start} - {end}") + return await utils.answer( + message, + self.strings["userinfo"].format( + common_chats_count=userinfo.get("common_chats_count") or 0, + phone=userinfo.get("phone") or self.strings["no"], + common_chats=( + ", ".join( + [ + f"{channel.title}" + for channel in userinfo.get("common_chats") + ] + ) + if userinfo.get("common_chats") + else self.strings["no"] + ), + user_id=userinfo.get("id", 0), + first_name=userinfo.get("first_name") or self.strings["no"], + last_name=userinfo.get("last_name") or self.strings["no"], + about=userinfo.get("about") or self.strings["no"], + emoji_status=( + f"🌙" + if userinfo.get("emoji_status") + else self.strings["no"] + ), + business_work_hours=", ".join(working_hours_output) + or self.strings["no"], + birthday=( + f"{userinfo.get('birthday').day or ''}." + f"{userinfo.get('birthday').month or ''}." + f"{userinfo.get('birthday').year or ''}" + if userinfo.get("birthday") + else self.strings["no"] + ), + stargifts_count=userinfo.get("stargifts_count") + or self.strings["no"], + usernames=( + ", ".join( + [ + f"@{username}" + for username in userinfo.get("usernames") + ] + ) + if userinfo.get("usernames") + else self.strings["no"] + ), + personal_channel=( + f"" + f"{personal_channel.title}" + if personal_channel + else self.strings["no"] + ), + ), + file=( + types.InputMediaPhoto( + types.InputPhoto( + photo.id, photo.access_hash, photo.file_reference + ) + ) + if photo + else None + ), + ) + except Exception as e: + logger.error(e) + return await utils.answer(message, self.strings["error"]) + + @loader.command(ru_doc="[-a] [-d] Управлять заявками на вступление") + @loader.tag("no_pm") + async def requests(self, message): + """[-a] [-d] Manage join requests""" + opts = self.xdlib.parse.opts(utils.get_args(message)) + approve_list = [x for x in str(opts.get("a", "")).split(",") if x] + dismiss_list = [x for x in str(opts.get("d", "")).split(",") if x] + sanitized_approve_list = [int(x) if x.isdigit() else x for x in approve_list] + sanitized_dismiss_list = [int(x) if x.isdigit() else x for x in approve_list] + all_list = sanitized_approve_list + sanitized_dismiss_list + all_targets = [ + await self._client.get_entity( + int(ent.strip()) if ent.strip().isdigit() else ent.strip() + ) + for ent in all_list + ] + for approve in approve_list: + if approve.isdigit(): + await self.xdlib.chat.join_request(message.chat, int(approve), True) + else: + await self.xdlib.chat.join_request(message.chat, approve, True) + for dismiss in dismiss_list: + if dismiss.isdigit(): + await self.xdlib.chat.join_request(message.chat, int(dismiss), False) + else: + await self.xdlib.chat.join_request(message.chat, dismiss, False) + return await utils.answer( + message, + self.strings["requests_checked"].format( + entities=", ".join( + ent.first_name + or getattr(ent, "username", None) + or str(getattr(ent, "id", "unknown")) + for ent in all_targets + ) + ), + ) + + @loader.command(ru_doc="Получить все свои чаты/каналы") + async def owns(self, message): + """Get all your chats/channels""" + owns = await self.xdlib.dialog.get_owns(self._client) + return await utils.answer( + message, + self.strings["owns"].format( + num=len(owns), + owns="\n".join( + [ + f" {own.title} [{str(own.id).replace('-100', '')}]" + for own in owns + ] + ), + ), + ) + + @loader.command(ru_doc="[-r] [-u] [-f] - Выдать админку участнику") + @loader.tag("no_pm") + async def promote(self, message): + """[-r] [-u] [-f] - Promote a participant""" + reply = await message.get_reply_message() + opts = self.xdlib.parse.opts(utils.get_args(message)) + user = opts.get("u") or getattr(reply, "sender_id") or None + if not user: + return await utils.answer(message, self.strings["no_user"]) + user = await self._client.get_entity(user) + rank = (opts.get("r")) or "XD Admin" + chat = await message.get_chat() + rights = await self.xdlib.chat.get_rights(message.chat, user) + if ( + not chat.admin_rights + or not getattr(chat.admin_rights, "add_admins") + or ( + getattr(rights.participant, "promoted_by", self.tg_id) != self.tg_id + and not getattr(chat, "creator", False) + ) + ): + return await utils.answer(message, self.strings["no_rights"]) + full = opts.get("f") + if full: + my_rights = [ + r for r, y in chat.admin_rights.to_dict().items() if y and r != "_" + ] + perms = self.xdlib.admin_rights(0) + perms = perms.add(*my_rights) + await self.xdlib.admin.set_rights(chat, user, perms.to_int(), rank) + return await utils.answer( + message, + self.strings["promoted"].format( + id=user.id, + name=user.first_name + if hasattr(user, "first_name") + else user.title + if hasattr(user, "title") + else "None", + rights=self.strings["full_rights"], + ), + ) + mask = ( + self.xdlib.admin_rights.to_mask(rights.participant.admin_rights) + if hasattr(rights.participant, "admin_rights") + else 0 + ) + + await utils.answer( + message, + self.strings["promote"].format( + id=user.id, + name=user.first_name + if hasattr(user, "first_name") + else user.title + if hasattr(user, "title") + else "None", + rank=rank, + ), + reply_markup=await self.build_markup(user.id, chat.id, mask, rank), + ) + + @loader.command(ru_doc="[-t] [-u] - Ограничить участника") + @loader.tag("no_pm") + async def restrict(self, message): + """[-t] [-u] - Restrict a participant""" + reply = await message.get_reply_message() + opts = self.xdlib.parse.opts(utils.get_args(message)) + + user = opts.get("u") or getattr(reply, "sender_id") or None + if not user: + return await utils.answer(message, self.strings["no_user"]) + + user = await self._client.get_entity(user) + chat = await message.get_chat() + + if not chat.admin_rights or not getattr(chat.admin_rights, "ban_users"): + return await utils.answer(message, self.strings["no_rights"]) + duration = opts.get("t", None) + if duration: + duration = self.xdlib.format.time(self.xdlib.parse.time(duration)) + + rights = await self.xdlib.chat.get_rights(chat, user) + mask = ( + self.xdlib.banned_rights.MAX_MASK + - self.xdlib.banned_rights.to_mask(rights.participant.banned_rights) + if hasattr(rights.participant, "banned_rights") + else 0 + ) + rank = "-" + + await utils.answer( + message, + self.strings["restrict"].format( + id=user.id, + name=user.first_name + if hasattr(user, "first_name") + else user.title + if hasattr(user, "title") + else "None", + time=f" {duration}" if duration else self.strings["forever"], + ), + reply_markup=await self.build_markup( + user.id, + chat.id, + mask, + rank, + mode="restrict", + duration=f" {duration}" if duration else None, + ), + ) + + async def build_markup( + self, + user_id: int, + chat_id: int, + mask: int, + rank: str, + duration: typing.Optional[int] = None, + mode="admin", + ): + rights_cls = ( + self.xdlib.admin_rights if mode == "admin" else self.xdlib.banned_rights + ) + rights_names = rights_cls.RIGHTS_LIST + rights = rights_cls(mask) + chat = await self._client.get_entity(chat_id) + + markup = utils.chunks( + [ + { + "text": f"{'🟢' if rights.has_index(idx) else '🔴'} {self.strings[name]}", + "callback": self._toggle_right, + "args": (user_id, chat_id, mask, idx, rank, mode, duration), + } + for idx, name in enumerate(rights_names) + if ( + name != "until_date" + and not ( + getattr(chat.default_banned_rights, name, True) + if mode != "admin" + else False + ) + ) + ], + 2, + ) + + markup.append( + [ + { + "text": self.strings["apply"], + "callback": self._apply_rights, + "args": (user_id, chat_id, mask, rank, mode, duration), + } + ] + ) + + markup.append([{"text": self.strings["close"], "action": "close"}]) + return markup + + async def _toggle_right( + self, + call, + user_id: int, + chat_id: int, + mask: int, + idx: int, + rank: str, + mode: str, + duration: str, + ): + new_mask = mask ^ (1 << idx) + + new_markup = await self.build_markup( + user_id, chat_id, new_mask, rank, mode=mode, duration=duration + ) + + user = await self._client.get_entity(user_id) + + title = self.strings["promote"] if mode == "admin" else self.strings["restrict"] + + await utils.answer( + call, + title.format( + id=user_id, + name=user.first_name + if hasattr(user, "first_name") + else user.title + if hasattr(user, "title") + else "None", + rank=rank, + time=f" {duration}" if duration else self.strings["forever"], + ), + reply_markup=new_markup, + ) + + async def _apply_rights( + self, + call, + user_id: int, + chat_id: int, + mask: int, + rank: str, + mode: str, + duration: typing.Optional[str] = None, + ): + user = await self._client.get_entity(user_id) + chat = await self._client.get_entity(chat_id) + + if mode == "admin": + ok = await self.xdlib.admin.set_rights(chat, user, mask, rank) + rights_items = self.xdlib.admin_rights(mask).to_dict() + else: + ok = await self.xdlib.chat.set_restrictions( + chat, user, mask, duration=duration + ) + rights_items = self.xdlib.banned_rights(mask).to_dict() + + rights_list = [r for r, v in rights_items.items() if v] + + if ok: + text = ( + self.strings["promoted"] + if mode == "admin" and mask + else self.strings["demoted"] + if mode == "admin" and not mask + else self.strings["restricted"] + ) + + await utils.answer( + call, + text.format( + id=user_id, + name=user.first_name + if hasattr(user, "first_name") + else user.title + if hasattr(user, "title") + else "None", + rights=", ".join([self.strings[r] for r in rights_list]) + if rights_list + else self.strings["no"], + duration=f" {duration}" if duration else self.strings["forever"], + time=f" {duration}" if duration else self.strings["forever"], + ), + reply_markup=[[{"text": self.strings["close"], "action": "close"}]], + ) + else: + await utils.answer( + call, + self.strings["error"], + reply_markup=[[{"text": self.strings["close"], "action": "close"}]], + ) \ No newline at end of file diff --git a/coddrago/modules/dbmod.py b/coddrago/modules/dbmod.py new file mode 100644 index 0000000..a8f69c9 --- /dev/null +++ b/coddrago/modules/dbmod.py @@ -0,0 +1,515 @@ +# meta developer: @codrago_m + +import html +from .. import loader, utils + + +class DBMod(loader.Module): + strings = { + "name": "DBMod", + "del_text": "Database\n\nSelect a key to view", + "deleted": "🗑 Key {key} deleted", + "deleted_all": "🗑 Deleted {count} keys", + "close_btn": "❌ Close", + "back_btn": "⬅ Back", + "del_btn": "🗑 Delete", + "del_all_btn": "💣 Delete all", + "not_found": "🔍 Key {key} not found", + "invalid_key": "⚠ Invalid key", + "page": "📄 Page {current}/{total}", + "module_not_found": "🔍 Module '{module}' not found in database", + "confirm_delete": "⚠ Are you sure you want to delete this?", + "view_path": "Path: {path}", + "root_path": "Root", + "value_display": "Value: {value}", + "yes_btn": "✅ Yes", + "no_btn": "❌ No", + "list_item_display": "List item [{index}]", + } + + strings_ru = { + "del_text": "База данных\n\nВыберите ключ для просмотра", + "deleted": "🗑 Ключ {key} удален", + "deleted_all": "🗑 Удалено {count} ключей", + "close_btn": "❌ Закрыть", + "back_btn": "⬅ Назад", + "del_btn": "🗑 Удалить", + "del_all_btn": "💣 Удалить все", + "not_found": "🔍 Ключ {key} не найден", + "invalid_key": "⚠ Некорректный ключ", + "page": "📄 Страница {current}/{total}", + "module_not_found": "🔍 Модуль '{module}' не найден в базе данных", + "confirm_delete": "⚠ Вы уверены, что хотите удалить это?", + "view_path": "Путь: {path}", + "root_path": "Корень", + "value_display": "Значение: {value}", + "yes_btn": "✅ Да", + "no_btn": "❌ Нет", + "list_item_display": "Элемент списка [{index}]", + } + + async def client_ready(self): + self.page_state = {} + + def _make_path_text(self, key_path): + path = "/".join(map(str, key_path)) if key_path else self.strings["root_path"] + return self.strings["view_path"].format(path=path) + + def _make_list_item_path_text(self, key_path, index): + """Создает заголовок для элемента списка""" + if key_path: + path = "/".join(map(str, key_path)) + f"[{index}]" + else: + path = f"[{index}]" + return self.strings["list_item_display"].format(index=index) + + async def show_menu(self, message, key_path=None, page=0): + if key_path is None: + key_path = [] + self.page_state[tuple(key_path)] = page + + current_data = self._db + for key in key_path: + if isinstance(current_data, (dict, list)): + if isinstance(current_data, dict) and key in current_data: + current_data = current_data[key] + elif ( + isinstance(current_data, list) + and isinstance(key, int) + and 0 <= key < len(current_data) + ): + current_data = current_data[key] + else: + await utils.answer(message, self.strings["invalid_key"]) + return + else: + await utils.answer(message, self.strings["invalid_key"]) + return + + header = self._make_path_text(key_path) + + if isinstance(current_data, (dict, list)) and current_data: + markup = self.generate_nested_markup(current_data, key_path, page) + await utils.answer(message, header, reply_markup=markup) + else: + text = f"{header}\n\n" + self.strings["value_display"].format( + value=html.escape(str(current_data)) + ) + markup = self.generate_value_markup(key_path, page) + await utils.answer(message, text, reply_markup=markup) + + async def navigate_db(self, call, key_path=None, page=0): + if key_path is None: + key_path = [] + self.page_state[tuple(key_path)] = page + + current_data = self._db + for key in key_path: + if isinstance(current_data, (dict, list)): + if isinstance(current_data, dict) and key in current_data: + current_data = current_data[key] + elif ( + isinstance(current_data, list) + and isinstance(key, int) + and 0 <= key < len(current_data) + ): + current_data = current_data[key] + else: + await call.answer(self.strings["invalid_key"]) + return + else: + await call.answer(self.strings["invalid_key"]) + return + + is_list_item = False + if key_path: + parent_data = self._db + for key in key_path[:-1]: + if isinstance(parent_data, (dict, list)): + if isinstance(parent_data, dict) and key in parent_data: + parent_data = parent_data[key] + elif ( + isinstance(parent_data, list) + and isinstance(key, int) + and 0 <= key < len(parent_data) + ): + parent_data = parent_data[key] + else: + break + + if ( + isinstance(parent_data, list) + and isinstance(key_path[-1], int) + and 0 <= key_path[-1] < len(parent_data) + ): + is_list_item = True + + if is_list_item: + header = self._make_list_item_path_text(key_path[:-1], key_path[-1]) + text = f"{header}\n\n" + self.strings["value_display"].format( + value=html.escape(str(current_data)) + ) + await call.edit( + text, reply_markup=self.generate_list_item_markup(key_path, page) + ) + elif isinstance(current_data, (dict, list)) and current_data: + header = self._make_path_text(key_path) + await call.edit( + header, + reply_markup=self.generate_nested_markup(current_data, key_path, page), + ) + else: + header = self._make_path_text(key_path) + text = f"{header}\n\n" + self.strings["value_display"].format( + value=html.escape(str(current_data)) + ) + await call.edit( + text, reply_markup=self.generate_value_markup(key_path, page) + ) + + def generate_nested_markup(self, data, key_path, page=0): + if isinstance(data, list) and data: + return self.generate_list_markup(data, key_path, page) + + items = list(data.items()) if isinstance(data, dict) else [] + items_per_page = 9 + total_pages = (len(items) + items_per_page - 1) // items_per_page + start_idx = page * items_per_page + end_idx = min(start_idx + items_per_page, len(items)) + page_items = items[start_idx:end_idx] + + markup = [] + row = [] + for i, (key, value) in enumerate(page_items): + if i % 3 == 0 and row: + markup.append(row) + row = [] + row.append( + { + "text": f"{key}", + "callback": self.navigate_db, + "args": [key_path + [key], 0], + } + ) + if row: + markup.append(row) + + nav_buttons = [] + if key_path: + parent_page = self.page_state.get(tuple(key_path[:-1]), 0) + nav_buttons.append( + { + "text": self.strings["back_btn"], + "callback": self.navigate_db, + "args": [key_path[:-1], parent_page], + } + ) + + if total_pages > 1: + if page > 0: + nav_buttons.append( + { + "text": "◀️", + "callback": self.navigate_db, + "args": [key_path, page - 1], + } + ) + nav_buttons.append( + { + "text": self.strings["page"].format( + current=page + 1, total=total_pages + ), + "callback": self.navigate_db, + "args": [key_path, page], + } + ) + if page < total_pages - 1: + nav_buttons.append( + { + "text": "▶️", + "callback": self.navigate_db, + "args": [key_path, page + 1], + } + ) + if nav_buttons: + markup.append(nav_buttons) + + if key_path: + markup.append( + [ + { + "text": self.strings["del_all_btn"], + "callback": self.confirm_delete_all, + "args": [key_path], + } + ] + ) + + if not key_path: + markup.append([{"text": self.strings["close_btn"], "action": "close"}]) + return markup + + def generate_list_markup(self, data, key_path, page=0): + """Генерирует разметку для списка, показывая элементы напрямую""" + items_per_page = 9 + total_pages = (len(data) + items_per_page - 1) // items_per_page + start_idx = page * items_per_page + end_idx = min(start_idx + items_per_page, len(data)) + page_items = list(enumerate(data[start_idx:end_idx], start_idx)) + + markup = [] + row = [] + for i, (index, value) in enumerate(page_items): + if i % 3 == 0 and row: + markup.append(row) + row = [] + + if isinstance(value, (dict, list)): + btn_text = f"[{index}]" + else: + value_str = str(value) + if len(value_str) > 10: + btn_text = f"{value_str[:10]}..." + else: + btn_text = value_str + + row.append( + { + "text": btn_text, + "callback": self.navigate_db, + "args": [key_path + [index], 0], + } + ) + if row: + markup.append(row) + + nav_buttons = [] + if key_path: + parent_page = self.page_state.get(tuple(key_path[:-1]), 0) + nav_buttons.append( + { + "text": self.strings["back_btn"], + "callback": self.navigate_db, + "args": [key_path[:-1], parent_page], + } + ) + + if total_pages > 1: + if page > 0: + nav_buttons.append( + { + "text": "◀️", + "callback": self.navigate_db, + "args": [key_path, page - 1], + } + ) + nav_buttons.append( + { + "text": self.strings["page"].format( + current=page + 1, total=total_pages + ), + "callback": self.navigate_db, + "args": [key_path, page], + } + ) + if page < total_pages - 1: + nav_buttons.append( + { + "text": "▶️", + "callback": self.navigate_db, + "args": [key_path, page + 1], + } + ) + if nav_buttons: + markup.append(nav_buttons) + + if key_path: + markup.append( + [ + { + "text": self.strings["del_all_btn"], + "callback": self.confirm_delete_all, + "args": [key_path], + } + ] + ) + + return markup + + def generate_list_item_markup(self, key_path, page=0): + """Генерирует разметку для отдельного элемента списка""" + parent_page = self.page_state.get(tuple(key_path[:-1]), 0) + return [ + [ + { + "text": self.strings["del_btn"], + "callback": self.delete_key, + "args": [key_path], + } + ], + [ + { + "text": self.strings["back_btn"], + "callback": self.navigate_db, + "args": [key_path[:-1], parent_page], + } + ], + ] + + def generate_value_markup(self, key_path, page=0): + parent_page = self.page_state.get(tuple(key_path[:-1]), 0) + return [ + [ + { + "text": self.strings["del_btn"], + "callback": self.delete_key, + "args": [key_path], + } + ], + [ + { + "text": self.strings["back_btn"], + "callback": self.navigate_db, + "args": [key_path[:-1], parent_page], + } + ], + ] + + async def confirm_delete_all(self, call, key_path): + await call.edit( + self.strings["confirm_delete"], + reply_markup=[ + [ + { + "text": self.strings["yes_btn"], + "callback": self.delete_all_keys, + "args": [key_path], + } + ], + [ + { + "text": self.strings["no_btn"], + "callback": self.navigate_db, + "args": [ + key_path, + self.page_state.get(tuple(key_path), 0), + ], + } + ], + ], + ) + + async def delete_all_keys(self, call, key_path): + if not key_path: + count = len(self._db) + self._db.clear() + self._db.save() + await call.answer(self.strings["deleted_all"].format(count=count)) + await self.navigate_db(call, [], self.page_state.get((), 0)) + else: + current = self._db + for key in key_path[:-1]: + if isinstance(current, (dict, list)): + if isinstance(current, dict) and key in current: + current = current[key] + elif ( + isinstance(current, list) + and isinstance(key, int) + and 0 <= key < len(current) + ): + current = current[key] + else: + await call.answer( + self.strings["not_found"].format(key=key_path[-1]) + ) + return + + if isinstance(current, (dict, list)) and key_path[-1] in current: + if isinstance(current[key_path[-1]], (dict, list)): + count = len(current[key_path[-1]]) + else: + count = 1 + del current[key_path[-1]] + self._db.save() + await call.answer(self.strings["deleted_all"].format(count=count)) + await self.navigate_db( + call, + key_path[:-1], + self.page_state.get(tuple(key_path[:-1]), 0), + ) + else: + await call.answer(self.strings["not_found"].format(key=key_path[-1])) + + async def delete_key(self, call, key_path): + parent_page = self.page_state.get(tuple(key_path[:-1]), 0) + + if len(key_path) == 1: + if key_path[0] in self._db: + del self._db[key_path[0]] + self._db.save() + await call.answer(self.strings["deleted"].format(key=key_path[0])) + await self.navigate_db(call, [], parent_page) + else: + await call.answer(self.strings["not_found"].format(key=key_path[0])) + else: + current = self._db + for key in key_path[:-1]: + if isinstance(current, (dict, list)): + if isinstance(current, dict) and key in current: + current = current[key] + elif ( + isinstance(current, list) + and isinstance(key, int) + and 0 <= key < len(current) + ): + current = current[key] + else: + await call.answer( + self.strings["not_found"].format(key=key_path[-1]) + ) + return + + if isinstance(current, dict) and key_path[-1] in current: + deleted_value = current[key_path[-1]] + del current[key_path[-1]] + key_display = key_path[-1] + self._db.save() + await call.answer(self.strings["deleted"].format(key=key_display)) + await self.navigate_db(call, key_path[:-1], parent_page) + elif ( + isinstance(current, list) + and isinstance(key_path[-1], int) + and 0 <= key_path[-1] < len(current) + ): + deleted_value = current.pop(key_path[-1]) + key_display = f"[{key_path[-1]}] = {deleted_value}" + self._db.save() + await call.answer(self.strings["deleted"].format(key=key_display)) + await self.navigate_db(call, key_path[:-1], parent_page) + else: + await call.answer(self.strings["not_found"].format(key=key_path[-1])) + + def find_module_key(self, module_name): + module_name_lower = module_name.lower() + for key in self._db.keys(): + if key.lower() == module_name_lower: + return key + return None + + @loader.command(ru_doc="Просмотр базы данных") + async def mydb(self, message): + """Viewing the database""" + args = utils.get_args_raw(message) + if args: + module_key = self.find_module_key(args) + if module_key: + await self.show_menu( + message, [module_key], self.page_state.get((module_key,), 0) + ) + return + else: + await utils.answer( + message, self.strings["module_not_found"].format(module=args) + ) + return + await self.show_menu(message, [], self.page_state.get((), 0)) \ No newline at end of file diff --git a/coddrago/modules/full.txt b/coddrago/modules/full.txt index 9be591f..b970f4c 100644 --- a/coddrago/modules/full.txt +++ b/coddrago/modules/full.txt @@ -18,3 +18,8 @@ promoclaimer passwordgen send lastfm +dbmod +chatmodule +stats +tagwatcher +hardspam \ No newline at end of file diff --git a/coddrago/modules/hardspam.py b/coddrago/modules/hardspam.py new file mode 100644 index 0000000..8387ad0 --- /dev/null +++ b/coddrago/modules/hardspam.py @@ -0,0 +1,62 @@ +# meta developer: @codrago_m + +import asyncio +from .. import loader, utils +from herokutl.tl.types import InputDocument +from herokutl.errors.rpcerrorlist import MediaEmptyError + + +@loader.tds +class HardSpam(loader.Module): + strings = { + "name": "HardSpam", + "spam_help": "Usage sample: {}hspam [-c|--clean] 23 text", + } + strings_ru = { + "spam_help": "Пример использования: {}hspam [-c|--clean] 23 text", + } + + async def send_msgs(self, c, chat_id, text): + msg = await c.send_message(chat_id, text) + return msg.id + + async def send_medias(self, c, chat_id, document, text): + msg = await c.send_file(chat_id, document, caption=None if text is None else text) + return msg.id + + @loader.command( + ru_doc="[-c|--clean] n text - Отправить n кол-во сообщений одновременно" + ) + async def hspamcmd(self, message): + """[-c|--clean] n text - Send n number of messages at the same time""" + args = utils.get_args(message) + r = await message.get_reply_message() + delete_all = False + + if "--clean" in args: + delete_all = True + args.remove("--clean") + elif "-c" in args: + delete_all = True + args.remove("-c") + if not args[0].isdigit() or len(args) < 1: + return await utils.answer( + message, + self.strings["spam_help"].format(self.get_prefix()), + ) + + number = int(args[0]) + text = " ".join(args[1:]) + if r and r.media: + document = InputDocument(id=r.media.document.id, access_hash=r.media.document.access_hash, file_reference=r.media.document.file_reference) + tasks = [ + self.send_medias(self._client, message.chat_id, document, None if text is None else text) for i in range(number) + ] + else: + tasks = [ + self.send_msgs(self._client, message.chat_id, text) for _ in range(number) + ] + message_ids = await asyncio.gather(*tasks) + if delete_all: + await self._client.delete_messages(message.chat_id, message_ids) + return await message.delete() \ No newline at end of file diff --git a/coddrago/modules/libs/xdlib.py b/coddrago/modules/libs/xdlib.py new file mode 100644 index 0000000..6ede7ca --- /dev/null +++ b/coddrago/modules/libs/xdlib.py @@ -0,0 +1,764 @@ +# This file is part of XDesai Mods. +# I made this library to share various utility functions across my modules. +# You can use this library in your own modules as well. + +# P.S this library is still under development and may receive updates in the future. + +# meta developer: @codrago_m + +import logging +import re +import typing + +from telethon.errors.rpcerrorlist import ( + UserNotParticipantError, + HideRequesterMissingError, +) +from telethon.functions import messages, channels +from telethon import types + +from .. import loader, utils +from ..types import SelfUnload + +logger = logging.getLogger("XDLib") + + +class XDLib(loader.Library): + """A library with various utility functions for codrago modules.""" + + developer = "@codrago_m" + + strings = { + "name": "XDLib", + "desc": "A library with various utility functions for codrago modules.", + "request_join_reason": "Stay tuned for updates.", + } + + async def init(self): + self.format = FormatUtils() + self.parse = ParseUtils() + self.messages = MessageUtils(self._client) + self.admin = AdminUtils(self._client, self) + self.chat = ChatUtils(self._client, self._db) + self.dialog = DialogUtils(self._client) + self.user = UserUtils(self._client, self._db) + self.admin_rights = AdminRights + self.banned_rights = BannedRights + + def unload_lib(self, name: str): + instance = self.lookup(name) + if isinstance(instance, loader.Library): + self.allmodules.libraries.remove(instance) + logger.info(f"Unloaded library: {name}") + return True + return False + + +class UserUtils: + def __init__(self, client, db): + self._client = client + self._db = db + + async def get_info( + self, user_id: typing.Union[str, int, types.PeerUser, types.User] + ): + userfull = await self._client.get_fulluser(user_id) + full_user = userfull.full_user + user = userfull.users[0] + usernames = user.usernames or [user] or None + unames = [] + if usernames: + for username in usernames: + unames.append(username.username) + personal_channel = ( + await self._client.get_entity(full_user.personal_channel_id) + if full_user.personal_channel_id + else None + ) + common = await self._client( + messages.GetCommonChatsRequest(user_id=user_id, max_id=0, limit=100) + ) + + return { + "common_chats_count": full_user.common_chats_count, + "common_chats": common.chats, + "id": user.id, + "personal_photo": full_user.personal_photo, + "business_work_hours": full_user.business_work_hours, + "business_intro": full_user.business_intro, + "birthday": full_user.birthday, + "personal_channel": personal_channel or None, + "stargifts_count": full_user.stargifts_count, + "first_name": user.first_name, + "last_name": user.last_name, + "usernames": unames, + "emoji_status": getattr(user.emoji_status, "document_id", None), + "color": user.color, + "blocked": full_user.blocked, + "about": full_user.about, + "profile_photo": full_user.profile_photo, + "phone": user.phone, + } + + +class ParseUtils: + def minutes_to_hhmm(self, m): + h = (m // 60) % 24 + mm = m % 60 + return f"{h:02d}:{mm:02d}" + + def opts(self, args: list) -> typing.Dict[str, typing.Any]: + """ + Parses command-line style options from a list of arguments. + Supports sequential operations (+, -, *, /) for numeric values. + """ + options = {} + i = 0 + + def auto_cast(value: str): + if not value: + return True + low = value.lower() + if low in {"true", "yes", "on"}: + return True + if low in {"false", "no", "off"}: + return False + if re.fullmatch(r"-?\d+", value): + return int(value) + if re.fullmatch(r"-?\d+\.\d+", value): + return float(value) + return value + + def apply_operations(base, ops: list[str]): + val = base + for op_str in ops: + m = re.fullmatch(r"([+*/])(\d+(\.\d+)?)", op_str) + if not m: + val = auto_cast(op_str) + continue + op, number, _ = m.groups() + number = float(number) if "." in number else int(number) + if op == "+": + val += number + elif op == "*": + val *= number + elif op == "/": + val /= number + return val + + while i < len(args): + arg = args[i] + + if "=" in arg: + key, value = arg.split("=", 1) + key = key.lstrip("-") + options[key] = auto_cast(value.strip("\"'")) + + elif arg.startswith("-"): + key = arg.lstrip("-") + values = [] + i += 1 + while i < len(args) and not args[i].startswith("-"): + values.append(args[i].strip("\"'")) + i += 1 + i -= 1 + + if key in options and isinstance(options[key], (int, float)): + options[key] = apply_operations(options[key], values) + else: + if values: + base = auto_cast(values[0]) + options[key] = apply_operations(base, values[1:]) + else: + options[key] = True + + i += 1 + + return options + + def bool(self, value: str) -> bool: + """Parses a string into a boolean value.""" + true_values = {"true", "yes", "1", "on"} + false_values = {"false", "no", "0", "off"} + low_value = value.lower() + if low_value in true_values: + return True + elif low_value in false_values: + return False + else: + raise ValueError(f"Cannot parse boolean from '{value}'") + + def time(self, time_str: str) -> int: + """Parses a time duration string into seconds.""" + time_units = { + "s": 1, + "m": 60, + "h": 3600, + "d": 86400, + "w": 604800, + "y": 31536000, + } + total_seconds = 0 + pattern = r"(\d+)([smhdwy])" + matches = re.findall(pattern, time_str) + for value, unit in matches: + total_seconds += int(value) * time_units[unit] + return total_seconds + + def size(self, size_str: str) -> int: + """Parses a size string into bytes.""" + size_units = { + "b": 1, + "kb": 1024, + "mb": 1024**2, + "gb": 1024**3, + "tb": 1024**4, + } + pattern = r"(\d+)([bkmgt]b?)" + match = re.match(pattern, size_str.lower()) + if match: + value, unit = match.groups() + return int(value) * size_units[unit] + return 0 + + def mentions(self, msg) -> typing.List[str]: + """Extracts mentions from a given message.""" + if msg.entities: + mentions = [] + for entity in msg.entities: + if isinstance(entity, types.MessageEntityMention): + offset = entity.offset + length = entity.length + mentions.append(msg.message[offset : offset + length]) + elif isinstance(entity, types.MessageEntityMentionName): + mentions.append(entity.user_id) + return mentions + return [] + + def urls(self, msg) -> typing.List[str]: + """Extracts URLs from a given message.""" + if msg.entities or msg.media: + urls = [] + for entity in msg.entities: + if isinstance(entity, types.MessageEntityTextUrl): + urls.append(entity.url) + elif isinstance(entity, types.MessageEntityUrl): + offset = entity.offset + length = entity.length + urls.append(msg.message[offset : offset + length]) + elif msg.media and hasattr(msg.media, "webpage"): + if msg.media.webpage.url: + urls.append(msg.media.webpage.url) + return urls + return [] + + +class DialogUtils: + def __init__(self, client) -> None: + self._client = client + + async def get_all(self, client): + dialogs = [] + async for dialog in client.iter_dialogs(): + dialogs.append(dialog) + return dialogs + + async def get_chats(self, client): + return [ + chat + for chat in await self.get_all(client) + if chat.is_group and chat.is_channel + ] + + async def get_pms(self, client): + return [pm for pm in await self.get_all(client) if pm.is_private] + + async def get_channels(self, client): + return [ + channel + for channel in await self.get_all(client) + if channel.is_channel and not channel.is_group + ] + + async def get_owns(self, client): + return [ + ent + for ent in await self.get_all(client) + if hasattr(ent.entity, "creator") and ent.entity.creator + ] + + +class MessageUtils: + def __init__(self, client): + self._client = client + + async def delete_messages(self, msg): + """Deletes multiple messages based on a specific pattern.""" + reply = await msg.get_reply_message() + pattern = r"([ab])(\d+)" + matches = re.findall(pattern, utils.get_args_raw(msg)) + + ids_to_delete = [msg.id] + if reply: + ids_to_delete.append(reply.id) + + for direction, count_str in matches: + count = int(count_str) + if direction == "a": # after + if reply: + async for m in self._client.iter_messages( + msg.chat_id, min_id=reply.id, limit=count, reverse=True + ): + ids_to_delete.append(m.id) + elif direction == "b": # before + async for m in self._client.iter_messages( + msg.chat_id, max_id=(reply if reply else msg).id, limit=count + ): + ids_to_delete.append(m.id) + + await self._client.delete_messages(msg.chat_id, message_ids=ids_to_delete) + + async def get_sender(self, message): + if message.out: + return await self._client.get_me() + if message.is_private: + return message.peer_id + if message.is_group and message.is_channel: + return message.sender or message.chat + + +class ChatUtils: + def __init__(self, client, db) -> None: + self._client = client + self._db = db + + async def set_restrictions( + self, chat, user, mask: int, duration: int = None + ) -> bool: + """ + Sets chat restrictions (mute/ban permissions) for a user based on a mask. + + :param chat: Chat entity + :param user: User entity + :param mask: Bitmask of BannedRights + :param duration: Ban duration in seconds (None = forever) + """ + + try: + rights = BannedRights(mask) + + rights_dict = rights.to_dict() + + rights_dict["until_date"] = ( + None if duration is None else utils.timestamp() + duration + ) + + new_banned_rights = types.ChatBannedRights(**rights_dict) + + await self._client( + channels.EditBannedRequest( + channel=chat, + participant=user, + banned_rights=new_banned_rights, + ) + ) + + return True + + except Exception: + logger.error( + f"Failed to set restrictions with mask {mask} for user {user.id} in chat {chat}", + exc_info=True, + ) + return False + + async def get_admin_logs(self, chat, limit: int = 5, **kwargs): + logs = [] + for log_event in await self._client.get_admin_log(chat, limit=limit, **kwargs): + logs.append(log_event) + return logs + + async def get_user_messages(self, chat, user_id): + msgs = [] + async for msg in self._client.iter_messages(chat, from_user=user_id): + msgs.append(msg) + return msgs + + async def join_request(self, chat, user_id, approved): + try: + await self._client( + messages.HideChatJoinRequestRequest( + peer=chat, user_id=user_id, approved=approved + ) + ) + except HideRequesterMissingError: + logger.error("Request not found") + + async def join_requests(self, chat, approved): + try: + await self._client( + messages.HideAllChatJoinRequestsRequest( + peer=chat, + approved=approved, + ) + ) + except HideRequesterMissingError: + logger.error("Request not found") + + async def get_members(self, chat): + try: + members = await self._client.get_participants(chat) + if members: + return members + return None + except Exception: + logger.error(f"Couldn't get members of the chat {chat}") + return None + + async def get_deleted(self, chat): + try: + members = await self._client.get_participants(chat) + deleted = [member for member in members if getattr(member, "deleted")] + if deleted: + return deleted + return None + except Exception: + logger.error(f"Couldn't get members of the chat {chat}") + return None + + async def get_bots(self, chat): + try: + bots = await self._client.get_participants( + chat, filter=types.ChannelParticipantsBots() + ) + if bots: + return bots + return None + except Exception: + logger.error(f"Couldn't get bots from the chat {chat}") + return None + + async def get_admins(self, chat, only_users: bool = False): + try: + admins = await self._client.get_participants( + chat, filter=types.ChannelParticipantsAdmins() + ) + users = [ + user + for user in admins + if user + and not getattr(user, "bot") + and not isinstance( + getattr(user, "participant"), types.ChannelParticipantCreator + ) + ] + if only_users: + return users + return admins + except Exception: + logger.error(f"Couldn't get admins from the chat {chat}") + return None + + async def get_creator(self, chat): + try: + admins = await self._client.get_participants( + chat, filter=types.ChannelParticipantsAdmins() + ) + if not admins: + return None + for admin in admins: + if hasattr(admin, "participant") and isinstance( + getattr(admin, "participant"), types.ChannelParticipantCreator + ): + return admin + return None + except Exception: + logger.error(f"Couldn't get the creator from the chat {chat}") + return None + + async def is_member(self, chat, user) -> bool: + """Checks if a user is a member of a chat.""" + try: + perms = await self._client.get_perms_cached(chat, user) + return True if perms else False + except UserNotParticipantError: + return False + except Exception: + logger.error( + f"Failed to check membership for user {user} in chat {chat.title}", + exc_info=True, + ) + return False + + async def get_rights(self, chat, user): + """Checks if a user is a member of a chat.""" + try: + perms = await self._client.get_perms_cached(chat, user) + return perms + except UserNotParticipantError: + return None + except Exception: + logger.error( + f"Failed to check membership for user {user} in chat {chat.title}", + exc_info=True, + ) + return None + + async def invite_user(self, chat, user): + """Invites a user to a chat.""" + try: + await self._client( + channels.InviteToChannelRequest(channel=chat, users=[user]) + ) + return True + except Exception: + logger.error( + f"Failed to invite user {user} to chat {chat.title}", exc_info=True + ) + return False + + async def get_info(self, chat) -> dict: + try: + chat_full = await self._client.get_fullchannel(chat) + full_chat = chat_full.full_chat + chat = chat_full.chats[0] + return { + "id": full_chat.id or 0, + "about": full_chat.about or "", + "chat_photo": full_chat.chat_photo, + "admins_count": full_chat.admins_count or 0, + "online_count": full_chat.online_count or 0, + "participants_count": full_chat.participants_count or 0, + "kicked_count": full_chat.kicked_count, + "slowmode_seconds": full_chat.slowmode_seconds or 0, + "call": full_chat.call or None, + "title": chat.title or "", + "ttl_period": full_chat.ttl_period or 0, + "available_reactions": full_chat.available_reactions or None, + "requests_pending": full_chat.requests_pending or 0, + "recent_requesters": full_chat.recent_requesters or [], + "is_forum": getattr(chat, "forum"), + "linked_chat_id": full_chat.linked_chat_id or 0, + "antispam": full_chat.antispam or False, + "participants_hidden": full_chat.participants_hidden or False, + "link": ( + f"https://t.me/{chat.username}" + if chat.username + else ( + full_chat.exported_invite.link + if full_chat.exported_invite + else "" + ) + ), + "is_channel": chat.broadcast or False, + "is_group": chat.megagroup or False, + } + except Exception: + logger.error("Failed to get the chat info") + return {} + + async def invite_bot(self, client, chat) -> bool: + """Invites an inline bot to a chat.""" + try: + await self._client( + channels.InviteToChannelRequest( + chat, + [client.loader.inline.bot_username or client.loader.inline.bot_id], + ) + ) + except Exception: + logger.error("Failed to invite inline bot to chat", exc_info=True) + return False + + rights = AdminRights.all() + rights.remove("anonymous") + admin = AdminUtils(self._client, self._db) + await admin.set_rights( + chat, + client.loader.inline.bot_username or client.loader.inline.bot_id, + rights.to_int(), + rank="XD Bot", + ) + return True + + +class AdminUtils: + def __init__(self, client, lib) -> None: + self._client = client + self._lib = lib + + async def set_role(self, chat, user, role_name, rank="XD Admin") -> bool: + rights_obj = self._lib.roles.get_role_perms(role_name) + if rights_obj is None: + return False + + return await self.set_rights(chat, user, rights_obj.to_int(), rank) + + async def set_rights(self, chat, user, mask: int, rank: str = "XD Admin") -> bool: + """Sets admin rights for a user in a chat based on a mask.""" + try: + rights = AdminRights(mask) + + new_admin_rights = rights.to_chat_rights() + + await self._client( + channels.EditAdminRequest( + chat, + user, + new_admin_rights, + rank=rank, + ) + ) + return True + except Exception: + logger.error( + f"Failed to set rights with mask {mask} for user {user.id} in chat {chat.title}", + exc_info=True, + ) + return False + + +class FormatUtils: + def bytes(self, size: int) -> str: + """Formats a size in bytes into a human-readable string.""" + if size < 1024: + if size == 1: + return f"{size} byte" + return f"{size} bytes" + elif size < 1024**2: + return f"{size / 1024:.2f} KB" + elif size < 1024**3: + return f"{size / 1024**2:.2f} MB" + elif size < 1024**4: + return f"{size / 1024**3:.2f} GB" + else: + return f"{size / 1024**4:.2f} TB" + + def time(self, seconds: int) -> str: + """Formats a time duration in seconds into a human-readable string.""" + intervals = ( + ("years", 31536000), + ("months", 2592000), + ("weeks", 604800), + ("days", 86400), + ("hours", 3600), + ("minutes", 60), + ("seconds", 1), + ) + result = [] + for name, count in intervals: + value = seconds // count + if value: + seconds -= value * count + if value == 1: + name = name.rstrip("s") + result.append(f"{value} {name}") + return ", ".join(result) if result else "0 seconds" + + +class Rights: + RIGHTS_LIST: typing.List = [] + + def __init__(self, mask: int = 0): + self.mask = mask & self.MAX_MASK + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls.RIGHTS = {name: 1 << i for i, name in enumerate(cls.RIGHTS_LIST)} + cls.MAX_MASK = (1 << len(cls.RIGHTS_LIST)) - 1 + + def add(self, *right_names: str) -> None: + for name in right_names: + if name in self.RIGHTS: + self.mask |= self.RIGHTS[name] + else: + return None + return self + + def remove(self, *right_names: str) -> None: + for name in right_names: + if name in self.RIGHTS: + self.mask &= ~self.RIGHTS[name] + else: + return None + return self + + def has(self, right_name: str) -> bool: + return bool(self.mask & self.RIGHTS.get(right_name, 0)) + + def add_index(self, idx: int) -> None: + if 0 <= idx < len(self.RIGHTS_LIST): + self.mask |= 1 << idx + return self + + @classmethod + def to_mask(self, chat_rights): + mask = 0 + for right, rmask in self.RIGHTS.items(): + if ( + getattr(chat_rights, right) + and isinstance(chat_rights, types.ChatAdminRights) + ) or ( + not getattr(chat_rights, right) + and isinstance(chat_rights, types.ChatBannedRights) + ): + mask |= rmask + return mask + + def remove_index(self, idx: int) -> None: + if 0 <= idx < len(self.RIGHTS_LIST): + self.mask &= ~(1 << idx) + return self + + def has_index(self, idx: int) -> bool: + if 0 <= idx < len(self.RIGHTS_LIST): + return bool(self.mask & (1 << idx)) + return False + + def to_dict(self) -> dict[str, bool]: + return {name: self.has(name) for name in self.RIGHTS_LIST} + + def to_int(self) -> int: + return self.mask + + def to_chat_rights(self): + return ( + types.ChatBannedRights(**self.to_dict()) + if self.__class__.__name__ == "BannedRights" + else types.ChatAdminRights(**self.to_dict()) + ) + + @classmethod + def stringify(cls) -> str: + max_len = max(len(name) for name in cls.RIGHTS_LIST) + lines = [] + for name in cls.RIGHTS_LIST: + mask = cls.RIGHTS[name] + lines.append(f"{name.ljust(max_len)} — {mask}") + return "\n".join(lines) + + @classmethod + def list_rights(cls) -> list[tuple[int, str]]: + return [(i, name) for i, name in enumerate(cls.RIGHTS_LIST)] + + @classmethod + def from_int(cls, mask: int): + return cls(mask) + + @classmethod + def all(cls): + return cls(cls.MAX_MASK) + + @classmethod + def none(cls): + return cls(0) + + +class BannedRights(Rights): + RIGHTS_LIST = [ + x for x in types.ChatBannedRights(until_date=None).to_dict().keys() if x != "_" + ] + + +class AdminRights(Rights): + RIGHTS_LIST = [x for x in types.ChatAdminRights().to_dict().keys() if x != "_"] \ No newline at end of file diff --git a/coddrago/modules/stats.py b/coddrago/modules/stats.py new file mode 100644 index 0000000..0b8ae13 --- /dev/null +++ b/coddrago/modules/stats.py @@ -0,0 +1,120 @@ +# meta developer: @codrago_m + +from .. import loader, utils +from telethon.tl.functions.contacts import GetBlockedRequest + + +@loader.tds +class Stats(loader.Module): + """Показывает статистику твоего аккаунта""" + + strings = { + "name": "Stats", + "stats": """ + Account Statistics + +💜 Total chats: {all_chats} + +👤 Private chats: {users} +🤖 Bots: {bots} +👥 Groups: {groups} +👥 Channels: {channels} +📨 Archived chats: {archived} + Total blocked: {blocked} + Ͱ👤 Users: {blocked_users} + Ͱ🤖 Bots: {blocked_bots}""", + "loading_stats": "🫥 Loading statistics...", + } + + strings_ru = { + "name": "Stats", + "stats": """ + Статистика аккаунта + +💜 Всего чатов: {all_chats} + +👤 Личных чатов: {users} +🤖 Ботов: {bots} +👥 Групп: {groups} +👥 Каналов: {channels} +📨 Архивированных чатов: {archived} + Всего заблокированных: {blocked} + Ͱ👤 Пользователи: {blocked_users} + Ͱ🤖 Боты: {blocked_bots}""", + "loading_stats": "🫥 Загрузка статистики...", + } + + async def client_ready(self, client, db): + self.db = db + self._client = client + + @loader.command() + async def stats(self, message): + """Получить статистику""" + await utils.answer(message, self.strings["loading_stats"]) + users = 0 + bots = 0 + groups = 0 + channels = 0 + all_chats = 0 + archived = 0 + blocked_bots = 0 + blocked_users = 0 + + limit = 100 + offset = 0 + total_blocked = 0 + while True: + blocked_chats = await self._client( + GetBlockedRequest(offset=offset, limit=limit) + ) + for user in blocked_chats.users: + if user.bot: + blocked_bots += 1 + else: + blocked_users += 1 + blocked = len(blocked_chats.users) + total_blocked += blocked + + if blocked < limit: + break + + offset += limit + + async for dialog in self._client.iter_dialogs(): + if getattr(dialog, "archived", False): + archived += 1 + if dialog.is_user: + if getattr(dialog.entity, "bot", False): + bots += 1 + all_chats += 1 + else: + users += 1 + all_chats += 1 + elif getattr(dialog, "is_group", False): + groups += 1 + all_chats += 1 + elif dialog.is_channel: + if getattr(dialog.entity, "megagroup", False) or getattr( + dialog.entity, "gigagroup", False + ): + groups += 1 + all_chats += 1 + elif getattr(dialog.entity, "broadcast", False): + channels += 1 + all_chats += 1 + + await utils.answer( + message, + self.strings["stats"].format( + users=users, + bots=bots, + channels=channels, + groups=groups, + all_chats=all_chats, + blocked=total_blocked, + archived=archived, + blocked_users=blocked_users, + blocked_bots=blocked_bots, + ), + ) \ No newline at end of file diff --git a/coddrago/modules/tagwatcher.py b/coddrago/modules/tagwatcher.py new file mode 100644 index 0000000..cd0b5ac --- /dev/null +++ b/coddrago/modules/tagwatcher.py @@ -0,0 +1,245 @@ +# meta developer: @codrago_m + +import logging +from .. import utils, loader, main +from telethon.tl.functions.messages import MarkDialogUnreadRequest + +logger = logging.getLogger("TagWatcher") + + +@loader.tds +class TagWatcher(loader.Module): + """Informs when you are tagged in chats and automatically reads pm.""" + + strings = { + "name": "TagWatcher", + "_cfg_doc_blacklist_chats": "List of chat IDs to ignore notifications from.", + "_cfg_doc_blacklist_users": "List of user IDs to ignore notifications from.", + "_cfg_doc_enabled": "Enable/Disable the module.", + "_cfg_doc_ignore_bots": "Ignore messages from bots.", + "_cfg_doc_pm_autoread": "Automatically mark private messages as read when you receive a message.", + "_cfg_doc_ignore_chats": "List of chat IDs to ignore tags from.", + "_cfg_doc_ignore_users": "List of user IDs to ignore tags from.", + "_cfg_doc_pm_mark_unread": "Mark the PM as unread after automatically reading it.", + "_cfg_doc_custom_notif_text": "Custom notification text. Available variables: {title}, {chat_id}, {name}, {user_id}, {msg_content}, {reply_content}, {link}.", + "enabled": " TagWatcher is enabled.", + "disabled": " TagWatcher is disabled.", + "mentioned": "You were mentioned in {title} [ {chat_id} ] by {name} [ {user_id} ]:\nReplying to message:\n{reply_content}\nMessage content: {msg_content}\n\nGo to message", + "reply_content": "Reply content: {reply_content}", + "no_message_content": "❓ Empty message text", + "msg_link_btn": "Go to message", + "first_msg": "This is the channel where you will receive notifications when someone mentions you in chats.\n\nYou can disable notifications using the {prefix}tagwatcher ({prefix}tw) command.", + "request_join_reason": "Stay tuned for updates.", + } + strings_ru = { + "_cls_doc": "Сообщает когда вас отмечают в чатах.", + "_cfg_doc_blacklist_chats": "Список ID чатов, от которых уведомления не будут приходить.", + "_cfg_doc_blacklist_users": "Список ID пользователей, от которых уведомления не будут приходить.", + "_cfg_doc_enabled": "Включить/Выключить модуль.", + "_cfg_doc_ignore_bots": "Игнорировать сообщения от ботов.", + "_cfg_doc_pm_autoread": "Автоматически отмечать личные сообщения как прочтённые при получении сообщения.", + "_cfg_doc_ignore_chats": "Список ID чатов, от которых не будут срабатывать упоминания.", + "_cfg_doc_ignore_users": "Список ID пользователей, от которых не будут срабатывать упоминания.", + "_cfg_doc_pm_mark_unread": "Помечать ЛС как непрочитанные после автоматического прочтения.", + "_cfg_doc_custom_notif_text": "Пользовательский текст уведомления. Доступные переменные: {title}, {chat_id}, {name}, {user_id}, {msg_content}, {reply_content}, {link}.", + "enabled": " TagWatcher включен.", + "disabled": " TagWatcher выключен.", + "mentioned": "Вас отметил(а) {name} [ {user_id} ] в {title} [ {chat_id} ]:\nВ ответ на сообщение:\n{reply_content}\nТекст сообщения: {msg_content}\n\nПерейти к сообщению", + "reply_content": "Ответ на сообщение: {reply_content}", + "no_message_content": "❓ Пустой текст сообщения", + "msg_link_btn": "Перейти к сообщению", + "first_msg": "Это канал, в который вы будете получать уведомления, когда кто-то упомянет вас в чатах.\n\nВы можете отключить уведомления с помощью команды {prefix}tagwatcher ({prefix}tw).", + "request_join_reason": "Следите за обновлениями модулей.", + } + + def __init__(self) -> None: + self.config = loader.ModuleConfig( + loader.ConfigValue( + "custom_notif_text", + None, + doc=lambda: self.strings["_cfg_doc_custom_notif_text"], + validator=loader.validators.Union( + loader.validators.String(), loader.validators.NoneType() + ), + ), + loader.ConfigValue( + "ignore_bots", + True, + doc=lambda: self.strings["_cfg_doc_ignore_bots"], + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "ignore_chats", + [], + doc=lambda: self.strings["_cfg_doc_ignore_chats"], + validator=loader.validators.Series( + validator=loader.validators.TelegramID() + ), + ), + loader.ConfigValue( + "blacklist_chats", + [], + doc=lambda: self.strings["_cfg_doc_blacklist_chats"], + validator=loader.validators.Series( + validator=loader.validators.TelegramID() + ), + ), + loader.ConfigValue( + "ignore_users", + [], + doc=lambda: self.strings["_cfg_doc_ignore_users"], + validator=loader.validators.Series( + validator=loader.validators.TelegramID() + ), + ), + loader.ConfigValue( + "blacklist_users", + [], + doc=lambda: self.strings["_cfg_doc_blacklist_users"], + validator=loader.validators.Series( + validator=loader.validators.TelegramID() + ), + ), + loader.ConfigValue( + "pm_autoread", + False, + doc=lambda: self.strings["_cfg_doc_pm_autoread"], + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "pm_mark_unread", + False, + doc=lambda: self.strings["_cfg_doc_pm_mark_unread"], + validator=loader.validators.Boolean(), + ), + ) + + async def client_ready(self): + await self.request_join("@xdesai_modules", self.strings["request_join_reason"]) + self.xdlib = await self.import_lib( + "https://raw.githubusercontent.com/xdesai96/modules/refs/heads/main/libs/xdlib.py", + suspend_on_error=True, + ) + + self.asset_channel = self._db.get("legacy.forums", "channel_id", 0) + self._notif_topic = await utils.asset_forum_topic( + self._client, + self._db, + self.asset_channel, + "TagWatcher", + description="Here will be notifications about mentions in chats.", + icon_emoji_id=5409025823388741707, + ) + + async def render_text(self, m): + if self.config["custom_notif_text"]: + text = self.config["custom_notif_text"] + else: + text = self.strings["mentioned"] + chat = await m.get_chat() + sender = await self.xdlib.messages.get_sender(m) + title = ( + utils.escape_html(chat.title) + if hasattr(chat, "title") + else utils.escape_html( + sender.first_name if hasattr(sender, "first_name") else sender.title + ) + ) + name = ( + utils.escape_html( + sender.first_name if hasattr(sender, "first_name") else sender.title + ) + if sender + else "Unknown" + ) + msg_content = ( + utils.escape_html(m.message) + if m.message + else self.strings["no_message_content"] + ) + id = sender.id if sender else 0 + reply_content = "" + if m.is_reply: + reply = await m.get_reply_message() + if reply: + reply_content = ( + utils.escape_html(reply.message) + if reply.message + else self.strings["no_message_content"] + ) + return text.format( + title=title, + name=name, + chat_id=chat.id, + user_id=id, + msg_content=msg_content, + reply_content=reply_content, + link=await m.link, + ) + + @loader.command( + ru_doc="Вкл/выкл TagWatcher.", + alias="tw", + ) + async def tagwatcher(self, m): + """Enable/Disable TagWatcher.""" + try: + disabled = self._db.pointer(main.__name__, "disabled_watchers", {}) + if self.strings["name"] in list(disabled.keys()): + del disabled[self.strings["name"]] + await utils.answer(m, self.strings["enabled"]) + else: + disabled[self.strings["name"]] = ["*"] + await utils.answer(m, self.strings["disabled"]) + except Exception as e: + logger.error(e) + + @loader.watcher("only_pm") + async def pm_reader(self, m): + """To automatically mark private messages as read.""" + if self.config["pm_autoread"]: + chat = await m.get_chat() + if chat.id in self.config["ignore_users"] or chat.bot: + return + try: + await self._client.send_read_acknowledge( + chat.id, m, clear_mentions=True + ) + if self.config["pm_mark_unread"]: + peer = await self._client.get_input_entity(chat.id) + await self._client( + MarkDialogUnreadRequest(peer, True if not m.out else False) + ) + except Exception as e: + logger.error(e) + + @loader.watcher("mention", "no_pm") + async def inform(self, m): + """To inform when you are mentioned in chats.""" + try: + sender = await utils.get_user(m) + if ( + utils.get_chat_id(m) in self.config["ignore_chats"] + or sender.id in self.config["ignore_users"] + ): + return + await self._client.send_read_acknowledge(m.chat_id, m, clear_mentions=True) + if ( + not sender + or utils.get_chat_id(m) in self.config["blacklist_chats"] + or utils.get_chat_id(m) == self._notif_topic.id + or sender.id in self.config["blacklist_users"] + ): + return + if self.config["ignore_bots"]: + if hasattr(sender, "bot"): + if sender.bot: + return + await self.inline.bot.send_message( + int(f"-100{self.asset_channel}"), + await self.render_text(m), + disable_web_page_preview=True, + message_thread_id=self._notif_topic.id, + ) + except Exception as e: + logger.error(e) \ No newline at end of file diff --git a/coddrago/modules/translations/chatmodule.yml b/coddrago/modules/translations/chatmodule.yml new file mode 100644 index 0000000..0c2808f --- /dev/null +++ b/coddrago/modules/translations/chatmodule.yml @@ -0,0 +1,197 @@ +en: + my_id: "👑 My ID: {id}" + chat_id: "💬 Chat ID: {id}" + user_id: "👤 User's ID: {id}" + user_not_participant: " User is not in this group." + admin_rights: "
📜 {name} Rights in this chat:\n\n{rights}\n\n🔼 Promoted by: {promoter_name} [{promoter_id}]
" + not_an_admin: " {user} is not an admin." + no_rights: " I don't have enough rights :(" + no_user: " User not found." + change_info: "Change Info" + delete_messages: "Delete messages" + other: "Other" + ban_users: "Ban users" + invite_users: "Invite Users" + pin_messages: "Pin Messages" + add_admins: "Add Admins" + manage_call: "Manage Call" + post_stories: "Post Stories" + edit_stories: "Edit Stories" + delete_stories: "Delete Stories" + anonymous: "Anonymous" + manage_topics: "Manage Topics" + post_messages: "Post messages" + edit_messages: "Edit messages" + until_date: "Until: {until_date}" + view_messages: "View messages" + send_messages: "Send messages" + send_media: "Send media" + send_stickers: "Send stickers" + send_gifs: "Send GIFs" + send_games: "Send games" + send_inline: "Use inline bots" + embed_links: "Embed links" + send_polls: "Send polls" + send_photos: "Send photos" + send_videos: "Send videos" + send_roundvideos: "Send round videos" + send_audios: "Send audio" + send_voices: "Send voice messages" + send_docs: "Send documents" + send_plain: "Send plain text" + invalid_args: " Invalid args." + error: "😖 Something went wrong. Check the logs." + successful_delete: " Entity successfully deleted" + no_deleted_accounts: "😶‍🌫️ No deleted accounts found here" + kicked_deleted_accounts: "🗑 Removed deleted accounts from the chat" + admins_in_chat: "👑 Admins in {title} ({count}):\n" + no_admins_in_chat: "✖️ No admins in this chat." + bot_list: "
🤖 Bots ({count}):\n{bots}
" + no_bots_in_chat: "✖️ No bots in this chat." + user_list: "
👤 Users ({count}):\n{users}
" + no_user_in_chat: "✖️ No users in this chat." + user_is_banned: "🚫 {name} [{id}] has been banned for{time_info}." + user_is_unbanned: "👋 {name} [{id}] has been unbanned." + user_is_kicked: "🚪 {name} [{id}] has been kicked." + user_is_muted: "🔕 {name} [{id}] has been muted for{time_info}." + reason: "Reason: {reason}" + forever: "ever" + user_is_unmuted: "🔉 {name} [{id}] has been unmuted." + title_changed: "The {type_of} title was successfully changed from {old_title} to {new_title}." + channel_created: " The channel {title} is created.\n🔗 Invite link: {link}" + group_created: " The group {title} is created.\n🔗 Invite link: {link}" + user_blocked: " {user} is blocked." + user_privacy_restricted: " {user} privacy settings restrict this action." + user_not_mutual_contact: " {user} is not a mutual contact." + user_kicked: " {user} is kicked from the chat." + user_invited: " User {user} is invited to the chat." + user_not_invited: " User could not be invited to the chat." + admin_list: "
👑 The creator is {name}\n\nAdmins ({admins_count}):\n{admins}
" + dnd: "🔕 Chat muted and archived" + dnd_failed: "⚠️ Failed to mute and archive chat" + pinned: " Pinned the message" + pin_failed: "✖️ Failed to pin the message" + unpinned: " Unpinned the message" + unpin_failed: "✖️ Failed to unpin the message" + type_group: "Group" + type_channel: "Channel" + type_unknown: "Unknown" + yes: " Yes" + no: "✖️ No" + chatinfo: "
🔒 Type: {type_of}\n#️⃣ Chat ID: {id}\n🔥 Title: {title}\n📖 Forum: {is_forum}
\n
🖋 About: {about}
\n
👑 Admin count: {admins_count}\n Online count: {online_count}\n👤 Participants count: {participants_count}\n🚫 Kicked сount: {kicked_count}\n🔀 Requests pending: {requests_pending}
\n
🕐 Slowmode period: {slowmode_seconds}\n📞 Call: {call}\n🗑 TTL period: {ttl_period}\n👤 Recent requesters: {recent_requesters}
\n
👥 Linked Chat ID: {linked_chat_id}\n🛡 Antispam: {antispam}\n👁 Participants hidden: {participants_hidden}
\n🔗 Link: {link}" + requests_checked: " Checked requests from {entities}" + promoted: "👑 {name} is promoted!\n Rights: {rights}" + full_rights: "Full rights" + promote: "Select rights for {name}!\nRank: {rank}" + restrict: "Restricting {name} for{time}\n\nSelect which actions to restrict. Options marked in green will be applied." + restricted: "🚫 {name} has been restricted for{time}" + demoted: "🔽 {name} is demoted" + userinfo: "
#️⃣ ID: {user_id}\n🔥 First name: {first_name}\n🔥 Last name: {last_name}\n📞 Phone number: {phone}\n🐶 Usernames: {usernames}
\n
ℹ️ About: {about}
\n
🕐 Work hours: {business_work_hours}\n❤️ Emoji status: {emoji_status}
\n
🔉 Personal channel: {personal_channel}\n🎂 Birthday: {birthday}\n🎁 Gifts count: {stargifts_count}
\n
🚨 Common chats count: {common_chats_count}\n👥 Common chats: {common_chats}
" + monday: "Monday" + tuesday: "Tuesday" + wednesday: "Wednesday" + thursday: "Thursday" + friday: "Friday" + saturday: "Saturday" + sunday: "Sunday" + owns: "👑 My kingdoms [{num}]:\n
{owns}
" + close: "❌ Close" + apply: "✅ Apply" +ru: + my_id: "👑 Мой ID: {id}" + chat_id: "💬 ID чата: {id}" + user_id: "👤 ID пользователя: {id}" + user_not_participant: " Пользователь не состоит в этом чате." + admin_rights: "
📜 {name} Права в этом чате:\n\n{rights}\n\n🔼 Повысил: {promoter_name} [{promoter_id}]
" + not_an_admin: " {user} не является админом." + no_rights: " У меня недостаточно прав :(" + no_user: " Пользователь не найден." + change_info: "Изменение информации" + delete_messages: "Удаление сообщений" + other: "Другое" + ban_users: "Бан пользователей" + invite_users: "Приглашение пользователей" + pin_messages: "Закрепление сообщений" + add_admins: "Добавление админов" + manage_call: "Управление звонками" + post_stories: "Публикация историй" + edit_stories: "Редактирование историй" + delete_stories: "Удаление историй" + anonymous: "Анонимность" + manage_topics: "Управление темами" + post_messages: "Публикация сообщений" + edit_messages: "Редактирование сообщений" + view_messages: "Просмотр сообщений" + send_messages: "Отправка сообщений" + send_media: "Отправка медиа" + send_stickers: "Отправка стикеров" + send_gifs: "Отправка GIF" + send_games: "Отправка игр" + send_inline: "Использование инлайн-ботов" + embed_links: "Встраивание ссылок" + send_polls: "Отправка опросов" + send_photos: "Отправка фото" + send_videos: "Отправка видео" + send_roundvideos: "Отправка круговых видео" + send_audios: "Отправка аудио" + send_voices: "Отправка голосовых" + send_docs: "Отправка документов" + send_plain: "Отправка текстовых сообщений" + invalid_args: " Неверные аргументы." + error: "😖 Что-то пошло не так. Проверьте логи." + successful_delete: " Сущность успешно удалена" + no_deleted_accounts: "😶‍🌫️ Удалённых аккаунтов здесь нет" + kicked_deleted_accounts: "🗑 Удалённые аккаунты очищены из чата" + admins_in_chat: "👑 Админы в {title} ({count}):\n" + no_admins_in_chat: "✖️ В этом чате нет админов." + bot_list: "
🤖 Боты ({count}):\n{bots}
" + no_bots_in_chat: "✖️ В этом чате нет ботов." + user_list: "
👤 Пользователи ({count}):\n{users}
" + no_user_in_chat: "✖️ В этом чате нет пользователей." + user_is_banned: "🚫 {name} [{id}] забанен на{time_info}." + user_is_unbanned: "👋 {name} [{id}] разбанен." + user_is_kicked: "🚪 {name} [{id}] кикнут." + user_is_muted: "🔕 {name} [{id}] замьючен на{time_info}." + reason: "Причина: {reason}" + forever: "всегда" + user_is_unmuted: "🔉 {name} [{id}] размьючен." + title_changed: "{type_of} название успешно изменено с {old_title} на {new_title}." + channel_created: " Канал {title} создан.\n🔗 Пригласительная ссылка: {link}" + group_created: " Группа {title} создана.\n🔗 Пригласительная ссылка: {link}" + user_blocked: " {user} заблокирован." + user_privacy_restricted: " {user} ограничил это действие настройками приватности." + user_not_mutual_contact: " {user} не является взаимным контактом." + user_kicked: " {user} исключён из чата." + user_invited: " Пользователь {user} приглашён в чат." + user_not_invited: " Не удалось пригласить пользователя." + admin_list: "
👑 Создатель: {name}\n\nАдмины ({admins_count}):\n{admins}
" + dnd: "🔕 Чат заглушён и архивирован" + dnd_failed: "⚠️ Не удалось заглушить и архивировать чат" + pinned: " Сообщение закреплено" + pin_failed: "✖️ Не удалось закрепить сообщение" + unpinned: " Сообщение откреплено" + unpin_failed: "✖️ Не удалось открепить сообщение" + type_group: "Группа" + type_channel: "Канал" + type_unknown: "Неизвестно" + yes: " Да" + no: "✖️ Нет" + chatinfo: "
🔒 Тип: {type_of}\n#️⃣ ID чата: {id}\n🔥 Название: {title}\n📖 Форум: {is_forum}
\n
🖋 Описание: {about}
\n
👑 Кол-во админов: {admins_count}\n Онлайн: {online_count}\n👤 Кол-во участников: {participants_count}\n🚫 Кикнутых: {kicked_count}\n🔀 Запросов в ожидании: {requests_pending}
\n
🕐 Слоумод: {slowmode_seconds}\n📞 Звонок: {call}\n🗑 Период TTL: {ttl_period}\n👤 Недавние запросы: {recent_requesters}
\n
👥 Связанный чат: {linked_chat_id}\n🛡 Антиспам: {antispam}\n👁 Скрытые участники: {participants_hidden}
\n🔗 Ссылка: {link}" + requests_checked: " Рассмотрены заявки от {entities}" + promoted: "👑 {name} назначен администратором!\n Права: {rights}" + full_rights: "Полные права" + promote: "Выберите права для {name}!\nРанг: {rank}" + restrict: "Ограничение {name} на{time}\n\nВыберите, что нужно запретить. Опции, отмеченные зелёным, будут применены." + restricted: "🚫 {name} был(а) ограничен(а) на{time}" + demoted: "🔽 {name} понижен" + userinfo: "
#️⃣ ID: {user_id}\n🔥 Имя: {first_name}\n🔥 Фамилия: {last_name}\n📞 Номер телефона: {phone}\n🐶 Имена пользователей: {usernames}
\n
ℹ️ О себе: {about}
\n
🕐 Рабочие часы: {business_work_hours}\n❤️ Статус с эмодзи: {emoji_status}
\n
🔉 Личный канал: {personal_channel}\n🎂 День рождения: {birthday}\n🎁 Количество подарков: {stargifts_count}
\n
🚨 Количество общих чатов: {common_chats_count}\n👥 Общие чаты: {common_chats}
" + monday: "Понедельник" + tuesday: "Вторник" + wednesday: "Среда" + thursday: "Четверг" + friday: "Пятница" + saturday: "Суббота" + sunday: "Воскресенье" + owns: "👑 Мои королевства [{num}]:\n
{owns}
" + close: "❌ Закрыть" + apply: "✅ Применить" \ No newline at end of file diff --git a/fajox1/famods/.DS_Store b/fajox1/famods/.DS_Store new file mode 100644 index 0000000..00ec97e Binary files /dev/null and b/fajox1/famods/.DS_Store differ diff --git a/fajox1/famods/assets/.DS_Store b/fajox1/famods/assets/.DS_Store new file mode 100644 index 0000000..3c1027d Binary files /dev/null and b/fajox1/famods/assets/.DS_Store differ diff --git a/fajox1/famods/assets/banners/.DS_Store b/fajox1/famods/assets/banners/.DS_Store new file mode 100644 index 0000000..745e632 Binary files /dev/null and b/fajox1/famods/assets/banners/.DS_Store differ diff --git a/fajox1/famods/assets/banners/ptichki.png b/fajox1/famods/assets/banners/ptichki.png new file mode 100644 index 0000000..cea6ed1 Binary files /dev/null and b/fajox1/famods/assets/banners/ptichki.png differ diff --git a/fajox1/famods/assets/birds/.DS_Store b/fajox1/famods/assets/birds/.DS_Store new file mode 100644 index 0000000..343cab0 Binary files /dev/null and b/fajox1/famods/assets/birds/.DS_Store differ diff --git a/fajox1/famods/assets/birds/birds.json b/fajox1/famods/assets/birds/birds.json new file mode 100644 index 0000000..7ef414f --- /dev/null +++ b/fajox1/famods/assets/birds/birds.json @@ -0,0 +1,9 @@ +[ + "blue", + "green", + "orange", + "pink", + "purple", + "white", + "yellow" +] \ No newline at end of file diff --git a/fajox1/famods/assets/birds/blue.png b/fajox1/famods/assets/birds/blue.png new file mode 100644 index 0000000..c430251 Binary files /dev/null and b/fajox1/famods/assets/birds/blue.png differ diff --git a/fajox1/famods/assets/birds/green.png b/fajox1/famods/assets/birds/green.png new file mode 100644 index 0000000..be681a8 Binary files /dev/null and b/fajox1/famods/assets/birds/green.png differ diff --git a/fajox1/famods/assets/birds/orange.png b/fajox1/famods/assets/birds/orange.png new file mode 100644 index 0000000..897175f Binary files /dev/null and b/fajox1/famods/assets/birds/orange.png differ diff --git a/fajox1/famods/assets/birds/pink.png b/fajox1/famods/assets/birds/pink.png new file mode 100644 index 0000000..2c4e2fc Binary files /dev/null and b/fajox1/famods/assets/birds/pink.png differ diff --git a/fajox1/famods/assets/birds/purple.png b/fajox1/famods/assets/birds/purple.png new file mode 100644 index 0000000..9726451 Binary files /dev/null and b/fajox1/famods/assets/birds/purple.png differ diff --git a/fajox1/famods/assets/birds/white.png b/fajox1/famods/assets/birds/white.png new file mode 100644 index 0000000..b6ee065 Binary files /dev/null and b/fajox1/famods/assets/birds/white.png differ diff --git a/fajox1/famods/assets/birds/yellow.png b/fajox1/famods/assets/birds/yellow.png new file mode 100644 index 0000000..ad5cda8 Binary files /dev/null and b/fajox1/famods/assets/birds/yellow.png differ diff --git a/fajox1/famods/assets/impact.ttf b/fajox1/famods/assets/impact.ttf new file mode 100644 index 0000000..114e6c1 Binary files /dev/null and b/fajox1/famods/assets/impact.ttf differ diff --git a/fajox1/famods/full.txt b/fajox1/famods/full.txt index 00728d0..b4be147 100644 --- a/fajox1/famods/full.txt +++ b/fajox1/famods/full.txt @@ -43,3 +43,4 @@ evalaliases spotify4ik picme hetsu +ptichki \ No newline at end of file diff --git a/fajox1/famods/ptichki.py b/fajox1/famods/ptichki.py new file mode 100644 index 0000000..2409485 --- /dev/null +++ b/fajox1/famods/ptichki.py @@ -0,0 +1,156 @@ +# █▀▀ ▄▀█   █▀▄▀█ █▀█ █▀▄ █▀ +# █▀░ █▀█   █░▀░█ █▄█ █▄▀ ▄█ + +# https://t.me/famods + +# 🔒 Licensed under the GNU AGPLv3 +# 🌐 https://www.gnu.org/licenses/agpl-3.0.html + +# --------------------------------------------------------------------------------- +# Name: Ptichki +# Description: Генератор птиц +# meta developer: @FAmods +# meta banner: https://github.com/FajoX1/FAmods/blob/main/assets/banners/ptichki.png?raw=true +# requires: aiohttp pillow +# --------------------------------------------------------------------------------- + +import json +import random +import aiohttp +import logging + +from io import BytesIO +from PIL import Image, ImageDraw, ImageFont + +from .. import loader, utils + +logger = logging.getLogger(__name__) + +@loader.tds +class Ptichki(loader.Module): + """Генератор птиц""" + + strings = { + "name": "Ptichki", + + "no_args": "🦅 Нужно {}{} {}", + + "generation": "🦅 Генерирую птичку...", + } + + async def client_ready(self, client, db): + self.db = db + self._client = client + + self.assets_link = "https://famods.fajox.one/assets" + + self.font_url = f"{self.assets_link}/impact.ttf" + self.birds_url = f"{self.assets_link}/birds/birds.json" + + async def fetch_bytes(self, url: str) -> bytes: + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + resp.raise_for_status() + return await resp.read() + + async def get_bird_url(self) -> str: + async with aiohttp.ClientSession() as session: + async with session.get(self.birds_url) as resp: + birds_list = json.loads(await resp.text()) + + return f"{self.assets_link}/birds/{random.choice(birds_list)}.png" + + async def generate_bird(self, text: str, format: str) -> bytes: + + text = text.upper() + + img_bytes = await self.fetch_bytes( + await self.get_bird_url() + ) + img = Image.open(BytesIO(img_bytes)).convert("RGBA") + img.thumbnail((512, 512)) + width, height = img.size + draw = ImageDraw.Draw(img) + + font_bytes = await self.fetch_bytes(self.font_url) + font_size = 55 + min_font_size = 12 + max_width_fraction = 0.9 + + font = ImageFont.truetype(BytesIO(font_bytes), font_size) + text_width = font.getlength(text) + + if text_width > max_width_fraction * width: + scale = (max_width_fraction * width) / text_width + font_size = max(int(font_size * scale), min_font_size) + font = ImageFont.truetype(BytesIO(font_bytes), font_size) + + bbox = draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + x = (width - text_width) / 2 + y = height - text_height - (height * 0.05) + + draw.text( + (x, y), + text, + font=font, + fill="white", + stroke_width=2, + stroke_fill="black" + ) + + output = BytesIO() + img.save(output, format=format.upper()) + output.seek(0) + output.name = f"ptitchka.{format.lower()}" + + return output + + @loader.command() + async def ptichka(self, message): + """[текст] - Сгенерировать стикер с птицей""" + + text = utils.get_args_raw(message) + if not text: + return await utils.answer( + message, + self.strings["no_args"].format( + self.get_prefix(), "ptichka", "[текст]" + ) + ) + + m = await utils.answer(message, self.strings['generation']) + + await self.client.send_file( + message.peer_id, + mime_type="image/webp", + file=await self.generate_bird(text, format="webp"), + reply_to=getattr(message.reply_to, "reply_to_msg_id", None), + ) + + return await m.delete() + + @loader.command() + async def ptichka_img(self, message): + """[текст] - Сгенерировать фото с птицей""" + + text = utils.get_args_raw(message) + if not text: + return await utils.answer( + message, + self.strings["no_args"].format( + self.get_prefix(), "ptichka_img", "[текст]" + ) + ) + + m = await utils.answer(message, self.strings['generation']) + + await self.client.send_file( + message.peer_id, + mime_type="image/png", + file=await self.generate_bird(text, format="png"), + reply_to=getattr(message.reply_to, "reply_to_msg_id", None), + ) + + return await m.delete() \ No newline at end of file diff --git a/fiksofficial/python-modules/full.txt b/fiksofficial/python-modules/full.txt index 73e0f01..09ff77b 100644 --- a/fiksofficial/python-modules/full.txt +++ b/fiksofficial/python-modules/full.txt @@ -20,4 +20,5 @@ createavatarspack multiunloadmodule tagall2.0 point -deviceinfo \ No newline at end of file +deviceinfo +mpi \ No newline at end of file diff --git a/fiksofficial/python-modules/mpi.py b/fiksofficial/python-modules/mpi.py new file mode 100644 index 0000000..8805a9a --- /dev/null +++ b/fiksofficial/python-modules/mpi.py @@ -0,0 +1,229 @@ +# ______ ___ ___ _ _ +# ____ | ___ \ | \/ | | | | | +# / __ \| |_/ / _| . . | ___ __| |_ _| | ___ +# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \ +# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/ +# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___| +# \____/ __/ | +# |___/ + +# На модуль распространяется лицензия "GNU General Public License v3.0" +# https://github.com/all-licenses/GNU-General-Public-License-v3.0 + +# meta developer: @pymodule + +import aiohttp +import base64 +import json +from datetime import datetime +from .. import loader, utils + +async def download_image(session: aiohttp.ClientSession, url: str): + try: + async with session.get(url, timeout=35) as resp: + if resp.status == 200: + return await resp.read() + except Exception: + pass + return None + +@loader.tds +class MinecraftPlayerInfo(loader.Module): + """A module for obtaining information about a Minecraft player by nickname""" + strings = { + "name": "MinecraftPlayerInfo", + "no_args": "❌ Specify the player's nickname", + "not_found": "❌ Player with this nickname not found", + "loading": "🔄 Loading information...", + "no_media": "❌ Failed to load any images", + "partial_media": "⚠️ Some images failed to load\n\n", + "no_history": "No nickname history", + "model_steve": "Classic (Steve)", + "model_alex": "Slim (Alex)", + "cape_yes": "Yes ✅", + "cape_no": "No ❌", + "cape_failed": " (render failed to load)", + "history_current": "— current", + "history_changed": "— changed {}", + "history_original": "— original", + "info": ( + "🔍 Minecraft Player Information\n\n" + "Nickname: {name}\n" + "UUID: {uuid_dashed}\n" + "Skin Model: {model}\n" + "Cape: {cape}\n\n" + "Nickname History:\n{history}\n\n" + "🔗 Full profile on NameMC" + ) + } + + strings_ru = { + "_cls_doc": "Модуль для получения информации о игроке Minecraft по никнейму", + "no_args": "❌ Укажите никнейм игрока", + "not_found": "❌ Игрок с таким никнеймом не найден", + "loading": "🔄 Загружаю информацию...", + "no_media": "❌ Не удалось загрузить ни одного изображения", + "partial_media": "⚠️ Некоторые изображения не загрузились\n\n", + "no_history": "Нет истории изменений", + "model_steve": "Classic (Steve)", + "model_alex": "Slim (Alex)", + "cape_yes": "Есть ✅", + "cape_no": "Нет ❌", + "cape_failed": " (рендер не загрузился)", + "history_current": "— текущий", + "history_changed": "— изменён {}", + "history_original": "— оригинальный", + "info": ( + "🔍 Информация о игроке Minecraft\n\n" + "Никнейм: {name}\n" + "UUID: {uuid_dashed}\n" + "Модель скина: {model}\n" + "Плащ: {cape}\n\n" + "История никнеймов:\n{history}\n\n" + "🔗 Полный профиль на NameMC" + ) + } + + async def client_ready(self, client, db): + self.client = client + + @loader.command(ru_doc="<никнейм> — отображает информацию об игроке Minecraft (3D-рендеринг, история, плащ)") + async def mcplayer(self, message): + """ — show Minecraft player info (3D renders, history, cape)""" + args = utils.get_args_raw(message) + if not args: + return await utils.answer(message, self.strings("no_args")) + + loading_msg = await utils.answer(message, self.strings("loading")) + + nick = args.strip() + + async with aiohttp.ClientSession() as session: + async with session.get(f"https://api.mojang.com/users/profiles/minecraft/{nick}") as resp: + if resp.status == 204 or resp.status != 200: + await loading_msg.edit(self.strings("not_found")) + return + data = await resp.json() + + name = data["name"] + uuid = data["id"] + uuid_dashed = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}" + + async with session.get(f"https://api.mojang.com/user/profiles/{uuid}/names") as resp: + names = await resp.json() if resp.status == 200 else [{"name": name}] + + history_lines = [] + for i, entry in enumerate(names): + uname = entry["name"] + if i == len(names) - 1: + history_lines.append(f"• {uname} {self.strings('history_current')}") + elif "changedToAt" in entry: + changed = datetime.utcfromtimestamp(entry["changedToAt"] / 1000).strftime("%d.%m.%Y") + history_lines.append(f"• {uname} {self.strings('history_changed').format(changed)}") + else: + history_lines.append(f"• {uname} {self.strings('history_original')}") + + history = "\n".join(history_lines) or self.strings("no_history") + + async with session.get(f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false") as resp: + profile = await resp.json() if resp.status == 200 else {} + + cape_url = None + model = self.strings("model_steve") + if profile.get("properties"): + for prop in profile["properties"]: + if prop["name"] == "textures": + textures_b64 = prop["value"] + textures_json = json.loads(base64.b64decode(textures_b64).decode("utf-8")) + textures = textures_json.get("textures", {}) + skin_data = textures.get("SKIN", {}) + cape_url = textures.get("CAPE", {}).get("url") + if "metadata" in skin_data and skin_data["metadata"].get("model") == "slim": + model = self.strings("model_alex") + + has_cape = bool(cape_url) + cape_text = self.strings("cape_yes") if has_cape else self.strings("cape_no") + + body_urls = [ + f"https://crafthead.net/body/{uuid}.png", + f"https://api.mineatar.io/body/{uuid}?scale=12", + f"https://mc-heads.net/body/{uuid}/500", + f"https://cravatar.eu/helmbody/{uuid}/500.png", + f"https://minotar.net/body/{uuid}/500.png", + ] + + head_urls = [ + f"https://crafthead.net/avatar/{uuid}.png", + f"https://api.mineatar.io/head/{uuid}?scale=12", + f"https://mc-heads.net/avatar/{uuid}/500", + f"https://cravatar.eu/head/{uuid}/500.png", + f"https://minotar.net/avatar/{uuid}/500.png", + ] + + body_bytes = None + for url in body_urls: + body_bytes = await download_image(session, url) + if body_bytes: + break + + head_bytes = None + for url in head_urls: + head_bytes = await download_image(session, url) + if head_bytes: + break + + cape_bytes = await download_image(session, cape_url) if cape_url else None + if not cape_bytes and has_cape: + cape_fallbacks = [ + f"https://crafthead.net/cape/{uuid}.png", + f"https://api.mineatar.io/cape/{uuid}.png", + f"https://mc-heads.net/cape/{uuid}", + ] + for url in cape_fallbacks: + cape_bytes = await download_image(session, url) + if cape_bytes: + break + if not cape_bytes: + cape_text += self.strings("cape_failed") + + uploaded_media = [] + + if body_bytes: + uploaded = await self.client.upload_file(body_bytes, file_name=f"{name}_body.png") + uploaded_media.append(uploaded) + + if head_bytes: + uploaded = await self.client.upload_file(head_bytes, file_name=f"{name}_head.png") + uploaded_media.append(uploaded) + + if cape_bytes: + uploaded = await self.client.upload_file(cape_bytes, file_name=f"{name}_cape.png") + uploaded_media.append(uploaded) + + if not uploaded_media: + await loading_msg.edit(self.strings("no_media")) + return + + caption = self.strings("info").format( + name=name, + uuid_dashed=uuid_dashed, + uuid_raw=uuid, + model=model, + cape=cape_text, + history=history + ) + + if not (body_bytes and head_bytes): + caption = self.strings("partial_media") + caption + + await self.client.send_file( + message.peer_id, + file=uploaded_media, + caption=caption, + parse_mode="html", + reply_to=message.reply_to_msg_id or message.id + ) + + await loading_msg.delete() + if message.out: + await message.delete() \ No newline at end of file diff --git a/modules.json b/modules.json index 51dd1f6..0b40406 100644 --- a/modules.json +++ b/modules.json @@ -12209,6 +12209,43 @@ "Tools" ] }, + "fajox1/famods/ptichki.py": { + "name": "Ptichki", + "description": "Генератор птиц", + "meta": { + "pic": null, + "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/ptichki.png?raw=true", + "developer": "@FAmods" + }, + "commands": [ + { + "ptichka": "[текст] - Сгенерировать стикер с птицей" + }, + { + "ptichka_img": "[текст] - Сгенерировать фото с птицей" + } + ], + "new_commands": [ + { + "ptichka": { + "ru_doc": null, + "en_doc": null, + "doc": "[текст] - Сгенерировать стикер с птицей" + } + }, + { + "ptichka_img": { + "ru_doc": null, + "en_doc": null, + "doc": "[текст] - Сгенерировать фото с птицей" + } + } + ], + "category": [ + "Tools", + "Chat" + ] + }, "fajox1/famods/fun.py": { "name": "Fun", "description": "Module for fun...", @@ -16278,6 +16315,33 @@ "Tools" ] }, + "fiksofficial/python-modules/mpi.py": { + "name": "MinecraftPlayerInfo", + "description": "A module for obtaining information about a Minecraft player by nickname", + "meta": { + "pic": null, + "banner": null, + "developer": "@pymodule" + }, + "commands": [ + { + "mcplayer": " — show Minecraft player info (3D renders, history, cape) <никнейм> — отображает информацию об игроке Minecraft (3D-рендеринг, история, плащ)" + } + ], + "new_commands": [ + { + "mcplayer": { + "ru_doc": "<никнейм> — отображает информацию об игроке Minecraft (3D-рендеринг, история, плащ)", + "en_doc": null, + "doc": " — show Minecraft player info (3D renders, history, cape)" + } + } + ], + "category": [ + "Tools", + "Admin" + ] + }, "fiksofficial/python-modules/tagall2.0.py": { "name": "TagAllMod", "description": "TagAll 2.0 — smart mention of chat participants: .tagall {all/admins/online/active} {text}", @@ -17009,7 +17073,7 @@ }, "Ruslan-Isaev/modules/youtube-loader.py": { "name": "YouTube_DLDMod", - "description": "Помогает скачивать видео с YouTube", + "description": "Помогает скачивать видео с YouTube, TikTok и др.", "meta": { "pic": null, "banner": null, @@ -17017,15 +17081,25 @@ }, "commands": [ { - "dlvideo": "<ссылка> или ответ на сообщение со ссылкой — скачивает видео с YouTube" + "dvlist": "Показать список всех поддерживаемых сайтов" + }, + { + "dlvideo": "<ссылка> или ответ на сообщение со ссылкой — скачивает видео с поддерживаемых платформ" } ], "new_commands": [ + { + "dvlist": { + "ru_doc": null, + "en_doc": null, + "doc": "Показать список всех поддерживаемых сайтов" + } + }, { "dlvideo": { "ru_doc": null, "en_doc": null, - "doc": "<ссылка> или ответ на сообщение со ссылкой — скачивает видео с YouTube" + "doc": "<ссылка> или ответ на сообщение со ссылкой — скачивает видео с поддерживаемых платформ" } } ], @@ -22136,7 +22210,7 @@ "send": "[text] - Write a message [text] - Написать сообщение" }, { - "sendclosedtopic": "[text] - Write a message to a closed topic [text] - Написать сообщение в закрытую тему" + "sendclosedtopic": "[text or reply(media/file/sticker) or coordinates (, )] - Write a message to a closed topic [text or reply(media/file/sticker) or coordinates (, )] - Написать сообщение в закрытую тему" }, { "sendpm": "[@UserName] [text or replay] - Write a message to personal messages [@UserName] [text or replay] - Написать сообщение в личные сообщения" @@ -22155,9 +22229,9 @@ }, { "sendclosedtopic": { - "ru_doc": "[text] - Написать сообщение в закрытую тему", + "ru_doc": "[text or reply(media/file/sticker) or coordinates (, )] - Написать сообщение в закрытую тему", "en_doc": null, - "doc": "[text] - Write a message to a closed topic" + "doc": "[text or reply(media/file/sticker) or coordinates (, )] - Write a message to a closed topic" } }, { @@ -39244,6 +39318,33 @@ "Tools" ] }, + "coddrago/modules/hardspam.py": { + "name": "HardSpam", + "description": null, + "meta": { + "pic": null, + "banner": null, + "developer": "@codrago_m" + }, + "commands": [ + { + "hspamcmd": "[-c|--clean] n text - Send n number of messages at the same time [-c|--clean] n text - Отправить n кол-во сообщений одновременно" + } + ], + "new_commands": [ + { + "hspam": { + "ru_doc": "[-c|--clean] n text - Отправить n кол-во сообщений одновременно", + "en_doc": null, + "doc": "[-c|--clean] n text - Send n number of messages at the same time" + } + } + ], + "category": [ + "Tools", + "Chat" + ] + }, "coddrago/modules/DoxTool.py": { "name": "dox", "description": "Maybe... doxing tool?", @@ -39446,6 +39547,33 @@ "Tools" ] }, + "coddrago/modules/stats.py": { + "name": "Stats", + "description": "Показывает статистику твоего аккаунта", + "meta": { + "pic": null, + "banner": null, + "developer": "@codrago_m" + }, + "commands": [ + { + "stats": "Получить статистику" + } + ], + "new_commands": [ + { + "stats": { + "ru_doc": null, + "en_doc": null, + "doc": "Получить статистику" + } + } + ], + "category": [ + "Tools", + "Fun" + ] + }, "coddrago/modules/modlist.py": { "name": "ModulesList", "description": "Модуль для быстрого доступа к каналам с модулями", @@ -39493,6 +39621,33 @@ "Tools" ] }, + "coddrago/modules/tagwatcher.py": { + "name": "TagWatcher", + "description": "Informs when you are tagged in chats and automatically reads pm.", + "meta": { + "pic": null, + "banner": null, + "developer": "@codrago_m" + }, + "commands": [ + { + "tagwatcher": "Enable/Disable TagWatcher. Вкл/выкл TagWatcher." + } + ], + "new_commands": [ + { + "tagwatcher": { + "ru_doc": "Вкл/выкл TagWatcher.", + "en_doc": null, + "doc": "Enable/Disable TagWatcher." + } + } + ], + "category": [ + "Automation", + "Chat" + ] + }, "coddrago/modules/id.py": { "name": "ID", "description": "ID of all!", @@ -39567,6 +39722,290 @@ "Media" ] }, + "coddrago/modules/dbmod.py": { + "name": "DBMod", + "description": null, + "meta": { + "pic": null, + "banner": null, + "developer": "@codrago_m" + }, + "commands": [ + { + "mydb": "Viewing the database Просмотр базы данных" + } + ], + "new_commands": [ + { + "mydb": { + "ru_doc": "Просмотр базы данных", + "en_doc": null, + "doc": "Viewing the database" + } + } + ], + "category": [ + "Tools", + "Info" + ] + }, + "coddrago/modules/chatmodule.py": { + "name": "ChatModuleMod", + "description": null, + "meta": { + "pic": null, + "banner": null, + "developer": "@codrago_m" + }, + "commands": [ + { + "id": "[reply] - Get the ID [reply] - Узнать ID" + }, + { + "rights": "[reply/-u username/id] - Check user's admin rights [reply/-u username/id] - Посмотреть права администратора пользователя" + }, + { + "leave": "Leave chat Покинуть чат" + }, + { + "d": "[a[1-100] b[1-100]] | [reply] - Delete messages [a[1-100] b[1-100]] | [reply] Удалить сообщения" + }, + { + "pin": "[reply] - Pin a message [reply] - Закрепить сообщение" + }, + { + "unpin": "Unpin a message Открепить сообщение" + }, + { + "dgc": "[-c id] Delete chat/channel [-c id] Удаляет группу/канал" + }, + { + "flush": "Removes deleted accounts from the chat/channel Очищает группу/канал от удаленных аккаунтов" + }, + { + "admins": "Shows the admins in the chat/channel Показывает админов в группе/канале" + }, + { + "bots": "Shows the bots in the chat/channel Показывает ботов в группе/канале" + }, + { + "users": "Shows the users in the chat/channel Показывает простых участников чата/канала" + }, + { + "ban": "[-u] [-t] [-r] Ban a participant temporarily or permanently [-u] [-t] [-r] Забанить участника" + }, + { + "unban": "[-u] Unban a user Разбанить пользователя" + }, + { + "kick": "[-u] [-r] Kick a participant [-u] [-r] Кикнуть участника" + }, + { + "mute": "[-u] [-t] [-r] Mute a participant temporarily or permanently [-u] [-t] [-r] Замутить участника" + }, + { + "unmute": "Unmute a participant Размутить участника" + }, + { + "create": "[-g|--group name] [-c|--channel name] - Create group/channel [-g|--group name] [-c|--channel name] - Создать группу/канал" + }, + { + "dnd": "Mutes and archives the current chat Отключает звук и архивирует чат" + }, + { + "invite": "-u username/id - Invite a user to the chat (use -b to invite the inline bot) -u username/id - Пригласить пользователя в чат (-b пригласить инлайн бота)" + }, + { + "inspect": "[-i] Get the info about the entity [-i] Получить информацию о сущности" + }, + { + "requests": "[-a] [-d] Manage join requests [-a] [-d] Управлять заявками на вступление" + }, + { + "owns": "Get all your chats/channels Получить все свои чаты/каналы" + }, + { + "promote": "[-r] [-u] [-f] - Promote a participant [-r] [-u] [-f] - Выдать админку участнику" + }, + { + "restrict": "[-t] [-u] - Restrict a participant [-t] [-u] - Ограничить участника" + } + ], + "new_commands": [ + { + "id": { + "ru_doc": "[reply] - Узнать ID", + "en_doc": null, + "doc": "[reply] - Get the ID" + } + }, + { + "rights": { + "ru_doc": "[reply/-u username/id] - Посмотреть права администратора пользователя", + "en_doc": null, + "doc": "[reply/-u username/id] - Check user's admin rights" + } + }, + { + "leave": { + "ru_doc": "Покинуть чат", + "en_doc": null, + "doc": "Leave chat" + } + }, + { + "d": { + "ru_doc": "[a[1-100] b[1-100]] | [reply] Удалить сообщения", + "en_doc": null, + "doc": "[a[1-100] b[1-100]] | [reply] - Delete messages" + } + }, + { + "pin": { + "ru_doc": "[reply] - Закрепить сообщение", + "en_doc": null, + "doc": "[reply] - Pin a message" + } + }, + { + "unpin": { + "ru_doc": "Открепить сообщение", + "en_doc": null, + "doc": "Unpin a message" + } + }, + { + "dgc": { + "ru_doc": "[-c id] Удаляет группу/канал", + "en_doc": null, + "doc": "[-c id] Delete chat/channel" + } + }, + { + "flush": { + "ru_doc": "Очищает группу/канал от удаленных аккаунтов", + "en_doc": null, + "doc": "Removes deleted accounts from the chat/channel" + } + }, + { + "admins": { + "ru_doc": "Показывает админов в группе/канале", + "en_doc": null, + "doc": "Shows the admins in the chat/channel" + } + }, + { + "bots": { + "ru_doc": "Показывает ботов в группе/канале", + "en_doc": null, + "doc": "Shows the bots in the chat/channel" + } + }, + { + "users": { + "ru_doc": "Показывает простых участников чата/канала", + "en_doc": null, + "doc": "Shows the users in the chat/channel" + } + }, + { + "ban": { + "ru_doc": "[-u] [-t] [-r] Забанить участника", + "en_doc": null, + "doc": "[-u] [-t] [-r] Ban a participant temporarily or permanently" + } + }, + { + "unban": { + "ru_doc": "Разбанить пользователя", + "en_doc": null, + "doc": "[-u] Unban a user" + } + }, + { + "kick": { + "ru_doc": "[-u] [-r] Кикнуть участника", + "en_doc": null, + "doc": "[-u] [-r] Kick a participant" + } + }, + { + "mute": { + "ru_doc": "[-u] [-t] [-r] Замутить участника", + "en_doc": null, + "doc": "[-u] [-t] [-r] Mute a participant temporarily or permanently" + } + }, + { + "unmute": { + "ru_doc": "Размутить участника", + "en_doc": null, + "doc": "Unmute a participant" + } + }, + { + "create": { + "ru_doc": "[-g|--group name] [-c|--channel name] - Создать группу/канал", + "en_doc": null, + "doc": "[-g|--group name] [-c|--channel name] - Create group/channel" + } + }, + { + "dnd": { + "ru_doc": "Отключает звук и архивирует чат", + "en_doc": null, + "doc": "Mutes and archives the current chat" + } + }, + { + "invite": { + "ru_doc": "-u username/id - Пригласить пользователя в чат (-b пригласить инлайн бота)", + "en_doc": null, + "doc": "-u username/id - Invite a user to the chat (use -b to invite the inline bot)" + } + }, + { + "inspect": { + "ru_doc": "[-i] Получить информацию о сущности", + "en_doc": null, + "doc": "[-i] Get the info about the entity" + } + }, + { + "requests": { + "ru_doc": "[-a] [-d] Управлять заявками на вступление", + "en_doc": null, + "doc": "[-a] [-d] Manage join requests" + } + }, + { + "owns": { + "ru_doc": "Получить все свои чаты/каналы", + "en_doc": null, + "doc": "Get all your chats/channels" + } + }, + { + "promote": { + "ru_doc": "[-r] [-u] [-f] - Выдать админку участнику", + "en_doc": null, + "doc": "[-r] [-u] [-f] - Promote a participant" + } + }, + { + "restrict": { + "ru_doc": "[-t] [-u] - Ограничить участника", + "en_doc": null, + "doc": "[-t] [-u] - Restrict a participant" + } + } + ], + "category": [ + "Chat", + "Tools" + ] + }, "coddrago/modules/compliments.py": { "name": "Compliments", "description": "Compliments for your partner", diff --git a/vsecoder/hikka_modules/hh.py b/vsecoder/hikka_modules/hh.py index 1e36dd8..1225bde 100644 --- a/vsecoder/hikka_modules/hh.py +++ b/vsecoder/hikka_modules/hh.py @@ -26,241 +26,6 @@ from .. import loader, utils __version__ = (2, 0, 0) -FLAGS = { - "ad": "🇦🇩", # Андорра - "ae": "🇦🇪", # ОАЭ - "af": "🇦🇫", # Афганистан - "ag": "🇦🇬", # Антигуа и Барбуда - "ai": "🇦🇮", # Ангилья - "al": "🇦🇱", # Албания - "am": "🇦🇲", # Армения - "ao": "🇦🇴", # Ангола - "aq": "🇦🇶", # Антарктика - "ar": "🇦🇷", # Аргентина - "at": "🇦🇹", # Австрия - "au": "🇦🇺", # Австралия - "aw": "🇦🇼", # Аруба - "ax": "🇦🇽", # Аландские острова - "az": "🇦🇿", # Азербайджан - "ba": "🇧🇦", # Босния и Герцеговина - "bb": "🇧🇧", # Барбадос - "bd": "🇧🇩", # Бангладеш - "be": "🇧🇪", # Бельгия - "bf": "🇧🇫", # Буркина-Фасо - "bg": "🇧🇬", # Болгария - "bh": "🇧🇭", # Бахрейн - "bi": "🇧🇮", # Бурунди - "bj": "🇧🇯", # Бенин - "bl": "🇧🇱", # Сен-Бартельми - "bm": "🇧🇲", # Бермудские острова - "bn": "🇧🇳", # Бруней - "bo": "🇧🇴", # Боливия - "bq": "🇧🇶", # Бонэйр, Синт-Эстатиус и Саба - "br": "🇧🇷", # Бразилия - "bs": "🇧🇸", # Багамы - "bt": "🇧🇹", # Бутан - "bv": "🇧🇻", # остров Буве - "bw": "🇧🇼", # Ботсвана - "by": "🇧🇾", # Беларусь - "bz": "🇧🇿", # Белиз - "ca": "🇨🇦", # Канада - "cc": "🇨🇨", # Кокосовые (Килинг) острова - "cd": "🇨🇩", # Конго - Киншаса - "cf": "🇨🇫", # Центральноафриканская Республика - "cg": "🇨🇬", # Конго - Браззавиль - "ch": "🇨🇭", # Швейцария - "ci": "🇨🇮", # Кот-д’Ивуар - "ck": "🇨🇰", # Острова Кука - "cl": "🇨🇱", # Чили - "cm": "🇨🇲", # Камерун - "cn": "🇨🇳", # Китай - "co": "🇨🇴", # Колумбия - "cr": "🇨🇷", # Коста-Рика - "cu": "🇨🇺", # Куба - "cv": "🇨🇻", # Кабо-Верде - "cw": "🇨🇼", # Кюрасао - "cx": "🇨🇽", # остров Рождества - "cy": "🇨🇾", # Кипр - "cz": "🇨🇿", # Чехия - "de": "🇩🇪", # Германия - "dj": "🇩🇯", # Джибути - "dk": "🇩🇰", # Дания - "dm": "🇩🇲", # Доминика - "do": "🇩🇴", # Доминиканская Республика - "dz": "🇩🇿", # Алжир - "ec": "🇪🇨", # Эквадор - "ee": "🇪🇪", # Эстония - "eg": "🇪🇬", # Египет - "eh": "🇪🇭", # Западная Сахара - "er": "🇪🇷", # Эритрея - "es": "🇪🇸", # Испания - "et": "🇪🇹", # Эфиопия - "fi": "🇫🇮", # Финляндия - "fj": "🇫🇯", # Фиджи - "fk": "🇫🇰", # Фолклендские острова - "fm": "🇫🇲", # Микронезия - "fo": "🇫🇴", # Фарерские острова - "fr": "🇫🇷", # Франция - "ga": "🇬🇦", # Габон - "gb": "🇬🇧", # Великобритания - "gd": "🇬🇩", # Гренада - "ge": "🇬🇪", # Грузия - "gf": "🇬🇫", # Французская Гвиана - "gg": "🇬🇬", # Гернси - "gh": "🇬🇭", # Гана - "gi": "🇬🇮", # Гибралтар - "gl": "🇬🇱", # Гренландия - "gm": "🇬🇲", # Гамбия - "gn": "🇬🇳", # Гвинея - "gp": "🇬🇵", # Гваделупа - "gq": "🇬🇶", # Экваториальная Гвинея - "gr": "🇬🇷", # Греция - "gs": "🇬🇸", # Южная Георгия и Южные Сандвичевы острова - "gt": "🇬🇹", # Гватемала - "gu": "🇬🇺", # Гуам - "gw": "🇬🇼", # Гвинея-Бисау - "gy": "🇬🇾", # Гайана - "hk": "🇭🇰", # Гонконг - "hm": "🇭🇲", # остров Херд и острова Макдональд - "hn": "🇭🇳", # Гондурас - "hr": "🇭🇷", # Хорватия - "ht": "🇭🇹", # Гаити - "hu": "🇭🇺", # Венгрия - "id": "🇮🇩", # Индонезия - "ie": "🇮🇪", # Ирландия - "il": "🇮🇱", # Израиль - "im": "🇮🇲", # остров Мэн - "in": "🇮🇳", # Индия - "io": "🇮🇴", # Британская территория в Индийском океане - "iq": "🇮🇶", # Ирак - "ir": "🇮🇷", # Иран - "is": "🇮🇸", # Исландия - "it": "🇮🇹", # Италия - "je": "🇯🇪", # Джерси - "jm": "🇯🇲", # Ямайка - "jo": "🇯🇴", # Иордания - "jp": "🇯🇵", # Япония - "ke": "🇰🇪", # Кения - "kg": "🇰🇬", # Киргизия - "kh": "🇰🇭", # Камбоджа - "ki": "🇰🇮", # Кирибати - "km": "🇰🇲", # Коморы - "kn": "🇰🇳", # Сент-Китс и Невис - "kp": "🇰🇵", # Корейская Народно-Демократическая Республика - "kr": "🇰🇷", # Республика Корея - "kw": "🇰🇼", # Кувейт - "ky": "🇰🇾", # Каймановы острова - "kz": "🇰🇿", # Казахстан - "la": "🇱🇦", # Лаос - "lb": "🇱🇧", # Ливан - "lc": "🇱🇨", # Сент-Люсия - "li": "🇱🇮", # Лихтенштейн - "lk": "🇱🇰", # Шри-Ланка - "lr": "🇱🇷", # Либерия - "ls": "🇱🇸", # Лесото - "lt": "🇱🇹", # Литва - "lu": "🇱🇺", # Люксембург - "lv": "🇱🇻", # Латвия - "ly": "🇱🇾", # Ливия - "my": "🇲🇾", - "md": "🇲🇩", - "mv": "🇲🇻", - "mw": "🇲🇼", - "mx": "🇲🇽", - "my": "🇲🇾", - "mz": "🇲🇿", - "na": "🇳🇦", - "nc": "🇳🇨", - "ne": "🇳🇪", - "nf": "🇳🇫", - "ng": "🇳🇬", - "ni": "🇳🇮", - "nl": "🇳🇱", - "no": "🇳🇴", - "np": "🇳🇵", - "nr": "🇳🇷", - "nu": "🇳🇺", - "nz": "🇳🇿", - "om": "🇴🇲", - "pa": "🇵🇦", - "pe": "🇵🇪", - "pf": "🇵🇫", - "pg": "🇵🇬", - "ph": "🇵🇭", - "pk": "🇵🇰", - "pl": "🇵🇱", - "pm": "🇵🇲", - "pn": "🇵🇳", - "pr": "🇵🇷", - "ps": "🇵🇸", - "pt": "🇵🇹", - "pw": "🇵🇼", - "py": "🇵🇾", - "qa": "🇶🇦", - "re": "🇷🇪", - "ro": "🇷🇴", - "rs": "🇷🇸", - "ru": "🇷🇺", - "rw": "🇷🇼", - "sa": "🇸🇦", - "sb": "🇸🇧", - "sc": "🇸🇨", - "sd": "🇸🇩", - "se": "🇸🇪", - "sg": "🇸🇬", - "sh": "🇸🇭", - "si": "🇸🇮", - "sj": "🇸🇯", - "sk": "🇸🇰", - "sl": "🇸🇱", - "sm": "🇸🇲", - "sn": "🇸🇳", - "so": "🇸🇴", - "sr": "🇸🇷", - "ss": "🇸🇸", - "st": "🇸🇹", - "sv": "🇸🇻", - "sx": "🇸🇽", - "sy": "🇸🇾", - "sz": "🇸🇿", - "tc": "🇹🇨", - "td": "🇹🇩", - "tf": "🇹🇫", - "tg": "🇹🇬", - "th": "🇹🇭", - "tj": "🇹🇯", - "tk": "🇹🇰", - "tl": "🇹🇱", - "tm": "🇹🇲", - "tn": "🇹🇳", - "to": "🇹🇴", - "tr": "🇹🇷", - "tt": "🇹🇹", - "tv": "🇹🇻", - "tw": "🇹🇼", - "tz": "🇹🇿", - "ua": "🇺🇦", - "ug": "🇺🇬", - "um": "🇺🇲", - "us": "🇺🇸", - "va": "🇻🇦", - "vc": "🇻🇨", - "ve": "🇻🇪", - "vg": "🇻🇬", - "vi": "🇻🇮", - "vn": "🇻🇳", - "vu": "🇻🇺", - "wf": "🇼🇫", - "ws": "🇼🇸", - "xk": "🇽🇰", - "ye": "🇾🇪", - "yt": "🇾🇹", - "za": "🇿🇦", - "zm": "🇿🇲", - "zw": "🇿🇼", -} - - class Error(enum.Enum): critical = 500 not_found = 404 @@ -381,7 +146,9 @@ class HostAPI(API): ) -> Dict: route = f"{self._url}/{tg_id}/logs/{lines}" return await self._request(route, method="GET", headers=self.auth_header) - + +def get_flag(country_code: str = None) -> str: + return ''.join( chr(ord(c.upper()) + 127397) for c in country_code) @loader.tds class HHMod(loader.Module): @@ -389,16 +156,19 @@ class HHMod(loader.Module): strings = { "name": "HH", - "info": ( + + "_cfg_doc_hinfo_message": "Custom message text in hinfo. May contain keywords: {id}, {status}, {server}, {days_end}, {cpu_percent}, {ram_usage}, {warns}.", + "_cfg_doc_hinfo_banner_url": "Link to banner image or None.", + + "default_info": ( "👤 Info for {id}\n\n" "📶 Status: {status}\n" "⚙️ Server: {server}\n" "❤️ The subscription expires after {days_end} days\n" - "{stats}\n" + "💾 Used now: {cpu_percent}% CPU, {ram_usage}MB RAM\n\n" "{warns}" ), "logs": "📄 All docker container logs from the userbot\n\nIn t.me/hikkahost_bot/hhapp logs more readable", - "stats": "💾 Used now: {cpu_percent}% CPU, {memory}MB RAM\n", "loading_info": "⌛️ Loading...", "no_apikey": "🚫 Not specified API Key, need get token:\n\n1. Go to the @hikkahost_bot\n2. Send /token\n3. Paste token to .config HH", "warn_sub_left": "🚫 There are less than 5 days left until the end of the subscription\n", @@ -413,16 +183,15 @@ class HHMod(loader.Module): strings_ru = { "name": "HH", - "info": ( - "👤 Информация о {id}\n\n" - "📶 Статус: {status}\n" - "⚙️ Сервер: {server}\n" - "❤️ Подписка истечёт через {days_end} дней\n" - "{stats}\n" - "{warns}" - ), + # "default_info": ( + # "👤 Информация о {id}\n\n" + # "📶 Статус: {status}\n" + # "⚙️ Сервер: {server}\n" + # "❤️ Подписка истечёт через {days_end} дней\n" + # "💾 Используется: {cpu_percent}% CPU, {ram_usage}MB RAM\n\n" + # "{warns}" + # ), "logs": "📄 Все логи docker контейнера от hikka\n\nВ t.me/hikkahost_bot/hhapp логи более читабельны", - "stats": "💾 Используется: {cpu_percent}% CPU, {memory}MB RAM\n", "loading_info": "⌛️ Загрузка...", "no_apikey": "🚫 Не задан ключ API, нужно получить токен:\n\n1. Зайдите в @hikkahost_bot\n2. Отправьте /token\n3. Запишите токен в .config HH", "warn_sub_left": "🚫 Менее чем через 5 дней подписка истечёт\n", @@ -443,6 +212,16 @@ class HHMod(loader.Module): None, validator=loader.validators.Hidden(), ), + loader.ConfigValue( + "hinfo_message", + self.strings["default_info"], + self.strings['_cfg_doc_hinfo_message'] + ), + loader.ConfigValue( + "hinfo_banner_url", + "https://github.com/hikkahost/.github/blob/main/banners/main.jpg?raw=true", + self.strings['_cfg_doc_hinfo_banner_url'] + ), ) async def client_ready(self, client, db): @@ -459,7 +238,7 @@ class HHMod(loader.Module): self._db = db self.me = await client.get_me() self.bot = "@hikkahost_bot" - + @loader.command( en_doc=" - ub status", ) @@ -485,19 +264,17 @@ class HHMod(loader.Module): stats = (await api.get_stats(user_id))["stats"] working = True if status["status"] == "running" else False + load = {} + if working: cpu_stats = stats["cpu_stats"] cpu_total_usage = cpu_stats['cpu_usage']['total_usage'] system_cpu_usage = cpu_stats['system_cpu_usage'] - ram_usage = round(stats["memory_stats"]["usage"] / (1024 * 1024), 2) - cpu_percent = round((cpu_total_usage / system_cpu_usage) * 100.0, 2) - - stats = self.strings["stats"].format( - cpu_percent=cpu_percent, memory=ram_usage - ) - else: - stats = "" + load = { + "ram_usage": round(stats["memory_stats"]["usage"] / (1024 * 1024), 2), + "cpu_percent": round((cpu_total_usage / system_cpu_usage) * 100.0, 2) + } end_date = host.end_date.replace(tzinfo=timezone.utc) warns = "" @@ -510,20 +287,22 @@ class HHMod(loader.Module): server = servers_dict.get(host.server_id) server = self.strings["server"].format( - flag=FLAGS[server["country_code"]], + flag=get_flag(server["country_code"]), name=server["name"], ) await utils.answer( message, - self.strings["info"].format( + self.config["hinfo_message"].format( id=user_id, warns=warns, - stats=stats, server=server, days_end=days_end, status=self.strings["statuses"][status["status"]], + ram_usage=load.get("ram_usage", "0.00"), + cpu_percent=load.get("cpu_percent", "0.00") ), + file=self.config['hinfo_banner_url'] ) @loader.command(