mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 06:24:18 +02:00
Added and updated repositories 2026-04-12 13:56:57
This commit is contained in:
326
KorenbZla/HikkaModules/InvalidFiles.py
Normal file
326
KorenbZla/HikkaModules/InvalidFiles.py
Normal file
@@ -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": "<emoji document_id=5456307331644037599>❌</emoji> <b>Invalid size format.</b>",
|
||||
"max_size": "<emoji document_id=5456307331644037599>❌</emoji> <b>Maximum file size is 2GB</b>",
|
||||
"file_created": (
|
||||
"<emoji document_id=5458805056990119991>✅</emoji><b> File successfully created and sent.</b>\n\n"
|
||||
"<blockquote>"
|
||||
"<emoji document_id=5456625794879099391>👤</emoji> <b>File name:</b> <code>{}</code>\n"
|
||||
"<emoji document_id=5456569114195692172>⚖️</emoji> <b>Size:</b> <code>{}{}</code>\n"
|
||||
"<emoji document_id=5456591761558245861>⌛️</emoji> <b>Creation:</b> <code>{:.2f} sec.</code>\n"
|
||||
"<tg-emoji emoji-id=5456350521835163323>📤</tg-emoji> <b>Upload:</b> <code>{:.2f} sec.</code>"
|
||||
"</blockquote>"
|
||||
),
|
||||
"invalid_args": (
|
||||
"<emoji document_id=5456307331644037599>❌</emoji><b> Invalid arguments</b>\n\n"
|
||||
"<b>Usage:</b> <code>{prefix}cifile <name> <size></code>\n"
|
||||
"<b>Example:</b> <code>{prefix}cifile test.txt 3.4mb</code>\n\n"
|
||||
"<i>Supported: b, kb, mb, gb</i>"
|
||||
),
|
||||
"creating": "<emoji document_id=5456591761558245861>⌛️</emoji> <b>Creating file...\n\n<i>*Large files may take a long time to upload.</i></b>",
|
||||
"error": "<emoji document_id=5456537889783452967>⚠️</emoji> <b>Error:</b>\n<i>{}</i>",
|
||||
"formats": (
|
||||
"<emoji document_id=5456367813373498016>📂</emoji> <b>Popular file extensions:</b>\n\n"
|
||||
"<b>📄 Documents:</b> <code>.txt .docx .pdf .rtf</code>\n"
|
||||
"<b>📊 Spreadsheets:</b> <code>.xlsx .csv</code>\n"
|
||||
"<b>📈 Presentations:</b> <code>.pptx</code>\n"
|
||||
"<b>🖼️ Images:</b> <code>.jpg .png .gif .bmp .webp</code>\n"
|
||||
"<b>🎵 Audio:</b> <code>.mp3 .wav .flac</code>\n"
|
||||
"<b>🎬 Video:</b> <code>.mp4 .mkv .avi</code>\n"
|
||||
"<b>📦 Archives:</b> <code>.zip .rar .7z</code>\n"
|
||||
"<b>💻 Code:</b> <code>.py .js .html .css .json</code>"
|
||||
),
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"invalid_format": "<emoji document_id=5456307331644037599>❌</emoji> <b>Неверный формат размера.</b>",
|
||||
"max_size": "<emoji document_id=5456307331644037599>❌</emoji> <b>Максимальный размер файла — 2GB</b>",
|
||||
"file_created": (
|
||||
"<emoji document_id=5458805056990119991>✅</emoji><b> Файл успешно создан и отправлен.</b>\n\n"
|
||||
"<blockquote>"
|
||||
"<emoji document_id=5456625794879099391>👤</emoji> <b>Имя файла:</b> <code>{}</code>\n"
|
||||
"<emoji document_id=5456569114195692172>⚖️</emoji> <b>Размер:</b> <code>{}{}</code>\n"
|
||||
"<emoji document_id=5456591761558245861>⌛️</emoji> <b>Создание:</b> <code>{:.2f} сек.</code>\n"
|
||||
"<tg-emoji emoji-id=5456350521835163323>📤</tg-emoji> <b>Отправка:</b> <code>{:.2f} сек.</code>"
|
||||
"</blockquote>"
|
||||
),
|
||||
"invalid_args": (
|
||||
"<emoji document_id=5456307331644037599>❌</emoji><b> Неверные аргументы</b>\n\n"
|
||||
"<b>Использование:</b> <code>{prefix}cifile <имя> <размер></code>\n"
|
||||
"<b>Пример:</b> <code>{prefix}cifile test.txt 3.4mb</code>\n\n"
|
||||
"<i>Поддерживаются: b, kb, mb, gb</i>"
|
||||
),
|
||||
"creating": "<emoji document_id=5456591761558245861>⌛️</emoji> <b>Создаю файл...\n\n<i>*Файлы большого размера могут долго загружаться.</i></b>",
|
||||
"error": "<emoji document_id=5456537889783452967>⚠️</emoji> <b>Ошибка:</b>\n<i>{}</i>",
|
||||
"formats": (
|
||||
"<emoji document_id=5456367813373498016>📂</emoji> <b>Популярные расширения файлов:</b>\n\n"
|
||||
"<b>📄 Документы:</b> <code>.txt .docx .pdf .rtf</code>\n"
|
||||
"<b>📊 Таблицы:</b> <code>.xlsx .csv</code>\n"
|
||||
"<b>📈 Презентации:</b> <code>.pptx</code>\n"
|
||||
"<b>🖼️ Изображения:</b> <code>.jpg .png .gif .bmp .webp</code>\n"
|
||||
"<b>🎵 Аудио:</b> <code>.mp3 .wav .flac</code>\n"
|
||||
"<b>🎬 Видео:</b> <code>.mp4 .mkv .avi</code>\n"
|
||||
"<b>📦 Архивы:</b> <code>.zip .rar .7z</code>\n"
|
||||
"<b>💻 Код:</b> <code>.py .js .html .css .json</code>"
|
||||
),
|
||||
}
|
||||
|
||||
strings_uz = {
|
||||
"invalid_format": "<emoji document_id=5456307331644037599>❌</emoji> <b>Hajm formati noto‘g‘ri.</b>",
|
||||
"max_size": "<emoji document_id=5456307331644037599>❌</emoji> <b>Maksimal fayl hajmi — 2GB</b>",
|
||||
"file_created": (
|
||||
"<emoji document_id=5458805056990119991>✅</emoji><b> Fayl muvaffaqiyatli yaratildi va yuborildi.</b>\n\n"
|
||||
"<blockquote>"
|
||||
"<emoji document_id=5456625794879099391>👤</emoji> <b>Fayl nomi:</b> <code>{}</code>\n"
|
||||
"<emoji document_id=5456569114195692172>⚖️</emoji> <b>Hajmi:</b> <code>{}{}</code>\n"
|
||||
"<emoji document_id=5456591761558245861>⌛️</emoji> <b>Yaratish:</b> <code>{:.2f} sek.</code>\n"
|
||||
"<tg-emoji emoji-id=5456350521835163323>📤</tg-emoji> <b>Yuborish:</b> <code>{:.2f} sek.</code>"
|
||||
"</blockquote>"
|
||||
),
|
||||
"invalid_args": (
|
||||
"<emoji document_id=5456307331644037599>❌</emoji><b> Noto‘g‘ri argumentlar</b>\n\n"
|
||||
"<b>Foydalanish:</b> <code>{prefix}cifile <nom> <hajm></code>\n"
|
||||
"<b>Misol:</b> <code>{prefix}cifile test.txt 3.4mb</code>\n\n"
|
||||
"<i>Qo‘llab-quvvatlanadi: b, kb, mb, gb</i>"
|
||||
),
|
||||
"creating": "<emoji document_id=5456591761558245861>⌛️</emoji> <b>Fayl yaratilmoqda...\n\n<i>*Katta fayllar uzoq yuklanishi mumkin.</i></b>",
|
||||
"error": "<emoji document_id=5456537889783452967>⚠️</emoji> <b>Xatolik:</b>\n<i>{}</i>",
|
||||
"formats": (
|
||||
"<emoji document_id=5456367813373498016>📂</emoji> <b>Mashhur fayl kengaytmalari:</b>\n\n"
|
||||
"<b>📄 Hujjatlar:</b> <code>.txt .docx .pdf .rtf</code>\n"
|
||||
"<b>📊 Jadvallar:</b> <code>.xlsx .csv</code>\n"
|
||||
"<b>📈 Taqdimotlar:</b> <code>.pptx</code>\n"
|
||||
"<b>🖼️ Rasmlar:</b> <code>.jpg .png .gif .bmp .webp</code>\n"
|
||||
"<b>🎵 Audio:</b> <code>.mp3 .wav .flac</code>\n"
|
||||
"<b>🎬 Video:</b> <code>.mp4 .mkv .avi</code>\n"
|
||||
"<b>📦 Arxivlar:</b> <code>.zip .rar .7z</code>\n"
|
||||
"<b>💻 Kod:</b> <code>.py .js .html .css .json</code>"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
strings_de = {
|
||||
"invalid_format": "<emoji document_id=5456307331644037599>❌</emoji> <b>Ungültiges Größenformat.</b>",
|
||||
"max_size": "<emoji document_id=5456307331644037599>❌</emoji> <b>Maximale Dateigröße — 2GB</b>",
|
||||
"file_created": (
|
||||
"<emoji document_id=5458805056990119991>✅</emoji><b> Datei erfolgreich erstellt und gesendet.</b>\n\n"
|
||||
"<blockquote>"
|
||||
"<emoji document_id=5456625794879099391>👤</emoji> <b>Dateiname:</b> <code>{}</code>\n"
|
||||
"<emoji document_id=5456569114195692172>⚖️</emoji> <b>Größe:</b> <code>{}{}</code>\n"
|
||||
"<emoji document_id=5456591761558245861>⌛️</emoji> <b>Erstellung:</b> <code>{:.2f} Sek.</code>\n"
|
||||
"<tg-emoji emoji-id=5456350521835163323>📤</tg-emoji> <b>Upload:</b> <code>{:.2f} Sek.</code>"
|
||||
"</blockquote>"
|
||||
),
|
||||
"invalid_args": (
|
||||
"<emoji document_id=5456307331644037599>❌</emoji><b> Ungültige Argumente</b>\n\n"
|
||||
"<b>Verwendung:</b> <code>{prefix}cifile <name> <größe></code>\n"
|
||||
"<b>Beispiel:</b> <code>{prefix}cifile test.txt 3.4mb</code>\n\n"
|
||||
"<i>Unterstützt: b, kb, mb, gb</i>"
|
||||
),
|
||||
"creating": "<emoji document_id=5456591761558245861>⌛️</emoji> <b>Datei wird erstellt...\n\n<i>*Große Dateien können lange zum Hochladen brauchen.</i></b>",
|
||||
"error": "<emoji document_id=5456537889783452967>⚠️</emoji> <b>Fehler:</b>\n<i>{}</i>",
|
||||
"formats": (
|
||||
"<emoji document_id=5456367813373498016>📂</emoji> <b>Beliebte Dateiendungen:</b>\n\n"
|
||||
"<b>📄 Dokumente:</b> <code>.txt .docx .pdf .rtf</code>\n"
|
||||
"<b>📊 Tabellen:</b> <code>.xlsx .csv</code>\n"
|
||||
"<b>📈 Präsentationen:</b> <code>.pptx</code>\n"
|
||||
"<b>🖼️ Bilder:</b> <code>.jpg .png .gif .bmp .webp</code>\n"
|
||||
"<b>🎵 Audio:</b> <code>.mp3 .wav .flac</code>\n"
|
||||
"<b>🎬 Video:</b> <code>.mp4 .mkv .avi</code>\n"
|
||||
"<b>📦 Archive:</b> <code>.zip .rar .7z</code>\n"
|
||||
"<b>💻 Code:</b> <code>.py .js .html .css .json</code>"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
strings_es = {
|
||||
"invalid_format": "<emoji document_id=5456307331644037599>❌</emoji> <b>Formato de tamaño inválido.</b>",
|
||||
"max_size": "<emoji document_id=5456307331644037599>❌</emoji> <b>El tamaño máximo del archivo es 2GB</b>",
|
||||
"file_created": (
|
||||
"<emoji document_id=5458805056990119991>✅</emoji><b> Archivo creado y enviado correctamente.</b>\n\n"
|
||||
"<blockquote>"
|
||||
"<emoji document_id=5456625794879099391>👤</emoji> <b>Nombre del archivo:</b> <code>{}</code>\n"
|
||||
"<emoji document_id=5456569114195692172>⚖️</emoji> <b>Tamaño:</b> <code>{}{}</code>\n"
|
||||
"<emoji document_id=5456591761558245861>⌛️</emoji> <b>Creación:</b> <code>{:.2f} seg.</code>\n"
|
||||
"<tg-emoji emoji-id=5456350521835163323>📤</tg-emoji> <b>Subida:</b> <code>{:.2f} seg.</code>"
|
||||
"</blockquote>"
|
||||
),
|
||||
"invalid_args": (
|
||||
"<emoji document_id=5456307331644037599>❌</emoji><b> Argumentos inválidos</b>\n\n"
|
||||
"<b>Uso:</b> <code>{prefix}cifile <nombre> <tamaño></code>\n"
|
||||
"<b>Ejemplo:</b> <code>{prefix}cifile test.txt 3.4mb</code>\n\n"
|
||||
"<i>Soportado: b, kb, mb, gb</i>"
|
||||
),
|
||||
"creating": "<emoji document_id=5456591761558245861>⌛️</emoji> <b>Creando archivo...\n\n<i>*Los archivos grandes pueden tardar en subirse.</i></b>",
|
||||
"error": "<emoji document_id=5456537889783452967>⚠️</emoji> <b>Error:</b>\n<i>{}</i>",
|
||||
"formats": (
|
||||
"<emoji document_id=5456367813373498016>📂</emoji> <b>Extensiones de archivo populares:</b>\n\n"
|
||||
"<b>📄 Documentos:</b> <code>.txt .docx .pdf .rtf</code>\n"
|
||||
"<b>📊 Hojas de cálculo:</b> <code>.xlsx .csv</code>\n"
|
||||
"<b>📈 Presentaciones:</b> <code>.pptx</code>\n"
|
||||
"<b>🖼️ Imágenes:</b> <code>.jpg .png .gif .bmp .webp</code>\n"
|
||||
"<b>🎵 Audio:</b> <code>.mp3 .wav .flac</code>\n"
|
||||
"<b>🎬 Video:</b> <code>.mp4 .mkv .avi</code>\n"
|
||||
"<b>📦 Archivos:</b> <code>.zip .rar .7z</code>\n"
|
||||
"<b>💻 Código:</b> <code>.py .js .html .css .json</code>"
|
||||
),
|
||||
}
|
||||
|
||||
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="<fayl>.<format> <hajm> — buzilgan fayl yaratish",
|
||||
de_doc="<datei>.<format> <größe> — beschädigte Datei erstellen",
|
||||
es_doc="<archivo>.<formato> <tamaño> — crear archivo corrupto",
|
||||
alias="cifile"
|
||||
)
|
||||
async def CreateInvalidFile(self, message: Message):
|
||||
"""<file>.<format> <size> - 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'))
|
||||
1952
SenkoGuardian/SenModules/ChatCopy.py
Normal file
1952
SenkoGuardian/SenModules/ChatCopy.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,13 +64,39 @@ 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"<code>[{record.levelname}]</code> {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"
|
||||
|
||||
# requires: google-genai google-api-core pytz markdown_it_py
|
||||
@@ -149,7 +183,7 @@ class Gemini(loader.Module):
|
||||
"gme_chat_not_found": "🚫 <b>Не удалось найти чат для экспорта:</b> <code>{}</code>",
|
||||
"gme_sent_to_saved": "💾 История экспортирована в избранное.",
|
||||
"new_sdk_missing": "⚠️ <b>Для работы модуля нужна библиотека google-genai.</b>\nВыполните: <code>pip install google-genai</code>",
|
||||
"gprompt_usage": "ℹ️ <b>Использование:</b>\n<code>.gprompt <текст></code> — установить промпт.\n<code>.gprompt -c</code> — очистить.\nИли ответьте на <b>.txt</b> файл.",
|
||||
"gprompt_usage": "ℹ️ <b>Использование:</b>\n<code>.gprompt <текст/пресет></code> — установить.\n<code>.gprompt -c</code> — очистить.\n<code>.gpresets</code> — база пресетов.",
|
||||
"gprompt_updated": "✅ <b>Системный промпт обновлен!</b>\nДлина: {} символов.",
|
||||
"gprompt_cleared": "🗑 <b>Системный промпт очищен.</b>",
|
||||
"gprompt_current": "📝 <b>Текущий системный промпт:</b>",
|
||||
@@ -159,7 +193,6 @@ class Gemini(loader.Module):
|
||||
"gmodel_no_models": "⚠️ Не удалось получить список моделей.",
|
||||
"gmodel_list_error": "❗️ Ошибка получения списка: {}",
|
||||
"gimg_process": "<emoji document_id=5325547803936572038>✨</emoji> <b>Генерация...</b>\n🧠 <i>Модель: {model}</i>",
|
||||
"gprompt_usage": "ℹ️ <b>Использование:</b>\n<code>.gprompt <текст/пресет></code> — установить.\n<code>.gprompt -c</code> — очистить.\n<code>.gpresets</code> — база пресетов.",
|
||||
"gpresets_usage": (
|
||||
"ℹ️ <b>Управление пресетами:</b>\n"
|
||||
"• <code>.gpresets save [Имя] текст</code> — сохранить (имя в скобках, если с пробелами).\n"
|
||||
@@ -173,13 +206,13 @@ class Gemini(loader.Module):
|
||||
"gpreset_not_found": "🚫 Пресет с таким именем или индексом не найден.",
|
||||
"gpreset_list_head": "📋 <b>Ваши пресеты:</b>\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()),
|
||||
@@ -219,7 +252,6 @@ class Gemini(loader.Module):
|
||||
self.memory_disabled_chats = set()
|
||||
self.pager_cache = {}
|
||||
self.key_model_map = {}
|
||||
self.prompt_presets = []
|
||||
self.api_keys =[]
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
@@ -243,13 +275,37 @@ class Gemini(loader.Module):
|
||||
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.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 =[]
|
||||
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"⚠️ <b>Ошибка FFmpeg:</b>\nНе удалось конвертировать '{filename}'. Детали:\n<code>{utils.escape_html(stderr_str)}</code>")
|
||||
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,15 +428,14 @@ 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 =[]
|
||||
@@ -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"<i>OpenRouter: <code>{target_model}</code></i>"
|
||||
response_html = self._markdown_to_html(result_text)
|
||||
formatted_body = self._format_response_with_smart_separation(response_html)
|
||||
question_html = f"<blockquote>{utils.escape_html(request_text_for_display[:200])}</blockquote>"
|
||||
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('<emoji document_id=', '<tg-emoji emoji-id=').replace('</emoji>', '</tg-emoji>')
|
||||
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)
|
||||
@@ -486,15 +552,14 @@ class Gemini(loader.Module):
|
||||
)
|
||||
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"<blockquote>{utils.escape_html(request_text_for_display[:200])}</blockquote>"
|
||||
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"<i>Модель: <code>{self.config['model_name']}</code></i>"
|
||||
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']} <blockquote>{utils.escape_html(request_text_for_display[:100])}...</blockquote>\n\n{self.strings['response_prefix']}{search_icon}\n"
|
||||
header = f"{mem_indicator}\n{model_info}\n{self.strings['question_prefix']} <blockquote>{utils.escape_html(request_text_for_display[:100])}...</blockquote>\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"
|
||||
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("Ответ длинный, отправляю файлом...", show_alert=False)
|
||||
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:
|
||||
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"<blockquote expandable='true'>{utils.escape_html(request_text_for_display[:180])}</blockquote>"
|
||||
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('<emoji document_id=', '<tg-emoji emoji-id=').replace('</emoji>', '</tg-emoji>')
|
||||
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,38 +770,49 @@ 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)
|
||||
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=types.GenerateContentConfig(safety_settings=[types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE")])
|
||||
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"<b>{header}</b>\n\n{self.strings['question_prefix']}\n<blockquote expandable>{utils.escape_html(user_prompt)}</blockquote>\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"<b>{header}</b>\n\nQ: <blockquote>{utils.escape_html(user_prompt)}</blockquote>\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)
|
||||
await utils.answer(status_msg, text_to_send)
|
||||
except Exception as e:
|
||||
await utils.answer(status_msg, self._handle_error(e))
|
||||
|
||||
@@ -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:
|
||||
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["no_memory_to_clear"])
|
||||
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):
|
||||
"""[<id/@юз чата>] [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" из чата <code>{src_id}</code>"
|
||||
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" из чата <code>{source_chat_id}</code>"
|
||||
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)
|
||||
|
||||
try:
|
||||
f = await self.client.download_media(reply, bytes)
|
||||
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
|
||||
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}")
|
||||
try:
|
||||
hist = json.load(file)
|
||||
if not isinstance(hist, list): raise ValueError("Файл не содержит список истории.")
|
||||
new_hist =[]
|
||||
for e in hist:
|
||||
if not isinstance(e, dict) or "role" not in e or "content" not in e:
|
||||
raise ValueError("Некорректная структура памяти.")
|
||||
entry = {
|
||||
"role": e["role"],
|
||||
"type": e.get("type", "text"),
|
||||
"content": e["content"],
|
||||
"date": e.get("date")
|
||||
}
|
||||
if e["role"] == "user":
|
||||
entry["user_id"] = e.get("user_id")
|
||||
entry["message_id"] = e.get("message_id")
|
||||
new_hist.append(entry)
|
||||
chat_id = 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"])
|
||||
@@ -1020,16 +1185,18 @@ class Gemini(loader.Module):
|
||||
mid = m["id"]
|
||||
line = f"• <code>{mid}</code>"
|
||||
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<i>...и еще {len(other_list)-50} моделей.</i>"
|
||||
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"• <code>{m.name.split('/')[-1]}</code> ({m.display_name})" for m in models])
|
||||
txt = "\n".join([f"• <code>{m.name.split('/')[-1]}</code>" 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"🔮 <b>Провайдер:</b> {provider}\n🧠 <b>Модель:</b> <code>{self.config['model_name']}</code>")
|
||||
return await utils.answer(message, f"🔮 <b>Провайдер:</b> <code>{provider}</code>\n🧠 <b>Модель:</b> <code>{self.config['model_name']}</code>")
|
||||
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⚠️ <b>Конфликт настроек!</b>\n"
|
||||
f"Вы установили модель <code>{args_raw}</code>, но провайдер остался <b>Google</b>.\n"
|
||||
"Смените провайдера командой:\n<code>.cfg gemini provider openrouter</code>"
|
||||
)
|
||||
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⚠️ <b>Совет:</b> Для OpenRouter лучше использовать полные ID.\n"
|
||||
f"Например: <code>google/{args_raw}</code>"
|
||||
@@ -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("🗑 <b>Сессия закрыта.</b>", 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"<tg-emoji emoji-id=5386367538735104399>⌛️</tg-emoji> <b>Регенерация...</b>", 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("⚠️ <b>Сессия истекла (RAM cleared).</b>", reply_markup=None)
|
||||
await entity.edit(
|
||||
"⚠️ <b>Сессия истекла или бот был перезагружен с потерей данных.</b>",
|
||||
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}"
|
||||
text_to_show = text_to_show.replace('<emoji document_id=', '<tg-emoji emoji-id=').replace('</emoji>', '</tg-emoji>')
|
||||
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"❗️ <b>Превышен лимит Google Gemini API для модели <code>{utils.escape_html(model)}</code>.</b>\n"
|
||||
f"❗️ <b>Превышен лимит Google Gemini API для модели <code>{utils.escape_html(model_name)}</code>.</b>"
|
||||
"\n\nЧаще всего это происходит на бесплатном тарифе. Вы можете:\n"
|
||||
"• Подождать, пока лимит сбросится (обычно раз в сутки).\n"
|
||||
"• Проверить свой тарифный план в <a href='https://aistudio.google.com/app/billing'>Google AI Studio</a>.\n"
|
||||
"• Узнать больше о лимитах <a href='https://ai.google.dev/gemini-api/docs/rate-limits'>здесь</a>.\n\n"
|
||||
f"<b>Детали ошибки:</b>\n<code>{utils.escape_html(msg)}</code>"
|
||||
)
|
||||
if "500 An internal error has occurred" in msg:
|
||||
return (
|
||||
"❗️ <b>Ошибка 500 от Google API.</b>\n"
|
||||
"Это значит, что формат медиа (файл или еще что то) который ты отправил, не поддерживается.\n"
|
||||
"Такое случается, по такой причине:\n "
|
||||
"• Если формат файла в принципе не поддерживается Gemini/Гуглом.\n "
|
||||
"• Временный сбой на серверах Google. Попробуйте повторить запрос позже."
|
||||
)
|
||||
if "User location is not supported" in msg or "location is not supported" in msg:
|
||||
return (
|
||||
'❗️ <b>В данном регионе Gemini API не доступен.</b>\n'
|
||||
'Используйте VPN или прокси.'
|
||||
'Скачайте VPN (для пк/тел) или поставьте прокси (платный/бесплатный).\n'
|
||||
'Или воспользуйтесь инструкцией <a href="https://t.me/SenkoGuardianModules/23">вот тут</a>\n'
|
||||
'А для тех у кого UserLand инструкция <a href="https://t.me/SenkoGuardianModules/35">тут</a>'
|
||||
)
|
||||
if "API key not valid" in msg:
|
||||
return self.strings["invalid_api_key"]
|
||||
if "blocked" in msg.lower():
|
||||
return self.strings["blocked_error"].format(utils.escape_html(msg))
|
||||
return self.strings["api_error"].format(utils.escape_html(msg))
|
||||
if isinstance(e, (OSError, socket.timeout)):
|
||||
if isinstance(e, (OSError, aiohttp.ClientError, socket.timeout)):
|
||||
return "❗️ <b>Сетевая ошибка:</b>\n<code>{}</code>".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}<b>{title}</b>"
|
||||
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
|
||||
text = re.sub(r"<thought>.*?</thought>", "", text, flags=re.DOTALL)
|
||||
text = re.sub(r"(?i)<br\s*/?>", "\n", text)
|
||||
def heading_replacer(match): level=len(match.group(1)); title=match.group(2).strip(); indent=" " * (level - 1); return f"{indent}<b>{title}</b>"
|
||||
text=re.sub(r"^(#+)\s+(.*)", heading_replacer, text, flags=re.MULTILINE)
|
||||
def list_replacer(match):
|
||||
indent = match.group(1)
|
||||
return f"{indent}• "
|
||||
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")
|
||||
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'<pre><code class="language-{lang}">{code}</code></pre>' if lang else f'<pre><code>{code}</code></pre>'
|
||||
html_text=re.sub(r"```(.*?)\n([\s\S]+?)\n```", format_code, html_text)
|
||||
html_text=re.sub(r"<p>(<pre>[\s\S]*?</pre>)</p>", r"\1", html_text, flags=re.DOTALL)
|
||||
html_text = html_text.replace("<p>", "").replace("</p>", "\n").strip()
|
||||
html_text=html_text.replace("<p>", "").replace("</p>", "\n")
|
||||
html_text=re.sub(r"(?i)<br\s*/?>", "\n", html_text).strip()
|
||||
return html_text
|
||||
|
||||
def _format_response_with_smart_separation(self, text: str) -> str:
|
||||
@@ -1409,7 +1643,8 @@ 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 not part or part.isspace():
|
||||
continue
|
||||
if i % 2 == 1:
|
||||
result_parts.append(part.strip())
|
||||
else:
|
||||
@@ -1419,10 +1654,9 @@ class Gemini(loader.Module):
|
||||
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)}
|
||||
]]
|
||||
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)
|
||||
|
||||
875
archquise/H.Modules/soundcloud.py
Normal file
875
archquise/H.Modules/soundcloud.py
Normal file
@@ -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": (
|
||||
"<emoji document_id=5778527486270770928>\u274c</emoji>"
|
||||
" <b>Set </b><code>oauth_token</code><b> in module config</b>\n\n"
|
||||
"\U0001f511 Get it via extension:\n"
|
||||
"\u2022 <a href='https://chromewebstore.google.com/detail/"
|
||||
"jgocamehhjhbhomfnhknmiljlhjbaldg'>Chromium</a>\n"
|
||||
"\u2022 <a href='https://addons.mozilla.org/en-US/firefox/addon/"
|
||||
"playinnowbot/'>Firefox</a>\n"
|
||||
"\u2022 Or via DevTools: Application \u2192 Cookies \u2192 "
|
||||
"<code>oauth_token</code>"
|
||||
),
|
||||
"nothing": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Nothing is playing right now</b>"
|
||||
),
|
||||
"error": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Error</b>\n<code>{}</code>"
|
||||
),
|
||||
"wait_card": (
|
||||
"\n\n<emoji document_id=5841359499146825803>🕔</emoji>"
|
||||
" <i>Generating card…</i>"
|
||||
),
|
||||
"wait_dl": (
|
||||
"\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>Downloading…</i>"
|
||||
),
|
||||
"dl_fail": (
|
||||
"\n\n<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <i>Download failed</i>"
|
||||
),
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_token": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Установи </b><code>oauth_token</code>"
|
||||
"<b> в конфиге модуля</b>\n\n"
|
||||
"🔑 Получить токен:\n"
|
||||
"• <a href='https://chromewebstore.google.com/detail/"
|
||||
"jgocamehhjhbhomfnhknmiljlhjbaldg'>Chromium</a>\n"
|
||||
"• <a href='https://addons.mozilla.org/en-US/firefox/addon/"
|
||||
"playinnowbot/'>Firefox</a>\n"
|
||||
"• Или через DevTools: Application → Cookies → "
|
||||
"<code>oauth_token</code>"
|
||||
),
|
||||
"nothing": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Сейчас ничего не играет</b>"
|
||||
),
|
||||
"error": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Ошибка</b>\n<code>{}</code>"
|
||||
),
|
||||
"wait_card": (
|
||||
"\n\n<emoji document_id=5841359499146825803>🕔</emoji>"
|
||||
" <i>Генерация карточки…</i>"
|
||||
),
|
||||
"wait_dl": (
|
||||
"\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>Скачивание…</i>"
|
||||
),
|
||||
"dl_fail": (
|
||||
"\n\n<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <i>Ошибка скачивания</i>"
|
||||
),
|
||||
}
|
||||
|
||||
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",
|
||||
(
|
||||
"<emoji document_id=6007938409857815902>🎧</emoji>"
|
||||
" <b>Now playing:</b> {artist} — {track}\n"
|
||||
"<emoji document_id=5776213190387961618>🕓</emoji>"
|
||||
" {duration}{genre}\n"
|
||||
"<emoji document_id=5877465816030515018>🔗</emoji>"
|
||||
" <b><a href='{url}'>SoundCloud</a></b>"
|
||||
),
|
||||
"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"<emoji document_id=6007938409857815902>🎧</emoji>"
|
||||
f" <b>{utils.escape_html(t.artist)} — {utils.escape_html(t.title)}</b>\n"
|
||||
f"<emoji document_id=5776213190387961618>🕓</emoji> {info}\n"
|
||||
f"<emoji document_id=5877465816030515018>🔗</emoji>"
|
||||
f" <b><a href='{t.permalink}'>SoundCloud</a></b>"
|
||||
)
|
||||
|
||||
@_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 = (
|
||||
"<emoji document_id=5776213190387961618>📜</emoji>"
|
||||
" <b>История прослушивания:</b>\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}. <b>{utils.escape_html(t.artist)} —"
|
||||
f" {utils.escape_html(t.title)}</b>\n"
|
||||
f" <emoji document_id=5776213190387961618>🕓</emoji>"
|
||||
f" {meta} | <a href='{t.permalink}'>Link</a>\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)
|
||||
@@ -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,35 +192,45 @@ 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
|
||||
|
||||
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",
|
||||
active_bars = int(num_bars * progress_ratio)
|
||||
|
||||
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
|
||||
|
||||
font_album = get_font(50)
|
||||
@@ -312,13 +321,7 @@ class YaMusicMod(loader.Module):
|
||||
"""The module for Yandex.Music streaming service"""
|
||||
|
||||
strings = {
|
||||
"name": "YaMusic",
|
||||
"iguide": '📜 <b><a href="https://yandex-music.rtfd.io/en/main/token.html">Guide for obtaining access token for Yandex.Music</a></b>',
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль для стримингового сервиса Яндекс.Музыка",
|
||||
"iguide": '📜 <b><a href="https://yandex-music.rtfd.io/en/main/token.html">Гайд по получению токена Яндекс.Музыки</a></b>',
|
||||
"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()
|
||||
@@ -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"<a href=\"https://music.yandex.ru/track/{now['playable_id']}\">Яндекс.Музыка</a>",
|
||||
)
|
||||
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"<a href=\"https://music.yandex.ru/track/{now['playable_id']}\">Яндекс.Музыка</a>",
|
||||
)
|
||||
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,
|
||||
|
||||
116
coddrago/modules/translations/yamusic.yml
Normal file
116
coddrago/modules/translations/yamusic.yml
Normal file
@@ -0,0 +1,116 @@
|
||||
en:
|
||||
iguide: "<emoji document_id=5956561916573782596>📜</emoji> <b><a href=\"https://yandex-music.rtfd.io/en/main/token.html\">Guide for obtaining access token for Yandex.Music</a></b>"
|
||||
search: "<emoji document_id=5474304919651491706>🎧</emoji> <b>{performer} — {title}</b>\n<emoji document_id=5242574232688298747>🎵</emoji> <b><a href=\"https://music.yandex.ru/track/{track_id}\">Yandex.Music</a> | <a href=\"https://song.link/ya/{track_id}\">song.link</a></b>"
|
||||
downloading_track: "\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>Downloading audio…</i>"
|
||||
uploading_banner: "\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>Uploading banner…</i>"
|
||||
lyrics: "<emoji document_id=5956561916573782596>📜</emoji> <b>Lyrics of the <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> track:</b>\n<blockquote expandable>{text}</blockquote>\n\n<emoji document_id=5776287149724798198>©️</emoji> <b>Writers:</b> {writers}"
|
||||
no_lyrics: "<emoji document_id=5872829476143894491>🚫</emoji> <b>Track <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> has no lyrics!</b>"
|
||||
errors:
|
||||
no_query: "<emoji document_id=5872829476143894491>🚫</emoji> <b>Specify the search query first!</b>"
|
||||
no_token_or_invalid: "<emoji document_id=5872829476143894491>🚫</emoji> <b>You specified an invalid access token or didn't specified it at all!</b>"
|
||||
not_found: "<emoji document_id=5872829476143894491>🚫</emoji> <b>No results found.</b>"
|
||||
no_playing: "<emoji document_id=5872829476143894491>🚫</emoji> <b>You don't listening to anything right now.</b>"
|
||||
autobio:
|
||||
enabled: "<emoji document_id=5242574232688298747>🎧</emoji> <b>Autobio was enabled.</b>"
|
||||
disabled: "<emoji document_id=5242574232688298747>🎧</emoji> <b>Autobio was disabled.</b>"
|
||||
likes:
|
||||
liked: "<emoji document_id=5899833370052923106>❤️</emoji> <b>Track <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> was liked.</b>"
|
||||
unliked: "<emoji document_id=5992453811510186287>🖤</emoji> <b>Track <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> was unliked.</b>"
|
||||
disliked: "<emoji document_id=5952055319059239589>💔</emoji> <b>Track <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> was disliked.</b>"
|
||||
_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: "🔁 <b>Repeat enabled</b>"
|
||||
repeat_off: "<tg-emoji emoji-id=5873146865637133757>➡️</tg-emoji> <b>Repeat disabled</b>"
|
||||
next_track: "<tg-emoji emoji-id=5873204392429096339>⏭</tg-emoji> <b>Next track</b>"
|
||||
prev_track: "<tg-emoji emoji-id=5873204392429096339>⏭</tg-emoji> <b>Previous track</b>"
|
||||
volume_set: "<tg-emoji emoji-id=5873146865637133757>➡️</tg-emoji> <b>Volume set to {vol}%</b>"
|
||||
volume_invalid: "<tg-emoji emoji-id=5465665476971471368>❌</tg-emoji> <b>Volume must be between 0 and 100</b>"
|
||||
ynison_error: "<tg-emoji emoji-id=5465665476971471368>❌</tg-emoji> <b>Failed to send command to Yandex.Music (Ynison)</b>"
|
||||
|
||||
ru:
|
||||
iguide: "<emoji document_id=5956561916573782596>📜</emoji> <b><a href=\"https://yandex-music.rtfd.io/en/main/token.html\">Гайд по получению токена Яндекс.Музыки</a></b>"
|
||||
search: "<emoji document_id=5474304919651491706>🎧</emoji> <b>{performer} — {title}</b>\n<emoji document_id=5242574232688298747>🎵</emoji> <b><a href=\"https://music.yandex.ru/track/{track_id}\">Яндекс.Музыка</a> | <a href=\"https://song.link/ya/{track_id}\">song.link</a></b>"
|
||||
downloading_track: "\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>Загрузка трека…</i>"
|
||||
uploading_banner: "\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>Загрузка баннера…</i>"
|
||||
lyrics: "<emoji document_id=5956561916573782596>📜</emoji> <b>Текст трека <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a>:</b>\n<blockquote expandable>{text}</blockquote>\n\n<emoji document_id=5776287149724798198>©️</emoji> <b>Авторы:</b> {writers}"
|
||||
no_lyrics: "<emoji document_id=5872829476143894491>🚫</emoji> <b>У трека <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> нет текста!</b>"
|
||||
errors:
|
||||
no_query: "<emoji document_id=5872829476143894491>🚫</emoji> <b>Укажите поисковый запрос!</b>"
|
||||
no_token_or_invalid: "<emoji document_id=5872829476143894491>🚫</emoji> <b>Вы указали невалидный токен или не указали его вообще!</b>"
|
||||
not_found: "<emoji document_id=5872829476143894491>🚫</emoji> <b>Результаты не найдены.</b>"
|
||||
no_playing: "<emoji document_id=5872829476143894491>🚫</emoji> <b>Вы ничего не слушаете сейчас.</b>"
|
||||
autobio:
|
||||
enabled: "<emoji document_id=5242574232688298747>🎧</emoji> <b>Автобио теперь включено.</b>"
|
||||
disabled: "<emoji document_id=5242574232688298747>🎧</emoji> <b>Автобио теперь выключено.</b>"
|
||||
likes:
|
||||
liked: "<emoji document_id=5899833370052923106>❤️</emoji> <b>Трек <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> был лайкнут.</b>"
|
||||
unliked: "<emoji document_id=5992453811510186287>🖤</emoji> <b>С трека <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> был снят лайк.</b>"
|
||||
disliked: "<emoji document_id=5952055319059239589>💔</emoji> <b>Трек <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> был дизлайкнут.</b>"
|
||||
_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: "🔁 <b>Повтор включен (Один трек)</b>"
|
||||
repeat_off: "<tg-emoji emoji-id=5873146865637133757>➡️</tg-emoji> <b>Повтор выключен</b>"
|
||||
next_track: "<tg-emoji emoji-id=5873204392429096339>⏭</tg-emoji> <b>Следующий трек</b>"
|
||||
prev_track: "<tg-emoji emoji-id=5873204392429096339>⏭</tg-emoji> <b>Предыдущий трек</b>"
|
||||
volume_set: "<tg-emoji emoji-id=5873146865637133757>➡️</tg-emoji> <b>Громкость установлена на {vol}%</b>"
|
||||
volume_invalid: "<tg-emoji emoji-id=5465665476971471368>❌</tg-emoji> <b>Громкость должна быть от 0 до 100</b>"
|
||||
ynison_error: "<tg-emoji emoji-id=5465665476971471368>❌</tg-emoji> <b>Не удалось отправить команду в Яндекс.Музыку (Ynison)</b>"
|
||||
|
||||
jp:
|
||||
iguide: "<emoji document_id=5956561916573782596>📜</emoji> <b><a href=\"https://yandex-music.rtfd.io/en/main/token.html\">Yandex.Music アクセストークン取得ガイド</a></b>"
|
||||
search: "<emoji document_id=5474304919651491706>🎧</emoji> <b>{performer} — {title}</b>\n<emoji document_id=5242574232688298747>🎵</emoji> <b><a href=\"https://music.yandex.ru/track/{track_id}\">Yandex.Music</a> | <a href=\"https://song.link/ya/{track_id}\">song.link</a></b>"
|
||||
downloading_track: "\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>オーディオをダウンロード中…</i>"
|
||||
uploading_banner: "\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>バナーをアップロード中…</i>"
|
||||
lyrics: "<emoji document_id=5956561916573782596>📜</emoji> <b>トラック <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> の歌詞:</b>\n<blockquote expandable>{text}</blockquote>\n\n<emoji document_id=5776287149724798198>©️</emoji> <b>作詞・作曲:</b> {writers}"
|
||||
no_lyrics: "<emoji document_id=5872829476143894491>🚫</emoji> <b>トラック <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> には歌詞がありません!</b>"
|
||||
errors:
|
||||
no_query: "<emoji document_id=5872829476143894491>🚫</emoji> <b>最初に検索クエリを指定してください!</b>"
|
||||
no_token_or_invalid: "<emoji document_id=5872829476143894491>🚫</emoji> <b>無効なアクセストークンを指定したか、指定されていません!</b>"
|
||||
not_found: "<emoji document_id=5872829476143894491>🚫</emoji> <b>結果が見つかりません。</b>"
|
||||
no_playing: "<emoji document_id=5872829476143894491>🚫</emoji> <b>現在何も再生していません。</b>"
|
||||
autobio:
|
||||
enabled: "<emoji document_id=5242574232688298747>🎧</emoji> <b>Autobio(自動プロフィール)が有効になりました。</b>"
|
||||
disabled: "<emoji document_id=5242574232688298747>🎧</emoji> <b>Autobioが無効になりました。</b>"
|
||||
likes:
|
||||
liked: "<emoji document_id=5899833370052923106>❤️</emoji> <b>トラック <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> に「いいね」しました。</b>"
|
||||
unliked: "<emoji document_id=5992453811510186287>🖤</emoji> <b>トラック <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> の「いいね」を取り消しました。</b>"
|
||||
disliked: "<emoji document_id=5952055319059239589>💔</emoji> <b>トラック <a href=\"https://music.yandex.ru/track/{track_id}\">{track}</a> に「低評価」しました。</b>"
|
||||
_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: "🔁 <b>リピート有効</b>"
|
||||
repeat_off: "<tg-emoji emoji-id=5873146865637133757>➡️</tg-emoji> <b>リピート無効</b>"
|
||||
next_track: "<tg-emoji emoji-id=5873204392429096339>⏭</tg-emoji> <b>次のトラック</b>"
|
||||
prev_track: "<tg-emoji emoji-id=5873204392429096339>⏭</tg-emoji> <b>前のトラック</b>"
|
||||
volume_set: "<tg-emoji emoji-id=5873146865637133757>➡️</tg-emoji> <b>音量を {vol}% に設定しました</b>"
|
||||
volume_invalid: "<tg-emoji emoji-id=5465665476971471368>❌</tg-emoji> <b>音量は0から100の間で指定してください</b>"
|
||||
ynison_error: "<tg-emoji emoji-id=5465665476971471368>❌</tg-emoji> <b>Yandex.Music (Ynison) へのコマンド送信に失敗しました</b>"
|
||||
109
fiksofficial/python-modules/PyInstall.py
Normal file
109
fiksofficial/python-modules/PyInstall.py
Normal file
@@ -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'])
|
||||
@@ -25,3 +25,5 @@ mpi
|
||||
aigenuser
|
||||
github
|
||||
stream
|
||||
placeholders+
|
||||
PyInstall
|
||||
@@ -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"<details[^>]*>.*?</details>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
||||
text = re.sub(r"<summary[^>]*>.*?</summary>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
||||
text = re.sub(r"<img[^>]*>", "", 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", "")
|
||||
# дедуп по 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, c, branch=branch)
|
||||
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)
|
||||
|
||||
|
||||
650
fiksofficial/python-modules/placeholders+.py
Normal file
650
fiksofficial/python-modules/placeholders+.py
Normal file
@@ -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'<a href="{link.strip()}">{name.strip()}</a>'
|
||||
return val
|
||||
|
||||
async def get_channel(self):
|
||||
ch = self.config["channel"]
|
||||
if ch.startswith("@"):
|
||||
return f'<a href="https://t.me/{ch[1:]}">{ch}</a>'
|
||||
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"🎵 <b>{track['name']}</b> — {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
|
||||
@@ -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": "▶️ <b>Stream is live</b>\n\n{icon} <code>{file}</code>\n⏱ Time: <b>{elapsed}</b>\n🔢 PID: <code>{pid}</code>\n📡 <code>{rtmp}</code>\n🎥 <b>{vbr}</b> | <b>{fps}fps</b> | <b>{preset}</b>\n🔊 <b>{abr}</b>\n📋 Queue: <b>{queue}</b>",
|
||||
"status_idle": "⏸ <b>Stream is not active</b>",
|
||||
"status_queue": "\n📋 Queue: <b>{n}</b>",
|
||||
"stopped": "⏹ <b>Stream stopped.</b>",
|
||||
"no_rtmp": "❌ <b>RTMP not configured!</b>\nTap a button to set it up.",
|
||||
"downloading": "⏳ Downloading…",
|
||||
"dl_failed": "❌ Failed to download file.",
|
||||
"queued": "📋 Added to queue ({n})\n{icon} <code>{file}</code>",
|
||||
"not_running": "Not running",
|
||||
"queue_empty": "Queue is empty",
|
||||
"queue_header": "📋 Queue:\n",
|
||||
"settings_title": "⚙️ <b>Stream settings</b>",
|
||||
"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": "▶️ <b>Трансляция идёт</b>\n\n{icon} <code>{file}</code>\n⏱ Время: <b>{elapsed}</b>\n🔢 PID: <code>{pid}</code>\n📡 <code>{rtmp}</code>\n🎥 <b>{vbr}</b> | <b>{fps}fps</b> | <b>{preset}</b>\n🔊 <b>{abr}</b>\n📋 В очереди: <b>{queue}</b>",
|
||||
"status_idle": "⏸ <b>Трансляция не активна</b>",
|
||||
"status_queue": "\n📋 В очереди: <b>{n}</b>",
|
||||
"stopped": "⏹ <b>Трансляция остановлена.</b>",
|
||||
"no_rtmp": "❌ <b>RTMP не настроен!</b>\nНажми кнопку чтобы задать прямо сейчас.",
|
||||
"downloading": "⏳ Скачиваю…",
|
||||
"dl_failed": "❌ Не удалось скачать файл.",
|
||||
"queued": "📋 Добавлено в очередь ({n} шт.)\n{icon} <code>{file}</code>",
|
||||
"not_running": "Не запущено",
|
||||
"queue_empty": "Очередь пуста",
|
||||
"queue_header": "📋 Очередь:\n",
|
||||
"settings_title": "⚙️ <b>Настройки трансляции</b>",
|
||||
"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"))
|
||||
@@ -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,10 +47,12 @@ class SenderGifts(loader.Module):
|
||||
"min_stars_error": "<emoji document_id=4958526153955476488>❌</emoji> Недостаточно звезд для отправки минимального подарка!",
|
||||
"no_available_gifts": "<emoji document_id=4958526153955476488>❌</emoji> Нет доступных подарков для вашего баланса",
|
||||
"balance_error": "<emoji document_id=4958526153955476488>❌</emoji> Ошибка при проверке баланса",
|
||||
"user_disallowed_gifts": "<emoji document_id=4958526153955476488>❌</emoji> Данный пользователь не принимает подарки!",
|
||||
"btn_public": "📢 Публично",
|
||||
"btn_anon": "🕵️ Анонимно",
|
||||
}
|
||||
|
||||
# резерв
|
||||
regular_gifts = {
|
||||
15:[
|
||||
{"id": 5170145012310081615, "emoji": "❤️", "name": "Сердце"},
|
||||
@@ -94,9 +100,29 @@ class SenderGifts(loader.Module):
|
||||
"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):
|
||||
"""- <username> <text*> - отправить подарок пользователю (* - необязательный параметр.) Поддерживается реплай режим."""
|
||||
|
||||
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),
|
||||
@@ -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,
|
||||
@@ -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(
|
||||
|
||||
62
mead0wsss/mead0wsMods/gifts.json
Normal file
62
mead0wsss/mead0wsMods/gifts.json
Normal file
@@ -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}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +201,7 @@ class PicToStoriesMod(loader.Module):
|
||||
title=args,
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
||||
await self.client(
|
||||
functions.stories.TogglePinnedRequest(
|
||||
peer=types.InputPeerSelf(), id=story_ids, pinned=True
|
||||
|
||||
181
radiocycle/Modules/RandomAnimePic.py
Normal file
181
radiocycle/Modules/RandomAnimePic.py
Normal file
@@ -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": "<tg-emoji emoji-id=4916036072560919511>✅</tg-emoji> <b>Your anime pic</b>\n<tg-emoji emoji-id=5877465816030515018>🔗</tg-emoji> <b>URL:</b> {}",
|
||||
"loading": "<tg-emoji emoji-id=4911241630633165627>✨</tg-emoji> <b>Loading image...</b>",
|
||||
"categories_loading": "<tg-emoji emoji-id=4911241630633165627>✨</tg-emoji> <b>Loading categories...</b>",
|
||||
"categories": "<tg-emoji emoji-id=4916036072560919511>✅</tg-emoji> <b>Available categories</b>\n<blockquote expandable>{}</blockquote>",
|
||||
"no_categories": "<tg-emoji emoji-id=5116151848855667552>🚫</tg-emoji> <b>Categories not found</b>",
|
||||
"error": "<tg-emoji emoji-id=5116151848855667552>🚫</tg-emoji> <b>An unexpected error occurred...</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"img": "<tg-emoji emoji-id=4916036072560919511>✅</tg-emoji> <b>Ваша аниме-картинка</b>\n<tg-emoji emoji-id=5877465816030515018>🔗</tg-emoji> <b>Ссылка:</b> {}",
|
||||
"loading": "<tg-emoji emoji-id=4911241630633165627>✨</tg-emoji> <b>Загрузка изображения...</b>",
|
||||
"categories_loading": "<tg-emoji emoji-id=4911241630633165627>✨</tg-emoji> <b>Загрузка категорий...</b>",
|
||||
"categories": "<tg-emoji emoji-id=4916036072560919511>✅</tg-emoji> <b>Доступные категории</b>\n<blockquote expandable>{}</blockquote>",
|
||||
"no_categories": "<tg-emoji emoji-id=5116151848855667552>🚫</tg-emoji> <b>Категории не найдены</b>",
|
||||
"error": "<tg-emoji emoji-id=5116151848855667552>🚫</tg-emoji> <b>Произошла непредвиденная ошибка...</b>",
|
||||
}
|
||||
|
||||
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"<code>{category}</code>" 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"))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
Neofetch
|
||||
randomanimepic
|
||||
RandomAnimePic
|
||||
SpotifyMod
|
||||
UnbanAll
|
||||
voicetotext
|
||||
|
||||
@@ -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": "<emoji document_id=4916036072560919511>✅</emoji> <b>Your anime pic</b>\n<emoji document_id=5877465816030515018>🔗</emoji> <b>URL:</b> {}",
|
||||
"loading": "<emoji document_id=4911241630633165627>✨</emoji> <b>Loading image...</b>",
|
||||
"error": "<emoji document_id=5116151848855667552>🚫</emoji> <b>An unexpected error occurred...</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"img": "<emoji document_id=4916036072560919511>✅</emoji> <b>Ваша аниме-картинка</b>\n<emoji document_id=5877465816030515018>🔗</emoji> <b>Ссылка:</b> {}",
|
||||
"loading": "<emoji document_id=4911241630633165627>✨</emoji> <b>Загрузка изображения...</b>",
|
||||
"error": "<emoji document_id=5116151848855667552>🚫</emoji> <b>Произошла непредвиденная ошибка...</b>",
|
||||
}
|
||||
|
||||
@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)
|
||||
@@ -1,36 +1,133 @@
|
||||
# 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
|
||||
self.default_chat = default_chat
|
||||
if not self.db.get('TTsaveMod', 'chat', False):
|
||||
self.db.set('TTsaveMod', 'chat', self.default_chat)
|
||||
|
||||
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 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('Скачиваю...')
|
||||
|
||||
async def erase_status():
|
||||
try:
|
||||
await status_msg.delete()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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()
|
||||
|
||||
# Определяем, в каком из 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
|
||||
|
||||
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
|
||||
|
||||
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"<b>бот не найден.</b>")
|
||||
self.db.set('TTsaveMod', 'bot', str(bot.id))
|
||||
await utils.answer(message, f"<b>бот <code>{bot.username}</code> установлен.</b>")
|
||||
|
||||
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"<b>Сейчас:</b> {tip}\n<code>.ttsendmode forward|download</code>",
|
||||
)
|
||||
if raw in ("forward", "пересылка", "fwd", "f"):
|
||||
self.db.set("TTsaveMod", "send_mode", MODE_FORWARD)
|
||||
return await utils.answer(message, "<b>Режим:</b> пересылка с бота.")
|
||||
if raw in ("download", "скачивание", "скачать", "dl", "d"):
|
||||
self.db.set("TTsaveMod", "send_mode", MODE_DOWNLOAD)
|
||||
return await utils.answer(message, "<b>Режим:</b> скачивание и отправка.")
|
||||
return await utils.answer(message, "<code>.ttsendmode forward|download</code>")
|
||||
|
||||
async def ttsavecmd(self, message):
|
||||
""".ttsave {link}"""
|
||||
|
||||
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")
|
||||
save_video = await self.save_video(message)
|
||||
if save_video:
|
||||
if self._send_mode() == MODE_FORWARD:
|
||||
await utils.answer(message, "<b>видео переслано.</b>")
|
||||
else:
|
||||
await utils.answer(message, "<b>видео успешно отправлено.</b>")
|
||||
else:
|
||||
await utils.answer(message, "<b>не удалось скачать видео.</b>")
|
||||
|
||||
async def ttacceptcmd(self, message):
|
||||
""" .ttaccept {reply/id} для открытия в чате автоматического скачивания ссылок. без аргументов тоже работает.\n.ttaccept -l для показа открытых чатов """
|
||||
@@ -65,19 +162,8 @@ class TTsaveMod(loader.Module):
|
||||
links = re.findall(r'((?:https?://)?v[mt]\.tiktok\.com/[A-Za-z0-9_]+/?)', message.raw_text)
|
||||
if len(links) == 0: return
|
||||
|
||||
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 self.save_video(message, url=link)
|
||||
await asyncio.sleep(5)
|
||||
except: pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user