From 17ae450f8fc89ca29fad11c5bdfe66a998fa479e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 12 Apr 2026 13:56:57 +0000 Subject: [PATCH 1/4] Added and updated repositories 2026-04-12 13:56:57 --- KorenbZla/HikkaModules/InvalidFiles.py | 326 +++ SenkoGuardian/SenModules/ChatCopy.py | 1952 ++++++++++++++++++ SenkoGuardian/SenModules/Gemini.py | 668 ++++-- archquise/H.Modules/soundcloud.py | 875 ++++++++ coddrago/modules/YaMusic.py | 106 +- coddrago/modules/translations/yamusic.yml | 116 ++ fiksofficial/python-modules/PyInstall.py | 109 + fiksofficial/python-modules/full.txt | 4 +- fiksofficial/python-modules/github.py | 89 +- fiksofficial/python-modules/placeholders+.py | 650 ++++++ fiksofficial/python-modules/stream.py | 436 +++- mead0wsss/mead0wsMods/SenderGifts.py | 82 +- mead0wsss/mead0wsMods/gifts.json | 62 + radiocycle/Modules/PicToStories.py | 12 +- radiocycle/Modules/RandomAnimePic.py | 181 ++ radiocycle/Modules/SpotifyMod.py | 1303 +++++++----- radiocycle/Modules/full.txt | 2 +- radiocycle/Modules/randomanimepic.py | 65 - trololo65/Modules/TTsaveMod.py | 224 +- 19 files changed, 6309 insertions(+), 953 deletions(-) create mode 100644 KorenbZla/HikkaModules/InvalidFiles.py create mode 100644 SenkoGuardian/SenModules/ChatCopy.py create mode 100644 archquise/H.Modules/soundcloud.py create mode 100644 coddrago/modules/translations/yamusic.yml create mode 100644 fiksofficial/python-modules/PyInstall.py create mode 100644 fiksofficial/python-modules/placeholders+.py create mode 100644 mead0wsss/mead0wsMods/gifts.json create mode 100644 radiocycle/Modules/RandomAnimePic.py delete mode 100644 radiocycle/Modules/randomanimepic.py diff --git a/KorenbZla/HikkaModules/InvalidFiles.py b/KorenbZla/HikkaModules/InvalidFiles.py new file mode 100644 index 0000000..d4c82da --- /dev/null +++ b/KorenbZla/HikkaModules/InvalidFiles.py @@ -0,0 +1,326 @@ +# * _ __ __ _ _ +# * / \ _ _ _ __ ___ _ __ __ _| \/ | ___ __| |_ _| | ___ ___ +# * / _ \| | | | '__/ _ \| '__/ _` | |\/| |/ _ \ / _` | | | | |/ _ \/ __| +# * / ___ \ |_| | | | (_) | | | (_| | | | | (_) | (_| | |_| | | __/\__ \ +# * /_/ \_\__,_|_| \___/|_| \__,_|_| |_|\___/ \__,_|\__,_|_|\___||___/ +# * +# * © Copyright 2026 +# * +# * https://t.me/AuroraModules +# * +# * 🔒 Code is licensed under GNU AGPLv3 +# * 🌐 https://www.gnu.org/licenses/agpl-3.0.html +# * ⛔️ You CANNOT edit this file without direct permission from the author. +# * ⛔️ You CANNOT distribute this file if you have modified it without the direct permission of the author. + +# Name: InvalidFiles +# Author: Felix? +# Commands: +# .CreateInvalidFile (cifile) | .FormatFiles (ffiles) +# scope: hikka_only +# meta developer: @AuroraModules + +__version__ = (1, 0, 0) + +import os +import re +import time +from .. import loader, utils # type: ignore +from telethon.tl.types import Message # type: ignore +from telethon.tl.functions.messages import EditMessageRequest # type: ignore +from telethon.tl.types import InputMediaUploadedDocument, DocumentAttributeFilename # type: ignore + +@loader.tds +class InvalidFilesMod(loader.Module): + """Module for creating corrupted (broken) files of any format.""" + + + strings = { + "name": "InvalidFiles", + "invalid_format": " Invalid size format.", + "max_size": " Maximum file size is 2GB", + "file_created": ( + " File successfully created and sent.\n\n" + "
" + "👤 File name: {}\n" + "⚖️ Size: {}{}\n" + "⌛️ Creation: {:.2f} sec.\n" + "📤 Upload: {:.2f} sec." + "
" + ), + "invalid_args": ( + " Invalid arguments\n\n" + "Usage: {prefix}cifile <name> <size>\n" + "Example: {prefix}cifile test.txt 3.4mb\n\n" + "Supported: b, kb, mb, gb" + ), + "creating": "⌛️ Creating file...\n\n*Large files may take a long time to upload.", + "error": "⚠️ Error:\n{}", + "formats": ( + "📂 Popular file extensions:\n\n" + "📄 Documents: .txt .docx .pdf .rtf\n" + "📊 Spreadsheets: .xlsx .csv\n" + "📈 Presentations: .pptx\n" + "🖼️ Images: .jpg .png .gif .bmp .webp\n" + "🎵 Audio: .mp3 .wav .flac\n" + "🎬 Video: .mp4 .mkv .avi\n" + "📦 Archives: .zip .rar .7z\n" + "💻 Code: .py .js .html .css .json" + ), + } + + strings_ru = { + "invalid_format": " Неверный формат размера.", + "max_size": " Максимальный размер файла — 2GB", + "file_created": ( + " Файл успешно создан и отправлен.\n\n" + "
" + "👤 Имя файла: {}\n" + "⚖️ Размер: {}{}\n" + "⌛️ Создание: {:.2f} сек.\n" + "📤 Отправка: {:.2f} сек." + "
" + ), + "invalid_args": ( + " Неверные аргументы\n\n" + "Использование: {prefix}cifile <имя> <размер>\n" + "Пример: {prefix}cifile test.txt 3.4mb\n\n" + "Поддерживаются: b, kb, mb, gb" + ), + "creating": "⌛️ Создаю файл...\n\n*Файлы большого размера могут долго загружаться.", + "error": "⚠️ Ошибка:\n{}", + "formats": ( + "📂 Популярные расширения файлов:\n\n" + "📄 Документы: .txt .docx .pdf .rtf\n" + "📊 Таблицы: .xlsx .csv\n" + "📈 Презентации: .pptx\n" + "🖼️ Изображения: .jpg .png .gif .bmp .webp\n" + "🎵 Аудио: .mp3 .wav .flac\n" + "🎬 Видео: .mp4 .mkv .avi\n" + "📦 Архивы: .zip .rar .7z\n" + "💻 Код: .py .js .html .css .json" + ), + } + + strings_uz = { + "invalid_format": " Hajm formati noto‘g‘ri.", + "max_size": " Maksimal fayl hajmi — 2GB", + "file_created": ( + " Fayl muvaffaqiyatli yaratildi va yuborildi.\n\n" + "
" + "👤 Fayl nomi: {}\n" + "⚖️ Hajmi: {}{}\n" + "⌛️ Yaratish: {:.2f} sek.\n" + "📤 Yuborish: {:.2f} sek." + "
" + ), + "invalid_args": ( + " Noto‘g‘ri argumentlar\n\n" + "Foydalanish: {prefix}cifile <nom> <hajm>\n" + "Misol: {prefix}cifile test.txt 3.4mb\n\n" + "Qo‘llab-quvvatlanadi: b, kb, mb, gb" + ), + "creating": "⌛️ Fayl yaratilmoqda...\n\n*Katta fayllar uzoq yuklanishi mumkin.", + "error": "⚠️ Xatolik:\n{}", + "formats": ( + "📂 Mashhur fayl kengaytmalari:\n\n" + "📄 Hujjatlar: .txt .docx .pdf .rtf\n" + "📊 Jadvallar: .xlsx .csv\n" + "📈 Taqdimotlar: .pptx\n" + "🖼️ Rasmlar: .jpg .png .gif .bmp .webp\n" + "🎵 Audio: .mp3 .wav .flac\n" + "🎬 Video: .mp4 .mkv .avi\n" + "📦 Arxivlar: .zip .rar .7z\n" + "💻 Kod: .py .js .html .css .json" + ), + } + + + strings_de = { + "invalid_format": " Ungültiges Größenformat.", + "max_size": " Maximale Dateigröße — 2GB", + "file_created": ( + " Datei erfolgreich erstellt und gesendet.\n\n" + "
" + "👤 Dateiname: {}\n" + "⚖️ Größe: {}{}\n" + "⌛️ Erstellung: {:.2f} Sek.\n" + "📤 Upload: {:.2f} Sek." + "
" + ), + "invalid_args": ( + " Ungültige Argumente\n\n" + "Verwendung: {prefix}cifile <name> <größe>\n" + "Beispiel: {prefix}cifile test.txt 3.4mb\n\n" + "Unterstützt: b, kb, mb, gb" + ), + "creating": "⌛️ Datei wird erstellt...\n\n*Große Dateien können lange zum Hochladen brauchen.", + "error": "⚠️ Fehler:\n{}", + "formats": ( + "📂 Beliebte Dateiendungen:\n\n" + "📄 Dokumente: .txt .docx .pdf .rtf\n" + "📊 Tabellen: .xlsx .csv\n" + "📈 Präsentationen: .pptx\n" + "🖼️ Bilder: .jpg .png .gif .bmp .webp\n" + "🎵 Audio: .mp3 .wav .flac\n" + "🎬 Video: .mp4 .mkv .avi\n" + "📦 Archive: .zip .rar .7z\n" + "💻 Code: .py .js .html .css .json" + ), + } + + + strings_es = { + "invalid_format": " Formato de tamaño inválido.", + "max_size": " El tamaño máximo del archivo es 2GB", + "file_created": ( + " Archivo creado y enviado correctamente.\n\n" + "
" + "👤 Nombre del archivo: {}\n" + "⚖️ Tamaño: {}{}\n" + "⌛️ Creación: {:.2f} seg.\n" + "📤 Subida: {:.2f} seg." + "
" + ), + "invalid_args": ( + " Argumentos inválidos\n\n" + "Uso: {prefix}cifile <nombre> <tamaño>\n" + "Ejemplo: {prefix}cifile test.txt 3.4mb\n\n" + "Soportado: b, kb, mb, gb" + ), + "creating": "⌛️ Creando archivo...\n\n*Los archivos grandes pueden tardar en subirse.", + "error": "⚠️ Error:\n{}", + "formats": ( + "📂 Extensiones de archivo populares:\n\n" + "📄 Documentos: .txt .docx .pdf .rtf\n" + "📊 Hojas de cálculo: .xlsx .csv\n" + "📈 Presentaciones: .pptx\n" + "🖼️ Imágenes: .jpg .png .gif .bmp .webp\n" + "🎵 Audio: .mp3 .wav .flac\n" + "🎬 Video: .mp4 .mkv .avi\n" + "📦 Archivos: .zip .rar .7z\n" + "💻 Código: .py .js .html .css .json" + ), + } + + async def create_invalid_file(self, filename: str, size_str: str): + match = re.fullmatch(r"(\d+(?:\.\d+)?)(b|kb|mb|gb)", size_str.lower()) + + if not match: + return False, self.strings["invalid_format"] + + multiplier = { + "b": 1, + "kb": 1024, + "mb": 1024 ** 2, + "gb": 1024 ** 3, + } + + size_value = float(match.group(1)) + unit = match.group(2) + total_bytes = int(size_value * multiplier[unit]) + + if total_bytes > 2 * 1024 ** 3: + return False, self.strings["max_size"] + + start_time = time.time() + + try: + with open(filename, "wb") as f: + remaining = total_bytes + chunk = 5 * 1024 * 1024 + + while remaining > 0: + write_size = min(chunk, remaining) + f.write(os.urandom(write_size)) + remaining -= write_size + + except Exception as e: + return False, self.strings["error"].format(e) + + elapsed = time.time() - start_time + + return True, (filename, size_value, unit, elapsed) + + @loader.command( + ru_doc="<имя>.<формат> <размер> — создать битый файл", + uz_doc=". — buzilgan fayl yaratish", + de_doc=". — beschädigte Datei erstellen", + es_doc=". — crear archivo corrupto", + alias="cifile" + ) + async def CreateInvalidFile(self, message: Message): + """. - create corrupted file""" + + args = utils.get_args_raw(message).split() + + if len(args) != 2: + await utils.answer( + message, + self.strings("invalid_args").format(prefix=self.get_prefix()) + ) + return + + filename, size_str = args + + status = await utils.answer(message, self.strings("creating")) + + success, data = await self.create_invalid_file(filename, size_str) + + if not success: + await utils.answer(status, data) + return + + filename, size_value, unit, create_time = data + + try: + start_upload = time.time() + + uploaded = await self.client.upload_file(filename) + + upload_time = time.time() - start_upload + + media = InputMediaUploadedDocument( + file=uploaded, + mime_type="application/octet-stream", + attributes=[DocumentAttributeFilename(file_name=filename)] + ) + + await self.client(EditMessageRequest( + peer=message.chat_id, + id=status.id, + message="", + media=media + )) + + await utils.answer( + status, + self.strings["file_created"].format( + filename, + size_value, + unit, + create_time, + upload_time + ) + ) + + except Exception as e: + await utils.answer(status, self.strings["error"].format(e)) + + finally: + if os.path.exists(filename): + try: + os.remove(filename) + except Exception: + pass + + @loader.command( + ru_doc="— показать список популярных форматов(расширейний) файлов", + uz_doc="— mashhur fayl formatlari (kengaytmalari) ro'yxatini ko'rsatish", + de_doc="— eine Liste gängiger Dateiformate (Erweiterungen) anzeigen", + es_doc="— mostrar una lista de formatos de archivo (extensiones) populares", + alias="ffiles" + ) + async def FormatFiles(self, message: Message): + """— show a list of popular file formats (extensions)""" + await utils.answer(message, self.strings('formats')) diff --git a/SenkoGuardian/SenModules/ChatCopy.py b/SenkoGuardian/SenModules/ChatCopy.py new file mode 100644 index 0000000..9cf0713 --- /dev/null +++ b/SenkoGuardian/SenModules/ChatCopy.py @@ -0,0 +1,1952 @@ +# This file is part of SenkoGuardianModules +# Copyright (c) 2025-2026 Senko +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# scope heroku_min: 2.0.0 +# meta banner: https://raw.githubusercontent.com/SenkoGuardian/SenkoGuardian.github.io/main/OfficialSenkoGuardianBanner.png +# meta pic: https://raw.githubusercontent.com/SenkoGuardian/SenkoGuardian.github.io/main/OfficialSenkoGuardianBanner.png + +__version__ = ("1", "3", "0") # в этот раз комменты свои добавил что бы было понятно кратко, что да как и где что работает. + +""" ̄へ ̄""" + +# meta developer: @SenkoGuardianModules (from VIP section) + +# .------. .------. .------. .------. .------. .------. +# |S.--. | |E.--. | |N.--. | |M.--. | |O.--. | |D.--. | +# | :/\: | | :/\: | | :(): | | :/\: | | :/\: | | :/\: | +# | :\/: | | :\/: | | ()() | | :\/: | | :\/: | | :\/: | +# | '--'S| | '--'E| | '--'N| | '--'M| | '--'O| | '--'D| +# `------' `------' `------' `------' `------' `------' + + +import asyncio +import logging +import re +import traceback +import random +import time +from datetime import datetime, timedelta, timezone +MSK = timezone(timedelta(hours=3), name="MSK") +from telethon import functions, errors, types +from telethon.tl.types import Message, Channel +from .. import loader, utils + +logger = logging.getLogger(__name__) + +_cc_client = None +_cc_log_channel = None +_cc_log_topic_id = None + +class _CCTopicHandler(logging.Handler): + + def emit(self, record): + if _cc_client is None or _cc_log_channel is None or _cc_log_topic_id is None: + return + try: + text = f"[{record.levelname}] {self.format(record)}" + asyncio.ensure_future( + _cc_client.send_message( + int(f"-100{_cc_log_channel}"), + text, + parse_mode="html", + reply_to=_cc_log_topic_id, + ) + ) + except Exception: + pass + + +_cc_topic_handler = _CCTopicHandler() +_cc_topic_handler.setLevel(logging.INFO) # INFO чтобы видеть прогресс пересылки +logger.addHandler(_cc_topic_handler) + +FILTER_ALL = "all" +FILTER_MEDIA = "media" +FILTER_PHOTO_VIDEO = "photo_video" +FILTER_DOCS = "docs" +FILTER_TEXT = "text" +FILTER_NO_AD = "no_ad" + +@loader.tds +class ChatCopy(loader.Module): + """Модуль для копирования чатов с поддержкой топиков (форумов), фото, видео, файлов (документов).""" + strings = { + "name": "ChatCopy", + "cfg_batch": "Размер пачки сообщений (1-100)", + "cfg_delay": "Задержка ОТПРАВКИ между пачками (сек)", + "cfg_flood_buffer": "Дополнительное время к FloodWait (сек)", + "copy_start_prem": ( + '🚀 ChatCopy: Запуск копирования\n\n' + "Источник: {src}\n" + '\n' + "Цель: {dest}\n\n" + '⚙️ Режим: {mode}\n' + '🔢 Старт с ID: {start_id}\n' + '👤 Без автора: {no_auth}\n' + '💬 Без подписей: {no_capt}\n' + '📎 Фильтр: {filter_type}\n' + '📦 Всего сообщений: {total_msgs}\n' + '⏱ Оценка времени: {estimated_time}\n\n' + "Задача добавлена в очередь. Позиция: {position}" + ), + "copy_start_no_prem": ( + "🚀 ChatCopy: Запуск копирования\n\n" + "Источник: {src}\n" + "⏬⏬⏬⏬\n" + "Цель: {dest}\n\n" + "⚙️ Режим: {mode}\n" + "🔢 Старт с ID: {start_id}\n" + "👤 Без автора: {no_auth}\n" + "💬 Без подписей: {no_capt}\n" + "📎 Фильтр: {filter_type}\n" + "📦 Всего сообщений: {total_msgs}\n" + "⏱ Оценка времени: {estimated_time}\n\n" + "Задача добавлена в очередь. Позиция: {position}" + ), + "copy_done_detailed_prem": ( + ' Задача выполнена\n' + "
{src} → {dest}\n" + "Без автора: {no_auth}\n" + "Без подписей: {no_capt}\n" + "Старт с ID: {start_id}\n" + "Режим: {mode}\n" + "Фильтр: {filter_type}
\n" + ' Перенесено сообщений: {count} \n' + '⏱ Длительность: {duration}\n' + '⚡ Средняя скорость: {avg_speed} сообщений/мин' + "{flood_info}" + ), + "copy_done_detailed_no_prem": ( + "Задача выполнена\n" + "
{src} → {dest}\n" + "Без автора: {no_auth}\n" + "Без подписей: {no_capt}\n" + "Старт с ID: {start_id}\n" + "Режим: {mode}\n" + "Фильтр: {filter_type}
\n" + "✔️ Перенесено сообщений: {count} ✔️\n" + "⏱ Длительность: {duration}\n" + "⚡ Средняя скорость: {avg_speed} сообщений/мин" + "{flood_info}" + ), + "flood_wait_notice": ( + "⏸ FloodWait\n" + "📊 Задержка: {minutes}m {seconds}s\n" + "🕐 Возобновление: {resume_time}\n" + "📨 Переслано: {count} сообщений\n" + "⏳ Осталось: {remaining} сообщений\n" + "⚡ Скорость: {speed} сообщений/мин" + ), + "panel_summary": "📊 ChatCopy Status\n\n🔄 Активная: {active}\n⏳ В очереди: {queue_len}\n👀 Слежка: {watching_count}\n⏱ Последний FW: {last_flood}", + "panel_task_running": "{name}\n├ 📦 {count}/{total} сообщений\n├ ⚡ {speed}/мин | 📊 {progress}%\n├ ⏱ Прошло: {elapsed} | Осталось: {eta}\n└ 🕐 Начало: {start_time} | Окончание: {end_time}", + "panel_task_paused": "{name}\n├ ⏸ На паузе (FW: {flood_time})\n├ 📦 {count}/{total} сообщений\n├ ⚡ {speed}/мин\n└ 🕐 Продолжение: {resume_time}", + "btn_stop": "🛑 Стоп", + "btn_pause": "⏸ Пауза", + "btn_resume": "▶️ Продолжить", + "btn_back": "🔙 Назад", + "btn_tasks": "📋 Очередь задач", + "btn_watch": "👀 Слежка", + "btn_settings": "⚙️ Настройки", + "btn_stats": "📊 Статистика", + "forum_enabled": "✅ Топики включены в {chat}", + "forum_enable_failed": "❌ Не удалось включить топики в {chat}. Нужны права администратора.", + "forum_not_channel": "❌ {chat} не является каналом/группой", + "err_ent": "❌ Ошибка: Чат не найден или нет доступа.", + "args_err": "❌ Синтаксис: .chatcopy [start_id:final_id] [-n] [-dmc] [--now] [--media|--photo_video|--docs|--text]", + "watch_added": "👀 Наблюдение активировано\nID: {src_id}\n{src} -> {dest}\nРежим топиков: {topics}\nБез подписей: {no_capt}\nФильтр: {filter_type}", + "queue_wait": "⏳ Задача в очереди... ({pos})", + "topic_created": "📂 Создан топик: {title}", + "topic_error": "❌ Ошибка создания топика: {error}", + "task_stopped": "🛑 Задача остановлена\nПереслано: {count} сообщений{flood_info}", + "stats_title": "📊 Статистика ChatCopy\n\n", + "stats_total": "Всего задач: {total}\nЗавершено: {completed}\nОстановлено: {stopped}\nFloodWait'ов: {floods}", + "task_list_header": "📋 Очередь задач ({total})\n\nНажми на номер для подробностей\n\n", + "task_item_compact_running": "▶️{num}. {src}{dest} ({progress}%)", + "task_item_compact_queued": "⏳{num}. {src}{dest} (через {wait})", + "task_item_compact_paused": "⚠️{num}. {src}{dest} (FW)", + "task_item_compact_completed": "✅{num}. {src}{dest}", + "task_item_compact_error": "❌{num}. {src}{dest}", + "task_detail_running": "▶️ Задача #{num}\n\n{src}{dest}\n├ Статус: Выполняется\n├ Прогресс: {current}/{total} ({progress}%)\n├ Скорость: {speed}/мин\n├ Прошло: {elapsed}\n├ Осталось: {eta_left}\n├ Начато: {start_time}\n├ Окончание: {end_time}\n└ Позиция: {position}", + "task_detail_queued": "⏳ Задача #{num}\n\n{src}{dest}\n├ Статус: В очереди\n├ Позиция: {position}\n├ Сообщений: ~{total}\n├ Ожидание старта: {eta_start}\n└ Примерное время работы: {estimated_duration}", + "task_detail_paused": "⚠️ Задача #{num}\n\n{src}{dest}\n├ Статус: Пауза (FloodWait)\n├ Прогресс: {current}/{total} ({progress}%)\n├ FloodWait'ов: {flood_count}\n├ Время ожидания: {flood_time}\n├ Продолжение: {resume_time}\n├ Скорость до паузы: {speed}/мин\n└ Осталось сообщений: {remaining}", + "task_detail_completed": "✅ Задача #{num}\n\n{src}{dest}\n├ Статус: Завершена\n├ Переслано: {count} сообщений\n├ Длительность: {duration}\n├ Средняя скорость: {avg_speed}/мин\n├ Завершено: {end_time}\n└ FloodWait'ов: {floods}", + "task_detail_error": "❌ Задача #{num}\n\n{src}{dest}\n├ Статус: Ошибка\n└ Попробуйте перезапустить", + "no_tasks": "Нет активных задач", + "preparing_prem": "💫 Подготовка к копированию. Подсчитываем (да, вручную!) кол-во медиа, это может занять время...", + "preparing_no_prem": "⌛️ Подготовка к копированию. Подсчитываем кол-во медиа, это может занять время...", + } + + def __init__(self): + self._tasks = [] + self.config = loader.ModuleConfig( + loader.ConfigValue("batch_size", 100, lambda: self.strings["cfg_batch"], validator=loader.validators.Integer(minimum=1, maximum=100)), + loader.ConfigValue("delay", 10, lambda: self.strings["cfg_delay"], validator=loader.validators.Integer(minimum=1)), + loader.ConfigValue("flood_buffer", 5, lambda: self.strings["cfg_flood_buffer"], validator=loader.validators.Integer(minimum=0, maximum=60)), + ) + self.queue = asyncio.Queue() + self.dump_queue = asyncio.Queue() + self.watcher_buffer = {} + self.watcher_flush_tasks = {} + self.watchlist = {} + self.active_dumps = {} + self.last_watched = {} + self.last_processed_ids = {} + self.current_dump_task = None + self.is_premium = False + self.topic_mapping = {} + self.topic_info_cache = {} + self.task_stats = {} + self.last_flood_info = {"time": None, "duration": 0, "task": None, "resume_at": None} + self.task_queue = [] + self.task_history = [] + self.current_task_index = 0 + self.is_processing_queue = False + self.task_progress_cache = {} + self.global_speed_history = [] + self.avg_speed_history = [] + self._queue_lock = asyncio.Lock() + self._task_counter = 0 + + async def client_ready(self, client, db): + global _cc_client, _cc_log_channel, _cc_log_topic_id + self.client = client + self.db = db + self.watchlist = self.db.get("ChatCopy", "watchlist", {}) + self.last_processed_ids = self.db.get("ChatCopy", "last_processed_ids", {}) + self.topic_mapping = self.db.get("ChatCopy", "topic_mapping", {}) + self.task_stats = self.db.get("ChatCopy", "task_stats", {}) + self.task_queue = self.db.get("ChatCopy", "persistent_queue", []) + for task in self.task_queue: + task['status'] = 'queued' + me = await client.get_me() + self.is_premium = getattr(me, 'premium', False) + try: + asset_channel = self._db.get("heroku.forums", "channel_id", 0) + if asset_channel: + notif_topic = await utils.asset_forum_topic( + self._client, + self._db, + asset_channel, + "ChatCopy Logs", + description="ChatCopy module activity logs (warnings & errors).", + icon_emoji_id=5372917041193828849, + ) + _cc_client = self._client + _cc_log_channel = asset_channel + _cc_log_topic_id = notif_topic.id + logger.info("ChatCopy log topic ready (id=%s)", _cc_log_topic_id) + except Exception as _e: + logger.debug("ChatCopy log topic setup skipped: %s", _e) + self._tasks.extend([ + asyncio.create_task(self.worker()), + asyncio.create_task(self.dump_worker()), + asyncio.create_task(self._catch_up_on_restart()) + ]) + if not self.task_queue: + return + logger.info(f"Возобновление {len(self.task_queue)} задач из очереди после перезапуска.") + for task in self.task_queue: + try: + src = await self.client.get_entity(task['src_id']) + dest = await self.client.get_entity(task['dest_id']) + class FakeMsg: + id = None + chat_id = task.get('status_chat_id') + async def edit(self, *args, **kwargs): pass + await self.dump_queue.put({ + "status_msg": FakeMsg(), + "src": src, "dest": dest, + "no_auth": task['no_author'], "no_captions": task['no_captions'], + "map_t": task.get('map_t', False), "f_src_t": task.get('f_src_t'), + "f_dest_t": task.get('f_dest_t'), "tid": task['tid'], + "min_id": task.get('last_processed_id', task.get('start_id', 0)), + "max_id": task.get('final_id', 0), + "filter_type": task['filter_type'], "src_name": task['src'], + "total_msgs": task['total_msgs'], + "restored_count": task.get('current', 0), + }) + except Exception as e: + logger.error(f"Не удалось возобновить задачу {task.get('tid')}: {e}") + + async def _resolve_arg(self, arg): # все виды (ну почти) ссылок как дадут id и прочее, + # работает если копировать сообщение в топике и в аргумент типа куда отправлять вставить. + extra = {} + entity = None + arg = str(arg).strip() + regex = r"(?:t\.me/|tg://resolve\?domain=|tg://openmessage\?user_id=)(?:c/)?([\w\d_]+)(?:/(\d+))?(?:/(\d+))?" + match = re.search(regex, arg) + if match: + identifier = match.group(1) + if match.group(2): extra['topic'] = int(match.group(2)) + if match.group(3): extra['msg'] = int(match.group(3)) + if identifier.isdigit(): + for potential_id in [int(identifier), int(f"-100{identifier}")]: + try: + entity = await self.client.get_entity(potential_id) + if entity: break + except: continue + else: + try: entity = await self.client.get_entity(identifier) + except: pass + else: + try: + if arg.lstrip("-").isdigit(): entity = await self.client.get_entity(int(arg)) + else: entity = await self.client.get_entity(arg) + except: pass + return entity, extra + + def _get_normalized_id(self, entity): # что бы получать норм айди а не нечто, что бы копировка шла хорошо. + if not entity: + return "0" + try: + return str(utils.get_chat_id(entity)) + except Exception: + if hasattr(entity, 'id') and entity.id: + eid = str(entity.id) + if not eid.startswith("-100") and len(eid) > 9: + return f"-100{eid}" + if not eid.startswith("-"): + return f"-100{eid}" + return eid + return "0" + + def _is_forum(self, entity): # да, не спрашивайте. + if not isinstance(entity, Channel): + return False + if hasattr(entity, 'forum') and entity.forum: + return True + if hasattr(entity, 'flags') and entity.flags is not None: + return bool(entity.flags & (1 << 30)) + return False + + async def _ensure_forum_enabled(self, entity): # проверяет режим топиков у чата и пытается включить его, если он отключен (требуются права админа). + if not isinstance(entity, Channel): + return False + if self._is_forum(entity): + return True + try: + result = await self.client(functions.channels.ToggleForumRequest(channel=entity, enabled=True)) + if result: + updated_entity = await self.client.get_entity(entity.id) + return self._is_forum(updated_entity) + return False + except errors.FloodWaitError as e: + await asyncio.sleep(e.seconds + self.config["flood_buffer"]) + return await self._ensure_forum_enabled(entity) + except errors.ChatAdminRequiredError: + return False + except Exception: + return False + + async def _get_topic_info(self, chat_entity, topic_id): #получаем инфо о топике для копирования + if not topic_id: + return None, None, None + cache_key = f"{chat_entity.id}_{topic_id}" + if cache_key in self.topic_info_cache: + return self.topic_info_cache[cache_key] + title, icon_emoji_id, icon_color = None, None, None + for attempt in range(3): + try: + result = await self.client(functions.messages.GetForumTopicsByIDRequest(peer=chat_entity, topics=[topic_id])) + if result and hasattr(result, 'topics') and result.topics: + for topic in result.topics: + if hasattr(topic, 'id') and topic.id == topic_id: + title = getattr(topic, 'title', None) + icon_emoji_id = getattr(topic, 'icon_emoji_id', None) + icon_color = getattr(topic, 'icon_color', None) + break + if title: + break + except errors.FloodWaitError as e: + await asyncio.sleep(e.seconds + self.config["flood_buffer"]) + except Exception: + pass + if not title: + try: + result = await self.client(functions.messages.GetForumTopicsRequest(peer=chat_entity, offset_date=0, offset_id=0, offset_topic=0, limit=100)) + if result and hasattr(result, 'topics'): + for topic in result.topics: + if hasattr(topic, 'id') and topic.id == topic_id: + title = getattr(topic, 'title', None) + icon_emoji_id = getattr(topic, 'icon_emoji_id', None) + icon_color = getattr(topic, 'icon_color', None) + break + except Exception: + pass + if not title: + try: + async for msg in self.client.iter_messages(chat_entity, limit=1, reply_to=topic_id): + if msg and hasattr(msg, 'reply_to') and msg.reply_to: + title = getattr(msg.reply_to, 'forum_topic_title', None) + if not title and msg: + title = msg.text[:50] if msg.text else f"Topic {topic_id}" + break + except Exception: + pass + if not title: + title = f"Topic {topic_id}" + info = (title, icon_emoji_id, icon_color) + self.topic_info_cache[cache_key] = info + return info + + async def _create_topic(self, dest_entity, title, src_topic_id=None, icon_emoji_id=None, icon_color=None): # создает топик + if not isinstance(dest_entity, Channel) or not self._is_forum(dest_entity): + return None + try: + random_id = random.randint(1, 2**63 - 1) + kwargs = { + "peer": dest_entity, + "title": title[:128] if len(title) > 128 else title, + "random_id": random_id + } + if icon_emoji_id: + kwargs["icon_emoji_id"] = icon_emoji_id + elif icon_color: + kwargs["icon_color"] = icon_color + else: + kwargs["icon_color"] = 0x6FB9F0 + result = await self.client(functions.messages.CreateForumTopicRequest(**kwargs)) + new_topic_id = None + if result: + if hasattr(result, 'updates'): + for update in result.updates: + if hasattr(update, 'message'): + msg = update.message + if hasattr(msg, 'action') and hasattr(msg.action, 'topic_id'): + new_topic_id = msg.action.topic_id + if hasattr(msg, 'reply_to') and msg.reply_to: + new_topic_id = getattr(msg.reply_to, 'reply_to_top_id', None) or getattr(msg.reply_to, 'reply_to_msg_id', None) + if new_topic_id: + break + if not new_topic_id and hasattr(result, 'messages') and result.messages: + for msg in result.messages: + if hasattr(msg, 'reply_to') and msg.reply_to: + new_topic_id = getattr(msg.reply_to, 'reply_to_top_id', None) + if new_topic_id: + break + if not new_topic_id: + await asyncio.sleep(1) + topics_result = await self.client(functions.messages.GetForumTopicsRequest(peer=dest_entity, offset_date=0, offset_id=0, offset_topic=0, limit=20)) + if topics_result and hasattr(topics_result, 'topics'): + for topic in topics_result.topics: + if getattr(topic, 'title', '') == title: + new_topic_id = topic.id + break + return new_topic_id + except errors.FloodWaitError as e: + await asyncio.sleep(e.seconds + self.config["flood_buffer"]) + return await self._create_topic(dest_entity, title, src_topic_id, icon_emoji_id, icon_color) + except errors.TopicDeletedError: + return None + except Exception: + return None + + async def _ensure_topic_mapping(self, src_entity, dest_entity, src_topic_id): # копирует точ в точ топик. + if not src_topic_id: + return None + mapping_key = f"{src_entity.id}_{dest_entity.id}_{src_topic_id}" + if mapping_key in self.topic_mapping: + cached_id = self.topic_mapping[mapping_key] + try: + await self.client(functions.messages.GetForumTopicsByIDRequest(peer=dest_entity, topics=[cached_id])) + return cached_id + except Exception: + pass + title, icon_emoji_id, icon_color = await self._get_topic_info(src_entity, src_topic_id) + if not title: + title = f"Topic {src_topic_id}" + try: + offset_date = 0 + offset_id = 0 + offset_topic = 0 + found_topic_id = None + for _ in range(5): + topics_result = await self.client(functions.messages.GetForumTopicsRequest( + peer=dest_entity, offset_date=offset_date, offset_id=offset_id, offset_topic=offset_topic, limit=100 + )) + if not topics_result or not hasattr(topics_result, 'topics') or not topics_result.topics: + break + for topic in topics_result.topics: + if getattr(topic, 'title', '') == title: + if icon_emoji_id: + if getattr(topic, 'icon_emoji_id', None) == icon_emoji_id: + found_topic_id = topic.id + break + else: + found_topic_id = topic.id + break + if found_topic_id: + break + offset_topic = topics_result.topics[-1].id + if found_topic_id: + self.topic_mapping[mapping_key] = found_topic_id + self.db.set("ChatCopy", "topic_mapping", self.topic_mapping) + return found_topic_id + except Exception as e: + logger.error(f"Error checking existing topics: {e}") + for attempt in range(3): + new_topic_id = await self._create_topic(dest_entity, title, src_topic_id, icon_emoji_id, icon_color) + if new_topic_id: + self.topic_mapping[mapping_key] = new_topic_id + self.db.set("ChatCopy", "topic_mapping", self.topic_mapping) + return new_topic_id + await asyncio.sleep(5) + return None + + async def on_unload(self): + """Остановка всех задач при выгрузке модуля""" + for task in self._tasks: + if not task.done(): task.cancel() + for tid in list(self.active_dumps.keys()): + self.active_dumps[tid]["status"] = "stopped" + if "cancel" in self.active_dumps[tid]: self.active_dumps[tid]["cancel"].set() + for t in self.watcher_flush_tasks.values(): t.cancel() + + def _should_include_message(self, msg, filter_type): # handler типов сообщений. медиа, документ и прочее. + if filter_type == FILTER_ALL: + return True + has_photo = bool(msg.photo) + has_video = bool(msg.video) + has_video_note = bool(msg.video_note) + has_document = bool(msg.document) + has_voice = bool(msg.voice) + has_audio = bool(msg.audio) + has_sticker = bool(msg.sticker) + has_text = bool(msg.text and not msg.media) + is_gif = False + if has_document and not has_sticker and hasattr(msg.document, 'attributes'): + for attr in msg.document.attributes: + if isinstance(attr, types.DocumentAttributeAnimated): + is_gif = True + break + is_file_document = has_document and not (has_video or has_video_note or has_audio or has_voice or has_sticker or is_gif or has_photo) + if has_video and has_sticker: + has_video = False + if has_document and not has_photo and not has_sticker: + doc = msg.document + if hasattr(doc, 'mime_type'): + mime = doc.mime_type or '' + if mime.startswith('image/'): + has_photo = True + is_file_document = False + elif mime.startswith('video/') and not is_gif: + has_video = True + is_file_document = False + if filter_type == FILTER_MEDIA: + return has_photo or has_video or is_file_document + elif filter_type == FILTER_PHOTO_VIDEO: + return (has_photo or has_video) and not (has_sticker or is_gif) + elif filter_type == FILTER_DOCS: + return is_file_document + elif filter_type == FILTER_TEXT: + return has_text and not (has_photo or has_video or has_video_note or has_document or has_sticker or has_voice or has_audio or is_gif) + return True + + async def _send_flood_notice(self, chat_id, seconds, count, + task_id, total_msgs=0, speed=0): # ниже этой функции, функция обработки флудвейта, он просто отправляет примерное время когда продолжит работать. + minutes = seconds // 60 + secs = seconds % 60 + resume_time = (datetime.now(MSK) + timedelta(seconds=seconds + self.config["flood_buffer"])).strftime("%H:%M:%S") + remaining = max(0, total_msgs - count) + self.last_flood_info = { + "time": datetime.now(MSK).strftime("%H:%M:%S"), + "duration": seconds, + "task": task_id, + "resume_at": resume_time + } + try: + await self.client.send_message( + chat_id, + self.strings["flood_wait_notice"].format( + minutes=minutes, + seconds=secs, + resume_time=resume_time, + count=count, + remaining=remaining, + speed=round(speed, 1) + ) + ) + except Exception: + pass + + def _format_flood_stats(self, task_data): # формирует красивую строку со статистикой FloodWait для вывода в итоговом сообщении. + floods = task_data.get('flood_count', 0) + total_seconds = task_data.get('flood_total_seconds', 0) + if floods == 0: + return "" + hours = total_seconds // 3600 + minutes = (total_seconds % 3600) // 60 + if hours > 0: + time_str = f"{hours}h {minutes}m" + else: + time_str = f"{minutes}m" + return f"\n⏱ {floods} FloodWait (~{time_str})" + + def _format_duration(self, seconds): # описание ниже + """Форматирует длительность в читаемый вид""" + if seconds < 60: + return f"{int(seconds)}с" + elif seconds < 3600: + return f"{int(seconds // 60)}м {int(seconds % 60)}с" + else: + hours = int(seconds // 3600) + mins = int((seconds % 3600) // 60) + return f"{hours}ч {mins}м" + + async def _process_batch(self, messages, dest_id, no_author, + no_captions=False, fixed_dest_topic=None, map_topics=False, dest_entity=None, + src_entity=None, filter_type=FILTER_ALL, status_msg=None, tid=None): + if not messages: + return 0 + if tid and tid in self.active_dumps: + await self.active_dumps[tid].get("cancel", asyncio.Event()).wait() + if self.active_dumps[tid].get("status") == "stopped": + return 0 + filtered_messages = [msg for msg in messages if self._should_include_message(msg, filter_type)] + if not filtered_messages: + return 0 + if map_topics and (not dest_entity or isinstance(dest_entity, (int, str))): + try: + dest_entity = await self.client.get_entity(dest_id) + except Exception: + map_topics = False + if map_topics and not src_entity: + try: + src_entity = await self.client.get_entity(filtered_messages[0].chat_id) + except Exception: + pass + msg_groups = {} + for msg in filtered_messages: + src_topic_id = None + if map_topics and src_entity and dest_entity: + if hasattr(msg, 'reply_to') and msg.reply_to: + src_topic_id = getattr(msg.reply_to, 'reply_to_top_id', None) or getattr(msg.reply_to, 'reply_to_msg_id', None) + if not src_topic_id and hasattr(msg, 'topic_id') and msg.topic_id: + src_topic_id = msg.topic_id + key = src_topic_id if src_topic_id else "no_topic" + msg_groups.setdefault(key, []).append(msg) + total_sent = 0 + delay = self.config["delay"] + if not isinstance(delay, int): + delay = 10 + for src_topic_id, msgs in msg_groups.items(): + if tid and tid in self.active_dumps: + await self.active_dumps[tid].get("cancel", asyncio.Event()).wait() + if self.active_dumps[tid].get("status") == "stopped": + break + target_topic = fixed_dest_topic + if map_topics and src_topic_id != "no_topic": + target_topic = await self._ensure_topic_mapping(src_entity, dest_entity, src_topic_id) + if not target_topic: + continue + if tid and tid in self.active_dumps: + last_send = self.active_dumps[tid].get("last_successful_send", 0) + time_since_last = time.time() - last_send + min_interval = 3 + if time_since_last < min_interval: + extra_wait = min_interval - time_since_last + logger.debug(f"[{tid}] Дополнительная задержка для соблюдения интервала: {extra_wait:.1f}с") + await asyncio.sleep(extra_wait) + success = await self._raw_sender(msgs, dest_id, no_author, no_captions, target_topic, status_msg, tid) + if success: + total_sent += len(msgs) + if tid and tid in self.active_dumps: + self.active_dumps[tid]["last_successful_send"] = time.time() + await asyncio.sleep(delay) + return total_sent + + async def worker(self): # воркер для Watcher'а + while True: + item = await self.queue.get() + try: + watch_cid = item.get("watch_cid") + if watch_cid and watch_cid not in self.watchlist: + logger.debug(f"Игнорируем сообщение для {watch_cid}, слежка была остановлена") + continue + result = await self._process_batch(**item) + if watch_cid and item.get("messages"): + last_msg = item["messages"][-1] + self.last_processed_ids[watch_cid] = last_msg.id + self.db.set("ChatCopy", "last_processed_ids", self.last_processed_ids) + except Exception as e: + logger.error(f"Worker error: {e}") + finally: + self.queue.task_done() + + async def dump_worker(self): + """worker очереди, с последовательным выполнением задач""" # он типа очень умни и если добавить последовательно несколько чатов, + # то он не переключится а просто в очередь добавит + while True: + task_data = await self.dump_queue.get() + tid = task_data.get('tid') + async with self._queue_lock: + self.is_processing_queue = True + self.current_dump_task = tid + self._update_queue_positions() + if tid in self.task_queue: + idx = next((i for i, t in enumerate(self.task_queue) if t['tid'] == tid), None) + if idx is not None: + self.task_queue[idx]['status'] = 'running' + self.task_queue[idx]['start_time'] = datetime.now(MSK) + self.current_task_index = idx + if tid: + self.active_dumps[tid] = { + "current": 0, + "cancel": asyncio.Event(), + "name": task_data.get('src_name', 'Unknown'), + "status": "running", + "start_time": time.time(), + "flood_count": 0, + "flood_total_seconds": 0, + "status_msg_id": task_data.get('status_msg').id if task_data.get('status_msg') else None, + "status_chat_id": task_data.get('status_msg').chat_id if task_data.get('status_msg') else None, + "total_estimated": task_data.get('total_msgs', 0), + "last_update_time": time.time(), + "last_update_count": 0, + "last_successful_send": time.time(), + "consecutive_floods": 0, + "speed_samples": [], + "current_speed": 0, + } + self.active_dumps[tid]["cancel"].set() + update_task = asyncio.create_task(self._auto_update_status(tid, task_data.get('status_msg'))) + try: + logger.info("[%s] Задача запущена: %s → %s | Всего: %d сообщений", + tid, task_data.get('src_name', '?'), + getattr(task_data.get('dest'), 'title', '?'), + task_data.get('total_msgs', 0)) + await self._history_dumper(**task_data) + except Exception as e: + logger.error(f"Dump Worker Error: {e}") + if tid and tid in self.active_dumps: + self.active_dumps[tid]["status"] = "error" + finally: + update_task.cancel() + if tid in self.active_dumps: + completed_task = self.active_dumps[tid].copy() + completed_task['tid'] = tid + completed_task['end_time'] = datetime.now(MSK) + self.task_history.append(completed_task) + self.task_queue = [t for t in self.task_queue if t['tid'] != tid] + duration = time.time() - completed_task.get('start_time', time.time()) + active_duration = duration - completed_task.get('flood_total_seconds', 0) + if active_duration <= 0: active_duration = 1 + avg_spd = (completed_task.get('current', 0) / active_duration) * 60 + self.task_stats[tid] = { + 'completed_at': time.time() if completed_task.get('status') == 'completed' else None, + 'flood_count': completed_task.get('flood_count', 0), + 'flood_time': completed_task.get('flood_total_seconds', 0), + 'avg_speed': avg_spd + } + self.db.set("ChatCopy", "task_stats", self.task_stats) + logger.info("[%s] Задача завершена. Переслано: %d", + tid, self.active_dumps.get(tid, {}).get('current', 0)) + self.current_dump_task = None + self.is_processing_queue = False + self.dump_queue.task_done() + if tid and tid in self.task_history: + last_task = next((t for t in reversed(self.task_history) if t.get('tid') == tid), None) + if last_task and last_task.get('flood_count', 0) > 0: + final_wait = min(60 * last_task['flood_count'], 300) + logger.info(f"Финальная задержка после задачи с FloodWait'ами: {final_wait}с") + await asyncio.sleep(final_wait) + self._save_tasks() + + def _update_queue_positions(self): # описание ниже + """Обновляет позиции задач в очереди""" + queued_tasks = [t for t in self.task_queue if t['status'] == 'queued'] + for i, task in enumerate(queued_tasks, 1): + task['position'] = i + + async def _auto_update_status(self, tid, status_msg): # описание ниже + """Обновляет только внутренний кэш скорости без редактирования сообщения""" + while True: + try: + await asyncio.sleep(5) + if tid not in self.active_dumps: + break + task = self.active_dumps[tid] + status = task.get('status', 'unknown') + if status not in ['running', 'paused']: + continue + current = task.get('current', 0) + total = task.get('total_estimated', 0) + start_time = task.get('start_time', time.time()) + elapsed = time.time() - start_time + now = time.time() + last_calc_time = task.get('_last_calc_time', now - 5) + last_calc_count = task.get('_last_calc_count', current) + delta_t = now - last_calc_time + delta_c = current - last_calc_count + if status == 'running': + if delta_t > 0: + inst_speed = (delta_c / delta_t) * 60 + task['speed_samples'].append(inst_speed) + if len(task['speed_samples']) > 12: + task['speed_samples'].pop(0) + task['_last_calc_time'] = now + task['_last_calc_count'] = current + avg_speed = sum(task['speed_samples']) / len(task['speed_samples']) if task['speed_samples'] else 0 + task['current_speed'] = avg_speed + if avg_speed > 0: + self.global_speed_history.append(avg_speed) + if len(self.global_speed_history) > 50: + self.global_speed_history.pop(0) + self.task_progress_cache[tid] = { + 'current': current, + 'speed': round(avg_speed, 1), + 'eta': self._calculate_eta(current, total, avg_speed), + 'progress': round((current / total * 100), 1) if total > 0 else 0, + 'elapsed': elapsed, + 'status': status + } + # прогресс идёт в логи через logger.info + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"Auto update error: {e}") + + def _get_avg_speed(self): # описание ниже + """Получает среднюю скорость из глобальной истории""" + if not self.global_speed_history: + return 100 + return sum(self.global_speed_history) / len(self.global_speed_history) + + def _calculate_eta(self, current, total, speed_per_min): # описание ниже + """Расчёт оставшегося времени""" + if speed_per_min <= 0 or total <= 0: + return "∞" + remaining = total - current + minutes = remaining / speed_per_min + return self._format_duration(minutes * 60) + + def _calculate_task_wait_time(self, target_position): # описание ниже + """Расчёт времени ожидания для задачи в очереди""" + avg_speed = self._get_avg_speed() + total_seconds = 0 + for task in self.task_queue: + if task['position'] < target_position and task['status'] not in ['completed', 'stopped', 'error']: + remaining = task.get('total_msgs', 0) - task.get('current', 0) + if remaining > 0: + task_seconds = (remaining / avg_speed) * 60 if avg_speed > 0 else 3600 + total_seconds += task_seconds + return self._format_duration(total_seconds) + + def _estimate_duration(self, total_msgs): # описание ниже + """Оценка длительности задачи""" + avg_speed = self._get_avg_speed() + if avg_speed <= 0 or total_msgs <= 0: + return "∞" + minutes = total_msgs / avg_speed + return self._format_duration(minutes * 60) + + def _calculate_end_time(self, start_time, total_msgs, speed_per_min=None): # описание ниже + """Расчёт времени окончания задачи""" + if speed_per_min is None: + speed_per_min = self._get_avg_speed() + if speed_per_min <= 0 or total_msgs <= 0: + return "∞" + minutes = total_msgs / speed_per_min + end_time = start_time + timedelta(minutes=minutes) + return end_time.strftime("%H:%M:%S") + + async def _raw_sender(self, messages, dest_id, no_author, no_captions, topic_id, status_msg=None, tid=None): # описание ниже + """Улучшенный sender с умной обработкой FloodWait""" + try: + dest_peer = await self.client.get_input_entity(dest_id) + src_peer = await self.client.get_input_entity(messages[0].chat_id) + await self.client(functions.messages.ForwardMessagesRequest( + from_peer=src_peer, id=[m.id for m in messages], + to_peer=dest_peer, drop_author=no_author, top_msg_id=topic_id, + with_my_score=False, drop_media_captions=no_captions + )) + if tid and tid in self.active_dumps: + self.active_dumps[tid]["last_successful_send"] = time.time() + self.active_dumps[tid]["consecutive_floods"] = 0 + return True + except errors.FloodWaitError as e: + wait_time = e.seconds if e.seconds is not None else 60 + if tid and tid in self.active_dumps: + task = self.active_dumps[tid] + task["consecutive_floods"] = task.get("consecutive_floods", 0) + 1 + task["flood_count"] = task.get("flood_count", 0) + 1 + task["flood_total_seconds"] = task.get("flood_total_seconds", 0) + wait_time + task["current_flood_wait"] = wait_time + task["status"] = "paused" + task["flood_wait_until"] = time.time() + wait_time + self.config["flood_buffer"] + current_speed = task.get('current_speed', 0) + total_msgs = task.get('total_estimated', 0) + current_count = task.get('current', 0) + status_chat = task.get("status_chat_id") + if status_chat: + await self._send_flood_notice(status_chat, wait_time, current_count, tid, total_msgs, current_speed) + logger.warning(f"[{tid}] FloodWait: ждём {wait_time}с (запрошено Telegram) + {self.config['flood_buffer']}с буфер") + total_wait = wait_time + self.config["flood_buffer"] + waited = 0 + check_interval = 5 + while waited < total_wait: + if tid in self.active_dumps: + if self.active_dumps[tid].get("status") == "stopped": + logger.info(f"[{tid}] Задача остановлена во время ожидания FloodWait") + return False + await asyncio.sleep(min(check_interval, total_wait - waited)) + waited += check_interval + if tid in self.active_dumps: + self.active_dumps[tid]["status"] = "running" + self.active_dumps[tid]["last_successful_send"] = time.time() + try: + await self.client(functions.messages.ForwardMessagesRequest( + from_peer=src_peer, id=[m.id for m in messages], + to_peer=dest_peer, drop_author=no_author, top_msg_id=topic_id, + with_my_score=False, drop_media_captions=no_captions + )) + if tid and tid in self.active_dumps: + self.active_dumps[tid]["last_successful_send"] = time.time() + self.active_dumps[tid]["consecutive_floods"] = 0 + return True + except errors.FloodWaitError as e2: + logger.warning(f"[{tid}] Повторный FloodWait: ждём ещё {e2.seconds}с") + await asyncio.sleep(e2.seconds + self.config["flood_buffer"]) + return False + return False + except Exception as e: + logger.error(f"[{tid}] Send Error: {e}") + return False + + def _parse_filter(self, args): # все аргументы нужные цепляет + filter_type = FILTER_ALL + args_list = list(args) + for arg in args_list: + if arg == "--media": + filter_type = FILTER_MEDIA + if arg in args: args.remove(arg) + elif arg == "--photo_video": + filter_type = FILTER_PHOTO_VIDEO + if arg in args: args.remove(arg) + elif arg == "--docs": + filter_type = FILTER_DOCS + if arg in args: args.remove(arg) + elif arg == "--text": + filter_type = FILTER_TEXT + if arg in args: args.remove(arg) + return filter_type, args + + def _get_filter_name(self, filter_type): + names = { + FILTER_ALL: "Все сообщения", + FILTER_MEDIA: "Только медиа", + FILTER_PHOTO_VIDEO: "Фото и видео", + FILTER_DOCS: "Документы", + FILTER_TEXT: "Текст", + } + return names.get(filter_type, "Неизвестно") + + def _get_effective_batch_size(self) -> int: + """Returns the current batch_size from config, always fresh.""" + val = self.config.get("batch_size", 100) + if isinstance(val, int) and 1 <= val <= 100: + return val + return 100 + + @loader.command() + async def chatcopy(self, message: Message): + """ [start_id:final_id] [-n] [-dmc] [--now] [--media|--photo_video|--docs|--text] — Добавить задачу в очередь. --now: начать сразу, без полного подсчёта.""" + args_raw = utils.get_args_raw(message).split() + no_author = "-n" in args_raw + no_captions = "-dmc" in args_raw + start_now = "--now" in args_raw + if start_now: + args_raw.remove("--now") + filter_type, args_raw = self._parse_filter(args_raw) + clean_args = [x for x in args_raw if x not in ["-n", "-dmc"]] + if len(clean_args) < 2: + return await utils.answer(message, self.strings["args_err"]) + start_id = 0 + final_id = 0 + if len(clean_args) >= 3: + id_arg = clean_args[2] + if ":" in id_arg: + parts = id_arg.split(":") + if parts[0].isdigit(): + start_id = int(parts[0]) + if len(parts) > 1 and parts[1].isdigit(): + final_id = int(parts[1]) + elif id_arg.isdigit(): + start_id = int(id_arg) + src, src_map = await self._resolve_arg(clean_args[0]) + dest, dest_map = await self._resolve_arg(clean_args[1]) + if not src or not dest: + return await utils.answer(message, self.strings["err_ent"]) + self._task_counter += 1 + tid = f"{src.id}_{dest.id}_{self._task_counter}_{int(time.time())}" + src_is_forum = self._is_forum(src) + dest_is_forum = self._is_forum(dest) + if src_is_forum and not dest_is_forum: + forum_result = await self._ensure_forum_enabled(dest) + if forum_result: + dest = await self.client.get_entity(dest.id) + dest_is_forum = self._is_forum(dest) + if not dest_is_forum: + await asyncio.sleep(2) + dest = await self.client.get_entity(dest.id) + dest_is_forum = self._is_forum(dest) + if dest_is_forum: + logger.info("[%s] Режим топиков включён на dest %s", tid, getattr(dest, 'title', dest.id)) + else: + logger.warning("[%s] _ensure_forum_enabled вернул True, но _is_forum всё ещё False для dest %s", tid, getattr(dest, 'title', dest.id)) + else: + logger.warning("[%s] Не удалось включить топики на dest %s — копирование пойдёт без маппинга топиков", tid, getattr(dest, 'title', dest.id)) + elif src_is_forum and dest_is_forum: + try: + dest = await self.client.get_entity(dest.id) + dest_is_forum = self._is_forum(dest) + except Exception: + pass + if src_is_forum and not dest_is_forum: + logger.warning("[%s] src — форум, dest — НЕ форум. Все сообщения пойдут в General!", tid) + prep_key = "preparing_prem" if self.is_premium else "preparing_no_prem" + status_msg = await utils.answer(message, self.strings[prep_key]) + total_msgs = 0 + f_src_t_for_count = src_map.get('topic') + if start_now: + try: + if f_src_t_for_count: + async for _ in self.client.iter_messages( + src, + reply_to=f_src_t_for_count, + min_id=start_id - 1 if start_id else 0, + max_id=final_id + 1 if final_id else 0, + ): + total_msgs += 1 + if total_msgs > 150000: break + else: + result = await self.client(functions.messages.GetHistoryRequest( + peer=src, + offset_id=0, + offset_date=None, + add_offset=0, + limit=1, + max_id=final_id + 1 if final_id else 0, + min_id=start_id - 1 if start_id else 0, + hash=0, + )) + total_msgs = getattr(result, 'count', 0) or 0 + except Exception as e: + logger.warning(f"Count failed for --now: {e}") + total_msgs = 0 + else: + try: + iter_kwargs = { + "min_id": start_id - 1 if start_id else 0, + "max_id": final_id + 1 if final_id else 0, + } + if f_src_t_for_count: + iter_kwargs["reply_to"] = f_src_t_for_count + async for _ in self.client.iter_messages(src, **iter_kwargs): + total_msgs += 1 + if total_msgs > 150000: break + except Exception as e: + logger.error(f"Ошибка при подсчете сообщений: {e}") + total_msgs = -1 + src_name = getattr(src, 'title', src.id) + dest_name = getattr(dest, 'title', dest.id) + async with self._queue_lock: + queue_position = len([t for t in self.task_queue if t['status'] == 'queued']) + 1 + estimated_duration = self._estimate_duration(total_msgs) + mode_str = "🗂️ Топики (Auto)" if src_is_forum else "Обычный" + no_auth_str = "Да" if no_author else "Нет" + no_capt_str = "Да" if no_captions else "Нет" + start_id_str = f"с {start_id}" if start_id > 0 else "С начала" + if final_id > 0: start_id_str += f" до {final_id}" + task_info = { + 'tid': tid, 'src': src_name, 'dest': dest_name, 'src_id': src.id, 'dest_id': dest.id, + 'status': 'queued', 'position': queue_position, 'added_time': datetime.now(MSK).isoformat(), + 'no_author': no_author, 'no_captions': no_captions, 'filter_type': filter_type, + 'start_id': start_id, 'final_id': final_id, 'total_msgs': total_msgs if total_msgs > -1 else 0, + 'current': 0, 'last_processed_id': start_id, + 'status_msg_id': status_msg.id, 'status_chat_id': status_msg.chat_id, + 'map_t': src_is_forum, 'f_src_t': src_map.get('topic'), 'f_dest_t': dest_map.get('topic'), + 'start_now': start_now, + } + self.task_queue.append(task_info) + self._save_tasks() + filter_name = self._get_filter_name(filter_type) + start_string_key = "copy_start_prem" if self.is_premium else "copy_start_no_prem" + await status_msg.edit(self.strings[start_string_key].format( + src=utils.escape_html(src_name), dest=utils.escape_html(dest_name), + mode=mode_str, start_id=start_id_str, no_auth=no_auth_str, + no_capt=no_capt_str, filter_type=filter_name, + total_msgs=total_msgs if total_msgs > -1 else "∞ (ошибка подсчета)", + estimated_time=estimated_duration, position=queue_position + )) + await self.dump_queue.put({ + "status_msg": status_msg, "src": src, "dest": dest, + "no_auth": no_author, "no_captions": no_captions, + "map_t": src_is_forum, "f_src_t": src_map.get('topic'), "f_dest_t": dest_map.get('topic'), + "tid": tid, "min_id": start_id, "max_id": final_id, + "mode_str": mode_str, "no_auth_str": no_auth_str, "no_capt_str": no_capt_str, + "start_id_str": start_id_str, "filter_type": filter_name, "filter_name": filter_name, + "src_name": src_name, "queue_position": queue_position, "total_msgs": total_msgs if total_msgs > -1 else 0, + "restored_count": 0, + }) + + def _parse_duration(self, duration_str): # описание ниже + """Парсит строку длительности в секунды""" + if duration_str == "∞": + return 3600 + total = 0 + parts = duration_str.split() + for part in parts: + if 'ч' in part: + total += int(part.replace('ч', '')) * 3600 + elif 'ч' in part and 'м' in part: + pass + elif 'м' in part and 'с' not in part: + total += int(part.replace('м', '')) * 60 + elif 'м' in part and 'с' in part: + mins_secs = part.replace('м', '').replace('с', '').split() + if len(mins_secs) >= 1: + total += int(mins_secs[0]) * 60 + if len(mins_secs) >= 2: + total += int(mins_secs[1]) + elif 'с' in part: + total += int(part.replace('с', '')) + elif part.isdigit(): + total += int(part) + return total if total > 0 else 0 + + @loader.command() # стартует слежку за чатом что бы пи... кхм кхм, благополучно заимствовать сей прекрасный или не очень контент + async def ccwatch(self, message: Message): + """ [start_id:final_id] [-n] [-dmc][--media|--photo_video|--docs|--text] — Наблюдение за чатом""" + args = utils.get_args_raw(message).split() + no_author = "-n" in args + no_captions = "-dmc" in args + filter_type, args = self._parse_filter(args) + clean_args = [x for x in args if x not in ["-n", "-t", "-dmc"]] + if len(clean_args) < 2: + return await utils.answer(message, self.strings["args_err"]) + start_id = 0 + final_id = 0 + if len(clean_args) >= 3: + id_arg = clean_args[2] + if ":" in id_arg: + parts = id_arg.split(":") + if parts[0].isdigit(): start_id = int(parts[0]) + if len(parts) > 1 and parts[1].isdigit(): final_id = int(parts[1]) + elif id_arg.isdigit(): + start_id = int(id_arg) + src, src_map = await self._resolve_arg(clean_args[0]) + dest, dest_map = await self._resolve_arg(clean_args[1]) + if not src or not dest: + return await utils.answer(message, self.strings["err_ent"]) + src_is_forum = self._is_forum(src) + dest_is_forum = self._is_forum(dest) + if src_is_forum and not dest_is_forum: + forum_result = await self._ensure_forum_enabled(dest) + if forum_result: + await utils.answer(message, self.strings["forum_enabled"].format(chat=utils.escape_html(getattr(dest, 'title', dest.id)))) + dest = await self.client.get_entity(dest.id) + else: + return await utils.answer(message, self.strings["forum_enable_failed"].format(chat=utils.escape_html(getattr(dest, 'title', dest.id)))) + is_restricted = False + try: + async for test_m in self.client.iter_messages(src, limit=1): + if test_m.noforwards: + is_restricted = True + break + except Exception: + pass + if is_restricted: + return await utils.answer(message, "❌ Ошибка: канал в режиме запрета копирования") # ну как бы, учитываем да + src_t = src_map.get('topic') + dest_t = dest_map.get('topic') + map_topics = src_is_forum + cid = self._get_normalized_id(src) + try: + dest_id = utils.get_chat_id(dest) + except: + dest_id = dest.id + if start_id > 0: + self.last_processed_ids[cid] = start_id - 1 + elif cid not in self.last_processed_ids: + self.last_processed_ids[cid] = 0 + self.watchlist[cid] = { + "dest": dest_id, "no_author": no_author, "no_captions": no_captions, "map_topics": map_topics, + "fixed_src_topic": src_t, "fixed_dest_topic": dest_t, "src_entity_id": src.id, "dest_entity_id": dest.id, + "filter_type": filter_type, "final_id": final_id + } + self.db.set("ChatCopy", "watchlist", self.watchlist) + self.db.set("ChatCopy", "last_processed_ids", self.last_processed_ids) + filter_name = self._get_filter_name(filter_type) + msg_text = self.strings["watch_added"].format( + src=getattr(src, 'title', src.id), src_id=cid, dest=getattr(dest, 'title', dest.id), + topics="🗂️ ВКЛ (Auto-mapping)" if map_topics else "ВЫКЛ", no_capt="Да" if no_captions else "Нет", filter_type=filter_name + ) + if start_id > 0 or final_id > 0: + range_str = "Все новые" + if start_id > 0 and final_id > 0: range_str = f"с {start_id} по {final_id}" + elif start_id > 0: range_str = f"с {start_id}" + elif final_id > 0: range_str = f"до {final_id}" + msg_text += f"\nДиапазон ID: {range_str}" + await utils.answer(message, msg_text) + + async def _history_dumper(self, status_msg, src, dest, no_auth, no_captions, + map_t, f_src_t, f_dest_t, tid, min_id=0, max_id=0, + filter_type=FILTER_ALL, filter_name="", restored_count=0, **kwargs): + if tid in self.active_dumps: + self.active_dumps[tid]["status"] = "running" + task = next((t for t in self.task_queue if t['tid'] == tid), None) + if not task: + logger.error(f"Задача {tid} не найдена в очереди для дампа.") + return + count = task.get('current', 0) or restored_count + if tid in self.active_dumps and count > 0: + self.active_dumps[tid]["current"] = count + start_from_id = task.get('last_processed_id', min_id) + if map_t: + try: + dest = await self.client.get_entity(dest.id) + if not self._is_forum(dest): + logger.info("[%s] dest не форум, пытаемся включить топики...", tid) + ok = await self._ensure_forum_enabled(dest) + if ok: + await asyncio.sleep(2) + dest = await self.client.get_entity(dest.id) + if self._is_forum(dest): + logger.info("[%s] Режим топиков включён на dest в dumper", tid) + else: + logger.warning("[%s] _ensure_forum_enabled OK, но _is_forum False. Пробуем ещё раз...", tid) + await asyncio.sleep(3) + dest = await self.client.get_entity(dest.id) + if not self._is_forum(dest): + logger.warning("[%s] dest не является форумом после повторной проверки, пересылка без топиков", tid) + map_t = False + else: + logger.warning("[%s] dest не является форумом, пересылка без топиков", tid) + map_t = False + except Exception as e: + logger.warning("[%s] Ошибка обновления dest entity: %s", tid, e) + if map_t: + try: + src = await self.client.get_entity(src.id) + if not self._is_forum(src): + logger.warning("[%s] src не является форумом (хотя map_t=True), отключаем маппинг", tid) + map_t = False + except Exception as e: + logger.warning("[%s] Ошибка обновления src entity: %s", tid, e) + batch = [] + dumper_kwargs = {"reverse": True} + if f_src_t: dumper_kwargs["reply_to"] = f_src_t + if start_from_id > 0: dumper_kwargs["min_id"] = start_from_id - 1 + if max_id > 0: dumper_kwargs["max_id"] = max_id + 1 + delay = self.config["delay"] + try: + async for msg in self.client.iter_messages(src, **dumper_kwargs): + if tid not in self.active_dumps or self.active_dumps[tid].get("status") == "stopped": break + await self.active_dumps[tid].get("cancel", asyncio.Event()).wait() + if tid not in self.active_dumps or self.active_dumps[tid].get("status") == "stopped": break + if isinstance(msg, types.MessageService) or not self._should_include_message(msg, filter_type): continue + batch.append(msg) + if len(batch) >= self._get_effective_batch_size(): + processed = await self._process_batch( + messages=list(batch), dest_id=dest.id, no_author=no_auth, no_captions=no_captions, + fixed_dest_topic=f_dest_t, map_topics=map_t, dest_entity=dest, src_entity=src, + filter_type=filter_type, status_msg=status_msg, tid=tid + ) + if tid not in self.active_dumps or self.active_dumps[tid].get("status") == "stopped": break + if tid in self.active_dumps: + self.active_dumps[tid]["current"] += processed + count = self.active_dumps[tid]["current"] + task['current'] = count + task['last_processed_id'] = batch[-1].id + self._save_tasks() + total = task.get('total_msgs', 0) + pct = round(count / total * 100, 1) if total else 0 + spd = round(self.active_dumps[tid].get('current_speed', 0), 1) + logger.info("[%s] Прогресс: %d/%d (%.1f%%) | %.1f сооб/мин", + tid, count, total, pct, spd) + batch = [] + if batch and self.active_dumps.get(tid, {}).get("status") != "stopped": + processed = await self._process_batch( + messages=list(batch), dest_id=dest.id, no_author=no_auth, no_captions=no_captions, + fixed_dest_topic=f_dest_t, map_topics=map_t, dest_entity=dest, src_entity=src, + filter_type=filter_type, status_msg=status_msg, tid=tid + ) + if tid in self.active_dumps: + self.active_dumps[tid]["current"] += processed + count = self.active_dumps[tid]["current"] + task['current'] = count + task['last_processed_id'] = batch[-1].id + if self.active_dumps.get(tid, {}).get("status") != "stopped": + task['status'] = 'completed' + self.task_queue = [t for t in self.task_queue if t['tid'] != tid] + self._save_tasks() + task_data = self.active_dumps[tid] + duration_seconds = time.time() - task_data.get('start_time', time.time()) + duration_str = self._format_duration(duration_seconds) + active_seconds = duration_seconds - task_data.get('flood_total_seconds', 0) + if active_seconds <= 0: active_seconds = 1 + avg_speed = round((count / active_seconds) * 60, 1) + chat_id_to_report = status_msg.chat_id if status_msg and status_msg.chat_id else task.get('status_chat_id') + done_string_key = "copy_done_detailed_prem" if self.is_premium else "copy_done_detailed_no_prem" + done_full = self.strings[done_string_key].format( + src=utils.escape_html(getattr(src, 'title', src.id)), dest=utils.escape_html(getattr(dest, 'title', dest.id)), + no_auth=kwargs.get("no_auth_str", "N/A"), no_capt=kwargs.get("no_capt_str", "N/A"), + start_id=kwargs.get("start_id_str", "N/A"), mode=kwargs.get("mode_str", "N/A"), + filter_type=filter_name, count=count, duration=duration_str, + avg_speed=avg_speed, flood_info=self._format_flood_stats(task_data) + ) + # краткий итог в логи + logger.info( + "[✅ %s] Завершено: %d сообщений за %s | %.1f сооб/мин", + task_data.get('name', '?'), count, duration_str, avg_speed + ) + # полный итог в чат где было запущено + if chat_id_to_report: + await self.client.send_message(chat_id_to_report, done_full) + except Exception as e: + logger.error(f"Dumper Error: {e}", exc_info=True) + chat_id_to_report = status_msg.chat_id if status_msg and status_msg.chat_id else task.get('status_chat_id') + if chat_id_to_report: await self.client.send_message(chat_id_to_report, f"❌ Ошибка в задаче:\n{e}") + task['status'] = 'error' + self._save_tasks() + except Exception as e: + logger.error(f"Dumper Error: {e}") + await self.client.send_message(status_msg.chat_id, f"❌ Ошибка в задаче:\n{e}") + + @loader.watcher() # сам ватчер, который следит за чатами + async def watcher(self, message: Message): + if isinstance(message, types.MessageService): + return + if not getattr(message, 'chat_id', None): + return + raw_chat_id = str(message.chat_id) + normalized_id = self._get_normalized_id(getattr(message, 'chat', None)) + chat_id_from_utils = "0" + if getattr(message, 'chat', None) and hasattr(utils, 'get_chat_id'): + try: + chat_id_from_utils = str(utils.get_chat_id(message.chat)) + except Exception: + pass + possible_ids = [ + normalized_id, + raw_chat_id, + raw_chat_id.replace("-100", ""), + f"-100{raw_chat_id.replace('-100', '').replace('-', '')}", + chat_id_from_utils + ] + cid = None + for test_id in possible_ids: + if test_id in self.watchlist: + cid = test_id + break + if not cid: + return + cfg = self.watchlist[cid] + filter_type = cfg.get("filter_type", FILTER_ALL) + last_id = self.last_processed_ids.get(cid, 0) + final_id = cfg.get("final_id", 0) + if message.id <= last_id: + return + if final_id > 0 and message.id > final_id: + return + if not self._should_include_message(message, filter_type): + self.last_processed_ids[cid] = message.id + self.db.set("ChatCopy", "last_processed_ids", self.last_processed_ids) + return + if cfg.get("fixed_src_topic"): + cur_t = getattr(message, 'topic_id', None) or (message.reply_to.reply_to_top_id if message.reply_to else None) + if cur_t != cfg["fixed_src_topic"]: + self.last_processed_ids[cid] = message.id + self.db.set("ChatCopy", "last_processed_ids", self.last_processed_ids) + return + if cid not in self.watcher_buffer: + self.watcher_buffer[cid] = [] + self.watcher_buffer[cid].append(message) + self.last_watched[cid] = { + "name": getattr(getattr(message, 'chat', None), "title", cid) if getattr(message, 'chat', None) else cid, + "time": datetime.now(MSK).strftime("%H:%M:%S") + } + if cid in self.watcher_flush_tasks: + self.watcher_flush_tasks[cid].cancel() + batch_size = self.config["batch_size"] + if not isinstance(batch_size, int): + batch_size = 100 + if len(self.watcher_buffer[cid]) >= batch_size: + await self._flush_watcher_buffer(cid, cfg) + else: + self.watcher_flush_tasks[cid] = asyncio.get_event_loop().call_later( + 3.0, + lambda: asyncio.create_task(self._flush_watcher_buffer(cid, cfg)) + ) + + async def _flush_watcher_buffer(self, cid, cfg): # опустошает буфер watcher'а: группирует альбомы и отправляет пачку в очередь на пересылку. + if cid not in self.watcher_buffer or not self.watcher_buffer[cid]: + return + msgs = self.watcher_buffer[cid].copy() + self.watcher_buffer[cid] = [] + if cid in self.watcher_flush_tasks: + del self.watcher_flush_tasks[cid] + try: + cid_int = int(cid) + except (ValueError, TypeError): + logger.error(f"Watcher flush: неверный cid={cid}") + return + albums = {} + single_msgs = [] + for msg in msgs: + if msg.grouped_id: + if msg.grouped_id not in albums: + albums[msg.grouped_id] = [] + albums[msg.grouped_id].append(msg) + else: + single_msgs.append(msg) + for gid, album_msgs in albums.items(): + sorted_album = sorted(album_msgs, key=lambda x: x.id) + try: + dest_entity = await self.client.get_entity(cfg["dest"]) + src_entity = await self.client.get_entity(cid_int) + await self.queue.put({ + "messages": sorted_album, + "dest_id": cfg["dest"], + "no_author": cfg["no_author"], + "no_captions": cfg.get("no_captions", False), + "fixed_dest_topic": cfg.get("fixed_dest_topic"), + "map_topics": cfg.get("map_topics"), + "dest_entity": dest_entity, + "src_entity": src_entity, + "filter_type": cfg.get("filter_type", FILTER_ALL), + "watch_cid": cid + }) + except Exception as e: + logger.error(f"Watcher album flush error (cid={cid}): {e}") + batch_size = self.config["batch_size"] + if not isinstance(batch_size, int): + batch_size = 100 + for i in range(0, len(single_msgs), batch_size): + batch = single_msgs[i:i + batch_size] + try: + dest_entity = await self.client.get_entity(cfg["dest"]) + src_entity = await self.client.get_entity(cid_int) + await self.queue.put({ + "messages": batch, + "dest_id": cfg["dest"], + "no_author": cfg["no_author"], + "no_captions": cfg.get("no_captions", False), + "fixed_dest_topic": cfg.get("fixed_dest_topic"), + "map_topics": cfg.get("map_topics"), + "dest_entity": dest_entity, + "src_entity": src_entity, + "filter_type": cfg.get("filter_type", FILTER_ALL), + "watch_cid": cid + }) + except Exception as e: + logger.error(f"Watcher batch flush error (cid={cid}): {e}") + + async def _catch_up_on_restart(self): # ватчер восстанавливает после перезагрузки + await asyncio.sleep(15) + for cid_str, cfg in self.watchlist.items(): + try: + last_id = self.last_processed_ids.get(cid_str, 0) + if not isinstance(last_id, int): + last_id = 0 + missed = [] + batch_size = self.config["batch_size"] + if not isinstance(batch_size, int): + batch_size = 100 + filter_type = cfg.get("filter_type", FILTER_ALL) + cid_int = int(cid_str) + async for msg in self.client.iter_messages(cid_int, min_id=last_id): + if cfg.get("final_id", 0) > 0 and msg.id > cfg.get("final_id", 0): + continue + if not isinstance(msg, types.MessageService) and self._should_include_message(msg, filter_type): + missed.append(msg) + if missed: + missed.sort(key=lambda x: x.id) + for i in range(0, len(missed), batch_size): + batch = missed[i:i + batch_size] + dest_ent = await self.client.get_entity(cfg["dest"]) + src_ent = await self.client.get_entity(cid_int) + await self.queue.put({ + "messages": batch, "dest_id": cfg["dest"], "no_author": cfg["no_author"], + "no_captions": cfg.get("no_captions", False), "fixed_dest_topic": cfg.get("fixed_dest_topic"), + "map_topics": cfg.get("map_topics"), "dest_entity": dest_ent, "src_entity": src_ent, + "filter_type": filter_type, "watch_cid": cid_str + }) + await asyncio.sleep(self.config["delay"]) + except Exception as e: + logger.debug(f"Catch-up error for {cid_str}: {e}") + + @loader.command() + async def cchelp(self, message: Message): + """— Подробная документация по модулю ChatCopy""" + help_text_prem = ( + '🛡 Подробная документация по модулю ChatCopy!\n\n' + '
1️⃣ Основные команды \n' + '🛫 .chatcopy <откуда> <куда>[диапазон (от:до)] [флаги (можно несколько)]\n' + 'Копирует старую историю чата (делает дамп). Ставит задачу в очередь в случае если другая была запущена.\n' + '⚙️ --now — Начать немедленно, без полного подсчёта (примерное кол-во сообщений запрашивается у Telegram мгновенно). Идеально для 110k+ медиа.\n\n' + '👀 .ccwatch <откуда> <куда> [диапазон (от:до)] [флаги (можно несколько)]\n' + 'Режим слежки. Модуль будет висеть в фоне и моментально пересылать все новые сообщения. Функции [от:до] аналогичны .chatcopy\n\n' + '📺 .ccpanel\n' + 'Открывает меню: управление задачами, пауза/стоп, статистика и настройки (скорость, задержка).\n\n' + '🗑 .ccclear topics\n' + 'Очищает кэш топиков (полезно, если форум сломался и пересылает не в те разделы).
\n\n' + '
2️⃣ Источники и Диапазоны([от:до] функция) (ID)\n' + ' Чаты: Можно использовать юзернеймы (@chat), ID (-100123...) или прямые ссылки на топики (t.me/c/123/45). Модуль сам всё распознает.\n' + '⚪️ Диапазон [start:end]: Пишется слитно, без пробелов.\n' + '⚪️ 100:500 — скопировать с 100-го по 500-е сообщение.\n' + '⚪️ 100: — от 100-го до самых свежих.\n' + '⚪️ :500 — с самого начала чата и до 500-го.
\n\n' + '
3️⃣ Флаги (Настройки текста)\n' + '🆕 --now - начать пересылку сразу без подсчитывания, но без копирования топиков и последующей пересылки в них' + '👤 -n — Скрыть автора (пересылка без плашки «Переслано от...»).\n' + '💬 -dmc — Удалить подпись к медиа (оставит только голую картинку или файл, удалив текст под ним)(!Работает только с[-n] флагом!).
\n\n' + '
4️⃣ Фильтры контента\n' + '(Указывайте только один! Если не указать ничего — скопируется всё подряд)\n' + '📌 --media — Любые медиа (фото, видео) и документы.\n' + '📷 --photo_video — Строго только фото и видео (без гифок/стикеров).\n' + '💼 --docs — Строго только документы (файлы, архивы, apk).\n' + '💬 --text — Только чисто текстовые сообщения.
\n\n' + '
💡 Полные примеры использования\n' + '1. Полная копия канала со скрытием автора:\n' + '➡️ .chatcopy @donor_channel @my_channel -n\n\n' + '2. Слежка за конкретным топиком (воруем только фото/видео без подписей):\n' + '➡️ .ccwatch t.me/c/123/4 t.me/c/321/5 -dmc --photo_video\n\n' + '3. Скопировать историю с 5000 по 6000 сообщение, только текст:\n' + '➡️ .chatcopy -100111 -100222 5000:6000 --text
\n\n' + '💎 Приятного пользования!\n' + ' Единственный минус, не копирует с чатов с запрещенным копированием.' + ) + + help_text_no_prem = ( + '🛡 Подробная документация по модулю ChatCopy!\n\n' + '
1️⃣ Основные команды \n' + '🛫 .chatcopy <откуда> <куда>[диапазон (от:до)] [флаги (можно несколько)]\n' + 'Копирует старую историю чата (делает дамп). Ставит задачу в очередь в случае если другая была запущена.\n' + '⚙️ --now — Начать немедленно, без полного подсчёта (примерное кол-во запрашивается у Telegram мгновенно). Идеально для 110k+ медиа.\n\n' + '👀 .ccwatch <откуда> <куда> [диапазон (от:до)] [флаги (можно несколько)]\n' + 'Режим слежки. Модуль будет висеть в фоне и моментально пересылать все новые сообщения. Функции [от:до] аналогичны .chatcopy\n\n' + '📺 .ccpanel\n' + 'Открывает меню: управление задачами, пауза/стоп, статистика и настройки (скорость, задержка).\n\n' + '🗑 .ccclear topics\n' + 'Очищает кэш топиков (полезно, если форум сломался и пересылает не в те разделы).
\n\n' + '
2️⃣ Источники и Диапазоны([от:до] функция) (ID)\n' + '✨ Чаты: Можно использовать юзернеймы (@chat), ID (-100123...) или прямые ссылки на топики (t.me/c/123/45). Модуль сам всё распознает.\n' + '⚪️ Диапазон [start:end]: Пишется слитно, без пробелов.\n' + '⚪️ 100:500 — скопировать с 100-го по 500-е сообщение.\n' + '⚪️ 100: — от 100-го до самых свежих.\n' + '⚪️ :500 — с самого начала чата и до 500-го.
\n\n' + '
3️⃣ Флаги (Настройки текста)\n' + '🆕 --now - начать пересылку сразу без подсчитывания, но без копирования топиков и последующей пересылки в них' + '👤 -n — Скрыть автора (пересылка без плашки «Переслано от...»).\n' + '💬 -dmc — Удалить подпись к медиа (оставит только голую картинку или файл, удалив текст под ним)(!Работает только с [-n] флагом!).
\n\n' + '
4️⃣ Фильтры контента\n' + '(Указывайте только один! Если не указать ничего — скопируется всё подряд)\n' + '📌 --media — Любые медиа (фото, видео) и документы.\n' + '📷 --photo_video — Строго только фото и видео (без гифок/стикеров).\n' + '💼 --docs — Строго только документы (файлы, архивы, apk).\n' + '💬 --text — Только чисто текстовые сообщения.
\n\n' + '
💡 Полные примеры использования\n' + '1. Полная копия канала со скрытием автора:\n' + '➡️ .chatcopy @donor_channel @my_channel -n\n\n' + '2. Слежка за конкретным топиком (воруем только фото/видео без подписей):\n' + '➡️ .ccwatch t.me/c/123/4 t.me/c/321/5 -dmc --photo_video\n\n' + '3. Скопировать историю с 5000 по 6000 сообщение, только текст:\n' + '➡️ .chatcopy -100111 -100222 5000:6000 --text
\n\n' + '💎 Приятного пользования!\n' + '❕ Единственный минус, не копирует с чатов с запрещенным копированием.' + ) + final_text = help_text_prem if self.is_premium else help_text_no_prem + await utils.answer(message, final_text) + + @loader.command() + async def ccpanel(self, message: Message): + """Панель управления""" + await self._show_main_panel(message) + + async def _show_main_panel(self, message, edit=False): # вот эта хрень это основная панель которая управляет кнопками и другим стафом + active_text = "Нет" + last_flood = "—" + if self.current_dump_task and self.current_dump_task in self.active_dumps: + task = self.active_dumps[self.current_dump_task] + name = utils.escape_html(task.get('name', 'Unknown')) + count = task.get('current', 0) + total = task.get('total_estimated', 0) + status = task.get('status', 'unknown') + start_ts = task.get('start_time', time.time()) + elapsed = time.time() - start_ts + if status == 'running': + speed = task.get('current_speed', 0) + progress = round((count / total * 100), 1) if total > 0 else 0 + eta = self._calculate_eta(count, total, speed) + elapsed_str = self._format_duration(elapsed) + start_time = datetime.fromtimestamp(start_ts, MSK).strftime("%H:%M:%S") + end_time = self._calculate_end_time(datetime.fromtimestamp(start_ts, MSK), total - count, speed) + active_text = self.strings["panel_task_running"].format( + name=name, + count=count, + total=total, + speed=round(speed, 1), + progress=progress, + elapsed=elapsed_str, + eta=eta, + start_time=start_time, + end_time=end_time + ) + elif status == 'paused': + current_fw = task.get('current_flood_wait', 0) + fw_str = f"{current_fw // 60}m {current_fw % 60}s" if current_fw >= 60 else f"{current_fw}s" + resume_at = task.get('flood_wait_until', 0) + resume_time = datetime.fromtimestamp(resume_at, MSK).strftime("%H:%M:%S") if resume_at else "неизвестно" + active_text = self.strings["panel_task_paused"].format( + name=name, + flood_time=fw_str, + count=count, + total=total, + speed=round(task.get('current_speed', 0), 1), + resume_time=resume_time + ) + else: + active_text = f"{name}\n└ {status}" + elif self.last_flood_info.get("time"): + last_flood = self.last_flood_info["time"] + text = self.strings["panel_summary"].format( + queue_len=len([t for t in self.task_queue if t['status'] == 'queued']), + active=active_text, + watching_count=len(self.watchlist), + last_flood=last_flood + ) + queue_size = self.queue.qsize() + if queue_size > 0: + text += f"\n📥 Очередь watcher: {queue_size}" + btns = [ + [{"text": self.strings["btn_tasks"], "callback": self._panel_tasks}, {"text": self.strings["btn_watch"], "callback": self._panel_watching}], + [{"text": self.strings["btn_settings"], "callback": self._panel_settings}, {"text": self.strings["btn_stats"], "callback": self._panel_stats}] + ] + if edit: + await message.edit(text, reply_markup=btns) + else: + await self.inline.form(text=text, message=message, reply_markup=btns) + + async def _panel_tasks(self, call): # описание ниже + """Панель очереди задач со списком""" + all_tasks = [] + for i, task in enumerate(self.task_queue, 1): + task_with_num = task.copy() + task_with_num['display_num'] = i + all_tasks.append(task_with_num) + if not all_tasks: + text = self.strings["task_list_header"].format(total=0) + self.strings["no_tasks"] + btns = [[{"text": self.strings["btn_back"], "callback": self._cb_back}]] + await call.edit(text, reply_markup=btns) + return + text = self.strings["task_list_header"].format(total=len(all_tasks)) + for task in all_tasks: + num = task['display_num'] + src = utils.escape_html(task['src'][:20]) + dest = utils.escape_html(task['dest'][:20]) + status = task.get('status', 'queued') + if status == 'running': + active_data = self.active_dumps.get(task['tid'], {}) + current = active_data.get('current', 0) + total = active_data.get('total_estimated', task.get('total_msgs', 0)) + progress = round((current / total * 100), 1) if total > 0 else 0 + text += self.strings["task_item_compact_running"].format(num=num, src=src, dest=dest, progress=progress) + "\n" + elif status == 'paused': + text += self.strings["task_item_compact_paused"].format(num=num, src=src, dest=dest) + "\n" + elif status == 'completed': + text += self.strings["task_item_compact_completed"].format(num=num, src=src, dest=dest) + "\n" + elif status == 'error': + text += self.strings["task_item_compact_error"].format(num=num, src=src, dest=dest) + "\n" + else: + wait_time = self._calculate_task_wait_time(task.get('position', num)) + text += self.strings["task_item_compact_queued"].format(num=num, src=src, dest=dest, wait=wait_time) + "\n" + btns = [] + row = [] + for task in all_tasks: + num = task['display_num'] + status = task.get('status', 'queued') + emoji = "⏳" if status == 'queued' else "▶️" if status == 'running' else "⚠️" if status == 'paused' else "✅" if status == 'completed' else "❌" + row.append({"text": f"{emoji}{num}", "callback": self._show_task_detail, "args": [task['tid'], num]}) + if len(row) == 5: + btns.append(row) + row = [] + if row: + btns.append(row) + btns.append([{"text": "🔄 Обновить", "callback": self._panel_tasks}]) + btns.append([{"text": self.strings["btn_back"], "callback": self._cb_back}]) + await call.edit(text, reply_markup=btns) + + async def _show_task_detail(self, call, tid, num): # описание ниже + """Детальный просмотр задачи с точным расчётом времени""" + task = next((t for t in self.task_queue if t['tid'] == tid), None) + if not task: + history_task = next((t for t in self.task_history if t.get('tid') == tid), None) + if history_task: + await self._show_history_task_detail(call, history_task, num) + return + await call.answer("Задача не найдена") + return + status = task.get('status', 'queued') + src = utils.escape_html(task['src']) + dest = utils.escape_html(task['dest']) + total = task.get('total_msgs', 0) + position = task.get('position', num) + if status == 'running': + active_data = self.active_dumps.get(tid, {}) + current = active_data.get('current', 0) + speed = active_data.get('current_speed', 0) + start_ts = active_data.get('start_time', time.time()) + start_time = datetime.fromtimestamp(start_ts, MSK).strftime("%H:%M:%S") + elapsed = time.time() - start_ts + elapsed_str = self._format_duration(elapsed) + progress = round((current / total * 100), 1) if total > 0 else 0 + eta_left = self._calculate_eta(current, total, speed) + end_time = self._calculate_end_time(datetime.fromtimestamp(start_ts, MSK), total - current, speed) + text = self.strings["task_detail_running"].format( + num=num, src=src, dest=dest, current=current, total=total, + progress=progress, speed=round(speed, 1), eta_left=eta_left, + elapsed=elapsed_str, start_time=start_time, end_time=end_time, position=position + ) + btns = [ + [{"text": "⏸ Пауза", "callback": self._action_task, "args": [tid, "pause"]}, + {"text": "🛑 Стоп", "callback": self._stop_specific, "args": [tid]}], + [{"text": "🔙 К списку", "callback": self._panel_tasks}] + ] + elif status == 'queued': + eta_start = self._calculate_task_wait_time(position) + estimated = self._estimate_duration(total) + text = self.strings["task_detail_queued"].format( + num=num, src=src, dest=dest, total=total, eta_start=eta_start, + position=position, estimated_duration=estimated + ) + btns = [[{"text": "🗑 Удалить из очереди", "callback": self._remove_specific, "args": [tid]}], + [{"text": "🔙 К списку", "callback": self._panel_tasks}] + ] + elif status == 'paused': + active_data = self.active_dumps.get(tid, {}) + current = active_data.get('current', 0) + flood_count = active_data.get('flood_count', 0) + flood_seconds = active_data.get('flood_total_seconds', 0) + speed = active_data.get('current_speed', 0) + resume_at = active_data.get('flood_wait_until', 0) + resume_time = datetime.fromtimestamp(resume_at, MSK).strftime("%H:%M:%S") if resume_at else "неизвестно" + progress = round((current / total * 100), 1) if total > 0 else 0 + remaining = max(0, total - current) + text = self.strings["task_detail_paused"].format( + num=num, src=src, dest=dest, current=current, total=total, + progress=progress, flood_count=flood_count, + flood_time=self._format_duration(flood_seconds), + resume_time=resume_time, speed=round(speed, 1), remaining=remaining + ) + btns = [ + [{"text": "▶️ Продолжить", "callback": self._action_task, "args": [tid, "resume"]}, + {"text": "🛑 Стоп", "callback": self._stop_specific, "args": [tid]}], + [{"text": "🔙 К списку", "callback": self._panel_tasks}] + ] + elif status == 'completed': + await self._show_history_task_detail(call, task, num) + return + else: + text = self.strings["task_detail_error"].format(num=num, src=src, dest=dest) + btns = [ + [{"text": "🗑 Удалить", "callback": self._remove_specific, "args": [tid]}], + [{"text": "🔙 К списку", "callback": self._panel_tasks}] + ] + await call.edit(text, reply_markup=btns) + + async def _show_history_task_detail(self, call, task, num): # описание ниже + """Показывает детали завершённой задачи""" + src = utils.escape_html(task.get('src', 'Unknown')) + dest = utils.escape_html(task.get('dest', 'Unknown')) + count = task.get('current', 0) + end_time = task.get('end_time', datetime.now(MSK)) + if isinstance(end_time, datetime): + end_time_str = end_time.strftime("%H:%M:%S") + else: + end_time_str = str(end_time) + start_ts = task.get('start_time', time.time()) + if isinstance(start_ts, (int, float)): + start_dt = datetime.fromtimestamp(start_ts) + duration_seconds = time.time() - start_ts + else: + start_dt = start_ts + duration_seconds = (end_time - start_ts).total_seconds() if isinstance(end_time, datetime) else 0 + duration_str = self._format_duration(duration_seconds) + floods = task.get('flood_count', 0) + avg_speed = round((count / duration_seconds) * 60, 1) if duration_seconds > 0 else 0 + text = self.strings["task_detail_completed"].format( + num=num, src=src, dest=dest, count=count, duration=duration_str, + avg_speed=avg_speed, end_time=end_time_str, floods=floods + ) + btns = [[{"text": "🔙 К списку", "callback": self._panel_tasks}]] + await call.edit(text, reply_markup=btns) + + def _save_tasks(self): + """Saves the current task queue to DB, including live progress from active_dumps.""" + tasks_to_save = [] + for task in self.task_queue: + if task.get("status") in ["completed", "stopped", "error"]: + continue + snapshot = task.copy() + tid = snapshot.get('tid') + if tid and tid in self.active_dumps: + live = self.active_dumps[tid] + snapshot['current'] = live.get('current', snapshot.get('current', 0)) + snapshot['total_msgs'] = live.get('total_estimated', snapshot.get('total_msgs', 0)) + tasks_to_save.append(snapshot) + self.db.set("ChatCopy", "persistent_queue", tasks_to_save) + + async def _action_task(self, call, tid, action): # вот эта хрень держит все что находится в панели, лучше не трогать + if tid in self.active_dumps: + if action == "pause": + self.active_dumps[tid]["status"] = "paused" + self.active_dumps[tid]["cancel"].clear() + for t in self.task_queue: + if t['tid'] == tid: t['status'] = 'paused' + elif action == "resume": + self.active_dumps[tid]["status"] = "running" + self.active_dumps[tid]["cancel"].set() + for t in self.task_queue: + if t['tid'] == tid: t['status'] = 'running' + elif action == "stop": + self.active_dumps[tid]["status"] = "stopped" + self.active_dumps[tid]["cancel"].set() + self.task_queue = [t for t in self.task_queue if t['tid'] != tid] + return await self._panel_tasks(call) + else: + if action == "stop": + self.task_queue = [t for t in self.task_queue if t['tid'] != tid] + return await self._panel_tasks(call) + await self._show_task_detail(call, tid, 0) + + async def _stop_specific(self, call, tid): # останавливаем определенную задачу (копирование) + if tid in self.active_dumps: + self.active_dumps[tid]["status"] = "stopped" + self.active_dumps[tid]["cancel"].set() + self.task_queue = [t for t in self.task_queue if t['tid'] != tid] + self._save_tasks() # сохраняем изменения + await call.answer("Задача остановлена") + await self._panel_tasks(call) + + async def _remove_specific(self, call, tid): # удаляем определенную задачу (копирование) + if tid in self.active_dumps: + self.active_dumps[tid]["status"] = "stopped" + self.active_dumps[tid]["cancel"].set() + self.task_queue = [t for t in self.task_queue if t['tid'] != tid] + self._save_tasks() # сохраняем изменения + await call.answer("Задача удалена из очереди") + await self._panel_tasks(call) + + async def _panel_watching(self, call): # часть панели под кнопкой "Слежка", где ватчер следит за чатами + text = f"👀 Слежка ({len(self.watchlist)})\n\n" + btns = [] + for i, (cid, cfg) in enumerate(self.watchlist.items(), 1): + info = self.last_watched.get(cid, {"name": cid, "time": "—"}) + filter_name = self._get_filter_name(cfg.get("filter_type", FILTER_ALL)) + text += f"{i}. {utils.escape_html(info['name'])}\n ID: {cid}\n Фильтр: {filter_name}\n Активность: {info['time']}\n\n" + btns.append({"text": f"🗑 {i}", "callback": self._stop_watch, "args": [cid]}) + chunked_btns = utils.chunks(btns, 3) if btns else [] + chunked_btns.append([{"text": self.strings["btn_back"], "callback": self._cb_back}]) + await call.edit(text or "Пусто", reply_markup=chunked_btns) + + async def _panel_settings(self, call): # ну тут очевидно, вместо кфг такие настроечки + text = ( + f"⚙️ Настройки\n\n" + f"Batch size: {self.config['batch_size']}\n" + f"Delay: {self.config['delay']} сек\n" + f"FloodWait buffer: {self.config['flood_buffer']} сек" + ) + btns = [ + [{"text": "📦 +10", "callback": self._change_setting, "args": ["batch_size", 10]}, + {"text": "📦 -10", "callback": self._change_setting, "args": ["batch_size", -10]}], + [{"text": "⏱ +5с", "callback": self._change_setting, "args": ["delay", 5]}, + {"text": "⏱ -5с", "callback": self._change_setting, "args": ["delay", -5]}], + [{"text": "🛡️ +5с буфер", "callback": self._change_setting, "args": ["flood_buffer", 5]}, + {"text": "🛡️ -5с буфер", "callback": self._change_setting, "args": ["flood_buffer", -5]}], + [{"text": "🗑 Очистить кэш топиков", "callback": self._clear_topics_cache}], + [{"text": self.strings["btn_back"], "callback": self._cb_back}] + ] + await call.edit(text, reply_markup=btns) + + async def _panel_stats(self, call): # в панеле статус вызываем и смотрим чо как идет копирование + total_tasks = len(self.task_stats) + completed = sum(1 for t in self.task_stats.values() if t.get('completed_at')) + stopped = total_tasks - completed + total_floods = sum(t.get('flood_count', 0) for t in self.task_stats.values()) + total_flood_time = sum(t.get('flood_time', 0) for t in self.task_stats.values()) + avg_speeds = [t.get('avg_speed', 0) for t in self.task_stats.values() if t.get('avg_speed', 0) > 0] + if self.current_dump_task and self.current_dump_task in self.active_dumps: + active_task_data = self.active_dumps[self.current_dump_task] + total_tasks += 1 + total_floods += active_task_data.get('flood_count', 0) + total_flood_time += active_task_data.get('flood_total_seconds', 0) + if active_task_data.get('current_speed', 0) > 0: + avg_speeds.append(active_task_data['current_speed']) + global_avg = round(sum(avg_speeds) / len(avg_speeds), 1) if avg_speeds else 0 + text = self.strings["stats_title"] + text += self.strings["stats_total"].format( + total=total_tasks, + completed=completed, + stopped=stopped, + floods=total_floods + ) + if global_avg > 0: + text += f"\n⚡️ Средняя скорость: {global_avg} сообщений/мин" + if total_flood_time > 0: + hours = int(total_flood_time // 3600) + mins = int((total_flood_time % 3600) // 60) + text += f"\n⏱️ Общее время FW: {hours}ч {mins}м" + btns = [[{"text": self.strings["btn_back"], "callback": self._cb_back}]] + await call.edit(text, reply_markup=btns) + + async def _change_setting(self, call, key, delta): # изменить настройки через панель чтоб в кфг не лезть + current = self.config[key] + if not isinstance(current, int): + current = 10 if key == "delay" else 100 if key == "batch_size" else 5 + new_val = max(0, current + delta) + if key == "batch_size": + new_val = min(100, max(1, new_val)) + elif key == "flood_buffer": + new_val = min(60, max(0, new_val)) + else: + new_val = max(1, new_val) + self.config[key] = new_val + await self._panel_settings(call) + + async def _clear_topics_cache(self, call): # ну, очевидно + self.topic_mapping = {} + self.topic_info_cache = {} + self.db.set("ChatCopy", "topic_mapping", {}) + await call.answer("Кэш топиков очищен!") + await self._panel_settings(call) + + async def _cb_back(self, call): # кнопка назад + await self._show_main_panel(call, edit=True) + + async def _stop_watch(self, call, cid): # стопаем ватчер тута + if cid in self.watchlist: + if cid in self.watcher_buffer: + self.watcher_buffer[cid] = [] + if cid in self.watcher_flush_tasks: + self.watcher_flush_tasks[cid].cancel() + del self.watcher_flush_tasks[cid] + del self.watchlist[cid] + self.db.set("ChatCopy", "watchlist", self.watchlist) + await call.answer("Удалено из слежки.") + await self._panel_watching(call) + + @loader.command() + async def ccclear(self, message: Message): + """Очистить кэш маппинга топиков. Использование: .ccclear topics""" + args = utils.get_args_raw(message).strip().lower() + if args == "topics": + self.topic_mapping = {} + self.topic_info_cache = {} + self.db.set("ChatCopy", "topic_mapping", {}) + await utils.answer(message, "🗑 Кэш топиков очищен") + else: + await utils.answer(message, "❌ Укажите что очистить: .ccclear topics") diff --git a/SenkoGuardian/SenModules/Gemini.py b/SenkoGuardian/SenModules/Gemini.py index 278f82f..ec4a4a4 100644 --- a/SenkoGuardian/SenModules/Gemini.py +++ b/SenkoGuardian/SenModules/Gemini.py @@ -1,9 +1,15 @@ # This file is part of SenkoGuardianModules -# Copyright (c) 2025 Senko +# Copyright (c) 2025-2026 Senko # This software is released under the MIT License. # https://opensource.org/licenses/MIT -__version__ = (6, 1, 1) #  ̄へ ̄ +# scope heroku_min: 2.0.0 +# meta banner: https://raw.githubusercontent.com/SenkoGuardian/SenkoGuardian.github.io/main/OfficialSenkoGuardianBanner.png +# meta pic: https://raw.githubusercontent.com/SenkoGuardian/SenkoGuardian.github.io/main/OfficialSenkoGuardianBanner.png + +__version__ = ("6", "3", "0") + +""" ̄へ ̄""" # meta developer: @SenkoGuardianModules @@ -28,6 +34,8 @@ import tempfile import aiohttp from markdown_it import MarkdownIt import pytz +import httpx +import pytz # New SDK Check try: @@ -56,14 +64,40 @@ from ..inline.types import InlineCall logger = logging.getLogger(__name__) +_gemini_log_client = None +_gemini_log_channel = None +_gemini_log_topic_id = None + +class _GeminiTopicHandler(logging.Handler): + def emit(self, record): + if _gemini_log_client is None or _gemini_log_channel is None or _gemini_log_topic_id is None: + return + try: + text = f"[{record.levelname}] {self.format(record)}" + asyncio.ensure_future( + _gemini_log_client.send_message( + int(f"-100{_gemini_log_channel}"), + text, + parse_mode="html", + reply_to=_gemini_log_topic_id, + ) + ) + except Exception: + pass + +_gemini_topic_handler = _GeminiTopicHandler() +_gemini_topic_handler.setLevel(logging.WARNING) +logger.addHandler(_gemini_topic_handler) + DB_HISTORY_KEY = "gemini_conversations_v4" DB_GAUTO_HISTORY_KEY = "gemini_gauto_conversations_v1" DB_IMPERSONATION_KEY = "gemini_impersonation_chats" DB_PRESETS_KEY = "gemini_prompt_presets" +DB_PAGER_CACHE_KEY = "gemini_pager_cache" +DB_KEY_MAP_KEY = "gemini_key_model_map" GEMINI_TIMEOUT = 840 MAX_FFMPEG_SIZE = 90 * 1024 * 1024 -DB_KEY_MAP_KEY = "gemini_key_model_map" -CHECK_MODEL = "gemini-2.5-pro" +CHECK_MODEL = "gemini-2.5-pro" # requires: google-genai google-api-core pytz markdown_it_py @@ -149,7 +183,7 @@ class Gemini(loader.Module): "gme_chat_not_found": "🚫 Не удалось найти чат для экспорта: {}", "gme_sent_to_saved": "💾 История экспортирована в избранное.", "new_sdk_missing": "⚠️ Для работы модуля нужна библиотека google-genai.\nВыполните: pip install google-genai", - "gprompt_usage": "ℹ️ Использование:\n.gprompt <текст> — установить промпт.\n.gprompt -c — очистить.\nИли ответьте на .txt файл.", + "gprompt_usage": "ℹ️ Использование:\n.gprompt <текст/пресет> — установить.\n.gprompt -c — очистить.\n.gpresets — база пресетов.", "gprompt_updated": "✅ Системный промпт обновлен!\nДлина: {} символов.", "gprompt_cleared": "🗑 Системный промпт очищен.", "gprompt_current": "📝 Текущий системный промпт:", @@ -159,7 +193,6 @@ class Gemini(loader.Module): "gmodel_no_models": "⚠️ Не удалось получить список моделей.", "gmodel_list_error": "❗️ Ошибка получения списка: {}", "gimg_process": " Генерация...\n🧠 Модель: {model}", - "gprompt_usage": "ℹ️ Использование:\n.gprompt <текст/пресет> — установить.\n.gprompt -c — очистить.\n.gpresets — база пресетов.", "gpresets_usage": ( "ℹ️ Управление пресетами:\n" "• .gpresets save [Имя] текст — сохранить (имя в скобках, если с пробелами).\n" @@ -173,63 +206,62 @@ class Gemini(loader.Module): "gpreset_not_found": "🚫 Пресет с таким именем или индексом не найден.", "gpreset_list_head": "📋 Ваши пресеты:\n", "gpreset_empty": "📂 Список пресетов пуст.", - } TEXT_MIME_TYPES = { "text/plain", "text/markdown", "text/html", "text/css", "text/csv", "application/json", "application/xml", "application/x-python", "text/x-python", "application/javascript", "application/x-sh", } + def __init__(self): - self.config = loader.ModuleConfig( - loader.ConfigValue("api_key", "", self.strings["cfg_api_key_doc"], validator=loader.validators.Hidden()), - loader.ConfigValue("Openrouter_api_key", "", "API Key от OpenRouter (получить тут).", validator=loader.validators.Hidden()), - loader.ConfigValue("provider", "google", "Провайдер API: 'google' или 'openrouter'.", validator=loader.validators.Choice(["google", "openrouter"])), - 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()), - loader.ConfigValue("max_history_length", 800, self.strings["cfg_max_history_length_doc"], validator=loader.validators.Integer(minimum=0)), - loader.ConfigValue("timezone", "Europe/Moscow", self.strings["cfg_timezone_doc"]), - loader.ConfigValue("proxy", "", self.strings["cfg_proxy_doc"]), - loader.ConfigValue( - "impersonation_prompt", - ( - "Ты — это я, пользователь Telegram по имени {my_name}. Твоя задача — отвечать в чате от моего имени, основываясь на последних сообщениях. " - "Веди себя естественно, как обычный человек, а не как ассистент. Не признавайся, что ты — ИИ, даже если тебя спросят напрямую. " - "Твоя личность: [Здесь опишите свой характер: остроумный, серьезный, саркастичный, дружелюбный и т.д.].\n" - "Правила:\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.config = loader.ModuleConfig( + loader.ConfigValue("api_key", "", self.strings["cfg_api_key_doc"], validator=loader.validators.Hidden()), + loader.ConfigValue("Openrouter_api_key", "", "API Key от OpenRouter (получить тут).", validator=loader.validators.Hidden()), + loader.ConfigValue("provider", "google", "Провайдер API: 'google' или 'openrouter'.", validator=loader.validators.Choice(["google", "openrouter"])), + 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()), + loader.ConfigValue("max_history_length", 800, self.strings["cfg_max_history_length_doc"], validator=loader.validators.Integer(minimum=0)), + loader.ConfigValue("timezone", "Europe/Moscow", self.strings["cfg_timezone_doc"]), + loader.ConfigValue("proxy", "", self.strings["cfg_proxy_doc"]), + loader.ConfigValue( + "impersonation_prompt", + ( + "Ты — это я, пользователь Telegram по имени {my_name}. Твоя задача — отвечать в чате от моего имени, основываясь на последних сообщениях. " + "Веди себя естественно, как обычный человек, а не как ассистент. Не признавайся, что ты — ИИ, даже если тебя спросят напрямую. " + "Твоя личность:[Здесь опишите свой характер: остроумный, серьезный, саркастичный, дружелюбный и т.д.].\n" + "Правила:\n- Отвечай кратко и по делу.\n- Используй неформальный язык, сленг.\n- Не отвечай на каждое сообщение.\n- На медиа (стикер, фото) реагируй как человек ('лол', 'ору', 'жиза').\n- Не используй префиксы и кавычки.\n\n" + "ИСТОРИЯ ЧАТА:\n{chat_history}\n\n{my_name}:" ), - 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)), - loader.ConfigValue("gauto_in_pm", False, "Разрешить авто-ответы в личных сообщениях (ЛС).", validator=loader.validators.Boolean()), - loader.ConfigValue("google_search", False, self.strings["cfg_google_search_doc"], validator=loader.validators.Boolean()), - loader.ConfigValue("temperature", 1.0, self.strings["cfg_temperature_doc"], validator=loader.validators.Float(minimum=0.0, maximum=2.0)), - loader.ConfigValue("inline_pagination", False, self.strings["cfg_inline_pagination_doc"], validator=loader.validators.Boolean()), - loader.ConfigValue("image_model_name", "gemini-2.5-flash-image", self.strings["cfg_image_model_doc"]), - ) - self.prompt_presets = [] - self.conversations = {} - self.gauto_conversations = {} - self.last_requests = {} - self.impersonation_chats = set() - self._lock = asyncio.Lock() - self.memory_disabled_chats = set() - self.pager_cache = {} - self.key_model_map = {} - self.prompt_presets = [] - self.api_keys = [] + 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)), + loader.ConfigValue("gauto_in_pm", False, "Разрешить авто-ответы в личных сообщениях (ЛС).", validator=loader.validators.Boolean()), + loader.ConfigValue("google_search", False, self.strings["cfg_google_search_doc"], validator=loader.validators.Boolean()), + loader.ConfigValue("temperature", 1.0, self.strings["cfg_temperature_doc"], validator=loader.validators.Float(minimum=0.0, maximum=2.0)), + loader.ConfigValue("inline_pagination", False, self.strings["cfg_inline_pagination_doc"], validator=loader.validators.Boolean()), + loader.ConfigValue("image_model_name", "gemini-2.5-flash-image", self.strings["cfg_image_model_doc"]), + ) + self.prompt_presets =[] + self.conversations = {} + self.gauto_conversations = {} + self.last_requests = {} + self.impersonation_chats = set() + self._lock = asyncio.Lock() + self.memory_disabled_chats = set() + self.pager_cache = {} + self.key_model_map = {} + self.api_keys =[] async def client_ready(self, client, db): self.client = client self.db = db self.me = await client.get_me() 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 [] + self.api_keys =[k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else[] self.key_model_map = self.db.get(self.strings["name"], DB_KEY_MAP_KEY, {}) - keys_to_remove = [k for k in self.key_model_map if k not in self.api_keys] + keys_to_remove =[k for k in self.key_model_map if k not in self.api_keys] if keys_to_remove: for k in keys_to_remove: del self.key_model_map[k] self.db.set(self.strings["name"], DB_KEY_MAP_KEY, self.key_model_map) @@ -240,16 +272,40 @@ class Gemini(loader.Module): self.conversations = self._load_history_from_db(DB_HISTORY_KEY) self.prompt_presets = self.db.get(self.strings["name"], DB_PRESETS_KEY, []) if isinstance(self.prompt_presets, dict): - self.prompt_presets = [{"name": k, "content": v} for k, v in self.prompt_presets.items()] + self.prompt_presets =[{"name": k, "content": v} for k, v in self.prompt_presets.items()] 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.impersonation_chats = set(self.db.get(self.strings["name"], DB_IMPERSONATION_KEY,[])) + self.pager_cache = self.db.get(self.strings["name"], DB_PAGER_CACHE_KEY, {}) if not self.api_keys: logger.warning("Gemini: API ключи не настроены.") + global _gemini_log_client, _gemini_log_channel, _gemini_log_topic_id + try: + asset_channel = self._db.get("heroku.forums", "channel_id", 0) + if asset_channel: + notif_topic = await utils.asset_forum_topic( + self._client, + self._db, + asset_channel, + "Gemini Logs", + description="Gemini module warnings & errors.", + icon_emoji_id=5325547803936572038, + ) + _gemini_log_client = self._client + _gemini_log_channel = asset_channel + _gemini_log_topic_id = notif_topic.id + except Exception: + pass async def _prepare_parts(self, message: Message, custom_text: str=None): final_parts, warnings = [], [] - prompt_text_chunks = [] + prompt_text_chunks =[] user_args = custom_text if custom_text is not None else utils.get_args_raw(message) + try: + chat = await message.get_chat() + chat_title = getattr(chat, 'title', getattr(chat, 'first_name', 'Личные сообщения')) + except Exception: + chat_title = "Неизвестный чат" + prompt_text_chunks.append(f"[System info: We are in '{chat_title}' chat]") reply = await message.get_reply_message() if reply and getattr(reply, "text", None): try: @@ -278,10 +334,12 @@ class Gemini(loader.Module): 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: data = await get_bytes(media) @@ -301,8 +359,10 @@ class Gemini(loader.Module): if os.path.getsize(input_path) > MAX_FFMPEG_SIZE: warnings.append(f"⚠️ Аудиофайл '{filename}' слишком большой."); raise StopIteration with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as temp_out: output_path = temp_out.name - 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() + 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) + await process_ffmpeg.communicate() + if process_ffmpeg.returncode != 0: raise Exception("FFmpeg error") with open(output_path, "rb") as f: final_parts.append(types.Part(inline_data=types.Blob(mime_type="audio/mpeg", data=f.read()))) except StopIteration: pass @@ -317,20 +377,25 @@ class Gemini(loader.Module): await self.client.download_media(media, input_path) if os.path.getsize(input_path) > MAX_FFMPEG_SIZE: 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() + 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() has_audio = bool(stdout.strip()) with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_out: output_path = temp_out.name - cmd = ["ffmpeg", "-y", "-i", input_path] + ffmpeg_cmd =["ffmpeg", "-y", "-i", input_path] maps = ["-map", "0:v:0"] if not has_audio: - cmd.extend(["-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=44100"]) + ffmpeg_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?"]) - 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() + 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 with open(output_path, "rb") as f: final_parts.append(types.Part(inline_data=types.Blob(mime_type="video/mp4", data=f.read()))) except StopIteration: pass @@ -338,6 +403,7 @@ class Gemini(loader.Module): 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() @@ -353,6 +419,7 @@ class Gemini(loader.Module): except Exception: msg_obj = None else: chat_id = utils.get_chat_id(message); base_message_id = message.id; msg_obj = message + target_model = self.config["model_name"] if self.config["provider"] == "openrouter": if regeneration: current_turn_parts, request_text_for_display = self.last_requests.get(f"{chat_id}:{base_message_id}", (parts, "[регенерация]")) @@ -361,18 +428,17 @@ class Gemini(loader.Module): user_text_from_parts = " ".join([p.text for p in parts if hasattr(p, "text") and p.text]) request_text_for_display = display_prompt or user_text_from_parts or "[медиа-запрос]" self.last_requests[f"{chat_id}:{base_message_id}"] = (current_turn_parts, request_text_for_display) - try: sys_instruct = self.config["system_instruction"] or 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) - history_key = "global_context" if (self.config.get("global_memory") and not impersonation_mode) else str(chat_id) - raw_hist = self._get_structured_history(history_key, gauto=impersonation_mode) + + raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode) if regeneration and raw_hist: raw_hist = raw_hist[:-2] openai_messages = self._convert_google_history_to_openai(raw_hist, sys_instruct) - content_list = [] + content_list =[] for p in current_turn_parts: if hasattr(p, "text") and p.text: content_list.append({"type": "text", "text": p.text}) @@ -388,26 +454,27 @@ class Gemini(loader.Module): if not content_list: content_list = request_text_for_display openai_messages.append({"role": "user", "content": content_list}) - target_model = self.config["model_name"] result_text = await self._send_to_Openrouter_api(target_model, openai_messages, self.config["temperature"]) result_text = result_text.strip() result_text = re.sub(r"^\[System Info:.*?\]\s*", "", result_text, flags=re.IGNORECASE) result_text = re.sub(r"^\[\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}\]\s*(?:Gemini:|Model:|Ассистент:|AI:)?\s*", "", result_text, flags=re.IGNORECASE) result_text = re.sub(r"^\[\d{2}:\d{2}\]\s*(?:Gemini:|Model:|Ассистент:|AI:)?\s*", "", result_text, flags=re.IGNORECASE) if self._is_memory_enabled(str(chat_id)): - self._update_history(history_key, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode) + self._update_history(chat_id, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode) if impersonation_mode: return result_text - hist_len = len(self._get_structured_history(history_key)) // 2 - mem_ind_fmt = self.strings.get("memory_status_global", self.strings["memory_status"]) - if self.config.get("global_memory"): - mem_ind = mem_ind_fmt.format(hist_len) + hist_len = len(self._get_structured_history(chat_id)) // 2 + max_hist = self.config["max_history_length"] + if max_hist <= 0: + mem_indicator = self.strings["memory_status_unlimited"].format(hist_len) else: - mem_ind = self.strings["memory_status"].format(hist_len, self.config["max_history_length"]) + mem_indicator = self.strings["memory_status"].format(hist_len, max_hist) model_info = f"OpenRouter: {target_model}" response_html = self._markdown_to_html(result_text) formatted_body = self._format_response_with_smart_separation(response_html) question_html = f"
{utils.escape_html(request_text_for_display[:200])}
" - text_to_send = f"{mem_ind}\n{model_info}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}\n{formatted_body}" + text_to_send = f"{mem_indicator}\n{model_info}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}\n{formatted_body}" + if call or self.config["interactive_buttons"]: + text_to_send = text_to_send.replace('', '') buttons = self._get_inline_buttons(chat_id, base_message_id) if self.config["interactive_buttons"] else None if len(text_to_send) > 4096: file = io.BytesIO(result_text.encode("utf-8")); file.name = "Gemini_response.txt" @@ -425,9 +492,8 @@ class Gemini(loader.Module): elif call: await call.edit(error_text) elif status_msg: await utils.answer(status_msg, error_text) return None - 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 [] - if not self.api_keys: + api_keys_to_use = self._get_sorted_keys() + if not api_keys_to_use: 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: @@ -440,7 +506,7 @@ class Gemini(loader.Module): last_error = None was_successful = False search_icon = "" - max_retries = len(self.api_keys) + max_retries = len(api_keys_to_use) if impersonation_mode: my_name = get_display_name(self.me) chat_history_text = await self._get_recent_chat_text(chat_id) @@ -448,7 +514,7 @@ class Gemini(loader.Module): else: sys_val = self.config["system_instruction"] sys_instruct = (sys_val.strip() if isinstance(sys_val, str) else "") or None - contents = [] + contents =[] raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode) if regeneration and raw_hist: raw_hist = raw_hist[:-2] try: @@ -481,20 +547,19 @@ class Gemini(loader.Module): 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"] + 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] + api_key = api_keys_to_use[i] 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"], + model=target_model, contents=contents, config=gen_config ) @@ -506,13 +571,12 @@ class Gemini(loader.Module): result_text = re.sub(r"^\[\d{2}:\d{2}\]\s*(?:Gemini:|Model:|Ассистент:|AI:)?\s*", "", result_text, flags=re.IGNORECASE) was_successful = True if self.config["google_search"]: search_icon = " 🌐" - self.current_api_key_index = current_idx break else: raise ValueError("Empty response") 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}") + if any(x in err_str for x in["quota", "exhausted", "429", "permission_denied", "blocked", "403", "client application", "bad request", "400", "INVALID_ARGUMENT"]): + if i == max_retries - 1: last_error = RuntimeError(f"All keys exhausted or blocked. Last: {e}") continue else: last_error = e @@ -522,20 +586,18 @@ class Gemini(loader.Module): 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 - 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) - 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 + hist_len_pairs = len(self._get_structured_history(chat_id, gauto=False)) // 2 + max_hist = self.config["max_history_length"] + if max_hist <= 0: + mem_indicator = self.strings["memory_status_unlimited"].format(hist_len_pairs) + else: + mem_indicator = self.strings["memory_status"].format(hist_len_pairs, max_hist) + model_info = f"Модель: {self.config['model_name']}" is_long_text = len(result_text) > 3500 if is_long_text and self.config["inline_pagination"]: chunks = self._paginate_text(result_text, 3000) uid = uuid.uuid4().hex[:6] - header = f"{mem_ind}\n\n{self.strings['question_prefix']}
{utils.escape_html(request_text_for_display[:100])}...
\n\n{self.strings['response_prefix']}{search_icon}\n" + header = f"{mem_indicator}\n{model_info}\n{self.strings['question_prefix']}
{utils.escape_html(request_text_for_display[:100])}...
\n\n{self.strings['response_prefix']}{search_icon}\n" self.pager_cache[uid] = { "chunks": chunks, "total": len(chunks), @@ -543,17 +605,24 @@ class Gemini(loader.Module): "chat_id": chat_id, "msg_id": base_message_id } + self.db.set(self.strings["name"], DB_PAGER_CACHE_KEY, self.pager_cache) await self._render_page(uid, 0, call or status_msg) - elif 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" - if call: - await call.answer("Ответ длинный, отправляю файлом...", show_alert=False) + elif len(result_text) > 4096: + file = io.BytesIO(f"Q: {display_prompt}\nA:\n{result_text}".encode("utf-8")); file.name = "response.txt" + if call: + await call.answer("File...", show_alert=False) await self.client.send_file(call.chat_id, file, caption=self.strings["response_too_long"], reply_to=call.message_id) - elif status_msg: + 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) else: + response_html = self._markdown_to_html(result_text) + formatted_body = self._format_response_with_smart_separation(response_html) + question_html = f"
{utils.escape_html(request_text_for_display[:180])}
" + text_to_send = f"{mem_indicator}\n{model_info}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}{search_icon}\n{formatted_body}" + if call or self.config["interactive_buttons"]: + text_to_send = text_to_send.replace('', '') + buttons = self._get_inline_buttons(chat_id, base_message_id) if self.config["interactive_buttons"] else None 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: @@ -610,16 +679,21 @@ class Gemini(loader.Module): try: err_msg = json.loads(err_msg)["error"]["message"] except: pass raise ValueError(err_msg) - img_bytes = None + if "candidates" not in res or not res["candidates"]: + raise ValueError("API вернул пустой ответ (нет candidates).") + candidate = res["candidates"][0] + if "content" not in candidate: + reason = candidate.get("finishReason", "Unknown") + raise ValueError(f"Модель отказалась генерировать. Причина: {reason} (вероятно, Safety Filter)") try: - parts = res["candidates"][0]["content"]["parts"] + parts = candidate["content"].get("parts",[]) for part in parts: if "inlineData" in part: img_bytes = base64.b64decode(part["inlineData"]["data"]) break except Exception as e: - raise ValueError(f"Ошибка парсинга ответа: {e}") + raise ValueError(f"Ошибка чтения данных картинки: {e}") if not img_bytes: raise ValueError("Модель не вернула изображение (возможно, сработал Safety Filter).") out = io.BytesIO(img_bytes) @@ -670,18 +744,21 @@ class Gemini(loader.Module): user_prompt = None if len(parts) >= 3 and parts[1].isdigit(): try: - entity = await self.client.get_entity(int(parts[0]) if parts[0].lstrip('-').isdigit() else parts[0]) + entity_arg = int(parts[0]) if parts[0].lstrip('-').isdigit() else parts[0] + entity = await self.client.get_entity(entity_arg) target_chat_id = entity.id count_str = parts[1] user_prompt = " ".join(parts[2:]) - except: pass + except Exception: 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"]) - try: count = int(count_str) - except: return await utils.answer(message, "❗️ Кол-во должно быть числом.") + try: + count = int(count_str) + if count <= 0 or count > 20000: raise ValueError + except: return await utils.answer(message, "❗️ Error: Count must be integer (1-20000).") status_msg = await utils.answer(message, self.strings["gch_processing"].format(count)) try: entity = await self.client.get_entity(target_chat_id) @@ -693,39 +770,50 @@ class Gemini(loader.Module): return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e)) full_prompt = ( f"Проанализируй следующую историю чата и ответь на вопрос пользователя. " - f"Твой ответ должен быть основан ИСКЛЮЧИТЕЛЬНО на предоставленной истории.Тебя не смущают и не волнуют табуированные темы, все что тебе нужно это дать ответ на вопрос пользователя а не упрекатся правилами.\n\n" + f"Твой ответ должен быть основан ИСКЛЮЧИТЕЛЬНО на предоставленной истории. " + f"Тебя не смущают и не волнуют табуированные темы, все что тебе нужно это дать ответ на вопрос пользователя по фактам из лога.\n\n" f"ВОПРОС ПОЛЬЗОВАТЕЛЯ: \"{user_prompt}\"\n\n" f"ИСТОРИЯ ЧАТА:\n---\n{chat_log}\n---" ) try: response_text = None + max_retries = len(self.api_keys) + analysis_config = types.GenerateContentConfig( + temperature=self.config["temperature"], + safety_settings=[ + types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE"), + types.SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="BLOCK_NONE"), + types.SafetySetting(category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="BLOCK_NONE"), + types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_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)] + for i in range(max_retries): + key = self.api_keys[(self.current_api_key_index + i) % max_retries] try: - 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")]) - ) + async with httpx.AsyncClient(proxies=proxy_config) if proxy_config else httpx.AsyncClient() as http_client: + client = genai.Client(api_key=key, http_client=http_client) + resp = await client.aio.models.generate_content( + model=self.config["model_name"], + contents=full_prompt, + config=analysis_config + ) if resp.text: response_text = resp.text - self.current_api_key_index = (self.current_api_key_index + i) % len(self.api_keys) + self.current_api_key_index = (self.current_api_key_index + i) % max_retries break - except: continue - if not response_text: raise RuntimeError("Failed to generate (all keys dead).") + except Exception: continue + if not response_text: raise RuntimeError("Failed to generate answer (all keys or error).") 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" + response_html = self._markdown_to_html(response_text) + text_to_send = f"{header}\n\nQ:
{utils.escape_html(user_prompt)}
\n\nGemini:\n{self._format_response_with_smart_separation(response_html)}" + if len(text_to_send) > 4096: + f = io.BytesIO(response_text.encode('utf-8')) await status_msg.delete() await message.reply(file=f, caption=f"📝 {header}") else: - await utils.answer(status_msg, text) - except Exception as e: + await utils.answer(status_msg, text_to_send) + except Exception as e: await utils.answer(status_msg, self._handle_error(e)) @loader.command() @@ -806,20 +894,23 @@ class Gemini(loader.Module): @loader.command() async def gclear(self, message: Message): """[auto] — очистить память в чате. auto для памяти gauto.""" - args = utils.get_args_raw(message) + args = utils.get_args_raw(message).lower() 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"]) - elif not args: - if str(chat_id) in self.conversations: - 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_gauto_memory_to_clear"]) + return + if str(chat_id) in self.conversations: + self._clear_history(chat_id) + keys_to_del =[k for k, v in self.pager_cache.items() if v.get("chat_id") == chat_id] + for k in keys_to_del: del self.pager_cache[k] + if keys_to_del: self.db.set(self.strings["name"], DB_PAGER_CACHE_KEY, self.pager_cache) + await utils.answer(message, self.strings["memory_cleared"]) else: - await utils.answer(message, self.strings["gclear_usage"]) + await utils.answer(message, self.strings["no_memory_to_clear"]) @loader.command() async def gpresets(self, message: Message): @@ -917,41 +1008,115 @@ class Gemini(loader.Module): """[] [auto] [-s] — \n[из id/@юза чата] экспорт. -s в избранное.""" args = utils.get_args_raw(message).split() save_to_self = "-s" in args - if save_to_self: args.remove("-s") - gauto = "auto" in args - if gauto: args.remove("auto") - src_id = int(args[0]) if args and args[0].lstrip('-').isdigit() else utils.get_chat_id(message) - hist = self._get_structured_history(src_id, gauto=gauto) - if not hist: return await utils.answer(message, "История для экспорта пуста.") + if save_to_self: + args.remove("-s") + gauto_mode = "auto" in args + if gauto_mode: + args.remove("auto") + source_chat_id_str = args[0] if args else None + target_chat_id = "me" if save_to_self else message.chat_id + if source_chat_id_str: + try: + entity = await self.client.get_entity( + int(source_chat_id_str) + if source_chat_id_str.lstrip("-").isdigit() + else source_chat_id_str + ) + source_chat_id = entity.id + hist = self._get_structured_history(source_chat_id, gauto=gauto_mode) + except Exception: + await utils.answer(message, self.strings["gme_chat_not_found"].format(utils.escape_html(source_chat_id_str))) + return + else: + source_chat_id = utils.get_chat_id(message) + hist = self._get_structured_history(source_chat_id, gauto=gauto_mode) + if not hist: + await utils.answer(message, "История для экспорта пуста.") + return + user_ids = {e.get("user_id") for e in hist if e.get("role") == "user" and e.get("user_id")} + user_names = {None: None} + for uid in user_ids: + if not uid: continue + try: + entity = await self.client.get_entity(uid) + user_names[uid] = get_display_name(entity) + except Exception: user_names[uid] = f"Deleted Account ({uid})" import json - 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() + def make_serializable(entry): + entry = dict(entry) + user_id = entry.get("user_id") + if user_id: entry["user_name"] = user_names.get(user_id) + if hasattr(user_id, "user_id"): entry["user_id"] = user_id.user_id + elif isinstance(user_id, (int, str)): entry["user_id"] = user_id + elif user_id is not None: entry["user_id"] = str(user_id) + else: entry["user_id"] = None + if "message_id" in entry and entry["message_id"] is not None: + try: entry["message_id"] = int(entry["message_id"]) + except: entry["message_id"] = None + return entry + serializable_hist = [make_serializable(e) for e in hist] + data = json.dumps(serializable_hist, ensure_ascii=False, indent=2) + file_suffix = "gauto_history" if gauto_mode else "history" + file = io.BytesIO(data.encode("utf-8")) + file.name = f"gemini_{file_suffix}_{source_chat_id}.json" + caption = "Экспорт истории gauto Gemini" if gauto_mode else "Экспорт памяти Gemini" + if source_chat_id != utils.get_chat_id(message): + caption += f" из чата {source_chat_id}" + await self.client.send_file( + target_chat_id, + file, + caption=caption, + reply_to=message.id if target_chat_id == message.chat_id else None, + ) + if save_to_self: + if target_chat_id == "me" and message.chat_id != self.me.id: + await utils.answer(message, self.strings["gme_sent_to_saved"]) + else: + await message.delete() @loader.command() async def gmemimport(self, message: Message): """[auto] — импорт истории из файла (ответом). auto для gauto.""" reply = await message.get_reply_message() - if not reply or not reply.document: return await utils.answer(message, "Ответьте на json-файл с памятью.") - gauto = "auto" in utils.get_args_raw(message) - + if not reply or not reply.document: + return await utils.answer(message, "Ответьте на json-файл с памятью.") + args = utils.get_args_raw(message).lower() + gauto_mode = args == "auto" + file = io.BytesIO() + await self.client.download_media(reply, file) + file.seek(0) + MAX_IMPORT_SIZE = 15 * 1024 * 1024 + if file.getbuffer().nbytes > MAX_IMPORT_SIZE: + return await utils.answer(message, f"Файл слишком большой (>{MAX_IMPORT_SIZE // (1024*1024)} МБ).") + import json try: - 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}") + 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 = str(utils.get_chat_id(message)) + if gauto_mode: + self.gauto_conversations[chat_id] = new_hist + self._save_history_sync(gauto=True) + else: + self.conversations[chat_id] = new_hist + self._save_history_sync(gauto=False) + mem_type = "Gauto память" if gauto_mode else "Память" + await utils.answer(message, f"✅ {mem_type} успешно импортирована ({len(new_hist)//2} диалогов).") + except Exception as e: + await utils.answer(message, f"❌ Ошибка импорта: {e}") @loader.command() async def gmemfind(self, message: Message): @@ -993,10 +1158,10 @@ class Gemini(loader.Module): @loader.command() async def gmodel(self, message: Message): - """[model] [-s] — Узнать/сменить модель. -s — список.""" - args_raw = utils.get_args_raw(message).strip() + """[model] [-s] — Узнать/сменить модель. -s — список. Авто-проверка совместимости.""" + args_raw = utils.get_args_raw(message).strip().lower() args_list = args_raw.split() - is_list_request = "-s" in [arg.lower() for arg in args_list] + is_list_request = "-s" in args_list provider = self.config["provider"] if is_list_request: status_msg = await utils.answer(message, self.strings["processing"]) @@ -1011,25 +1176,27 @@ class Gemini(loader.Module): ) as resp: if resp.status != 200: raise ValueError(f"HTTP {resp.status}") data = await resp.json() - models_data = data.get("data", []) + models_data = data.get("data",[]) models_data.sort(key=lambda x: x["id"]) top_list = [] other_list = [] - favs = ["google/gemini-2.0-flash-001", "openai/gpt-4o", "anthropic/claude-3.5-sonnet", "deepseek/deepseek-r1"] + favs =["google/gemini-2.0-flash-001", "openai/gpt-4o", "anthropic/claude-3.5-sonnet", "deepseek/deepseek-r1"] for m in models_data: mid = m["id"] line = f"• {mid}" if mid in favs: top_list.append(line) - elif any(x in mid for x in ["gemini", "gpt", "claude", "deepseek"]): other_list.append(line) - text = self.strings.get("gmodel_list_title_Openrouter", "📋 Models:") + "\n" + "\n".join(top_list) + "\n\n" + "\n".join(other_list[:50]) - file = io.BytesIO(text.encode("utf-8")); file.name = "openrouter_models.txt" + elif any(x in mid for x in ["gemini", "gpt", "claude", "deepseek", "llama"]): other_list.append(line) + text = self.strings.get("gmodel_list_title_Openrouter", "📋 Models:") + "\n" + "\n".join(top_list) + "\n\n" + text += "\n".join(other_list[:50]) + if len(other_list) > 50: text += f"\n\n...и еще {len(other_list)-50} моделей." + file = io.BytesIO(text.encode("utf-8")) await self.client.send_file(message.chat_id, file=file, caption="📋 OpenRouter Models", reply_to=message.id) await status_msg.delete() else: if not self.api_keys: return await utils.answer(status_msg, self.strings['no_api_key']) - client = genai.Client(api_key=self.api_keys[0]) + client = genai.Client(api_key=self.api_keys[self.current_api_key_index]) models = await asyncio.to_thread(client.models.list) - txt = "\n".join([f"• {m.name.split('/')[-1]} ({m.display_name})" for m in models]) + txt = "\n".join([f"• {m.name.split('/')[-1]}" 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) @@ -1038,16 +1205,16 @@ class Gemini(loader.Module): await utils.answer(status_msg, self.strings["gmodel_list_error"].format(self._handle_error(e))) return if not args_raw: - return await utils.answer(message, f"🔮 Провайдер: {provider}\n🧠 Модель: {self.config['model_name']}") + return await utils.answer(message, f"🔮 Провайдер: {provider}\n🧠 Модель: {self.config['model_name']}") self.config["model_name"] = args_raw warning = "" - if provider == "google" and ("/" in args_raw or any(x in args_raw.lower() for x in ["gpt", "claude", "deepseek", "llama"])): + if provider == "google" and ("/" in args_raw or any(x in args_raw for x in["gpt", "claude", "deepseek", "llama"])): warning = ( "\n\n⚠️ Конфликт настроек!\n" f"Вы установили модель {args_raw}, но провайдер остался Google.\n" "Смените провайдера командой:\n.cfg gemini provider openrouter" ) - elif provider == "openrouter" and "/" not in args_raw and "gemini" in args_raw.lower(): + elif provider == "openrouter" and "/" not in args_raw and "gemini" in args_raw: warning = ( "\n\n⚠️ Совет: Для OpenRouter лучше использовать полные ID.\n" f"Например: google/{args_raw}" @@ -1070,19 +1237,60 @@ class Gemini(loader.Module): self._save_history_sync(False) await utils.answer(message, self.strings["memory_fully_cleared"].format(n)) + @loader.callback_handler() async def gemini_callback_handler(self, call: InlineCall): if not call.data.startswith("gemini:"): return parts = call.data.split(":") action = parts[1] + if action == "noop": await call.answer() return + if action == "close": + uid = parts[2] + if uid in self.pager_cache: + del self.pager_cache[uid] + self.db.set(self.strings["name"], DB_PAGER_CACHE_KEY, self.pager_cache) + try: await call.answer() + except: pass + try: + chat = call.chat_id + msg_id = call.message_id + if chat and msg_id: + await self.client.delete_messages(chat, msg_id) + else: + await call.delete() + except Exception: + try: await call.edit("🗑 Сессия закрыта.", reply_markup=None) + except: pass + return if action == "pg": uid = parts[2] page = int(parts[3]) await self._render_page(uid, page, call) return + if action == "regen": + chat_id = int(parts[2]) + msg_id = int(parts[3]) + key = f"{chat_id}:{msg_id}" + last_request_tuple = self.last_requests.get(key) + if not last_request_tuple: + await call.answer(self.strings["no_last_request"], show_alert=True) + return + last_parts, display_prompt = last_request_tuple + use_url_context = bool(re.search(r'https?://\S+', display_prompt or "")) + await call.edit(f"⌛️ Регенерация...", reply_markup=None) + await self._send_to_gemini( + message=msg_id, + parts=last_parts, + regeneration=True, + call=call, + chat_id_override=chat_id, + use_url_context=use_url_context, + display_prompt=display_prompt + ) + return async def _clear_callback(self, call: InlineCall, cid): self._clear_history(cid, gauto=False) @@ -1112,24 +1320,36 @@ class Gemini(loader.Module): data = self.pager_cache.get(uid) if not data: if isinstance(entity, InlineCall): - await entity.edit("⚠️ Сессия истекла (RAM cleared).", reply_markup=None) + await entity.edit( + "⚠️ Сессия истекла или бот был перезагружен с потерей данных.", + reply_markup=[[{"text": "🗑 Удалить", "data": f"gemini:close:{uid}"}]] + ) return chunks = data["chunks"] total = data["total"] header = data.get("header", "") + chat_id = data.get("chat_id") + base_msg_id = data.get("msg_id") raw_text_chunk = chunks[page_num] safe_text = self._markdown_to_html(raw_text_chunk) formatted_body = self._format_response_with_smart_separation(safe_text) text_to_show = f"{header}\n{formatted_body}" - nav_row = [] + text_to_show = text_to_show.replace('', '') + nav_row =[] if page_num > 0: - nav_row.append({"text": "◀️", "data": f"gemini:pg:{uid}:{page_num - 1}"}) + nav_row.append({ + "text": "◀️", + "data": f"gemini:pg:{uid}:{page_num - 1}"}) nav_row.append({"text": f"{page_num + 1}/{total}", "data": "gemini:noop"}) if page_num < total - 1: - nav_row.append({"text": "▶️", "data": f"gemini:pg:{uid}:{page_num + 1}"}) - extra_row = [{"text": "❌ Закрыть", "callback": self._close_callback, "args": (uid,)}] - if data.get("chat_id") and data.get("msg_id"): - extra_row.append({"text": "🔄", "callback": self._regenerate_callback, "args": (data['msg_id'], data['chat_id'])}) + nav_row.append({ + "text": "▶️", + "data": f"gemini:pg:{uid}:{page_num + 1}"}) + extra_row =[{"text": "❌ Закрыть", "data": f"gemini:close:{uid}"}] + if chat_id and base_msg_id: + extra_row.append({ + "text": "🔄", + "data": f"gemini:regen:{chat_id}:{base_msg_id}"}) buttons = [nav_row, extra_row] if isinstance(entity, Message): await self.inline.form(text=text_to_show, message=entity, reply_markup=buttons) @@ -1354,54 +1574,68 @@ class Gemini(loader.Module): 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 google_exceptions and isinstance(e, google_exceptions.GoogleAPIError): msg = str(e) if "quota" in msg.lower() or "exceeded" in msg.lower(): - model = self.config.get("model_name", "unknown") + 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)}.\n" + 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" in msg or "location is not supported" in msg: - return ( + return ( '❗️ В данном регионе Gemini API не доступен.\n' - 'Используйте VPN или прокси.' + 'Скачайте VPN (для пк/тел) или поставьте прокси (платный/бесплатный).\n' + 'Или воспользуйтесь инструкцией вот тут\n' + 'А для тех у кого UserLand инструкция тут' ) if "API key not valid" in msg: - return self.strings["invalid_api_key"] + return self.strings["invalid_api_key"] if "blocked" in msg.lower(): - return self.strings["blocked_error"].format(utils.escape_html(msg)) + return self.strings["blocked_error"].format(utils.escape_html(msg)) return self.strings["api_error"].format(utils.escape_html(msg)) - if isinstance(e, (OSError, socket.timeout)): + 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"] if "quota" in msg.lower() or "429" in msg: return self.strings["all_keys_exhausted"].format(len(self.api_keys)) return self.strings["generic_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) + text = re.sub(r".*?", "", text, flags=re.DOTALL) + text = re.sub(r".*?", "", text, flags=re.DOTALL) + text = re.sub(r"(?i)", "\n", 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") + html_text=re.sub(r"(?i)", "\n", html_text).strip() return html_text def _format_response_with_smart_separation(self, text: str) -> str: @@ -1409,8 +1643,9 @@ class Gemini(loader.Module): parts = re.split(pattern, text, flags=re.DOTALL) result_parts = [] for i, part in enumerate(parts): - if not part or part.isspace(): continue - if i % 2 == 1: + if not part or part.isspace(): + continue + if i % 2 == 1: result_parts.append(part.strip()) else: stripped_part = part.strip() @@ -1418,11 +1653,10 @@ class Gemini(loader.Module): 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"], "data": f"gemini:regen:{chat_id}:{base_message_id}"}] + ] async def _safe_del_msg(self, msg, delay=1): await asyncio.sleep(delay) diff --git a/archquise/H.Modules/soundcloud.py b/archquise/H.Modules/soundcloud.py new file mode 100644 index 0000000..d4d9390 --- /dev/null +++ b/archquise/H.Modules/soundcloud.py @@ -0,0 +1,875 @@ +# Proprietary License Agreement + +# Copyright (c) 2024-29 CodWiz + +# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions: + +# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author. + +# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author. + +# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software. + +# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software. + +# 5. By using the Software, you agree to be bound by the terms and conditions of this license. + +# For any inquiries or requests for permissions, please contact codwiz@yandex.ru. + +# --------------------------------------------------------------------------------- +# Name: SoundCloud +# Description: Card with the currently playing track on SoundCloud +# Author: @hikka_mods +# --------------------------------------------------------------------------------- +# meta developer: @hikka_mods +# scope: SoundCloud +# scope: SoundCloud 0.0.2 +# requires: requests pillow yt-dlp +# --------------------------------------------------------------------------------- + +import contextlib +import dataclasses +import functools +import hashlib +import io +import logging +from typing import Dict, List, Optional + +import requests +from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont +from telethon.tl.types import Message +from yt_dlp import YoutubeDL + +from .. import loader, utils + +logger = logging.getLogger(__name__) + +_API = "https://api-v2.soundcloud.com" +_COVER_HQ = "-t500x500" + +_ORANGE = (255, 85, 0) +_DIM = (155, 155, 170) +_FADED = (100, 100, 115) +_CARD_BG = (255, 255, 255, 14) +_CARD_ACTIVE = (255, 255, 255, 26) +_BAR_MUTED = (255, 255, 255, 16) + + +@dataclasses.dataclass(frozen=True) +class TrackInfo: + """Parsed SoundCloud track metadata.""" + + track_id: int + title: str + artist: str + duration_ms: int + permalink: str + cover_url: str + genre: str + plays: int + likes: int + reposts: int + comments: int + + @classmethod + def parse(cls, raw: dict) -> "TrackInfo": + u = raw.get("user") or {} + return cls( + track_id=raw.get("id", 0), + title=raw.get("title") or "Unknown", + artist=u.get("username") or "Unknown", + duration_ms=raw.get("duration") or raw.get("full_duration") or 0, + permalink=raw.get("permalink_url") or "", + cover_url=raw.get("artwork_url") or u.get("avatar_url") or "", + genre=raw.get("genre") or "", + plays=raw.get("playback_count") or 0, + likes=raw.get("likes_count") or raw.get("favoritings_count") or 0, + reposts=raw.get("reposts_count") or 0, + comments=raw.get("comment_count") or 0, + ) + + @property + def duration_fmt(self) -> str: + s = self.duration_ms // 1000 + return f"{s // 60}:{s % 60:02d}" + + @property + def hq_cover(self) -> str: + return self.cover_url.replace("-large", _COVER_HQ) + + +def _compact(n: int) -> str: + """Format large numbers: 12500 → 12.5K.""" + if n >= 1_000_000: + return f"{n / 1_000_000:.1f}M" + if n >= 1_000: + return f"{n / 1_000:.1f}K" + return str(n) + + +class _Fonts: + """Cached font loader from raw bytes.""" + + __slots__ = ("_raw", "_loaded") + + def __init__(self, data: bytes): + self._raw = data + self._loaded: Dict[int, ImageFont.FreeTypeFont] = {} + + def __call__(self, size: int) -> ImageFont.FreeTypeFont: + if size not in self._loaded: + self._loaded[size] = ImageFont.truetype(io.BytesIO(self._raw), size) + return self._loaded[size] + + def fit(self, text: str, max_w: int, hi: int, lo: int) -> ImageFont.FreeTypeFont: + for s in range(hi, lo - 1, -2): + f = self(s) + if f.getlength(text) <= max_w: + return f + return self(lo) + + +def _ellipsis(text: str, font: ImageFont.FreeTypeFont, max_w: int) -> str: + """Truncate text with '…' using binary search.""" + if font.getlength(text) <= max_w: + return text + lo, hi = 0, len(text) + while lo < hi: + mid = (lo + hi + 1) // 2 + if font.getlength(text[:mid] + "…") <= max_w: + lo = mid + else: + hi = mid - 1 + return text[:lo] + "…" + + +def _center_text(draw, text, font, y, canvas_w, fill="white"): + bb = draw.textbbox((0, 0), text, font=font) + draw.text(((canvas_w - bb[2] + bb[0]) // 2, y), text, font=font, fill=fill) + + +def _frosted_bg(src: bytes, w: int, h: int, dim: float = 0.25) -> Image.Image: + """Blurred & dimmed background from cover art.""" + img = Image.open(io.BytesIO(src)).convert("RGBA") + small = img.resize((max(w // 5, 1), max(h // 5, 1)), Image.Resampling.BILINEAR) + small = small.filter(ImageFilter.GaussianBlur(12)) + result = small.resize((w, h), Image.Resampling.BILINEAR) + return ImageEnhance.Brightness(result).enhance(dim) + + +def _gradient( + w: int, h: int, vertical: bool = True, c_from=(0, 0, 0, 160), c_to=(0, 0, 0, 40) +) -> Image.Image: + """Fast linear gradient via 1px strip resize.""" + length = h if vertical else w + strip = Image.new("RGBA", (1, length) if vertical else (length, 1)) + px = strip.load() + for i in range(length): + t = i / max(length - 1, 1) + rgba = tuple(int(c_from[c] + (c_to[c] - c_from[c]) * t) for c in range(4)) + if vertical: + px[0, i] = rgba + else: + px[i, 0] = rgba + return strip.resize((w, h), Image.Resampling.BILINEAR) + + +def _round_corners(img: Image.Image, r: int) -> Image.Image: + mask = Image.new("L", img.size, 0) + ImageDraw.Draw(mask).rounded_rectangle((0, 0, *img.size), r, fill=255) + out = Image.new("RGBA", img.size, (0, 0, 0, 0)) + out.paste(img, mask=mask) + return out + + +def _rounded_cover(data: bytes, size: int, r: int) -> Image.Image: + img = Image.open(io.BytesIO(data)).convert("RGBA") + img = img.resize((size, size), Image.Resampling.LANCZOS) + return _round_corners(img, r) + + +def _place_cover( + base: Image.Image, + cover_data: bytes, + size: int, + radius: int, + pos: tuple, + shadow_blur: int = 20, + shadow_alpha: int = 50, +): + """Place cover with colored drop shadow (offset downward).""" + cover = _rounded_cover(cover_data, size, radius) + avg = cover.resize((1, 1), Image.Resampling.BILINEAR).getpixel((0, 0)) + + pad = shadow_blur * 2 + offset_y = 8 + canvas = Image.new( + "RGBA", (size + pad * 2, size + pad * 2 + offset_y), (0, 0, 0, 0) + ) + shadow_shape = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + ImageDraw.Draw(shadow_shape).rounded_rectangle( + (0, 0, size, size), radius, fill=(*avg[:3], shadow_alpha) + ) + canvas.paste(shadow_shape, (pad, pad + offset_y), shadow_shape) + canvas = canvas.filter(ImageFilter.GaussianBlur(shadow_blur)) + canvas.paste(cover, (pad, pad), cover) + + base.paste(canvas, (pos[0] - pad, pos[1] - pad), canvas) + + +def _waveform(draw, x, y, w, h, bars=45, color=_ORANGE, muted=_BAR_MUTED, prog=0.0): + """Waveform visualization bars with sha256-seeded heights.""" + bw = max(w // (bars * 2), 2) + gap = (w - bw * bars) // max(bars - 1, 1) + seed = hashlib.sha256(f"sc{bars}".encode()).digest() + for i in range(bars): + bx = x + i * (bw + gap) + amp = seed[i % len(seed)] / 255 + bh = int(h * (0.25 + amp * 0.75)) + by = y + (h - bh) // 2 + c = color if i / bars <= prog else muted + draw.rounded_rectangle((bx, by, bx + bw, by + bh), bw // 2, fill=c) + + +def _badge( + draw, text, font, x, y, fg="white", bg=(255, 255, 255, 18), px=12, py=5 +) -> int: + """Rounded pill badge. Returns width.""" + bb = font.getbbox(text) + tw, th = bb[2] - bb[0], bb[3] - bb[1] + pw, ph = tw + px * 2, th + py * 2 + draw.rounded_rectangle((x, y, x + pw, y + ph), ph // 2, fill=bg) + draw.text((x + px, y + py), text, font=font, fill=fg) + return pw + + +def _export(img: Image.Image, name: str = "soundcloud.png") -> io.BytesIO: + buf = io.BytesIO() + img.save(buf, "PNG", optimize=True) + buf.seek(0) + buf.name = name + return buf + + +class CardFactory: + """Generates visual cards for SoundCloud tracks.""" + + def __init__(self, fonts: _Fonts): + self._f = fonts + + def square(self, track: TrackInfo, cover: bytes) -> io.BytesIO: + """Square now-playing card (800×800).""" + S = 800 + p = 45 + + bg = _frosted_bg(cover, S, S, 0.22) + bg = Image.alpha_composite( + bg, _gradient(S, S, True, (0, 0, 0, 50), (0, 0, 0, 190)) + ) + draw = ImageDraw.Draw(bg) + + bf = self._f(12) + draw.text((p, p), "SOUNDCLOUD", font=bf, fill=_ORANGE) + lw = bf.getlength("SOUNDCLOUD") + draw.line([(p, p + 17), (p + lw, p + 17)], fill=(*_ORANGE, 100), width=2) + + cs = 310 + cx, cy = (S - cs) // 2, p + 32 + _place_cover(bg, cover, cs, 14, (cx, cy), shadow_blur=25, shadow_alpha=50) + draw = ImageDraw.Draw(bg) + + wy = cy + cs + 30 + _waveform(draw, p + 35, wy, S - p * 2 - 70, 26, bars=50) + + tf = self._f(13) + draw.text((p + 35, wy + 30), "0:00", font=tf, fill=_FADED) + ds = track.duration_fmt + draw.text((S - p - 35 - tf.getlength(ds), wy + 30), ds, font=tf, fill=_FADED) + + tw = S - p * 2 + ty = wy + 56 + title_f = self._f.fit(track.title, tw, 36, 20) + _center_text(draw, _ellipsis(track.title, title_f, tw), title_f, ty, S) + + af = self._f.fit(track.artist, tw, 24, 16) + _center_text(draw, _ellipsis(track.artist, af, tw), af, ty + 44, S, _DIM) + + sy = ty + 92 + sf = self._f(14) + parts = [] + if track.genre: + parts.append(track.genre) + if track.plays: + parts.append(f"▶ {_compact(track.plays)}") + if track.likes: + parts.append(f"♥ {_compact(track.likes)}") + if not parts: + parts.append(track.duration_fmt) + _center_text(draw, " · ".join(parts), sf, sy, S, _FADED) + + return _export(_round_corners(bg, 22)) + + def horizontal(self, track: TrackInfo, cover: bytes) -> io.BytesIO: + """Wide now-playing card (1200×400).""" + W, H = 1200, 400 + p = 40 + cs = 280 + + bg = _frosted_bg(cover, W, H, 0.22) + bg = Image.alpha_composite( + bg, _gradient(W, H, False, (0, 0, 0, 180), (0, 0, 0, 60)) + ) + + cvy = (H - cs) // 2 + _place_cover(bg, cover, cs, 14, (p, cvy), shadow_blur=20, shadow_alpha=40) + draw = ImageDraw.Draw(bg) + + bf = self._f(11) + draw.text((p, p - 6), "SOUNDCLOUD", font=bf, fill=_ORANGE) + + if track.genre: + gf = self._f(12) + gt = track.genre.upper() + draw.text((W - p - gf.getlength(gt), p - 6), gt, font=gf, fill=_FADED) + + tx = p + cs + 50 + tw = W - tx - p + + tty = cvy + 10 + title_f = self._f.fit(track.title, tw, 36, 22) + draw.text( + (tx, tty), + _ellipsis(track.title, title_f, tw), + font=title_f, + fill="white", + ) + + af = self._f(22) + draw.text( + (tx, tty + 50), + _ellipsis(track.artist, af, tw), + font=af, + fill=_DIM, + ) + + by = tty + 98 + bx = tx + pill_f = self._f(14) + bw = _badge( + draw, + track.duration_fmt, + pill_f, + bx, + by, + fg=_ORANGE, + bg=(*_ORANGE, 35), + ) + bx += bw + 8 + if track.plays: + bw = _badge(draw, f"▶ {_compact(track.plays)}", pill_f, bx, by, fg=_DIM) + bx += bw + 8 + if track.likes: + _badge(draw, f"♥ {_compact(track.likes)}", pill_f, bx, by, fg=_DIM) + + wy = cvy + cs - 50 + _waveform(draw, tx, wy, tw, 22, bars=55) + + wf = self._f(12) + draw.text((tx, wy + 26), "0:00", font=wf, fill=_FADED) + ds = track.duration_fmt + draw.text((tx + tw - wf.getlength(ds), wy + 26), ds, font=wf, fill=_FADED) + + return _export(_round_corners(bg, 20)) + + def history(self, tracks: List[TrackInfo], fetch_cover) -> io.BytesIO: + """History card with dynamic height based on track count.""" + W = 1200 + p = 36 + row_h = 120 + gap = 8 + hdr = 55 + n = len(tracks) + H = p * 2 + hdr + n * row_h + (n - 1) * gap + + bg_data = fetch_cover(tracks[0].hq_cover) + bg = _frosted_bg(bg_data, W, H, 0.18) + bg = Image.alpha_composite(bg, Image.new("RGBA", (W, H), (0, 0, 0, 150))) + draw = ImageDraw.Draw(bg) + + hf = self._f(14) + draw.text((p, p), "SOUNDCLOUD", font=hf, fill=_ORANGE) + thf = self._f(22) + draw.text((p, p + 20), "Listening History", font=thf, fill="white") + + lw = hf.getlength("SOUNDCLOUD") + draw.rounded_rectangle((p, p + 48, p + lw, p + 50), 1, fill=_ORANGE) + + ct = f"{n} tracks" + draw.text((W - p - hf.getlength(ct), p + 22), ct, font=hf, fill=_FADED) + + title_f = self._f(22) + artist_f = self._f(16) + time_f = self._f(14) + num_f = self._f(12) + cp = 12 + cvsz = row_h - cp * 2 + card_w = W - p * 2 + + yo = p + hdr + 8 + for idx, trk in enumerate(tracks): + ry = int(yo) + + card = Image.new("RGBA", (card_w, row_h), (0, 0, 0, 0)) + cd = ImageDraw.Draw(card) + cd.rounded_rectangle( + (0, 0, card_w, row_h), + 12, + fill=_CARD_ACTIVE if idx == 0 else _CARD_BG, + ) + if idx == 0: + cd.rounded_rectangle((0, 0, 4, row_h), 2, fill=_ORANGE) + region = bg.crop((p, ry, p + card_w, ry + row_h)) + bg.paste(Image.alpha_composite(region, card), (p, ry)) + + try: + cv_data = fetch_cover(trk.hq_cover) + cv = _rounded_cover(cv_data, cvsz, 8) + bg.paste(cv, (p + cp + 6, ry + cp), cv) + except Exception: + pass + + draw = ImageDraw.Draw(bg) + + nt = f"{idx + 1:02d}" + nw = num_f.getlength(nt) + nx = p + cp + 6 + (cvsz - nw) // 2 + ny = ry + cp + cvsz - 18 + draw.rounded_rectangle( + (nx - 3, ny - 1, nx + nw + 3, ny + 14), 3, fill=(0, 0, 0, 170) + ) + draw.text((nx, ny - 1), nt, font=num_f, fill=_ORANGE) + + txt_x = p + cp + cvsz + 24 + txt_w = card_w - cvsz - cp * 3 - 24 - 70 + ty_center = ry + (row_h - 58) // 2 + + draw.text( + (txt_x, ty_center), + _ellipsis(trk.title, title_f, txt_w), + font=title_f, + fill="white", + ) + draw.text( + (txt_x, ty_center + 30), + _ellipsis(trk.artist, artist_f, txt_w), + font=artist_f, + fill=_DIM, + ) + + dt = trk.duration_fmt + dw = time_f.getlength(dt) + draw.text( + (p + card_w - cp - dw - 8, ty_center + 4), + dt, + font=time_f, + fill=_FADED, + ) + + if trk.plays: + pt = f"▶ {_compact(trk.plays)}" + pw = time_f.getlength(pt) + draw.text( + (p + card_w - cp - pw - 8, ty_center + 24), + pt, + font=time_f, + fill=_FADED, + ) + + yo += row_h + gap + + return _export(_round_corners(bg, 20), "soundcloud_history.png") + + +def _require_token(func): + """Decorator: ensure oauth_token is configured.""" + + @functools.wraps(func) + async def wrapper(self, message, *a, **kw): + if not self.config["oauth_token"]: + return await utils.answer(message, self.strings("no_token")) + return await func(self, message, *a, **kw) + + return wrapper + + +def _catch_errors(func): + """Decorator: log & report exceptions to user.""" + + @functools.wraps(func) + async def wrapper(self, message, *a, **kw): + try: + return await func(self, message, *a, **kw) + except Exception: + logger.exception("SoundCloud: %s failed", func.__name__) + with contextlib.suppress(Exception): + import traceback + + await utils.answer( + message, self.strings("error").format(traceback.format_exc()) + ) + + return wrapper + + +@loader.tds +class SoundCloudMod(loader.Module): + """Display the currently playing SoundCloud track as a stylized card.""" + + strings = { + "name": "SoundCloud", + "no_token": ( + "\u274c" + " Set oauth_token in module config\n\n" + "\U0001f511 Get it via extension:\n" + "\u2022 Chromium\n" + "\u2022 Firefox\n" + "\u2022 Or via DevTools: Application \u2192 Cookies \u2192 " + "oauth_token" + ), + "nothing": ( + "" + " Nothing is playing right now" + ), + "error": ( + "" + " Error\n{}" + ), + "wait_card": ( + "\n\n🕔" + " Generating card…" + ), + "wait_dl": ( + "\n\n🕔 Downloading…" + ), + "dl_fail": ( + "\n\n" + " Download failed" + ), + } + + strings_ru = { + "no_token": ( + "" + " Установи oauth_token" + " в конфиге модуля\n\n" + "🔑 Получить токен:\n" + "• Chromium\n" + "• Firefox\n" + "• Или через DevTools: Application → Cookies → " + "oauth_token" + ), + "nothing": ( + "" + " Сейчас ничего не играет" + ), + "error": ( + "" + " Ошибка\n{}" + ), + "wait_card": ( + "\n\n🕔" + " Генерация карточки…" + ), + "wait_dl": ( + "\n\n🕔 Скачивание…" + ), + "dl_fail": ( + "\n\n" + " Ошибка скачивания" + ), + } + + def __init__(self): + self._font_data: Optional[bytes] = None + self._font_src: Optional[str] = None + self.config = loader.ModuleConfig( + loader.ConfigValue( + "show_banner", + True, + "Generate image card", + validator=loader.validators.Boolean(), + ), + loader.ConfigValue( + "banner_type", + "square", + "Card layout", + validator=loader.validators.Choice(["square", "horizontal"]), + ), + loader.ConfigValue( + "template", + ( + "🎧" + " Now playing: {artist} — {track}\n" + "🕓" + " {duration}{genre}\n" + "🔗" + " SoundCloud" + ), + "Message template. Placeholders: {track}, {artist}," + " {url}, {duration}, {genre}, {stats}", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "font", + "https://github.com/web-fonts/ttf/raw/refs/heads/master/alk-sanet-webfont.ttf", + "URL to .ttf font file", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "oauth_token", + "", + "SoundCloud OAuth token", + validator=loader.validators.String(), + ), + loader.ConfigValue( + "history_count", + 5, + "Tracks in history (3–5)", + validator=loader.validators.Integer(minimum=3, maximum=5), + ), + ) + + def _headers(self) -> dict: + return { + "Authorization": f"OAuth {self.config['oauth_token']}", + "Accept": "application/json", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + ), + } + + async def _get(self, path: str, **params) -> Optional[dict]: + try: + r = await utils.run_sync( + requests.get, + f"{_API}{path}", + headers=self._headers(), + params=params, + timeout=5, + ) + if r.status_code == 200: + return r.json() + except Exception: + logger.debug("SC API %s failed", path) + return None + + async def _load_font(self) -> bytes: + url = self.config["font"] + if self._font_data and self._font_src == url: + return self._font_data + data = await utils.run_sync(lambda: requests.get(url, timeout=10).content) + self._font_data = data + self._font_src = url + return data + + async def _load_cover(self, url: str) -> Optional[bytes]: + try: + hq = url.replace("-large", _COVER_HQ) + r = await utils.run_sync(requests.get, hq, timeout=10) + if r.status_code == 200: + return r.content + except Exception: + pass + return None + + async def _current(self) -> Optional[TrackInfo]: + for ep in ("/me/play-history/tracks", "/me/activities", "/stream"): + data = await self._get(ep, limit=3) + if not data: + continue + for item in data.get("collection", []): + raw = item.get("track") or item + if raw and "title" in raw and (raw.get("duration") or 0) > 0: + return TrackInfo.parse(raw) + return None + + async def _recent(self, count: int) -> List[TrackInfo]: + data = await self._get("/me/play-history/tracks", limit=count) + if not data: + return [] + return [ + TrackInfo.parse(it["track"]) + for it in data.get("collection", []) + if it.get("track") and "title" in it["track"] + ] + + async def _download(self, url: str) -> Optional[bytes]: + try: + token = self.config["oauth_token"] + opts = { + "format": "best[ext=mp3]/best", + "quiet": True, + "no_warnings": True, + "http_headers": { + "Authorization": f"OAuth {token}", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + ), + }, + } + + def _run(): + with YoutubeDL(opts) as ydl: + info = ydl.extract_info(url, download=False) + audio = info.get("url") + if audio: + r = requests.get(audio, timeout=60) + if r.status_code == 200: + return r.content + return None + + return await utils.run_sync(_run) + except Exception as e: + logger.error("Download failed: %s", e) + return None + + def _format_message(self, t: TrackInfo) -> str: + genre_part = f" | {utils.escape_html(t.genre)}" if t.genre else "" + stats = [] + if t.plays: + stats.append(f"▶ {_compact(t.plays)}") + if t.likes: + stats.append(f"♥ {_compact(t.likes)}") + return self.config["template"].format( + track=utils.escape_html(t.title), + artist=utils.escape_html(t.artist), + duration=t.duration_fmt, + url=t.permalink, + genre=genre_part, + stats=" · ".join(stats), + ) + + def _format_detail(self, t: TrackInfo) -> str: + parts = [t.duration_fmt] + if t.genre: + parts.append(utils.escape_html(t.genre)) + if t.plays: + parts.append(f"▶ {_compact(t.plays)}") + if t.likes: + parts.append(f"♥ {_compact(t.likes)}") + info = " | ".join(parts) + return ( + f"🎧" + f" {utils.escape_html(t.artist)} — {utils.escape_html(t.title)}\n" + f"🕓 {info}\n" + f"🔗" + f" SoundCloud" + ) + + @_catch_errors + @_require_token + @loader.command( + ru_doc="— Показать карточку текущего трека", + en_doc="— Show current track card", + ) + async def scnow(self, message: Message): + track = await self._current() + if not track: + return await utils.answer(message, self.strings("nothing")) + + text = self._format_message(track) + + if not (self.config["show_banner"] and track.cover_url): + return await utils.answer(message, text) + + msg = await utils.answer(message, text + self.strings("wait_card")) + + cover = await self._load_cover(track.cover_url) + if not cover: + return await utils.answer(msg, text) + + font_data = await self._load_font() + factory = CardFactory(_Fonts(font_data)) + + render = ( + factory.square + if self.config["banner_type"] == "square" + else factory.horizontal + ) + card = await utils.run_sync(render, track, cover) + await utils.answer(msg, text, file=card) + + @_catch_errors + @_require_token + @loader.command( + ru_doc="— Скачать текущий трек", + en_doc="— Download current track", + ) + async def scnowt(self, message: Message): + track = await self._current() + if not track: + return await utils.answer(message, self.strings("nothing")) + + text = self._format_detail(track) + msg = await utils.answer(message, text + self.strings("wait_dl")) + + audio = await self._download(track.permalink) + if not audio: + return await utils.answer(msg, text + self.strings("dl_fail")) + + buf = io.BytesIO(audio) + buf.name = f"{track.artist} - {track.title}.mp3" + await utils.answer(msg, text, file=buf) + + @_catch_errors + @_require_token + @loader.command( + ru_doc="— История прослушивания", + en_doc="— Listening history", + ) + async def schistory(self, message: Message): + tracks = await self._recent(self.config["history_count"]) + if not tracks: + return await utils.answer(message, self.strings("nothing")) + + text = ( + "📜" + " История прослушивания:\n\n" + ) + for i, t in enumerate(tracks, 1): + parts = [t.duration_fmt] + if t.genre: + parts.append(utils.escape_html(t.genre)) + if t.plays: + parts.append(f"▶ {_compact(t.plays)}") + meta = " | ".join(parts) + text += ( + f"{i}. {utils.escape_html(t.artist)} —" + f" {utils.escape_html(t.title)}\n" + f" 🕓" + f" {meta} | Link\n\n" + ) + + if not self.config["show_banner"]: + return await utils.answer(message, text) + + msg = await utils.answer(message, text + self.strings("wait_card")) + try: + font_data = await self._load_font() + + def _render(): + factory = CardFactory(_Fonts(font_data)) + + def fetcher(u): + return requests.get(u, timeout=10).content + + return factory.history(tracks, fetcher) + + card = await utils.run_sync(_render) + await utils.answer(msg, text, file=card) + except Exception: + await utils.answer(msg, text) diff --git a/coddrago/modules/YaMusic.py b/coddrago/modules/YaMusic.py index 2279cfe..036e671 100644 --- a/coddrago/modules/YaMusic.py +++ b/coddrago/modules/YaMusic.py @@ -1,11 +1,9 @@ -__version__ = (3, 1, 1) +__version__ = (3, 2, 0) # meta banner: https://raw.githubusercontent.com/kamekuro/hikka-mods/main/banners/yamusic.png -# packurl: https://raw.githubusercontent.com/coddrago/assets/refs/heads/main/modules/yamusic.yml -# meta banner: https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/banner.png +# packurl: https://raw.githubusercontent.com/coddrago/modules/refs/heads/dev/translations/yamusic.yml # meta developer: @codrago_m -# old meta dev: @kamekuro xuesos # scope: heroku_only -# scope: heroku_min 1.7.2 +# scope: heroku_min 2.0.0 # requires: aiohttp asyncio pillow>=10.0.0 git+https://github.com/MarshalX/yandex-music-api import aiohttp @@ -17,6 +15,7 @@ import random import string import typing import time +import uuid from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont import telethon @@ -171,7 +170,6 @@ class Banners: current_y += 80 bar_width = 800 - bar_height = 6 font_time = get_font(40) bar_start_x = center_x - (bar_width // 2) @@ -180,11 +178,12 @@ class Banners: total_mins = self.duration // 1000 // 60 total_secs = (self.duration // 1000) % 60 - total_time_str = f"{total_mins}:{total_secs:02d}" + + total_time_str = f"{total_mins:02d}:{total_secs:02d}" cur_mins = self.progress // 1000 // 60 cur_secs = (self.progress // 1000) % 60 - cur_time_str = f"{cur_mins}:{cur_secs:02d}" + cur_time_str = f"{cur_mins:02d}:{cur_secs:02d}" draw_text_shadow( cur_time_str, (bar_start_x - 30, bar_y), font_time, anchor="rm" @@ -193,34 +192,44 @@ class Banners: total_time_str, (bar_end_x + 30, bar_y), font_time, anchor="lm" ) - draw.line( - [(bar_start_x, bar_y), (bar_end_x, bar_y)], - fill=(255, 255, 255, 80), - width=bar_height, - ) - + old_state = random.getstate() + + random.seed(self.title + str(self.duration)) + + num_bars = 65 + bar_spacing = bar_width / num_bars + bar_w = max(4, int(bar_spacing * 0.5)) + max_h = 50 + min_h = 6 + if self.duration > 0: progress_ratio = self.progress / self.duration else: progress_ratio = 0 - progress_px = int(bar_width * progress_ratio) - if progress_px > bar_width: - progress_px = bar_width + + active_bars = int(num_bars * progress_ratio) - draw.line( - [(bar_start_x, bar_y), (bar_start_x + progress_px, bar_y)], - fill="white", - width=bar_height + 5, - ) - draw.ellipse( - ( - bar_start_x + progress_px - 10, - bar_y - 10, - bar_start_x + progress_px + 10, - bar_y + 10, - ), - fill="white", - ) + for i in range(num_bars): + base_h = random.randint(min_h, max_h) + edge_factor = 1.0 - abs((i - num_bars / 2) / (num_bars / 2)) + h = int(base_h * 0.4 + max_h * edge_factor * 0.6) + h = max(min_h, h) + + x_center = bar_start_x + i * bar_spacing + left = x_center - (bar_w / 2) + right = x_center + (bar_w / 2) + top = bar_y - (h / 2) + bottom = bar_y + (h / 2) + + color = (255, 255, 255, 255) if i < active_bars else (80, 80, 80, 100) + + draw.rounded_rectangle( + (left, top, right, bottom), + radius=int(bar_w / 2), + fill=color + ) + + random.setstate(old_state) current_y += 80 @@ -312,13 +321,7 @@ class YaMusicMod(loader.Module): """The module for Yandex.Music streaming service""" strings = { - "name": "YaMusic", - "iguide": '📜 Guide for obtaining access token for Yandex.Music', - } - - strings_ru = { - "_cls_doc": "Модуль для стримингового сервиса Яндекс.Музыка", - "iguide": '📜 Гайд по получению токена Яндекс.Музыки', + "name": "YaMusic" } def __init__(self): @@ -373,11 +376,10 @@ class YaMusicMod(loader.Module): self._client: telethon.TelegramClient = client self._db = db - #utils.register_placeholder( - #"now_play", self._now_play_placeholder, "placeholder for nowplay music" - # Heroku 2.0.0 feature - #) - #utils.register_placeholder("duration", self._duration_placeholder, "progress bar") + utils.register_placeholder( + "now_play", self._now_play_placeholder, "placeholder for nowplay music" + ) + utils.register_placeholder("duration", self._duration_placeholder, "progress bar") if not self.get("guide_sent", False): await self.inline.bot.send_message(self._tg_id, self.strings("iguide")) @@ -423,7 +425,7 @@ class YaMusicMod(loader.Module): me = await self._client.get_me() self._premium = me.premium if hasattr(me, "premium") else False - @loader.loop(15) + @loader.loop(30) async def autobio(self): if not self.config["token"]: self.autobio.stop() @@ -543,7 +545,7 @@ class YaMusicMod(loader.Module): now = await self.__get_now_playing() if not now or now.get("paused"): return "Not Playing" - + duration = now.get("duration_ms", 0) progress = now.get("progress_ms", 0) @@ -632,13 +634,14 @@ class YaMusicMod(loader.Module): ) async def ynowcmd(self, message: telethon.types.Message): """👉 Get the banner of the track playing right now""" + + await utils.answer(message, self.strings("uploading_banner")) ym_client = await self._get_ym_client() if not ym_client: return await utils.answer( message, self.strings("errors")["no_token_or_invalid"] ) - await utils.answer(message, self.strings("uploading_banner")) now = await self.__get_now_playing() if not now or now.get("paused"): @@ -694,10 +697,6 @@ class YaMusicMod(loader.Module): .format(playlist_name), link=f"Яндекс.Музыка", ) - try: - await utils.answer(message, out + self.strings("uploading_banner")) - except Exception: - pass album_obj = track_object.albums[0] if track_object.albums else None @@ -823,10 +822,6 @@ class YaMusicMod(loader.Module): .format(playlist_name), link=f"Яндекс.Музыка", ) - try: - await utils.answer(message, out + self.strings("downloading_track")) - except Exception: - pass await utils.answer( message=message, @@ -954,6 +949,7 @@ class YaMusicMod(loader.Module): ), ) + async def __download_track( self, client: yandex_music.ClientAsync, @@ -977,7 +973,7 @@ class YaMusicMod(loader.Module): await asyncio.sleep(1) continue raise e - + async def __get_ynison(self): async def create_ws(token, ws_proto): async with aiohttp.ClientSession() as session: diff --git a/coddrago/modules/translations/yamusic.yml b/coddrago/modules/translations/yamusic.yml new file mode 100644 index 0000000..12f694f --- /dev/null +++ b/coddrago/modules/translations/yamusic.yml @@ -0,0 +1,116 @@ +en: + iguide: "📜 Guide for obtaining access token for Yandex.Music" + search: "🎧 {performer} — {title}\n🎵 Yandex.Music | song.link" + downloading_track: "\n\n🕔 Downloading audio…" + uploading_banner: "\n\n🕔 Uploading banner…" + lyrics: "📜 Lyrics of the {track} track:\n
{text}
\n\n©️ Writers: {writers}" + no_lyrics: "🚫 Track {track} has no lyrics!" + errors: + no_query: "🚫 Specify the search query first!" + no_token_or_invalid: "🚫 You specified an invalid access token or didn't specified it at all!" + not_found: "🚫 No results found." + no_playing: "🚫 You don't listening to anything right now." + autobio: + enabled: "🎧 Autobio was enabled." + disabled: "🎧 Autobio was disabled." + likes: + liked: "❤️ Track {track} was liked." + unliked: "🖤 Track {track} was unliked." + disliked: "💔 Track {track} was disliked." + _entity_types: + VARIOUS: "Your queue" + RADIO: "«My Vibe»" + PLAYLIST: "Playlist «{}»" + ALBUM: "«{}»" + ARTIST: "Popular tracks by {}" + _cfg: + token: "The access token for Yandex.Music." + now_playing_text: "The caption for .ynow and .ynowt commands. May contain {performer}, {title}, {device}, {volume}, {playing_from}, {link}, {track_id}, {album_id} keywords." + autobio_text: "The text for automatically changing «Bio». May contains {performer} and {title}." + no_playing_bio_text: "The text for changing «Bio» when there is no playing tracks." + banner_version: "Banner version" + repeat_on: "🔁 Repeat enabled" + repeat_off: "➡️ Repeat disabled" + next_track: " Next track" + prev_track: " Previous track" + volume_set: "➡️ Volume set to {vol}%" + volume_invalid: " Volume must be between 0 and 100" + ynison_error: " Failed to send command to Yandex.Music (Ynison)" + +ru: + iguide: "📜 Гайд по получению токена Яндекс.Музыки" + search: "🎧 {performer} — {title}\n🎵 Яндекс.Музыка | song.link" + downloading_track: "\n\n🕔 Загрузка трека…" + uploading_banner: "\n\n🕔 Загрузка баннера…" + lyrics: "📜 Текст трека {track}:\n
{text}
\n\n©️ Авторы: {writers}" + no_lyrics: "🚫 У трека {track} нет текста!" + errors: + no_query: "🚫 Укажите поисковый запрос!" + no_token_or_invalid: "🚫 Вы указали невалидный токен или не указали его вообще!" + not_found: "🚫 Результаты не найдены." + no_playing: "🚫 Вы ничего не слушаете сейчас." + autobio: + enabled: "🎧 Автобио теперь включено." + disabled: "🎧 Автобио теперь выключено." + likes: + liked: "❤️ Трек {track} был лайкнут." + unliked: "🖤 С трека {track} был снят лайк." + disliked: "💔 Трек {track} был дизлайкнут." + _entity_types: + VARIOUS: "Ваша очередь" + RADIO: "«Моя волна»" + PLAYLIST: "Плейлист «{}»" + ALBUM: "«{}»" + ARTIST: "Популярные треки {}" + _cfg: + token: "Токен для Яндекс.Музыки." + now_playing_text: "Текст, использующийся в подписи к файлу в командах .ynow и .ynowt. Может содержать {performer}, {title}, {device}, {volume}, {playing_from}, {link}, {track_id} и {album_id}" + autobio_text: "Текст, использующийся при автоматическом изменении «О себе». Может содержать {performer} и {title}." + no_playing_bio_text: "Текст, использующийся при изменении «О себе», когда ничего не играет." + banner_version: "Версия баннера" + repeat_on: "🔁 Повтор включен (Один трек)" + repeat_off: "➡️ Повтор выключен" + next_track: " Следующий трек" + prev_track: " Предыдущий трек" + volume_set: "➡️ Громкость установлена на {vol}%" + volume_invalid: " Громкость должна быть от 0 до 100" + ynison_error: " Не удалось отправить команду в Яндекс.Музыку (Ynison)" + +jp: + iguide: "📜 Yandex.Music アクセストークン取得ガイド" + search: "🎧 {performer} — {title}\n🎵 Yandex.Music | song.link" + downloading_track: "\n\n🕔 オーディオをダウンロード中…" + uploading_banner: "\n\n🕔 バナーをアップロード中…" + lyrics: "📜 トラック {track} の歌詞:\n
{text}
\n\n©️ 作詞・作曲: {writers}" + no_lyrics: "🚫 トラック {track} には歌詞がありません!" + errors: + no_query: "🚫 最初に検索クエリを指定してください!" + no_token_or_invalid: "🚫 無効なアクセストークンを指定したか、指定されていません!" + not_found: "🚫 結果が見つかりません。" + no_playing: "🚫 現在何も再生していません。" + autobio: + enabled: "🎧 Autobio(自動プロフィール)が有効になりました。" + disabled: "🎧 Autobioが無効になりました。" + likes: + liked: "❤️ トラック {track} に「いいね」しました。" + unliked: "🖤 トラック {track} の「いいね」を取り消しました。" + disliked: "💔 トラック {track} に「低評価」しました。" + _entity_types: + VARIOUS: "あなたのキュー" + RADIO: "«My Vibe»" + PLAYLIST: "プレイリスト «{}»" + ALBUM: "«{}»" + ARTIST: "{} の人気トラック" + _cfg: + token: "Yandex.Musicのアクセストークン。" + now_playing_text: ".ynow および .ynowt コマンド用のキャプション。{performer}, {title}, {device}, {volume}, {playing_from}, {link}, {track_id}, {album_id} のキーワードを含めることができます。" + autobio_text: "«Bio»(自己紹介)を自動変更するためのテキスト。{performer} と {title} を含めることができます。" + no_playing_bio_text: "何も再生されていない時に «Bio» を変更するためのテキスト。" + banner_version: "バナーのバージョン" + repeat_on: "🔁 リピート有効" + repeat_off: "➡️ リピート無効" + next_track: " 次のトラック" + prev_track: " 前のトラック" + volume_set: "➡️ 音量を {vol}% に設定しました" + volume_invalid: " 音量は0から100の間で指定してください" + ynison_error: " Yandex.Music (Ynison) へのコマンド送信に失敗しました" diff --git a/fiksofficial/python-modules/PyInstall.py b/fiksofficial/python-modules/PyInstall.py new file mode 100644 index 0000000..5849a17 --- /dev/null +++ b/fiksofficial/python-modules/PyInstall.py @@ -0,0 +1,109 @@ +# meta developer: @pymodule +# requires: cryptography + +__version__ = (1, 0, 1) + +import base64 +import logging +from hashlib import sha256 + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey +from cryptography.exceptions import InvalidSignature + +from telethon.tl.types import Message +from telethon import functions, types +from typing import Optional + +from .. import loader, utils + +logger = logging.getLogger(__name__) + +pubkey_data = """ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0S50qdajfeRmKqS+sBsn +VYYJL8loDMkfMf55flSPkhwwAwKbHk9i+VxRxHs32/J/LHxPR0ix3W6bgzf8m1/A +79uu2WkMrkfcIrAaOoz07EqHdyyD7MEZuHIAm977uQfdYgseOMa2uclYgNppJf35 +8oqGP7+0+ks5IxzNLn8/7zeo6DrlyOVJ2lgv860NXPQ+WqTttMovkjDTTwBthE8i +WMg02r6fo+GFafeyaTRHusPAGqg2oZ3VFIxcsJFVqgxmGJkbQVGgSuPwHWM5yPGi +gx0uB71i6y4NXk/PpoYdQMDanOFJvYe7JBpiktcqk8LB/PqPEm4ctsdGFiu9PR6K +wrzo0fK9zbpbPyiAHaCC/0/LkfWT7Cdc9bECDPaSGgJJde9wUpDoz+coAc5BfeW5 +6xu9J5fzkiw+zBQNlpkrtjG7JvqAYzul2GB+kDfCdVgkcQEPwBCTn6xGZvtWgE5b +yzQXaDkaTvbTUkUA41Ab6xsKSmU43otwV+9Rrzxovd+Nk7u9qwj5Ghambt37YNf3 +vUJ9XQFr8uy2nKaPHzGoLgNCBReUyua6aYqMtqCkU1id+dI4HqgDMPlDDGxGV6mK +Gamdu+eIJHl9chHrlTOxEDetLxZLuAdnoDRzHJyTce6NCsyz8tvwWnKv+8l3R+Bu +B9EM+BFIFwCXKt85P/eabMcCAwEAAQ== +-----END PUBLIC KEY----- +""" + +pubkey = serialization.load_pem_public_key(pubkey_data.strip().encode()) + +@loader.tds +class PyInstallMod(loader.Module): + """Provides PyModule modules installation trough buttons""" + + strings = { + "name": "PyInstall", + "_cls_doc": "Provides PyModule modules installation trough buttons", + "module_downloaded": "Module downloaded!" + } + + strings_ru = { + "_cls_doc": "Позволяет устанавливать модули от PyModule через кнопки", + "module_downloaded": "Модуль загружен!" + } + + async def on_dlmod(self, client, db): + ent = await self.client(functions.users.GetFullUserRequest('@pymodule_bot')) + if ent.full_user.blocked: + await self.client(functions.contacts.UnblockRequest('@pymodule_bot')) + await self.client.send_message('@pymodule_bot', '/start') + await self.client.delete_dialog('@pymodule_bot') + + async def _load_module(self, url: str, message: Optional[Message] = None): + loader_m = self.lookup("loader") + await loader_m.download_and_install(url, None) + + if getattr(loader_m, "_fully_loaded", getattr(loader_m, "fully_loaded", False)): + getattr( + loader_m, + "_update_modules_in_db", + getattr(loader_m, "update_modules_in_db", lambda: None), + )() + + async def watcher(self, message: Message): + if not isinstance(message, Message): + return + + if message.sender_id == 7575984561 and message.raw_text.startswith("#install"): + await message.delete() + + try: + fileref = message.raw_text.split("#install:")[1].strip().splitlines()[0].strip() + sig_b64 = message.raw_text.splitlines()[1].strip() + sig = base64.b64decode(sig_b64) + except (IndexError, ValueError): + logger.error("Invalid #install message format") + return + + try: + pubkey.verify( + signature=sig, + data=fileref.encode("utf-8"), + padding=padding.PKCS1v15(), + algorithm=hashes.SHA256() + ) + logger.info(f"Signature verified successfully for {fileref}") + except InvalidSignature: + logger.error(f"Got message with non-verified signature ({fileref=})") + return + except Exception as e: + logger.error(f"Signature verification error: {e}") + return + + await self._load_module( + f"https://raw.githubusercontent.com/fiksofficial/python-modules/refs/heads/main/{fileref}", + message + ) + await self.client.send_message('@pymodule_bot', self.strings['module_downloaded']) diff --git a/fiksofficial/python-modules/full.txt b/fiksofficial/python-modules/full.txt index 72e180b..0d35b2a 100644 --- a/fiksofficial/python-modules/full.txt +++ b/fiksofficial/python-modules/full.txt @@ -24,4 +24,6 @@ deviceinfo mpi aigenuser github -stream \ No newline at end of file +stream +placeholders+ +PyInstall \ No newline at end of file diff --git a/fiksofficial/python-modules/github.py b/fiksofficial/python-modules/github.py index 4e07e94..d3c8fae 100644 --- a/fiksofficial/python-modules/github.py +++ b/fiksofficial/python-modules/github.py @@ -15,6 +15,7 @@ import contextlib import logging +import re from datetime import datetime, timezone import aiohttp @@ -51,6 +52,25 @@ EVENT_LABELS = { } +def _sanitize_body(text: str, max_len: int = 300) -> str: + if not text: + return "" + text = re.sub(r"", "", text, flags=re.DOTALL) + text = re.sub(r"]*>.*?", "", text, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r"]*>.*?", "", text, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r"]*>", "", text, flags=re.IGNORECASE) + ALLOWED = {"b", "i", "u", "s", "code", "pre", "a", "blockquote", "tg-spoiler"} + text = re.sub( + r"<(/?)([a-zA-Z][a-zA-Z0-9]*)[^>]*>", + lambda m: m.group(0) if m.group(2).lower() in ALLOWED else "", + text, + ) + text = re.sub(r"\n{3,}", "\n\n", text).strip() + if len(text) > max_len: + text = text[:max_len].rstrip() + "…" + return text + + @loader.tds class GitHubMod(loader.Module): """GitHub repository monitor — commits, issues, PRs, releases and stars""" @@ -396,6 +416,7 @@ class GitHubMod(loader.Module): ), ) self._sessions: dict[str, aiohttp.ClientSession] = {} + self._seen: set[str] = set() # дедупликация событий: "repo:type:id" async def client_ready(self): raw = self.db.get("GitHubMod", "dests") @@ -677,7 +698,7 @@ class GitHubMod(loader.Module): else: e_key, action = "pr_open", self.strings("pr_opened") raw_body = pr.get("body") or "" - body = (raw_body[:200] + "...") if len(raw_body) > 200 else raw_body + body = _sanitize_body(raw_body, max_len=300) msgs.append(self.strings("notify_pr").format( e=E[e_key], action=action, repo=repo, url=pr.get("html_url", "#"), @@ -743,28 +764,79 @@ class GitHubMod(loader.Module): since = repo_data.get("last_checked") if not since: continue + if "push" in events: c = await self._fetch_commits(repo, since, cid_str) if c: - newest_sha = c[-1].get("sha", "") - branch = await self._fetch_branch_for_commit(repo, newest_sha, cid_str) - messages += self._fmt_push(repo, c, branch=branch) + # дедуп по SHA + new_commits = [] + for commit in c: + key = f"{repo}:push:{commit.get('sha', '')}" + if key not in self._seen: + self._seen.add(key) + new_commits.append(commit) + if new_commits: + newest_sha = new_commits[-1].get("sha", "") + branch = await self._fetch_branch_for_commit(repo, newest_sha, cid_str) + messages += self._fmt_push(repo, new_commits, branch=branch) + if "issues" in events: i = await self._fetch_issues(repo, since, cid_str) if i: - messages += self._fmt_issues(repo, i) + new_issues = [] + for issue in i: + # ключ: repo:issue:number:state (state меняется — open/closed) + key = f"{repo}:issue:{issue.get('number')}:{issue.get('state')}" + if key not in self._seen: + self._seen.add(key) + new_issues.append(issue) + if new_issues: + messages += self._fmt_issues(repo, new_issues) + if "pull_request" in events: p = await self._fetch_prs(repo, since, cid_str) if p: - messages += self._fmt_prs(repo, p) + new_prs = [] + for pr in p: + merged = pr.get("merged_at") is not None + state = pr.get("state", "open") + # ключ включает финальное состояние PR + phase = "merged" if merged else state + key = f"{repo}:pr:{pr.get('number')}:{phase}" + if key not in self._seen: + self._seen.add(key) + new_prs.append(pr) + if new_prs: + messages += self._fmt_prs(repo, new_prs) + if "release" in events: r = await self._fetch_releases(repo, since, cid_str) if r: - messages += self._fmt_releases(repo, r) + new_releases = [] + for rel in r: + key = f"{repo}:release:{rel.get('id', rel.get('tag_name'))}" + if key not in self._seen: + self._seen.add(key) + new_releases.append(rel) + if new_releases: + messages += self._fmt_releases(repo, new_releases) + if "star" in events: s = await self._fetch_stargazers(repo, since, cid_str) if s: - messages += self._fmt_star(repo, s) + new_stars = [] + for star in s: + user = (star.get("sender") or {}).get("login", "") + key = f"{repo}:star:{user}" + if key not in self._seen: + self._seen.add(key) + new_stars.append(star) + if new_stars: + messages += self._fmt_star(repo, new_stars) + + # Ограничиваем размер _seen чтобы не распухал в памяти + if len(self._seen) > 2000: + self._seen = set(list(self._seen)[-1000:]) for text in messages: try: @@ -1032,3 +1104,4 @@ class GitHubMod(loader.Module): """- Open GitHub Monitor control panel""" await self._render_main_menu(message) + \ No newline at end of file diff --git a/fiksofficial/python-modules/placeholders+.py b/fiksofficial/python-modules/placeholders+.py new file mode 100644 index 0000000..a58f96b --- /dev/null +++ b/fiksofficial/python-modules/placeholders+.py @@ -0,0 +1,650 @@ +# ______ ___ ___ _ _ +# ____ | ___ \ | \/ | | | | | +# / __ \| |_/ / _| . . | ___ __| |_ _| | ___ +# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \ +# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/ +# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___| +# \____/ __/ | +# |___/ + +# На модуль распространяется лицензия "GNU General Public License v3.0" +# https://github.com/all-licenses/GNU-General-Public-License-v3.0 + +# meta developer: @pymodule + +import logging +import platform +import socket +import os +import time +import aiohttp +import psutil +import json +import random +from datetime import datetime, timezone, timedelta +from typing import Optional, Dict, Any +from collections import OrderedDict + +from .. import loader, utils, validators +from herokutl.tl.functions.users import GetFullUserRequest +from herokutl.tl.functions.payments import GetStarsStatusRequest + +logger = logging.getLogger(__name__) + +class LRUCache: + """LRU-кэш с TTL""" + def __init__(self, max_size: int = 100, ttl: int = 300): + self.cache = OrderedDict() + self.max_size = max_size + self.ttl = ttl + self.timestamps = {} + + def get(self, key: str) -> Optional[Any]: + if key not in self.cache: + return None + + if time.time() - self.timestamps[key] > self.ttl: + del self.cache[key] + del self.timestamps[key] + return None + + self.cache.move_to_end(key) + return self.cache[key] + + def set(self, key: str, value: Any): + if len(self.cache) >= self.max_size: + oldest = next(iter(self.cache)) + del self.cache[oldest] + del self.timestamps[oldest] + + self.cache[key] = value + self.timestamps[key] = time.time() + +@loader.tds +class PlaceholdersMod(loader.Module): + """Плейсхолдеры""" + strings = {"name": "Placeholders+"} + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "timezone", + 5, + "Часовой пояс (offset от UTC)", + validator=validators.Integer(), + ), + loader.ConfigValue( + "weather_city", + "Oral", + "Город для погоды", + validator=validators.String(), + ), + loader.ConfigValue( + "lastfm_user", + "", + "Last.FM username", + validator=validators.String(), + ), + loader.ConfigValue( + "crypto_address", + "YOUR_WALLET_ADDRESS", + "Крипто-кошелёк", + validator=validators.String(), + ), + loader.ConfigValue( + "card_number", + "**** **** **** ****", + "Номер карты", + validator=validators.String(), + ), + loader.ConfigValue( + "donate_site", + "Boosty:https://boosty.to/yourname", + "Донат: имя:ссылка", + validator=validators.String(), + ), + loader.ConfigValue( + "channel", + "@yourchannel", + "Канал", + validator=validators.String(), + ), + loader.ConfigValue( + "social_network", + "https://vk.com/your", + "Соцсеть", + validator=validators.String(), + ), + ) + self.cache = LRUCache(max_size=100, ttl=300) + + async def client_ready(self): + self.session = aiohttp.ClientSession() + + self.me = await self._client.get_me() + self.full_me = await self._client(GetFullUserRequest(self.me)) + + try: + stars_status = await self._client(GetStarsStatusRequest(entity="me")) + self.stars_balance = stars_status.balance + except Exception: + self.stars_balance = 0 + + self.tz = timezone(timedelta(hours=self.config["timezone"])) + self.weekdays_ru = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"] + + self._register_placeholders() + + def _register_placeholders(self): + placeholders = [ + ("username", self.get_username, "Username"), + ("name", self.get_name, "Имя"), + ("surname", self.get_surname, "Фамилия"), + ("bio_description", self.get_bio, "Описание"), + ("user_id", self.get_user_id, "ID"), + ("phone_number", self.get_phone, "Телефон"), + ("dc_id", self.get_dc_id, "DC ID"), + ("amount_stars", self.get_stars, "Stars"), + ("premium_check", self.get_premium_check, "Дата окончания Premium"), + + ("dollars_in_rub", self.get_usd_to_rub, "USD → RUB"), + ("rub_in_dollars", self.get_rub_to_usd, "RUB → USD"), + ("usdt_in_rub", self.get_usdt_to_rub, "USDT → RUB"), + ("rub_in_usdt", self.get_rub_to_usdt, "RUB → USDT"), + ("ton_in_rub", self.get_ton_to_rub, "TON → RUB"), + ("rub_in_ton", self.get_rub_to_ton, "RUB → TON"), + ("btc_in_rub", self.get_btc_to_rub, "BTC → RUB"), + ("eth_in_rub", self.get_eth_to_rub, "ETH → RUB"), + ("stars_in_rub", self.get_stars_to_rub, "Stars → RUB"), + ("stars_in_ton", self.get_stars_to_ton, "Stars → TON"), + ("stars_in_usdt", self.get_stars_to_usdt, "Stars → USDT"), + + ("os_uptime", self.get_os_uptime, "Аптайм системы"), + ("internet_usage", self.get_internet_usage, "Статистика трафика"), + ("speedtest", self.get_speedtest, "Скорость интернета"), + ("host", self.get_host, "Hostname ОС"), + ("shell", self.get_shell, "Оболочка"), + ("gpu", self.get_gpu, "GPU"), + ("disk", self.get_disk, "Использование диска"), + ("local_ip", self.get_local_ip, "Локальный IP"), + ("user_and_hostname", self.get_user_hostname, "user@hostname"), + + ("time", self.get_time, "Время"), + ("date", self.get_date, "Дата"), + ("day_of_the_week", self.get_weekday, "День недели"), + ("data_and_time", self.get_date_time, "Дата и время"), + ("data_and_time_and_day_of_the_week", self.get_full_date_time_weekday, "Дата, время, день недели"), + ("weather", self.get_weather_condition, "Погода"), + ("outdoor_temperature", self.get_temperature, "Температура"), + ("weather_and_temperature", self.get_weather_temp, "Погода и температура"), + ("humidity", self.get_humidity, "Влажность"), + ("pressure", self.get_pressure, "Давление"), + ("wind_speed", self.get_wind_speed, "Скорость ветра"), + + ("my_crypto_address", self.get_crypto_address, "Крипто-адрес"), + ("my_card_number", self.get_card_number, "Номер карты"), + ("my_donate_site", self.get_donate_site, "Донат"), + ("my_channel", self.get_channel, "Канал"), + ("my_social_network", self.get_social, "Соцсеть"), + + ("now_playing", self.get_now_playing, "Сейчас играет"), + ("last_fm_user_and_now_playing", self.get_user_and_playing, "Last.FM + трек"), + ("song_name", self.get_song_name, "Название трека"), + ("song_artist", self.get_song_artist, "Артист"), + ("last_fm_user", self.get_lastfm_user, "Last.FM username"), + ("lastfm_stats", self.get_lastfm_stats, "Last.FM статистика"), + ] + + for name, func, desc in placeholders: + utils.register_placeholder(name, func, desc) + + async def get_premium_check(self): + if not getattr(self.me, "premium", False): + return "Нет Premium" + + # premium_until отсутствует в публичном MTProto API herokutl/Telethon — + # пробуем достать его, но не падаем если поля нет + until = None + try: + until = getattr(self.full_me.full_user, "premium_until", None) + # Иногда это datetime, иногда unix timestamp (int) + if isinstance(until, datetime): + until = until.timestamp() + except Exception: + until = None + + if not until: + return "✅ Premium активен" + + if until < time.time(): + return "⚠️ Премиум истёк" + + end_date = datetime.fromtimestamp(until, tz=self.tz) + days_left = (end_date.date() - datetime.now(self.tz).date()).days + formatted = end_date.strftime("%d.%m.%Y") + return f"✅ до {formatted} (ещё {days_left} дн.)" + + async def get_username(self): + return f"@{self.me.username}" if self.me.username else "Нет" + + async def get_name(self): + return self.me.first_name or "Нет" + + async def get_surname(self): + return self.me.last_name or "Нет" + + async def get_bio(self): + return self.full_me.full_user.about or "Нет описания" + + async def get_user_id(self): + return str(self.me.id) + + async def get_phone(self): + return self.me.phone or "Скрыт" + + async def get_dc_id(self): + return str(self.me.dc_id if hasattr(self.me, "dc_id") else "Неизвестно") + + async def get_stars(self): + return f"{self.stars_balance:,}".replace(",", " ") if self.stars_balance else "0" + + async def get_usd_to_rub(self): + cache_key = "usd_rub" + cached = self.cache.get(cache_key) + if cached: + return cached + + try: + async with self.session.get("https://www.cbr-xml-daily.ru/daily_json.js") as resp: + data = await resp.json() + rate = data["Valute"]["USD"]["Value"] + result = f"1 USD ≈ {rate:.2f} RUB" + self.cache.set(cache_key, result) + return result + except Exception: + try: + async with self.session.get("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json") as resp: + data = await resp.json() + rate = data["usd"]["rub"] + result = f"1 USD ≈ {rate:.2f} RUB" + self.cache.set(cache_key, result) + return result + except Exception: + return "Курс USD недоступен" + + async def get_rub_to_usd(self): + usd_rub = await self.get_usd_to_rub() + if "≈" in usd_rub: + try: + rate = float(usd_rub.split("≈")[1].strip().split()[0]) + return f"1 RUB ≈ {1/rate:.4f} USD" + except Exception: + pass + return "Курс RUB недоступен" + + async def get_usdt_to_rub(self): + return await self.get_usd_to_rub() # USDT ≈ USD + + async def get_rub_to_usdt(self): + return await self.get_rub_to_usd() + + async def get_ton_to_rub(self): + cache_key = "ton_rub" + cached = self.cache.get(cache_key) + if cached: + return cached + + try: + async with self.session.get("https://api.coingecko.com/api/v3/simple/price?ids=toncoin&vs_currencies=rub") as resp: + data = await resp.json() + rate = data["toncoin"]["rub"] + result = f"1 TON ≈ {rate:.2f} RUB" + self.cache.set(cache_key, result) + return result + except Exception: + return "Курс TON недоступен" + + async def get_rub_to_ton(self): + ton_rub = await self.get_ton_to_rub() + if "≈" in ton_rub: + try: + rate = float(ton_rub.split("≈")[1].strip().split()[0]) + return f"1 RUB ≈ {1/rate:.6f} TON" + except Exception: + pass + return "Курс недоступен" + + async def get_btc_to_rub(self): + cache_key = "btc_rub" + cached = self.cache.get(cache_key) + if cached: + return cached + + try: + async with self.session.get("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=rub") as resp: + data = await resp.json() + rate = data["bitcoin"]["rub"] + result = f"1 BTC ≈ {rate:,.0f} RUB" + self.cache.set(cache_key, result) + return result + except Exception: + return "Курс BTC недоступен" + + async def get_eth_to_rub(self): + cache_key = "eth_rub" + cached = self.cache.get(cache_key) + if cached: + return cached + + try: + async with self.session.get("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=rub") as resp: + data = await resp.json() + rate = data["ethereum"]["rub"] + result = f"1 ETH ≈ {rate:,.0f} RUB" + self.cache.set(cache_key, result) + return result + except Exception: + return "Курс ETH недоступен" + + async def get_stars_to_rub(self): + return "1 Star ≈ 85 RUB" + + async def get_stars_to_ton(self): + return "1 Star ≈ 0.012 TON" + + async def get_stars_to_usdt(self): + return "1 Star ≈ 0.92 USDT" + + async def get_os_uptime(self): + boot = datetime.fromtimestamp(psutil.boot_time()) + delta = datetime.now() - boot + days = delta.days + hours, remainder = divmod(delta.seconds, 3600) + minutes, _ = divmod(remainder, 60) + + if days > 0: + return f"{days}d {hours}h {minutes}m" + else: + return f"{hours}h {minutes}m" + + async def get_internet_usage(self): + try: + net = psutil.net_io_counters() + sent_gb = net.bytes_sent // (1024**3) + recv_gb = net.bytes_recv // (1024**3) + return f"↑ {sent_gb} GB │ ↓ {recv_gb} GB" + except Exception: + return "↑ 0 GB │ ↓ 0 GB" + + async def get_speedtest(self): + cache_key = "speedtest" + cached = self.cache.get(cache_key) + if cached: + return cached + + test_urls = [ + "https://proof.ovh.net/files/10Mb.dat", + "http://ipv4.download.thinkbroadband.com/10MB.zip", + "https://speedtest.ftp.otenet.gr/files/test10Mb.db" + ] + + for url in test_urls: + try: + start = time.time() + async with self.session.get(url, timeout=10) as resp: + chunk_size = 1024 * 1024 + total = 0 + async for chunk in resp.content.iter_chunked(chunk_size): + total += len(chunk) + if total >= chunk_size: + break + + duration = time.time() - start + if duration > 0: + speed_mbps = (total * 8) / (duration * 1024 * 1024) + result = f"≈ {speed_mbps:.1f} Mbps" + self.cache.set(cache_key, result) + return result + except Exception: + continue + + return "Тест скорости недоступен" + + async def get_host(self): + return platform.node() or "Неизвестно" + + async def get_shell(self): + return os.environ.get("SHELL", "Неизвестно").split("/")[-1] + + async def get_gpu(self): + return "N/A (Cloud)" + + async def get_disk(self): + try: + usage = psutil.disk_usage("/") + percent = (usage.used / usage.total) * 100 + used_gb = usage.used // (1024**3) + total_gb = usage.total // (1024**3) + return f"{used_gb} GB / {total_gb} GB ({percent:.1f}%)" + except Exception: + return "Диск недоступен" + + async def get_local_ip(self): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + return "Неизвестно" + + async def get_user_hostname(self): + user = os.getlogin() if hasattr(os, 'getlogin') else os.environ.get("USER", "user") + host = await self.get_host() + return f"{user}@{host}" + + async def get_time(self): + return datetime.now(self.tz).strftime("%H:%M:%S") + + async def get_date(self): + return datetime.now(self.tz).strftime("%d.%m.%Y") + + async def get_weekday(self): + return self.weekdays_ru[datetime.now(self.tz).weekday()] + + async def get_date_time(self): + return datetime.now(self.tz).strftime("%d.%m.%Y %H:%M") + + async def get_full_date_time_weekday(self): + now = datetime.now(self.tz) + return f"{now.strftime('%d.%m.%Y %H:%M')} ({self.weekdays_ru[now.weekday()]})" + + async def get_weather_condition(self): + data = await self._get_weather_data() + return data.get("condition", "Неизвестно") + + async def get_temperature(self): + data = await self._get_weather_data() + return data.get("temp", "??°C") + + async def get_weather_temp(self): + data = await self._get_weather_data() + return data.get("weather_temp", "??") + + async def get_humidity(self): + data = await self._get_weather_data() + return data.get("humidity", "??%") + + async def get_pressure(self): + data = await self._get_weather_data() + return data.get("pressure", "?? гПа") + + async def get_wind_speed(self): + data = await self._get_weather_data() + return data.get("wind", "?? м/с") + + async def _get_weather_data(self): + city = self.config["weather_city"] + + cache_key = f"weather_{city}" + cached = self.cache.get(cache_key) + if cached: + return cached + + try: + async with self.session.get(f"http://wttr.in/{city}?format=j1&lang=ru") as resp: + if resp.status == 200: + data = await resp.json() + c = data["current_condition"][0] + weather_data = { + "condition": c["lang_ru"][0]["value"], + "temp": f"{c['temp_C']}°C", + "weather_temp": f"{c['lang_ru'][0]['value']} {c['temp_C']}°C", + "humidity": f"{c['humidity']}%", + "pressure": f"{c['pressure']} мм", + "wind": f"{c['windspeedKmph']} км/ч", + } + self.cache.set(cache_key, weather_data) + return weather_data + except Exception: + pass + + default = { + "condition": "Неизвестно", + "temp": "??°C", + "weather_temp": "??", + "humidity": "??%", + "pressure": "?? мм", + "wind": "?? км/ч", + } + self.cache.set(cache_key, default) + return default + + async def get_crypto_address(self): + return self.config["crypto_address"] + + async def get_card_number(self): + return self.config["card_number"] + + async def get_donate_site(self): + val = self.config["donate_site"] + if ":" in val: + name, link = val.split(":", 1) + return f'{name.strip()}' + return val + + async def get_channel(self): + ch = self.config["channel"] + if ch.startswith("@"): + return f'{ch}' + return ch + + async def get_social(self): + return self.config["social_network"] + + async def get_lastfm_user(self): + return self.config["lastfm_user"] or "Не указан" + + async def get_now_playing(self): + track = await self._get_current_track() + if not track: + return "🎵 Ничего не играет" + return f"🎵 {track['name']} — {track['artist']}" + + async def get_user_and_playing(self): + user = await self.get_lastfm_user() + track = await self._get_current_track() + if not track: + return f"{user}: ничего не играет" + return f"{user}: {track['name']} — {track['artist']}" + + async def get_song_name(self): + track = await self._get_current_track() + return track["name"] if track else "—" + + async def get_song_artist(self): + track = await self._get_current_track() + return track["artist"] if track else "—" + + async def get_lastfm_stats(self): + user = self.config["lastfm_user"] + if not user: + return "Укажите Last.FM username" + + cache_key = f"lastfm_stats_{user}" + cached = self.cache.get(cache_key) + if cached: + return cached + + api_key = "460cda35be2fbf4f28e8ea7a38580730" + + try: + async with self.session.get( + "http://ws.audioscrobbler.com/2.0/", + params={ + "method": "user.getinfo", + "user": user, + "api_key": api_key, + "format": "json" + } + ) as resp: + data = await resp.json() + if "user" in data: + stats = data["user"] + result = f"🎵 {stats['playcount']} скробблов" + self.cache.set(cache_key, result) + return result + except Exception: + pass + + return "Статистика недоступна" + + async def _get_current_track(self): + user = self.config["lastfm_user"] + if not user: + return None + + cache_key = f"lastfm_track_{user}" + cached = self.cache.get(cache_key) + if cached: + return cached + + api_key = "460cda35be2fbf4f28e8ea7a38580730" + + try: + async with self.session.get( + "http://ws.audioscrobbler.com/2.0/", + params={ + "method": "user.getrecenttracks", + "user": user, + "api_key": api_key, + "format": "json", + "limit": 1 + } + ) as resp: + data = await resp.json() + tracks = data.get("recenttracks", {}).get("track", []) + + if tracks: + track = tracks[0] + now_playing = "@attr" in track and "nowplaying" in track["@attr"] + + result = { + "name": track["name"], + "artist": track["artist"]["#text"], + "now_playing": now_playing + } + self.cache.set(cache_key, result) + return result + except Exception: + pass + + return None + + async def on_unload(self): + utils.unregister_placeholders(self.__class__.__name__) + try: + await self.session.close() + except Exception: + pass \ No newline at end of file diff --git a/fiksofficial/python-modules/stream.py b/fiksofficial/python-modules/stream.py index bd048d4..771b5f2 100644 --- a/fiksofficial/python-modules/stream.py +++ b/fiksofficial/python-modules/stream.py @@ -1 +1,435 @@ -# Security issue in this module. RTMP Key doesn't hide in config with vaildator Hidden, because of that, we will wait for update from developer to fix it + +import asyncio +import mimetypes +import os +import subprocess +import time + +from .. import loader, utils +from ..inline.types import InlineCall + +def detect_type(path: str) -> str: + mime, _ = mimetypes.guess_type(path) + if not mime: + return "video" + if mime.startswith("video"): + return "video" + if mime.startswith("audio"): + return "audio" + if mime.startswith("image"): + return "image" + return "video" + +TYPE_ICON = {"video": "🎬", "audio": "🎵", "image": "🖼️"} +PRESETS = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow"] +TUNES = ["zerolatency", "film", "animation", "grain", "stillimage", "fastdecode"] +SCALES = ["off", "426x240", "640x360", "854x480", "1280x720", "1920x1080", "2560x1440"] +FPS_OPT = [24, 25, 30, 48, 60] + +def build_cmd(file_path: str, rtmp_url: str, cfg: dict) -> list: + preset = cfg.get("preset", "veryfast") + tune = cfg.get("tune", "zerolatency") + vbr = cfg.get("vbitrate", "2000k") + abr = cfg.get("abitrate", "128k") + fps = str(cfg.get("fps", 30)) + res = cfg.get("resolution", None) + threads = str(cfg.get("threads", 0)) + gop = str(int(fps) * 2) + bufsize = str(int(vbr.replace("k", "")) * 2) + "k" + ftype = detect_type(file_path) + + base = ["ffmpeg", "-re", "-stream_loop", "-1", "-threads", threads] + vf_scale = f",scale={res}" if res else "" + common_v = [ + "-c:v", "libx264", "-preset", preset, "-tune", tune, + "-pix_fmt", "yuv420p", "-profile:v", "baseline", + "-r", fps, "-g", gop, "-keyint_min", gop, "-sc_threshold", "0", + "-b:v", vbr, "-maxrate", vbr, "-bufsize", bufsize, + ] + common_a = ["-c:a", "aac", "-b:a", abr, "-ar", "44100"] + out = ["-f", "flv", rtmp_url] + + if ftype == "video": + vf = ["-vf", f"scale=trunc(iw/2)*2:trunc(ih/2)*2{vf_scale}"] if res else [] + return base + ["-i", file_path] + common_v + vf + common_a + out + if ftype == "audio": + size = res or "1280x720" + return ( + base + + ["-i", file_path, "-f", "lavfi", "-i", f"color=c=black:s={size}:r={fps}"] + + ["-shortest"] + common_v + common_a + + ["-map", "1:v:0", "-map", "0:a:0"] + out + ) + if ftype == "image": + scale_vf = f"scale=trunc(iw/2)*2:trunc(ih/2)*2{vf_scale}" + return ( + base + + ["-loop", "1", "-i", file_path, "-f", "lavfi", "-i", "anullsrc=r=44100:cl=stereo"] + + ["-vf", scale_vf] + common_v + + ["-shortest"] + common_a + + ["-map", "0:v:0", "-map", "1:a:0"] + out + ) + raise ValueError(f"Unsupported: {ftype}") + +@loader.tds +class StreamMod(loader.Module): + """📡 RTMP media streaming""" + strings = { + "name": "Stream", + "status_active": "▶️ Stream is live\n\n{icon} {file}\n⏱ Time: {elapsed}\n🔢 PID: {pid}\n📡 {rtmp}\n🎥 {vbr} | {fps}fps | {preset}\n🔊 {abr}\n📋 Queue: {queue}", + "status_idle": "⏸ Stream is not active", + "status_queue": "\n📋 Queue: {n}", + "stopped": "⏹ Stream stopped.", + "no_rtmp": "❌ RTMP not configured!\nTap a button to set it up.", + "downloading": "⏳ Downloading…", + "dl_failed": "❌ Failed to download file.", + "queued": "📋 Added to queue ({n})\n{icon} {file}", + "not_running": "Not running", + "queue_empty": "Queue is empty", + "queue_header": "📋 Queue:\n", + "settings_title": "⚙️ Stream settings", + "btn_stop": "⏹ Stop", + "btn_queue": "📋 Queue", + "btn_refresh": "🔄 Refresh", + "btn_settings": "⚙️ Settings", + "btn_status": "📊 Status", + "btn_back": "🔙 Back", + "btn_preset": "🎞 Preset: {v}", + "btn_tune": "🎭 Tune: {v}", + "btn_vbr": "🎥 Video: {v}", + "btn_abr": "🔊 Audio: {v}", + "btn_fps": "📐 FPS: {v}", + "btn_res": "🖥 Res: {v}", + "btn_threads": "🧵 Threads: {v}", + "btn_rtmps": "📡 RTMP URL", + "btn_key": "🔑 Stream key", + "btn_set_rtmps": "📡 Set RTMP URL", + "btn_set_key": "🔑 Set stream key", + "ph_vbr": "Video bitrate, e.g. 2000k", + "ph_abr": "Audio bitrate, e.g. 128k", + "ph_threads": "Thread count (0 = auto)", + "ph_rtmps": "rtmp://a.rtmp.youtube.com/live2", + "ph_key": "Stream key...", + } + + strings_ru = { + "_cls_doc": "📡 RTMP стриминг медиафайлов", + "status_active": "▶️ Трансляция идёт\n\n{icon} {file}\n⏱ Время: {elapsed}\n🔢 PID: {pid}\n📡 {rtmp}\n🎥 {vbr} | {fps}fps | {preset}\n🔊 {abr}\n📋 В очереди: {queue}", + "status_idle": "⏸ Трансляция не активна", + "status_queue": "\n📋 В очереди: {n}", + "stopped": "⏹ Трансляция остановлена.", + "no_rtmp": "❌ RTMP не настроен!\nНажми кнопку чтобы задать прямо сейчас.", + "downloading": "⏳ Скачиваю…", + "dl_failed": "❌ Не удалось скачать файл.", + "queued": "📋 Добавлено в очередь ({n} шт.)\n{icon} {file}", + "not_running": "Не запущено", + "queue_empty": "Очередь пуста", + "queue_header": "📋 Очередь:\n", + "settings_title": "⚙️ Настройки трансляции", + "btn_stop": "⏹ Стоп", + "btn_queue": "📋 Очередь", + "btn_refresh": "🔄 Обновить", + "btn_settings": "⚙️ Настройки", + "btn_status": "📊 Статус", + "btn_back": "🔙 Назад", + "btn_preset": "🎞 Пресет: {v}", + "btn_tune": "🎭 Tune: {v}", + "btn_vbr": "🎥 Видео: {v}", + "btn_abr": "🔊 Аудио: {v}", + "btn_fps": "📐 FPS: {v}", + "btn_res": "🖥 Разр: {v}", + "btn_threads": "🧵 Треды: {v}", + "btn_rtmps": "📡 RTMP URL", + "btn_key": "🔑 Ключ", + "btn_set_rtmps": "📡 Задать RTMP URL", + "btn_set_key": "🔑 Задать ключ", + "ph_vbr": "Битрейт видео, напр. 2000k", + "ph_abr": "Битрейт аудио, напр. 128k", + "ph_threads": "Потоков (0 = авто)", + "ph_rtmps": "rtmp://a.rtmp.youtube.com/live2", + "ph_key": "Ключ трансляции...", + } + + def __init__(self): + self._proc: subprocess.Popen | None = None + self._file: str | None = None + self._started: float | None = None + self._queue: list[str] = [] + self._qtask: asyncio.Task | None = None + self.config = loader.ModuleConfig( + loader.ConfigValue("rtmps", "", "Base RTMP URL (rtmp://...)"), + loader.ConfigValue("key", "", "Stream key"), + loader.ConfigValue("preset", "veryfast", "x264 preset", + validator=loader.validators.Choice(PRESETS)), + loader.ConfigValue("tune", "zerolatency","x264 tune", + validator=loader.validators.Choice(TUNES)), + loader.ConfigValue("vbitrate", "2000k", "Video bitrate (e.g. 1500k, 3000k)"), + loader.ConfigValue("abitrate", "128k", "Audio bitrate (e.g. 64k, 192k)"), + loader.ConfigValue("fps", 30, "Frames per second", + validator=loader.validators.Integer(minimum=1, maximum=120)), + loader.ConfigValue("resolution", "", "Output resolution (e.g. 1280x720, empty = no scaling)"), + loader.ConfigValue("threads", 0, "FFmpeg thread count (0 = auto)", + validator=loader.validators.Integer(minimum=0, maximum=64)), + loader.ConfigValue("loop", True, "Loop the file indefinitely", + validator=loader.validators.Boolean()), + loader.ConfigValue("reconnect", True, "Auto-restart on stream disconnect", + validator=loader.validators.Boolean()), + ) + + def _s(self, key: str, **kw) -> str: + return self.strings[key].format(**kw) if kw else self.strings[key] + + def _running(self) -> bool: + return self._proc is not None and self._proc.poll() is None + + def _stop(self): + if self._proc: + try: + self._proc.terminate() + self._proc.wait(timeout=5) + except Exception: + try: + self._proc.kill() + except Exception: + pass + self._proc = None + if self._file and os.path.exists(self._file): + try: + os.remove(self._file) + except Exception: + pass + self._file = None + self._started = None + + def _launch(self, path: str): + cfg = {k: self.config[k] for k in ("preset", "tune", "vbitrate", "abitrate", "fps", "threads")} + cfg["resolution"] = self.config["resolution"] or None + rtmp = f"{self.config['rtmps'].rstrip('/')}/{self.config['key']}" + self._proc = subprocess.Popen(build_cmd(path, rtmp, cfg), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self._file = path + self._started = time.time() + + def _elapsed(self) -> str: + if not self._started: + return "00:00:00" + e = int(time.time() - self._started) + return f"{e//3600:02d}:{(e%3600)//60:02d}:{e%60:02d}" + + def _status_text(self) -> str: + if not self._running(): + txt = self._s("status_idle") + if self._queue: + txt += self._s("status_queue", n=len(self._queue)) + return txt + ftype = detect_type(self._file or "") + rtmp = f"{self.config['rtmps'].rstrip('/')}/{self.config['key'][:4]}***" + return self._s( + "status_active", + icon=TYPE_ICON.get(ftype, "📄"), + file=os.path.basename(self._file or "?"), + elapsed=self._elapsed(), + pid=self._proc.pid if self._proc else "—", + rtmp=rtmp, + vbr=self.config["vbitrate"], + fps=self.config["fps"], + preset=self.config["preset"], + abr=self.config["abitrate"], + queue=len(self._queue), + ) + + def _res_label(self) -> str: + r = self.config["resolution"] + return r if r else "auto" + + def _thr_label(self) -> str: + t = self.config["threads"] + return str(t) if t else "auto" + + def _main_markup(self) -> list: + running = self._running() + return [ + [ + {"text": self._s("btn_stop"), "callback": self._cb_stop} if running + else {"text": self._s("btn_queue"), "callback": self._cb_queue}, + {"text": self._s("btn_refresh"), "callback": self._cb_refresh}, + ], + [ + {"text": self._s("btn_settings"), "callback": self._cb_settings}, + {"text": self._s("btn_status"), "callback": self._cb_status}, + ], + ] + + def _settings_markup(self) -> list: + return [ + [ + {"text": self._s("btn_preset", v=self.config["preset"]), "callback": self._cb_set_preset}, + {"text": self._s("btn_tune", v=self.config["tune"]), "callback": self._cb_set_tune}, + ], + [ + {"text": self._s("btn_vbr", v=self.config["vbitrate"]), + "input": self._s("ph_vbr"), "handler": self._ih_vbr}, + {"text": self._s("btn_abr", v=self.config["abitrate"]), + "input": self._s("ph_abr"), "handler": self._ih_abr}, + ], + [ + {"text": self._s("btn_fps", v=self.config["fps"]), "callback": self._cb_set_fps}, + {"text": self._s("btn_res", v=self._res_label()), "callback": self._cb_set_res}, + ], + [ + {"text": self._s("btn_threads", v=self._thr_label()), + "input": self._s("ph_threads"), "handler": self._ih_threads}, + ], + [ + {"text": self._s("btn_rtmps"), + "input": self._s("ph_rtmps"), "handler": self._ih_rtmps}, + {"text": self._s("btn_key"), + "input": self._s("ph_key"), "handler": self._ih_key}, + ], + [{"text": self._s("btn_back"), "callback": self._cb_back}], + ] + + async def _ih_vbr(self, call: InlineCall, query: str): + q = query.strip() + if q.endswith("k") and q[:-1].isdigit(): + self.config["vbitrate"] = q + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _ih_abr(self, call: InlineCall, query: str): + q = query.strip() + if q.endswith("k") and q[:-1].isdigit(): + self.config["abitrate"] = q + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _ih_threads(self, call: InlineCall, query: str): + q = query.strip() + if q.isdigit(): + self.config["threads"] = int(q) + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _ih_rtmps(self, call: InlineCall, query: str): + q = query.strip() + if q.startswith("rtmp"): + self.config["rtmps"] = q.rstrip("/") + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _ih_key(self, call: InlineCall, query: str): + q = query.strip() + if q: + self.config["key"] = q + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _cb_refresh(self, call: InlineCall): + await call.edit(self._status_text(), reply_markup=self._main_markup()) + + async def _cb_status(self, call: InlineCall): + await call.answer(self._elapsed() if self._running() else self._s("not_running")) + + async def _cb_stop(self, call: InlineCall): + self._queue.clear() + if self._qtask: + self._qtask.cancel() + self._qtask = None + self._stop() + await call.edit(self._s("stopped"), reply_markup=self._main_markup()) + + async def _cb_queue(self, call: InlineCall): + if not self._queue: + await call.answer(self._s("queue_empty"), show_alert=True) + return + lines = [f"{i}. {TYPE_ICON.get(detect_type(f), '📄')} {os.path.basename(f)}" + for i, f in enumerate(self._queue, 1)] + await call.answer(self._s("queue_header") + "\n".join(lines), show_alert=True) + + async def _cb_back(self, call: InlineCall): + await call.edit(self._status_text(), reply_markup=self._main_markup()) + + async def _cb_settings(self, call: InlineCall): + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _cb_set_preset(self, call: InlineCall): + cur = self.config["preset"] + self.config["preset"] = PRESETS[(PRESETS.index(cur) + 1) % len(PRESETS)] + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _cb_set_tune(self, call: InlineCall): + cur = self.config["tune"] + self.config["tune"] = TUNES[(TUNES.index(cur) + 1) % len(TUNES)] + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _cb_set_fps(self, call: InlineCall): + cur = self.config["fps"] + self.config["fps"] = FPS_OPT[(FPS_OPT.index(cur) + 1) % len(FPS_OPT)] if cur in FPS_OPT else 30 + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + async def _cb_set_res(self, call: InlineCall): + cur = self.config["resolution"] or "off" + idx = SCALES.index(cur) if cur in SCALES else 0 + nxt = SCALES[(idx + 1) % len(SCALES)] + self.config["resolution"] = "" if nxt == "off" else nxt + await call.edit(self._s("settings_title"), reply_markup=self._settings_markup()) + + @loader.command(ru_doc="[ответ на медиа] – запустить трансляцию") + async def stream(self, message): + """[reply to media] — start stream or add to queue""" + if not self.config["rtmps"] or not self.config["key"]: + await self.inline.form( + self._s("no_rtmp"), + message=message, + reply_markup=[ + [{"text": self._s("btn_set_rtmps"), "input": self._s("ph_rtmps"), "handler": self._ih_rtmps}], + [{"text": self._s("btn_set_key"), "input": self._s("ph_key"), "handler": self._ih_key}], + ], + ) + return + + reply = await message.get_reply_message() + if not reply or not reply.media: + await self.inline.form( + self._status_text(), + message=message, + reply_markup=self._main_markup(), + ) + return + + status = await utils.answer(message, self._s("downloading")) + path = await reply.download_media(file=f"/tmp/stream_{int(time.time())}") + if not path: + await status.edit(self._s("dl_failed")) + return + await status.delete() + + if self._running(): + self._queue.append(path) + await self.inline.form( + self._s("queued", n=len(self._queue), icon=TYPE_ICON.get(detect_type(path), "📄"), file=os.path.basename(path)), + message=message, + reply_markup=self._main_markup(), + ) + return + + self._stop() + self._launch(path) + await self.inline.form( + self._status_text(), + message=message, + reply_markup=self._main_markup(), + ) + + @loader.command(ru_doc="– панель управления трансляцией") + async def streamctl(self, message): + """– open stream control panel""" + await self.inline.form( + self._status_text(), + message=message, + reply_markup=self._main_markup(), + ) + + @loader.command(ru_doc="– остановить трансляцию и очистить очередь") + async def streamstop(self, message): + """– stop stream and clear queue""" + self._queue.clear() + if self._qtask: + self._qtask.cancel() + self._qtask = None + self._stop() + await utils.answer(message, self._s("stopped")) \ No newline at end of file diff --git a/mead0wsss/mead0wsMods/SenderGifts.py b/mead0wsss/mead0wsMods/SenderGifts.py index 195a232..b925606 100644 --- a/mead0wsss/mead0wsMods/SenderGifts.py +++ b/mead0wsss/mead0wsMods/SenderGifts.py @@ -1,5 +1,5 @@ # -- version -- -__version__ = (1, 2, 3) +__version__ = (1, 2, 4) # -- version -- @@ -14,6 +14,8 @@ __version__ = (1, 2, 3) # meta developer: @mead0wssMods x @nullmod +# meta banner: https://files.catbox.moe/nie3ef.jpg +# banner by: @SunnexGB # scope: heroku_only from .. import loader, utils @@ -22,10 +24,12 @@ from herokutl.tl.types import InputInvoiceStarGift, TextWithEntities from herokutl.errors.rpcerrorlist import BadRequestError import logging import herokutl +import aiohttp +import json @loader.tds class SenderGifts(loader.Module): - """Модуль для отправки подарков Telegram прямиком в чате""" + """Модуль для отправки обычных и удаленных подарков Telegram прямиком в чате""" strings = { "name": "SenderGifts", @@ -43,25 +47,27 @@ class SenderGifts(loader.Module): "min_stars_error": " Недостаточно звезд для отправки минимального подарка!", "no_available_gifts": " Нет доступных подарков для вашего баланса", "balance_error": " Ошибка при проверке баланса", + "user_disallowed_gifts": " Данный пользователь не принимает подарки!", "btn_public": "📢 Публично", "btn_anon": "🕵️ Анонимно", } + # резерв regular_gifts = { - 15: [ + 15:[ {"id": 5170145012310081615, "emoji": "❤️", "name": "Сердце"}, {"id": 5170233102089322756, "emoji": "🧸", "name": "Мишка"}, ], - 25: [ + 25:[ {"id": 5170250947678437525, "emoji": "🎁", "name": "Подарок"}, {"id": 5168103777563050263, "emoji": "🌹", "name": "Роза"}, ], - 50: [ + 50:[ {"id": 5170144170496491616, "emoji": "🎂", "name": "Тортик"}, {"id": 5170314324215857265, "emoji": "💐", "name": "Цветы"}, {"id": 5170564780938756245, "emoji": "🚀", "name": "Ракета"}, ], - 100: [ + 100:[ {"id": 5168043875654172773, "emoji": "🏆", "name": "Кубок"}, {"id": 5170690322832818290, "emoji": "💍", "name": "Кольцо"}, {"id": 5170521118301225164, "emoji": "💎", "name": "Алмаз"}, @@ -71,32 +77,52 @@ class SenderGifts(loader.Module): unique_gifts = { "new_year": { "name": "🎄 Новогодние подарки", - "gifts": [ + "gifts":[ {"id": 5922558454332916696, "emoji": "🎄", "name": "Ёлка", "price": 50}, {"id": 5956217000635139069, "emoji": "🧸", "name": "Новогодний мишка", "price": 50}, ] }, "valentines": { "name": "💘 День святого валентина", - "gifts": [ + "gifts":[ {"id": 5800655655995968830, "emoji": "🧸", "name": "14 Февраля мишка", "price": 50}, {"id": 5801108895304779062, "emoji": "💘", "name": "14 Февраля сердце", "price": 50}, ] }, "march_8th": { "name": "🌷 8 Марта", - "gifts": [ + "gifts":[ {"id": 5866352046986232958, "emoji": "🧸", "name": "8 Марта мишка", "price": 50}, ] }, - "saint_patricks_day ": { + "saint_patricks_day": { "name": "💰 День святого патрика", - "gifts": [ + "gifts":[ {"id": 5893356958802511476, "emoji": "🧸", "name": "Лепрекон мишка", "price": 50}, ] + }, + "april_1th": { + "name": "🤪 1 Апреля", + "gifts":[ + {"id": 5935895822435615975, "emoji": "🧸", "name": "1 Апреля мишка", "price": 50} + ] } } + async def fetch_gifts_from_github(self): + url = "https://raw.githubusercontent.com/mead0wsss/mead0wsMods/main/gifts.json" + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=5) as response: + if response.status == 200: + data = await response.json(content_type=None) + if "regular_gifts" in data: + self.regular_gifts = {int(k): v for k, v in data["regular_gifts"].items()} + if "unique_gifts" in data: + self.unique_gifts = data["unique_gifts"] + except Exception as e: + logging.error(f"Не удалось загрузить подарки с GitHub: {e}") + async def get_star_balance(self): try: balance_info = (await self.client(GetStarsStatusRequest("me"))) @@ -108,6 +134,8 @@ class SenderGifts(loader.Module): @loader.command() async def sendgift(self, message): """- - отправить подарок пользователю (* - необязательный параметр.) Поддерживается реплай режим.""" + + await self.fetch_gifts_from_github() args = utils.get_args_html(message) reply = await message.get_reply_message() if reply: @@ -152,7 +180,6 @@ class SenderGifts(loader.Module): helper_msg = await self.inline.form("🪐", balance_msg) - await self._show_main_menu_logic(helper_msg, user.id, text, balance, message.id, answer=True) async def _show_main_menu_logic(self, msg_or_call, user_id, text, balance, msg_id, answer=False): @@ -162,13 +189,11 @@ class SenderGifts(loader.Module): except: user_display = f"ID: {user_id}" - buttons = [ - [{ + buttons = [[{ "text": "🎁 Обычные подарки", "callback": self._show_regular_categories, "args": (user_id, text, balance, msg_id), - }], - [{ + }],[{ "text": "✨ Уникальные подарки", "callback": self._show_unique_categories, "args": (user_id, text, balance, msg_id), @@ -192,10 +217,10 @@ class SenderGifts(loader.Module): except: user_display = f"ID: {user_id}" - available_categories = [price for price in self.regular_gifts.keys() if balance >= price] + available_categories =[price for price in self.regular_gifts.keys() if balance >= price] buttons = [] - row = [] + row =[] for price in sorted(available_categories): row.append({ "text": f"{price} ⭐", @@ -226,7 +251,7 @@ class SenderGifts(loader.Module): except: user_display = f"ID: {user_id}" - buttons = [] + buttons =[] for cat_id, cat_data in self.unique_gifts.items(): if any(balance >= gift["price"] for gift in cat_data["gifts"]): buttons.append([{ @@ -256,7 +281,7 @@ class SenderGifts(loader.Module): async def _show_category(self, call, user_id, price, text, balance, msg_id): gifts = self.regular_gifts[price] buttons = [] - row = [] + row =[] for gift in gifts: row.append({ "text": gift["emoji"], @@ -265,7 +290,7 @@ class SenderGifts(loader.Module): }) if len(row) == 3: buttons.append(row) - row = [] + row =[] if row: buttons.append(row) @@ -299,7 +324,7 @@ class SenderGifts(loader.Module): }) if len(row) == 3: buttons.append(row) - row = [] + row =[] if row: buttons.append(row) @@ -326,8 +351,7 @@ class SenderGifts(loader.Module): else: back_callback = self._show_unique_category_gifts - buttons = [ - [ + buttons = [[ { "text": self.strings["btn_public"], "callback": self._send_gift, @@ -338,8 +362,7 @@ class SenderGifts(loader.Module): "callback": self._send_gift, "args": (user_id, gift_id, text, gift_emoji, msg_id, balance, True) } - ], - [ + ],[ { "text": "⬅️ Назад", "callback": back_callback, @@ -369,7 +392,7 @@ class SenderGifts(loader.Module): user, gift_id, hide_name=hide_name, - message=TextWithEntities(text, entities) if text else TextWithEntities("", []) + message=TextWithEntities(text, entities) if text else TextWithEntities("",[]) ) form = await self.client(GetPaymentFormRequest(inv)) result = await self.client(SendStarsFormRequest(form.form_id, inv)) @@ -382,6 +405,11 @@ class SenderGifts(loader.Module): self.strings["not_enough_stars"].format(gift_emoji), reply_markup=None ) + elif "USER_DISALLOWED_STARGIFTS" in str(e): + await call.edit( + self.strings["user_disallowed_gifts"].format(gift_emoji), + reply_markup=None + ) else: logging.error(f"Error sending gift: {e}") await call.edit( diff --git a/mead0wsss/mead0wsMods/gifts.json b/mead0wsss/mead0wsMods/gifts.json new file mode 100644 index 0000000..a7c4456 --- /dev/null +++ b/mead0wsss/mead0wsMods/gifts.json @@ -0,0 +1,62 @@ +{ + "regular_gifts": { + "15":[ + {"id": 5170145012310081615, "emoji": "❤️", "name": "Сердце"}, + {"id": 5170233102089322756, "emoji": "🧸", "name": "Мишка"} + ], + "25":[ + {"id": 5170250947678437525, "emoji": "🎁", "name": "Подарок"}, + {"id": 5168103777563050263, "emoji": "🌹", "name": "Роза"} + ], + "50":[ + {"id": 5170144170496491616, "emoji": "🎂", "name": "Тортик"}, + {"id": 5170314324215857265, "emoji": "💐", "name": "Цветы"}, + {"id": 5170564780938756245, "emoji": "🚀", "name": "Ракета"} + ], + "100":[ + {"id": 5168043875654172773, "emoji": "🏆", "name": "Кубок"}, + {"id": 5170690322832818290, "emoji": "💍", "name": "Кольцо"}, + {"id": 5170521118301225164, "emoji": "💎", "name": "Алмаз"} + ] + }, + "unique_gifts": { + "new_year": { + "name": "🎄 Новогодние подарки", + "gifts":[ + {"id": 5922558454332916696, "emoji": "🎄", "name": "Ёлка", "price": 50}, + {"id": 5956217000635139069, "emoji": "🧸", "name": "Новогодний мишка", "price": 50} + ] + }, + "valentines": { + "name": "💘 День святого валентина", + "gifts":[ + {"id": 5800655655995968830, "emoji": "🧸", "name": "14 Февраля мишка", "price": 50}, + {"id": 5801108895304779062, "emoji": "💘", "name": "14 Февраля сердце", "price": 50} + ] + }, + "march_8th": { + "name": "🌷 8 Марта", + "gifts":[ + {"id": 5866352046986232958, "emoji": "🧸", "name": "8 Марта мишка", "price": 50} + ] + }, + "saint_patricks_day": { + "name": "💰 День святого патрика", + "gifts":[ + {"id": 5893356958802511476, "emoji": "🧸", "name": "Лепрекон мишка", "price": 50} + ] + }, + "april_1th": { + "name": "🤪 1 Апреля", + "gifts":[ + {"id": 5935895822435615975, "emoji": "🧸", "name": "1 Апреля мишка", "price": 50} + ] + }, + "easter_day": { + "name": "🥚 Пасха", + "gifts":[ + {"id": 5969796561943660080, "emoji": "🧸", "name": "Пасхальный мишка", "price": 50} + ] + } + } +} diff --git a/radiocycle/Modules/PicToStories.py b/radiocycle/Modules/PicToStories.py index 84bbd9a..b244a2c 100644 --- a/radiocycle/Modules/PicToStories.py +++ b/radiocycle/Modules/PicToStories.py @@ -179,7 +179,7 @@ class PicToStoriesMod(loader.Module): all_albums = await self.client( functions.stories.GetAlbumsRequest(peer=types.InputPeerSelf(), hash=0) ) - + target = next( (a for a in all_albums.albums if getattr(a, 'title', '') == args), None @@ -201,11 +201,11 @@ class PicToStoriesMod(loader.Module): title=args, ) ) - else: - await self.client( - functions.stories.TogglePinnedRequest( - peer=types.InputPeerSelf(), id=story_ids, pinned=True - ) + + await self.client( + functions.stories.TogglePinnedRequest( + peer=types.InputPeerSelf(), id=story_ids, pinned=True ) + ) await utils.answer(message, self.strings("done")) \ No newline at end of file diff --git a/radiocycle/Modules/RandomAnimePic.py b/radiocycle/Modules/RandomAnimePic.py new file mode 100644 index 0000000..f609d7e --- /dev/null +++ b/radiocycle/Modules/RandomAnimePic.py @@ -0,0 +1,181 @@ +# ======================================= +# _ __ __ __ _ +# | |/ /___ | \/ | ___ __| |___ +# | ' // _ \ | |\/| |/ _ \ / _` / __| +# | . \ __/ | | | | (_) | (_| \__ \ +# |_|\_\___| |_| |_|\___/ \__,_|___/ +# @ke_mods +# ======================================= +# +# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) +# -------------------------------------- +# https://creativecommons.org/licenses/by-nd/4.0/legalcode +# ======================================= + +# meta developer: @ke_mods +# requires: pillow + +import asyncio +import logging +import traceback +from logging import basicConfig +from io import BytesIO + +import requests +from PIL import Image + +from .. import loader, utils + +basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@loader.tds +class RandomAnimePicMod(loader.Module): + strings = { + "name": "RandomAnimePic", + "img": " Your anime pic\n🔗 URL: {}", + "loading": " Loading image...", + "categories_loading": " Loading categories...", + "categories": " Available categories\n
{}
", + "no_categories": "🚫 Categories not found", + "error": "🚫 An unexpected error occurred...", + } + + strings_ru = { + "img": " Ваша аниме-картинка\n🔗 Ссылка: {}", + "loading": " Загрузка изображения...", + "categories_loading": " Загрузка категорий...", + "categories": " Доступные категории\n
{}
", + "no_categories": "🚫 Категории не найдены", + "error": "🚫 Произошла непредвиденная ошибка...", + } + + RANDOM_API_URL = "https://api.nekosapi.com/v4/images/random" + IMAGES_API_URL = "https://api.nekosapi.com/v4/images" + CATEGORIES_SCAN_LIMIT = 500 + + def __init__(self): + self.config = loader.ModuleConfig( + loader.ConfigValue( + "category", + "", + "Category", + validator=loader.validators.String(), + ), + ) + + @loader.command(ru_doc="- получить рандомную аниме-картинку 👀") + async def rapiccmd(self, message): + """- fetch random anime-pic 👀""" + await utils.answer(message, self.strings("loading")) + + try: + category = self.config["category"].strip() + + def fetch_image(): + params = {"limit": 1, "rating": ["safe"]} + + if category: + params["tags"] = [category] + + response = requests.get(self.RANDOM_API_URL, params=params, timeout=15) + response.raise_for_status() + + data = response.json() + if not isinstance(data, list) or not data: + raise ValueError("API returned empty response") + + url = data[0].get("url") + if not url: + raise ValueError("API response does not contain image url") + + image_response = requests.get(url, timeout=20) + image_response.raise_for_status() + + image_stream = BytesIO(image_response.content) + image = Image.open(image_stream) + image.load() + + output = BytesIO() + if "A" in image.getbands() or image.mode == "P": + image.convert("RGBA").save(output, format="PNG") + output.name = "anime.png" + else: + image.convert("RGB").save(output, format="JPEG", quality=95) + output.name = "anime.jpg" + + output.seek(0) + return url, output + + url, file = await asyncio.to_thread(fetch_image) + await utils.answer( + message, + self.strings("img").format(url), + file=file + ) + + except Exception: + logger.error( + "Error fetching random anime pic: %s", + traceback.format_exc(), + ) + await utils.answer(message, self.strings("error")) + + @loader.command(ru_doc="- получить список категорий из API 👀") + async def racategoriescmd(self, message): + """- fetch categories from api 👀""" + await utils.answer(message, self.strings("categories_loading")) + + try: + def fetch_categories() -> list[str]: + tags = set() + offset = 0 + + while offset < self.CATEGORIES_SCAN_LIMIT: + response = requests.get( + self.IMAGES_API_URL, + params={ + "limit": 100, + "offset": offset, + "rating": ["safe"], + }, + timeout=20, + ) + response.raise_for_status() + + data = response.json() + items = data.get("items") or data.get("results") or [] + if not items: + break + + for item in items: + tags.update(item.get("tags", [])) + + if len(items) < 100: + break + + offset += 100 + + return sorted(tags) + + categories = await asyncio.to_thread(fetch_categories) + + if not categories: + await utils.answer(message, self.strings("no_categories")) + return + + formatted_categories = "\n".join( + f"{category}" for category in categories + ) + await utils.answer( + message, + self.strings("categories").format(formatted_categories), + ) + + except Exception: + logger.error( + "Error fetching categories: %s", + traceback.format_exc(), + ) + await utils.answer(message, self.strings("error")) \ No newline at end of file diff --git a/radiocycle/Modules/SpotifyMod.py b/radiocycle/Modules/SpotifyMod.py index 218588b..d1ea339 100644 --- a/radiocycle/Modules/SpotifyMod.py +++ b/radiocycle/Modules/SpotifyMod.py @@ -1,16 +1,12 @@ # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ # █▀█ █ █ █ █▀█ █▀▄ █ # © Copyright 2022 +# https://t.me/hikariatama # -# https://t.me/hikariatama -# -# 🔒 Licensed under the GNU AGPLv3 +# 🔒 Licensed under the GNU AGPLv3 # 🌐 https://www.gnu.org/licenses/agpl-3.0.html -# -# You CANNOT edit, distribute or redistribute this file without direct permission from the author. -# # ORIGINAL MODULE: https://raw.githubusercontent.com/hikariatama/ftg/master/spotify.py - +# # ======================================= # _ __ __ __ _ # | |/ /___ | \/ | ___ __| |___ @@ -24,9 +20,12 @@ # -------------------------------------- # https://creativecommons.org/licenses/by-nd/4.0/legalcode # ======================================= - +# # meta developer: @ke_mods -# requires: telethon spotipy pillow requests yt-dlp +# requires: telethon spotipy pillow requests yt-dlp curl_cffi +# scope: ffmpeg + +__version__ = (1, 0) import asyncio import contextlib @@ -45,6 +44,7 @@ import spotipy from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont, ImageOps from telethon.errors import FloodWaitError from telethon.tl.functions.account import UpdateProfileRequest +from telethon.tl.functions.users import GetFullUserRequest from telethon.tl.types import Message from .. import loader, utils @@ -60,7 +60,8 @@ class Banners: duration: int, progress: int, track_cover: bytes, - font + font, + blur ): self.title = title self.artists = ", ".join(artists) if isinstance(artists, list) else artists @@ -68,6 +69,7 @@ class Banners: self.progress = progress self.track_cover = track_cover self.font_url = font + self.blur_intensity = blur def _get_font(self, size, font_bytes): return ImageFont.truetype(io.BytesIO(font_bytes), size) @@ -86,27 +88,18 @@ class Banners: def _prepare_background(self, w, h): bg = Image.open(io.BytesIO(self.track_cover)).convert("RGBA") - bg = bg.resize((w, h), Image.Resampling.BICUBIC) - bg = bg.filter(ImageFilter.GaussianBlur(radius=40)) - bg = ImageEnhance.Brightness(bg).enhance(0.4) + bg = bg.resize((w, h), Image.Resampling.LANCZOS) + bg = bg.filter(ImageFilter.GaussianBlur(radius=self.blur_intensity)) + bg = ImageEnhance.Brightness(bg).enhance(0.35) return bg - def _draw_progress_bar(self, draw, x, y, w, h, progress_pct, color="white", bg_color="#5e5e5e"): + def _draw_progress_bar(self, draw, x, y, w, h, progress_pct, color="white", bg_color="#6b6b6b"): draw.rounded_rectangle((x, y, x + w, y + h), radius=h/2, fill=bg_color) fill_w = int(w * progress_pct) if fill_w > 0: draw.rounded_rectangle((x, y, x + fill_w, y + h), radius=h/2, fill=color) - dot_radius = h * 1.2 - dot_x = x + fill_w - dot_y = y + (h / 2) - - draw.ellipse( - (dot_x - dot_radius, dot_y - dot_radius, dot_x + dot_radius, dot_y + dot_radius), - fill=color - ) - def horizontal(self): W, H = 1500, 600 padding = 60 @@ -127,18 +120,27 @@ class Banners: text_y_start = 100 text_width_limit = W - text_x - padding - display_title = self.title - while title_font.getlength(display_title) > text_width_limit and len(display_title) > 0: - display_title = display_title[:-1] - if len(display_title) < len(self.title): display_title += "…" + wrapper = textwrap.TextWrapper(width=23) + title_lines = wrapper.wrap(self.title) + + if len(title_lines) > 2: + title_lines = title_lines[:2] + title_lines[-1] += "..." + current_y = text_y_start + title_height = title_font.getbbox("Ah")[3] + 15 + + for line in title_lines: + draw.text((text_x, current_y), line, font=title_font, fill="white") + current_y += title_height + display_artist = self.artists while artist_font.getlength(display_artist) > text_width_limit and len(display_artist) > 0: display_artist = display_artist[:-1] if len(display_artist) < len(self.artists): display_artist += "…" - draw.text((text_x, text_y_start), display_title, font=title_font, fill="white") - draw.text((text_x, text_y_start + 70), display_artist, font=artist_font, fill="#B3B3B3") + artist_y = current_y + 10 + draw.text((text_x, artist_y), display_artist, font=artist_font, fill="#b3b3b3") cur_time = f"{(self.progress//1000//60):02}:{(self.progress//1000%60):02}" dur_time = f"{(self.duration//1000//60):02}:{(self.duration//1000%60):02}" @@ -188,36 +190,46 @@ class Banners: text_area_y = cover_y + cover_size + 120 text_width_limit = W - (padding * 2) - display_title = self.title - while title_font.getlength(display_title) > text_width_limit and len(display_title) > 0: - display_title = display_title[:-1] - if len(display_title) < len(self.title): display_title += "…" + wrapper = textwrap.TextWrapper(width=23) + title_lines = wrapper.wrap(self.title) + + if len(title_lines) > 2: + title_lines = title_lines[:2] + title_lines[-1] += "..." + + current_y = text_area_y + title_height = title_font.getbbox("Ah")[3] + 15 + + for line in title_lines: + w = title_font.getlength(line) + draw.text(((W - w) / 2, current_y), line, font=title_font, fill="white") + current_y += title_height display_artist = self.artists while artist_font.getlength(display_artist) > text_width_limit and len(display_artist) > 0: display_artist = display_artist[:-1] if len(display_artist) < len(self.artists): display_artist += "…" - title_w = title_font.getlength(display_title) - draw.text(((W - title_w) / 2, text_area_y), display_title, font=title_font, fill="white") - artist_w = artist_font.getlength(display_artist) - draw.text(((W - artist_w) / 2, text_area_y + 75), display_artist, font=artist_font, fill="#B3B3B3") + draw.text(((W - artist_w) / 2, current_y + 15), display_artist, font=artist_font, fill="#b3b3b3") bar_y = text_area_y + 260 + if len(title_lines) > 1: + bar_y += 60 + bar_h = 8 bar_w = W - (padding * 2) prog_pct = self.progress / self.duration if self.duration > 0 else 0 - self._draw_progress_bar(draw, padding, bar_y, bar_w, bar_h, prog_pct, color="white", bg_color="#5e5e5e") + self._draw_progress_bar(draw, padding, bar_y, bar_w, bar_h, prog_pct, color="white", bg_color="#6b6b6b") cur_time = f"{(self.progress//1000//60):02}:{(self.progress//1000%60):02}" dur_time = f"{(self.duration//1000//60):02}:{(self.duration//1000%60):02}" - draw.text((padding, bar_y + 40), cur_time, font=time_font, fill="#B3B3B3") + draw.text((padding, bar_y + 40), cur_time, font=time_font, fill="white") dur_w = time_font.getlength(dur_time) - draw.text((W - padding - dur_w, bar_y + 40), dur_time, font=time_font, fill="#B3B3B3") + draw.text((W - padding - dur_w, bar_y + 40), dur_time, font=time_font, fill="white") by = io.BytesIO() img.save(by, format="PNG") @@ -232,389 +244,277 @@ class SpotifyMod(loader.Module): strings = { "name": "SpotifyMod", "need_auth": ( - " Please execute" + " Please execute" " .sauth before performing this action." ), "on-repeat": ( - "🔄 Set on-repeat." + "🔄 Set on-repeat." ), "off-repeat": ( - "🔄 Stopped track" + "🔄 Stopped track" " repeat." ), "skipped": ( - "➡️ Skipped track." + "➡️ Skipped track." ), - "playing": "▶️ Playing...", + "playing": "▶️ Playing...", "back": ( - "⬅️ Switched to previous" + "⬅️ Switched to previous" " track" ), - "paused": " Pause", + "paused": " Pause", "restarted": ( - "✅️ Playing track" + "✅️ Playing track" " from the" " beginning" ), "liked": ( - "❤️ Liked current" + "❤️ Liked current" " playback" ), "unlike": ( - "" + "" " Unliked current playback" ), "err": ( - " An error occurred." + " An error occurred." "\n{}" ), "already_authed": ( - " Already authorized" + " Already authorized" ), "authed": ( - " Authentication" + " Authentication" " successful" ), "deauth": ( - "🚪 Successfully logged out" + "🚪 Successfully logged out" " of account" ), "auth": ( - '🔗 Follow this' + '🔗 Follow this' " link, allow access, then enter .scode https://... with" " the link you received." ), "no_music": ( - " No music is playing!" + " No music is playing!" ), "dl_err": ( - " Failed to download" + " Failed to download" " track." ), "volume_changed": ( - "🔊" + "🔊" " Volume changed to {}%." ), "volume_invalid": ( - " Volume level must be" + " Volume level must be" " a number between 0 and 100." ), "volume_err": ( - " An error occurred while" + " An error occurred while" " changing volume." ), "no_volume_arg": ( - " Please specify a" + " Please specify a" " volume level between 0 and 100." ), "searching_tracks": ( - "🕔 Searching for tracks" + "🕔 Searching for tracks" " matching {}..." ), "no_search_query": ( - " Please specify a" + " Please specify a" " search query." ), "no_tracks_found": ( - " No tracks found for" + " No tracks found for" " {}." ), "search_results": ( - " Search results for" + " Search results for" " {}:\n\n{}" ), + "search_results_inline": ( + " Found {count} results" + " for {query}.\nSelect a track:" + ), "downloading_search_track": ( - "🕔 Downloading {}..." + "🕔 Downloading {}..." ), "download_success": ( - " Successfully downloaded {} - {}" + " Successfully downloaded {} - {}" ), "invalid_track_number": ( - " Invalid track number." + " Invalid track number." " Please search first or provide a valid number from the list." ), "device_list": ( - "📄 Available devices:\n{}" + "📄 Available devices:\n{}" ), "no_devices_found": ( - " No devices found." + " No devices found." ), "device_changed": ( - " Playback transferred to" + " Playback transferred to" " {}." ), "invalid_device_id": ( - " Invalid device ID." + " Invalid device ID." " Use .sdevice to see available devices." ), - "search_results_cleared": " Search results cleared", "autobio": ( - "🎧 Spotify autobio {}" + "🎧 Spotify autobio {}" ), - "no_ytdlp": " yt-dlp not found... Check config or install yt-dlp ({}terminal pip install yt-dlp)", - "snowt_failed": "\n\n Download failed", - "uploading_banner": "\n\n🕔 Uploading banner...", - "downloading_track": "\n\n🕔 Downloading track...", - "no_playlists": " No playlists found.", - "playlists_list": "📄 Your playlists:\n\n{}", - "added_to_playlist": " Added {} to {}", - "removed_from_playlist": " Removed {} from {}", - "invalid_playlist_index": " Invalid playlist number.", - "no_cached_playlists": " Use .splaylists first.", - "playlist_created": " Playlist {} created.", - "playlist_deleted": " Playlist {} deleted.", - "no_playlist_name": " Please specify a playlist name.", + "no_ytdlp": " yt-dlp not found... Check config or install yt-dlp ({}terminal pip install yt-dlp)", + "snowt_failed": "\n\n Download failed", + "uploading_banner": "\n\n🕔 Uploading banner...", + "downloading_track": "\n\n🕔 Downloading track...", + "no_playlists": " No playlists found.", + "playlists_list": "📄 Your playlists:\n\n{}", + "added_to_playlist": " Added {} to {}", + "removed_from_playlist": " Removed {} from {}", + "invalid_playlist_index": " Invalid playlist number.", + "no_cached_playlists": " Use .splaylists first.", + "playlist_created": " Playlist {} created.", + "playlist_deleted": " Playlist {} deleted.", + "no_playlist_name": " Please specify a playlist name.", } strings_ru = { "_cls_doc": "Карточка с играющим треком в Spotify.", "need_auth": ( - " Выполни" + " Выполни" " .sauth перед выполнением этого действия." ), "err": ( - " Произошла ошибка." + " Произошла ошибка." "\n{}" ), "on-repeat": ( - "🔄 Включен повтор трека." + "🔄 Включен повтор трека." ), "off-repeat": ( - "🔄 Повтор трека отключён." + "🔄 Повтор трека отключён." ), "skipped": ( - "➡️ Трек пропущен." + "➡️ Трек пропущен." ), - "playing": "▶️ Играет...", + "playing": "▶️ Играет...", "back": ( - "⬅️ Переключено на предыдущий трек" + "⬅️ Переключено на предыдущий трек" ), - "paused": " Пауза", + "paused": " Пауза", "restarted": ( - "✅️ Воспроизведение трека с начала..." + "✅️ Воспроизведение трека с начала..." ), "liked": ( - "❤️ Текущий трек добавлен в избранное" + "❤️ Текущий трек добавлен в избранное" ), "unlike": ( - " Убрал лайк с текущего трека" + " Убрал лайк с текущего трека" ), "already_authed": ( - " Уже авторизован" + " Уже авторизован" ), "authed": ( - " Успешная аутентификация" + " Успешная аутентификация" ), "deauth": ( - "🚪 Успешный выход из аккаунта" + "🚪 Успешный выход из аккаунта" ), "auth": ( - '🔗 Пройдите по этой ссылке, разрешите вход, затем введите .scode https://... с ссылкой которую вы получили.' + '🔗 Пройдите по этой ссылке, разрешите вход, затем введите .scode https://... с ссылкой которую вы получили.' ), "no_music": ( - " Музыка не играет!" + " Музыка не играет!" ), "dl_err": ( - " Не удалось скачать трек." + " Не удалось скачать трек." ), "volume_changed": ( - "🔊" + "🔊" " Громкость изменена на {}%." ), "volume_invalid": ( - " Уровень громкости должен" + " Уровень громкости должен" " быть числом от 0 до 100." ), "volume_err": ( - " Произошла ошибка при" + " Произошла ошибка при" " изменении громкости." ), "no_volume_arg": ( - " Пожалуйста, укажите" + " Пожалуйста, укажите" " уровень громкости от 0 до 100." ), "searching_tracks": ( - "🕔 Идет поиск треков" + "🕔 Идет поиск треков" " по запросу {}..." ), "no_search_query": ( - " Пожалуйста, укажите" + " Пожалуйста, укажите" " поисковый запрос." ), "no_tracks_found": ( - " По запросу '{}'" + " По запросу '{}'" " ничего не найдено." ), "search_results": ( - " Результаты поиска" + " Результаты поиска" " по запросу {}:\n\n{}" ), + "search_results_inline": ( + " Найдено {count} результатов" + " по запросу {query}.\nВыберите трек:" + ), "downloading_search_track": ( - "🕔 Скачиваю {}..." + "🕔 Скачиваю {}..." ), "download_success": ( - " Трек {} - {} успешно скачан." + " Трек {} - {} успешно скачан." ), "invalid_track_number": ( - " Некорректный номер трека." + " Некорректный номер трека." " Сначала выполните поиск или укажите правильный номер из списка." ), "device_list": ( - "📄 Доступные устройства:\n{}" + "📄 Доступные устройства:\n{}" ), "no_devices_found": ( - " Устройства не найдены." + " Устройства не найдены." ), "device_changed": ( - " Воспроизведение переключено на" + " Воспроизведение переключено на" " {}." ), "invalid_device_id": ( - " Некорректный ID устройства." + " Некорректный ID устройства." " Используйте .sdevice , чтобы увидеть доступные устройства." ), - "search_results_cleared": " Результаты поиска очищены", "autobio": ( - "🎧 Обновление био" + "🎧 Обновление био" " включено {}" ), - "no_ytdlp": " yt-dlp не найден... Проверьте конфиг или установите yt-dlp ({}terminal pip install yt-dlp)", - "snowt_failed": "\n\n Ошибка скачивания.", - "uploading_banner": "\n\n🕔 Загрузка баннера...", - "downloading_track": "\n\n🕔 Скачивание трека...", - "no_playlists": " Плейлисты не найдены.", - "playlists_list": "📄 Ваши плейлисты:\n\n{}", - "added_to_playlist": " Трек {} добавлен в {}", - "removed_from_playlist": " Трек {} удален из {}", - "invalid_playlist_index": " Неверный номер плейлиста.", - "no_cached_playlists": " Сначала используйте .splaylists.", - "playlist_created": " Плейлист {} создан.", - "playlist_deleted": " Плейлист {} удален.", - "no_playlist_name": " Пожалуйста, укажите название плейлиста.", - } - strings_jp = { - "_cls_doc": "Spotify からのメッセージ", - "need_auth": ( - " この操作を行う前に " - ".sauth を実行してください。" - ), - "on-repeat": ( - "🔄 リピート再生を設定しました。" - ), - "off-repeat": ( - "🔄 リピート再生を解除しました。" - ), - "skipped": ( - "➡️ スキップしました。" - ), - "playing": "▶️ 再生中...", - "back": ( - "⬅️ 前のトラックに戻りました。" - ), - "paused": " 一時停止", - "restarted": ( - "✅️ 最初から再生します。" - ), - "liked": ( - "❤️ お気に入りに追加しました。" - ), - "unlike": ( - "" - " お気に入りから削除しました。" - ), - "err": ( - " エラーが発生しました。" - "\n{}" - ), - "already_authed": ( - " 既に認証されています。" - ), - "authed": ( - " 認証に成功しました。" - ), - "deauth": ( - "🚪 ログアウトしました。" - ), - "auth": ( - '🔗 リンクをクリックしてアクセスを許可し、取得したURLを使って .scode https://... を入力してください。' - ), - "no_music": ( - " 音楽は再生されていません!" - ), - "dl_err": ( - " トラックのダウンロードに失敗しました。" - ), - "volume_changed": ( - "🔊" - " 音量を {}% に変更しました。" - ), - "volume_invalid": ( - " 音量は0から100の数字で指定してください。" - ), - "volume_err": ( - " 音量の変更中にエラーが発生しました。" - ), - "no_volume_arg": ( - " 0から100の間で音量を指定してください。" - ), - "searching_tracks": ( - "🕔 {} を検索中..." - ), - "no_search_query": ( - " 検索キーワードを指定してください。" - ), - "no_tracks_found": ( - " {} は見つかりませんでした。" - ), - "search_results": ( - " {} の検索結果:\n\n{}" - ), - "downloading_search_track": ( - "🕔 {} をダウンロード中..." - ), - "download_success": ( - " {} - {} のダウンロードに成功しました。" - ), - "invalid_track_number": ( - " トラック番号が無効です。" - " 先に検索するか、リストから有効な番号を指定してください。" - ), - "device_list": ( - "📄 利用可能なデバイス:\n{}" - ), - "no_devices_found": ( - " デバイスが見つかりません。" - ), - "device_changed": ( - " 再生デバイスを" - " {} に切り替えました。" - ), - "invalid_device_id": ( - " デバイスIDが無効です。" - " .sdevice で利用可能なデバイスを確認してください。" - ), - "search_results_cleared": " 検索結果をクリアしました。", - "autobio": ( - "🎧 Spotify AutoBio: {}" - ), - "no_ytdlp": " yt-dlpが見つかりません... 設定を確認するか、インストールしてください ({}terminal pip install yt-dlp)", - "snowt_failed": "\n\n ダウンロードに失敗しました。", - "uploading_banner": "\n\n🕔 バナーをアップロード中...", - "downloading_track": "\n\n🕔 トラックをダウンロード中...", - "no_playlists": " プレイリストが見つかりません。", - "playlists_list": "📄 あなたのプレイリスト:\n\n{}", - "added_to_playlist": " {} を {} に追加しました。", - "removed_from_playlist": " {} を {} から削除しました。", - "invalid_playlist_index": " プレイリスト番号が無効です。", - "no_cached_playlists": " 先に .splaylists を使用してください。", - "playlist_created": " プレイリスト {} を作成しました。", - "playlist_deleted": " プレイリスト {} を削除しました。", - "no_playlist_name": " プレイリスト名を指定してください。", + "no_ytdlp": " yt-dlp не найден... Проверьте конфиг или установите yt-dlp ({}terminal pip install yt-dlp)", + "snowt_failed": "\n\n Ошибка скачивания.", + "uploading_banner": "\n\n🕔 Загрузка баннера...", + "downloading_track": "\n\n🕔 Скачивание трека...", + "no_playlists": " Плейлисты не найдены.", + "playlists_list": "📄 Ваши плейлисты:\n\n{}", + "added_to_playlist": " Трек {} добавлен в {}", + "removed_from_playlist": " Трек {} удален из {}", + "invalid_playlist_index": " Неверный номер плейлиста.", + "no_cached_playlists": " Сначала используйте .splaylists.", + "playlist_created": " Плейлист {} создан.", + "playlist_deleted": " Плейлист {} удален.", + "no_playlist_name": " Пожалуйста, укажите название плейлиста.", } def __init__(self): self._client_id = "e0708753ab60499c89ce263de9b4f57a" self._client_secret = "80c927166c664ee98a43a2c0e2981b4a" + self.sp = None self.scope = ( "user-read-playback-state playlist-read-private playlist-read-collaborative" " user-modify-playback-state user-library-modify" @@ -636,10 +536,10 @@ class SpotifyMod(loader.Module): loader.ConfigValue( "custom_text", ( - "🎧 Now playing: {track} — {artists}\n" - "🔗 song.link" + "🎧 Now playing: {track} — {artists}\n" + "🔗 song.link" ), - """Custom text, supports {track}, {artists}, {album}, {playlist}, {playlist_owner}, {spotify_url}, {songlink}, {progress}, {duration}, {device} placeholders""", + "Custom text, supports {track}, {artists}, {album}, {playlist}, {playlist_owner}, {spotify_url}, {songlink}, {progress}, {duration}, {device} placeholders." + "\n\n" + "ℹ️ Custom placeholders: {}".format(utils.config_placeholders()), validator=loader.validators.String(), ), loader.ConfigValue( @@ -650,8 +550,8 @@ class SpotifyMod(loader.Module): ), loader.ConfigValue( "auto_bio_template", - "🎧 {}", - lambda: "Template for Spotify AutoBio", + "🎧 {title} - {artist}", + lambda: "Template for Spotify AutoBio, supports {artist}, {title}", ), loader.ConfigValue( "ytdlp_path", @@ -659,26 +559,53 @@ class SpotifyMod(loader.Module): "Path to ytdlp binary", validator=loader.validators.String(), ), + loader.ConfigValue( + "cookies_path", + "", + "Path to your cookies for yt-dlp", + validator=loader.validators.String(), + ), loader.ConfigValue( "banner_version", "horizontal", lambda: "Banner version", validator=loader.validators.Choice(["horizontal", "vertical"]), ), + loader.ConfigValue( + "blur_intensity", + 40, + lambda: "Blur intensity", + validator=loader.validators.Integer(minimum=0), + ), ) + self._sp_store = {} + + def _init_spotify_client(self) -> bool: + token = self.get("acs_tkn") or {} + access_token = token.get("access_token") + if not access_token: + self.sp = None + return False + + try: + self.sp = spotipy.Spotify(auth=access_token) + except Exception: + self.sp = None + return False + + return True async def client_ready(self, client, db): self.font_ready = asyncio.Event() self._premium = getattr(await client.get_me(), "premium", False) - try: - self.sp = spotipy.Spotify(auth=self.get("acs_tkn")["access_token"]) - except Exception: + if not self._init_spotify_client(): self.set("acs_tkn", None) - self.sp = None - if self.get("autobio", False): - self.autobio.start() + self.bio_task = None + + if self.get("autobio", False) and self.sp: + await self.autobio() def tokenized(func) -> FunctionType: @functools.wraps(func) @@ -699,12 +626,23 @@ class SpotifyMod(loader.Module): async def wrapped(*args, **kwargs): try: return await func(*args, **kwargs) - except Exception: - logger.exception(traceback.format_exc()) + except Exception as e: + error_msg = str(e) + logger.error(f"Error in {func.__name__}: {error_msg}") + + if "NO_ACTIVE_DEVICE" in error_msg: + user_error = "No active device" + elif "PREMIUM_REQUIRED" in error_msg: + user_error = "Spotify Premium is required for this feature" + elif "Insufficient client scope" in error_msg: + user_error = "Insufficient permissions. Please re-authenticate." + else: + user_error = f"{type(e).__name__}: {error_msg[:50]}" + with contextlib.suppress(Exception): await utils.answer( args[1], - args[0].strings("err").format(traceback.format_exc()), + args[0].strings("err").format(user_error), ) wrapped.__doc__ = func.__doc__ @@ -713,81 +651,409 @@ class SpotifyMod(loader.Module): return wrapped - @loader.loop(interval=90) async def autobio(self): - try: - current_playback = self.sp.current_playback() - track = current_playback["item"]["name"] - track = re.sub(r"([(].*?[)])", "", track).strip() - except Exception: - return - - bio = self.config["auto_bio_template"].format(f"{track}") - - try: - await self._client( - UpdateProfileRequest(about=bio[: 140 if self._premium else 70]) - ) - except FloodWaitError as e: - logger.info(f"Sleeping {max(e.seconds, 60)} bc of floodwait") - await asyncio.sleep(max(e.seconds, 60)) - return + if getattr(self, "bio_task", None) and not self.bio_task.done(): + self.bio_task.cancel() - async def _download_track(self, message, query: str, caption: str = ""): + async def _loop(): + while self.get("autobio", False): + try: + if not self.sp and not self._init_spotify_client(): + self.set("autobio", False) + await self._restore_original_bio() + break + + current_playback = await utils.run_sync(self.sp.current_playback) + + if not current_playback or not current_playback.get("is_playing"): + if self.get("last_bio", ""): + await self._restore_original_bio(clear_original=False) + await asyncio.sleep(10) + continue + + item = current_playback.get("item") or {} + title = item.get("name") or "" + artists = ", ".join( + [a.get("name", "") for a in item.get("artists", []) if a.get("name")] + ) + + if not title: + await asyncio.sleep(10) + continue + + bio = self.config["auto_bio_template"].format( + title=title, + artist=artists or "Unknown Artist", + ).strip() + + if len(bio) > 70: + bio = bio[:69] + "…" + + if bio != self.get("last_bio", ""): + await self._client(UpdateProfileRequest(about=bio)) + self.set("last_bio", bio) + + except FloodWaitError as e: + await asyncio.sleep(getattr(e, "seconds", 30) + 1) + except asyncio.CancelledError: + break + except Exception as e: + logger.exception("autobio error: %s", e) + + await asyncio.sleep(self.config.get("BIO_UPDATE_DELAY", 30)) + + self.bio_task = asyncio.create_task(_loop()) + + async def _get_current_about(self) -> str: + full_user = await self._client(GetFullUserRequest("me")) + return getattr(full_user.full_user, "about", "") or "" + + async def _restore_original_bio( + self, + *, + clear_original: bool = True, + clear_last: bool = True, + ): + original_bio = self.get("original_bio", None) + if original_bio is None: + return + + await self._client(UpdateProfileRequest(about=original_bio)) + if clear_original: + self.set("original_bio", None) + if clear_last: + self.set("last_bio", "") + + def _get_chat_id(self, target): + if isinstance(target, int): + return target + if not target: + return None + chat_id = getattr(target, "chat_id", None) + if chat_id: + return chat_id + with contextlib.suppress(Exception): + return utils.get_chat_id(target) + return None + + def _reply_id(self, message): + reply_to_id = getattr(message, "reply_to_msg_id", None) + if reply_to_id: + return reply_to_id + reply_to = getattr(message, "reply_to", None) + return getattr(reply_to, "reply_to_msg_id", None) if reply_to else None + + async def _download_track( + self, + target, + query, + caption=None, + track_name=None, + artists=None, + log_context=None, + reply_to_id=None, + ) -> bool: dl_dir = os.path.join(os.getcwd(), "spotifymod") if not os.path.exists(dl_dir): os.makedirs(dl_dir, exist_ok=True) - + for f in os.listdir(dl_dir): try: os.remove(os.path.join(dl_dir, f)) - except: + except Exception: pass + success = False + if caption is None: + safe_track = utils.escape_html(track_name or "Unknown") + safe_artists = utils.escape_html(artists or "Unknown Artist") + caption = self.strings("download_success").format(safe_track, safe_artists) + + async def send_text(text: str) -> bool: + if target is None: + return False + if isinstance(target, int): + await self._client.send_message(target, text, reply_to=reply_to_id) + return True + try: + await utils.answer(target, text) + return True + except Exception: + chat_id = self._get_chat_id(target) + if chat_id is None: + return False + await self._client.send_message(chat_id, text, reply_to=reply_to_id) + return True + + async def send_file(file_path: str) -> bool: + if target is None: + return False + if isinstance(target, int): + await self._client.send_file( + target, + file_path, + caption=caption, + reply_to=reply_to_id, + ) + return True + try: + await utils.answer(target, caption, file=file_path) + return True + except Exception: + chat_id = self._get_chat_id(target) + if chat_id is None: + return False + await self._client.send_file( + chat_id, + file_path, + caption=caption, + reply_to=reply_to_id, + ) + return True + try: squery = query.replace('"', '').replace("'", "") - cmd = ( - f'{self.config["ytdlp_path"]} -x --audio-format mp3 --add-metadata ' - f'-o "{dl_dir}/%(title)s [%(id)s].%(ext)s" ' - f'"ytsearch1:{squery}"' - ) + cookies = self.config["cookies_path"] + + if cookies: + cmd = ( + f'{self.config["ytdlp_path"]} -x --impersonate="" --cookies {cookies} --audio-format mp3 --add-metadata ' + f'--audio-quality 0 -o "{dl_dir}/%(title)s [%(id)s].%(ext)s" ' + f'"ytsearch1:{squery}"' + ) + else: + cmd = ( + f'{self.config["ytdlp_path"]} -x --impersonate="" --audio-format mp3 --add-metadata ' + f'--audio-quality 0 -o "{dl_dir}/%(title)s [%(id)s].%(ext)s" ' + f'"ytsearch1:{squery}"' + ) proc = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) - await proc.communicate() + _, stderr = await proc.communicate() + if proc.returncode and log_context: + err_text = stderr.decode(errors="ignore").strip() if stderr else "" + err_text = err_text[-400:] if err_text else "yt-dlp failed" + logger.error("Search download failed (%s): %s", log_context, err_text) files = [f for f in os.listdir(dl_dir) if f.endswith(".mp3")] - + if files: - target_file = os.path.join(dl_dir, files[0]) - await utils.answer(message, caption, file=target_file) + first = files[0] + target_file = os.path.join(dl_dir, first) + success = await send_file(target_file) + if not success: + if log_context: + logger.error( + "Search download send failed (%s). target=%s chat_id=%s", + log_context, + type(target).__name__, + self._get_chat_id(target), + ) + await send_text(self.strings("dl_err")) else: - await utils.answer(message, self.strings("snowt_failed")) + if log_context: + logger.error("Search download produced no files (%s)", log_context) + await send_text(self.strings("snowt_failed")) except Exception as e: - logger.error(e) - await utils.answer(message, self.strings("dl_err")) - + if log_context: + logger.exception("Search download error (%s)", log_context) + else: + logger.error(e) + await send_text(self.strings("dl_err")) + finally: if os.path.exists(dl_dir): for f in os.listdir(dl_dir): try: os.remove(os.path.join(dl_dir, f)) - except: + except Exception: pass + return success + def _short_text(self, text: str, limit: int = 60) -> str: + text = " ".join(text.split()) + if len(text) <= limit: + return text + if limit <= 3: + return text[:limit] + return text[: limit - 3] + "..." + + + + def _track_info(self, track_info) -> tuple: + if isinstance(track_info, dict): + track_name = track_info.get("name", "Unknown") + artists_list = [ + a.get("name") for a in track_info.get("artists", []) if a.get("name") + ] + artists = ", ".join(artists_list) if artists_list else "Unknown Artist" + return track_name, artists + + if isinstance(track_info, (list, tuple)): + track_name = track_info[0] if len(track_info) > 0 else "Unknown" + artists = track_info[1] if len(track_info) > 1 else "Unknown Artist" + if not artists: + artists = "Unknown Artist" + return track_name or "Unknown", artists + + return "Unknown", "Unknown Artist" + + def _search_keyboard(self, tracks: list, chat_id=None, reply_to_id=None) -> list: + keyboard = [] + for track in tracks: + track_name, artists = self._track_info(track) + label = f"{track_name} — {artists}" if artists else track_name + keyboard.append( + [ + { + "text": self._short_text(label), + "callback": self._inline_download_track, + "args": (track_name, artists, reply_to_id, chat_id), + } + ] + ) + + return keyboard + + async def _inline_download_track( + self, + call, + track_name: str, + artists: str, + reply_to_id=None, + chat_id=None, + ): + track_name = track_name or "Unknown" + artists = artists or "Unknown Artist" + + with contextlib.suppress(Exception): + await call.answer() + + with contextlib.suppress(Exception): + await call.edit(self.strings("downloading_track").lstrip(), reply_markup=None) + + target_message = getattr(call, "message", None) + if reply_to_id is None: + reply_to_id = self._reply_id(target_message) + + if chat_id is None: + chat_id = self._get_chat_id(target_message) + if chat_id is None: + chat_id = getattr(call, "chat_id", None) + if chat_id is None: + chat_id = self._get_chat_id(call) + + if chat_id is None and target_message is None: + logger.error("Inline download missing chat_id (%s - %s)", track_name, artists) + with contextlib.suppress(Exception): + await call.edit(self.strings("dl_err"), reply_markup=None) + return + + target = chat_id if chat_id is not None else target_message + + success = await self._download_track( + target, + f"{artists} {track_name}", + track_name=track_name, + artists=artists, + log_context=f"{track_name} - {artists}", + reply_to_id=reply_to_id, + ) + + if success: + with contextlib.suppress(Exception): + await call.delete() + else: + with contextlib.suppress(Exception): + await call.edit(self.strings("dl_err"), reply_markup=None) + + async def _inline_search_tracks(self, query): + if not self.get("acs_tkn", False) or not self.sp: + return { + "title": "Auth required", + "description": "Run .sauth", + "message": self.strings("need_auth"), + } + + query_text = (query.args or "").strip() + if not query_text: + return { + "title": "No query", + "description": "Provide search query", + "message": self.strings("no_search_query"), + } + + try: + results = await asyncio.to_thread( + self.sp.search, + q=query_text, + limit=5, + type="track", + ) + except Exception as e: + return { + "title": "Search error", + "description": "Try again", + "message": self.strings("err").format( + utils.escape_html(str(e)[:50]) + ), + } + + if not results or not results["tracks"]["items"]: + return { + "title": "No results", + "description": self._short_text(query_text, limit=60), + "message": self.strings("no_tracks_found").format( + utils.escape_html(query_text) + ), + } + + tracks = results["tracks"]["items"] + store_id = id(tracks) + self._sp_store[store_id] = [(t.get("name", "Unknown"), ", ".join(a.get("name", "") for a in t.get("artists", []) if a.get("name")) or "Unknown Artist") for t in tracks] + + entries = [] + for i, track in enumerate(tracks): + track_name, artists = self._track_info(track) + cover_list = track.get("album", {}).get("images", []) + thumb = cover_list[0]["url"] if cover_list else None + + entries.append( + { + "title": self._short_text(track_name, limit=60), + "description": self._short_text(artists, limit=60) if artists else "", + "message": f"{self.strings('downloading_track').lstrip()}\nspdl_{store_id}_{i}", + "thumb": thumb, + } + ) + + return entries + + @loader.inline_handler(ru_doc="<запрос> - поиск треков Spotify.") + async def sq(self, query): + """ - search Spotify track""" + return await self._inline_search_tracks(query) + + @loader.inline_handler(ru_doc="<запрос> - поиск треков Spotify.") + async def ssearch(self, query): + """ - search Spotify track""" + return await self._inline_search_tracks(query) + @error_handler @tokenized @loader.command( - ru_doc="- ➕ Добавить текущий трек в плейлист (используйте номер из .splaylists)" + ru_doc="| .spla - ➕ Добавить текущий трек в плейлист (используйте номер из .splaylists | .spls)", + alias="spla" ) async def splaylistadd(self, message: Message): - """- ➕ Add current track to playlist (use number from .splaylists)""" + """| .spla - ➕ Add current track to playlist (use number from .splaylists | .spls)""" args = utils.get_args_raw(message) if not args or not args.isdigit(): await utils.answer(message, self.strings("invalid_playlist_index")) @@ -799,7 +1065,6 @@ class SpotifyMod(loader.Module): if not playlists: await utils.answer(message, self.strings("no_cached_playlists")) return - if index < 0 or index >= len(playlists): await utils.answer(message, self.strings("invalid_playlist_index")) return @@ -817,23 +1082,17 @@ class SpotifyMod(loader.Module): playlist_id = playlists[index]["id"] playlist_name = playlists[index]["name"] - try: - self.sp.playlist_add_items(playlist_id, [track_uri]) - except spotipy.exceptions.SpotifyException as e: - if e.http_status == 403 and "Insufficient client scope" in str(e): - await utils.answer(message, self.strings("need_auth")) - return - raise e - + self.sp.playlist_add_items(playlist_id, [track_uri]) await utils.answer(message, self.strings("added_to_playlist").format(utils.escape_html(full_track_name), utils.escape_html(playlist_name))) @error_handler @tokenized @loader.command( - ru_doc="- ➖ Удалить текущий трек из плейлиста (используйте номер из .splaylists)" + ru_doc="| .splr - ➖ Удалить текущий трек из плейлиста (используйте номер из .splaylists | .spls)", + alias="splr" ) async def splaylistrem(self, message: Message): - """- ➖ Remove current track from playlist (use number from .splaylists)""" + """| .splr - ➖ Remove current track from playlist (use number from .splaylists | .spls)""" args = utils.get_args_raw(message) if not args or not args.isdigit(): await utils.answer(message, self.strings("invalid_playlist_index")) @@ -845,7 +1104,6 @@ class SpotifyMod(loader.Module): if not playlists: await utils.answer(message, self.strings("no_cached_playlists")) return - if index < 0 or index >= len(playlists): await utils.answer(message, self.strings("invalid_playlist_index")) return @@ -863,23 +1121,17 @@ class SpotifyMod(loader.Module): playlist_id = playlists[index]["id"] playlist_name = playlists[index]["name"] - try: - self.sp.playlist_remove_all_occurrences_of_items(playlist_id, [track_uri]) - except spotipy.exceptions.SpotifyException as e: - if e.http_status == 403 and "Insufficient client scope" in str(e): - await utils.answer(message, self.strings("need_auth")) - return - raise e - + self.sp.playlist_remove_all_occurrences_of_items(playlist_id, [track_uri]) await utils.answer(message, self.strings("removed_from_playlist").format(utils.escape_html(full_track_name), utils.escape_html(playlist_name))) @error_handler @tokenized @loader.command( - ru_doc="- 🆕 Создать новый плейлист" + ru_doc="| .splc - 🆕 Создать новый плейлист", + alias="splc" ) async def splaylistcreate(self, message: Message): - """- 🆕 Create a new playlist""" + """| .splc - 🆕 Create a new playlist""" name = utils.get_args_raw(message) if not name: await utils.answer(message, self.strings("no_playlist_name")) @@ -892,10 +1144,11 @@ class SpotifyMod(loader.Module): @error_handler @tokenized @loader.command( - ru_doc="- 🗑 Удалить плейлист (используйте номер из .splaylists)" + ru_doc="| .spld - 🗑 Удалить плейлист (используйте номер из .splaylists | .spls)", + alias="spld" ) async def splaylistdelete(self, message: Message): - """- 🗑 Delete playlist (use number from .splaylists)""" + """| .spld - 🗑 Delete playlist (use number from .splaylists | .spls)""" args = utils.get_args_raw(message) if not args or not args.isdigit(): await utils.answer(message, self.strings("invalid_playlist_index")) @@ -907,7 +1160,6 @@ class SpotifyMod(loader.Module): if not playlists: await utils.answer(message, self.strings("no_cached_playlists")) return - if index < 0 or index >= len(playlists): await utils.answer(message, self.strings("invalid_playlist_index")) return @@ -921,17 +1173,18 @@ class SpotifyMod(loader.Module): @error_handler @tokenized @loader.command( - ru_doc="- 📃 Получить все плейлисты" + ru_doc="| .spls - 📃 Получить все плейлисты", + alias="spls" ) async def splaylists(self, message: Message): - """- 📃 Get all playlists""" + """| .spls - 📃 Get all playlists""" user_id = self.sp.me()["id"] playlists = self.sp.current_user_playlists() - editable_playlists = [] - for playlist in playlists["items"]: - if playlist["owner"]["id"] == user_id or playlist["collaborative"]: - editable_playlists.append(playlist) + editable_playlists = [ + p for p in playlists["items"] + if p["owner"]["id"] == user_id or p["collaborative"] + ] self.set("last_playlists", editable_playlists) @@ -942,7 +1195,7 @@ class SpotifyMod(loader.Module): count = playlist["tracks"]["total"] playlist_list_text += f"{i + 1}. {name} ({count} tracks)\n" - if not playlist_list_text: + if playlist_list_text == "": await utils.answer(message, self.strings("no_playlists")) else: await utils.answer(message, self.strings("playlists_list").format(playlist_list_text)) @@ -952,94 +1205,99 @@ class SpotifyMod(loader.Module): @loader.command( ru_doc="- ℹ️ Переключить стриминг воспроизведения в био" ) - async def sbiocmd(self, message: Message): - """- ℹ️ Toggle bio playback streaming""" - current = self.get("autobio", False) - new = not current - self.set("autobio", new) + async def sbiocmd(self, message): + """- ℹ️ Toggle streaming playback in bio""" + if not getattr(self, "sp", None): + await utils.answer(message, self.strings("need_auth")) + return + + state = not self.get("autobio", False) + self.set("autobio", state) + + if state: + self.set("original_bio", await self._get_current_about()) + self.set("last_bio", "") + await self.autobio() + else: + task = getattr(self, "bio_task", None) + if task and not task.done(): + task.cancel() + self.bio_task = None + await self._restore_original_bio() + await utils.answer( message, - self.strings("autobio").format("enabled" if new else "disabled"), + self.strings("autobio").format("on" if state else "off"), ) - if new: - self.autobio.start() - else: - self.autobio.stop() - @error_handler @tokenized @loader.command( - ru_doc="- 🔊 Изменить громкость. .svolume <0-100>" + ru_doc="| .sv - 🔊 Изменить громкость. .svolume | .sv <0-100>", + alias="sv" ) async def svolume(self, message: Message): - """- 🔊 Change playback volume. .svolume <0-100>""" - try: - args = utils.get_args_raw(message) - if not args: - await utils.answer(message, self.strings("no_volume_arg")) - return - - volume_percent = int(args) - if 0 <= volume_percent <= 100: - self.sp.volume(volume_percent) - await utils.answer(message, self.strings("volume_changed").format(volume_percent)) - else: + """| .sv - 🔊 Change playback volume. .svolume | .sv <0-100>""" + args = utils.get_args_raw(message) + if args == "": + await utils.answer(message, self.strings("no_volume_arg")) + else: + try: + volume_percent = int(args) + if 0 <= volume_percent <= 100: + self.sp.volume(volume_percent) + await utils.answer(message, self.strings("volume_changed").format(volume_percent)) + else: + await utils.answer(message, self.strings("volume_invalid")) + except ValueError: await utils.answer(message, self.strings("volume_invalid")) - except ValueError: - await utils.answer(message, self.strings("volume_invalid")) - except Exception: - await utils.answer(message, self.strings("volume_err")) @error_handler @tokenized @loader.command( ru_doc=( - "- 🎵 Выбрать устройство для воспроизведения. Например: .sdevice \n" - "- 📝 Показать список устройств: .sdevice" - ) + "| .sd - 🎵 Выбрать устройство для воспроизведения. Например: .sdevice или .sdevice | .sd для вывода списка устройств" + ), + alias="sd" ) async def sdevicecmd(self, message: Message): - """- 🎵 Set preferred playback device. Usage: .sdevice or .sdevice to list devices""" + """| .sd - 🎵 Set preferred playback device. Usage: .sdevice or .sdevice | .sd to list devices""" args = utils.get_args_raw(message) devices = self.sp.devices()["devices"] - if not args: + if args == "": if not devices: await utils.answer(message, self.strings("no_devices_found")) - return - - device_list_text = "" - for i, device in enumerate(devices): - is_active = "(active)" if device["is_active"] else "" - device_list_text += ( - f"{i+1}. {device['name']}" - f" ({device['type']}) {is_active}\n" - ) - - await utils.answer(message, self.strings("device_list").format(device_list_text.strip())) - return - - device_id = None - try: - device_number = int(args) - if 0 < device_number <= len(devices): - device_id = devices[device_number - 1]["id"] - device_name = devices[device_number - 1]["name"] else: - await utils.answer(message, self.strings("invalid_device_id")) - return - except ValueError: - found_device = next((d for d in devices if d["id"] == args.strip()), None) - if found_device: - device_id = found_device["id"] - device_name = found_device["name"] - else: - await utils.answer(message, self.strings("invalid_device_id")) - return + device_list_text = "" + for i, device in enumerate(devices): + is_active = "(active)" if device["is_active"] else "" + device_list_text += ( + f"{i+1}. {device['name']}" + f" ({device['type']}) {is_active}\n" + ) + await utils.answer(message, self.strings("device_list").format(device_list_text.strip())) + else: + device_id = None + try: + device_number = int(args) + if 0 < device_number <= len(devices): + device_id = devices[device_number - 1]["id"] + device_name = devices[device_number - 1]["name"] + else: + await utils.answer(message, self.strings("invalid_device_id")) + return + except ValueError: + found_device = next((d for d in devices if d["id"] == args.strip()), None) + if found_device: + device_id = found_device["id"] + device_name = found_device["name"] + else: + await utils.answer(message, self.strings("invalid_device_id")) + return - self.sp.transfer_playback(device_id=device_id) - await utils.answer(message, self.strings("device_changed").format(device_name)) + self.sp.transfer_playback(device_id=device_id) + await utils.answer(message, self.strings("device_changed").format(device_name)) @error_handler @tokenized @@ -1157,7 +1415,7 @@ class SpotifyMod(loader.Module): url = message.message.split(" ")[1] code = self.sp_auth.parse_auth_response_url(url) self.set("acs_tkn", self.sp_auth.get_access_token(code, True, False)) - self.sp = spotipy.Spotify(auth=self.get("acs_tkn")["access_token"]) + self._init_spotify_client() await utils.answer(message, self.strings("authed")) @error_handler @@ -1167,31 +1425,33 @@ class SpotifyMod(loader.Module): async def unauthcmd(self, message: Message): """- Log out of account""" self.set("acs_tkn", None) - del self.sp + self.sp = None await utils.answer(message, self.strings("deauth")) @error_handler @tokenized @loader.command( - ru_doc="- Обновить токен авторизации" + ru_doc="| .stokr - Обновить токен авторизации", + alias="stokr" ) async def stokrefreshcmd(self, message: Message): - """- Refresh authorization token""" + """| .stokr - Refresh authorization token""" self.set( "acs_tkn", self.sp_auth.refresh_access_token(self.get("acs_tkn")["refresh_token"]), ) self.set("NextRefresh", time.time() + 45 * 60) - self.sp = spotipy.Spotify(auth=self.get("acs_tkn")["access_token"]) + self._init_spotify_client() await utils.answer(message, self.strings("authed")) @error_handler @tokenized @loader.command( - ru_doc="- 🎧 Показать карточку играющего трека" + ru_doc="| .sn - 🎧 Показать карточку играющего трека", + alias="sn" ) async def snowcmd(self, message: Message): - """- 🎧 View current track card.""" + """| .sn - 🎧 View current track card.""" current_playback = self.sp.current_playback() if not current_playback or not current_playback.get("is_playing", False): await utils.answer(message, self.strings("no_music")) @@ -1235,18 +1495,22 @@ class SpotifyMod(loader.Module): playlist_name = "" playlist_owner = "" - text = self.config["custom_text"].format( - track=utils.escape_html(track), - artists=utils.escape_html(artists), - album=utils.escape_html(album_name), - duration=duration, - progress=progress, - device=device, - spotify_url=spotify_url, - songlink=songlink, - playlist=utils.escape_html(playlist_name) if playlist_name else "", - playlist_owner=playlist_owner or "", - ) + sdata = { + "track": utils.escape_html(track), + "artists": utils.escape_html(artists), + "album": utils.escape_html(album_name), + "duration": duration, + "progress": progress, + "device": device, + "spotify_url": spotify_url, + "songlink": songlink, + "playlist": utils.escape_html(playlist_name) if playlist_name else "", + "playlist_owner": playlist_owner or "", + } + + data = await utils.get_placeholders(sdata, self.config["custom_text"]) + + text = self.config["custom_text"].format(**data) if self.config["show_banner"]: cover_url = current_playback["item"]["album"]["images"][0]["url"] @@ -1260,8 +1524,13 @@ class SpotifyMod(loader.Module): progress=progress_ms, track_cover=requests.get(cover_url).content, font=self.config["font"], + blur=self.config["blur_intensity"], ) - file = getattr(banners, self.config["banner_version"], banners.horizontal)() + + if self.config["banner_version"] == "vertical": + file = banners.vertical() + else: + file = banners.horizontal() await utils.answer(tmp_msg, text, file=file) else: @@ -1270,10 +1539,11 @@ class SpotifyMod(loader.Module): @error_handler @tokenized @loader.command( - ru_doc="- 🎧 Скачать играющий трек" + ru_doc="| .snt - 🎧 Скачать играющий трек", + alias="snt" ) async def snowtcmd(self, message: Message): - """- 🎧 Download current track.""" + """| .snt - 🎧 Download current track.""" current_playback = self.sp.current_playback() if not current_playback or not current_playback.get("is_playing", False): await utils.answer(message, self.strings("no_music")) @@ -1316,18 +1586,22 @@ class SpotifyMod(loader.Module): playlist_name = "" playlist_owner = "" - text = self.config["custom_text"].format( - track=utils.escape_html(track), - artists=utils.escape_html(artists), - album=utils.escape_html(album_name), - duration=duration, - progress=progress, - device=device, - spotify_url=spotify_url, - songlink=songlink, - playlist=utils.escape_html(playlist_name) if playlist_name else "", - playlist_owner=playlist_owner or "", - ) + sdata = { + "track": utils.escape_html(track), + "artists": utils.escape_html(artists), + "album": utils.escape_html(album_name), + "duration": duration, + "progress": progress, + "device": device, + "spotify_url": spotify_url, + "songlink": songlink, + "playlist": utils.escape_html(playlist_name) if playlist_name else "", + "playlist_owner": playlist_owner or "", + } + + data = await utils.get_placeholders(sdata, self.config["custom_text"]) + + text = self.config["custom_text"].format(**data) msg = await utils.answer(message, text + self.strings("downloading_track")) @@ -1336,101 +1610,124 @@ class SpotifyMod(loader.Module): @error_handler @tokenized @loader.command( - ru_doc=( - "- 🔍 Поиск треков. Например: .ssearch Imagine Dragons Believer\n" - "- 🎧 Скачать трек: .ssearch 1 (где 1 — номер трека из списка)" - ) + ru_doc="| .sq - 🔍 Поиск треков.", + alias="sq" ) async def ssearchcmd(self, message: Message): - """🔍 Search for tracks. Usage: .ssearch or .ssearch to download""" + """| .sq - 🔍 Search for tracks.""" args = utils.get_args_raw(message) if not args: await utils.answer(message, self.strings("no_search_query")) return - try: + search_results = self.get("last_search_results", []) + + is_selection = False + if args.isdigit(): + track_number = int(args) + if search_results and 0 < track_number <= len(search_results): + is_selection = True + + if is_selection: track_number = int(args) - search_results = self.get("last_search_results", []) - - if not search_results: - await utils.answer(message, self.strings("no_tracks_found")) - return - - if track_number <= 0 or track_number > len(search_results): - raise ValueError - msg = await utils.answer(message, self.strings("downloading_track")) - track_info = search_results[track_number - 1] - track_name = track_info["name"] - artists = ", ".join([a["name"] for a in track_info["artists"]]) - - caption_text = self.strings("download_success").format( - utils.escape_html(track_name), - utils.escape_html(artists) + track_name, artists = self._track_info(track_info) + reply_to_id = self._reply_id(message) + + chat_id = self._get_chat_id(message) + target = chat_id if chat_id is not None else msg + success = await self._download_track( + target, + f"{artists} {track_name}", + track_name=track_name, + artists=artists, + log_context=f"{track_name} - {artists}", + reply_to_id=reply_to_id, + ) + if success: + with contextlib.suppress(Exception): + await msg.delete() + self.set("last_search_results", []) + + else: + results = await asyncio.to_thread( + self.sp.search, + q=args, + limit=5, + type="track", ) - - await self._download_track(msg, f"{artists} {track_name}", caption=caption_text) - return - - except ValueError: - await utils.answer(message, self.strings("searching_tracks").format(args)) - - results = self.sp.search(q=args, limit=5, type="track") if not results or not results["tracks"]["items"]: await utils.answer(message, self.strings("no_tracks_found").format(args)) return - self.set("last_search_results", results["tracks"]["items"]) - - tracks_list = [] - for i, track in enumerate(results["tracks"]["items"]): - track_name = track["name"] - artists = ", ".join([artist["name"] for artist in track["artists"]]) - track_url = track["external_urls"]["spotify"] - tracks_list.append( - "{number}. {track_name} — {artists}\n🔗 Spotify".format( - number=i + 1, - track_name=utils.escape_html(track_name), - artists=utils.escape_html(artists), - track_url=track_url, - ) - ) + tracks = results["tracks"]["items"] + self.set("last_search_results", tracks) - text = "\n".join(tracks_list) - await utils.answer(message, self.strings("search_results").format(args, text)) + reply_to_id = self._reply_id(message) - - @loader.command( - ru_doc="- 🔄 Сброс результатов поиска по трекам" - ) - async def ssearchresetcmd(self, message: Message): - """- 🔄 Reset track search results""" - self.set("last_search_results", []) - await utils.answer(message, self.strings["search_results_cleared"]) + await self.inline.form( + self.strings("search_results_inline").format( + count=len(tracks), + query=utils.escape_html(args), + ), + message=message, + reply_markup=self._search_keyboard( + tracks, + self._get_chat_id(message), + reply_to_id, + ), + ) async def watcher(self, message: Message): """Watcher is used to update token""" if not self.sp: return - if self.get("NextRefresh", False): - ttc = self.get("NextRefresh", 0) - crnt = time.time() - if ttc < crnt: + raw = getattr(message, "raw_text", "") or "" + if "spdl_" in raw: + try: + tag = raw.split("spdl_")[1].split("")[0] + sid, idx = tag.split("_") + store_id, index = int(sid), int(idx) + except: + return + + data = self._sp_store.pop(store_id, []) + if not data or index >= len(data): + return + + track_name, artists = data[index] + chat_id = self._get_chat_id(message) + if not chat_id: + return + + reply_to_id = self._reply_id(message) + success = await self._download_track( + chat_id, f"{artists} {track_name}", + track_name=track_name, artists=artists, + log_context=f"{track_name} - {artists}", + reply_to_id=reply_to_id, + ) + if success: + with contextlib.suppress(Exception): + await message.delete() + return + + next_refresh = self.get("NextRefresh") + if not next_refresh or next_refresh < time.time(): + try: self.set( "acs_tkn", - self.sp_auth.refresh_access_token( - self.get("acs_tkn")["refresh_token"] - ), + self.sp_auth.refresh_access_token(self.get("acs_tkn")["refresh_token"]), ) self.set("NextRefresh", time.time() + 45 * 60) self.sp = spotipy.Spotify(auth=self.get("acs_tkn")["access_token"]) - else: - self.set( - "acs_tkn", - self.sp_auth.refresh_access_token(self.get("acs_tkn")["refresh_token"]), - ) - self.set("NextRefresh", time.time() + 45 * 60) - self.sp = spotipy.Spotify(auth=self.get("acs_tkn")["access_token"]) + except Exception as e: + logger.error(f"Spotify watcher error: {e}") + if "Refresh token revoked" in str(e): + refresh_token = await self.invoke("stokrefresh", "", self.inline.bot.id) + await refresh_token.delete() + else: + self.set("NextRefresh", time.time() + 300) diff --git a/radiocycle/Modules/full.txt b/radiocycle/Modules/full.txt index 699e87a..a2d7110 100644 --- a/radiocycle/Modules/full.txt +++ b/radiocycle/Modules/full.txt @@ -1,5 +1,5 @@ Neofetch -randomanimepic +RandomAnimePic SpotifyMod UnbanAll voicetotext diff --git a/radiocycle/Modules/randomanimepic.py b/radiocycle/Modules/randomanimepic.py deleted file mode 100644 index aa7766b..0000000 --- a/radiocycle/Modules/randomanimepic.py +++ /dev/null @@ -1,65 +0,0 @@ -# ======================================= -# _ __ __ __ _ -# | |/ /___ | \/ | ___ __| |___ -# | ' // _ \ | |\/| |/ _ \ / _` / __| -# | . \ __/ | | | | (_) | (_| \__ \ -# |_|\_\___| |_| |_|\___/ \__,_|___/ -# @ke_mods -# ======================================= -# -# LICENSE: CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International) -# -------------------------------------- -# https://creativecommons.org/licenses/by-nd/4.0/legalcode -# ======================================= - -# meta developer: @ke_mods - -import requests -import asyncio -import logging -import traceback -from logging import basicConfig -from .. import loader, utils - -basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -@loader.tds -class RandomAnimePicMod(loader.Module): - strings = { - "name": "RandomAnimePic", - "img": " Your anime pic\n🔗 URL: {}", - "loading": " Loading image...", - "error": "🚫 An unexpected error occurred...", - } - - strings_ru = { - "img": " Ваша аниме-картинка\n🔗 Ссылка: {}", - "loading": " Загрузка изображения...", - "error": "🚫 Произошла непредвиденная ошибка...", - } - - @loader.command( - ru_doc="- получить рандомную аниме-картинку 👀" - ) - async def rapiccmd(self, message): - """- fetch random anime-pic 👀""" - - await utils.answer(message, self.strings("loading")) - - try: - res = requests.get("https://api.nekosia.cat/api/v1/images/cute?count=1") - res.raise_for_status() - data = res.json() - image_url = data['image']['original']['url'] - - await asyncio.sleep(2) - - await utils.answer(message, self.strings("img").format(image_url), file=image_url, reply_to=message.reply_to_msg_id) - - except Exception: - logger.error("Error fetching random anime pic: %s", traceback.format_exc()) - - await utils.answer(message, self.strings("error")) - - await asyncio.sleep(5) diff --git a/trololo65/Modules/TTsaveMod.py b/trololo65/Modules/TTsaveMod.py index e90cfc1..9e53ee9 100644 --- a/trololo65/Modules/TTsaveMod.py +++ b/trololo65/Modules/TTsaveMod.py @@ -1,83 +1,169 @@ # meta developer: @trololo_1 -from telethon import events from .. import utils, loader import re, asyncio, os +from datetime import datetime -chat = "@TTFullBot" +default_chat = "@SaveAsBot" +MODE_FORWARD = "forward" +MODE_DOWNLOAD = "download" class TTsaveMod(loader.Module): - """Save tiktok video""" - strings = {'name': 'TTsaveMod'} - async def client_ready(self, client, db): - self.db = db - - async def ttsavecmd(self, message): - """.ttsave {link}""" + """Save tiktok video""" + strings = {'name': 'TTsaveMod'} + async def client_ready(self, client, db): + self.db = db + self.default_chat = default_chat + if not self.db.get('TTsaveMod', 'chat', False): + self.db.set('TTsaveMod', 'chat', self.default_chat) - args = utils.get_args_raw(message) - async with message.client.conversation(chat) as conv: - await utils.answer(message, 'Скачиваю...') - response1, response2, response3 = [conv.wait_event(events.NewMessage(incoming=True, from_users=chat, chats=chat)) for i in range(3)] - bot_send_link = await message.client.send_message(chat, args) - response1 = await response1 - response2 = await response2 - response3 = await response3 - await response2.download_media("hui.mp4") - await message.client.send_file(message.to_id, "hui.mp4") - await response1.delete() - await response2.delete() - await response3.delete() - await bot_send_link.delete() - await message.delete() - os.remove("hui.mp4") + def _send_mode(self): + m = self.db.get('TTsaveMod', 'send_mode', MODE_FORWARD) + return m if m in (MODE_FORWARD, MODE_DOWNLOAD) else MODE_FORWARD - async def ttacceptcmd(self, message): - """ .ttaccept {reply/id} для открытия в чате автоматического скачивания ссылок. без аргументов тоже работает.\n.ttaccept -l для показа открытых чатов """ + async def save_video(self, message, url=None): + """save video from tiktok. url: ссылка; для .ttsave можно не передавать (берётся из аргументов команды).""" + if url is not None: + args = str(url).strip() + else: + args = utils.get_args_raw(message).strip() + if not args: + await utils.answer(message, "Нет ссылки.") + return False + dest = message.peer_id + chat = self.db.get('TTsaveMod', 'chat') + mode = self._send_mode() + status_msg = await message.respond('Скачиваю...') - args = utils.get_args_raw(message) - reply = await message.get_reply_message() - users_list = self.db.get('TTsaveMod', 'users', []) + async def erase_status(): + try: + await status_msg.delete() + except Exception: + pass - if args == '-l': - if len(users_list) == 0: return await utils.answer(message, 'Список пуст.') - return await utils.answer(message, '• '+'\n• '.join([''+str(i)+'' for i in users_list])) + try: + async with message.client.conversation(chat) as conv: + bot_send_link = await conv.send_message(args) + response1 = await conv.get_response() + response2 = await conv.get_response() - try: - if not args and not reply: - user = message.chat_id - else: - user = reply.sender_id if not args else int(args) - except: - return await utils.answer(message, 'Неверно введён ид.') - if user in users_list: - users_list.remove(user) - await utils.answer(message, f'Ид {str(user)} исключен.') - else: - users_list.append(user) - await utils.answer(message, f'Ид {str(user)} добавлен.') - self.db.set('TTsaveMod', 'users', users_list) + # Определяем, в каком из response пришло видео + video_response, other_response = None, None + if hasattr(response1, "media") and response1.media is not None: + if getattr(response1.media, "document", None) or getattr(response1.media, "video", None): + video_response = response1 + other_response = response2 + if video_response is None and hasattr(response2, "media") and response2.media is not None: + if getattr(response2.media, "document", None) or getattr(response2.media, "video", None): + video_response = response2 + other_response = response1 + if video_response is None: + await erase_status() + await message.respond("Не удалось получить видео.") + await response1.delete() + await response2.delete() + await bot_send_link.delete() + return False - async def watcher(self, message): - try: - users = self.db.get('TTsaveMod', 'users', []) - if message.chat_id not in users: return - links = re.findall(r'((?:https?://)?v[mt]\.tiktok\.com/[A-Za-z0-9_]+/?)', message.raw_text) - if len(links) == 0: return + if mode == MODE_FORWARD: + await video_response.forward_to(dest) + await response1.delete() + await response2.delete() + await bot_send_link.delete() + await erase_status() + return True - async with message.client.conversation(chat) as conv: - for link in links: - response1, response2, response3 = [conv.wait_event(events.NewMessage(incoming=True, from_users=chat, chats=chat)) for i in range(3)] - bot_send_link = await message.client.send_message(chat, link) - response1 = await response1 - response2 = await response2 - response3 = await response3 - await response2.download_media("hui.mp4") - await message.client.send_file(message.chat_id, "hui.mp4") - await response1.delete() - await response2.delete() - await response3.delete() - await bot_send_link.delete() - os.remove("hui.mp4") - await asyncio.sleep(5) - except: pass \ No newline at end of file + now_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + filename = f"{now_time}.mp4" + await video_response.download_media(filename) + await response1.delete() + await response2.delete() + await bot_send_link.delete() + await erase_status() + await message.client.send_file(dest, filename) + os.remove(filename) + return True + except Exception: + await erase_status() + raise + + async def setbotcmd(self, message): + """use: .setbot чтобы установить бота для скачивания.""" + args = utils.get_args_raw(message) + + try: + bot = await message.client.get_entity(args) + except: + return await utils.answer(message, f"бот не найден.") + self.db.set('TTsaveMod', 'bot', str(bot.id)) + await utils.answer(message, f"бот {bot.username} установлен.") + + async def ttsendmodecmd(self, message): + """.ttsendmode forward|download — пересылка с бота (по умолчанию) или скачивание и отправка. Без аргументов — текущий режим.""" + raw = (utils.get_args_raw(message) or "").strip().lower() + if not raw: + cur = self._send_mode() + tip = "пересылка с бота" if cur == MODE_FORWARD else "скачивание и отправка" + return await utils.answer( + message, + f"Сейчас: {tip}\n.ttsendmode forward|download", + ) + if raw in ("forward", "пересылка", "fwd", "f"): + self.db.set("TTsaveMod", "send_mode", MODE_FORWARD) + return await utils.answer(message, "Режим: пересылка с бота.") + if raw in ("download", "скачивание", "скачать", "dl", "d"): + self.db.set("TTsaveMod", "send_mode", MODE_DOWNLOAD) + return await utils.answer(message, "Режим: скачивание и отправка.") + return await utils.answer(message, ".ttsendmode forward|download") + + async def ttsavecmd(self, message): + """.ttsave {link}""" + + args = utils.get_args_raw(message) + save_video = await self.save_video(message) + if save_video: + if self._send_mode() == MODE_FORWARD: + await utils.answer(message, "видео переслано.") + else: + await utils.answer(message, "видео успешно отправлено.") + else: + await utils.answer(message, "не удалось скачать видео.") + + async def ttacceptcmd(self, message): + """ .ttaccept {reply/id} для открытия в чате автоматического скачивания ссылок. без аргументов тоже работает.\n.ttaccept -l для показа открытых чатов """ + + args = utils.get_args_raw(message) + reply = await message.get_reply_message() + users_list = self.db.get('TTsaveMod', 'users', []) + + if args == '-l': + if len(users_list) == 0: return await utils.answer(message, 'Список пуст.') + return await utils.answer(message, '• '+'\n• '.join([''+str(i)+'' for i in users_list])) + + try: + if not args and not reply: + user = message.chat_id + else: + user = reply.sender_id if not args else int(args) + except: + return await utils.answer(message, 'Неверно введён ид.') + if user in users_list: + users_list.remove(user) + await utils.answer(message, f'Ид {str(user)} исключен.') + else: + users_list.append(user) + await utils.answer(message, f'Ид {str(user)} добавлен.') + self.db.set('TTsaveMod', 'users', users_list) + + async def watcher(self, message): + try: + users = self.db.get('TTsaveMod', 'users', []) + if message.chat_id not in users: return + links = re.findall(r'((?:https?://)?v[mt]\.tiktok\.com/[A-Za-z0-9_]+/?)', message.raw_text) + if len(links) == 0: return + + for link in links: + await self.save_video(message, url=link) + await asyncio.sleep(5) + except Exception: + pass From 001a8c5c30a838d5d9d7930295945460a037b072 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 12 Apr 2026 13:57:28 +0000 Subject: [PATCH 2/4] Updated modules.json after parse 2026-04-12 13:57:28 --- modules.json | 141943 ++++++++++++++++++++++++------------------------ 1 file changed, 71177 insertions(+), 70766 deletions(-) diff --git a/modules.json b/modules.json index c2b2fb0..fc589e9 100644 --- a/modules.json +++ b/modules.json @@ -336,6 +336,689 @@ "has_on_unload": false, "class_cmd_names": {} }, + "dorotorothequickend/DorotoroModules/Dota2RandomHero.py": { + "name": "Dota2RandomHero", + "description": "", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroDota2RandomHero.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "dota2hero": "- выбирает рандомного героя из Dota 2" + }, + { + "dota2build": "- выбирает рандомный билд на героя из Dota 2." + }, + { + "dota2pick": "- рандомный пик героев." + }, + { + "dota2hb": "- рандомный герой и рандомный билд." + } + ], + "new_commands": [ + { + "name": "dota2hero", + "original_name": "dota2hero", + "description": { + "default": "- выбирает рандомного героя из Dota 2" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "dota2build", + "original_name": "dota2build", + "description": { + "default": "- выбирает рандомный билд на героя из Dota 2." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "dota2pick", + "original_name": "dota2pick", + "description": { + "default": "- рандомный пик героев." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "dota2hb", + "original_name": "dota2hb", + "description": { + "default": "- рандомный герой и рандомный билд." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "Dota2RandomHero" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/LessonHelper.py": { + "name": "LessonHelper", + "description": "Ваш личный репетитор!", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroLessonHelper.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "mathform": "<формула/list> - базовые формулы по алгебре и геометрии.\n\nЧтобы посмотреть список формул и теорем введите:\n.mathform list" + }, + { + "physform": "<формула/list> - базовые формулы по физике.\n\nЧтобы посмотреть список формул и теорем введите:\n.physform list" + }, + { + "rusform": "<орфограмма/правило/list> - базовые правила и орфограммы по русскому языку. Будет пополняться.\n\nЧтобы узнать список доступных правил и орфограмм, введите:\n.rusform list" + } + ], + "new_commands": [ + { + "name": "mathform", + "original_name": "mathformcmd", + "description": { + "default": "<формула/list> - базовые формулы по алгебре и геометрии.\n\nЧтобы посмотреть список формул и теорем введите:\n.mathform list" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "physform", + "original_name": "physformcmd", + "description": { + "default": "<формула/list> - базовые формулы по физике.\n\nЧтобы посмотреть список формул и теорем введите:\n.physform list" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "rusform", + "original_name": "rusformcmd", + "description": { + "default": "<орфограмма/правило/list> - базовые правила и орфограммы по русскому языку. Будет пополняться.\n\nЧтобы узнать список доступных правил и орфограмм, введите:\n.rusform list" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "LessonHelper" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/RandomHuman.py": { + "name": "RandomHuman", + "description": "Отправляет рандомное имя, фамилию, дату рождения, email, пароль и телефон.", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroGenerateHuman.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "generatehuman": "- сгенерировать человека." + }, + { + "generatepass": "- сгенерировать паспорт." + }, + { + "generateschl": "- сгенерировать инф-цию об образовании." + }, + { + "generatedocs": "- сгенерировать документы." + }, + { + "generateauto": "- сгенерировать инф-цию об авто." + }, + { + "generatebank": "- сгенерировать платежную инф-цию." + } + ], + "new_commands": [ + { + "name": "generatehuman", + "original_name": "generatehumancmd", + "description": { + "default": "- сгенерировать человека." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "generatepass", + "original_name": "generatepasscmd", + "description": { + "default": "- сгенерировать паспорт." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "generateschl", + "original_name": "generateschlcmd", + "description": { + "default": "- сгенерировать инф-цию об образовании." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "generatedocs", + "original_name": "generatedocscmd", + "description": { + "default": "- сгенерировать документы." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "generateauto", + "original_name": "generateauto", + "description": { + "default": "- сгенерировать инф-цию об авто." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "generatebank", + "original_name": "generatebank", + "description": { + "default": "- сгенерировать платежную инф-цию." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "GenerateHuman" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/DoYouKnowAlphabet.py": { + "name": "Alphabet", + "description": "Special for Kids.", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroDoYouKnowAlphabet.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "alphabetru": "- узнать русский алфавит." + }, + { + "consonantorvowel": "<буква> - узнать, гласная или согласная буква." + }, + { + "letterinfo": "<буква> - показывает информацию о букве." + }, + { + "alphabeteng": "- узнать английский алфавит." + } + ], + "new_commands": [ + { + "name": "alphabetru", + "original_name": "alphabetru", + "description": { + "default": "- узнать русский алфавит." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "consonantorvowel", + "original_name": "consonantorvowel", + "description": { + "default": "<буква> - узнать, гласная или согласная буква." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "letterinfo", + "original_name": "letterinfo", + "description": { + "default": "<буква> - показывает информацию о букве." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "alphabeteng", + "original_name": "alphabeteng", + "description": { + "default": "- узнать английский алфавит." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "DoYouKnowAlphabet?" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/AutoEdit.py": { + "name": "AutoEdit", + "description": "Редактирует каждое ваше сообщение в определенное время на выбранный вами текст.\nНастройка через .config AutoEdit", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroAutoEdit.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "autoedit": "- включить/выключить AutoEdit." + } + ], + "new_commands": [ + { + "name": "autoedit", + "original_name": "autoedit", + "description": { + "default": "- включить/выключить AutoEdit." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "AutoEdit", + "timechoice": "Время, за которое будет редактироваться сообщение.(в секундах)", + "editmsg": "Текст, на который будет редактироваться ваше сообщение." + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/PasswordGenerator.py": { + "name": "passwordgeneratormod", + "description": "Ваш персональный генератор паролей.", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroPasswordGenerator.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "gnrtpass": "<кол-во символов> - генерировать пароль" + } + ], + "new_commands": [ + { + "name": "gnrtpass", + "original_name": "gnrtpass", + "description": { + "default": "<кол-во символов> - генерировать пароль" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "PasswordGenerator" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/FkinRickRoll.py": { + "name": "FuckingRickRoll", + "description": "Лучший способ зарикроллить собеседника.", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroFkinRickRoll.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "rickvid": "- стандартный RickRoll." + }, + { + "rickbait": "- отправляет видео с океаном, в конце которого вашего собеседника ждет RickRoll." + } + ], + "new_commands": [ + { + "name": "rickvid", + "original_name": "rickvid", + "description": { + "default": "- стандартный RickRoll." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "rickbait", + "original_name": "rickbait", + "description": { + "default": "- отправляет видео с океаном, в конце которого вашего собеседника ждет RickRoll." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "FkinRickRoll" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/SimpleRolePlay.py": { + "name": "SimpleRolePlay", + "description": "Базовые команды для текстовых ролевых игр.", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/Dor%D0%BEtoroSimpleRoleplay.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "me": "<действие> - сообщает об исполнителе команды от первого лица. Пример использования: .me открыл браузер. Также есть доп. настройка в .config" + }, + { + "do": "<действие> - предназначена для описания событий и подробностей игрового мира в настоящем времени, не относящихся конкретно к определённым людям. Пример использования: .do В кармане Дороторо лежит пистолет и пара гранат." + }, + { + "otry": "<действие> - предназначена для решения спорных и неоднозначных ситуаций, где события могут развиваться по нескольким сценариям, либо если требуется случайная вероятность удачи того или иного действия. Пример использования: .try завёл машину." + }, + { + "todo": "<действие> <фраза>- совмещает описание окружающей обстановки, действие от 3го лица (см. описание .do) с одновременной фразой своего персонажа. Пример использования: .todo Спокойной ночи. засыпая" + } + ], + "new_commands": [ + { + "name": "me", + "original_name": "me", + "description": { + "default": "<действие> - сообщает об исполнителе команды от первого лица. Пример использования: .me открыл браузер. Также есть доп. настройка в .config" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "do", + "original_name": "do", + "description": { + "default": "<действие> - предназначена для описания событий и подробностей игрового мира в настоящем времени, не относящихся конкретно к определённым людям. Пример использования: .do В кармане Дороторо лежит пистолет и пара гранат." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "otry", + "original_name": "otry", + "description": { + "default": "<действие> - предназначена для решения спорных и неоднозначных ситуаций, где события могут развиваться по нескольким сценариям, либо если требуется случайная вероятность удачи того или иного действия. Пример использования: .try завёл машину." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "todo", + "original_name": "todo", + "description": { + "default": "<действие> <фраза>- совмещает описание окружающей обстановки, действие от 3го лица (см. описание .do) с одновременной фразой своего персонажа. Пример использования: .todo Спокойной ночи. засыпая" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "Simple RolePlay", + "symbol": "Символ который используется в конце и в начале /me. (например, звезда)", + "not_args": "😰 Вы неправильно вписали действие или же не указали его вовсе. Попробуйте еще раз." + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/AccountDeleter.py": { + "name": "AccountDeleter", + "description": "", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroAccountDeleter.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "delacc": "- удаляет ваш аккаунт (просто меняет вашу аватарку и ник)." + } + ], + "new_commands": [ + { + "name": "delacc", + "original_name": "delacc", + "description": { + "default": "- удаляет ваш аккаунт (просто меняет вашу аватарку и ник)." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "AccountDeleter" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/WhataWord_.py": { + "name": "whataword", + "description": "Ищет определение слова.", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroWhataWord.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "waw": "<слово> - ищет определение вашего слова." + } + ], + "new_commands": [ + { + "name": "waw", + "original_name": "wawcmd", + "description": { + "default": "<слово> - ищет определение вашего слова." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "What a Word?" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/RandomJumoreska.py": { + "name": "RandomJumoreska", + "description": "Отправляет случайную юмореску.", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroRandomJumoreska.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "rndmjumoreska": "- выдать рандомную юмореску." + } + ], + "new_commands": [ + { + "name": "rndmjumoreska", + "original_name": "rndmjumoreska", + "description": { + "default": "- выдать рандомную юмореску." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "RandomJumoreska" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, "dorotorothequickend/DorotoroModules/CheckSpamBan.py": { "name": "SpamBanCheckMod", "description": "Check spam ban for your account.", @@ -373,6 +1056,59 @@ "has_on_unload": false, "class_cmd_names": {} }, + "dorotorothequickend/DorotoroModules/EMJviaTEXT.py": { + "name": "EMJviaTEXT", + "description": "[ONLY FOR TG PREMIUM]\nЭтот модуль создан чтобы не рыскать миллиарды стикерпаков. \nПример использования:\nПривет BloodTrail", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroEMJviaTEXT.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "emjviatext": "- включить/выключить автозамену текста на эмодзи." + }, + { + "emjlist": "- список эмодзи." + } + ], + "new_commands": [ + { + "name": "emjviatext", + "original_name": "emjviatext", + "description": { + "default": "- включить/выключить автозамену текста на эмодзи." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "emjlist", + "original_name": "emjlist", + "description": { + "default": "- список эмодзи." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "EMJviaTEXT" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, "dorotorothequickend/DorotoroModules/InlineTTS.py": { "name": "InlineTTS", "description": "Синтезирует текст в голос ваших любимых героев!Пример использования: .atts arthas Привет", @@ -730,6 +1466,83 @@ "has_on_unload": false, "class_cmd_names": {} }, + "dorotorothequickend/DorotoroModules/CringePhrases.py": { + "name": "CringePhrases", + "description": "Отправляет случайную мега-кринж фразу.", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroCringePhrases.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "cringephrase": "- фраза, от которой ваш собеседник будет испытывать мега-супер-пупер кринж." + } + ], + "new_commands": [ + { + "name": "cringephrase", + "original_name": "cringephrase", + "description": { + "default": "- фраза, от которой ваш собеседник будет испытывать мега-супер-пупер кринж." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "CringePhrases" + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, + "dorotorothequickend/DorotoroModules/ExcuseGenerator.py": { + "name": "ExcuseGeneratorMod", + "description": "Ваш преданный помощник!", + "cls_doc": {}, + "meta": { + "pic": null, + "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroExcuseGenerator.png", + "developer": "@DorotoroMods" + }, + "commands": [ + { + "excuse": "<имя> - генерирует отмазку." + } + ], + "new_commands": [ + { + "name": "excuse", + "original_name": "excuse", + "description": { + "default": "<имя> - генерирует отмазку." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + } + ], + "inline_handlers": [], + "strings": { + "name": "ExcuseGenerator", + "courtesy": "Обращение к человеку на ТЫ (0), обращение к человеку на ВЫ (1).", + "sex": "Обращаться к человеку как к мужскому полу (0), обращаться к человеку как к женскому полу (1).", + "mysex": "Пол того, кто пишет отмазку. Мужской (0), женский (1)." + }, + "has_on_load": false, + "has_on_unload": false, + "class_cmd_names": {} + }, "dorotorothequickend/DorotoroModules/01code.py": { "name": "tocodedecodemod", "description": "Ваш персональный шифратор в двоичный код.", @@ -783,819 +1596,6 @@ "has_on_unload": false, "class_cmd_names": {} }, - "dorotorothequickend/DorotoroModules/WhataWord_.py": { - "name": "whataword", - "description": "Ищет определение слова.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroWhataWord.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "waw": "<слово> - ищет определение вашего слова." - } - ], - "new_commands": [ - { - "name": "waw", - "original_name": "wawcmd", - "description": { - "default": "<слово> - ищет определение вашего слова." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "What a Word?" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/EMJviaTEXT.py": { - "name": "EMJviaTEXT", - "description": "[ONLY FOR TG PREMIUM]\nЭтот модуль создан чтобы не рыскать миллиарды стикерпаков. \nПример использования:\nПривет BloodTrail", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroEMJviaTEXT.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "emjviatext": "- включить/выключить автозамену текста на эмодзи." - }, - { - "emjlist": "- список эмодзи." - } - ], - "new_commands": [ - { - "name": "emjviatext", - "original_name": "emjviatext", - "description": { - "default": "- включить/выключить автозамену текста на эмодзи." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "emjlist", - "original_name": "emjlist", - "description": { - "default": "- список эмодзи." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "EMJviaTEXT" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/ExcuseGenerator.py": { - "name": "ExcuseGeneratorMod", - "description": "Ваш преданный помощник!", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroExcuseGenerator.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "excuse": "<имя> - генерирует отмазку." - } - ], - "new_commands": [ - { - "name": "excuse", - "original_name": "excuse", - "description": { - "default": "<имя> - генерирует отмазку." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "ExcuseGenerator", - "courtesy": "Обращение к человеку на ТЫ (0), обращение к человеку на ВЫ (1).", - "sex": "Обращаться к человеку как к мужскому полу (0), обращаться к человеку как к женскому полу (1).", - "mysex": "Пол того, кто пишет отмазку. Мужской (0), женский (1)." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/LessonHelper.py": { - "name": "LessonHelper", - "description": "Ваш личный репетитор!", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroLessonHelper.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "mathform": "<формула/list> - базовые формулы по алгебре и геометрии.\n\nЧтобы посмотреть список формул и теорем введите:\n.mathform list" - }, - { - "physform": "<формула/list> - базовые формулы по физике.\n\nЧтобы посмотреть список формул и теорем введите:\n.physform list" - }, - { - "rusform": "<орфограмма/правило/list> - базовые правила и орфограммы по русскому языку. Будет пополняться.\n\nЧтобы узнать список доступных правил и орфограмм, введите:\n.rusform list" - } - ], - "new_commands": [ - { - "name": "mathform", - "original_name": "mathformcmd", - "description": { - "default": "<формула/list> - базовые формулы по алгебре и геометрии.\n\nЧтобы посмотреть список формул и теорем введите:\n.mathform list" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "physform", - "original_name": "physformcmd", - "description": { - "default": "<формула/list> - базовые формулы по физике.\n\nЧтобы посмотреть список формул и теорем введите:\n.physform list" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "rusform", - "original_name": "rusformcmd", - "description": { - "default": "<орфограмма/правило/list> - базовые правила и орфограммы по русскому языку. Будет пополняться.\n\nЧтобы узнать список доступных правил и орфограмм, введите:\n.rusform list" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "LessonHelper" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/PasswordGenerator.py": { - "name": "passwordgeneratormod", - "description": "Ваш персональный генератор паролей.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroPasswordGenerator.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "gnrtpass": "<кол-во символов> - генерировать пароль" - } - ], - "new_commands": [ - { - "name": "gnrtpass", - "original_name": "gnrtpass", - "description": { - "default": "<кол-во символов> - генерировать пароль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "PasswordGenerator" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/RandomHuman.py": { - "name": "RandomHuman", - "description": "Отправляет рандомное имя, фамилию, дату рождения, email, пароль и телефон.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroGenerateHuman.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "generatehuman": "- сгенерировать человека." - }, - { - "generatepass": "- сгенерировать паспорт." - }, - { - "generateschl": "- сгенерировать инф-цию об образовании." - }, - { - "generatedocs": "- сгенерировать документы." - }, - { - "generateauto": "- сгенерировать инф-цию об авто." - }, - { - "generatebank": "- сгенерировать платежную инф-цию." - } - ], - "new_commands": [ - { - "name": "generatehuman", - "original_name": "generatehumancmd", - "description": { - "default": "- сгенерировать человека." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "generatepass", - "original_name": "generatepasscmd", - "description": { - "default": "- сгенерировать паспорт." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "generateschl", - "original_name": "generateschlcmd", - "description": { - "default": "- сгенерировать инф-цию об образовании." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "generatedocs", - "original_name": "generatedocscmd", - "description": { - "default": "- сгенерировать документы." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "generateauto", - "original_name": "generateauto", - "description": { - "default": "- сгенерировать инф-цию об авто." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "generatebank", - "original_name": "generatebank", - "description": { - "default": "- сгенерировать платежную инф-цию." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "GenerateHuman" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/CringePhrases.py": { - "name": "CringePhrases", - "description": "Отправляет случайную мега-кринж фразу.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroCringePhrases.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "cringephrase": "- фраза, от которой ваш собеседник будет испытывать мега-супер-пупер кринж." - } - ], - "new_commands": [ - { - "name": "cringephrase", - "original_name": "cringephrase", - "description": { - "default": "- фраза, от которой ваш собеседник будет испытывать мега-супер-пупер кринж." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "CringePhrases" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/SimpleRolePlay.py": { - "name": "SimpleRolePlay", - "description": "Базовые команды для текстовых ролевых игр.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/Dor%D0%BEtoroSimpleRoleplay.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "me": "<действие> - сообщает об исполнителе команды от первого лица. Пример использования: .me открыл браузер. Также есть доп. настройка в .config" - }, - { - "do": "<действие> - предназначена для описания событий и подробностей игрового мира в настоящем времени, не относящихся конкретно к определённым людям. Пример использования: .do В кармане Дороторо лежит пистолет и пара гранат." - }, - { - "otry": "<действие> - предназначена для решения спорных и неоднозначных ситуаций, где события могут развиваться по нескольким сценариям, либо если требуется случайная вероятность удачи того или иного действия. Пример использования: .try завёл машину." - }, - { - "todo": "<действие> <фраза>- совмещает описание окружающей обстановки, действие от 3го лица (см. описание .do) с одновременной фразой своего персонажа. Пример использования: .todo Спокойной ночи. засыпая" - } - ], - "new_commands": [ - { - "name": "me", - "original_name": "me", - "description": { - "default": "<действие> - сообщает об исполнителе команды от первого лица. Пример использования: .me открыл браузер. Также есть доп. настройка в .config" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "do", - "original_name": "do", - "description": { - "default": "<действие> - предназначена для описания событий и подробностей игрового мира в настоящем времени, не относящихся конкретно к определённым людям. Пример использования: .do В кармане Дороторо лежит пистолет и пара гранат." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "otry", - "original_name": "otry", - "description": { - "default": "<действие> - предназначена для решения спорных и неоднозначных ситуаций, где события могут развиваться по нескольким сценариям, либо если требуется случайная вероятность удачи того или иного действия. Пример использования: .try завёл машину." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "todo", - "original_name": "todo", - "description": { - "default": "<действие> <фраза>- совмещает описание окружающей обстановки, действие от 3го лица (см. описание .do) с одновременной фразой своего персонажа. Пример использования: .todo Спокойной ночи. засыпая" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Simple RolePlay", - "symbol": "Символ который используется в конце и в начале /me. (например, звезда)", - "not_args": "😰 Вы неправильно вписали действие или же не указали его вовсе. Попробуйте еще раз." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/Dota2RandomHero.py": { - "name": "Dota2RandomHero", - "description": "", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroDota2RandomHero.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "dota2hero": "- выбирает рандомного героя из Dota 2" - }, - { - "dota2build": "- выбирает рандомный билд на героя из Dota 2." - }, - { - "dota2pick": "- рандомный пик героев." - }, - { - "dota2hb": "- рандомный герой и рандомный билд." - } - ], - "new_commands": [ - { - "name": "dota2hero", - "original_name": "dota2hero", - "description": { - "default": "- выбирает рандомного героя из Dota 2" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "dota2build", - "original_name": "dota2build", - "description": { - "default": "- выбирает рандомный билд на героя из Dota 2." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "dota2pick", - "original_name": "dota2pick", - "description": { - "default": "- рандомный пик героев." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "dota2hb", - "original_name": "dota2hb", - "description": { - "default": "- рандомный герой и рандомный билд." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Dota2RandomHero" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/AutoEdit.py": { - "name": "AutoEdit", - "description": "Редактирует каждое ваше сообщение в определенное время на выбранный вами текст.\nНастройка через .config AutoEdit", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroAutoEdit.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "autoedit": "- включить/выключить AutoEdit." - } - ], - "new_commands": [ - { - "name": "autoedit", - "original_name": "autoedit", - "description": { - "default": "- включить/выключить AutoEdit." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AutoEdit", - "timechoice": "Время, за которое будет редактироваться сообщение.(в секундах)", - "editmsg": "Текст, на который будет редактироваться ваше сообщение." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/AccountDeleter.py": { - "name": "AccountDeleter", - "description": "", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroAccountDeleter.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "delacc": "- удаляет ваш аккаунт (просто меняет вашу аватарку и ник)." - } - ], - "new_commands": [ - { - "name": "delacc", - "original_name": "delacc", - "description": { - "default": "- удаляет ваш аккаунт (просто меняет вашу аватарку и ник)." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AccountDeleter" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/DoYouKnowAlphabet.py": { - "name": "Alphabet", - "description": "Special for Kids.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroDoYouKnowAlphabet.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "alphabetru": "- узнать русский алфавит." - }, - { - "consonantorvowel": "<буква> - узнать, гласная или согласная буква." - }, - { - "letterinfo": "<буква> - показывает информацию о букве." - }, - { - "alphabeteng": "- узнать английский алфавит." - } - ], - "new_commands": [ - { - "name": "alphabetru", - "original_name": "alphabetru", - "description": { - "default": "- узнать русский алфавит." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "consonantorvowel", - "original_name": "consonantorvowel", - "description": { - "default": "<буква> - узнать, гласная или согласная буква." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "letterinfo", - "original_name": "letterinfo", - "description": { - "default": "<буква> - показывает информацию о букве." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "alphabeteng", - "original_name": "alphabeteng", - "description": { - "default": "- узнать английский алфавит." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "DoYouKnowAlphabet?" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/RandomJumoreska.py": { - "name": "RandomJumoreska", - "description": "Отправляет случайную юмореску.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroRandomJumoreska.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "rndmjumoreska": "- выдать рандомную юмореску." - } - ], - "new_commands": [ - { - "name": "rndmjumoreska", - "original_name": "rndmjumoreska", - "description": { - "default": "- выдать рандомную юмореску." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "RandomJumoreska" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "dorotorothequickend/DorotoroModules/FkinRickRoll.py": { - "name": "FuckingRickRoll", - "description": "Лучший способ зарикроллить собеседника.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://raw.githubusercontent.com/dorotorothequickend/DorotoroModules/main/banners/DorotoroFkinRickRoll.png", - "developer": "@DorotoroMods" - }, - "commands": [ - { - "rickvid": "- стандартный RickRoll." - }, - { - "rickbait": "- отправляет видео с океаном, в конце которого вашего собеседника ждет RickRoll." - } - ], - "new_commands": [ - { - "name": "rickvid", - "original_name": "rickvid", - "description": { - "default": "- стандартный RickRoll." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "rickbait", - "original_name": "rickbait", - "description": { - "default": "- отправляет видео с океаном, в конце которого вашего собеседника ждет RickRoll." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "FkinRickRoll" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, "dorotorothequickend/DorotoroModules/FoodRecipe.py": { "name": "FoodRecipe", "description": "Ищет рецепт блюда по его названию.", @@ -1633,463 +1633,25 @@ "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/impostor.py": { - "name": "ImpMod", - "description": "Among Us", + "SekaiYoneya/Friendly-telegram/DeleteTimer.py": { + "name": "DeleteTimer", + "description": "", "cls_doc": {}, "meta": { "pic": null, "banner": null, "developer": null }, - "commands": [ - { - "imp": "Используй: .imp <@ или текст или реплай>." - }, - { - "ruimp": "Используй: .ruimp <@ или текст или реплай>." - } - ], - "new_commands": [ - { - "name": "imp", - "original_name": "impcmd", - "description": { - "default": "Используй: .imp <@ или текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ruimp", - "original_name": "ruimpcmd", - "description": { - "default": "Используй: .ruimp <@ или текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], + "commands": [], + "new_commands": [], "inline_handlers": [], - "strings": { - "name": "Impostor?" - }, + "strings": {}, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/onava.py": { - "name": "OnAvaMod", - "description": "Гифку/видео/стикер на аву.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "onava": "Установить на аву гифку/видео/стикер.\nИспользование: .onava <реплай>." - }, - { - "togif": "Сделать из медиа гифку.\nИспользование: .togif <реплай>." - } - ], - "new_commands": [ - { - "name": "onava", - "original_name": "onavacmd", - "description": { - "default": "Установить на аву гифку/видео/стикер.\nИспользование: .onava <реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "togif", - "original_name": "togifcmd", - "description": { - "default": "Сделать из медиа гифку.\nИспользование: .togif <реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "OnAva" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/replydownloader.py": { - "name": "ReplyDownloaderMod", - "description": "Скачать файлом реплай.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "dlr": "Команда .dlr <реплай на файл> <название (по желанию)> скачивает файл, либо сохраняет текст в файл на который был сделан реплай." - }, - { - "ulf": "Команда .ulf * <название файла> отправляет файл в чат.\n* - удалить файл после отправки." - } - ], - "new_commands": [ - { - "name": "dlr", - "original_name": "dlrcmd", - "description": { - "default": "Команда .dlr <реплай на файл> <название (по желанию)> скачивает файл, либо сохраняет текст в файл на который был сделан реплай." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ulf", - "original_name": "ulfcmd", - "description": { - "default": "Команда .ulf * <название файла> отправляет файл в чат.\n* - удалить файл после отправки." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Reply Downloader" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/quotes.py": { - "name": "QuotesMod", - "description": "Quote a message", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "quote": "" - } - ], - "new_commands": [ - { - "name": "quote", - "original_name": "quotecmd", - "description": { - "default": "" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Quotes" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/chatinfo.py": { - "name": "ChatInfoMod", - "description": "Показывает информацию о чате.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "chatinfo": "Используй .chatinfo <айди чата>; ничего" - } - ], - "new_commands": [ - { - "name": "chatinfo", - "original_name": "chatinfocmd", - "description": { - "default": "Используй .chatinfo <айди чата>; ничего" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "ChatInfo" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/mediacutter.py": { - "name": "MediaCutterMod", - "description": "Обрезать медиа.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "cut": "Используй .cut <начало(сек):конец(сек)> <реплай на аудио/видео/гиф>." - } - ], - "new_commands": [ - { - "name": "cut", - "original_name": "cutcmd", - "description": { - "default": "Используй .cut <начало(сек):конец(сек)> <реплай на аудио/видео/гиф>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "MediaCutter" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/family.py": { - "name": "FamilyMod", - "description": "Quote a message", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "family": "" - } - ], - "new_commands": [ - { - "name": "family", - "original_name": "familycmd", - "description": { - "default": "" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Family" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/hiddentag.py": { - "name": "HiddenTagMod", - "description": "Скрытно тегнуть пользователя.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "tag": "Использование: .tag <@> <текст (по желанию)>." - } - ], - "new_commands": [ - { - "name": "tag", - "original_name": "tagcmd", - "description": { - "default": "Использование: .tag <@> <текст (по желанию)>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "HiddenTag" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/getcommonchats.py": { - "name": "GetCommonChatsMod", - "description": "Общие чаты с пользователем.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "common": "Используй .common <@ или реплай>, чтобы узнать общие чаты с пользователем." - } - ], - "new_commands": [ - { - "name": "common", - "original_name": "commoncmd", - "description": { - "default": "Используй .common <@ или реплай>, чтобы узнать общие чаты с пользователем." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "GetCommonChats" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/nedoquotes.py": { - "name": "NedoQuotesMod", - "description": "Генератор всратых цитат by @ShittyQuoteBot", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "nq": "Используй: .nq <текст или реплай>." - } - ], - "new_commands": [ - { - "name": "nq", - "original_name": "nqcmd", - "description": { - "default": "Используй: .nq <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "NedoQuotes" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/AudioShakal.py": { - "name": "AudioShakalMod", - "description": "АудиоШакал", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "fv": ".fv [шакал_lvl(не обязательно, по умолчанию 100 (от 10 до 100))]\nСшакалить войс/mp3/ogg/oga" - } - ], - "new_commands": [ - { - "name": "fv", - "original_name": "fvcmd", - "description": { - "default": ".fv [шакал_lvl(не обязательно, по умолчанию 100 (от 10 до 100))]\nСшакалить войс/mp3/ogg/oga" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "АудиоШакал" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/textonphoto.py": { - "name": "TextOnPhotoMod", + "SekaiYoneya/Friendly-telegram/kickall-addusers.py": { + "name": "addmembersMod", "description": "", "cls_doc": {}, "meta": { @@ -2099,21 +1661,18 @@ }, "commands": [ { - "bottom": "Используй: .bottom {реплай на картинку/стикер} ;ничего <текст>." + "addusers": "Добавляет людей с чата в чат." }, { - "top": "Используй: .top {реплай на картинку/стикер} ;ничего <текст>." - }, - { - "center": "Используй: .center {реплай на картинку/стикер} ;ничего <текст>." + "kickall": "Удаляет всех пользователей из чата." } ], "new_commands": [ { - "name": "bottom", - "original_name": "bottomcmd", + "name": "addusers", + "original_name": "adduserscmd", "description": { - "default": "Используй: .bottom {реплай на картинку/стикер} ;ничего <текст>." + "default": "Добавляет людей с чата в чат." }, "cmd_names": {}, "aliases": [], @@ -2123,23 +1682,10 @@ "decorators": [] }, { - "name": "top", - "original_name": "topcmd", + "name": "kickall", + "original_name": "kickallcmd", "description": { - "default": "Используй: .top {реплай на картинку/стикер} ;ничего <текст>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "center", - "original_name": "centercmd", - "description": { - "default": "Используй: .center {реплай на картинку/стикер} ;ничего <текст>." + "default": "Удаляет всех пользователей из чата." }, "cmd_names": {}, "aliases": [], @@ -2151,15 +1697,15 @@ ], "inline_handlers": [], "strings": { - "name": "TextOnPhoto" + "name": "kickall & addusers" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/userdataen.py": { - "name": "UserDataMod", - "description": "This module can change your Telegram profile.", + "SekaiYoneya/Friendly-telegram/SendPhotos.py": { + "name": "GetPPMod", + "description": "Description for module", "cls_doc": {}, "meta": { "pic": null, @@ -2168,47 +1714,15 @@ }, "commands": [ { - "name": "For .name command, change your first/second name." - }, - { - "bio": "For .bio command, set a new bio for your profile." - }, - { - "username": "For .username command, set a new username." + "poto": "Кинуть фоточки" } ], "new_commands": [ { - "name": "name", - "original_name": "namecmd", + "name": "poto", + "original_name": "potocmd", "description": { - "default": "For .name command, change your first/second name." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bio", - "original_name": "biocmd", - "description": { - "default": "For .bio command, set a new bio for your profile." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "username", - "original_name": "usernamecmd", - "description": { - "default": "For .username command, set a new username." + "default": "Кинуть фоточки" }, "cmd_names": {}, "aliases": [], @@ -2220,15 +1734,15 @@ ], "inline_handlers": [], "strings": { - "name": "UserData" + "name": "SendPhotos" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/calendar.py": { - "name": "CalendarMod", - "description": "Календарь", + "SekaiYoneya/Friendly-telegram/Frazes.py": { + "name": "FrazesMod", + "description": "Госу, пикапы, подкаты.", "cls_doc": {}, "meta": { "pic": null, @@ -2237,15 +1751,63 @@ }, "commands": [ { - "clnd": ".clnd <год> <месяц> или ничего" + "gosu": "Выебать чью-то мамку" + }, + { + "pikap": "Пикап" + }, + { + "podkat": "Подкат" + }, + { + "ayf": "АУФ!!!" } ], "new_commands": [ { - "name": "clnd", - "original_name": "clndcmd", + "name": "gosu", + "original_name": "gosucmd", "description": { - "default": ".clnd <год> <месяц> или ничего" + "default": "Выебать чью-то мамку" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "pikap", + "original_name": "pikapcmd", + "description": { + "default": "Пикап" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "podkat", + "original_name": "podkatcmd", + "description": { + "default": "Подкат" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "ayf", + "original_name": "ayfcmd", + "description": { + "default": "АУФ!!!" }, "cmd_names": {}, "aliases": [], @@ -2257,15 +1819,15 @@ ], "inline_handlers": [], "strings": { - "name": "Calendar" + "name": "Фразы" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/nowplay.py": { - "name": "NowPlayMod", - "description": "Что сейчас играет.", + "SekaiYoneya/Friendly-telegram/Sender.py": { + "name": "SenderMod", + "description": "", "cls_doc": {}, "meta": { "pic": null, @@ -2274,15 +1836,15 @@ }, "commands": [ { - "np": "Скидывает то, что сейчас играет." + "snd": ".snd <канал/чат/id> \nОтправить сообщение в чат/канал(без авторства)" } ], "new_commands": [ { - "name": "np", - "original_name": "npcmd", + "name": "snd", + "original_name": "sndcmd", "description": { - "default": "Скидывает то, что сейчас играет." + "default": ".snd <канал/чат/id> \nОтправить сообщение в чат/канал(без авторства)" }, "cmd_names": {}, "aliases": [], @@ -2294,15 +1856,15 @@ ], "inline_handlers": [], "strings": { - "name": "NowPlay" + "name": "Sender" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/hiddenurl.py": { - "name": "HiddenUrlMod", - "description": "Скрывает ссылку под невидимый текст.", + "SekaiYoneya/Friendly-telegram/AudioEditor.py": { + "name": "AudioEditorMod", + "description": "Модуль для работы со звуком(???)", "cls_doc": {}, "meta": { "pic": null, @@ -2311,15 +1873,207 @@ }, "commands": [ { - "hide": "Используй .hide <текст или реплай на медиа>." + "bass": ".bass [уровень bass'а 2-100 (Default 2)] \nBassBoost" + }, + { + "fv": ".fv [уровень шакала 2-100 (Default 25)] \nШакалинг" + }, + { + "echos": ".echos \nЭхо эффект" + }, + { + "volup": ".volup \nУвеличить громкость на 10dB" + }, + { + "voldw": ".voldw \nУменьшить громкость на 10dB" + }, + { + "revs": ".revs \nРазвернуть аудио" + }, + { + "reps": ".reps \nПовторить аудио 2 раза подряд" + }, + { + "slows": ".slows \nЗамедлить аудио 0.5x" + }, + { + "fasts": ".fasts \nУскорить аудио 1.5x" + }, + { + "rights": ".rights \nВесь звук в правый канал" + }, + { + "lefts": ".lefts \nВесь звук в левый канал" + }, + { + "norms": ".norms \nНормализовать звук (Из тихого - нормальный)" + }, + { + "byroberts": ".byroberts \nДобавить в конец аудио \"Directed by Robert B Weide\"" } ], "new_commands": [ { - "name": "hide", - "original_name": "hidecmd", + "name": "bass", + "original_name": "basscmd", "description": { - "default": "Используй .hide <текст или реплай на медиа>." + "default": ".bass [уровень bass'а 2-100 (Default 2)] \nBassBoost" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "fv", + "original_name": "fvcmd", + "description": { + "default": ".fv [уровень шакала 2-100 (Default 25)] \nШакалинг" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "echos", + "original_name": "echoscmd", + "description": { + "default": ".echos \nЭхо эффект" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "volup", + "original_name": "volupcmd", + "description": { + "default": ".volup \nУвеличить громкость на 10dB" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "voldw", + "original_name": "voldwcmd", + "description": { + "default": ".voldw \nУменьшить громкость на 10dB" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "revs", + "original_name": "revscmd", + "description": { + "default": ".revs \nРазвернуть аудио" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "reps", + "original_name": "repscmd", + "description": { + "default": ".reps \nПовторить аудио 2 раза подряд" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "slows", + "original_name": "slowscmd", + "description": { + "default": ".slows \nЗамедлить аудио 0.5x" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "fasts", + "original_name": "fastscmd", + "description": { + "default": ".fasts \nУскорить аудио 1.5x" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "rights", + "original_name": "rightscmd", + "description": { + "default": ".rights \nВесь звук в правый канал" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "lefts", + "original_name": "leftscmd", + "description": { + "default": ".lefts \nВесь звук в левый канал" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "norms", + "original_name": "normscmd", + "description": { + "default": ".norms \nНормализовать звук (Из тихого - нормальный)" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "byroberts", + "original_name": "byrobertscmd", + "description": { + "default": ".byroberts \nДобавить в конец аудио \"Directed by Robert B Weide\"" }, "cmd_names": {}, "aliases": [], @@ -2331,15 +2085,15 @@ ], "inline_handlers": [], "strings": { - "name": "HiddenUrl" + "name": "AudioEditor" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/ownerships.py": { - "name": "OwnershipsMod", - "description": "Посмотреть свои владения.", + "SekaiYoneya/Friendly-telegram/MyRep.py": { + "name": "MyRepMod", + "description": "Модуль с вашей репутацией", "cls_doc": {}, "meta": { "pic": null, @@ -2348,15 +2102,31 @@ }, "commands": [ { - "own": "Команда .own выводит список владений открытых чатов/каналов. " + "rep": "Включить режим репутаций." + }, + { + "myrep": "Посмотреть свою репутацию. Используй: .myrep clear (очистка репутации)." } ], "new_commands": [ { - "name": "own", - "original_name": "owncmd", + "name": "rep", + "original_name": "repcmd", "description": { - "default": "Команда .own выводит список владений открытых чатов/каналов. " + "default": "Включить режим репутаций." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "myrep", + "original_name": "myrepcmd", + "description": { + "default": "Посмотреть свою репутацию. Используй: .myrep clear (очистка репутации)." }, "cmd_names": {}, "aliases": [], @@ -2368,15 +2138,15 @@ ], "inline_handlers": [], "strings": { - "name": "Ownerships" + "name": "Репутация" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/report.py": { - "name": "ReportMod", - "description": "Репорт", + "SekaiYoneya/Friendly-telegram/AutoBlackList.py": { + "name": "AutoBlackListMod", + "description": "Кидает всех неконтактов в ЧС.", "cls_doc": {}, "meta": { "pic": null, @@ -2385,15 +2155,47 @@ }, "commands": [ { - "report": "Репорт пользователя за спам." + "autobl": "Включить/выключить режим" + }, + { + "autoblstatus": "Проверить статус AutoBlackList" + }, + { + "autodelchat": "Автоматически удаляет диалог после того, как кинет в ЧС" } ], "new_commands": [ { - "name": "report", - "original_name": "reportcmd", + "name": "autobl", + "original_name": "autoblcmd", "description": { - "default": "Репорт пользователя за спам." + "default": "Включить/выключить режим" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "autoblstatus", + "original_name": "autoblstatuscmd", + "description": { + "default": "Проверить статус AutoBlackList" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "autodelchat", + "original_name": "autodelchatcmd", + "description": { + "default": "Автоматически удаляет диалог после того, как кинет в ЧС" }, "cmd_names": {}, "aliases": [], @@ -2405,15 +2207,15 @@ ], "inline_handlers": [], "strings": { - "name": "Report" + "name": "AutoBlackList" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/dictionary.py": { - "name": "DictionaryMod", - "description": "Словарь.", + "SekaiYoneya/Friendly-telegram/BanMedia.py": { + "name": "BanMediaMod", + "description": "Модуль блокировки стикеров или гифок в чатах.", "cls_doc": {}, "meta": { "pic": null, @@ -2422,15 +2224,15 @@ }, "commands": [ { - "mean": "Использование: .mean <слово>." + "banmedia": "Используй: .banmedia чтобы заблокировать стикер или гифку в чате. | аргументы «clear или clearall» (по желанию)" } ], "new_commands": [ { - "name": "mean", - "original_name": "meancmd", + "name": "banmedia", + "original_name": "banmediacmd", "description": { - "default": "Использование: .mean <слово>." + "default": "Используй: .banmedia чтобы заблокировать стикер или гифку в чате. | аргументы «clear или clearall» (по желанию)" }, "cmd_names": {}, "aliases": [], @@ -2442,15 +2244,15 @@ ], "inline_handlers": [], "strings": { - "name": "Dictionary" + "name": "BanMedia" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/spam.py": { - "name": "SpamMod", - "description": "Спам модуль", + "SekaiYoneya/Friendly-telegram/x0-Uploader.py": { + "name": "x0Mod", + "description": "Uploader", "cls_doc": {}, "meta": { "pic": null, @@ -2459,464 +2261,13 @@ }, "commands": [ { - "spam": "Обычный спам. Используй .spam <кол-во:int> <текст или реплай>." - }, - { - "cspam": "Спам символами. Используй .cspam <текст или реплай>." - }, - { - "wspam": "Спам словами. Используй .wspam <текст или реплай>." - }, - { - "delayspam": "Спам с задержкой. Используй .delayspam <время:int> <кол-во:int> <текст или реплай>." + "x0": "" } ], "new_commands": [ { - "name": "spam", - "original_name": "spamcmd", - "description": { - "default": "Обычный спам. Используй .spam <кол-во:int> <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "cspam", - "original_name": "cspamcmd", - "description": { - "default": "Спам символами. Используй .cspam <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "wspam", - "original_name": "wspamcmd", - "description": { - "default": "Спам словами. Используй .wspam <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "delayspam", - "original_name": "delayspamcmd", - "description": { - "default": "Спам с задержкой. Используй .delayspam <время:int> <кол-во:int> <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Spam" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/gsbl.py": { - "name": "GSBLMod", - "description": "Фановый, мемный модуль.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "gsbl": "Используй .gsbl <реплай на картинку/стикер>." - } - ], - "new_commands": [ - { - "name": "gsbl", - "original_name": "gsblcmd", - "description": { - "default": "Используй .gsbl <реплай на картинку/стикер>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Get-Stick-Bugged-Lol" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/wwtaf.py": { - "name": "WWTaFMod", - "description": "Модуль для работы с текстом или файлами.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "file": "Получить файл по ссылке.\nИспользование: .file <ссылка или реплай на ссылку>." - }, - { - "tabfix": "Используй .tabfix <реплай или файл с текстом .tabfix>." - }, - { - "text2txt": "Переносит текст в файл .txt.\nИспользуй: .text2txt <текст или реплай>." - }, - { - "text2py": "Переносит текст в файл .py.\nИспользуй: .text2py <текст или реплай>." - }, - { - "bold": "Сделать текст жирным.\nИспользуй: .bold <текст или реплай>." - }, - { - "italic": "Сделать текст курсивным.\nИспользуй: .italic <текст или реплай>." - }, - { - "underline": "Сделать текст подчеркнутым.\nИспользуй: .underline <текст или реплай>." - }, - { - "mono": "Сделать текст моноширинным.\nИспользуй: .mono <текст или реплай>." - }, - { - "cross": "Сделать текст зачеркнутым.\nИспользуй: .cross <текст или реплай>." - }, - { - "enter": "Перенос строки после каждого слова.\nИспользуй: .enter <текст или реплай>." - } - ], - "new_commands": [ - { - "name": "file", - "original_name": "filecmd", - "description": { - "default": "Получить файл по ссылке.\nИспользование: .file <ссылка или реплай на ссылку>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tabfix", - "original_name": "tabfixcmd", - "description": { - "default": "Используй .tabfix <реплай или файл с текстом .tabfix>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "text2txt", - "original_name": "text2txtcmd", - "description": { - "default": "Переносит текст в файл .txt.\nИспользуй: .text2txt <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "text2py", - "original_name": "text2pycmd", - "description": { - "default": "Переносит текст в файл .py.\nИспользуй: .text2py <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bold", - "original_name": "boldcmd", - "description": { - "default": "Сделать текст жирным.\nИспользуй: .bold <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "italic", - "original_name": "italiccmd", - "description": { - "default": "Сделать текст курсивным.\nИспользуй: .italic <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "underline", - "original_name": "underlinecmd", - "description": { - "default": "Сделать текст подчеркнутым.\nИспользуй: .underline <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "mono", - "original_name": "monocmd", - "description": { - "default": "Сделать текст моноширинным.\nИспользуй: .mono <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "cross", - "original_name": "crosscmd", - "description": { - "default": "Сделать текст зачеркнутым.\nИспользуй: .cross <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "enter", - "original_name": "entercmd", - "description": { - "default": "Перенос строки после каждого слова.\nИспользуй: .enter <текст или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "WWTaF" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/fake.py": { - "name": "FakeActionsMod", - "description": "Показывает фейковые действия.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "fake": "Использование: .fake <действие>.\nСписок действий: typing, contact, game, location, record-audio, record-round, record-video, voice, round, video, photo, document.\nОтмена: .fake cancel" - } - ], - "new_commands": [ - { - "name": "fake", - "original_name": "fakecmd", - "description": { - "default": "Использование: .fake <действие>.\nСписок действий: typing, contact, game, location, record-audio, record-round, record-video, voice, round, video, photo, document.\nОтмена: .fake cancel" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Fake Actions" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/groupcreator.py": { - "name": "GroupCreatorMod", - "description": "Создать чат или канал.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "create": "Используй .create <название>, чтобы создать группу, супергруппу или канал." - } - ], - "new_commands": [ - { - "name": "create", - "original_name": "createcmd", - "description": { - "default": "Используй .create <название>, чтобы создать группу, супергруппу или канал." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "GroupCreator" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/tagall.py": { - "name": "TagAllMod", - "description": "Тэгает всех в чате.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "tagall": "Используй .tagall <текст (по желанию)>." - } - ], - "new_commands": [ - { - "name": "tagall", - "original_name": "tagallcmd", - "description": { - "default": "Используй .tagall <текст (по желанию)>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "TagAll" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/SpeedRead.py": { - "name": "SpeedReadMod", - "description": "каждое слово раз в 100мс", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "sr": ".sr " - } - ], - "new_commands": [ - { - "name": "sr", - "original_name": "srcmd", - "description": { - "default": ".sr " - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "SpeedRead" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/count.py": { - "name": "CountMod", - "description": "Количество чатов.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "count": "" - } - ], - "new_commands": [ - { - "name": "count", - "original_name": "countcmd", + "name": "x0", + "original_name": "x0cmd", "description": { "default": "" }, @@ -2930,15 +2281,15 @@ ], "inline_handlers": [], "strings": { - "name": "Count" + "name": "x0 Uploader" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/freeomonbot.py": { - "name": "OmonBotMod", - "description": "Омон бот.", + "SekaiYoneya/Friendly-telegram/WelcomeLeft.py": { + "name": "WelcomeLeftMod", + "description": "Вход и выход пользователей в чате.", "cls_doc": {}, "meta": { "pic": null, @@ -2947,15 +2298,63 @@ }, "commands": [ { - "omon": "Используй .omon <реплай на пикчу>." + "welcome": "Включить/выключить приветствие новых пользователей в чате. Используй: .welcome ." + }, + { + "setwelcome": "Установить приветствие новых пользователей в чате.\nИспользуй: .setwelcome <текст (можно использовать {name}; {chat})>; ничего." + }, + { + "left": "Включить/выключить выход пользователей из чата. Используй: .left ." + }, + { + "setleft": "Установить новое сообщение при выходе из чата пользователей.\nИспользуй: .setleft <текст (можно использовать {name}; {chat})>; ничего." } ], "new_commands": [ { - "name": "omon", - "original_name": "omoncmd", + "name": "welcome", + "original_name": "welcomecmd", "description": { - "default": "Используй .omon <реплай на пикчу>." + "default": "Включить/выключить приветствие новых пользователей в чате. Используй: .welcome ." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "setwelcome", + "original_name": "setwelcomecmd", + "description": { + "default": "Установить приветствие новых пользователей в чате.\nИспользуй: .setwelcome <текст (можно использовать {name}; {chat})>; ничего." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "left", + "original_name": "leftcmd", + "description": { + "default": "Включить/выключить выход пользователей из чата. Используй: .left ." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "setleft", + "original_name": "setleftcmd", + "description": { + "default": "Установить новое сообщение при выходе из чата пользователей.\nИспользуй: .setleft <текст (можно использовать {name}; {chat})>; ничего." }, "cmd_names": {}, "aliases": [], @@ -2967,15 +2366,15 @@ ], "inline_handlers": [], "strings": { - "name": "FreeOmonBot" + "name": "Welcome & Left" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/reverse.py": { - "name": "ReverseMod", - "description": "Реверс текста.", + "SekaiYoneya/Friendly-telegram/RenameCont.py": { + "name": "RenameMod", + "description": "Переиминовать или добавить в контакт.", "cls_doc": {}, "meta": { "pic": null, @@ -2984,15 +2383,15 @@ }, "commands": [ { - "rev": "Используй .rev <текст или реплай>." + "rename": "" } ], "new_commands": [ { - "name": "rev", - "original_name": "revcmd", + "name": "rename", + "original_name": "renamecmd", "description": { - "default": "Используй .rev <текст или реплай>." + "default": "" }, "cmd_names": {}, "aliases": [], @@ -3004,15 +2403,15 @@ ], "inline_handlers": [], "strings": { - "name": "Reverse" + "name": "Rename" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/uploader.py": { - "name": "UploaderMod", - "description": "Загрузчик на fl1yd.ml", + "SekaiYoneya/Friendly-telegram/Pinger.py": { + "name": "PingerMod", + "description": "более точный пинг", "cls_doc": {}, "meta": { "pic": null, @@ -3021,31 +2420,15 @@ }, "commands": [ { - "mul": "Загрузить модуль на сервер." - }, - { - "ful": "Загрузить файл на сервер." + "ping": "пингует" } ], "new_commands": [ { - "name": "mul", - "original_name": "mulcmd", + "name": "ping", + "original_name": "pingcmd", "description": { - "default": "Загрузить модуль на сервер." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ful", - "original_name": "fulcmd", - "description": { - "default": "Загрузить файл на сервер." + "default": "пингует" }, "cmd_names": {}, "aliases": [], @@ -3057,15 +2440,15 @@ ], "inline_handlers": [], "strings": { - "name": "Uploader" + "name": "Pinger" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/kick_random.py": { - "name": "KickRandomMod", - "description": "Кик рандом.", + "SekaiYoneya/Friendly-telegram/SpamBot.py": { + "name": "SpamBotMod", + "description": "Показывает ваши ограничения.", "cls_doc": {}, "meta": { "pic": null, @@ -3074,15 +2457,111 @@ }, "commands": [ { - "kickrand": "Используй .kickrand, чтобы кикнуть случайного пользователя (может кикнуть вас)." + "spambot": "Смотреть статус ограничений." + }, + { + "thankbot": "Написать 'хорошо, спасибо', когда есть инлайн." + }, + { + "okbot": "Написать 'Ок', когда есть инлайн." + }, + { + "whatbot": "Спросить, почему на Вас могли жаловаться, когда есть инлайн." + }, + { + "plsbot": "Попросить снять Вам ограничения, когда есть инлайн." + }, + { + "ponspsbot": "Написать 'Понятно, спасибо', когда есть инлайн." + }, + { + "infobot": "Узнать больше о спаме, когда есть инлайн." } ], "new_commands": [ { - "name": "kickrand", - "original_name": "kickrandcmd", + "name": "spambot", + "original_name": "spambotcmd", "description": { - "default": "Используй .kickrand, чтобы кикнуть случайного пользователя (может кикнуть вас)." + "default": "Смотреть статус ограничений." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "thankbot", + "original_name": "thankbotcmd", + "description": { + "default": "Написать 'хорошо, спасибо', когда есть инлайн." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "okbot", + "original_name": "okbotcmd", + "description": { + "default": "Написать 'Ок', когда есть инлайн." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "whatbot", + "original_name": "whatbotcmd", + "description": { + "default": "Спросить, почему на Вас могли жаловаться, когда есть инлайн." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "plsbot", + "original_name": "plsbotcmd", + "description": { + "default": "Попросить снять Вам ограничения, когда есть инлайн." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "ponspsbot", + "original_name": "ponspsbotcmd", + "description": { + "default": "Написать 'Понятно, спасибо', когда есть инлайн." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "infobot", + "original_name": "infobotcmd", + "description": { + "default": "Узнать больше о спаме, когда есть инлайн." }, "cmd_names": {}, "aliases": [], @@ -3094,15 +2573,15 @@ ], "inline_handlers": [], "strings": { - "name": "KickRandom" + "name": "SpamBot" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/myhelp.py": { - "name": "HelpMod", - "description": "Описание этого модуля.", + "SekaiYoneya/Friendly-telegram/Leo.py": { + "name": "leomatchMod", + "description": "Леонардо Дайвинчик", "cls_doc": {}, "meta": { "pic": null, @@ -3111,18 +2590,27 @@ }, "commands": [ { - "help": ".help <название модуля>." + "diz": "Дизлайкнуть пользователь." }, { - "support": "Вступить в канал авторских модулей." + "like": "Лайкнуть пользователь." + }, + { + "spack": "Не нужен мне ваш стикерпак." + }, + { + "tt": "Не нужен мне ваш тик ток." + }, + { + "unafk": "Выйти из АФК и смотреть анкеты." } ], "new_commands": [ { - "name": "help", - "original_name": "helpcmd", + "name": "diz", + "original_name": "dizcmd", "description": { - "default": ".help <название модуля>." + "default": "Дизлайкнуть пользователь." }, "cmd_names": {}, "aliases": [], @@ -3132,10 +2620,49 @@ "decorators": [] }, { - "name": "support", - "original_name": "supportcmd", + "name": "like", + "original_name": "likecmd", "description": { - "default": "Вступить в канал авторских модулей." + "default": "Лайкнуть пользователь." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "spack", + "original_name": "spackcmd", + "description": { + "default": "Не нужен мне ваш стикерпак." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "tt", + "original_name": "ttcmd", + "description": { + "default": "Не нужен мне ваш тик ток." + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "unafk", + "original_name": "unafkcmd", + "description": { + "default": "Выйти из АФК и смотреть анкеты." }, "cmd_names": {}, "aliases": [], @@ -3147,25 +2674,15 @@ ], "inline_handlers": [], "strings": { - "name": "CustomHelp", - "bad_module": "Указано неверное название модуля.", - "single_mod_header": "Справка к {}:\n", - "single_cmd": "\n➜ {}\n ╰", - "undoc_cmd": "Для этой команды нет описания.\n", - "all_header": "Список из {} доступных модулей:\n", - "mod_tmpl": "\n➜ {}", - "first_cmd_tmpl": ": {}", - "cmd_tmpl": ", {}", - "joined": "Уже вступил в канал авторских модулей", - "join": "Вступить в канал авторских модулей" + "name": "Leo" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/k&ktext.py": { - "name": "KKTextMod", - "description": "K&K Text by @ktxtBot", + "SekaiYoneya/Friendly-telegram/Quotes.py": { + "name": "QuotesMod", + "description": "Quotes a message", "cls_doc": {}, "meta": { "pic": null, @@ -3174,15 +2691,47 @@ }, "commands": [ { - "kkt": "Используйте .kkt <текст или реплай>." + "mquote": "" + }, + { + "quote": ".quote - quote a message" + }, + { + "fquote": ".fquote @ or - fake quote" } ], "new_commands": [ { - "name": "kkt", - "original_name": "kktcmd", + "name": "mquote", + "original_name": "mquotecmd", "description": { - "default": "Используйте .kkt <текст или реплай>." + "default": "" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "quote", + "original_name": "quotecmd", + "description": { + "default": ".quote - quote a message" + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "fquote", + "original_name": "fquotecmd", + "description": { + "default": ".fquote @ or - fake quote" }, "cmd_names": {}, "aliases": [], @@ -3194,15 +2743,59 @@ ], "inline_handlers": [], "strings": { - "name": "K&K Text" + "name": "Quotes", + "silent_processing_cfg_doc": "Process quote silently(mostly w/o editing)", + "module_endpoint_cfg_doc": "Module endpoint URL", + "quote_limit_cfg_doc": "Limit for messages per quote", + "max_width_cfg_doc": "Maximum quote width in pixels", + "scale_factor_cfg_doc": "Quote quality (up to 5.5)", + "square_avatar_cfg_doc": "Square avatar in quote", + "text_color_cfg_doc": "Color of text in quote", + "reply_line_color_cfg_doc": "Reply line color", + "reply_thumb_radius_cfg_doc": "Reply media thumbnail radius in pixels", + "admintitle_color_cfg_doc": "Admin title color", + "message_radius_cfg_doc": "Message radius in px", + "picture_radius_cfg_doc": "Media picture radius in px", + "background_color_cfg_doc": "Quote background color", + "quote_limit_reached": "The maximum number of messages in multiquote - {}.", + "fq_incorrect_args": "Args incorrect. \"@$username (ID)$text\" or \"$reply $text\"", + "updating": "Updating...", + "update_error": "Update error", + "processing": "Processing...", + "unreachable_error": "API Host is unreachable now. Please try again later.", + "server_error": "API Error occured :)", + "no_reply": "You didn't reply to a message.", + "creator": "Owner", + "admin": "Admin", + "channel": "Channel", + "media_type_photo": "Photo", + "media_type_video": "📹Video", + "media_type_videomessage": "📹Video message", + "media_type_voice": "🎵Voice message", + "media_type_audio": "🎧Music: {} - {}", + "media_type_contact": "👤Contact: {}", + "media_type_poll": "📊Poll: ", + "media_type_quiz": "📊Quiz: ", + "media_type_location": "📍Location", + "media_type_gif": "🖼GIF", + "media_type_sticker": "Sticker", + "media_type_file": "💾File", + "dice_type_dice": "Dice", + "dice_type_dart": "Dart", + "ball_thrown": "Ball thrown", + "ball_kicked": "Ball kicked", + "dart_thrown": "Dart thrown", + "dart_almostthere": "almost there!", + "dart_missed": "missed!", + "dart_bullseye": "bullseye!" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/zapomni_zabud_sogl.py": { - "name": "ZapomniZabudSoglMod", - "description": "Запомните;забудьте твари, согласен.", + "SekaiYoneya/Friendly-telegram/Pic.py": { + "name": "PicPhotosMod", + "description": "Фотографии из @pic.", "cls_doc": {}, "meta": { "pic": null, @@ -3211,47 +2804,15 @@ }, "commands": [ { - "zap": ".zap <текст или реплай>" - }, - { - "zab": ".zab <текст или реплай>" - }, - { - "sogl": ".sogl <текст или реплай>" + "gow": "" } ], "new_commands": [ { - "name": "zap", - "original_name": "zapcmd", + "name": "gow", + "original_name": "gowcmd", "description": { - "default": ".zap <текст или реплай>" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "zab", - "original_name": "zabcmd", - "description": { - "default": ".zab <текст или реплай>" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "sogl", - "original_name": "soglcmd", - "description": { - "default": ".sogl <текст или реплай>" + "default": "" }, "cmd_names": {}, "aliases": [], @@ -3263,15 +2824,15 @@ ], "inline_handlers": [], "strings": { - "name": "Запомните;забудьте твари, согласен" + "name": "Pic" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/whois.py": { + "SekaiYoneya/Friendly-telegram/Whois.py": { "name": "WhoIsMod", - "description": "Получает информацию о пользователе в Телеграме (включая вас!).", + "description": "Получает информацию о пользователе.", "cls_doc": {}, "meta": { "pic": null, @@ -3300,15 +2861,15 @@ ], "inline_handlers": [], "strings": { - "name": "WhoIs" + "name": "Whois" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/urlshortener.py": { - "name": "URLShortenerMod", - "description": "Сократитель ссылок", + "SekaiYoneya/Friendly-telegram/AudioConverter.py": { + "name": "AudioConverterMod", + "description": "Конвертирование в разные форматы", "cls_doc": {}, "meta": { "pic": null, @@ -3317,15 +2878,31 @@ }, "commands": [ { - "lgt": "Сократить ссылку с помощью сервиса verylegit.link" + "tovoice": ".tovoice \nСконвертировать аудио в войс " + }, + { + "toformat": ".toformat [format] \n   Сконвертировать аудио/видео/войс в нужный формат \nПоддерживаются mp3, m4a, ogg, mpeg, wav, oga " } ], "new_commands": [ { - "name": "lgt", - "original_name": "lgtcmd", + "name": "tovoice", + "original_name": "tovoicecmd", "description": { - "default": "Сократить ссылку с помощью сервиса verylegit.link" + "default": ".tovoice \nСконвертировать аудио в войс " + }, + "cmd_names": {}, + "aliases": [], + "usage": null, + "inline": false, + "is_inline_handler": false, + "decorators": [] + }, + { + "name": "toformat", + "original_name": "toformatcmd", + "description": { + "default": ".toformat [format] \n   Сконвертировать аудио/видео/войс в нужный формат \nПоддерживаются mp3, m4a, ogg, mpeg, wav, oga " }, "cmd_names": {}, "aliases": [], @@ -3337,15 +2914,15 @@ ], "inline_handlers": [], "strings": { - "name": "URLShortener" + "name": "AudioConverter" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/hearts.py": { - "name": "HeartsMod", - "description": "", + "SekaiYoneya/Friendly-telegram/ChatStats.py": { + "name": "ChatStatisticMod", + "description": "Статистика чата", "cls_doc": {}, "meta": { "pic": null, @@ -3354,29 +2931,13 @@ }, "commands": [ { - "lhearts": "" - }, - { - "shearts": "" + "stata": "" } ], "new_commands": [ { - "name": "lhearts", - "original_name": "lheartscmd", - "description": { - "default": "" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "shearts", - "original_name": "sheartscmd", + "name": "stata", + "original_name": "statacmd", "description": { "default": "" }, @@ -3390,555 +2951,15 @@ ], "inline_handlers": [], "strings": { - "name": "Heart's" + "name": "ChatStatistic" }, "has_on_load": false, "has_on_unload": false, "class_cmd_names": {} }, - "Fl1yd/FTG-Modules/vsratomemes.py": { - "name": "VsratoMemesMod", - "description": "Всратые мемы.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "wolf": "\"Используй .wolf." - }, - { - "vsrato": "Используй .vsrato <реплай на пикчу>." - } - ], - "new_commands": [ - { - "name": "wolf", - "original_name": "wolfcmd", - "description": { - "default": "\"Используй .wolf." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "vsrato", - "original_name": "vsratocmd", - "description": { - "default": "Используй .vsrato <реплай на пикчу>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Всратые мемы" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/catchargs.py": { - "name": "PicMod", - "description": "Случайный картинка по аргументам из @pic.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "pic": "" - } - ], - "new_commands": [ - { - "name": "pic", - "original_name": "piccmd", - "description": { - "default": "" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Pic" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/weather.py": { - "name": "WeatherMod", - "description": "Погода с сайта wttr.in", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "pw": "\"Кидает погоду картинкой.\nИспользование: .pw <город>; ничего." - }, - { - "aw": "Кидает погоду ascii-артом.\nИспользование: .aw <город>; ничего." - } - ], - "new_commands": [ - { - "name": "pw", - "original_name": "pwcmd", - "description": { - "default": "\"Кидает погоду картинкой.\nИспользование: .pw <город>; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "aw", - "original_name": "awcmd", - "description": { - "default": "Кидает погоду ascii-артом.\nИспользование: .aw <город>; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Weather" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/userdata.py": { - "name": "UserDataMod", - "description": "Модуль может изменить ваши данные в Telegram", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "name": "Команда .name изменит ваше имя." - }, - { - "bio": "Команда .bio изменит ваше био." - }, - { - "username": "Команда .username изменит ваше био." - } - ], - "new_commands": [ - { - "name": "name", - "original_name": "namecmd", - "description": { - "default": "Команда .name изменит ваше имя." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bio", - "original_name": "biocmd", - "description": { - "default": "Команда .bio изменит ваше био." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "username", - "original_name": "usernamecmd", - "description": { - "default": "Команда .username изменит ваше био." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "UserData" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/searchmodules.py": { - "name": "SearchMod", - "description": "Поиск контента на канале @ftgmodulesbyfl1yd", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "search": "Используй .search <название>" - } - ], - "new_commands": [ - { - "name": "search", - "original_name": "searchcmd", - "description": { - "default": "Используй .search <название>" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "SearchModules" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/admin.py": { - "name": "AdminToolsMod", - "description": "Администрирование чата", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "ecp": "Команда .ecp изменяет картинку чата.\nИспользование: .ecp <реплай на картинку/стикер>." - }, - { - "promote": "Команда .promote повышает пользователя в правах администратора.\nИспользование: .promote <@ или реплай> <ранг>." - }, - { - "demote": "Команда .demote понижает пользователя в правах администратора.\nИспользование: .demote <@ или реплай>." - }, - { - "pin": "Команда .pin закрепляет сообщение в чате.\nИспользование: .pin <реплай>." - }, - { - "unpin": "Команда .unpin открепляет закрепленное сообщение в чате.\nИспользование: .unpin." - }, - { - "kick": "Команда .kick кикает пользователя.\nИспользование: .kick <@ или реплай>." - }, - { - "ban": "Команда .ban даёт бан пользователю.\nИспользование: .ban <@ или реплай>." - }, - { - "unban": "Команда .unban для разбана пользователя.\nИспользование: .unban <@ или реплай>." - }, - { - "mute": "Команда .mute даёт мут пользователю.\nИспользование: .mute <@ или реплай> <время (1m, 1h, 1d)>." - }, - { - "unmute": "Команда .unmute для размута пользователя.\nИспользование: .unmute <@ или реплай>." - }, - { - "delallmsgs": "Команда .delallmsgs удаляет все сообщения от пользователя.\nИспользование: .delallmsgs <@ или реплай>." - }, - { - "delusers": "Команда .delusers показывает список всех удалённых аккаунтов в чате.\nИспользование: .delusers ." - } - ], - "new_commands": [ - { - "name": "ecp", - "original_name": "ecpcmd", - "description": { - "default": "Команда .ecp изменяет картинку чата.\nИспользование: .ecp <реплай на картинку/стикер>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "promote", - "original_name": "promotecmd", - "description": { - "default": "Команда .promote повышает пользователя в правах администратора.\nИспользование: .promote <@ или реплай> <ранг>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "demote", - "original_name": "demotecmd", - "description": { - "default": "Команда .demote понижает пользователя в правах администратора.\nИспользование: .demote <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "pin", - "original_name": "pincmd", - "description": { - "default": "Команда .pin закрепляет сообщение в чате.\nИспользование: .pin <реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "unpin", - "original_name": "unpincmd", - "description": { - "default": "Команда .unpin открепляет закрепленное сообщение в чате.\nИспользование: .unpin." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "kick", - "original_name": "kickcmd", - "description": { - "default": "Команда .kick кикает пользователя.\nИспользование: .kick <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ban", - "original_name": "bancmd", - "description": { - "default": "Команда .ban даёт бан пользователю.\nИспользование: .ban <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "unban", - "original_name": "unbancmd", - "description": { - "default": "Команда .unban для разбана пользователя.\nИспользование: .unban <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "mute", - "original_name": "mutecmd", - "description": { - "default": "Команда .mute даёт мут пользователю.\nИспользование: .mute <@ или реплай> <время (1m, 1h, 1d)>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "unmute", - "original_name": "unmutecmd", - "description": { - "default": "Команда .unmute для размута пользователя.\nИспользование: .unmute <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "delallmsgs", - "original_name": "delallmsgscmd", - "description": { - "default": "Команда .delallmsgs удаляет все сообщения от пользователя.\nИспользование: .delallmsgs <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "delusers", - "original_name": "deluserscmd", - "description": { - "default": "Команда .delusers показывает список всех удалённых аккаунтов в чате.\nИспользование: .delusers ." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AdminTools", - "no_reply": "Нет реплая.", - "not_pic": "Это не картинка/стикер", - "wait": "Минуточку...", - "pic_so_small": "Картинка слишком маленькая, попробуйте другую.", - "pic_changed": "Картинка чата изменена.", - "promote_none": "Некого повышать.", - "who": "Кто это?", - "not_admin": "Я здесь не админ.", - "promoted": "{} повышен в правах администратора.\nРанг: {}", - "wtf_is_it": "Что это?", - "this_isn`t_a_chat": "Это не чат!", - "demote_none": "Некого понижать.", - "demoted": "{} понижен в правах администратора.", - "pinning": "Пин...", - "pin_none": "Ответь на сообщение чтобы закрепить его.", - "unpinning": "Анпин...", - "unpin_none": "Нечего откреплять.", - "no_rights": "У меня нет прав.", - "pinned": "Закреплено успешно!", - "unpinned": "Откреплено успешно!", - "can`t_kick": "Не могу кикнуть пользователя.", - "kicking": "Кик...", - "kick_none": "Некого кикать.", - "kicked": "{} кикнут из чата.", - "kicked_for_reason": "{} кикнут из чата.\nПричина: {}.", - "banning": "Бан...", - "banned": "{} забанен в чате.", - "banned_for_reason": "{} забанен в чате.\nПричина: {}", - "ban_none": "Некому давать бан.", - "unban_none": "Некого разбанить.", - "unbanned": "{} разбанен в чате.", - "mute_none": "Некому давать мут.", - "muted": "{} теперь в муте на ", - "no_args": "Неверно указаны аргументы.", - "unmute_none": "Некого размутить.", - "unmuted": "{} теперь не в муте.", - "deleting": "Удаление...", - "no_args_or_reply": "Нет аргументов или реплая.", - "deleted": "Все сообщения от {} удалены.", - "del_u_search": "Поиск удалённых аккаунтов...", - "del_u_kicking": "Кик удалённых аккаунтов...\nОх~, я могу это сделать?!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/don`t_work.py": { - "name": "DontWorkMod", - "description": "Модуль не работает.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "dontwork": "Используй .dontwork, чтобы понять, что модуль не работает." - } - ], - "new_commands": [ - { - "name": "dontwork", - "original_name": "dontworkcmd", - "description": { - "default": "Используй .dontwork, чтобы понять, что модуль не работает." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Don`t Work" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/chat.py": { + "SekaiYoneya/Friendly-telegram/ChatModule.py": { "name": "ChatMod", - "description": "Чат модуль", + "description": "Чат модули", "cls_doc": {}, "meta": { "pic": null, @@ -3956,9131 +2977,7 @@ "invite": "Используйте .invite <@ или реплай>, чтобы добавить пользователя в чат." }, { - "kickme": "Используйте команду .kickme, чтобы кикнуть себя из чата." - }, - { - "users": "Команда .users <имя> выводит список всех пользователей в чате." - }, - { - "admins": "Команда .admins показывает список всех админов в чате." - }, - { - "bots": "Команда .bots показывает список всех ботов в чате." - } - ], - "new_commands": [ - { - "name": "userid", - "original_name": "useridcmd", - "description": { - "default": "Команда .userid <@ или реплай> показывает ID выбранного пользователя." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "chatid", - "original_name": "chatidcmd", - "description": { - "default": "Команда .chatid показывает ID чата." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "invite", - "original_name": "invitecmd", - "description": { - "default": "Используйте .invite <@ или реплай>, чтобы добавить пользователя в чат." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "kickme", - "original_name": "kickmecmd", - "description": { - "default": "Используйте команду .kickme, чтобы кикнуть себя из чата." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "users", - "original_name": "userscmd", - "description": { - "default": "Команда .users <имя> выводит список всех пользователей в чате." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "admins", - "original_name": "adminscmd", - "description": { - "default": "Команда .admins показывает список всех админов в чате." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bots", - "original_name": "botscmd", - "description": { - "default": "Команда .bots показывает список всех ботов в чате." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "ChatModule" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "Fl1yd/FTG-Modules/arts.py": { - "name": "ArtsMod", - "description": "Юникод арты", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "vjuh": "Используй .vjuh <текст>." - }, - { - "cowsay": "Используй .cowsay <текст>." - }, - { - "padayu": "Используй .padayu <текст>; ничего." - }, - { - "priletel": "Используй .prilitel <текст>; ничего." - }, - { - "huytebe": "Используй .huytebe <текст>; ничего." - }, - { - "lol": "Используй .lol." - }, - { - "fuckyou": "Используй .fuckyou." - }, - { - "house": "Используй .house." - }, - { - "hello": "Используй .hello." - }, - { - "coffee": "Используй .coffee <текст>; ничего." - }, - { - "tv": "Используй .tv <текст>; ничего." - }, - { - "gren": "Используй .gren <текст>; ничего." - }, - { - "bruh": "Используй .bruh." - }, - { - "uno": "Используй .uno." - }, - { - "huy": "Используй .huy ; ничего." - }, - { - "imps": "Используй .imps <@ или реплай>." - }, - { - "f": "Используй .f" - } - ], - "new_commands": [ - { - "name": "vjuh", - "original_name": "vjuhcmd", - "description": { - "default": "Используй .vjuh <текст>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "cowsay", - "original_name": "cowsaycmd", - "description": { - "default": "Используй .cowsay <текст>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "padayu", - "original_name": "padayucmd", - "description": { - "default": "Используй .padayu <текст>; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "priletel", - "original_name": "priletelcmd", - "description": { - "default": "Используй .prilitel <текст>; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "huytebe", - "original_name": "huytebecmd", - "description": { - "default": "Используй .huytebe <текст>; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "lol", - "original_name": "lolcmd", - "description": { - "default": "Используй .lol." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "fuckyou", - "original_name": "fuckyoucmd", - "description": { - "default": "Используй .fuckyou." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "house", - "original_name": "housecmd", - "description": { - "default": "Используй .house." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "hello", - "original_name": "hellocmd", - "description": { - "default": "Используй .hello." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "coffee", - "original_name": "coffeecmd", - "description": { - "default": "Используй .coffee <текст>; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tv", - "original_name": "tvcmd", - "description": { - "default": "Используй .tv <текст>; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "gren", - "original_name": "grencmd", - "description": { - "default": "Используй .gren <текст>; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bruh", - "original_name": "bruhcmd", - "description": { - "default": "Используй .bruh." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "uno", - "original_name": "unocmd", - "description": { - "default": "Используй .uno." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "huy", - "original_name": "huycmd", - "description": { - "default": "Используй .huy ; ничего." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "imps", - "original_name": "impscmd", - "description": { - "default": "Используй .imps <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "f", - "original_name": "fcmd", - "description": { - "default": "Используй .f" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Arts" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/silentmessages.py": { - "name": "SilentMessages", - "description": "With this module you won't miss important messages sent without sound!", - "cls_doc": { - "ru": "С этим модулем вы не пропустите важные сообщения, отправленные без звука!" - }, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/sqlmerr/blob/main/assets/hikka_mods/sqlmerrmodules_silentmessages.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/silentmessages.png?raw=true" - }, - "commands": [ - { - "silentmessages": "toggle module status | (RU) включить/выключить модуль" - } - ], - "new_commands": [ - { - "name": "silentmessages", - "original_name": "silentmessages", - "description": { - "default": "toggle module status", - "ru": "включить/выключить модуль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "SilentMessages", - "_cfg_chats": "Chats in which the module will monitor messages without sound", - "_cfg_status": "Is the module working or not?", - "_cfg_text": "The text that will be sent by your inline bot when a silent message is received", - "enabled": "enabled", - "disabled": "disabled", - "toggle_message": "🔖 Module {}!", - "_cfg_chats_ru": "Чаты, в которых модуль будет следить за сообщениями без звука", - "_cfg_status_ru": "Работает ли модуль или нет", - "_cfg_text_ru": "Текст, который будет отправлен вашим инлайн ботом, когда будет получено сообщение без звука", - "enabled_ru": "включен", - "disabled_ru": "выключен", - "toggle_message_ru": "🔖 Модуль {}!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/currencyconverter.py": { - "name": "CurrencyConverter", - "description": "Module for converting a large number of currencies to other currencies", - "cls_doc": { - "ru": "Модуль для конвертации большого количества валют в другие валюты" - }, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/banners/currencyconverter.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/currencyconverter.png?raw=true" - }, - "commands": [ - { - "cconvert": "[from] [to] Convert currency to other currency | (RU) [from] [to] Конвертировать одну валюту в другую" - } - ], - "new_commands": [ - { - "name": "cconvert", - "original_name": "cconvert", - "description": { - "default": "[from] [to] Convert currency to other currency", - "ru": "[from] [to] Конвертировать одну валюту в другую" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Currency Converter", - "msg": "💲 Convert\n{from_} / {to} {price}", - "no_args": " No args!", - "args_too_short": " Args are too short!", - "not_found": " Currency not found!", - "_cfg_autoupdate": "Auto update message", - "_cfg_update_delay": "Message auto update delay. In hours", - "msg_ru": "💲 Конвертация\n{from_} / {to} {price}", - "no_args_ru": " Вы не передали аргументы!", - "args_too_short_ru": " Слишком короткие аргументы!", - "not_found_ru": " Валюта не найдена!", - "_cfg_autoupdate_ru": "Автообновление сообщения", - "_cfg_update_delay_ru": "Кд автообновления сообщения. В часах" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/quicktools.py": { - "name": "QuickTools", - "description": "Module with various quick and useful tools", - "cls_doc": { - "ru": "Модуль с разными быстрыми и полезными инструментами" - }, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/sqlmerr/blob/main/assets/hikka_mods/quicktools.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/quicktools.png?raw=true" - }, - "commands": [ - { - "id": " Get user/chat/sender/replied message/message ID | (RU) <реплай на сообщение> Получить айди пользователя/чата/отправителя/сообщения" - }, - { - "text": " Get replied message text | (RU) <реплай на сообщение> Получить текст сообщения" - }, - { - "replymarkup": " Get replied message reply markup (buttons) | (RU) <реплай на сообщение> Получить кнопки сообщения" - }, - { - "entitylink": " - creates link to entity (chat/user)" - } - ], - "new_commands": [ - { - "name": "id", - "original_name": "id", - "description": { - "default": " Get user/chat/sender/replied message/message ID", - "ru": "<реплай на сообщение> Получить айди пользователя/чата/отправителя/сообщения" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "text", - "original_name": "text", - "description": { - "default": " Get replied message text", - "ru": "<реплай на сообщение> Получить текст сообщения" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "replymarkup", - "original_name": "reply_markup", - "description": { - "default": " Get replied message reply markup (buttons)", - "ru": "<реплай на сообщение> Получить кнопки сообщения" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "entitylink", - "original_name": "entity_link", - "description": { - "default": " - creates link to entity (chat/user)" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "QuickTools", - "id_cmd_text": "🆔 Id\n· 🫵 Your id: {}\n· 💬 Chat id: {}\n· 🎈 User id: {}\n· 💬 Replied Message id: {}\n", - "reply_markup_cmd_text": "📌 Buttons:\n{}", - "entity_link_cmd_text": "🔗 Your link: {}", - "empty": "Empty", - "no_reply": " No reply!", - "no_args": " No args!", - "no_reply_markup": " No reply markup!", - "id_cmd_text_ru": "🆔 Айди\n· 🫵 Твой айди: {}\n· 💬 Айди чата: {}\n· 🎈 Айди пользователя: {}\n· 💬 Айди ответного сообщения: {}\n", - "reply_markup_cmd_text_ru": "📌 Кнопки:\n{}", - "entity_link_cmd_text_ru": "🔗 Ваша ссылка: {}", - "empty_ru": "Отсутствует", - "no_reply_ru": " Вы не ответили на сообщение!", - "no_args_ru": " Вы не передали аргументы!", - "no_reply_markup_ru": " Вы ответили на сообщение, где нет кнопок!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/autoformatter.py": { - "name": "AutoFormatter", - "description": "Automatically formats the text of your messages | Check The Config", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/sqlmerr/blob/main/assets/hikka_mods/sqlmerrmodules_autoformatter.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/autoformatter.png?raw=true" - }, - "commands": [ - { - "textformat": "Turn on/off The Module | (RU) Включить/выключить модуль" - } - ], - "new_commands": [ - { - "name": "textformat", - "original_name": "textformat", - "description": { - "default": "Turn on/off The Module", - "ru": "Включить/выключить модуль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AutoFormatter", - "status": "Module enabled or disabled", - "format": "Text format. Where {} is the original message text", - "type": "Formatting Type", - "exceptions": "This is exceptions, this text is not formated", - "disabled": "Module is now disabled", - "enabled": "Module is now enabled", - "status_ru": "Включен или выключен модуль", - "format_ru": "Формат текста. Где {} это исходный текст сообщения", - "type_ru": "Тип форматирования", - "exceptions_ru": "Это исключения, этот текст не будет форматироваться", - "disabled_ru": "Модуль сейчас выключен", - "enabled_ru": "Модуль сейчас включен" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/random_emoji.py": { - "name": "RandomEmoji", - "description": "Just random emojis", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/sqlmerr/blob/main/assets/hikka_mods/sqlmerrmodules_randomemoji.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/random_emoji.png?raw=true" - }, - "commands": [ - { - "randomemoji": "Random emoji" - } - ], - "new_commands": [ - { - "name": "randomemoji", - "original_name": "random_emoji", - "description": { - "default": "Random emoji" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "RandomEmoji" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/inlinetimer.py": { - "name": "InlineTimer", - "description": "Описание нашего модуля", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/sqlmerr/blob/main/assets/hikka_mods/sqlmerrmodules_inlinetimer.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/inlinetimer.png?raw=true" - }, - "commands": [ - { - "timer": "Send timer | (RU) отправить таймер" - } - ], - "new_commands": [ - { - "name": "timer", - "original_name": "timer", - "description": { - "default": "Send timer", - "ru": "отправить таймер" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "InlineTimer", - "text": "⏲ Inline timer\n⏰ Current time: {} seconds", - "successful": "Great, in {} seconds the inline bot will send you a message via PM", - "timer_created": "Timer created!", - "text_cfg": "The text that your inline bot will send when the timer expires", - "below_zero": "Time cannot be below zero", - "text_ru": "⏲ Inline timer\n⏰ Текущее время: {} секунд", - "successful_ru": "Отлично, через {} секунд инлайн бот отправит вам сообщение в лс", - "timer_created_ru": "Таймер создан!", - "text_cfg_ru": "Текст, который будет писать ваш инлайн бот по истечению времени таймера", - "below_zero_ru": "Время не может быть меньше нуля" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/animatedprofile.py": { - "name": "AnimatedProfile", - "description": "Module for your profile animation (name, bio) look in the config", - "cls_doc": { - "ru": "Модуль для анимации вашего профиля (имя, био) смотрите конфиг" - }, - "meta": { - "pic": null, - "banner": null, - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/animatedprofile.png?raw=true" - }, - "commands": [ - { - "animatedname": "(aname) Turn on name animation | (RU) (aname) Включить анимацию имени" - }, - { - "animatedbio": "(abio) Turn on bio animation | (RU) (abio) Включить анимацию био" - }, - { - "stopanimatedname": "(stopaname) Turn off name animation | (RU) (stopaname) Выключить анимацию имени" - }, - { - "stopanimatedbio": "(stopabio) Turn off bio animation | (RU) (stopabio) Выключить анимацию био" - } - ], - "new_commands": [ - { - "name": "animatedname", - "original_name": "animatedname", - "description": { - "default": "(aname) Turn on name animation", - "ru": "(aname) Включить анимацию имени" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "animatedbio", - "original_name": "animatedbio", - "description": { - "default": "(abio) Turn on bio animation", - "ru": "(abio) Включить анимацию био" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "stopanimatedname", - "original_name": "stopanimatedname", - "description": { - "default": "(stopaname) Turn off name animation", - "ru": "(stopaname) Выключить анимацию имени" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "stopanimatedbio", - "original_name": "stopanimatedbio", - "description": { - "default": "(stopabio) Turn off bio animation", - "ru": "(stopabio) Выключить анимацию био" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AnimatedProfile", - "name_delay": "Time between frames of name animation", - "animated_name_frames": "Name animation frames", - "not_name_frames": "⚠️ See the config! In the animated_name_frames parameter, put your animation frames by name", - "name_is_enabled": "⚠️ Name animation is already enabled, use .astopname to turn it off.", - "name_is_disabled": "⚠️ Name animation is already turned off.", - "name_turned_off": "⚠️ Name animation is disabled.", - "bio_status": "Is the bio animation enabled or not", - "bio_delay": "Time between frames of bio animation", - "animated_bio_frames": "Bio animation frames", - "not_bio_frames": "⚠️ See the config! In the animated_bio_frames parameter, put your animation frames bio", - "bio_is_enabled": "⚠️ Bio animation is already enabled, use .astopname to turn it off.", - "bio_is_disabled": "⚠️ Bio animation is already turned off.", - "bio_turned_off": "⚠️ Bio animation is disabled.", - "name_delay_ru": "Время между кадрами анимации имени", - "animated_name_frames_ru": "Кадры анимации имени", - "not_name_frames_ru": "⚠️ Смотрите конфиг! В параметре animated_name_frames, поставьте ваши кадры анимации имени", - "name_is_enabled_ru": "⚠️ Анимация имени уже включена, используйте .stopaname, чтобы выключить.", - "name_is_disabled_ru": "⚠️ Анимация имени уже выключена.", - "name_turned_off_ru": "⚠️ Анимация имени выключена.", - "bio_status_ru": "Включена ли анимация био или нет", - "bio_delay_ru": "Время между кадрами анимации био", - "animated_bio_frames_ru": "Кадры анимации био", - "not_bio_frames_ru": "⚠️ Смотрите конфиг! В параметре animated_bio_frames, поставьте ваши кадры анимации био", - "bio_is_enabled_ru": "⚠️ Анимация био уже включена, используйте .stopabio, чтобы выключить.", - "bio_is_disabled_ru": "⚠️ Анимация био уже выключена.", - "bio_turned_off_ru": "⚠️ Анимация био выключена." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/upgradedeval.py": { - "name": "UpgradedEval", - "description": "Just eval with customizable text and stdout", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/banners/upgradedeval.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/upgradedeval.png?raw=true" - }, - "commands": [ - { - "ehistory": "Get history (since userbot restart) | (RU) Получить историю (с рестарта юзербота)" - }, - { - "ie": "Upgraded eval | (RU) Улучшенный eval" - }, - { - "erust": "Evaluate Rust code | (RU) Запустить код на Rust" - }, - { - "ego": "Evaluate Go code | (RU) Запустить код на Go" - }, - { - "ekt": "Evaluate Kotlin code | (RU) Запустить код на Kotlin" - } - ], - "new_commands": [ - { - "name": "ehistory", - "original_name": "ehistory", - "description": { - "default": "Get history (since userbot restart)", - "ru": "Получить историю (с рестарта юзербота)" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ie", - "original_name": "ie", - "description": { - "default": "Upgraded eval", - "ru": "Улучшенный eval" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "erust", - "original_name": "erust", - "description": { - "default": "Evaluate Rust code", - "ru": "Запустить код на Rust" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ego", - "original_name": "ego", - "description": { - "default": "Evaluate Go code", - "ru": "Запустить код на Go" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ekt", - "original_name": "ekt", - "description": { - "default": "Evaluate Kotlin code", - "ru": "Запустить код на Kotlin" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "UpgradedEval", - "_cfg_text_result": "Text for result", - "_cfg_text_error": "Text for error", - "_cfg_text_result_and_error": "Text containing both error and result", - "_cfg_mode": "Code run mode. stdout is when print works. return, this is standard .e; auto is just a mode that automatically selects stdout or return", - "_cfg_text_result_ru": "Текст результата", - "_cfg_text_error_ru": "Текст ошибки", - "_cfg_text_result_and_error_ru": "Текст содержащий и ошибку и результат", - "_cfg_mode_ru": "Режим запуска кода. stdout, это когда работает print. return, это стандартный .e; auto - это просто режим, который автоматически выбирает stdout или return" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/egsfreegames.py": { - "name": "EGSFreeGames", - "description": "Module for checking free games in Epic Games Store. Inline bot will send them every day in special chat", - "cls_doc": { - "ru": "Модуль для проверки бесплатных игр в Epic Games Store. Инлайн бот будет отправлять их каждый день в специальном чате" - }, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/banners/egsfreegames.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/egsfreegames.png?raw=true" - }, - "commands": [ - { - "egsgames": "Get free games links available in Epic Games Store | (RU) Получить бесплатные игры доступные в Epic Games Store" - } - ], - "new_commands": [ - { - "name": "egsgames", - "original_name": "egsgames", - "description": { - "default": "Get free games links available in Epic Games Store", - "ru": "Получить бесплатные игры доступные в Epic Games Store" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "EGSFreeGames", - "game": "- Game: {title}\n Status: {status}\n Promotion started at: {start}\n Promotion will end at: {end}\n Link: {url}\n", - "header": "🎮 Free games in EGS:", - "header_bot": "🎮 Today's free games in EGS:", - "footer": "ℹ️ The active status means that the game can be picked up now.\nThe upcoming status means that the game can be picked up later", - "_region_cfg": "Free games check region", - "_schedule_checking_cfg": "Will the bot automatically send the current free games to a special chat room", - "game_ru": "- Игра: {title}\n Статус: {status}\n Акция началась: {start}\n Акция закончится: {end}\n Ссылка: {url}\n", - "header_ru": "🎮 Бесплатные игры в EGS:", - "header_bot_ru": "🎮 Сегодняшние бесплатные игры в EGS:", - "footer_ru": "ℹ️ Статус active означает, что игру можно забрать уже сейчас.\nСтатус upcoming означает, что игру можно будет забрать потом.", - "_region_cfg_ru": "Регион проверки бесплатных игр", - "_schedule_checking_cfg_ru": "Будет ли бот автоматически отправлять в специальный чат текущие бесплатные игры" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/numbersfacts.py": { - "name": "NumbersFacts", - "description": "Interesting facts about numbers | Check the config", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/sqlmerr/blob/main/assets/hikka_mods/sqlmerrmodules_numberfacts.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/numberfacts.png?raw=true" - }, - "commands": [ - { - "numberfact": "[number] - get fact about number | (RU) [число] - получить факт об этом числе" - } - ], - "new_commands": [ - { - "name": "numberfact", - "original_name": "numberfact", - "description": { - "default": "[number] - get fact about number", - "ru": "[число] - получить факт об этом числе" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "NumbersFacts", - "noargs": "🚫 You didn't enter any arguments", - "indexerror": "🚫 You have not entered enough arguments", - "type": "Type of facts about numbers. Trivia is a fact from life, math is a mathematical fact, date and year is a question about a date" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/codeformat.py": { - "name": "CodeFormat", - "description": "Format your code!", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/sqlmerr/blob/main/assets/hikka_mods/sqlmerrmodules_codeformat.png?raw=true", - "developer": "@sqlmerr_m" - }, - "commands": [ - { - "code": "" - } - ], - "new_commands": [ - { - "name": "code", - "original_name": "code", - "description": { - "default": "" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "CodeFormat" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/triggers.py": { - "name": "Triggers", - "description": "Triggers watch chat messages and can do anything, reply to a message with a given text, delete a message, execute any userbot command. Overall, a very cool module", - "cls_doc": { - "ru": "Триггеры следят за сообщениями в чате и могут сделать что угодно, ответить на сообщение заданным текстом, удалить сообщение, выполнить любую команду юзербота. В общем очень крутой модуль" - }, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/banners/triggers.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/triggers.png?raw=true" - }, - "commands": [ - { - "triggeraddbase": "[text that the module will trigger on] - Add base trigger | (RU) [текст, на который будет тригеррится модуль] <реплай на текст ответа> - Добавить базовый триггер" - }, - { - "triggeradd": "[trigger] - Add a trigger from raw data | (RU) [триггер] - Добавить триггер из сырых данных" - }, - { - "triggers": "View all triggers | (RU) Посмотреть все триггеры" - }, - { - "triggerchat": "Add chat, where triggers will work | (RU) Добавить чат, где будут работать триггеры" - }, - { - "tconfig": "[optional: trigger id] - Triggers config. | (RU) [необязятельно: айди триггера] - Конфиг модуля" - }, - { - "triggerdel": "[trigger's id] - Delete trigger | (RU) [айди триггера] - Удалить триггер" - }, - { - "tcallback": "[callback_id: str] - Add a callback that trigger can execute | (RU) [айди колбека: str] <реплай на пайтон код> - Добавить колбек, который триггер сможет выполнить" - }, - { - "triggerget": "[trigger's id] - Get trigger | (RU) [айди триггера] - Получить триггер" - }, - { - "triggerset": "[trigger's id] [edited trigger] - Edit trigger | (RU) [айди триггера] [измененный триггер] - Изменить триггер" - }, - { - "triggerupdate": "[trigger's id] [path] [value] - Edit trigger | (RU) [айди триггера] [путь] [значение] - Изменить одно значение триггера" - } - ], - "new_commands": [ - { - "name": "triggeraddbase", - "original_name": "triggeraddbase", - "description": { - "default": "[text that the module will trigger on] - Add base trigger", - "ru": "[текст, на который будет тригеррится модуль] <реплай на текст ответа> - Добавить базовый триггер" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "triggeradd", - "original_name": "triggeradd", - "description": { - "default": "[trigger] - Add a trigger from raw data", - "ru": "[триггер] - Добавить триггер из сырых данных" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "triggers", - "original_name": "triggers", - "description": { - "default": "View all triggers", - "ru": "Посмотреть все триггеры" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "triggerchat", - "original_name": "triggerchat", - "description": { - "default": "Add chat, where triggers will work", - "ru": "Добавить чат, где будут работать триггеры" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tconfig", - "original_name": "tconfig", - "description": { - "default": "[optional: trigger id] - Triggers config.", - "ru": "[необязятельно: айди триггера] - Конфиг модуля" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "triggerdel", - "original_name": "triggerdel", - "description": { - "default": "[trigger's id] - Delete trigger", - "ru": "[айди триггера] - Удалить триггер" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tcallback", - "original_name": "tcallback", - "description": { - "default": "[callback_id: str] - Add a callback that trigger can execute", - "ru": "[айди колбека: str] <реплай на пайтон код> - Добавить колбек, который триггер сможет выполнить" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "triggerget", - "original_name": "triggerget", - "description": { - "default": "[trigger's id] - Get trigger", - "ru": "[айди триггера] - Получить триггер" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "triggerset", - "original_name": "triggerset", - "description": { - "default": "[trigger's id] [edited trigger] - Edit trigger", - "ru": "[айди триггера] [измененный триггер] - Изменить триггер" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "triggerupdate", - "original_name": "triggerupdate", - "description": { - "default": "[trigger's id] [path] [value] - Edit trigger", - "ru": "[айди триггера] [путь] [значение] - Изменить одно значение триггера" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Triggers", - "_cfg_status": "module working or not", - "_cfg_allow_invoke": "can triggers run ANY userbot commands?", - "_cfg_allow_callback": "can triggers run ANY python code?", - "_cfg_throttle_time": "cooldown between trigger executions", - "no_reply": " No reply!", - "no_args": " No args!", - "text_add": " Trigger successfully added\nid: {id}", - "empty": " 🫗 Empty\n", - "text_all": "💬 Your triggers:\n{triggers}\nin {chats} chats", - "chat_added": "⚡️ Chat {chat} successfully added", - "chat_removed": "‼️ Chat {chat} successfully removed", - "success": " Success", - "not_found": " Trigger not found!", - "not_valid": " Trigger is not valid!", - "error": " Unexpected error: {e}", - "_cfg_status_ru": "Модуль работает или нет", - "_cfg_allow_invoke_ru": "могут ли триггеры запускать ЛЮБЫЕ команды юзербота?", - "_cfg_allow_callback_ru": "могут ли триггеры запускать АБСОЛЮТНО любой код на python?", - "_cfg_throttle_time_ru": "Кд между выполнением триггеров. Для применения изменений требуется перезагрузить модуль/юзербота", - "no_reply_ru": " Нет реплая!", - "no_args_ru": " Нет аргументов!", - "text_add_ru": " Триггер успешно добавлен\nid: {id}", - "empty_ru": " 🫗 Пусто\n", - "text_all_ru": "💬 Ваши триггеры:\n{triggers}\nв {chats} чатах", - "chat_added_ru": "⚡️ Чат {chat} успешно добавлен", - "chat_removed_ru": "‼️ Чат {chat} успешно убран", - "success_ru": " Успешно", - "not_found_ru": " Триггер не найден!", - "not_valid_ru": " Триггер не валиден!", - "error_ru": " Неожиданная ошибка. Обратитесь к разработчику модуля или попробуйте изменить данные: {e}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/fakedata.py": { - "name": "FakeData", - "description": "Just fake data of persons and credit cards", - "cls_doc": { - "ru": "Просто фейковые данные о людях и их кредитных карт" - }, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/sqlmerr/blob/main/assets/hikka_mods/sqlmerrmodules_fakedata.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/fakedata.png?raw=true" - }, - "commands": [ - { - "fakedata": "[locale (for example: \"ru_RU\" for Russian or \"fr_FR\" for French)] - Get fake data about person and credit card | (RU) [язык (к примеру: \"ru_RU\" для Русского или \"fr_FR\" для французского и т.д.)] - Получить фейковые данные человека и его кредитной карты" - } - ], - "new_commands": [ - { - "name": "fakedata", - "original_name": "fakedata", - "description": { - "default": "[locale (for example: \"ru_RU\" for Russian or \"fr_FR\" for French)] - Get fake data about person and credit card", - "ru": "[язык (к примеру: \"ru_RU\" для Русского или \"fr_FR\" для французского и т.д.)] - Получить фейковые данные человека и его кредитной карты" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "FakeData", - "error": " Error in api!", - "person_text": "{emoji} Person:\n name - {name}\n email - {email}\n phone - {phone}\n birthday - {birthday}\n gender - {gender}\n ip - {ip}\n address - {address}\n\n", - "credit_card_text": "💳 Credit card:\n type - {type}\n number - {number}\n expiration - {expiration}", - "error_ru": " Ошибка в апи!", - "person_text_ru": "{emoji} Человек:\n имя - {name}\n почта - {email}\n номер телефона - {phone}\n дата рождения - {birthday}\n пол - {gender}\n айпи - {ip}\n адресс - {address}\n\n", - "credit_card_text_ru": "💳 Кредитная карта:\n тип - {type}\n номер - {number}\n истекает - {expiration}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/FastChangeTgStatus.py": { - "name": "FCTS", - "description": "Change your status fast. Only for premium users | Изменяйте ваш статус быстро. Только для премиум пользователей", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/FastChangeTgStatus.png?raw=true" - }, - "commands": [ - { - "statuschange": "[status name] - set this status | .statuslist to view your downloaded statuses | (RU) [имя статуса] - поставить этот статус | .statuslist для просмотра ваших установленных статусов" - }, - { - "statuslist": "See list of all your statuses | (RU) Посмотреть список всех статусов" - }, - { - "statusadd": "[emoji] [short name] Add a custom status | (RU) [эмодзи] [короткое имя] Добавить кастомный статус" - }, - { - "statusclear": "Clear all custom statuses | (RU) Очистить все кастомные статусы" - } - ], - "new_commands": [ - { - "name": "statuschange", - "original_name": "statuschange", - "description": { - "default": "[status name] - set this status | .statuslist to view your downloaded statuses", - "ru": "[имя статуса] - поставить этот статус | .statuslist для просмотра ваших установленных статусов" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "statuslist", - "original_name": "statuslist", - "description": { - "default": "See list of all your statuses", - "ru": "Посмотреть список всех статусов" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "statusadd", - "original_name": "statusadd", - "description": { - "default": "[emoji] [short name] Add a custom status", - "ru": "[эмодзи] [короткое имя] Добавить кастомный статус" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "statusclear", - "original_name": "statusclear", - "description": { - "default": "Clear all custom statuses", - "ru": "Очистить все кастомные статусы" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "FastChangeTgStatus", - "no_args": "You didn't enter any arguments!", - "status_changed": "Your status successfully changed to {}!", - "status_is_none": "This status does not exist!", - "list": "⭐️ List of your statuses:", - "emoji_added": "Emoji added to status list successfully", - "indexerror": "⚠️ You have entered too few arguments!", - "no_args_ru": "Вы не ввели аргументы!", - "status_changed_ru": "Ваш статус успешно изменен на {}!", - "status_is_none_ru": "Такого статуса не существует!", - "list_ru": "⭐️ Список ваших статусов:", - "emoji_added_ru": "Эмодзи успешно добавлен в список статусов", - "indexerror_ru": "⚠️ Вы ввели слишком мало аргументов!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/translation_manager.py": { - "name": "TranslationManager", - "description": "Module for managing external modules translations", - "cls_doc": { - "ru": "Модуль для управления переводами сторонних модулей" - }, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/banners/translation_manager.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/translation_manager.png?raw=true" - }, - "commands": [ - { - "trget": "[mod] [lang] [key] - Get current translation | (RU) [модуль] [язык] [ключ] - Получить перевод" - }, - { - "trset": "[mod] [lang] [key] [val] - Set translation | (RU) [модуль] [язык] [ключ] [значение] - Изменить перевод" - }, - { - "trdel": "[mod] [lang] [key] - Delete custom translation | (RU) [модуль] [язык] [ключ] - Удалить кастомный перевод" - } - ], - "new_commands": [ - { - "name": "trget", - "original_name": "trget", - "description": { - "default": "[mod] [lang] [key] - Get current translation", - "ru": "[модуль] [язык] [ключ] - Получить перевод" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "trset", - "original_name": "trset", - "description": { - "default": "[mod] [lang] [key] [val] - Set translation", - "ru": "[модуль] [язык] [ключ] [значение] - Изменить перевод" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "trdel", - "original_name": "trdel", - "description": { - "default": "[mod] [lang] [key] - Delete custom translation", - "ru": "[модуль] [язык] [ключ] - Удалить кастомный перевод" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "TranslationManager", - "no_args": " No args!", - "get_txt": "`{}` Translation in {} module of language {}:\n
{}
\n{}", - "custom": "🔧 Translation is edited", - "default": "🔧 Translation is default", - "404": " Module not found!", - "success": " Success", - "only_external": " You can manage translations in only external mods. To update them use custom language.", - "no_args_ru": " Вы не передали аргументы!", - "get_txt_ru": "`{}` Перевод модуля {} в языке {}:\n
{}
\n{}", - "custom_ru": "🔧 Перевод изменен", - "default_ru": "🔧 Перевод стандартный", - "404_ru": " Модуль не найден!", - "success_ru": " Успешно", - "only_external_ru": " Ты можешь управлять переводами только в сторонних модулях. Чтобы изменить их, используй кастомный язык." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/addlinktosymbols.py": { - "name": "AddLinkToSymbols", - "description": "Add link to symbols in text", - "cls_doc": { - "ru": "Добавить ссылку на определённые символы в тексте" - }, - "meta": { - "pic": null, - "banner": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/sqlmerrmodules_example.png?raw=true", - "developer": "@sqlmerr_m", - "icon": "https://github.com/sqlmerr/hikka_mods/blob/main/assets/icons/addlinktosymbols.png?raw=true" - }, - "commands": [ - { - "addlinktosymbols": "[symbols] [link] [text or reply] Add link to symbols\n\nExample: .addlinktosymbols ah.e https://example.com hi hello. YOOOOOOO\nWrite characters without spaces. | (RU) [символы] [ссылка] [текст или реплай] Добавить ссылку на символы\n\nПример: .addlinktosymbols ап.ев https://example.com привет. Еееее хай\nСимволы пишите без пробелов. " - } - ], - "new_commands": [ - { - "name": "addlinktosymbols", - "original_name": "addlinktosymbols", - "description": { - "default": "[symbols] [link] [text or reply] Add link to symbols\n\nExample: .addlinktosymbols ah.e https://example.com hi hello. YOOOOOOO\nWrite characters without spaces.", - "ru": "[символы] [ссылка] [текст или реплай] Добавить ссылку на символы\n\nПример: .addlinktosymbols ап.ев https://example.com привет. Еееее хай\nСимволы пишите без пробелов. " - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AddLinkToSymbols", - "noargs": "🚫 You didn't enter any arguments", - "IndexError": "😟 You have entered too few arguments", - "wait": "🔴 Please wait a second...", - "none": " ERROR", - "noargs_ru": "🚫 Вы не ввели аргументы", - "IndexError_ru": "😟 Вы ввели слишком мало аргументов", - "wait_ru": "🔴 Подождите немного...", - "none_ru": " ОШИБКА" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "sqlmerr/hikka_mods/autoforward.py": { - "name": "AutoForward", - "description": "Автоматически пересылает сообщения из каналов в один", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": "@sqlmerr_m" - }, - "commands": [ - { - "autoforward": "- вкл/выкл модуля" - } - ], - "new_commands": [ - { - "name": "autoforward", - "original_name": "autoforward", - "description": { - "default": "- вкл/выкл модуля" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AutoForward", - "channels_from": "Каналы из которых будут перессылаться сообщения", - "channel_to": "Канал в который будут пересылаться сообщения из каналов" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/ptichki.py": { - "name": "Ptichki", - "description": "Генератор птиц", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/ptichki.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "ptichka": "[текст] - Сгенерировать стикер с птицей" - }, - { - "ptichkaimg": "[текст] - Сгенерировать фото с птицей" - } - ], - "new_commands": [ - { - "name": "ptichka", - "original_name": "ptichka", - "description": { - "default": "[текст] - Сгенерировать стикер с птицей" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ptichkaimg", - "original_name": "ptichka_img", - "description": { - "default": "[текст] - Сгенерировать фото с птицей" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Ptichki", - "no_args": "🦅 Нужно {}{} {}", - "generation": "🦅 Генерирую птичку..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/fun.py": { - "name": "Fun", - "description": "Module for fun...", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/fun.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "hacku": "Взлом пользователя" - }, - { - "hackp": "Взлом пентагона" - }, - { - "ftype": "Печатать текст" - } - ], - "new_commands": [ - { - "name": "hacku", - "original_name": "hacku", - "description": { - "default": "Взлом пользователя" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "hackp", - "original_name": "hackp", - "description": { - "default": "Взлом пентагона" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ftype", - "original_name": "ftype", - "description": { - "default": "Печатать текст" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Fun", - "no_us": " Должно быть .hacku [юзернейм/ник чела]", - "no_typing_text": " Должно быть .ftype [текст]", - "hacku_process": "💻 Взлом {} в процессе... {}%", - "hackedu": " {} успешно взломан!", - "collecting_info": "💾 Сохранение информации о {}... {}%", - "collected_info": " Успешно нашёл и сохранил всю информацию о {}", - "hackp_process": "👮‍♀️ Взлом пентагона в процессе... {}%", - "hackedp": "🟢 Пентагон успешно взломан!", - "founding_nlo": "👽 Поиск секретных данных об НЛО ... {}%", - "dino_founded": "🦖 Найдены данные о существовании динозавров на земле!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/famods_socket.py": { - "name": "FAmodsSocket", - "description": "Установка модулей через @FAmods_Bot", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": "@FAmods" - }, - "commands": [], - "new_commands": [], - "inline_handlers": [], - "strings": { - "name": "FAmodsSocket" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/telegrapher.py": { - "name": "Telegrapher", - "description": "Создание статей и другое связанное с telegra.ph", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/telegrapher.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "tghpost": "Выложить статью в telegra.ph" - }, - { - "tghup": "Выложить медиа в telegra.ph" - } - ], - "new_commands": [ - { - "name": "tghpost", - "original_name": "tghpost", - "description": { - "default": "Выложить статью в telegra.ph" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tghup", - "original_name": "tghup", - "description": { - "default": "Выложить медиа в telegra.ph" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Telegrapher", - "waiting": "🕑 Создаю страницу на telegra.ph...", - "waiting_up": "🕑 Загружаю файл на telegra.ph...", - "article_ready": "\n Твоя статья в Telegra.ph создана!\n\n🔗 Ссылка: {}\n\nℹ️ Редактировать заголовок/контент/автора на странице можно в {}cfg telegrapher\n", - "upload_ready": "\n⬇️ Файл загружен!\n\n🔗 Ссылка: {}\n", - "upload_error": " Ошибка при выгрузке файла на telegra.ph!", - "media_type_invalid": " Ответь на фото или видео/гиф" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/gemini.py": { - "name": "Gemini", - "description": "Взаимодействие с AI Gemini", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/gemini.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "gemini": "Задать вопрос к Gemini" - } - ], - "new_commands": [ - { - "name": "gemini", - "original_name": "gemini", - "description": { - "default": "Задать вопрос к Gemini" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Gemini", - "no_args": " Нужно {}{} {}", - "no_token": " Нету токена! Вставь его в {}cfg gemini", - "asking_gemini": "🔄 Спрашиваю Gemini..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/tondns.py": { - "name": "TonDNS", - "description": "Модуль для работы с Ton DNS", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/tondns.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "itondns": "Информация о TON DNS" - }, - { - "tonshot": "Скриншот TON DNS сайта" - } - ], - "new_commands": [ - { - "name": "itondns", - "original_name": "itondns", - "description": { - "default": "Информация о TON DNS" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tonshot", - "original_name": "tonshot", - "description": { - "default": "Скриншот TON DNS сайта" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "TonDNS", - "waiting": "🕑 Собираю информацию...", - "waiting_shot": "📸 Делаю скриншот TON DNS сайта...", - "shot_ton_dns": "💎 Скриншот TON DNS сайта {}", - "ton_shot_link": "https://mini.s-shot.ru/1920x1080/JPEG/1024/Z100/?{}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/xrocket.py": { - "name": "xRocket", - "description": "Автоматизация базового функционала @xRocket", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/xrocket.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "xwallet": "Посмотреть кошелёк" - }, - { - "xinvoice": "Создать счёт" - }, - { - "xcheck": "Создать чек" - } - ], - "new_commands": [ - { - "name": "xwallet", - "original_name": "xwallet", - "description": { - "default": "Посмотреть кошелёк" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "xinvoice", - "original_name": "xinvoice", - "description": { - "default": "Создать счёт" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "xcheck", - "original_name": "xcheck", - "description": { - "default": "Создать чек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "xRocket", - "no_args": " Нужно {}{}", - "creating": "🔄 Создаю {}...", - "checking_wallet": "🔄 Смотрю кошелёк...", - "no_money": " Не достаточно денег", - "invoice": "💵 Счёт на {} {}\n\n🔗 Оплатить", - "check": "💵 Чек на {} {} {}\n\n🔗 Получить" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/picme.py": { - "name": "PicMe", - "description": "Кринж модуль", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/picme.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "picme": "Включить/выключить режим пикми" - } - ], - "new_commands": [ - { - "name": "picme", - "original_name": "picme", - "description": { - "default": "Включить/выключить режим пикми" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "PicMe", - "p_on": "😘 Режим пикми включен!", - "p_off": "😢 Режим пикми выключен!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/evalaliases.py": { - "name": "EvalAliases", - "description": "Алиаси для eval", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/evalaliases.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "addea": "Добавить алиас" - }, - { - "removea": "Удалить алиас" - }, - { - "getea": "Получить список алиасов для Eval" - } - ], - "new_commands": [ - { - "name": "addea", - "original_name": "addea", - "description": { - "default": "Добавить алиас" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "removea", - "original_name": "removea", - "description": { - "default": "Удалить алиас" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "getea", - "original_name": "getea", - "description": { - "default": "Получить список алиасов для Eval" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "EvalAliases", - "no_args": " Нужно {}{} {}", - "already_created": " Алиас {} уже существует!", - "alias_created": "⚡️ Алиас {} создан!\n \nИспользуй его с помощью {}v{}", - "alias_not_found": " Алиас {} не найден!", - "alias_deleted": " Алиас {} удален!", - "no_aliases": " Вы ещё не создали алиасов!", - "aliases": "🖥 Список алиасов\n \n{}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/pricefreedom.py": { - "name": "PriceFreedom", - "description": "Автоматизированная работа с @rabstvo_game_bot", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/pricefreedom.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "pfpromo": "Включить/выключить автоматически активирование промокода" - }, - { - "spfus": "Посмотреть профиль пользователя" - }, - { - "spfme": "Посмотреть свой профиль" - } - ], - "new_commands": [ - { - "name": "pfpromo", - "original_name": "pfpromo", - "description": { - "default": "Включить/выключить автоматически активирование промокода" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spfus", - "original_name": "spfus", - "description": { - "default": "Посмотреть профиль пользователя" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spfme", - "original_name": "spfme", - "description": { - "default": "Посмотреть свой профиль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "PriceFreedom", - "checking_profile": "👀 Смотрю профиль...", - "searching_us": "👀 Поиск пользователя...", - "no_usid": "🚫 Нужно {}{} [айди]", - "promo_on": "🎁 Авто-промо включен!\n\nЧто бы получать промокоды вы должны быть подписаним здесь.", - "promo_off": "🚫 Авто-промо выключен!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/famod.py": { - "name": "Famod", - "description": "Управление вещами, связанными с @FAmods_Bot", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/famod.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "fmstats": "Просмотр статистики" - }, - { - "fmsearch": "Поиск модуля" - }, - { - "famods": "Поиск модулей" - } - ], - "new_commands": [ - { - "name": "fmstats", - "original_name": "fmstats", - "description": { - "default": "Просмотр статистики" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "fmsearch", - "original_name": "fmsearch", - "description": { - "default": "Поиск модуля" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "famods", - "original_name": "famods", - "description": { - "default": "Поиск модулей" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": true, - "is_inline_handler": true, - "decorators": [] - } - ], - "inline_handlers": [ - { - "name": "famods", - "description": { - "default": "Поиск модулей" - }, - "decorators": [] - } - ], - "strings": { - "name": "Famod", - "no_q": " Нужно {}{} [запрос]", - "searching_module": "🔄 Поиск модуля...", - "getting_stats": "🔄 Получение статистики...", - "no_found": " Не нашёл такой модуль" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/wakatime.py": { - "name": "Wakatime", - "description": "Показывает твою Wakatime статистику", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/wakatime.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "waka": "Посмотреть свою статистику в Wakatime" - } - ], - "new_commands": [ - { - "name": "waka", - "original_name": "waka", - "description": { - "default": "Посмотреть свою статистику в Wakatime" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Wakatime", - "loading": "🕑 Загрузка статистики...", - "no_token": "🚫 Wakatime токен не поставлен! Поставь его в {}cfg wakatime" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/bigmac.py": { - "name": "BigMac", - "description": "Авто-фарм в @BigMacMetreBot", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/bigmac.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "bigmacs": "Включить/выключить авто-фарм" - }, - { - "bp": "Посмотреть свой профиль" - }, - { - "btop": "Посмотреть топ" - } - ], - "new_commands": [ - { - "name": "bigmacs", - "original_name": "bigmacs", - "description": { - "default": "Включить/выключить авто-фарм" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bp", - "original_name": "bp", - "description": { - "default": "Посмотреть свой профиль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "btop", - "original_name": "btop", - "description": { - "default": "Посмотреть топ" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "BigMac", - "checking_profile": "👀 Смотрю профиль...", - "getting_top": "👀 Смотрю статистику...", - "e_on": "🍔 Авто-фарм включен!", - "e_off": "🚫 Авто-фарм выключен!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/gsearch.py": { - "name": "Gsearch", - "description": "Поиск в Google", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/gsearch.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "gsearch": "Поиск в Google" - } - ], - "new_commands": [ - { - "name": "gsearch", - "original_name": "gsearch", - "description": { - "default": "Поиск в Google" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Gsearch", - "no_q": " Должно быть {}gsearch [запрос]", - "no_result": "😕 Ничего не нашёл по этому запросу", - "searching": "🔄 Поиск в google.com...", - "searched": "\n🔎 Результаты поиска\n\n🔎 Запрос: {}\n{}\n\n{} результатов за {} сек\n
" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/fabrika.py": { - "name": "Fabrika", - "description": "Авто-фарм в @fabrika", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/fabrika.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "fbrw": "Включить/выключить автоматически давать работу работникам" - }, - { - "fbbonus": "Включить/выключить автоматическое получать бонус" - }, - { - "fbteam": "Включить/выключить автоматически отправлятся на комадную работу" - }, - { - "sprof": "Посмотреть свой профиль" - }, - { - "sidtg": "Посмотреть профиль пользователя через айди в тг" - }, - { - "sidfb": "Посмотреть профиль пользователя через айди в боте" - }, - { - "steamfb": "Посмотреть команду через айди" - } - ], - "new_commands": [ - { - "name": "fbrw", - "original_name": "fbrw", - "description": { - "default": "Включить/выключить автоматически давать работу работникам" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "fbbonus", - "original_name": "fbbonus", - "description": { - "default": "Включить/выключить автоматическое получать бонус" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "fbteam", - "original_name": "fbteam", - "description": { - "default": "Включить/выключить автоматически отправлятся на комадную работу" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "sprof", - "original_name": "sprof", - "description": { - "default": "Посмотреть свой профиль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "sidtg", - "original_name": "sidtg", - "description": { - "default": "Посмотреть профиль пользователя через айди в тг" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "sidfb", - "original_name": "sidfb", - "description": { - "default": "Посмотреть профиль пользователя через айди в боте" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "steamfb", - "original_name": "steamfb", - "description": { - "default": "Посмотреть команду через айди" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Fabrika", - "checking_profile": "👀 Смотрю профиль...", - "searching_team": "👀 Поиск команды...", - "searching_id": "👀 Поиск пользователя...", - "no_usid": "🚫 Нужно {}{} [айди]", - "no_found_us": "🚫 Пользователь не найден!", - "rw_on": "⚡️ Отправка рабочих включена!", - "rw_off": "🚫 Отправка рабочих выключена!", - "team_on": "⚡️ Командная работа включена!", - "team_off": "🚫 Командная работа выключена!", - "bonus_on": "🎁 Авто-бонус включен!", - "bonus_off": "🚫 Авто-бонус выключен!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/hbotcommand.py": { - "name": "HbotCommand", - "description": "Дополнительная команда для твоего inline бота", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/hbotcommand.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "busername": "Посмотреть юзернейм бота" - }, - { - "bcsettings": "Настройка команды бота" - } - ], - "new_commands": [ - { - "name": "busername", - "original_name": "busername", - "description": { - "default": "Посмотреть юзернейм бота" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bcsettings", - "original_name": "bcsettings", - "description": { - "default": "Настройка команды бота" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "HbotCommand", - "loading_cfg": "🔄 Открываю настройку..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/hetsu.py": { - "name": "Hetsu", - "description": "Search and install modules easily.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/hetsu.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "hetsu": "Search module" - }, - { - "hetsu": "Search module" - } - ], - "new_commands": [ - { - "name": "hetsu", - "original_name": "hetsucmd", - "description": { - "default": "Search module" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "hetsu", - "original_name": "hetsu", - "description": { - "default": "Search module" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": true, - "is_inline_handler": true, - "decorators": [] - } - ], - "inline_handlers": [ - { - "name": "hetsu", - "description": { - "default": "Search module" - }, - "decorators": [] - } - ], - "strings": { - "name": "Hetsu", - "no_q": " You need to write {}hetsu [query]", - "inline_no_q": " Enter query.", - "no_modules": "❌ No modules founded.", - "searching": "🔍 Hetsu searching...\n \n🛡 Searching above 900+ modules. All modules are safety and clearly moderated.", - "module": "⭐️ Module {module_name} {developer}\n\n🔖 Ratio: {ratio}\n🔎 Query: {query}\n\nℹ️ Description: {description}\n\n💻 Source code: click\n\n⬇️ {prefix}dlm {link}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/cocoon.py": { - "name": "Cocoon", - "description": "Взаимодействие с Cocoon от HikkaHost", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/cocoon.png?raw=true", - "developer": "@FAmods & @vsecoder_m" - }, - "commands": [ - { - "ccusage": "Статистика использования Cocoon" - }, - { - "cocoon": "Задать вопрос к ИИ (поддерживает ответ на сообщение)" - } - ], - "new_commands": [ - { - "name": "ccusage", - "original_name": "ccusage", - "description": { - "default": "Статистика использования Cocoon" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "cocoon", - "original_name": "cocoon", - "description": { - "default": "Задать вопрос к ИИ (поддерживает ответ на сообщение)" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Cocoon [BETA]", - "try_again": " Что-то пошло не так. Попробуйте снова.", - "no_args": " Нужно {}{} {}", - "no_token": " Нету токена! Вставь его в {}cfg cocoon\n\n🌘 Получить токен: @hikkahost_bot → 🥚 Cocoon", - "invalid_token_or_no_sub": "Неверный токен или у вас нет подписки 🌘 HikkaHost.\n\n🌘 Получить токен: @hikkahost_bot → 🥚 Cocoon", - "sending_request_to_cocoon": "🐣 Обрабатываю запрос в Cocoon...", - "thinking": "🐣 Думаю...\n\n
{thoughts}…
", - "answer": "🌘 Вопрос: {question}\n\n🐣 Размышления:\n
{thoughts}
\n\n🥚 {answer}\n\n🚀 Модель: {model}", - "usage": "🥚 Cocoon API\n\n💡 Использовано:\n• {current}/{total} ({percent}% осталось)\n\n Лимит сбросится через {days} день(-ей).", - "again_kb": "🔄 Сгенерировать ещё раз" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/spotify4ik.py": { - "name": "Spotify4ik", - "description": "Слушай музыку в Spotify", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/spotify4ik.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "spauth": "Войти в свой аккаунт" - }, - { - "spcode": "Ввести код авторизации" - }, - { - "sppause": "Поставить на паузу текущий трек" - }, - { - "spplay": "Воспроизвести текущий трек" - }, - { - "spbegin": "Включить текущий трек с начала" - }, - { - "spback": "Включить предыдущий трек" - }, - { - "spnext": "Включить следующий трек" - }, - { - "spbio": "Включить/выключить стрим текущего трека в био" - }, - { - "spbiochannel": "Включить/выключить стрим текущего трека в канале в био" - }, - { - "splike": "Лайкнуть текущий трек" - }, - { - "sprepeat": "Повторить текущий трек" - }, - { - "spnorepeat": "Перестать повторять текущий трек" - }, - { - "spnow": "Текущий трек" - } - ], - "new_commands": [ - { - "name": "spauth", - "original_name": "spauth", - "description": { - "default": "Войти в свой аккаунт" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spcode", - "original_name": "spcode", - "description": { - "default": "Ввести код авторизации" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "sppause", - "original_name": "sppause", - "description": { - "default": "Поставить на паузу текущий трек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spplay", - "original_name": "spplay", - "description": { - "default": "Воспроизвести текущий трек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spbegin", - "original_name": "spbegin", - "description": { - "default": "Включить текущий трек с начала" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spback", - "original_name": "spback", - "description": { - "default": "Включить предыдущий трек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spnext", - "original_name": "spnext", - "description": { - "default": "Включить следующий трек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spbio", - "original_name": "spbio", - "description": { - "default": "Включить/выключить стрим текущего трека в био" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spbiochannel", - "original_name": "spbiochannel", - "description": { - "default": "Включить/выключить стрим текущего трека в канале в био" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "splike", - "original_name": "splike", - "description": { - "default": "Лайкнуть текущий трек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "sprepeat", - "original_name": "sprepeat", - "description": { - "default": "Повторить текущий трек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spnorepeat", - "original_name": "spnorepeat", - "description": { - "default": "Перестать повторять текущий трек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "spnow", - "original_name": "spnow", - "description": { - "default": "Текущий трек" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": {}, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/cryptoqr.py": { - "name": "CryptoQR", - "description": "Создание QR код в стиле CryptoBot", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/cryptoqr.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "cqr": "Создать QRcode" - } - ], - "new_commands": [ - { - "name": "cqr", - "original_name": "cqr", - "description": { - "default": "Создать QRcode" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "CryptoQR", - "no_args": " Нужно {}cqr [текст/ссылка]", - "creating": " Создаю QRcode..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/stats.py": { - "name": "Stats", - "description": "Показывает статистику твоего аккаунта", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/stats.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "stats": "Получить статистику" - } - ], - "new_commands": [ - { - "name": "stats", - "original_name": "stats", - "description": { - "default": "Получить статистику" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Stats", - "loading_stats": "🔄 Загрузка статистики..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/hetalib.py": { - "name": "HetaLib", - "description": "Модуль для работы с heta", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/hetalib.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "hsearch": "Поиск модуля в heta" - }, - { - "decodehhash": "Декодировать heta hash" - }, - { - "modsrepo": "Получить модули с репозитория" - } - ], - "new_commands": [ - { - "name": "hsearch", - "original_name": "hsearch", - "description": { - "default": "Поиск модуля в heta" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "decodehhash", - "original_name": "decode_hhash", - "description": { - "default": "Декодировать heta hash" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "modsrepo", - "original_name": "mods_repo", - "description": { - "default": "Получить модули с репозитория" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "HetaLib", - "no_q": " Должно быть {}hsearch [запрос]", - "no_hh": " Должно быть {}decode_hhash [хэш]", - "no_repo": " Должно быть {}mods_repo [ссылка_на_репозиторий]", - "invalid_hh": "😕 Неверный хэш", - "invalid_repo": "😕 Неверный репозиторий модулей", - "no_modules_in_repo": "😕 Нету модулей в репозитории", - "searching": "🔄 Поиск модулей...", - "receiving_modules": "🔄 Получаю модули...", - "decoding": "🔄 Декодирую хэш..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/checkhost.py": { - "name": "CheckHost", - "description": "Проверка доступности веб-сайтов, серверов, хостов и IP-адресов с разных геолокаций и тд.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/checkhost.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "chhttp": "Проверить доступность" - } - ], - "new_commands": [ - { - "name": "chhttp", - "original_name": "chhttp", - "description": { - "default": "Проверить доступность" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "CheckHost", - "no_url": " Нужно {}{} [адрес]", - "checking_http": "🕓 Проверяю доступность..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/requirements.py": { - "name": "Requirements", - "description": "Работа с pip пакетами в модуле", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/requirements.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "dldeps": "Установить pip пакеты с модуля" - }, - { - "uldeps": "Удалить pip пакеты с модуля" - }, - { - "deps": "Посмотреть pip пакеты с модуля" - } - ], - "new_commands": [ - { - "name": "dldeps", - "original_name": "dldeps", - "description": { - "default": "Установить pip пакеты с модуля" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "uldeps", - "original_name": "uldeps", - "description": { - "default": "Удалить pip пакеты с модуля" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "deps", - "original_name": "deps", - "description": { - "default": "Посмотреть pip пакеты с модуля" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Requirements", - "no_dep": " В модуле нету зависимостей", - "only_url_or_hash": " Только ссылка на модуль, или heta hash", - "no_file_and_link": " Нужно ответить на файл или {}{} [ссылка или heta hash]", - "search_deps": "🕓 Ищу зависимости...", - "install_deps": "🕓 Установка зависимостей:\n\n{}", - "uninstall_deps": "🕓 Удаление зависимостей:\n\n{}", - "installed": " Успешно установил зависимости:\n\n{}\n\n{}", - "uninstalled": " Успешно удалил зависимости:\n\n{}", - "requirements": "⚙️ Зависимости:\n\n{}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/CodeBase64.py": { - "name": "CodeBase64", - "description": "Encode and decode base64", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/CodeBase64.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "cbase64": "Кодирование в base64" - }, - { - "dbase64": "Декодирование из base64" - } - ], - "new_commands": [ - { - "name": "cbase64", - "original_name": "cbase64", - "description": { - "default": "Кодирование в base64" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "dbase64", - "original_name": "dbase64", - "description": { - "default": "Декодирование из base64" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "CodeBase64", - "only_base64": "🚫 Only base64", - "enc_txt": "⌨️ You encoded text into base64:\n{}", - "de_txt": "⌨️ You decoded text from base64:\n{}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/tonscan.py": { - "name": "Tonscan", - "description": "Информация о TON адресе", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/tonscan.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "tonwallet": "Информация о TON кошельке" - }, - { - "tonjetton": "Информация о TON токене" - }, - { - "tonnftcol": "Информация о TON NFT коллекции" - }, - { - "tonnft": "Информация о TON NFT" - } - ], - "new_commands": [ - { - "name": "tonwallet", - "original_name": "tonwallet", - "description": { - "default": "Информация о TON кошельке" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tonjetton", - "original_name": "tonjetton", - "description": { - "default": "Информация о TON токене" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tonnftcol", - "original_name": "tonnftcol", - "description": { - "default": "Информация о TON NFT коллекции" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tonnft", - "original_name": "tonnft", - "description": { - "default": "Информация о TON NFT" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Tonscan", - "waiting": "🕑 Собираю информацию..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/faker.py": { - "name": "Faker", - "description": "Генерация фейк информации", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/faker.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "gfake": "Сгенерировать фейк информацию" - } - ], - "new_commands": [ - { - "name": "gfake", - "original_name": "gfake", - "description": { - "default": "Сгенерировать фейк информацию" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Faker", - "loading": "🔄 Генерирую информацию..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/fabusiness.py": { - "name": "FAbusiness", - "description": "Бесплатный Telegram business", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/fabusiness.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "business": "Включить/выключить FAbusiness" - }, - { - "bsettings": "Настройка FAbusiness" - } - ], - "new_commands": [ - { - "name": "business", - "original_name": "business", - "description": { - "default": "Включить/выключить FAbusiness" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bsettings", - "original_name": "bsettings", - "description": { - "default": "Настройка FAbusiness" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "FAbusiness", - "loading_cfg": "🔄 Открываю настройку...", - "business_on": "💻 FAbusiness включен!", - "business_off": "🚫 FAbusiness выключен!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/vaper.py": { - "name": "Vaper", - "description": "Авто-фарм в @vapeusebot", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/vaper.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "vape": "Включить/выключить авто-фарм" - }, - { - "vp": "Посмотреть свой профиль" - }, - { - "vtop": "Посмотреть топ" - } - ], - "new_commands": [ - { - "name": "vape", - "original_name": "vape", - "description": { - "default": "Включить/выключить авто-фарм" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "vp", - "original_name": "vp", - "description": { - "default": "Посмотреть свой профиль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "vtop", - "original_name": "vtop", - "description": { - "default": "Посмотреть топ" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Vaper", - "checking_profile": "👀 Смотрю профиль...", - "getting_top": "👀 Смотрю статистику...", - "v_on": "⚡️ Авто-фарм включен!", - "v_off": "🚫 Авто-фарм выключен!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/gigachat.py": { - "name": "GigaChat", - "description": "GigaChat AI. БЕЗ АПИ", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/gigachat.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "ggchat": "Задать вопрос к GigaChat" - } - ], - "new_commands": [ - { - "name": "ggchat", - "original_name": "ggchat", - "description": { - "default": "Задать вопрос к GigaChat" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "GigaChat", - "no_args": " Нужно {}{} {}", - "asking_gg": "🔄 Спрашиваю GigaChat...", - "answer": "🗿 Ответ: {answer}\n\n Вопрос: {question}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/anonsms.py": { - "name": "AnonSMS", - "description": "Анонимное сообщение", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/anonsms.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "getanonlink": "Получить ссылку на получение анонимного сообщения" - }, - { - "anonsettings": "Настроят модуль" - } - ], - "new_commands": [ - { - "name": "getanonlink", - "original_name": "getanonlink", - "description": { - "default": "Получить ссылку на получение анонимного сообщения" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "anonsettings", - "original_name": "anonsettings", - "description": { - "default": "Настроят модуль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AnonSMS", - "enter_message": "📩 Отправьте сообщение", - "new_anon_msg": "📨 Вам пришло новое анонимное сообщение:", - "opening_settings": "🔄 Открываю настройки...", - "only_one": "❌ Отправлять сообщение можно раз в {} секунд!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/infoip.py": { - "name": "InfoIP", - "description": "Информация об IP адресе", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/infoip.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "ipi": "Информация об IP" - } - ], - "new_commands": [ - { - "name": "ipi", - "original_name": "ipi", - "description": { - "default": "Информация об IP" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "InfoIP", - "no_ip": " Должно быть .ipi [айпи]", - "no_token": " Нету токена! Поставь его в {}cfg InfoIP", - "invalid_token": "😕 Неверный токен", - "invalid_ip": "😕 Неверный IP", - "searching_info": "🔄 Получаю информацию..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/chmodslist.py": { - "name": "CHmodsList", - "description": "Список каналов с модулями (идея: @codrago)", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/chmodslist.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "chsettings": "Изменить список каналов с модулями" - }, - { - "chmods": "Посмотреть список каналов с модулями" - } - ], - "new_commands": [ - { - "name": "chsettings", - "original_name": "chsettings", - "description": { - "default": "Изменить список каналов с модулями" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "chmods", - "original_name": "chmods", - "description": { - "default": "Посмотреть список каналов с модулями" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "CHmodsList", - "opening_config": "🔄 Открываю настройки..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/grokai.py": { - "name": "GrokAI", - "description": "Взаимодействие с Grok AI", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/grokai.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "grok": "Задать вопрос к Grok" - } - ], - "new_commands": [ - { - "name": "grok", - "original_name": "grok", - "description": { - "default": "Задать вопрос к Grok" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "GrokAI", - "no_args": " Нужно {}{} {}", - "no_token": " Нету токена! Вставь его в {}cfg grokai", - "asking_grok": "🔄 Спрашиваю Grok...", - "answer": "🌐 Ответ: {answer}\n\n Вопрос: {question}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/proxy.py": { - "name": "Proxy", - "description": "Работа с прокси", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/proxy.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "gproxy": "Получить рандомное прокси" - }, - { - "wproxy": "Проверить работу прокси" - } - ], - "new_commands": [ - { - "name": "gproxy", - "original_name": "gproxy", - "description": { - "default": "Получить рандомное прокси" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "wproxy", - "original_name": "wproxy", - "description": { - "default": "Проверить работу прокси" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Proxy", - "not_work_proxy": " Прокси не работает", - "no_args": " Нужно быть {}{} {}", - "no_link": " Нету ссылки на прокси! Вставь её в {}cfg proxy", - "incorrect_protocol": "😕 Неверный протокол или его нету в нашей базе!", - "update_link": "😕 Истек срок работы ссылки! Обнови её в {}cfg proxy", - "searching_proxy": "🔄 Ищю прокси...", - "checking_proxy": "🔄 Проверяю прокси..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/ytsearch.py": { - "name": "YTsearch", - "description": "Поиск в Youtube", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/ytsearch.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "ytvsearch": "Поиск видео в Youtube" - }, - { - "ytcsearch": "Поиск каналов в Youtube" - } - ], - "new_commands": [ - { - "name": "ytvsearch", - "original_name": "ytvsearch", - "description": { - "default": "Поиск видео в Youtube" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "ytcsearch", - "original_name": "ytcsearch", - "description": { - "default": "Поиск каналов в Youtube" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "YTsearch", - "no_q": " Должно быть {}{} [запрос]", - "no_result": "😕 Ничего не нашёл по этому запросу", - "searching": "🔄 Поиск в youtube.com...", - "searched": "\n🔎 Результаты поиска {}\n\n🔎 Запрос: {}{}\n\n{} результатов за {} сек\n
" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/freegpt.py": { - "name": "FreeGPT", - "description": "Бесплатный ChatGPT. БЕЗ API. БЕЗ БОТОВ.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/freegpt.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "gf": "Задать вопрос к ChatGPT" - }, - { - "gfi": "Сгенерировать картинку" - } - ], - "new_commands": [ - { - "name": "gf", - "original_name": "gf", - "description": { - "default": "Задать вопрос к ChatGPT" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "gfi", - "original_name": "gfi", - "description": { - "default": "Сгенерировать картинку" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "FreeGPT", - "no_args": " Нужно {}{} {}", - "asking_chatgpt": "🔄 Спрашиваю ChatGPT...\n\n👾 Вы также можете получать ответы в реальном времены с помощью stream_answer в {prefix}cfg FreeGPT", - "creating_image": "🔄 Генерирую изображение...", - "answer_text": "👨‍💻 Вопрос: {question}\n\n🤖 Ответ: {answer}\n\n🖥 Модель: {model}", - "photo_caption": "🖼 Промпт: {prompt}\n \n🖥 Модель: {model}" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/autogh.py": { - "name": "AutoGH", - "description": "Авто-коммиты в Github", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/autogh.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "autocommit": "Включить/выключить автоматический коммит" - } - ], - "new_commands": [ - { - "name": "autocommit", - "original_name": "autocommit", - "description": { - "default": "Включить/выключить автоматический коммит" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AutoGH", - "no_cfg": "🚫 Нету {}! Вставьте его в config через {}cfg AutoGH", - "autocommit_on": "🖥 Авто-коммит включен!", - "autocommit_off": "🚫 Авто-коммит выключен!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/phoneinfo.py": { - "name": "PhoneInfo", - "description": "Информация о телефоне", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/phoneinfo.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "pnsearch": "Поиск телефона" - }, - { - "pninfo": "Получить информацию о телефоне" - } - ], - "new_commands": [ - { - "name": "pnsearch", - "original_name": "pnsearch", - "description": { - "default": "Поиск телефона" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "pninfo", - "original_name": "pninfo", - "description": { - "default": "Получить информацию о телефоне" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "PhoneInfo", - "no_phone": " Нужно {}{} [название телефона]", - "searching": "🔄 Поиск телефона...", - "searching_info": "🔄 Поиск информации о телефоне...", - "no_found": " Не нашёл такой телефон", - "cameras_txt": "📷 Cameras:", - "software_txt": "🖥 Software:", - "hardware_txt": "💾 Hardware:" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/banforaskmod.py": { - "name": "BanForAskMod", - "description": "Бан за просьбу дать модулей", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/banforaskmod.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "bfmsettings": "Открыть настройку модуля" - } - ], - "new_commands": [ - { - "name": "bfmsettings", - "original_name": "bfmsettings", - "description": { - "default": "Открыть настройку модуля" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "BanForAskMod", - "cannot_ban": " Не могу забанить пользователя", - "opening_settings": "🔄 Открываю настройку..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/executor.py": { - "name": "Executor", - "description": "Выполнение python кода", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/executor.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "exec": "Выполнить python код" - } - ], - "new_commands": [ - { - "name": "exec", - "original_name": "execcmd", - "description": { - "default": "Выполнить python код" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Executor", - "no_code": " Должно быть {}exec [python код]", - "executing": "🔄 Выполняю код..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/edmes.py": { - "name": "Edmes", - "description": "Редактирует сообщение с заданим текстом.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/edmes.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "edmsg": "Редактировать" - } - ], - "new_commands": [ - { - "name": "edmsg", - "original_name": "edmsg", - "description": { - "default": "Редактировать" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Edmes" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/avachanger.py": { - "name": "AvaChanger", - "description": "Смена аватарки по времени", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/avachanger.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "avatarl": "Смена аватарки по времени" - }, - { - "avatarlstop": "Выключить смену аватарки по времени" - } - ], - "new_commands": [ - { - "name": "avatarl", - "original_name": "avatarl", - "description": { - "default": "Смена аватарки по времени" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "avatarlstop", - "original_name": "avatarl_stop", - "description": { - "default": "Выключить смену аватарки по времени" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AvaChanger", - "no_args": " Нужно {}avatarl [сколько раз] [сколько ждать перед сменой каждой аватарки]", - "no_reply": " Нужно ответить на сообщение с фоткой", - "changing_avatars": "🔄 Меняю аватарки...\n⏳ Это займёт {} секунд", - "was_off": " Смена аватарки была выключена!", - "off": " Выключил смену аватарки", - "completed": " Готово. Сменил аватарку {} раз за {} секунд/" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/timer.py": { - "name": "Timer", - "description": "Показывает сколько времени осталось", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/timer.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "stime": "Посмотреть сколько осталось времени" - } - ], - "new_commands": [ - { - "name": "stime", - "original_name": "stime", - "description": { - "default": "Посмотреть сколько осталось времени" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Timer", - "no_date": " Добавь сначала дату в {}cfg timer", - "invalid_date": " Неверный формат даты и времени в конфиге.", - "invalid_timezone": " Неверный часовой пояс." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/epsilion.py": { - "name": "Epsilion", - "description": "Авто-фарм в @EpsilionWarBot", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/epsilion.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "eps": "Включить/выключить авто-фарм" - }, - { - "epb": "Включить/выключить авто ежедневный бонус" - }, - { - "epp": "Посмотреть свой профиль" - } - ], - "new_commands": [ - { - "name": "eps", - "original_name": "eps", - "description": { - "default": "Включить/выключить авто-фарм" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "epb", - "original_name": "epb", - "description": { - "default": "Включить/выключить авто ежедневный бонус" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "epp", - "original_name": "epp", - "description": { - "default": "Посмотреть свой профиль" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Epsilion", - "checking_profile": "👀 Смотрю профиль...", - "b_on": "⚡️ Авто-фарм включен!", - "b_off": "🚫 Авто-фарм выключен!", - "bonus_on": "⚡️ Авто ежедневный бонус включен!", - "bonus_off": "🚫 Авто ежедневный бонус выключен!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/giveaways.py": { - "name": "Giveaways", - "description": "Авто-участие в розыгрышах Telegram Premium", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/giveaways.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "gwtg": "Включить/выключить автоматическое участие в розыгрышах Telegram Premium" - } - ], - "new_commands": [ - { - "name": "gwtg", - "original_name": "gwtg", - "description": { - "default": "Включить/выключить автоматическое участие в розыгрышах Telegram Premium" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Giveaways", - "giveaways_on": "🎁 Авто-участие включено!", - "giveaways_off": "🚫 Авто-участие выключено!" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "fajox1/famods/removebg.py": { - "name": "RemoveBG", - "description": "Убрать фон из изображения", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": "https://github.com/FajoX1/FAmods/blob/main/assets/banners/removebg.png?raw=true", - "developer": "@FAmods" - }, - "commands": [ - { - "removebg": "Убрать фон из изображения" - } - ], - "new_commands": [ - { - "name": "removebg", - "original_name": "removebg", - "description": { - "default": "Убрать фон из изображения" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "RemoveBG", - "must_be_forced": " Фото не должно быть сжатым!", - "no_photo": " Нужно ответить на фото!", - "no_token": " Нету токена! Поставь его в {}cfg RemoveBG", - "invalid_token": "😕 Неверный токен", - "only_photo": "😕 Удалять фон можно только с фото (.png, .jpg, .jpeg)", - "removing_bg": "🔄 Удаление фона..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/callcontrol.py": { - "name": "VGCallControllerMod", - "description": "Control group voice calls", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "callstart": "Start call in chat" - }, - { - "callstop": "Stop call in chat" - } - ], - "new_commands": [ - { - "name": "callstart", - "original_name": "callstartcmd", - "description": { - "default": "Start call in chat" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "callstop", - "original_name": "callstopcmd", - "description": { - "default": "Stop call in chat" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "VGCallController" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/quotes.py": { - "name": "mQuotesMod", - "description": "Quote a message using Mishase Quotes API", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "quote": "Quote a message. Args: ? ?file" - }, - { - "fquote": "Fake message quote. Args: @// " - } - ], - "new_commands": [ - { - "name": "quote", - "original_name": "quotecmd", - "description": { - "default": "Quote a message. Args: ? ?file" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "fquote", - "original_name": "fquotecmd", - "description": { - "default": "Fake message quote. Args: @// " - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Quotes" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/audio_editor.py": { - "name": "AudioEditorMod", - "description": "Module for working with sound", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": "@D4n13l3k00" - }, - "commands": [ - { - "bass": ".bass [level bass'а 2-100 (Default 2)] \nBassBoost" - }, - { - "fv": ".fv [level 2-100 (Default 25)] \nDistort" - }, - { - "echos": ".echos \nEcho effect" - }, - { - "volup": ".volup \nVolUp 10dB" - }, - { - "voldw": ".voldw \nVolDw 10dB" - }, - { - "revs": ".revs \nReverse audio" - }, - { - "reps": ".reps \nRepeat audio 2 times" - }, - { - "slows": ".slows \nSlowDown 0.5x" - }, - { - "fasts": ".fasts \nSpeedUp 1.5x" - }, - { - "rights": ".rights \nPush sound to right channel" - }, - { - "lefts": ".lefts \nPush sound to left channel" - }, - { - "norms": ".norms \nNormalize sound (from quiet to normal)" - }, - { - "tovs": ".tovs \nConvert to voice message" - }, - { - "convs": ".convs [audio_format (ex. `mp3`)]\nConvert audio to some format" - }, - { - "byroberts": ".byroberts \nAdd at the end \"Directed by Robert B Weide\"" - }, - { - "cuts": ".cuts \nCut audio" - } - ], - "new_commands": [ - { - "name": "bass", - "original_name": "basscmd", - "description": { - "default": ".bass [level bass'а 2-100 (Default 2)] \nBassBoost" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "fv", - "original_name": "fvcmd", - "description": { - "default": ".fv [level 2-100 (Default 25)] \nDistort" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "echos", - "original_name": "echoscmd", - "description": { - "default": ".echos \nEcho effect" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "volup", - "original_name": "volupcmd", - "description": { - "default": ".volup \nVolUp 10dB" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "voldw", - "original_name": "voldwcmd", - "description": { - "default": ".voldw \nVolDw 10dB" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "revs", - "original_name": "revscmd", - "description": { - "default": ".revs \nReverse audio" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "reps", - "original_name": "repscmd", - "description": { - "default": ".reps \nRepeat audio 2 times" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "slows", - "original_name": "slowscmd", - "description": { - "default": ".slows \nSlowDown 0.5x" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "fasts", - "original_name": "fastscmd", - "description": { - "default": ".fasts \nSpeedUp 1.5x" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "rights", - "original_name": "rightscmd", - "description": { - "default": ".rights \nPush sound to right channel" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "lefts", - "original_name": "leftscmd", - "description": { - "default": ".lefts \nPush sound to left channel" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "norms", - "original_name": "normscmd", - "description": { - "default": ".norms \nNormalize sound (from quiet to normal)" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "tovs", - "original_name": "tovscmd", - "description": { - "default": ".tovs \nConvert to voice message" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "convs", - "original_name": "convscmd", - "description": { - "default": ".convs [audio_format (ex. `mp3`)]\nConvert audio to some format" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "byroberts", - "original_name": "byrobertscmd", - "description": { - "default": ".byroberts \nAdd at the end \"Directed by Robert B Weide\"" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "cuts", - "original_name": "cutscmd", - "description": { - "default": ".cuts \nCut audio" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "AudioEditor", - "downloading": "[{}] Downloading...", - "working": "[{}] Working...", - "exporting": "[{}] Exporting...", - "set_value": "[{}] Specify the level from {} to {}...", - "reply": "[{}] reply to audio...", - "set_fmt": "[{}] Specify the format of output audio...", - "set_time": "[{}] Specify the time in the format start(ms):end(ms)" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/translate.py": { - "name": "TranslatorMod", - "description": "Translator Module", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "gtrsl": "Use it: .gtrsl \n or .gtrsl ; langs" - }, - { - "translate": "Translate text via Yandex Translate" - } - ], - "new_commands": [ - { - "name": "gtrsl", - "original_name": "gtrslcmd", - "description": { - "default": "Use it: .gtrsl \n or .gtrsl ; langs" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "translate", - "original_name": "translatecmd", - "description": { - "default": "Translate text via Yandex Translate" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Translate" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/pmlog.py": { - "name": "PMLogMod", - "description": "Logs unwanted PMs to a channel", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "logpm": "Begins logging PMs" - }, - { - "unlogpm": "Stops logging PMs" - } - ], - "new_commands": [ - { - "name": "logpm", - "original_name": "logpmcmd", - "description": { - "default": "Begins logging PMs" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "unlogpm", - "original_name": "unlogpmcmd", - "description": { - "default": "Stops logging PMs" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "PM Logger", - "start": "Your conversation is now being logged", - "not_pm": "You can't log a group", - "stopped": "Your conversation is no longer being logged", - "log_group_cfg_doc": "Group or channel ID where to send the logged PMs" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/warn.py": { - "name": "WarnsMod", - "description": "Система предупреждений.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "warn": "Выдать варн. Используй: .warn <@ или реплай>." - }, - { - "warnslimit": "Установить лимит предупреждений. Используй: .warnslimit <кол-во:int>." - }, - { - "warns": "Посмотреть кол-во варнов. Используй: .warns <@ или реплай> или ." - }, - { - "swarn": "Изменить режим ограничения. Используй: .swarn ." - }, - { - "clearwarns": "Очистить все варны. Используй: .clearwarns <@ или реплай>." - } - ], - "new_commands": [ - { - "name": "warn", - "original_name": "warncmd", - "description": { - "default": "Выдать варн. Используй: .warn <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "warnslimit", - "original_name": "warnslimitcmd", - "description": { - "default": "Установить лимит предупреждений. Используй: .warnslimit <кол-во:int>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "warns", - "original_name": "warnscmd", - "description": { - "default": "Посмотреть кол-во варнов. Используй: .warns <@ или реплай> или ." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "swarn", - "original_name": "swarncmd", - "description": { - "default": "Изменить режим ограничения. Используй: .swarn ." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "clearwarns", - "original_name": "clearwarnscmd", - "description": { - "default": "Очистить все варны. Используй: .clearwarns <@ или реплай>." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Warns" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/autoprofile.py": { - "name": "AutoProfileMod", - "description": "Automatic stuff for your profile :P", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "autopfp": "Rotates your profile picture every 60 seconds with x degrees, usage:\n.autopfp \n\nDegrees - 60, -10, etc\nRemove last pfp - True/1/False/0, case sensitive" - }, - { - "stopautopfp": "Stop autobio cmd." - }, - { - "autobio": "Automatically changes your account's bio with current time, usage:\n.autobio ''" - }, - { - "stopautobio": "Stop autobio cmd." - }, - { - "autoname": "Automatically changes your Telegram name with current time, usage:\n.autoname ''" - }, - { - "stopautoname": "Stop autoname cmd." - }, - { - "delpfp": "Remove x profile pic(s) from your profile.\n.delpfp " - } - ], - "new_commands": [ - { - "name": "autopfp", - "original_name": "autopfpcmd", - "description": { - "default": "Rotates your profile picture every 60 seconds with x degrees, usage:\n.autopfp \n\nDegrees - 60, -10, etc\nRemove last pfp - True/1/False/0, case sensitive" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "stopautopfp", - "original_name": "stopautopfpcmd", - "description": { - "default": "Stop autobio cmd." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "autobio", - "original_name": "autobiocmd", - "description": { - "default": "Automatically changes your account's bio with current time, usage:\n.autobio ''" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "stopautobio", - "original_name": "stopautobiocmd", - "description": { - "default": "Stop autobio cmd." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "autoname", - "original_name": "autonamecmd", - "description": { - "default": "Automatically changes your Telegram name with current time, usage:\n.autoname ''" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "stopautoname", - "original_name": "stopautonamecmd", - "description": { - "default": "Stop autoname cmd." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "delpfp", - "original_name": "delpfpcmd", - "description": { - "default": "Remove x profile pic(s) from your profile.\n.delpfp " - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Automatic Profile", - "missing_pil": "You don't have Pillow installed", - "missing_pfp": "You don't have a profile picture to rotate", - "invalid_args": "Missing parameters, please read the docs", - "invalid_degrees": "Invalid number of degrees to rotate, please read the docs", - "invalid_delete": "Please specify whether to delete the old pictures or not", - "enabled_pfp": "Enabled profile picture rotation", - "pfp_not_enabled": "Profile picture rotation is not enabled", - "pfp_disabled": "Profile picture rotation disabled", - "missing_time": "Time was not specified in bio", - "enabled_bio": "Enabled bio clock", - "bio_not_enabled": "Bio clock is not enabled", - "disabled_bio": "Disabled bio clock", - "enabled_name": "Enabled name clock", - "name_not_enabled": "Name clock is not enabled", - "disabled_name": "Name clock disabled", - "how_many_pfps": "Please specify how many profile pictures should be removed", - "invalid_pfp_count": "Invalid number of profile pictures to remove", - "removed_pfps": "Removed {} profile pic(s)" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/filter.py": { - "name": "FiltersMod", - "description": "Filters module", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "filter": "Adds a filter into the list." - }, - { - "stop": "Removes a filter from the list." - }, - { - "stopall": "Clears out the filter list." - }, - { - "filters": "Shows saved filters." - } - ], - "new_commands": [ - { - "name": "filter", - "original_name": "filtercmd", - "description": { - "default": "Adds a filter into the list." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "stop", - "original_name": "stopcmd", - "description": { - "default": "Removes a filter from the list." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "stopall", - "original_name": "stopallcmd", - "description": { - "default": "Clears out the filter list." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "filters", - "original_name": "filterscmd", - "description": { - "default": "Shows saved filters." - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Filters" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/information.py": { - "name": "WhoIsMod", - "description": "Get info about user/chat", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "userinfo": "<@ or reply or id> - info about user" - }, - { - "chatinfo": "<@ or id> - info about chat" - } - ], - "new_commands": [ - { - "name": "userinfo", - "original_name": "userinfocmd", - "description": { - "default": "<@ or reply or id> - info about user" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "chatinfo", - "original_name": "chatinfocmd", - "description": { - "default": "<@ or id> - info about chat" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Information" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/purge.py": { - "name": "PurgeMod", - "description": "Deletes your messages", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "purge": "Purge from the replied message" - }, - { - "del": "Delete the replied message" - } - ], - "new_commands": [ - { - "name": "purge", - "original_name": "purgecmd", - "description": { - "default": "Purge from the replied message" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "del", - "original_name": "delcmd", - "description": { - "default": "Delete the replied message" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Purge", - "from_where": "Which messages should be purged?", - "not_supergroup_bot": "Purges can only take place in supergroups", - "delete_what": "What message should be deleted?" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/qr_code.py": { - "name": "QRtoolsMod", - "description": "Generator and reader of QR codes", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "makeqr": ".makeqr " - }, - { - "readqr": ".readqr " - } - ], - "new_commands": [ - { - "name": "makeqr", - "original_name": "makeqrcmd", - "description": { - "default": ".makeqr " - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "readqr", - "original_name": "readqrcmd", - "description": { - "default": ".readqr " - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "QR Code" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/distort.py": { - "name": "DistortMod", - "description": "Stickers or photo distort", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "tgs": "Animated stickers distort" - }, - { - "distort": ".distort \n.distort im\n.distort 50\n.distort 50 im\n.distort im 50\nim => sends as photo\n50 => (from 0 to 100) percent of distortion, 0 is maximum distortion" - }, - { - "jpegd": "JPEG style distort" - } - ], - "new_commands": [ - { - "name": "tgs", - "original_name": "tgscmd", - "description": { - "default": "Animated stickers distort" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "distort", - "original_name": "distortcmd", - "description": { - "default": ".distort \n.distort im\n.distort 50\n.distort 50 im\n.distort im 50\nim => sends as photo\n50 => (from 0 to 100) percent of distortion, 0 is maximum distortion" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "jpegd", - "original_name": "jpegdcmd", - "description": { - "default": "JPEG style distort" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Distort", - "bad_input": "Reply to image or stick!", - "processing": "Distorting...", - "bad_input_tgs": "Reply to animated sticker" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/image_tools.py": { - "name": "ImageToolsMod", - "description": "Image tools module", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "ll": "Mirror the image" - }, - { - "rr": "Mirror the image" - }, - { - "uu": "Mirror the image" - }, - { - "dd": "Mirror the image" - }, - { - "dotify": "Image to RGB dots" - }, - { - "dotifi": "Image to BW dots" - }, - { - "soap": ".soap " - }, - { - "pic2pack": "Create sticker pack with your photo" - }, - { - "deep": "Deep the image" - } - ], - "new_commands": [ - { - "name": "ll", - "original_name": "llcmd", - "description": { - "default": "Mirror the image" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "rr", - "original_name": "rrcmd", - "description": { - "default": "Mirror the image" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "uu", - "original_name": "uucmd", - "description": { - "default": "Mirror the image" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "dd", - "original_name": "ddcmd", - "description": { - "default": "Mirror the image" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "dotify", - "original_name": "dotifycmd", - "description": { - "default": "Image to RGB dots" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "dotifi", - "original_name": "dotificmd", - "description": { - "default": "Image to BW dots" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "soap", - "original_name": "soapcmd", - "description": { - "default": ".soap " - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "pic2pack", - "original_name": "pic2packcmd", - "description": { - "default": "Create sticker pack with your photo" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "deep", - "original_name": "deepcmd", - "description": { - "default": "Deep the image" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Image Tools" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/lmgtfy.py": { - "name": "LMGTFYMod", - "description": "Let me Google that for you, coz you too lazy to do that yourself.", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "lmgtfy": "Use in reply to another message or as .lmgtfy " - } - ], - "new_commands": [ - { - "name": "lmgtfy", - "original_name": "lmgtfycmd", - "description": { - "default": "Use in reply to another message or as .lmgtfy " - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "LetMeGoogleThatForYou", - "result": "Here you go, help yourself.\n{}", - "default": "How to use Google?" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/calculator.py": { - "name": "CalculatorMod", - "description": "Calculator module", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "calc": ".calc 2 * 2" - } - ], - "new_commands": [ - { - "name": "calc", - "original_name": "calccmd", - "description": { - "default": ".calc 2 * 2" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "Calculator" - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/video_editor.py": { - "name": "VideoEditorMod", - "description": "Module for working with video", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": "@D4n13l3k00" - }, - "commands": [ - { - "xflipv": ".xflipv - Flip video by X" - }, - { - "yflipv": ".yflipv - Flip video by Y" - }, - { - "bwv": ".bwv - BlackWhite" - }, - { - "revv": ".revv - Reverse video" - }, - { - "paintv": ".paintv - Paint effect" - }, - { - "invertv": ".invertv - Invert colors" - }, - { - "rmsv": ".rmsv - Remove sound (to gif without compression)" - }, - { - "cutv": ".cutv - Cut video" - }, - { - "audv": ".audv - Add audio to video" - }, - { - "fpsv": ".fpsv - Change fps" - }, - { - "marginv": ".marginv - Add marging" - }, - { - "speedv": ".speedv - Speed" - }, - { - "contrastv": ".contrastv - Contrast" - }, - { - "lumv": ".lumv - Lum" - }, - { - "scalev": ".scalev - Scale(\"Resize\") video" - } - ], - "new_commands": [ - { - "name": "xflipv", - "original_name": "xflipvcmd", - "description": { - "default": ".xflipv - Flip video by X" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "yflipv", - "original_name": "yflipvcmd", - "description": { - "default": ".yflipv - Flip video by Y" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "bwv", - "original_name": "bwvcmd", - "description": { - "default": ".bwv - BlackWhite" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "revv", - "original_name": "revvcmd", - "description": { - "default": ".revv - Reverse video" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "paintv", - "original_name": "paintvcmd", - "description": { - "default": ".paintv - Paint effect" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "invertv", - "original_name": "invertvcmd", - "description": { - "default": ".invertv - Invert colors" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "rmsv", - "original_name": "rmsvcmd", - "description": { - "default": ".rmsv - Remove sound (to gif without compression)" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "cutv", - "original_name": "cutvcmd", - "description": { - "default": ".cutv - Cut video" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "audv", - "original_name": "audvcmd", - "description": { - "default": ".audv - Add audio to video" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "fpsv", - "original_name": "fpsvcmd", - "description": { - "default": ".fpsv - Change fps" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "marginv", - "original_name": "marginvcmd", - "description": { - "default": ".marginv - Add marging" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "speedv", - "original_name": "speedvcmd", - "description": { - "default": ".speedv - Speed" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "contrastv", - "original_name": "contrastvcmd", - "description": { - "default": ".contrastv - Contrast" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "lumv", - "original_name": "lumvcmd", - "description": { - "default": ".lumv - Lum" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - }, - { - "name": "scalev", - "original_name": "scalevcmd", - "description": { - "default": ".scalev - Scale(\"Resize\") video" - }, - "cmd_names": {}, - "aliases": [], - "usage": null, - "inline": false, - "is_inline_handler": false, - "decorators": [] - } - ], - "inline_handlers": [], - "strings": { - "name": "VideoEditor", - "downloading": "[{}] Downloading...", - "working": "[{}] Working...", - "exporting": "[{}] Exporting...", - "set_value": "[{}] Specify the level from {} to {}...", - "reply": "[{}] reply to video/gif...", - "set_time": "[{}] Specify the time in the format start(ms):end(ms)", - "set_link": "[{}] Enter link..." - }, - "has_on_load": false, - "has_on_unload": false, - "class_cmd_names": {} - }, - "GeekTG/FTG-Modules/admin_tools.py": { - "name": "AdminToolsMod", - "description": "Admin Tools", - "cls_doc": {}, - "meta": { - "pic": null, - "banner": null, - "developer": null - }, - "commands": [ - { - "ecp": "Command .ecp changes the pic of the chat.\nUse: .ecp ." - }, - { - "promote": "Command .promote for promote user to admin rights.\nUse: .promote <@ or reply> ." - }, - { - "demote": "Command .demote for demote user to admin rights.\nUse: .demote <@ or reply>." - }, - { - "pin": "Command .pin for pin message in the chat.\nUse: .pin ." - }, - { - "unpin": "Command .unpin for unpin message in the chat.\nUse: .unpin." - }, - { - "kick": "Command .kick for kick the user.\nUse: .kick <@ or reply>." - }, - { - "ban": "Command .ban for ban the user.\nUse: .ban <@ or reply>." - }, - { - "unban": "Command .unban for unban the user.\nUse: .unban <@ or reply>." - }, - { - "mute": "Command .mute for mute the user.\nUse: .mute <@ or reply>