Added and updated repositories 2026-04-12 13:56:57

This commit is contained in:
github-actions[bot]
2026-04-12 13:56:57 +00:00
parent 7555ea280e
commit 17ae450f8f
19 changed files with 6309 additions and 953 deletions

View 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'])

View File

@@ -24,4 +24,6 @@ deviceinfo
mpi
aigenuser
github
stream
stream
placeholders+
PyInstall

View File

@@ -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)

View 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

View File

@@ -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"))