mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 14:34:17 +02:00
Added and updated repositories 2026-04-12 13:56:57
This commit is contained in:
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'])
|
||||
@@ -24,4 +24,6 @@ deviceinfo
|
||||
mpi
|
||||
aigenuser
|
||||
github
|
||||
stream
|
||||
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"))
|
||||
Reference in New Issue
Block a user