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 ( (Это может занять до 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"?emoji[^>]*>", "", 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"?emoji[^>]*>", "", 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):
"""{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):
"""[{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'
' 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"{utils.escape_html(m.group(2).strip())}[\s\S]*?
)
", "").replace("
", "\n").strip() + + def _format_response_with_smart_separation(self, text): + parts = re.split(r"({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"({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"
{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.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.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"{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": """
+{all_chats}
+
+{users}
+{bots}
+{groups}
+{channels}
+{archived}
+{blocked}
+ Ͱ{blocked_users}
+ Ͱ{blocked_bots}""",
+ "loading_stats": "{all_chats}
+
+{users}
+{bots}
+{groups}
+{channels}
+{archived}
+{blocked}
+ Ͱ{blocked_users}
+ Ͱ{blocked_bots}""",
+ "loading_stats": "{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": "{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: "{id}"
+ chat_id: "{id}"
+ user_id: "{id}"
+ user_not_participant: "" + not_an_admin: "📜 {name} Rights in this chat:\n\n{rights}\n\n🔼 Promoted by: {promoter_name} [{promoter_id}]
{title} ({count}):\n"
+ no_admins_in_chat: "" + no_bots_in_chat: "🤖 Bots ({count}):\n{bots}
" + no_user_in_chat: "👤 Users ({count}):\n{users}
{id}] has been banned for{time_info}."
+ user_is_unbanned: "{id}] has been unbanned."
+ user_is_kicked: "{name} [{id}] has been kicked."
+ user_is_muted: "{id}] has been muted for{time_info}."
+ reason: "Reason: {reason}"
+ forever: "ever"
+ user_is_unmuted: "{id}] has been unmuted."
+ title_changed: "The {type_of} title was successfully changed from {old_title} to {new_title}."
+ channel_created: "{title} is created.\n{title} is created.\n" + dnd: "👑 The creator is {name}\n\nAdmins ({admins_count}):\n{admins}
\n🔒 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#️⃣ 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}
" + monday: "Monday" + tuesday: "Tuesday" + wednesday: "Wednesday" + thursday: "Thursday" + friday: "Friday" + saturday: "Saturday" + sunday: "Sunday" + owns: "🚨 Common chats count: {common_chats_count}\n👥 Common chats: {common_chats}
{owns}" + close: "❌ Close" + apply: "✅ Apply" +ru: + my_id: "
{id}"
+ chat_id: "{id}"
+ user_id: "{id}"
+ user_not_participant: "" + not_an_admin: "📜 {name} Права в этом чате:\n\n{rights}\n\n🔼 Повысил: {promoter_name} [{promoter_id}]
{title} ({count}):\n"
+ no_admins_in_chat: "" + no_bots_in_chat: "🤖 Боты ({count}):\n{bots}
" + no_user_in_chat: "👤 Пользователи ({count}):\n{users}
{id}] забанен на{time_info}."
+ user_is_unbanned: "{id}] разбанен."
+ user_is_kicked: "{name} [{id}] кикнут."
+ user_is_muted: "{id}] замьючен на{time_info}."
+ reason: "Причина: {reason}"
+ forever: "всегда"
+ user_is_unmuted: "{id}] размьючен."
+ title_changed: "{type_of} название успешно изменено с {old_title} на {new_title}."
+ channel_created: "{title} создан.\n{title} создана.\n" + dnd: "👑 Создатель: {name}\n\nАдмины ({admins_count}):\n{admins}
\n🔒 Тип: {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#️⃣ 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}
" + monday: "Понедельник" + tuesday: "Вторник" + wednesday: "Среда" + thursday: "Четверг" + friday: "Пятница" + saturday: "Суббота" + sunday: "Воскресенье" + owns: "🚨 Количество общих чатов: {common_chats_count}\n👥 Общие чаты: {common_chats}
{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": "{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):
+ """{id}\n\n"
"{days_end} days\n"
- "{stats}\n"
+ "{cpu_percent}% CPU, {ram_usage}MB RAM\n\n"
"{warns}"
),
"logs": "{cpu_percent}% CPU, {memory}MB RAM\n",
"loading_info": "{id}\n\n"
- "{days_end} дней\n"
- "{stats}\n"
- "{warns}"
- ),
+ # "default_info": (
+ # "{id}\n\n"
+ # "{days_end} дней\n"
+ # "{cpu_percent}% CPU, {ram_usage}MB RAM\n\n"
+ # "{warns}"
+ # ),
"logs": "{cpu_percent}% CPU, {memory}MB RAM\n",
"loading_info": "