mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 22:34:19 +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
File diff suppressed because it is too large
Load Diff
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,34 +192,44 @@ class Banners:
|
||||
total_time_str, (bar_end_x + 30, bar_y), font_time, anchor="lm"
|
||||
)
|
||||
|
||||
draw.line(
|
||||
[(bar_start_x, bar_y), (bar_end_x, bar_y)],
|
||||
fill=(255, 255, 255, 80),
|
||||
width=bar_height,
|
||||
)
|
||||
old_state = random.getstate()
|
||||
|
||||
random.seed(self.title + str(self.duration))
|
||||
|
||||
num_bars = 65
|
||||
bar_spacing = bar_width / num_bars
|
||||
bar_w = max(4, int(bar_spacing * 0.5))
|
||||
max_h = 50
|
||||
min_h = 6
|
||||
|
||||
if self.duration > 0:
|
||||
progress_ratio = self.progress / self.duration
|
||||
else:
|
||||
progress_ratio = 0
|
||||
progress_px = int(bar_width * progress_ratio)
|
||||
if progress_px > bar_width:
|
||||
progress_px = bar_width
|
||||
|
||||
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
|
||||
|
||||
@@ -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", "")
|
||||
branch = await self._fetch_branch_for_commit(repo, newest_sha, cid_str)
|
||||
messages += self._fmt_push(repo, c, branch=branch)
|
||||
# дедуп по SHA
|
||||
new_commits = []
|
||||
for commit in c:
|
||||
key = f"{repo}:push:{commit.get('sha', '')}"
|
||||
if key not in self._seen:
|
||||
self._seen.add(key)
|
||||
new_commits.append(commit)
|
||||
if new_commits:
|
||||
newest_sha = new_commits[-1].get("sha", "")
|
||||
branch = await self._fetch_branch_for_commit(repo, newest_sha, cid_str)
|
||||
messages += self._fmt_push(repo, new_commits, branch=branch)
|
||||
|
||||
if "issues" in events:
|
||||
i = await self._fetch_issues(repo, since, cid_str)
|
||||
if i:
|
||||
messages += self._fmt_issues(repo, i)
|
||||
new_issues = []
|
||||
for issue in i:
|
||||
# ключ: repo:issue:number:state (state меняется — open/closed)
|
||||
key = f"{repo}:issue:{issue.get('number')}:{issue.get('state')}"
|
||||
if key not in self._seen:
|
||||
self._seen.add(key)
|
||||
new_issues.append(issue)
|
||||
if new_issues:
|
||||
messages += self._fmt_issues(repo, new_issues)
|
||||
|
||||
if "pull_request" in events:
|
||||
p = await self._fetch_prs(repo, since, cid_str)
|
||||
if p:
|
||||
messages += self._fmt_prs(repo, p)
|
||||
new_prs = []
|
||||
for pr in p:
|
||||
merged = pr.get("merged_at") is not None
|
||||
state = pr.get("state", "open")
|
||||
# ключ включает финальное состояние PR
|
||||
phase = "merged" if merged else state
|
||||
key = f"{repo}:pr:{pr.get('number')}:{phase}"
|
||||
if key not in self._seen:
|
||||
self._seen.add(key)
|
||||
new_prs.append(pr)
|
||||
if new_prs:
|
||||
messages += self._fmt_prs(repo, new_prs)
|
||||
|
||||
if "release" in events:
|
||||
r = await self._fetch_releases(repo, since, cid_str)
|
||||
if r:
|
||||
messages += self._fmt_releases(repo, r)
|
||||
new_releases = []
|
||||
for rel in r:
|
||||
key = f"{repo}:release:{rel.get('id', rel.get('tag_name'))}"
|
||||
if key not in self._seen:
|
||||
self._seen.add(key)
|
||||
new_releases.append(rel)
|
||||
if new_releases:
|
||||
messages += self._fmt_releases(repo, new_releases)
|
||||
|
||||
if "star" in events:
|
||||
s = await self._fetch_stargazers(repo, since, cid_str)
|
||||
if s:
|
||||
messages += self._fmt_star(repo, s)
|
||||
new_stars = []
|
||||
for star in s:
|
||||
user = (star.get("sender") or {}).get("login", "")
|
||||
key = f"{repo}:star:{user}"
|
||||
if key not in self._seen:
|
||||
self._seen.add(key)
|
||||
new_stars.append(star)
|
||||
if new_stars:
|
||||
messages += self._fmt_star(repo, new_stars)
|
||||
|
||||
# Ограничиваем размер _seen чтобы не распухал в памяти
|
||||
if len(self._seen) > 2000:
|
||||
self._seen = set(list(self._seen)[-1000:])
|
||||
|
||||
for text in messages:
|
||||
try:
|
||||
@@ -1032,3 +1104,4 @@ class GitHubMod(loader.Module):
|
||||
"""- Open GitHub Monitor control panel"""
|
||||
await self._render_main_menu(message)
|
||||
|
||||
|
||||
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,25 +47,27 @@ 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: [
|
||||
15:[
|
||||
{"id": 5170145012310081615, "emoji": "❤️", "name": "Сердце"},
|
||||
{"id": 5170233102089322756, "emoji": "🧸", "name": "Мишка"},
|
||||
],
|
||||
25: [
|
||||
25:[
|
||||
{"id": 5170250947678437525, "emoji": "🎁", "name": "Подарок"},
|
||||
{"id": 5168103777563050263, "emoji": "🌹", "name": "Роза"},
|
||||
],
|
||||
50: [
|
||||
50:[
|
||||
{"id": 5170144170496491616, "emoji": "🎂", "name": "Тортик"},
|
||||
{"id": 5170314324215857265, "emoji": "💐", "name": "Цветы"},
|
||||
{"id": 5170564780938756245, "emoji": "🚀", "name": "Ракета"},
|
||||
],
|
||||
100: [
|
||||
100:[
|
||||
{"id": 5168043875654172773, "emoji": "🏆", "name": "Кубок"},
|
||||
{"id": 5170690322832818290, "emoji": "💍", "name": "Кольцо"},
|
||||
{"id": 5170521118301225164, "emoji": "💎", "name": "Алмаз"},
|
||||
@@ -71,32 +77,52 @@ class SenderGifts(loader.Module):
|
||||
unique_gifts = {
|
||||
"new_year": {
|
||||
"name": "🎄 Новогодние подарки",
|
||||
"gifts": [
|
||||
"gifts":[
|
||||
{"id": 5922558454332916696, "emoji": "🎄", "name": "Ёлка", "price": 50},
|
||||
{"id": 5956217000635139069, "emoji": "🧸", "name": "Новогодний мишка", "price": 50},
|
||||
]
|
||||
},
|
||||
"valentines": {
|
||||
"name": "💘 День святого валентина",
|
||||
"gifts": [
|
||||
"gifts":[
|
||||
{"id": 5800655655995968830, "emoji": "🧸", "name": "14 Февраля мишка", "price": 50},
|
||||
{"id": 5801108895304779062, "emoji": "💘", "name": "14 Февраля сердце", "price": 50},
|
||||
]
|
||||
},
|
||||
"march_8th": {
|
||||
"name": "🌷 8 Марта",
|
||||
"gifts": [
|
||||
"gifts":[
|
||||
{"id": 5866352046986232958, "emoji": "🧸", "name": "8 Марта мишка", "price": 50},
|
||||
]
|
||||
},
|
||||
"saint_patricks_day ": {
|
||||
"saint_patricks_day": {
|
||||
"name": "💰 День святого патрика",
|
||||
"gifts": [
|
||||
"gifts":[
|
||||
{"id": 5893356958802511476, "emoji": "🧸", "name": "Лепрекон мишка", "price": 50},
|
||||
]
|
||||
},
|
||||
"april_1th": {
|
||||
"name": "🤪 1 Апреля",
|
||||
"gifts":[
|
||||
{"id": 5935895822435615975, "emoji": "🧸", "name": "1 Апреля мишка", "price": 50}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async def fetch_gifts_from_github(self):
|
||||
url = "https://raw.githubusercontent.com/mead0wsss/mead0wsMods/main/gifts.json"
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, timeout=5) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json(content_type=None)
|
||||
if "regular_gifts" in data:
|
||||
self.regular_gifts = {int(k): v for k, v in data["regular_gifts"].items()}
|
||||
if "unique_gifts" in data:
|
||||
self.unique_gifts = data["unique_gifts"]
|
||||
except Exception as e:
|
||||
logging.error(f"Не удалось загрузить подарки с GitHub: {e}")
|
||||
|
||||
async def get_star_balance(self):
|
||||
try:
|
||||
balance_info = (await self.client(GetStarsStatusRequest("me")))
|
||||
@@ -108,6 +134,8 @@ class SenderGifts(loader.Module):
|
||||
@loader.command()
|
||||
async def sendgift(self, message):
|
||||
"""- <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),
|
||||
@@ -192,10 +217,10 @@ class SenderGifts(loader.Module):
|
||||
except:
|
||||
user_display = f"ID: {user_id}"
|
||||
|
||||
available_categories = [price for price in self.regular_gifts.keys() if balance >= price]
|
||||
available_categories =[price for price in self.regular_gifts.keys() if balance >= price]
|
||||
|
||||
buttons = []
|
||||
row = []
|
||||
row =[]
|
||||
for price in sorted(available_categories):
|
||||
row.append({
|
||||
"text": f"{price} ⭐",
|
||||
@@ -226,7 +251,7 @@ class SenderGifts(loader.Module):
|
||||
except:
|
||||
user_display = f"ID: {user_id}"
|
||||
|
||||
buttons = []
|
||||
buttons =[]
|
||||
for cat_id, cat_data in self.unique_gifts.items():
|
||||
if any(balance >= gift["price"] for gift in cat_data["gifts"]):
|
||||
buttons.append([{
|
||||
@@ -256,7 +281,7 @@ class SenderGifts(loader.Module):
|
||||
async def _show_category(self, call, user_id, price, text, balance, msg_id):
|
||||
gifts = self.regular_gifts[price]
|
||||
buttons = []
|
||||
row = []
|
||||
row =[]
|
||||
for gift in gifts:
|
||||
row.append({
|
||||
"text": gift["emoji"],
|
||||
@@ -265,7 +290,7 @@ class SenderGifts(loader.Module):
|
||||
})
|
||||
if len(row) == 3:
|
||||
buttons.append(row)
|
||||
row = []
|
||||
row =[]
|
||||
|
||||
if row:
|
||||
buttons.append(row)
|
||||
@@ -299,7 +324,7 @@ class SenderGifts(loader.Module):
|
||||
})
|
||||
if len(row) == 3:
|
||||
buttons.append(row)
|
||||
row = []
|
||||
row =[]
|
||||
|
||||
if row:
|
||||
buttons.append(row)
|
||||
@@ -326,8 +351,7 @@ class SenderGifts(loader.Module):
|
||||
else:
|
||||
back_callback = self._show_unique_category_gifts
|
||||
|
||||
buttons = [
|
||||
[
|
||||
buttons = [[
|
||||
{
|
||||
"text": self.strings["btn_public"],
|
||||
"callback": self._send_gift,
|
||||
@@ -338,8 +362,7 @@ class SenderGifts(loader.Module):
|
||||
"callback": self._send_gift,
|
||||
"args": (user_id, gift_id, text, gift_emoji, msg_id, balance, True)
|
||||
}
|
||||
],
|
||||
[
|
||||
],[
|
||||
{
|
||||
"text": "⬅️ Назад",
|
||||
"callback": back_callback,
|
||||
@@ -369,7 +392,7 @@ class SenderGifts(loader.Module):
|
||||
user,
|
||||
gift_id,
|
||||
hide_name=hide_name,
|
||||
message=TextWithEntities(text, entities) if text else TextWithEntities("", [])
|
||||
message=TextWithEntities(text, entities) if text else TextWithEntities("",[])
|
||||
)
|
||||
form = await self.client(GetPaymentFormRequest(inv))
|
||||
result = await self.client(SendStarsFormRequest(form.form_id, inv))
|
||||
@@ -382,6 +405,11 @@ class SenderGifts(loader.Module):
|
||||
self.strings["not_enough_stars"].format(gift_emoji),
|
||||
reply_markup=None
|
||||
)
|
||||
elif "USER_DISALLOWED_STARGIFTS" in str(e):
|
||||
await call.edit(
|
||||
self.strings["user_disallowed_gifts"].format(gift_emoji),
|
||||
reply_markup=None
|
||||
)
|
||||
else:
|
||||
logging.error(f"Error sending gift: {e}")
|
||||
await call.edit(
|
||||
|
||||
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,11 +201,11 @@ class PicToStoriesMod(loader.Module):
|
||||
title=args,
|
||||
)
|
||||
)
|
||||
else:
|
||||
await self.client(
|
||||
functions.stories.TogglePinnedRequest(
|
||||
peer=types.InputPeerSelf(), id=story_ids, pinned=True
|
||||
)
|
||||
|
||||
await self.client(
|
||||
functions.stories.TogglePinnedRequest(
|
||||
peer=types.InputPeerSelf(), id=story_ids, pinned=True
|
||||
)
|
||||
)
|
||||
|
||||
await utils.answer(message, self.strings("done"))
|
||||
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,83 +1,169 @@
|
||||
# meta developer: @trololo_1
|
||||
|
||||
from telethon import events
|
||||
from .. import utils, loader
|
||||
import re, asyncio, os
|
||||
from datetime import datetime
|
||||
|
||||
chat = "@TTFullBot"
|
||||
default_chat = "@SaveAsBot"
|
||||
MODE_FORWARD = "forward"
|
||||
MODE_DOWNLOAD = "download"
|
||||
|
||||
class TTsaveMod(loader.Module):
|
||||
"""Save tiktok video"""
|
||||
strings = {'name': 'TTsaveMod'}
|
||||
async def client_ready(self, client, db):
|
||||
self.db = db
|
||||
"""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)
|
||||
|
||||
async def ttsavecmd(self, message):
|
||||
""".ttsave {link}"""
|
||||
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
|
||||
|
||||
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")
|
||||
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 ttacceptcmd(self, message):
|
||||
""" .ttaccept {reply/id} для открытия в чате автоматического скачивания ссылок. без аргументов тоже работает.\n.ttaccept -l для показа открытых чатов """
|
||||
async def erase_status():
|
||||
try:
|
||||
await status_msg.delete()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
reply = await message.get_reply_message()
|
||||
users_list = self.db.get('TTsaveMod', 'users', [])
|
||||
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()
|
||||
|
||||
if args == '-l':
|
||||
if len(users_list) == 0: return await utils.answer(message, 'Список пуст.')
|
||||
return await utils.answer(message, '• '+'\n• '.join(['<code>'+str(i)+'</code>' for i in users_list]))
|
||||
# Определяем, в каком из response пришло видео
|
||||
video_response, other_response = None, None
|
||||
if hasattr(response1, "media") and response1.media is not None:
|
||||
if getattr(response1.media, "document", None) or getattr(response1.media, "video", None):
|
||||
video_response = response1
|
||||
other_response = response2
|
||||
if video_response is None and hasattr(response2, "media") and response2.media is not None:
|
||||
if getattr(response2.media, "document", None) or getattr(response2.media, "video", None):
|
||||
video_response = response2
|
||||
other_response = response1
|
||||
if video_response is None:
|
||||
await erase_status()
|
||||
await message.respond("Не удалось получить видео.")
|
||||
await response1.delete()
|
||||
await response2.delete()
|
||||
await bot_send_link.delete()
|
||||
return False
|
||||
|
||||
try:
|
||||
if not args and not reply:
|
||||
user = message.chat_id
|
||||
else:
|
||||
user = reply.sender_id if not args else int(args)
|
||||
except:
|
||||
return await utils.answer(message, 'Неверно введён ид.')
|
||||
if user in users_list:
|
||||
users_list.remove(user)
|
||||
await utils.answer(message, f'Ид <code>{str(user)}</code> исключен.')
|
||||
else:
|
||||
users_list.append(user)
|
||||
await utils.answer(message, f'Ид <code>{str(user)}</code> добавлен.')
|
||||
self.db.set('TTsaveMod', 'users', users_list)
|
||||
if mode == MODE_FORWARD:
|
||||
await video_response.forward_to(dest)
|
||||
await response1.delete()
|
||||
await response2.delete()
|
||||
await bot_send_link.delete()
|
||||
await erase_status()
|
||||
return True
|
||||
|
||||
async def watcher(self, message):
|
||||
try:
|
||||
users = self.db.get('TTsaveMod', 'users', [])
|
||||
if message.chat_id not in users: return
|
||||
links = re.findall(r'((?:https?://)?v[mt]\.tiktok\.com/[A-Za-z0-9_]+/?)', message.raw_text)
|
||||
if len(links) == 0: return
|
||||
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 with message.client.conversation(chat) as conv:
|
||||
for link in links:
|
||||
response1, response2, response3 = [conv.wait_event(events.NewMessage(incoming=True, from_users=chat, chats=chat)) for i in range(3)]
|
||||
bot_send_link = await message.client.send_message(chat, link)
|
||||
response1 = await response1
|
||||
response2 = await response2
|
||||
response3 = await response3
|
||||
await response2.download_media("hui.mp4")
|
||||
await message.client.send_file(message.chat_id, "hui.mp4")
|
||||
await response1.delete()
|
||||
await response2.delete()
|
||||
await response3.delete()
|
||||
await bot_send_link.delete()
|
||||
os.remove("hui.mp4")
|
||||
await asyncio.sleep(5)
|
||||
except: pass
|
||||
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)
|
||||
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 для показа открытых чатов """
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
reply = await message.get_reply_message()
|
||||
users_list = self.db.get('TTsaveMod', 'users', [])
|
||||
|
||||
if args == '-l':
|
||||
if len(users_list) == 0: return await utils.answer(message, 'Список пуст.')
|
||||
return await utils.answer(message, '• '+'\n• '.join(['<code>'+str(i)+'</code>' for i in users_list]))
|
||||
|
||||
try:
|
||||
if not args and not reply:
|
||||
user = message.chat_id
|
||||
else:
|
||||
user = reply.sender_id if not args else int(args)
|
||||
except:
|
||||
return await utils.answer(message, 'Неверно введён ид.')
|
||||
if user in users_list:
|
||||
users_list.remove(user)
|
||||
await utils.answer(message, f'Ид <code>{str(user)}</code> исключен.')
|
||||
else:
|
||||
users_list.append(user)
|
||||
await utils.answer(message, f'Ид <code>{str(user)}</code> добавлен.')
|
||||
self.db.set('TTsaveMod', 'users', users_list)
|
||||
|
||||
async def watcher(self, message):
|
||||
try:
|
||||
users = self.db.get('TTsaveMod', 'users', [])
|
||||
if message.chat_id not in users: return
|
||||
links = re.findall(r'((?:https?://)?v[mt]\.tiktok\.com/[A-Za-z0-9_]+/?)', message.raw_text)
|
||||
if len(links) == 0: return
|
||||
|
||||
for link in links:
|
||||
await self.save_video(message, url=link)
|
||||
await asyncio.sleep(5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user