Merge pull request #226 from MuRuLOSE/update-submodules_e8f808b7dc145caab9c5cc1ce2901adcdb112f03

Update of repositories 2026-03-23 01:31:45
This commit is contained in:
Macsim
2026-03-23 18:57:25 +03:00
committed by GitHub
6 changed files with 403 additions and 39 deletions

View File

@@ -24,3 +24,4 @@ deviceinfo
mpi
aigenuser
github
stream

View File

@@ -0,0 +1,222 @@
# ______ ___ ___ _ _
# ____ | ___ \ | \/ | | | | |
# / __ \| |_/ / _| . . | ___ __| |_ _| | ___
# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \
# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/
# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___|
# \____/ __/ |
# |___/
# На модуль распространяется лицензия "GNU General Public License v3.0"
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule
# requires: aiohttp
import asyncio
import hashlib
import hmac
import logging
import re
import time
from .. import loader
logger = logging.getLogger(__name__)
BLOB_RE = re.compile(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.+)")
RAW_RE = re.compile(r"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)")
API_BASE = "https://37a5bcc11453.vps.myjino.ru"
BOT_USERNAME = "pyupdater_bot"
GITHUB_TOKEN = ""
TOKEN_DB_KEY = "user_token"
TOKEN_PREFIX = "pyut_"
def _parse(url: str):
for pat in (BLOB_RE, RAW_RE):
m = pat.match(url.strip())
if m:
return m.groups()
return None
def _gh_headers() -> dict:
h = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
if GITHUB_TOKEN:
h["Authorization"] = f"Bearer {GITHUB_TOKEN}"
return h
def _sign(secret_key: str, api_key: str) -> str:
bucket = (int(time.time()) // 60) * 60
msg = f"{api_key}:{bucket}".encode()
return hmac.new(secret_key.encode(), msg, hashlib.sha256).hexdigest()
class PyUpdaterLib(loader.Library):
developer = "@pyupdater"
version = (1, 0, 0)
async def init(self):
await self._refresh_token()
async def _refresh_token(self):
"""Каждый раз при загрузке библиотеки запрашивает свежий токен у бота."""
try:
sent = await self.client.send_message(BOT_USERNAME, "/token")
response = None
for _ in range(20):
await asyncio.sleep(0.5)
msgs = await self.client.get_messages(BOT_USERNAME, limit=1)
if (
msgs
and msgs[0].id != sent.id
and msgs[0].text
and msgs[0].text.startswith(TOKEN_PREFIX)
):
response = msgs[0]
break
if response is None:
logger.warning("PyUpdater: не получили токен от бота")
return
self._lib_set(TOKEN_DB_KEY, response.text.strip())
logger.info("PyUpdater: токен обновлён")
async def _cleanup():
await asyncio.sleep(3)
try:
await sent.delete()
await response.delete()
except Exception:
pass
asyncio.create_task(_cleanup())
except Exception:
logger.exception("PyUpdater: ошибка при получении токена")
async def check(
self,
module: loader.Module,
github_url: str,
api_key: str,
secret_key: str,
) -> None:
parts = _parse(github_url)
if parts is None:
logger.warning("PyUpdater: cannot parse URL %r", github_url)
return
owner, repo, branch, path = parts
module_name = module.strings["name"]
asyncio.create_task(self._ping(api_key, secret_key, module.tg_id, module_name))
asyncio.create_task(self._maybe_confirm_test(api_key, module.tg_id))
commits_url = (
f"https://api.github.com/repos/{owner}/{repo}/commits"
f"?path={path}&sha={branch}&per_page=1"
)
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}"
db_key = f"pyu_sha_{owner}_{repo}_{path.replace('/', '_')}"
saved_sha: str = module._db.get(module_name, db_key, "")
try:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(
commits_url,
headers=_gh_headers(),
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
if resp.status in (403, 429):
logger.warning("PyUpdater: GitHub rate limit for %s", module_name)
return
resp.raise_for_status()
commits = await resp.json()
if not commits:
return
latest_sha: str = commits[0]["sha"]
if latest_sha == saved_sha:
return
async with session.get(
raw_url,
headers=_gh_headers(),
timeout=aiohttp.ClientTimeout(total=20),
) as resp:
resp.raise_for_status()
new_code = await resp.text()
except asyncio.TimeoutError:
logger.warning("PyUpdater: timeout for %s", module_name)
return
except Exception:
logger.exception("PyUpdater: error checking %s", module_name)
return
module._db.set(module_name, db_key, latest_sha)
logger.info("PyUpdater: reloading %s (commit %s)", module_name, latest_sha[:7])
loader_mod = next(
(m for m in module.allmodules.modules if m.__class__.__name__ == "LoaderMod"),
None,
)
if loader_mod is None:
logger.error("PyUpdater: LoaderMod not found")
return
asyncio.create_task(loader_mod.load_module(new_code, None, save_fs=True))
async def _maybe_confirm_test(self, api_key: str, user_tg_id: int) -> None:
try:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(
f"{API_BASE}/test/pending",
params={"api_key": api_key},
timeout=aiohttp.ClientTimeout(total=8),
) as resp:
if resp.status != 200:
return
data = await resp.json()
if not data.get("pending"):
return
await session.post(
f"{API_BASE}/test/confirm",
json={"api_key": api_key, "user_tg_id": user_tg_id},
timeout=aiohttp.ClientTimeout(total=8),
)
except Exception:
pass
async def _ping(self, api_key: str, secret_key: str, user_tg_id: int, module_name: str) -> None:
user_token: str = self._lib_get(TOKEN_DB_KEY, "")
payload = {
"api_key": api_key,
"signature": _sign(secret_key, api_key),
"user_tg_id": user_tg_id,
"module_name": module_name,
}
if user_token:
payload["user_token"] = user_token
try:
import aiohttp
async with aiohttp.ClientSession() as session:
await session.post(
f"{API_BASE}/ping",
json=payload,
timeout=aiohttp.ClientTimeout(total=8),
)
except Exception:
pass

View File

@@ -0,0 +1 @@
# 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

View File

@@ -88,6 +88,12 @@ class SenderGifts(loader.Module):
"gifts": [
{"id": 5866352046986232958, "emoji": "🧸", "name": "8 Марта мишка", "price": 50},
]
},
"saint_patricks_day ": {
"name": "💰 День святого патрика",
"gifts": [
{"id": 5893356958802511476, "emoji": "🧸", "name": "Лепрекон мишка", "price": 50},
]
}
}

View File

@@ -52266,13 +52266,7 @@
"_doc_text_ru": "Текст, который будет написан рядом с файлом",
"_doc_username_ru": "Ваш username с last.fm",
"nick_error_ru": "<emoji document_id=5465665476971471368>❌</emoji> <b>Укажите ваш никнейм с last.fm</b>",
"uploading_ru": "<emoji document_id=5841359499146825803>🕔</emoji> <i>Загрузка баннера...</i>",
"name_jp": "LastFm",
"no_track_jp": "<emoji document_id=5465665476971471368>❌</emoji> <b>現在再生中のトラックはありません</b>",
"_doc_text_jp": "ファイルの横に表示されるテキスト",
"_doc_username_jp": "Last.fmのユーザー名",
"nick_error_jp": "<emoji document_id=5465665476971471368>❌</emoji> <b>Last.fmのニックネームを入力してください</b>",
"uploading_jp": "<emoji document_id=5841359499146825803>🕔</emoji> <i>バナーをアップロード中...</i>"
"uploading_ru": "<emoji document_id=5841359499146825803>🕔</emoji> <i>Загрузка баннера...</i>"
},
"has_on_load": false,
"has_on_unload": false,
@@ -62648,6 +62642,148 @@
"has_on_unload": false,
"class_cmd_names": {}
},
"fiksofficial/python-modules/stream.py": {
"name": "StreamMod",
"description": "📡 RTMP media streaming",
"cls_doc": {
"ru": "📡 RTMP стриминг медиафайлов"
},
"meta": {
"pic": null,
"banner": null,
"developer": null
},
"commands": [
{
"stream": "[reply to media] — start stream or add to queue | (RU) [ответ на медиа] запустить трансляцию"
},
{
"streamctl": " open stream control panel | (RU) панель управления трансляцией"
},
{
"streamstop": " stop stream and clear queue | (RU) остановить трансляцию и очистить очередь"
}
],
"new_commands": [
{
"name": "stream",
"original_name": "stream",
"description": {
"default": "[reply to media] — start stream or add to queue",
"ru": "[ответ на медиа] запустить трансляцию"
},
"cmd_names": {},
"aliases": [],
"usage": null,
"inline": false,
"is_inline_handler": false,
"decorators": []
},
{
"name": "streamctl",
"original_name": "streamctl",
"description": {
"default": " open stream control panel",
"ru": " панель управления трансляцией"
},
"cmd_names": {},
"aliases": [],
"usage": null,
"inline": false,
"is_inline_handler": false,
"decorators": []
},
{
"name": "streamstop",
"original_name": "streamstop",
"description": {
"default": " stop stream and clear queue",
"ru": " остановить трансляцию и очистить очередь"
},
"cmd_names": {},
"aliases": [],
"usage": null,
"inline": false,
"is_inline_handler": false,
"decorators": []
}
],
"inline_handlers": [],
"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...",
"status_active_ru": "▶️ <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_ru": "⏸ <b>Трансляция не активна</b>",
"status_queue_ru": "\n📋 В очереди: <b>{n}</b>",
"stopped_ru": "⏹ <b>Трансляция остановлена.</b>",
"no_rtmp_ru": "❌ <b>RTMP не настроен!</b>\nНажми кнопку чтобы задать прямо сейчас.",
"downloading_ru": "⏳ Скачиваю…",
"dl_failed_ru": "❌ Не удалось скачать файл.",
"queued_ru": "📋 Добавлено в очередь ({n} шт.)\n{icon} <code>{file}</code>",
"not_running_ru": "Не запущено",
"queue_empty_ru": "Очередь пуста",
"queue_header_ru": "📋 Очередь:\n",
"settings_title_ru": "⚙️ <b>Настройки трансляции</b>",
"btn_stop_ru": "⏹ Стоп",
"btn_queue_ru": "📋 Очередь",
"btn_refresh_ru": "🔄 Обновить",
"btn_settings_ru": "⚙️ Настройки",
"btn_status_ru": "📊 Статус",
"btn_back_ru": "🔙 Назад",
"btn_preset_ru": "🎞 Пресет: {v}",
"btn_tune_ru": "🎭 Tune: {v}",
"btn_vbr_ru": "🎥 Видео: {v}",
"btn_abr_ru": "🔊 Аудио: {v}",
"btn_fps_ru": "📐 FPS: {v}",
"btn_res_ru": "🖥 Разр: {v}",
"btn_threads_ru": "🧵 Треды: {v}",
"btn_rtmps_ru": "📡 RTMP URL",
"btn_key_ru": "🔑 Ключ",
"btn_set_rtmps_ru": "📡 Задать RTMP URL",
"btn_set_key_ru": "🔑 Задать ключ",
"ph_vbr_ru": "Битрейт видео, напр. 2000k",
"ph_abr_ru": "Битрейт аудио, напр. 128k",
"ph_threads_ru": "Потоков (0 = авто)",
"ph_rtmps_ru": "rtmp://a.rtmp.youtube.com/live2",
"ph_key_ru": "Ключ трансляции..."
},
"has_on_load": false,
"has_on_unload": false,
"class_cmd_names": {}
},
"fiksofficial/python-modules/lyrics.py": {
"name": "LyricsMod",
"description": "Модуль для поиска текста песни через Genius API напрямую",
@@ -82294,7 +82430,7 @@
}
},
"meta": {
"total_modules": 1021,
"generated_at": "2026-03-11T01:22:17.528364"
"total_modules": 1022,
"generated_at": "2026-03-23T01:31:16.986395"
}
}

View File

@@ -63,7 +63,7 @@ class Banners:
font_bytes = requests.get(self.font_url).content
title_font = self._get_font(55, font_bytes)
artist_font = self._get_font(45, font_bytes)
lfm_font = self._get_font(35, font_bytes)
lfm_font = self._get_font(55, font_bytes)
img = self._prepare_background(W, H)
draw = ImageDraw.Draw(img)
@@ -88,8 +88,8 @@ class Banners:
draw.text((text_x, text_y_start), display_title, font=title_font, fill="white")
draw.text((text_x, text_y_start + 70), display_artist, font=artist_font, fill="#B3B3B3")
bar_y = 480
draw.text((text_x, bar_y), "last.fm", font=lfm_font, fill="white")
text_y = 430
draw.text((text_x, text_y), "last.fm", font=lfm_font, fill="white")
by = io.BytesIO()
img.save(by, format="PNG")
@@ -98,24 +98,24 @@ class Banners:
return by
def vertical(self):
W, H = 1000, 1500
padding = 80
W, H = 1000, 1300
padding = 60
cover_size = 800
font_bytes = requests.get(self.font_url).content
title_font = self._get_font(60, font_bytes)
artist_font = self._get_font(45, font_bytes)
lfm_font = self._get_font(35, font_bytes)
lfm_font = self._get_font(60, font_bytes)
img = self._prepare_background(W, H)
draw = ImageDraw.Draw(img)
cover = self._prepare_cover(cover_size, 40)
cover_x = (W - cover_size) // 2
cover_y = 120
cover_y = 100
img.paste(cover, (cover_x, cover_y), cover)
text_area_y = cover_y + cover_size + 120
text_area_y = cover_y + cover_size + 60
text_width_limit = W - (padding * 2)
display_title = self.title
@@ -134,10 +134,10 @@ class Banners:
artist_w = artist_font.getlength(display_artist)
draw.text(((W - artist_w) / 2, text_area_y + 75), display_artist, font=artist_font, fill="#B3B3B3")
bar_y = text_area_y + 260
text_y = text_area_y + 180
lfm_w = lfm_font.getlength("last.fm")
draw.text(((W - lfm_w) / 2, bar_y), "last.fm", font=lfm_font, fill="white")
draw.text(((W - lfm_w) / 2, text_y), "last.fm", font=lfm_font, fill="white")
by = io.BytesIO()
img.save(by, format="PNG")
@@ -165,14 +165,6 @@ class lastfmmod(loader.Module):
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> <b>Укажите ваш никнейм с last.fm</b>",
"uploading": "<emoji document_id=5841359499146825803>🕔</emoji> <i>Загрузка баннера...</i>",
}
strings_jp = {
"name": "LastFm",
"no_track": "<emoji document_id=5465665476971471368>❌</emoji> <b>現在再生中のトラックはありません</b>",
"_doc_text": "ファイルの横に表示されるテキスト",
"_doc_username": "Last.fmのユーザー名",
"nick_error": "<emoji document_id=5465665476971471368>❌</emoji> <b>Last.fmのニックネームを入力してください</b>",
"uploading": "<emoji document_id=5841359499146825803>🕔</emoji> <i>バナーをアップロード中...</i>",
}
def __init__(self):
self.config = loader.ModuleConfig(
@@ -180,6 +172,7 @@ class lastfmmod(loader.Module):
loader.ConfigValue("custom_text", "<emoji document_id=5413612466208799435>🤩</emoji> <b>{song_name}</b> — <b>{song_artist}</b>", lambda: self.strings["_doc_text"]),
loader.ConfigValue("font", "https://raw.githubusercontent.com/kamekuro/assets/master/fonts/Onest-Bold.ttf", "Custom font URL (ttf)"),
loader.ConfigValue("banner_version", "horizontal", lambda: "Banner version", validator=loader.validators.Choice(["horizontal", "vertical"])),
loader.ConfigValue("fallback_cover", "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png", "Fallback cover URL if track has no image"),
)
@loader.command(alias="np")
@@ -191,20 +184,25 @@ class lastfmmod(loader.Module):
return await utils.answer(message, self.strings["nick_error"])
try:
msg = await utils.answer(message, self.strings["uploading"])
url = f'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&nowplaying=true&user={user}&api_key=460cda35be2fbf4f28e8ea7a38580730&format=json'
data = requests.get(url).json()
track = next((t for t in data.get('recenttracks', {}).get('track', []) if t.get('@attr', {}).get('nowplaying')), None)
if not track:
return await utils.answer(message, self.strings["no_track"])
return await utils.answer(msg, self.strings["no_track"])
name = track.get('name', 'Unknown')
artist = track.get('artist', {}).get('#text', 'Unknown')
caption = self.config["custom_text"].format(song_artist=artist, song_name=name)
imgs = track.get('image', [])
cov_url = next((i['#text'] for i in imgs if i['size'] == 'extralarge'), imgs[-1]['#text'] if imgs else None)
if not cov_url:
return await utils.answer(message, caption)
msg = await utils.answer(message, self.strings["uploading"])
if not cov_url or not str(cov_url).strip():
cov_url = self.config["fallback_cover"]
if not cov_url or not str(cov_url).strip():
return await utils.answer(msg, caption)
cov_bytes = await utils.run_sync(requests.get, cov_url)
banners = Banners(name, artist, cov_bytes.content, self.config["font"])
file = await utils.run_sync(getattr(banners, self.config["banner_version"]))