Added and updated repositories 2026-01-06 01:11:05

This commit is contained in:
github-actions[bot]
2026-01-06 01:11:05 +00:00
parent 4f81ff0547
commit cea0d40a58
30 changed files with 4204 additions and 1026 deletions

View File

@@ -23,10 +23,11 @@
# meta pic: https://i.postimg.cc/Hx3Zm8rB/logo.png
# meta banner: https://te.legra.ph/file/55fa6eebae860a359ac27.jpg
__version__ = (1, 2, 0)
__version__ = (1, 3, 2)
from .. import loader, utils
from telethon.tl.types import Message # type: ignore
from .. import loader, utils # type: ignore
from telethon.tl import types # type: ignore
from telethon.tl.types import Message, InputDocument # type: ignore
@loader.tds
class SendMod(loader.Module):
@@ -104,19 +105,68 @@ class SendMod(loader.Module):
@loader.command(
ru_doc="[text] - Написать сообщение в закрытую тему",
uz_doc="[text] - Yopiq mavzuga xabar yozing",
de_doc="[text] - Schreiben Sie eine Nachricht zu einem geschlossenen Thema",
es_doc="[text] - Escribir un mensaje a un tema cerrado",
ru_doc="[text or reply(media/file/sticker) or coordinates (<lat>, <long>)] - Написать сообщение в закрытую тему",
uz_doc="[text or reply(media/file/sticker) or coordinates (<lat>, <long>)] - Yopiq mavzuga xabar yozing",
de_doc="[text or reply(media/file/sticker) or coordinates (<lat>, <long>)] - Schreiben Sie eine Nachricht zu einem geschlossenen Thema",
es_doc="[text or reply(media/file/sticker) or coordinates (<lat>, <long>)] - Escribir un mensaje a un tema cerrado",
)
async def sendclosedtopic(self, message: Message):
"""[text] - Write a message to a closed topic"""
if not utils.get_args_raw(message):
await utils.answer(message, self.strings["error_send"])
"""[text or reply(media/file/sticker) or coordinates (<lat>, <long>)] - Write a message to a closed topic"""
args = utils.get_args_raw(message)
message_text = args if args else ""
reply = await message.get_reply_message()
media = None
temp_file = None
if reply and reply.media:
doc = getattr(reply.media, "document", None)
if doc and any(a.__class__.__name__ == "DocumentAttributeSticker" for a in doc.attributes):
media = InputDocument(
id=doc.id,
access_hash=doc.access_hash,
file_reference=doc.file_reference
)
message_text = ""
elif doc and doc.mime_type == "image/webp":
temp_file = await reply.download_media()
media = temp_file
else:
media = reply.media
else:
text2 = f"{utils.get_args_raw(message)}"
await message.delete()
await message.reply(text2)
media = message.media
if message_text and "," in message_text:
lat_str, long_str = message_text.split(",", 1)
try:
gps_x = float(lat_str.strip())
gps_y = float(long_str.strip())
if -90 <= gps_x <= 90 and -180 <= gps_y <= 180:
geo_point = types.InputGeoPoint(lat=gps_x, long=gps_y)
media = types.InputMediaGeoPoint(geo_point)
message_text = ""
except ValueError:
pass
if not message_text and not media:
await utils.answer(message, self.strings["error_send_2"])
return
await message.delete()
await message.reply(
message_text,
file=media if media else None,
parse_mode="html"
)
if temp_file:
import os
try:
os.remove(temp_file)
except:
pass
@loader.command(
ru_doc="[@UserName] [text or replay] - Написать сообщение в личные сообщения",

View File

@@ -1,4 +1,4 @@
__version__ = (1, 1, 0)
version = (1, 4, 0)
# meta developer: @RUIS_VlP, @RoKrz
# requires: yt_dlp
@@ -7,51 +7,190 @@ import yt_dlp
import uuid
import os
import re
import tempfile
from .. import loader, utils
def extract_youtube_link(text):
def extract_video_link(text):
"""Извлекает видео с сайтов"""
if not text:
return None
match = re.search(r"(https?://)?(www\.)?(youtube\.com|youtu\.be)/[^\s]+", text)
return match.group(0) if match else None
video_sites_patterns = [
# YouTube
r"(https?://)?(www\.)?(youtube\.com|youtu\.be)/[^\s]+",
# Social Media
r"(https?://)?(www\.)?(tiktok\.com|vt\.tiktok\.com|vm\.tiktok\.com)/[^\s]+",
r"(https?://)?(www\.)?instagram\.com/(p|reel|tv)/[^\s]+",
r"(https?://)?(www\.)?(twitter\.com|x\.com)/[^\s]+/status/[^\s]+",
r"(https?://)?(www\.)?facebook\.com/[^\s]+/videos/[^\s]+",
r"(https?://)?(www\.)?reddit\.com/r/[^\s]+/comments/[^\s]+",
async def download_video(url):
# Video Platforms
r"(https?://)?(www\.)?vimeo\.com/[^\s]+",
r"(https?://)?(www\.)?dailymotion\.com/video/[^\s]+",
r"(https?://)?(www\.)?twitch\.tv/(videos/|clip/|[^/]+$)[^\s]*",
r"(https?://)?(www\.)?streamable\.com/[^\s]+",
# News & Media
r"(https?://)?(www\.)?bbc\.co\.uk/iplayer/[^\s]+",
r"(https?://)?(www\.)?cnn\.com/videos/[^\s]+",
r"(https?://)?(www\.)?reuters\.com/video/[^\s]+",
# Educational
r"(https?://)?(www\.)?coursera\.org/learn/[^\s]+",
r"(https?://)?(www\.)?udemy\.com/course/[^\s]+",
r"(https?://)?(www\.)?khanacademy\.org/[^\s]+",
# Russian platforms
r"(https?://)?(www\.)?rutube\.ru/video/[^\s]+",
r"(https?://)?(www\.)?vk\.com/(video|clip)[^\s]+",
r"(https?://)?(www\.)?ok\.ru/video/[^\s]+",
# Other popular platforms
r"(https?://)?(www\.)?pornhub\.com/view_video\.php\?viewkey=[^\s]+",
r"(https?://)?(www\.)?xvideos\.com/video[^\s]+",
r"(https?://)?(www\.)?soundcloud\.com/[^\s]+",
r"(https?://)?(www\.)?bandcamp\.com/track/[^\s]+",
r"(https?://)?(www\.)?mixcloud\.com/[^\s]+",
# Live streaming
r"(https?://)?(www\.)?periscope\.tv/[^\s]+",
r"(https?://)?(www\.)?ustream\.tv/[^\s]+",
# International
r"(https?://)?(www\.)?bilibili\.com/video/[^\s]+",
r"(https?://)?(www\.)?niconico\.jp/watch/[^\s]+",
r"(https?://)?(www\.)?youku\.com/v_show/[^\s]+",
# Generic fallback for other video URLs
r"https?://[^\s]+\.(mp4|webm|avi|mkv|mov|flv|m4v)",
]
for pattern in video_sites_patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
return match.group(0)
general_url_pattern = r"https?://[^\s]+"
match = re.search(general_url_pattern, text)
if match:
url = match.group(0)
excluded_domains = [
'google.com', 'yandex.ru', 'wikipedia.org', 'github.com',
'stackoverflow.com', 'reddit.com/r/', 'amazon.com'
]
if not any(domain in url.lower() for domain in excluded_domains):
return url
return None
async def download_video(url, cookies_text=None, youtube_client="default", custom_user_agent=None):
output_dir = utils.get_base_dir()
random_uuid = str(uuid.uuid4())
os.makedirs(output_dir, exist_ok=True)
ydl_opts = {
'format': 'best',
'outtmpl': os.path.join(output_dir, f'{random_uuid}.%(ext)s'),
'noplaylist': True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info_dict = ydl.extract_info(url, download=True)
video_ext = info_dict.get('ext', None)
file_path = os.path.join(output_dir, f"{random_uuid}.{video_ext}")
title = info_dict.get('title', None)
return file_path, title
formats_to_try = [
'best[ext=mp4]',
'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]',
'bestvideo+bestaudio/best',
'best',
'best*',
'bestvideo+bestaudio',
'best[height<=1080]',
'best[height<=720]',
'worst',
'worst*',
]
cookies_file = None
if cookies_text and cookies_text.strip():
cleaned_cookies = cookies_text.strip()
if cleaned_cookies.startswith('"') or cleaned_cookies.startswith("'"):
cleaned_cookies = cleaned_cookies[1:]
if cleaned_cookies.endswith('"') or cleaned_cookies.endswith("'"):
cleaned_cookies = cleaned_cookies[:-1]
cookies_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8')
cookies_file.write(cleaned_cookies)
cookies_file.close()
try:
is_youtube = 'youtube.com' in url.lower() or 'youtu.be' in url.lower()
for format_option in formats_to_try:
ydl_opts = {
'format': format_option,
'outtmpl': os.path.join(output_dir, f'{random_uuid}.%(ext)s'),
'noplaylist': True,
'merge_output_format': 'mp4',
}
if cookies_file:
ydl_opts['cookiefile'] = cookies_file.name
if custom_user_agent:
ydl_opts['http_headers'] = {'User-Agent': custom_user_agent}
if is_youtube and youtube_client != "default":
ydl_opts['extractor_args'] = {'youtube': {'player_client': [youtube_client]}}
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info_dict = ydl.extract_info(url, download=True)
video_ext = info_dict.get('ext', None)
file_path = os.path.join(output_dir, f"{random_uuid}.{video_ext}")
title = info_dict.get('title', None)
channel = info_dict.get('uploader', None)
return file_path, title, channel
except Exception as e:
if "Requested format is not available" in str(e) or "No video formats found" in str(e):
continue
else:
raise e
raise Exception("Не удалось скачать видео ни в одном формате")
finally:
if cookies_file:
try:
os.unlink(cookies_file.name)
except:
pass
def convert_markdown_to_html(template: str, link: str) -> str:
return re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2">\1</a>', template).replace("{link}", link)
@loader.tds
class YouTube_DLDMod(loader.Module):
"""Помогает скачивать видео с YouTube"""
"""Помогает скачивать видео с YouTube, TikTok и др."""
strings = {
"name": "YouTube-DLD",
"no_link": "❌ <b>Пожалуйста, укажите ссылку на YouTube либо ответьте на сообщение с ней.</b>",
"no_link": "❌ <b>Пожалуйста, укажите ссылку на видео либо ответьте на сообщение с ней.</b>",
"default_downloading": "📥 <b>Начинаю загрузку видео.</b>\n\n <code>Это может занять до 5 минут, в зависимости от длины и качества видео.</code>",
"default_error": "❌ <b>Ошибка!</b>\n\n<code>{}</code>",
"default_response": "🎥 Вот [ваше видео]({link})!\n\n<code>{title}</code>",
}
"default_channel": "📺 Канал: <code>{channel}</code>",
"cookies_error": "🍪 <b>YouTube требует аутентификацию!</b>\n\n❌ Ошибка: <code>Sign in to confirm you're not a bot</code>\n\n<b>Возможные причины:</b>\n▫️ YouTube детектит запросы без аутентификации\n▫️ IP сервера может быть заблокирован YouTube\n▫️ Видео требует подтверждения возраста/входа\n\n<b>Решения (попробуй по порядку):</b>\n\n<b>1⃣ Смени YouTube клиент:</b>\n• Открой <code>.cfg YouTube-DLD</code>\n• Попробуй разные значения <code>youtube_client</code>:\n - <code>mweb</code> (мобильная веб-версия, часто работает)\n - <code>android</code> (Android приложение, сработало на сервере во Франции)\n - <code>ios</code> (iOS приложение)\n - <code>tv_embedded</code> (встроенный ТВ плеер)\n\n<b>2⃣ Добавь куки (для пользователей из проблемных регионов):</b>\n• Открой НОВОЕ приватное окно (в браузере ctrl+shift+N) и залогинься на YouTube\n• Перейди на https://www.youtube.com/robots.txt в ТОЙ же вкладке\n• Cookie-Editor (расширение) → Export → Netscape format\nСРАЗУ закрой приватное окно\n• Вставь куки в <code>youtube_cookies</code> (БЕЗ кавычек)",
"supported_sites": """🎥 <b>Поддерживаемые сайты:</b>
🔴 <b>YouTube</b> — youtube.com, youtu.be
🎵 <b>TikTok</b> — tiktok.com, vt.tiktok.com, vm.tiktok.com
📸 <b>Instagram</b> — instagram.com (посты, reels, IGTV)
🐦 <b>X (Twitter)</b> — x.com, twitter.com
👥 <b>Facebook</b> — facebook.com (видео)
🎬 <b>Vimeo</b> — vimeo.com
📺 <b>Twitch</b> — twitch.tv (стримы, клипы, VOD)
🤖 <b>Reddit</b> — reddit.com (видео из постов)
⚡ <b>Dailymotion</b> — dailymotion.com
<b>🇷🇺 Российские:</b>
▫️ <b>RuTube</b> — rutube.ru
▫️ <b>ВКонтакте</b> — vk.com (видео)
▫️ <b>Одноклассники</b> — ok.ru
<b>📚 Образовательные:</b>
▫️ <b>Coursera</b> — coursera.org
▫️ <b>Udemy</b> — udemy.com
▫️ <b>Khan Academy</b> — khanacademy.org
<b>🌍 Международные:</b>
▫️ <b>Bilibili</b> — bilibili.com
▫️ <b>NicoNico</b> — niconico.jp
▫️ <b>BBC iPlayer</b> — bbc.co.uk/iplayer
<b>🎵 Аудио:</b>
▫️ <b>SoundCloud</b> — soundcloud.com
▫️ <b>Bandcamp</b> — bandcamp.com
▫️ <b>Mixcloud</b> — mixcloud.com
<i>И многие другие платформы...</i>"""
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
@@ -75,32 +214,84 @@ class YouTube_DLDMod(loader.Module):
self.strings["default_response"],
"Ответ после загрузки. (используй {link} для ссылки и {title} для названия видео)"
),
loader.ConfigValue(
"show_channel",
True,
"Показывать название канала?",
validator=loader.validators.Boolean(),
),
loader.ConfigValue(
"channel_text",
self.strings["default_channel"],
"Текст для отображения канала. (используй {channel} для имени канала)"
),
loader.ConfigValue(
"youtube_cookies",
"",
"🍪 Куки YouTube в формате Netscape (опционально)\n\n"
"⚠️ ВНИМАНИЕ: Риск бана аккаунта! Используй тестовый аккаунт.\n\n"
"Как получить:\n"
"1. НОВОЕ приватное окно (Ctrl+Shift+N) → залогинься на YouTube\n"
"2. Перейди на https://www.youtube.com/robots.txt в той же вкладке\n"
"3. Cookie-Editor (расширение) → Export → Netscape format\n"
"4. СРАЗУ закрой приватное окно\n"
"5. Вставь текст сюда (БЕЗ кавычек)",
validator=loader.validators.String(),
),
loader.ConfigValue(
"youtube_client",
"mweb",
"📱 YouTube клиент для обхода блокировок\n\n"
"Доступные варианты:\n"
"• default - стандартный (может не работать)\n"
"• mweb - мобильная веб-версия\n"
"• android - Android приложение (рекомендуется)\n"
"• ios - iOS приложение\n"
"• tv_embedded - встроенный ТВ плеер\n\n"
"Если видео не скачивается, попробуй другой клиент!",
validator=loader.validators.Choice(["default", "mweb", "android", "ios", "tv_embedded"]),
),
loader.ConfigValue(
"custom_user_agent",
"",
"🌐 Кастомный User-Agent (опционально)\n\n"
"Можно указать User-Agent браузера для обхода некоторых блокировок.\n"
"Например:\n"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\n\n"
"Оставь пустым для использования стандартного.",
validator=loader.validators.String(),
),
)
@loader.command()
async def dlvideo(self, message):
"""<ссылка> или ответ на сообщение со ссылкой — скачивает видео с YouTube"""
async def dvlist(self, message):
"""Показать список всех поддерживаемых сайтов"""
await utils.answer(message, self.strings["supported_sites"])
@loader.command()
async def dlvideo(self, message):
"""<ссылка> или ответ на сообщение со ссылкой — скачивает видео с поддерживаемых платформ"""
args = utils.get_args_raw(message)
reply = await message.get_reply_message()
link = extract_youtube_link(args) if args else None
link = extract_video_link(args) if args else None
if not link and reply:
link = extract_youtube_link(reply.raw_text)
link = extract_video_link(reply.raw_text)
if not link:
await utils.answer(message, self.strings["no_link"])
return
await utils.answer(message, self.config["downloading_text"])
try:
video, title = await download_video(link)
cookies_text = self.config["youtube_cookies"].strip() if self.config["youtube_cookies"] else None
youtube_client = self.config["youtube_client"]
user_agent = self.config["custom_user_agent"].strip() if self.config["custom_user_agent"] else None
video, title, channel = await download_video(link, cookies_text, youtube_client, user_agent)
if self.config["show_link"]:
caption_template = self.config["response_text"]
caption = convert_markdown_to_html(caption_template, link)
caption = caption.replace("{title}", title or "")
if self.config["show_channel"] and channel:
channel_text = self.config["channel_text"].replace("{channel}", channel)
caption += f"\n\n{channel_text}"
else:
caption = title or "Готово!"
@@ -108,9 +299,10 @@ class YouTube_DLDMod(loader.Module):
message,
video,
caption=caption,
parse_mode="HTML"
parse_mode="HTML",
reply_to=reply or message,
silent=True
)
try:
await message.delete()
except:
@@ -120,9 +312,14 @@ class YouTube_DLDMod(loader.Module):
except:
pass
except Exception as e:
error_msg = self.config["error_text"].format(e)
await utils.answer(message, error_msg)
error_str = str(e)
if "Sign in to confirm you're not a bot" in error_str or "Use --cookies" in error_str:
await utils.answer(message, self.strings["cookies_error"])
else:
error_msg = self.config["error_text"].format(e)
await utils.answer(message, error_msg)
try:
os.remove(video)
if 'video' in locals():
os.remove(video)
except:
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,999 @@
# meta developer: @codrago_m
# scope: disable_onload_docs
# packurl: https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/translations/chatmodule.yml
import logging
import typing
from datetime import datetime, timedelta, timezone
from telethon.tl import types
from telethon.tl.functions import channels, messages
from .. import loader, utils
logger = logging.getLogger("ChatModule")
@loader.tds
class ChatModuleMod(loader.Module):
strings = {
"name": "ChatModule",
}
async def client_ready(self, client, db):
self._client = client
self._db = db
self.xdlib = await self.import_lib(
"https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/libs/xdlib.py",
suspend_on_error=True,
)
@loader.command(ru_doc="[reply] - Узнать ID")
async def id(self, message):
"""[reply] - Get the ID"""
ids = [self.strings["my_id"].format(id=self.tg_id)]
if message.is_private:
ids.append(self.strings["user_id"].format(id=message.to_id.user_id))
return await utils.answer(message, "\n".join(ids))
ids.append(self.strings["chat_id"].format(id=message.chat_id))
reply = await message.get_reply_message()
if (
reply
and not getattr(reply, "is_private")
and not getattr(reply, "sender_id") == self.tg_id
):
user_id = (await reply.get_sender()).id
ids.append(self.strings["user_id"].format(id=user_id))
return await utils.answer(message, "\n".join(ids))
@loader.command(
ru_doc="[reply/-u username/id] - Посмотреть права администратора пользователя",
)
@loader.tag("no_pm")
async def rights(self, message):
"""[reply/-u username/id] - Check user's admin rights"""
opts = self.xdlib.parse.opts(utils.get_args(message))
reply = await message.get_reply_message()
user = opts.get("u") or opts.get("user") or (reply.sender_id if reply else None)
if not user:
return await utils.answer(message, self.strings["no_user"])
rights = await self.xdlib.chat.get_rights(message.chat, user)
participant = rights.participant
user = await self._client.get_entity(user)
if not hasattr(participant, "admin_rights"):
return await utils.answer(
message, self.strings["not_an_admin"].format(user=user.first_name)
)
if participant.admin_rights:
can_do = []
rights = participant.to_dict().get("admin_rights")
for right, is_permitted in rights.items():
if right == "_":
continue
if is_permitted:
can_do.append(right)
promoter = (
await self._client.get_entity(participant.promoted_by)
if hasattr(participant, "promoted_by")
else None
)
return await utils.answer(
message,
self.strings["admin_rights"].format(
rights="\n".join(
[
f"<emoji document_id=5409029658794537988>✅</emoji> {self.strings[right]}"
for right in can_do
]
),
promoter_id=promoter.id if promoter else 0,
promoter_name=(
promoter.first_name if promoter else self.strings["no"]
),
name=user.first_name,
),
)
return await utils.answer(
message, self.strings["not_an_admin"].format(user=user.first_name)
)
@loader.command(
ru_doc="Покинуть чат",
)
@loader.tag("no_pm")
async def leave(self, message):
"""Leave chat"""
await message.delete()
await self._client(channels.LeaveChannelRequest((await message.get_chat()).id))
@loader.command(
ru_doc="[a[1-100] b[1-100]] | [reply] Удалить сообщения",
)
async def d(self, message):
"""[a[1-100] b[1-100]] | [reply] - Delete messages"""
await self.xdlib.messages.delete_messages(message)
@loader.command(ru_doc="[reply] - Закрепить сообщение")
@loader.tag("only_reply")
async def pin(self, message):
"""[reply] - Pin a message"""
reply = await message.get_reply_message()
try:
await reply.pin(notify=True, pm_oneside=False)
except Exception as e:
logger.error(str(e))
return await utils.answer(message, self.strings["pin_failed"])
await utils.answer(message, self.strings["pinned"])
@loader.command(ru_doc="Открепить сообщение")
@loader.tag("only_reply")
async def unpin(self, message):
"""Unpin a message"""
reply = await message.get_reply_message()
try:
await reply.unpin()
except Exception as e:
logger.error(str(e))
return await utils.answer(message, self.strings["unpin_failed"])
await utils.answer(message, self.strings["unpinned"])
@loader.command(ru_doc="[-c id] Удаляет группу/канал")
async def dgc(self, message):
"""[-c id] Delete chat/channel"""
args = utils.get_args(message)
opts = self.xdlib.parse.opts(args)
chat_id = opts.get("c") or opts.get("chat")
if chat_id:
chat = await self._client.get_entity(chat_id)
if isinstance(chat, types.Channel):
await self._client(channels.DeleteChannelRequest(chat.id))
elif isinstance(chat, types.Chat):
await self._client(messages.DeleteChatRequest(chat.id))
else:
return await utils.answer(message, self.strings["failed_to_delete"])
return await utils.answer(message, self.strings["successful_delete"])
if isinstance(message.chat, types.Channel):
await self._client(channels.DeleteChannelRequest(message.chat))
elif isinstance(message.chat, types.Chat):
await self._client(messages.DeleteChatRequest(message.chat))
else:
return await utils.answer(message, self.strings["failed_to_delete"])
return
@loader.command(ru_doc="Очищает группу/канал от удаленных аккаунтов")
@loader.tag("no_pm")
async def flush(self, message):
"""Removes deleted accounts from the chat/channel"""
chat = await message.get_chat()
if not getattr(chat, "admin_rights", False) and not getattr(
getattr(chat, "admin_rights", None), "ban_users", False
):
return await utils.answer(message, self.strings["no_rights"])
deleted = await self.xdlib.chat.get_deleted(chat)
if not deleted:
return await utils.answer(message, self.strings["no_deleted_accounts"])
for to_delete in deleted:
try:
await self._client.kick_participant(chat, to_delete)
except Exception as e:
logger.error(str(e))
return await utils.answer(message, self.strings["error"])
return await utils.answer(message, self.strings["kicked_deleted_accounts"])
@loader.command(ru_doc="Показывает админов в группе/канале")
@loader.tag("no_pm")
async def admins(self, message):
"""Shows the admins in the chat/channel"""
admins = await self.xdlib.chat.get_admins(message.chat, True)
creator = await self.xdlib.chat.get_creator(message.chat)
return await utils.answer(
message,
self.strings["admin_list"].format(
id=creator.id if creator else 0,
name=creator.first_name if creator else self.strings["no"],
admins_count=len(admins) or 0,
admins=(
"\n".join(
f"<emoji document_id=5774022692642492953>✅</emoji> <a href='tg://user?id={admin.id}'>{admin.first_name}</a> [<code>{admin.id}</code>] / <code>{admin.participant.rank}</code>"
for admin in admins
)
if admins
else f"\n{self.strings['no_admins_in_chat']}"
),
),
)
@loader.command(ru_doc="Показывает ботов в группе/канале")
@loader.tag("no_pm")
async def bots(self, message):
"""Shows the bots in the chat/channel"""
bots = await self.xdlib.chat.get_bots(message.chat)
if not bots:
return await utils.answer(message, self.strings["no_bots_in_chat"])
await utils.answer(
message,
self.strings["bot_list"].format(
count=len(bots),
bots="\n".join(
[
f"<emoji document_id=5774022692642492953>✅</emoji> <a href='tg://user?id={bot.id}'>{bot.first_name}</a> [<code>{bot.id}</code>]"
for bot in bots
]
),
),
)
@loader.command(ru_doc="Показывает простых участников чата/канала")
@loader.tag("no_pm")
async def users(self, message):
"""Shows the users in the chat/channel"""
users = await self.xdlib.chat.get_members(message.chat)
if not users:
return await utils.answer(message, self.strings["no_user_in_chat"])
await utils.answer(
message,
self.strings["user_list"].format(
count=len(users),
users="\n".join(
[
f"<emoji document_id=5774022692642492953>✅</emoji> <a href='tg://user?id={user.id}'>{user.first_name}</a> [<code>{user.id}</code>]"
for user in users
]
),
),
)
@loader.command(ru_doc="[-u] [-t] [-r] Забанить участника")
@loader.tag("no_pm")
async def ban(self, message):
"""[-u] [-t] [-r] Ban a participant temporarily or permanently"""
opts = self.xdlib.parse.opts(utils.get_args(message))
reason = opts.get("r")
reply = await message.get_reply_message()
user = opts.get("u") or (reply.sender_id if reply else None)
user = await self._client.get_entity(user) if user else None
strings = []
if not user:
return await utils.answer(message, self.strings["no_user"])
seconds = self.xdlib.parse.time(opts.get("t")) if opts.get("t") else None
until_date = (
(datetime.now(timezone.utc) + timedelta(seconds=seconds))
if seconds
else None
)
time_info = f" {self.xdlib.format.time(seconds)}" if seconds else None
try:
await self._client.edit_permissions(
message.chat,
user,
until_date=until_date,
view_messages=False,
)
except Exception as e:
logger.error(str(e))
return await utils.answer(message, self.strings["error"])
strings.append(
self.strings["user_is_banned"].format(
id=user.id,
name=(
getattr(user, "first_name")
if hasattr(user, "first_name")
else getattr(user, "title")
),
time_info=time_info or self.strings["forever"],
)
)
if reason:
strings.append(self.strings["reason"].format(reason=reason))
return await utils.answer(message, "\n".join(strings))
@loader.command(ru_doc="Разбанить пользователя")
@loader.tag("no_pm")
async def unban(self, message):
"""[-u] Unban a user"""
opts = self.xdlib.parse.opts(utils.get_args(message))
reply = await message.get_reply_message()
user = opts.get("u") or (reply.sender_id if reply else None)
user = await self._client.get_entity(user) if user else None
if not user:
return await utils.answer(message, self.strings["no_user"])
try:
await self._client.edit_permissions(message.chat, user, view_messages=True)
except Exception as e:
logger.error(str(e))
return await utils.answer(message, self.strings["error"])
return await utils.answer(
message,
self.strings["user_is_unbanned"].format(
id=user.id,
name=(
getattr(user, "first_name")
if hasattr(user, "first_name")
else getattr(user, "title")
),
),
)
@loader.command(ru_doc="[-u] [-r] Кикнуть участника")
@loader.tag("no_pm")
async def kick(self, message):
"""[-u] [-r] Kick a participant"""
opts = self.xdlib.parse.opts(utils.get_args(message))
reason = opts.get("r")
reply = await message.get_reply_message()
user = opts.get("u") or (reply.sender_id if reply else None)
user = await self._client.get_entity(user) if user else None
strings = []
if not user:
return await utils.answer(message, self.strings["no_user"])
try:
await self._client.kick_participant(message.chat, user)
except Exception as e:
logging.error(str(e))
return await utils.answer(message, self.strings["error"])
strings.append(
self.strings["user_is_kicked"].format(
id=user.id,
name=(
getattr(user, "first_name")
if hasattr(user, "first_name")
else getattr(user, "title")
),
)
)
if reason:
strings.append(self.strings["reason"].format(reason=reason))
return await utils.answer(message, "\n".join(strings))
@loader.command(ru_doc="[-u] [-t] [-r] Замутить участника")
@loader.tag("no_pm")
async def mute(self, message):
"""[-u] [-t] [-r] Mute a participant temporarily or permanently"""
opts = self.xdlib.parse.opts(utils.get_args(message))
reason = opts.get("r")
reply = await message.get_reply_message()
user = opts.get("u") or (reply.sender_id if reply else None)
user = await self._client.get_entity(user) if user else None
strings = []
if not user:
return await utils.answer(message, self.strings["no_user"])
seconds = self.xdlib.parse.time(opts.get("t")) if opts.get("t") else None
until_date = (
(datetime.now(timezone.utc) + timedelta(seconds=seconds))
if seconds
else None
)
time_info = f" {self.xdlib.format.time(seconds)}" if seconds else None
try:
await self._client.edit_permissions(
message.chat,
user,
until_date=until_date,
send_messages=False,
)
except Exception as e:
logger.error(str(e))
return await utils.answer(message, self.strings["error"])
strings.append(
self.strings["user_is_muted"].format(
id=user.id,
name=(
getattr(user, "first_name")
if hasattr(user, "first_name")
else getattr(user, "title")
),
time_info=time_info or self.strings["forever"],
)
)
if reason:
strings.append(self.strings["reason"].format(reason=reason))
return await utils.answer(message, "\n".join(strings))
@loader.command(ru_doc="Размутить участника")
@loader.tag("no_pm")
async def unmute(self, message):
"""Unmute a participant"""
opts = self.xdlib.parse.opts(utils.get_args(message))
reply = await message.get_reply_message()
user = opts.get("u") or (reply.sender_id if reply else None)
user = await self._client.get_entity(user) if user else None
if not user:
return await utils.answer(message, self.strings["no_user"])
try:
await self._client.edit_permissions(message.chat, user, send_messages=True)
except Exception as e:
logger.error(str(e))
return await utils.answer(message, self.strings["error"])
return await utils.answer(
message,
self.strings["user_is_unmuted"].format(
id=user.id,
name=(
getattr(user, "first_name")
if hasattr(user, "first_name")
else getattr(user, "title")
),
),
)
@loader.command(
ru_doc="[-g|--group name] [-c|--channel name] - Создать группу/канал"
)
async def create(self, message):
"""[-g|--group name] [-c|--channel name] - Create group/channel"""
opts = self.xdlib.parse.opts(utils.get_args(message))
group_name = opts.get("g") or opts.get("group")
channel_name = opts.get("c") or opts.get("channel")
if channel_name:
result = await self._client(
channels.CreateChannelRequest(
title=channel_name, broadcast=True, about=""
)
)
chat = await self.xdlib.chat.get_info(result.chats[0])
return await utils.answer(
message,
self.strings["channel_created"].format(
link=chat.get("link"), title=channel_name
),
)
if group_name:
result = await self._client(
channels.CreateChannelRequest(
title=group_name, megagroup=True, about=""
)
)
chat = await self.xdlib.chat.get_info(result.chats[0])
return await utils.answer(
message,
self.strings["group_created"].format(
link=chat.get("link"), title=group_name
),
)
return await utils.answer(message, self.strings["invalid_args"])
@loader.command(
ru_doc="Отключает звук и архивирует чат",
)
async def dnd(self, message):
"""Mutes and archives the current chat"""
dnd = await utils.dnd(self._client, await message.get_chat())
if dnd:
return await utils.answer(message, self.strings["dnd"])
else:
return await utils.answer(message, self.strings["dnd_failed"])
@loader.command(
ru_doc="-u username/id - Пригласить пользователя в чат (-b пригласить инлайн бота)"
)
async def invite(self, message):
"""-u username/id - Invite a user to the chat (use -b to invite the inline bot)"""
args = utils.get_args(message)
opts = self.xdlib.parse.opts(args)
if opts.get("b") or opts.get("bot"):
invited = await self.xdlib.chat.invite_bot(self._client, message.chat)
entity = await self._client.get_entity(self.inline.bot_id)
if invited:
return await utils.answer(
message,
self.strings["user_invited"].format(
user=entity.first_name, id=entity.id
),
)
return await utils.answer(message, self.strings["user_not_invited"])
reply = await message.get_reply_message()
user = opts.get("u") or opts.get("user") or (reply.sender_id if reply else None)
if not user:
return await utils.answer(message, self.strings["no_user"])
entity = await self._client.get_entity(user)
invited = await self.xdlib.chat.invite_user(message.chat, user)
if invited:
return await utils.answer(
message,
self.strings["user_invited"].format(
user=entity.first_name, id=entity.id
),
)
return await utils.answer(message, self.strings["user_not_invited"])
@loader.command(ru_doc="[-i] Получить информацию о сущности")
async def inspect(self, message):
"""[-i] Get the info about the entity"""
opts = self.xdlib.parse.opts(utils.get_args(message))
reply = await message.get_reply_message()
target = (
opts.get("i")
or (reply.sender if reply else await message.get_chat())
or None
)
if not target:
return await utils.answer(message, self.strings["no_user"])
ent = await self._client.get_entity(target)
if isinstance(ent, types.Channel):
try:
chatinfo = await self.xdlib.chat.get_info(ent)
photo = chatinfo.get("chat_photo")
photo = photo if not isinstance(photo, types.PhotoEmpty) else None
return await utils.answer(
message,
self.strings["chatinfo"].format(
id=chatinfo.get("id"),
title=chatinfo.get("title"),
about=chatinfo.get("about") or self.strings["no"],
admins_count=chatinfo.get("admins_count"),
online_count=chatinfo.get("online_count"),
participants_count=chatinfo.get("participants_count"),
kicked_count=chatinfo.get("kicked_count"),
slowmode_seconds=(
self.xdlib.format.time(chatinfo.get("slowmode_seconds"))
if chatinfo.get("slowmode_seconds")
else self.strings["no"]
),
call=(
self.strings["yes"]
if chatinfo.get("call")
else self.strings["no"]
),
ttl_period=(
self.xdlib.format.time(chatinfo.get("ttl_period"))
if chatinfo.get("ttl_period")
else self.strings["no"]
),
requests_pending=chatinfo.get("requests_pending"),
recent_requesters=", ".join(
[
f"<code>{user}</code>"
for user in chatinfo.get("recent_requesters")
]
)
or self.strings["no"],
linked_chat_id=chatinfo.get("linked_chat_id")
or self.strings["no"],
antispam=(
self.strings["yes"]
if chatinfo.get("antispam")
else self.strings["no"]
),
participants_hidden=(
self.strings["yes"]
if chatinfo.get("participants_hidden")
else self.strings["no"]
),
link=chatinfo.get("link") or self.strings["no"],
is_forum=(
self.strings["yes"]
if chatinfo.get("is_forum")
else self.strings["no"]
),
type_of=(
self.strings["type_group"]
if chatinfo.get("is_group")
else (
self.strings["type_channel"]
if chatinfo.get("is_channel")
else self.strings["type_unknown"]
)
),
),
file=(
types.InputMediaPhoto(
types.InputPhoto(
photo.id, photo.access_hash, photo.file_reference
)
)
if photo
else None
),
)
except Exception as e:
logger.error(str(e))
return await utils.answer(message, self.strings["error"])
if isinstance(ent, types.User):
try:
userinfo = await self.xdlib.user.get_info(ent)
photo = userinfo.get("profile_photo")
working_hours = (
userinfo.get("business_work_hours").weekly_open
if userinfo.get("business_work_hours")
else 0
)
weekdays = [
self.strings["monday"],
self.strings["tuesday"],
self.strings["wednesday"],
self.strings["thursday"],
self.strings["friday"],
self.strings["saturday"],
self.strings["sunday"],
]
personal_channel = userinfo.get("personal_channel")
working_hours_output = []
if working_hours:
for item in working_hours:
day_index = item.start_minute // (24 * 60)
day = weekdays[day_index]
start = self.xdlib.parse.minutes_to_hhmm(item.start_minute)
end = self.xdlib.parse.minutes_to_hhmm(item.end_minute)
working_hours_output.append(f"<b>{day}: {start} - {end}</b>")
return await utils.answer(
message,
self.strings["userinfo"].format(
common_chats_count=userinfo.get("common_chats_count") or 0,
phone=userinfo.get("phone") or self.strings["no"],
common_chats=(
", ".join(
[
f"<a href='{(await self.xdlib.chat.get_info(channel)).get('link')}'>{channel.title}</a>"
for channel in userinfo.get("common_chats")
]
)
if userinfo.get("common_chats")
else self.strings["no"]
),
user_id=userinfo.get("id", 0),
first_name=userinfo.get("first_name") or self.strings["no"],
last_name=userinfo.get("last_name") or self.strings["no"],
about=userinfo.get("about") or self.strings["no"],
emoji_status=(
f"<emoji document_id={userinfo.get('emoji_status')}>🌙</emoji>"
if userinfo.get("emoji_status")
else self.strings["no"]
),
business_work_hours=", ".join(working_hours_output)
or self.strings["no"],
birthday=(
f"{userinfo.get('birthday').day or ''}."
f"{userinfo.get('birthday').month or ''}."
f"{userinfo.get('birthday').year or ''}"
if userinfo.get("birthday")
else self.strings["no"]
),
stargifts_count=userinfo.get("stargifts_count")
or self.strings["no"],
usernames=(
", ".join(
[
f"@{username}"
for username in userinfo.get("usernames")
]
)
if userinfo.get("usernames")
else self.strings["no"]
),
personal_channel=(
f"<a href='{(await self.xdlib.chat.get_info(personal_channel)).get('link')}'>"
f"{personal_channel.title}</a>"
if personal_channel
else self.strings["no"]
),
),
file=(
types.InputMediaPhoto(
types.InputPhoto(
photo.id, photo.access_hash, photo.file_reference
)
)
if photo
else None
),
)
except Exception as e:
logger.error(e)
return await utils.answer(message, self.strings["error"])
@loader.command(ru_doc="[-a] [-d] Управлять заявками на вступление")
@loader.tag("no_pm")
async def requests(self, message):
"""[-a] [-d] Manage join requests"""
opts = self.xdlib.parse.opts(utils.get_args(message))
approve_list = [x for x in str(opts.get("a", "")).split(",") if x]
dismiss_list = [x for x in str(opts.get("d", "")).split(",") if x]
sanitized_approve_list = [int(x) if x.isdigit() else x for x in approve_list]
sanitized_dismiss_list = [int(x) if x.isdigit() else x for x in approve_list]
all_list = sanitized_approve_list + sanitized_dismiss_list
all_targets = [
await self._client.get_entity(
int(ent.strip()) if ent.strip().isdigit() else ent.strip()
)
for ent in all_list
]
for approve in approve_list:
if approve.isdigit():
await self.xdlib.chat.join_request(message.chat, int(approve), True)
else:
await self.xdlib.chat.join_request(message.chat, approve, True)
for dismiss in dismiss_list:
if dismiss.isdigit():
await self.xdlib.chat.join_request(message.chat, int(dismiss), False)
else:
await self.xdlib.chat.join_request(message.chat, dismiss, False)
return await utils.answer(
message,
self.strings["requests_checked"].format(
entities=", ".join(
ent.first_name
or getattr(ent, "username", None)
or str(getattr(ent, "id", "unknown"))
for ent in all_targets
)
),
)
@loader.command(ru_doc="Получить все свои чаты/каналы")
async def owns(self, message):
"""Get all your chats/channels"""
owns = await self.xdlib.dialog.get_owns(self._client)
return await utils.answer(
message,
self.strings["owns"].format(
num=len(owns),
owns="\n".join(
[
f"<emoji document_id=5458833171846029357>✅</emoji> {own.title} [<code>{str(own.id).replace('-100', '')}</code>]"
for own in owns
]
),
),
)
@loader.command(ru_doc="[-r] [-u] [-f] - Выдать админку участнику")
@loader.tag("no_pm")
async def promote(self, message):
"""[-r] [-u] [-f] - Promote a participant"""
reply = await message.get_reply_message()
opts = self.xdlib.parse.opts(utils.get_args(message))
user = opts.get("u") or getattr(reply, "sender_id") or None
if not user:
return await utils.answer(message, self.strings["no_user"])
user = await self._client.get_entity(user)
rank = (opts.get("r")) or "XD Admin"
chat = await message.get_chat()
rights = await self.xdlib.chat.get_rights(message.chat, user)
if (
not chat.admin_rights
or not getattr(chat.admin_rights, "add_admins")
or (
getattr(rights.participant, "promoted_by", self.tg_id) != self.tg_id
and not getattr(chat, "creator", False)
)
):
return await utils.answer(message, self.strings["no_rights"])
full = opts.get("f")
if full:
my_rights = [
r for r, y in chat.admin_rights.to_dict().items() if y and r != "_"
]
perms = self.xdlib.admin_rights(0)
perms = perms.add(*my_rights)
await self.xdlib.admin.set_rights(chat, user, perms.to_int(), rank)
return await utils.answer(
message,
self.strings["promoted"].format(
id=user.id,
name=user.first_name
if hasattr(user, "first_name")
else user.title
if hasattr(user, "title")
else "None",
rights=self.strings["full_rights"],
),
)
mask = (
self.xdlib.admin_rights.to_mask(rights.participant.admin_rights)
if hasattr(rights.participant, "admin_rights")
else 0
)
await utils.answer(
message,
self.strings["promote"].format(
id=user.id,
name=user.first_name
if hasattr(user, "first_name")
else user.title
if hasattr(user, "title")
else "None",
rank=rank,
),
reply_markup=await self.build_markup(user.id, chat.id, mask, rank),
)
@loader.command(ru_doc="[-t] [-u] - Ограничить участника")
@loader.tag("no_pm")
async def restrict(self, message):
"""[-t] [-u] - Restrict a participant"""
reply = await message.get_reply_message()
opts = self.xdlib.parse.opts(utils.get_args(message))
user = opts.get("u") or getattr(reply, "sender_id") or None
if not user:
return await utils.answer(message, self.strings["no_user"])
user = await self._client.get_entity(user)
chat = await message.get_chat()
if not chat.admin_rights or not getattr(chat.admin_rights, "ban_users"):
return await utils.answer(message, self.strings["no_rights"])
duration = opts.get("t", None)
if duration:
duration = self.xdlib.format.time(self.xdlib.parse.time(duration))
rights = await self.xdlib.chat.get_rights(chat, user)
mask = (
self.xdlib.banned_rights.MAX_MASK
- self.xdlib.banned_rights.to_mask(rights.participant.banned_rights)
if hasattr(rights.participant, "banned_rights")
else 0
)
rank = "-"
await utils.answer(
message,
self.strings["restrict"].format(
id=user.id,
name=user.first_name
if hasattr(user, "first_name")
else user.title
if hasattr(user, "title")
else "None",
time=f" {duration}" if duration else self.strings["forever"],
),
reply_markup=await self.build_markup(
user.id,
chat.id,
mask,
rank,
mode="restrict",
duration=f" {duration}" if duration else None,
),
)
async def build_markup(
self,
user_id: int,
chat_id: int,
mask: int,
rank: str,
duration: typing.Optional[int] = None,
mode="admin",
):
rights_cls = (
self.xdlib.admin_rights if mode == "admin" else self.xdlib.banned_rights
)
rights_names = rights_cls.RIGHTS_LIST
rights = rights_cls(mask)
chat = await self._client.get_entity(chat_id)
markup = utils.chunks(
[
{
"text": f"{'🟢' if rights.has_index(idx) else '🔴'} {self.strings[name]}",
"callback": self._toggle_right,
"args": (user_id, chat_id, mask, idx, rank, mode, duration),
}
for idx, name in enumerate(rights_names)
if (
name != "until_date"
and not (
getattr(chat.default_banned_rights, name, True)
if mode != "admin"
else False
)
)
],
2,
)
markup.append(
[
{
"text": self.strings["apply"],
"callback": self._apply_rights,
"args": (user_id, chat_id, mask, rank, mode, duration),
}
]
)
markup.append([{"text": self.strings["close"], "action": "close"}])
return markup
async def _toggle_right(
self,
call,
user_id: int,
chat_id: int,
mask: int,
idx: int,
rank: str,
mode: str,
duration: str,
):
new_mask = mask ^ (1 << idx)
new_markup = await self.build_markup(
user_id, chat_id, new_mask, rank, mode=mode, duration=duration
)
user = await self._client.get_entity(user_id)
title = self.strings["promote"] if mode == "admin" else self.strings["restrict"]
await utils.answer(
call,
title.format(
id=user_id,
name=user.first_name
if hasattr(user, "first_name")
else user.title
if hasattr(user, "title")
else "None",
rank=rank,
time=f" {duration}" if duration else self.strings["forever"],
),
reply_markup=new_markup,
)
async def _apply_rights(
self,
call,
user_id: int,
chat_id: int,
mask: int,
rank: str,
mode: str,
duration: typing.Optional[str] = None,
):
user = await self._client.get_entity(user_id)
chat = await self._client.get_entity(chat_id)
if mode == "admin":
ok = await self.xdlib.admin.set_rights(chat, user, mask, rank)
rights_items = self.xdlib.admin_rights(mask).to_dict()
else:
ok = await self.xdlib.chat.set_restrictions(
chat, user, mask, duration=duration
)
rights_items = self.xdlib.banned_rights(mask).to_dict()
rights_list = [r for r, v in rights_items.items() if v]
if ok:
text = (
self.strings["promoted"]
if mode == "admin" and mask
else self.strings["demoted"]
if mode == "admin" and not mask
else self.strings["restricted"]
)
await utils.answer(
call,
text.format(
id=user_id,
name=user.first_name
if hasattr(user, "first_name")
else user.title
if hasattr(user, "title")
else "None",
rights=", ".join([self.strings[r] for r in rights_list])
if rights_list
else self.strings["no"],
duration=f" {duration}" if duration else self.strings["forever"],
time=f" {duration}" if duration else self.strings["forever"],
),
reply_markup=[[{"text": self.strings["close"], "action": "close"}]],
)
else:
await utils.answer(
call,
self.strings["error"],
reply_markup=[[{"text": self.strings["close"], "action": "close"}]],
)

515
coddrago/modules/dbmod.py Normal file
View File

@@ -0,0 +1,515 @@
# meta developer: @codrago_m
import html
from .. import loader, utils
class DBMod(loader.Module):
strings = {
"name": "DBMod",
"del_text": "<b>Database</b>\n\nSelect a key to view",
"deleted": "🗑 Key {key} deleted",
"deleted_all": "🗑 Deleted {count} keys",
"close_btn": "❌ Close",
"back_btn": "⬅ Back",
"del_btn": "🗑 Delete",
"del_all_btn": "💣 Delete all",
"not_found": "🔍 Key {key} not found",
"invalid_key": "⚠ Invalid key",
"page": "📄 Page {current}/{total}",
"module_not_found": "🔍 Module '{module}' not found in database",
"confirm_delete": "⚠ Are you sure you want to delete this?",
"view_path": "<b>Path: {path}</b>",
"root_path": "Root",
"value_display": "<b>Value:</b> <code>{value}</code>",
"yes_btn": "✅ Yes",
"no_btn": "❌ No",
"list_item_display": "<b>List item [{index}]</b>",
}
strings_ru = {
"del_text": "<b>База данных</b>\n\nВыберите ключ для просмотра",
"deleted": "🗑 Ключ {key} удален",
"deleted_all": "🗑 Удалено {count} ключей",
"close_btn": "❌ Закрыть",
"back_btn": "⬅ Назад",
"del_btn": "🗑 Удалить",
"del_all_btn": "💣 Удалить все",
"not_found": "🔍 Ключ {key} не найден",
"invalid_key": "⚠ Некорректный ключ",
"page": "📄 Страница {current}/{total}",
"module_not_found": "🔍 Модуль '{module}' не найден в базе данных",
"confirm_delete": "⚠ Вы уверены, что хотите удалить это?",
"view_path": "<b>Путь: {path}</b>",
"root_path": "Корень",
"value_display": "<b>Значение:</b> <code>{value}</code>",
"yes_btn": "✅ Да",
"no_btn": "❌ Нет",
"list_item_display": "<b>Элемент списка [{index}]</b>",
}
async def client_ready(self):
self.page_state = {}
def _make_path_text(self, key_path):
path = "/".join(map(str, key_path)) if key_path else self.strings["root_path"]
return self.strings["view_path"].format(path=path)
def _make_list_item_path_text(self, key_path, index):
"""Создает заголовок для элемента списка"""
if key_path:
path = "/".join(map(str, key_path)) + f"[{index}]"
else:
path = f"[{index}]"
return self.strings["list_item_display"].format(index=index)
async def show_menu(self, message, key_path=None, page=0):
if key_path is None:
key_path = []
self.page_state[tuple(key_path)] = page
current_data = self._db
for key in key_path:
if isinstance(current_data, (dict, list)):
if isinstance(current_data, dict) and key in current_data:
current_data = current_data[key]
elif (
isinstance(current_data, list)
and isinstance(key, int)
and 0 <= key < len(current_data)
):
current_data = current_data[key]
else:
await utils.answer(message, self.strings["invalid_key"])
return
else:
await utils.answer(message, self.strings["invalid_key"])
return
header = self._make_path_text(key_path)
if isinstance(current_data, (dict, list)) and current_data:
markup = self.generate_nested_markup(current_data, key_path, page)
await utils.answer(message, header, reply_markup=markup)
else:
text = f"{header}\n\n" + self.strings["value_display"].format(
value=html.escape(str(current_data))
)
markup = self.generate_value_markup(key_path, page)
await utils.answer(message, text, reply_markup=markup)
async def navigate_db(self, call, key_path=None, page=0):
if key_path is None:
key_path = []
self.page_state[tuple(key_path)] = page
current_data = self._db
for key in key_path:
if isinstance(current_data, (dict, list)):
if isinstance(current_data, dict) and key in current_data:
current_data = current_data[key]
elif (
isinstance(current_data, list)
and isinstance(key, int)
and 0 <= key < len(current_data)
):
current_data = current_data[key]
else:
await call.answer(self.strings["invalid_key"])
return
else:
await call.answer(self.strings["invalid_key"])
return
is_list_item = False
if key_path:
parent_data = self._db
for key in key_path[:-1]:
if isinstance(parent_data, (dict, list)):
if isinstance(parent_data, dict) and key in parent_data:
parent_data = parent_data[key]
elif (
isinstance(parent_data, list)
and isinstance(key, int)
and 0 <= key < len(parent_data)
):
parent_data = parent_data[key]
else:
break
if (
isinstance(parent_data, list)
and isinstance(key_path[-1], int)
and 0 <= key_path[-1] < len(parent_data)
):
is_list_item = True
if is_list_item:
header = self._make_list_item_path_text(key_path[:-1], key_path[-1])
text = f"{header}\n\n" + self.strings["value_display"].format(
value=html.escape(str(current_data))
)
await call.edit(
text, reply_markup=self.generate_list_item_markup(key_path, page)
)
elif isinstance(current_data, (dict, list)) and current_data:
header = self._make_path_text(key_path)
await call.edit(
header,
reply_markup=self.generate_nested_markup(current_data, key_path, page),
)
else:
header = self._make_path_text(key_path)
text = f"{header}\n\n" + self.strings["value_display"].format(
value=html.escape(str(current_data))
)
await call.edit(
text, reply_markup=self.generate_value_markup(key_path, page)
)
def generate_nested_markup(self, data, key_path, page=0):
if isinstance(data, list) and data:
return self.generate_list_markup(data, key_path, page)
items = list(data.items()) if isinstance(data, dict) else []
items_per_page = 9
total_pages = (len(items) + items_per_page - 1) // items_per_page
start_idx = page * items_per_page
end_idx = min(start_idx + items_per_page, len(items))
page_items = items[start_idx:end_idx]
markup = []
row = []
for i, (key, value) in enumerate(page_items):
if i % 3 == 0 and row:
markup.append(row)
row = []
row.append(
{
"text": f"{key}",
"callback": self.navigate_db,
"args": [key_path + [key], 0],
}
)
if row:
markup.append(row)
nav_buttons = []
if key_path:
parent_page = self.page_state.get(tuple(key_path[:-1]), 0)
nav_buttons.append(
{
"text": self.strings["back_btn"],
"callback": self.navigate_db,
"args": [key_path[:-1], parent_page],
}
)
if total_pages > 1:
if page > 0:
nav_buttons.append(
{
"text": "◀️",
"callback": self.navigate_db,
"args": [key_path, page - 1],
}
)
nav_buttons.append(
{
"text": self.strings["page"].format(
current=page + 1, total=total_pages
),
"callback": self.navigate_db,
"args": [key_path, page],
}
)
if page < total_pages - 1:
nav_buttons.append(
{
"text": "▶️",
"callback": self.navigate_db,
"args": [key_path, page + 1],
}
)
if nav_buttons:
markup.append(nav_buttons)
if key_path:
markup.append(
[
{
"text": self.strings["del_all_btn"],
"callback": self.confirm_delete_all,
"args": [key_path],
}
]
)
if not key_path:
markup.append([{"text": self.strings["close_btn"], "action": "close"}])
return markup
def generate_list_markup(self, data, key_path, page=0):
"""Генерирует разметку для списка, показывая элементы напрямую"""
items_per_page = 9
total_pages = (len(data) + items_per_page - 1) // items_per_page
start_idx = page * items_per_page
end_idx = min(start_idx + items_per_page, len(data))
page_items = list(enumerate(data[start_idx:end_idx], start_idx))
markup = []
row = []
for i, (index, value) in enumerate(page_items):
if i % 3 == 0 and row:
markup.append(row)
row = []
if isinstance(value, (dict, list)):
btn_text = f"[{index}]"
else:
value_str = str(value)
if len(value_str) > 10:
btn_text = f"{value_str[:10]}..."
else:
btn_text = value_str
row.append(
{
"text": btn_text,
"callback": self.navigate_db,
"args": [key_path + [index], 0],
}
)
if row:
markup.append(row)
nav_buttons = []
if key_path:
parent_page = self.page_state.get(tuple(key_path[:-1]), 0)
nav_buttons.append(
{
"text": self.strings["back_btn"],
"callback": self.navigate_db,
"args": [key_path[:-1], parent_page],
}
)
if total_pages > 1:
if page > 0:
nav_buttons.append(
{
"text": "◀️",
"callback": self.navigate_db,
"args": [key_path, page - 1],
}
)
nav_buttons.append(
{
"text": self.strings["page"].format(
current=page + 1, total=total_pages
),
"callback": self.navigate_db,
"args": [key_path, page],
}
)
if page < total_pages - 1:
nav_buttons.append(
{
"text": "▶️",
"callback": self.navigate_db,
"args": [key_path, page + 1],
}
)
if nav_buttons:
markup.append(nav_buttons)
if key_path:
markup.append(
[
{
"text": self.strings["del_all_btn"],
"callback": self.confirm_delete_all,
"args": [key_path],
}
]
)
return markup
def generate_list_item_markup(self, key_path, page=0):
"""Генерирует разметку для отдельного элемента списка"""
parent_page = self.page_state.get(tuple(key_path[:-1]), 0)
return [
[
{
"text": self.strings["del_btn"],
"callback": self.delete_key,
"args": [key_path],
}
],
[
{
"text": self.strings["back_btn"],
"callback": self.navigate_db,
"args": [key_path[:-1], parent_page],
}
],
]
def generate_value_markup(self, key_path, page=0):
parent_page = self.page_state.get(tuple(key_path[:-1]), 0)
return [
[
{
"text": self.strings["del_btn"],
"callback": self.delete_key,
"args": [key_path],
}
],
[
{
"text": self.strings["back_btn"],
"callback": self.navigate_db,
"args": [key_path[:-1], parent_page],
}
],
]
async def confirm_delete_all(self, call, key_path):
await call.edit(
self.strings["confirm_delete"],
reply_markup=[
[
{
"text": self.strings["yes_btn"],
"callback": self.delete_all_keys,
"args": [key_path],
}
],
[
{
"text": self.strings["no_btn"],
"callback": self.navigate_db,
"args": [
key_path,
self.page_state.get(tuple(key_path), 0),
],
}
],
],
)
async def delete_all_keys(self, call, key_path):
if not key_path:
count = len(self._db)
self._db.clear()
self._db.save()
await call.answer(self.strings["deleted_all"].format(count=count))
await self.navigate_db(call, [], self.page_state.get((), 0))
else:
current = self._db
for key in key_path[:-1]:
if isinstance(current, (dict, list)):
if isinstance(current, dict) and key in current:
current = current[key]
elif (
isinstance(current, list)
and isinstance(key, int)
and 0 <= key < len(current)
):
current = current[key]
else:
await call.answer(
self.strings["not_found"].format(key=key_path[-1])
)
return
if isinstance(current, (dict, list)) and key_path[-1] in current:
if isinstance(current[key_path[-1]], (dict, list)):
count = len(current[key_path[-1]])
else:
count = 1
del current[key_path[-1]]
self._db.save()
await call.answer(self.strings["deleted_all"].format(count=count))
await self.navigate_db(
call,
key_path[:-1],
self.page_state.get(tuple(key_path[:-1]), 0),
)
else:
await call.answer(self.strings["not_found"].format(key=key_path[-1]))
async def delete_key(self, call, key_path):
parent_page = self.page_state.get(tuple(key_path[:-1]), 0)
if len(key_path) == 1:
if key_path[0] in self._db:
del self._db[key_path[0]]
self._db.save()
await call.answer(self.strings["deleted"].format(key=key_path[0]))
await self.navigate_db(call, [], parent_page)
else:
await call.answer(self.strings["not_found"].format(key=key_path[0]))
else:
current = self._db
for key in key_path[:-1]:
if isinstance(current, (dict, list)):
if isinstance(current, dict) and key in current:
current = current[key]
elif (
isinstance(current, list)
and isinstance(key, int)
and 0 <= key < len(current)
):
current = current[key]
else:
await call.answer(
self.strings["not_found"].format(key=key_path[-1])
)
return
if isinstance(current, dict) and key_path[-1] in current:
deleted_value = current[key_path[-1]]
del current[key_path[-1]]
key_display = key_path[-1]
self._db.save()
await call.answer(self.strings["deleted"].format(key=key_display))
await self.navigate_db(call, key_path[:-1], parent_page)
elif (
isinstance(current, list)
and isinstance(key_path[-1], int)
and 0 <= key_path[-1] < len(current)
):
deleted_value = current.pop(key_path[-1])
key_display = f"[{key_path[-1]}] = {deleted_value}"
self._db.save()
await call.answer(self.strings["deleted"].format(key=key_display))
await self.navigate_db(call, key_path[:-1], parent_page)
else:
await call.answer(self.strings["not_found"].format(key=key_path[-1]))
def find_module_key(self, module_name):
module_name_lower = module_name.lower()
for key in self._db.keys():
if key.lower() == module_name_lower:
return key
return None
@loader.command(ru_doc="Просмотр базы данных")
async def mydb(self, message):
"""Viewing the database"""
args = utils.get_args_raw(message)
if args:
module_key = self.find_module_key(args)
if module_key:
await self.show_menu(
message, [module_key], self.page_state.get((module_key,), 0)
)
return
else:
await utils.answer(
message, self.strings["module_not_found"].format(module=args)
)
return
await self.show_menu(message, [], self.page_state.get((), 0))

View File

@@ -18,3 +18,8 @@ promoclaimer
passwordgen
send
lastfm
dbmod
chatmodule
stats
tagwatcher
hardspam

View File

@@ -0,0 +1,62 @@
# meta developer: @codrago_m
import asyncio
from .. import loader, utils
from herokutl.tl.types import InputDocument
from herokutl.errors.rpcerrorlist import MediaEmptyError
@loader.tds
class HardSpam(loader.Module):
strings = {
"name": "HardSpam",
"spam_help": "<b>Usage sample: {}hspam [-c|--clean] 23 text</b>",
}
strings_ru = {
"spam_help": "<b>Пример использования: {}hspam [-c|--clean] 23 text</b>",
}
async def send_msgs(self, c, chat_id, text):
msg = await c.send_message(chat_id, text)
return msg.id
async def send_medias(self, c, chat_id, document, text):
msg = await c.send_file(chat_id, document, caption=None if text is None else text)
return msg.id
@loader.command(
ru_doc="[-c|--clean] n text - Отправить n кол-во сообщений одновременно"
)
async def hspamcmd(self, message):
"""[-c|--clean] n text - Send n number of messages at the same time"""
args = utils.get_args(message)
r = await message.get_reply_message()
delete_all = False
if "--clean" in args:
delete_all = True
args.remove("--clean")
elif "-c" in args:
delete_all = True
args.remove("-c")
if not args[0].isdigit() or len(args) < 1:
return await utils.answer(
message,
self.strings["spam_help"].format(self.get_prefix()),
)
number = int(args[0])
text = " ".join(args[1:])
if r and r.media:
document = InputDocument(id=r.media.document.id, access_hash=r.media.document.access_hash, file_reference=r.media.document.file_reference)
tasks = [
self.send_medias(self._client, message.chat_id, document, None if text is None else text) for i in range(number)
]
else:
tasks = [
self.send_msgs(self._client, message.chat_id, text) for _ in range(number)
]
message_ids = await asyncio.gather(*tasks)
if delete_all:
await self._client.delete_messages(message.chat_id, message_ids)
return await message.delete()

View File

@@ -0,0 +1,764 @@
# This file is part of XDesai Mods.
# I made this library to share various utility functions across my modules.
# You can use this library in your own modules as well.
# P.S this library is still under development and may receive updates in the future.
# meta developer: @codrago_m
import logging
import re
import typing
from telethon.errors.rpcerrorlist import (
UserNotParticipantError,
HideRequesterMissingError,
)
from telethon.functions import messages, channels
from telethon import types
from .. import loader, utils
from ..types import SelfUnload
logger = logging.getLogger("XDLib")
class XDLib(loader.Library):
"""A library with various utility functions for codrago modules."""
developer = "@codrago_m"
strings = {
"name": "XDLib",
"desc": "A library with various utility functions for codrago modules.",
"request_join_reason": "Stay tuned for updates.",
}
async def init(self):
self.format = FormatUtils()
self.parse = ParseUtils()
self.messages = MessageUtils(self._client)
self.admin = AdminUtils(self._client, self)
self.chat = ChatUtils(self._client, self._db)
self.dialog = DialogUtils(self._client)
self.user = UserUtils(self._client, self._db)
self.admin_rights = AdminRights
self.banned_rights = BannedRights
def unload_lib(self, name: str):
instance = self.lookup(name)
if isinstance(instance, loader.Library):
self.allmodules.libraries.remove(instance)
logger.info(f"Unloaded library: {name}")
return True
return False
class UserUtils:
def __init__(self, client, db):
self._client = client
self._db = db
async def get_info(
self, user_id: typing.Union[str, int, types.PeerUser, types.User]
):
userfull = await self._client.get_fulluser(user_id)
full_user = userfull.full_user
user = userfull.users[0]
usernames = user.usernames or [user] or None
unames = []
if usernames:
for username in usernames:
unames.append(username.username)
personal_channel = (
await self._client.get_entity(full_user.personal_channel_id)
if full_user.personal_channel_id
else None
)
common = await self._client(
messages.GetCommonChatsRequest(user_id=user_id, max_id=0, limit=100)
)
return {
"common_chats_count": full_user.common_chats_count,
"common_chats": common.chats,
"id": user.id,
"personal_photo": full_user.personal_photo,
"business_work_hours": full_user.business_work_hours,
"business_intro": full_user.business_intro,
"birthday": full_user.birthday,
"personal_channel": personal_channel or None,
"stargifts_count": full_user.stargifts_count,
"first_name": user.first_name,
"last_name": user.last_name,
"usernames": unames,
"emoji_status": getattr(user.emoji_status, "document_id", None),
"color": user.color,
"blocked": full_user.blocked,
"about": full_user.about,
"profile_photo": full_user.profile_photo,
"phone": user.phone,
}
class ParseUtils:
def minutes_to_hhmm(self, m):
h = (m // 60) % 24
mm = m % 60
return f"{h:02d}:{mm:02d}"
def opts(self, args: list) -> typing.Dict[str, typing.Any]:
"""
Parses command-line style options from a list of arguments.
Supports sequential operations (+, -, *, /) for numeric values.
"""
options = {}
i = 0
def auto_cast(value: str):
if not value:
return True
low = value.lower()
if low in {"true", "yes", "on"}:
return True
if low in {"false", "no", "off"}:
return False
if re.fullmatch(r"-?\d+", value):
return int(value)
if re.fullmatch(r"-?\d+\.\d+", value):
return float(value)
return value
def apply_operations(base, ops: list[str]):
val = base
for op_str in ops:
m = re.fullmatch(r"([+*/])(\d+(\.\d+)?)", op_str)
if not m:
val = auto_cast(op_str)
continue
op, number, _ = m.groups()
number = float(number) if "." in number else int(number)
if op == "+":
val += number
elif op == "*":
val *= number
elif op == "/":
val /= number
return val
while i < len(args):
arg = args[i]
if "=" in arg:
key, value = arg.split("=", 1)
key = key.lstrip("-")
options[key] = auto_cast(value.strip("\"'"))
elif arg.startswith("-"):
key = arg.lstrip("-")
values = []
i += 1
while i < len(args) and not args[i].startswith("-"):
values.append(args[i].strip("\"'"))
i += 1
i -= 1
if key in options and isinstance(options[key], (int, float)):
options[key] = apply_operations(options[key], values)
else:
if values:
base = auto_cast(values[0])
options[key] = apply_operations(base, values[1:])
else:
options[key] = True
i += 1
return options
def bool(self, value: str) -> bool:
"""Parses a string into a boolean value."""
true_values = {"true", "yes", "1", "on"}
false_values = {"false", "no", "0", "off"}
low_value = value.lower()
if low_value in true_values:
return True
elif low_value in false_values:
return False
else:
raise ValueError(f"Cannot parse boolean from '{value}'")
def time(self, time_str: str) -> int:
"""Parses a time duration string into seconds."""
time_units = {
"s": 1,
"m": 60,
"h": 3600,
"d": 86400,
"w": 604800,
"y": 31536000,
}
total_seconds = 0
pattern = r"(\d+)([smhdwy])"
matches = re.findall(pattern, time_str)
for value, unit in matches:
total_seconds += int(value) * time_units[unit]
return total_seconds
def size(self, size_str: str) -> int:
"""Parses a size string into bytes."""
size_units = {
"b": 1,
"kb": 1024,
"mb": 1024**2,
"gb": 1024**3,
"tb": 1024**4,
}
pattern = r"(\d+)([bkmgt]b?)"
match = re.match(pattern, size_str.lower())
if match:
value, unit = match.groups()
return int(value) * size_units[unit]
return 0
def mentions(self, msg) -> typing.List[str]:
"""Extracts mentions from a given message."""
if msg.entities:
mentions = []
for entity in msg.entities:
if isinstance(entity, types.MessageEntityMention):
offset = entity.offset
length = entity.length
mentions.append(msg.message[offset : offset + length])
elif isinstance(entity, types.MessageEntityMentionName):
mentions.append(entity.user_id)
return mentions
return []
def urls(self, msg) -> typing.List[str]:
"""Extracts URLs from a given message."""
if msg.entities or msg.media:
urls = []
for entity in msg.entities:
if isinstance(entity, types.MessageEntityTextUrl):
urls.append(entity.url)
elif isinstance(entity, types.MessageEntityUrl):
offset = entity.offset
length = entity.length
urls.append(msg.message[offset : offset + length])
elif msg.media and hasattr(msg.media, "webpage"):
if msg.media.webpage.url:
urls.append(msg.media.webpage.url)
return urls
return []
class DialogUtils:
def __init__(self, client) -> None:
self._client = client
async def get_all(self, client):
dialogs = []
async for dialog in client.iter_dialogs():
dialogs.append(dialog)
return dialogs
async def get_chats(self, client):
return [
chat
for chat in await self.get_all(client)
if chat.is_group and chat.is_channel
]
async def get_pms(self, client):
return [pm for pm in await self.get_all(client) if pm.is_private]
async def get_channels(self, client):
return [
channel
for channel in await self.get_all(client)
if channel.is_channel and not channel.is_group
]
async def get_owns(self, client):
return [
ent
for ent in await self.get_all(client)
if hasattr(ent.entity, "creator") and ent.entity.creator
]
class MessageUtils:
def __init__(self, client):
self._client = client
async def delete_messages(self, msg):
"""Deletes multiple messages based on a specific pattern."""
reply = await msg.get_reply_message()
pattern = r"([ab])(\d+)"
matches = re.findall(pattern, utils.get_args_raw(msg))
ids_to_delete = [msg.id]
if reply:
ids_to_delete.append(reply.id)
for direction, count_str in matches:
count = int(count_str)
if direction == "a": # after
if reply:
async for m in self._client.iter_messages(
msg.chat_id, min_id=reply.id, limit=count, reverse=True
):
ids_to_delete.append(m.id)
elif direction == "b": # before
async for m in self._client.iter_messages(
msg.chat_id, max_id=(reply if reply else msg).id, limit=count
):
ids_to_delete.append(m.id)
await self._client.delete_messages(msg.chat_id, message_ids=ids_to_delete)
async def get_sender(self, message):
if message.out:
return await self._client.get_me()
if message.is_private:
return message.peer_id
if message.is_group and message.is_channel:
return message.sender or message.chat
class ChatUtils:
def __init__(self, client, db) -> None:
self._client = client
self._db = db
async def set_restrictions(
self, chat, user, mask: int, duration: int = None
) -> bool:
"""
Sets chat restrictions (mute/ban permissions) for a user based on a mask.
:param chat: Chat entity
:param user: User entity
:param mask: Bitmask of BannedRights
:param duration: Ban duration in seconds (None = forever)
"""
try:
rights = BannedRights(mask)
rights_dict = rights.to_dict()
rights_dict["until_date"] = (
None if duration is None else utils.timestamp() + duration
)
new_banned_rights = types.ChatBannedRights(**rights_dict)
await self._client(
channels.EditBannedRequest(
channel=chat,
participant=user,
banned_rights=new_banned_rights,
)
)
return True
except Exception:
logger.error(
f"Failed to set restrictions with mask {mask} for user {user.id} in chat {chat}",
exc_info=True,
)
return False
async def get_admin_logs(self, chat, limit: int = 5, **kwargs):
logs = []
for log_event in await self._client.get_admin_log(chat, limit=limit, **kwargs):
logs.append(log_event)
return logs
async def get_user_messages(self, chat, user_id):
msgs = []
async for msg in self._client.iter_messages(chat, from_user=user_id):
msgs.append(msg)
return msgs
async def join_request(self, chat, user_id, approved):
try:
await self._client(
messages.HideChatJoinRequestRequest(
peer=chat, user_id=user_id, approved=approved
)
)
except HideRequesterMissingError:
logger.error("Request not found")
async def join_requests(self, chat, approved):
try:
await self._client(
messages.HideAllChatJoinRequestsRequest(
peer=chat,
approved=approved,
)
)
except HideRequesterMissingError:
logger.error("Request not found")
async def get_members(self, chat):
try:
members = await self._client.get_participants(chat)
if members:
return members
return None
except Exception:
logger.error(f"Couldn't get members of the chat {chat}")
return None
async def get_deleted(self, chat):
try:
members = await self._client.get_participants(chat)
deleted = [member for member in members if getattr(member, "deleted")]
if deleted:
return deleted
return None
except Exception:
logger.error(f"Couldn't get members of the chat {chat}")
return None
async def get_bots(self, chat):
try:
bots = await self._client.get_participants(
chat, filter=types.ChannelParticipantsBots()
)
if bots:
return bots
return None
except Exception:
logger.error(f"Couldn't get bots from the chat {chat}")
return None
async def get_admins(self, chat, only_users: bool = False):
try:
admins = await self._client.get_participants(
chat, filter=types.ChannelParticipantsAdmins()
)
users = [
user
for user in admins
if user
and not getattr(user, "bot")
and not isinstance(
getattr(user, "participant"), types.ChannelParticipantCreator
)
]
if only_users:
return users
return admins
except Exception:
logger.error(f"Couldn't get admins from the chat {chat}")
return None
async def get_creator(self, chat):
try:
admins = await self._client.get_participants(
chat, filter=types.ChannelParticipantsAdmins()
)
if not admins:
return None
for admin in admins:
if hasattr(admin, "participant") and isinstance(
getattr(admin, "participant"), types.ChannelParticipantCreator
):
return admin
return None
except Exception:
logger.error(f"Couldn't get the creator from the chat {chat}")
return None
async def is_member(self, chat, user) -> bool:
"""Checks if a user is a member of a chat."""
try:
perms = await self._client.get_perms_cached(chat, user)
return True if perms else False
except UserNotParticipantError:
return False
except Exception:
logger.error(
f"Failed to check membership for user {user} in chat {chat.title}",
exc_info=True,
)
return False
async def get_rights(self, chat, user):
"""Checks if a user is a member of a chat."""
try:
perms = await self._client.get_perms_cached(chat, user)
return perms
except UserNotParticipantError:
return None
except Exception:
logger.error(
f"Failed to check membership for user {user} in chat {chat.title}",
exc_info=True,
)
return None
async def invite_user(self, chat, user):
"""Invites a user to a chat."""
try:
await self._client(
channels.InviteToChannelRequest(channel=chat, users=[user])
)
return True
except Exception:
logger.error(
f"Failed to invite user {user} to chat {chat.title}", exc_info=True
)
return False
async def get_info(self, chat) -> dict:
try:
chat_full = await self._client.get_fullchannel(chat)
full_chat = chat_full.full_chat
chat = chat_full.chats[0]
return {
"id": full_chat.id or 0,
"about": full_chat.about or "",
"chat_photo": full_chat.chat_photo,
"admins_count": full_chat.admins_count or 0,
"online_count": full_chat.online_count or 0,
"participants_count": full_chat.participants_count or 0,
"kicked_count": full_chat.kicked_count,
"slowmode_seconds": full_chat.slowmode_seconds or 0,
"call": full_chat.call or None,
"title": chat.title or "",
"ttl_period": full_chat.ttl_period or 0,
"available_reactions": full_chat.available_reactions or None,
"requests_pending": full_chat.requests_pending or 0,
"recent_requesters": full_chat.recent_requesters or [],
"is_forum": getattr(chat, "forum"),
"linked_chat_id": full_chat.linked_chat_id or 0,
"antispam": full_chat.antispam or False,
"participants_hidden": full_chat.participants_hidden or False,
"link": (
f"https://t.me/{chat.username}"
if chat.username
else (
full_chat.exported_invite.link
if full_chat.exported_invite
else ""
)
),
"is_channel": chat.broadcast or False,
"is_group": chat.megagroup or False,
}
except Exception:
logger.error("Failed to get the chat info")
return {}
async def invite_bot(self, client, chat) -> bool:
"""Invites an inline bot to a chat."""
try:
await self._client(
channels.InviteToChannelRequest(
chat,
[client.loader.inline.bot_username or client.loader.inline.bot_id],
)
)
except Exception:
logger.error("Failed to invite inline bot to chat", exc_info=True)
return False
rights = AdminRights.all()
rights.remove("anonymous")
admin = AdminUtils(self._client, self._db)
await admin.set_rights(
chat,
client.loader.inline.bot_username or client.loader.inline.bot_id,
rights.to_int(),
rank="XD Bot",
)
return True
class AdminUtils:
def __init__(self, client, lib) -> None:
self._client = client
self._lib = lib
async def set_role(self, chat, user, role_name, rank="XD Admin") -> bool:
rights_obj = self._lib.roles.get_role_perms(role_name)
if rights_obj is None:
return False
return await self.set_rights(chat, user, rights_obj.to_int(), rank)
async def set_rights(self, chat, user, mask: int, rank: str = "XD Admin") -> bool:
"""Sets admin rights for a user in a chat based on a mask."""
try:
rights = AdminRights(mask)
new_admin_rights = rights.to_chat_rights()
await self._client(
channels.EditAdminRequest(
chat,
user,
new_admin_rights,
rank=rank,
)
)
return True
except Exception:
logger.error(
f"Failed to set rights with mask {mask} for user {user.id} in chat {chat.title}",
exc_info=True,
)
return False
class FormatUtils:
def bytes(self, size: int) -> str:
"""Formats a size in bytes into a human-readable string."""
if size < 1024:
if size == 1:
return f"{size} byte"
return f"{size} bytes"
elif size < 1024**2:
return f"{size / 1024:.2f} KB"
elif size < 1024**3:
return f"{size / 1024**2:.2f} MB"
elif size < 1024**4:
return f"{size / 1024**3:.2f} GB"
else:
return f"{size / 1024**4:.2f} TB"
def time(self, seconds: int) -> str:
"""Formats a time duration in seconds into a human-readable string."""
intervals = (
("years", 31536000),
("months", 2592000),
("weeks", 604800),
("days", 86400),
("hours", 3600),
("minutes", 60),
("seconds", 1),
)
result = []
for name, count in intervals:
value = seconds // count
if value:
seconds -= value * count
if value == 1:
name = name.rstrip("s")
result.append(f"{value} {name}")
return ", ".join(result) if result else "0 seconds"
class Rights:
RIGHTS_LIST: typing.List = []
def __init__(self, mask: int = 0):
self.mask = mask & self.MAX_MASK
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.RIGHTS = {name: 1 << i for i, name in enumerate(cls.RIGHTS_LIST)}
cls.MAX_MASK = (1 << len(cls.RIGHTS_LIST)) - 1
def add(self, *right_names: str) -> None:
for name in right_names:
if name in self.RIGHTS:
self.mask |= self.RIGHTS[name]
else:
return None
return self
def remove(self, *right_names: str) -> None:
for name in right_names:
if name in self.RIGHTS:
self.mask &= ~self.RIGHTS[name]
else:
return None
return self
def has(self, right_name: str) -> bool:
return bool(self.mask & self.RIGHTS.get(right_name, 0))
def add_index(self, idx: int) -> None:
if 0 <= idx < len(self.RIGHTS_LIST):
self.mask |= 1 << idx
return self
@classmethod
def to_mask(self, chat_rights):
mask = 0
for right, rmask in self.RIGHTS.items():
if (
getattr(chat_rights, right)
and isinstance(chat_rights, types.ChatAdminRights)
) or (
not getattr(chat_rights, right)
and isinstance(chat_rights, types.ChatBannedRights)
):
mask |= rmask
return mask
def remove_index(self, idx: int) -> None:
if 0 <= idx < len(self.RIGHTS_LIST):
self.mask &= ~(1 << idx)
return self
def has_index(self, idx: int) -> bool:
if 0 <= idx < len(self.RIGHTS_LIST):
return bool(self.mask & (1 << idx))
return False
def to_dict(self) -> dict[str, bool]:
return {name: self.has(name) for name in self.RIGHTS_LIST}
def to_int(self) -> int:
return self.mask
def to_chat_rights(self):
return (
types.ChatBannedRights(**self.to_dict())
if self.__class__.__name__ == "BannedRights"
else types.ChatAdminRights(**self.to_dict())
)
@classmethod
def stringify(cls) -> str:
max_len = max(len(name) for name in cls.RIGHTS_LIST)
lines = []
for name in cls.RIGHTS_LIST:
mask = cls.RIGHTS[name]
lines.append(f"{name.ljust(max_len)}{mask}")
return "\n".join(lines)
@classmethod
def list_rights(cls) -> list[tuple[int, str]]:
return [(i, name) for i, name in enumerate(cls.RIGHTS_LIST)]
@classmethod
def from_int(cls, mask: int):
return cls(mask)
@classmethod
def all(cls):
return cls(cls.MAX_MASK)
@classmethod
def none(cls):
return cls(0)
class BannedRights(Rights):
RIGHTS_LIST = [
x for x in types.ChatBannedRights(until_date=None).to_dict().keys() if x != "_"
]
class AdminRights(Rights):
RIGHTS_LIST = [x for x in types.ChatAdminRights().to_dict().keys() if x != "_"]

120
coddrago/modules/stats.py Normal file
View File

@@ -0,0 +1,120 @@
# meta developer: @codrago_m
from .. import loader, utils
from telethon.tl.functions.contacts import GetBlockedRequest
@loader.tds
class Stats(loader.Module):
"""Показывает статистику твоего аккаунта"""
strings = {
"name": "Stats",
"stats": """
<emoji document_id=5774022692642492953>✅</emoji><b> Account Statistics</b>
</b><emoji document_id=5208454037531280484>💜</emoji><b> Total chats: </b><code>{all_chats}</code><b>
</b><emoji document_id=6035084557378654059>👤</emoji><b> Private chats: </b><code>{users}</code><b>
</b><emoji document_id=6030400221232501136>🤖</emoji><b> Bots: </b><code>{bots}</code><b>
</b><emoji document_id=6032609071373226027>👥</emoji><b> Groups: </b><code>{groups}</code><b>
</b><emoji document_id=5870886806601338791>👥</emoji><b> Channels: </b><code>{channels}</code><b>
</b><emoji document_id=5870563425628721113>📨</emoji><b> Archived chats: </b><code>{archived}</code><b>
</b><emoji document_id=5870948572526022116>✋</emoji><b> Total blocked: </b><code>{blocked}</code>
<b>Ͱ</b><emoji document_id=6035084557378654059>👤</emoji><b> Users: </b><code>{blocked_users}</code>
<b>Ͱ</b><emoji document_id=6030400221232501136>🤖</emoji><b> Bots: </b><code>{blocked_bots}</code>""",
"loading_stats": "<b><emoji document_id=5309893756244206277>🫥</emoji> Loading statistics...</b>",
}
strings_ru = {
"name": "Stats",
"stats": """
<emoji document_id=5774022692642492953>✅</emoji><b> Статистика аккаунта
</b><emoji document_id=5208454037531280484>💜</emoji><b> Всего чатов: </b><code>{all_chats}</code><b>
</b><emoji document_id=6035084557378654059>👤</emoji><b> Личных чатов: </b><code>{users}</code><b>
</b><emoji document_id=6030400221232501136>🤖</emoji><b> Ботов: </b><code>{bots}</code><b>
</b><emoji document_id=6032609071373226027>👥</emoji><b> Групп: </b><code>{groups}</code><b>
</b><emoji document_id=5870886806601338791>👥</emoji><b> Каналов: </b><code>{channels}</code><b>
</b><emoji document_id=5870563425628721113>📨</emoji><b> Архивированных чатов: </b><code>{archived}</code><b>
</b><emoji document_id=5870948572526022116>✋</emoji><b> Всего заблокированных: </b><code>{blocked}</code>
<b>Ͱ</b><emoji document_id=6035084557378654059>👤</emoji><b> Пользователи: </b><code>{blocked_users}</code>
<b>Ͱ</b><emoji document_id=6030400221232501136>🤖</emoji><b> Боты: </b><code>{blocked_bots}</code>""",
"loading_stats": "<b><emoji document_id=5309893756244206277>🫥</emoji> Загрузка статистики...</b>",
}
async def client_ready(self, client, db):
self.db = db
self._client = client
@loader.command()
async def stats(self, message):
"""Получить статистику"""
await utils.answer(message, self.strings["loading_stats"])
users = 0
bots = 0
groups = 0
channels = 0
all_chats = 0
archived = 0
blocked_bots = 0
blocked_users = 0
limit = 100
offset = 0
total_blocked = 0
while True:
blocked_chats = await self._client(
GetBlockedRequest(offset=offset, limit=limit)
)
for user in blocked_chats.users:
if user.bot:
blocked_bots += 1
else:
blocked_users += 1
blocked = len(blocked_chats.users)
total_blocked += blocked
if blocked < limit:
break
offset += limit
async for dialog in self._client.iter_dialogs():
if getattr(dialog, "archived", False):
archived += 1
if dialog.is_user:
if getattr(dialog.entity, "bot", False):
bots += 1
all_chats += 1
else:
users += 1
all_chats += 1
elif getattr(dialog, "is_group", False):
groups += 1
all_chats += 1
elif dialog.is_channel:
if getattr(dialog.entity, "megagroup", False) or getattr(
dialog.entity, "gigagroup", False
):
groups += 1
all_chats += 1
elif getattr(dialog.entity, "broadcast", False):
channels += 1
all_chats += 1
await utils.answer(
message,
self.strings["stats"].format(
users=users,
bots=bots,
channels=channels,
groups=groups,
all_chats=all_chats,
blocked=total_blocked,
archived=archived,
blocked_users=blocked_users,
blocked_bots=blocked_bots,
),
)

View File

@@ -0,0 +1,245 @@
# meta developer: @codrago_m
import logging
from .. import utils, loader, main
from telethon.tl.functions.messages import MarkDialogUnreadRequest
logger = logging.getLogger("TagWatcher")
@loader.tds
class TagWatcher(loader.Module):
"""Informs when you are tagged in chats and automatically reads pm."""
strings = {
"name": "TagWatcher",
"_cfg_doc_blacklist_chats": "List of chat IDs to ignore notifications from.",
"_cfg_doc_blacklist_users": "List of user IDs to ignore notifications from.",
"_cfg_doc_enabled": "Enable/Disable the module.",
"_cfg_doc_ignore_bots": "Ignore messages from bots.",
"_cfg_doc_pm_autoread": "Automatically mark private messages as read when you receive a message.",
"_cfg_doc_ignore_chats": "List of chat IDs to ignore tags from.",
"_cfg_doc_ignore_users": "List of user IDs to ignore tags from.",
"_cfg_doc_pm_mark_unread": "Mark the PM as unread after automatically reading it.",
"_cfg_doc_custom_notif_text": "Custom notification text. Available variables: {title}, {chat_id}, {name}, {user_id}, {msg_content}, {reply_content}, {link}.",
"enabled": "<emoji document_id=5208808350858364013>✅</emoji> <b>TagWatcher is enabled.</b>",
"disabled": "<emoji document_id=5219776129669276751>❌</emoji> <b>TagWatcher is disabled.</b>",
"mentioned": "<b>You were mentioned in <code>{title}</code> [ <code>{chat_id}</code> ] by <a href='tg://user?id={user_id}'>{name}</a> [ <code>{user_id}</code> ]:</b>\nReplying to message:\n{reply_content}\n<b>Message content:</b> {msg_content}\n\n<a href='{link}'>Go to message</a>",
"reply_content": "<b>Reply content:</b> {reply_content}",
"no_message_content": "❓ Empty message text",
"msg_link_btn": "<a href='{msg_url}'>Go to message</a>",
"first_msg": "<b>This is the channel where you will receive notifications when someone mentions you in chats.</b>\n\nYou can disable notifications using the <code>{prefix}tagwatcher</code> (<code>{prefix}tw</code>) command.",
"request_join_reason": "Stay tuned for updates.",
}
strings_ru = {
"_cls_doc": "Сообщает когда вас отмечают в чатах.",
"_cfg_doc_blacklist_chats": "Список ID чатов, от которых уведомления не будут приходить.",
"_cfg_doc_blacklist_users": "Список ID пользователей, от которых уведомления не будут приходить.",
"_cfg_doc_enabled": "Включить/Выключить модуль.",
"_cfg_doc_ignore_bots": "Игнорировать сообщения от ботов.",
"_cfg_doc_pm_autoread": "Автоматически отмечать личные сообщения как прочтённые при получении сообщения.",
"_cfg_doc_ignore_chats": "Список ID чатов, от которых не будут срабатывать упоминания.",
"_cfg_doc_ignore_users": "Список ID пользователей, от которых не будут срабатывать упоминания.",
"_cfg_doc_pm_mark_unread": "Помечать ЛС как непрочитанные после автоматического прочтения.",
"_cfg_doc_custom_notif_text": "Пользовательский текст уведомления. Доступные переменные: {title}, {chat_id}, {name}, {user_id}, {msg_content}, {reply_content}, {link}.",
"enabled": "<emoji document_id=5208808350858364013>✅</emoji> <b>TagWatcher включен.</b>",
"disabled": "<emoji document_id=5219776129669276751>❌</emoji> <b>TagWatcher выключен.</b>",
"mentioned": "<b>Вас отметил(а) <a href='tg://user?id={user_id}'>{name}</a> [ <code>{user_id}</code> ] в <code>{title}</code> [ <code>{chat_id}</code> ]:</b>\nВ ответ на сообщение:\n{reply_content}\n<b>Текст сообщения:</b> {msg_content}\n\n<a href='{link}'>Перейти к сообщению</a>",
"reply_content": "<b>Ответ на сообщение:</b> {reply_content}",
"no_message_content": "❓ Пустой текст сообщения",
"msg_link_btn": "<a href='{msg_url}'>Перейти к сообщению</a>",
"first_msg": "<b>Это канал, в который вы будете получать уведомления, когда кто-то упомянет вас в чатах.</b>\n\nВы можете отключить уведомления с помощью команды <code>{prefix}tagwatcher</code> (<code>{prefix}tw</code>).",
"request_join_reason": "Следите за обновлениями модулей.",
}
def __init__(self) -> None:
self.config = loader.ModuleConfig(
loader.ConfigValue(
"custom_notif_text",
None,
doc=lambda: self.strings["_cfg_doc_custom_notif_text"],
validator=loader.validators.Union(
loader.validators.String(), loader.validators.NoneType()
),
),
loader.ConfigValue(
"ignore_bots",
True,
doc=lambda: self.strings["_cfg_doc_ignore_bots"],
validator=loader.validators.Boolean(),
),
loader.ConfigValue(
"ignore_chats",
[],
doc=lambda: self.strings["_cfg_doc_ignore_chats"],
validator=loader.validators.Series(
validator=loader.validators.TelegramID()
),
),
loader.ConfigValue(
"blacklist_chats",
[],
doc=lambda: self.strings["_cfg_doc_blacklist_chats"],
validator=loader.validators.Series(
validator=loader.validators.TelegramID()
),
),
loader.ConfigValue(
"ignore_users",
[],
doc=lambda: self.strings["_cfg_doc_ignore_users"],
validator=loader.validators.Series(
validator=loader.validators.TelegramID()
),
),
loader.ConfigValue(
"blacklist_users",
[],
doc=lambda: self.strings["_cfg_doc_blacklist_users"],
validator=loader.validators.Series(
validator=loader.validators.TelegramID()
),
),
loader.ConfigValue(
"pm_autoread",
False,
doc=lambda: self.strings["_cfg_doc_pm_autoread"],
validator=loader.validators.Boolean(),
),
loader.ConfigValue(
"pm_mark_unread",
False,
doc=lambda: self.strings["_cfg_doc_pm_mark_unread"],
validator=loader.validators.Boolean(),
),
)
async def client_ready(self):
await self.request_join("@xdesai_modules", self.strings["request_join_reason"])
self.xdlib = await self.import_lib(
"https://raw.githubusercontent.com/xdesai96/modules/refs/heads/main/libs/xdlib.py",
suspend_on_error=True,
)
self.asset_channel = self._db.get("legacy.forums", "channel_id", 0)
self._notif_topic = await utils.asset_forum_topic(
self._client,
self._db,
self.asset_channel,
"TagWatcher",
description="Here will be notifications about mentions in chats.",
icon_emoji_id=5409025823388741707,
)
async def render_text(self, m):
if self.config["custom_notif_text"]:
text = self.config["custom_notif_text"]
else:
text = self.strings["mentioned"]
chat = await m.get_chat()
sender = await self.xdlib.messages.get_sender(m)
title = (
utils.escape_html(chat.title)
if hasattr(chat, "title")
else utils.escape_html(
sender.first_name if hasattr(sender, "first_name") else sender.title
)
)
name = (
utils.escape_html(
sender.first_name if hasattr(sender, "first_name") else sender.title
)
if sender
else "Unknown"
)
msg_content = (
utils.escape_html(m.message)
if m.message
else self.strings["no_message_content"]
)
id = sender.id if sender else 0
reply_content = ""
if m.is_reply:
reply = await m.get_reply_message()
if reply:
reply_content = (
utils.escape_html(reply.message)
if reply.message
else self.strings["no_message_content"]
)
return text.format(
title=title,
name=name,
chat_id=chat.id,
user_id=id,
msg_content=msg_content,
reply_content=reply_content,
link=await m.link,
)
@loader.command(
ru_doc="Вкл/выкл TagWatcher.",
alias="tw",
)
async def tagwatcher(self, m):
"""Enable/Disable TagWatcher."""
try:
disabled = self._db.pointer(main.__name__, "disabled_watchers", {})
if self.strings["name"] in list(disabled.keys()):
del disabled[self.strings["name"]]
await utils.answer(m, self.strings["enabled"])
else:
disabled[self.strings["name"]] = ["*"]
await utils.answer(m, self.strings["disabled"])
except Exception as e:
logger.error(e)
@loader.watcher("only_pm")
async def pm_reader(self, m):
"""To automatically mark private messages as read."""
if self.config["pm_autoread"]:
chat = await m.get_chat()
if chat.id in self.config["ignore_users"] or chat.bot:
return
try:
await self._client.send_read_acknowledge(
chat.id, m, clear_mentions=True
)
if self.config["pm_mark_unread"]:
peer = await self._client.get_input_entity(chat.id)
await self._client(
MarkDialogUnreadRequest(peer, True if not m.out else False)
)
except Exception as e:
logger.error(e)
@loader.watcher("mention", "no_pm")
async def inform(self, m):
"""To inform when you are mentioned in chats."""
try:
sender = await utils.get_user(m)
if (
utils.get_chat_id(m) in self.config["ignore_chats"]
or sender.id in self.config["ignore_users"]
):
return
await self._client.send_read_acknowledge(m.chat_id, m, clear_mentions=True)
if (
not sender
or utils.get_chat_id(m) in self.config["blacklist_chats"]
or utils.get_chat_id(m) == self._notif_topic.id
or sender.id in self.config["blacklist_users"]
):
return
if self.config["ignore_bots"]:
if hasattr(sender, "bot"):
if sender.bot:
return
await self.inline.bot.send_message(
int(f"-100{self.asset_channel}"),
await self.render_text(m),
disable_web_page_preview=True,
message_thread_id=self._notif_topic.id,
)
except Exception as e:
logger.error(e)

View File

@@ -0,0 +1,197 @@
en:
my_id: "<emoji document_id=5361912768045792571>👑</emoji><b> My ID: </b><code>{id}</code>"
chat_id: "<emoji document_id=5886436057091673541>💬</emoji> <b>Chat ID:</b> <code>{id}</code>"
user_id: "<emoji document_id=6035084557378654059>👤</emoji> <b>User's ID:</b> <code>{id}</code>"
user_not_participant: "<emoji document_id=5019523782004441717>❌</emoji> <b>User is not in this group.</b>"
admin_rights: "<blockquote expandable><emoji document_id=6023985764885338464>📜</emoji> {name} <b>Rights in this chat:\n\n{rights}</b>\n\n<emoji document_id=5287734473775918473>🔼</emoji><b> Promoted by: {promoter_name}</b> [{promoter_id}]</blockquote>"
not_an_admin: "<emoji document_id=5019523782004441717>❌</emoji><b> {user} is not an admin.</b>"
no_rights: "<emoji document_id=5019523782004441717>❌</emoji> <b>I don't have enough rights :(</b>"
no_user: "<emoji document_id=5019523782004441717>❌</emoji> <b>User not found.</b>"
change_info: "Change Info"
delete_messages: "Delete messages"
other: "Other"
ban_users: "Ban users"
invite_users: "Invite Users"
pin_messages: "Pin Messages"
add_admins: "Add Admins"
manage_call: "Manage Call"
post_stories: "Post Stories"
edit_stories: "Edit Stories"
delete_stories: "Delete Stories"
anonymous: "Anonymous"
manage_topics: "Manage Topics"
post_messages: "Post messages"
edit_messages: "Edit messages"
until_date: "Until: {until_date}"
view_messages: "View messages"
send_messages: "Send messages"
send_media: "Send media"
send_stickers: "Send stickers"
send_gifs: "Send GIFs"
send_games: "Send games"
send_inline: "Use inline bots"
embed_links: "Embed links"
send_polls: "Send polls"
send_photos: "Send photos"
send_videos: "Send videos"
send_roundvideos: "Send round videos"
send_audios: "Send audio"
send_voices: "Send voice messages"
send_docs: "Send documents"
send_plain: "Send plain text"
invalid_args: "<emoji document_id=5219776129669276751>❌</emoji> <b>Invalid args.</b>"
error: "<emoji document_id=5458497936763676259>😖</emoji><b> Something went wrong. Check the logs.</b>"
successful_delete: "<emoji document_id=5409029658794537988>✅</emoji> <b>Entity successfully deleted</b>"
no_deleted_accounts: "<emoji document_id=5238020759900668600>😶‍🌫️</emoji> <b>No deleted accounts found here</b>"
kicked_deleted_accounts: "<emoji document_id=5408832111773757273>🗑</emoji> <b>Removed deleted accounts from the chat</b>"
admins_in_chat: "<emoji document_id=5276229330131772747>👑</emoji> <b>Admins in <code>{title}</code> ({count}):</b>\n"
no_admins_in_chat: "<emoji document_id=5458610095539645297>✖️</emoji> <b>No admins in this chat.</b>"
bot_list: "<blockquote expandable><emoji document_id=5355051922862653659>🤖</emoji><b> Bots ({count}):</b>\n{bots}</blockquote>"
no_bots_in_chat: "<emoji document_id=5458610095539645297>✖️</emoji> <b>No bots in this chat.</b>"
user_list: "<blockquote expandable><emoji document_id=5408846628763217930>👤</emoji><b> Users ({count}):</b>\n{users}</blockquote>"
no_user_in_chat: "<emoji document_id=5458610095539645297>✖️</emoji> <b>No users in this chat.</b>"
user_is_banned: "<emoji document_id=5348402067947929537>🚫</emoji> <b>{name} [<code>{id}</code>] has been banned for{time_info}.</b>"
user_is_unbanned: "<emoji document_id=5355277430120523169>👋</emoji> <b>{name} [<code>{id}</code>] has been unbanned.</b>"
user_is_kicked: "<emoji document_id=5983033346207256798>🚪</emoji> <b><code>{name}</code> [<code>{id}</code>] has been kicked.</b>"
user_is_muted: "<emoji document_id=5409380965644514142>🔕</emoji> <b>{name} [<code>{id}</code>] has been muted for{time_info}.</b>"
reason: "<i>Reason: {reason}</i>"
forever: "ever"
user_is_unmuted: "<emoji document_id=5409331062419502443>🔉</emoji> <b>{name} [<code>{id}</code>] has been unmuted.</b>"
title_changed: "<b>The {type_of} title was successfully changed from <code>{old_title}</code> to <code>{new_title}</code>.</b>"
channel_created: "<emoji document_id=6296367896398399651>✅</emoji> <b>The channel <code>{title}</code> is created.\n</b><emoji document_id=5237918475254526196>🔗</emoji><b> Invite link: {link}</b>"
group_created: "<emoji document_id=6296367896398399651>✅</emoji> <b>The group <code>{title}</code> is created.\n</b><emoji document_id=5237918475254526196>🔗</emoji><b> Invite link: {link}</b>"
user_blocked: "<emoji document_id=5019523782004441717>❌</emoji> <b><a href='tg://user?id={user_id}'>{user}</a> is blocked.</b>"
user_privacy_restricted: "<emoji document_id=5019523782004441717>❌</emoji> <b><a href='tg://user?id={user_id}'>{user}</a> privacy settings restrict this action.</b>"
user_not_mutual_contact: "<emoji document_id=5019523782004441717>❌</emoji> <b><a href='tg://user?id={user_id}'>{user}</a> is not a mutual contact.</b>"
user_kicked: "<emoji document_id=5019523782004441717>❌</emoji> <b><a href='tg://user?id={user_id}'>{user}</a> is kicked from the chat.</b>"
user_invited: "<emoji document_id=5409029658794537988>✅</emoji> <b>User <a href='tg://user?id={id}'>{user}</a> is invited to the chat.</b>"
user_not_invited: "<emoji document_id=5019523782004441717>❌</emoji> <b>User could not be invited to the chat.</b>"
admin_list: "<blockquote expandable><emoji document_id=5361912768045792571>👑</emoji> <b>The creator is <a href='tg://user?id={id}'>{name}</a>\n\nAdmins ({admins_count}):</b>\n{admins}</blockquote>"
dnd: "<emoji document_id=5384262794306669858>🔕</emoji> <b>Chat muted and archived</b>"
dnd_failed: "<emoji document_id=5312383351217201533>⚠️</emoji> <b>Failed to mute and archive chat</b>"
pinned: "<emoji document_id=6296367896398399651>✅</emoji> <b>Pinned the message</b>"
pin_failed: "<emoji document_id=5458610095539645297>✖️</emoji><b> Failed to pin the message</b>"
unpinned: "<emoji document_id=6296367896398399651>✅</emoji> <b>Unpinned the message</b>"
unpin_failed: "<emoji document_id=5458610095539645297>✖️</emoji><b> Failed to unpin the message</b>"
type_group: "Group"
type_channel: "Channel"
type_unknown: "Unknown"
yes: "<emoji document_id=5408909562919007848>✅</emoji> Yes"
no: "<emoji document_id=5361566877149578396>✖️</emoji> No"
chatinfo: "<blockquote><emoji document_id=5983036958274752500>🔒</emoji><b> Type: {type_of}\n</b><emoji document_id=5985457743576698865>#️⃣</emoji><b> Chat ID: </b><code>{id}</code><b>\n</b><emoji document_id=5408849420491962048>🔥</emoji><b> Title: {title}\n<emoji document_id=5258328383183396223>📖</emoji><b> Forum:</b> {is_forum}</blockquote>\n</b><blockquote><emoji document_id=5870676941614354370>🖋</emoji><b> About: {about}</blockquote>\n</b><blockquote><emoji document_id=5805553606635559688>👑</emoji><b> Admin count: {admins_count}\n</b><emoji document_id=5433648711982921307>✅</emoji><b> Online count: {online_count}\n</b><emoji document_id=6024039683904772353>👤</emoji><b> Participants count: {participants_count}\n</b><emoji document_id=5816617137447376501>🚫</emoji><b> Kicked сount: {kicked_count}\n</b><emoji document_id=5431560533243346887>🔀</emoji><b> Requests pending: {requests_pending}</blockquote>\n</b><blockquote><emoji document_id=5408910404732595664>🕐</emoji><b> Slowmode period: {slowmode_seconds}\n</b><emoji document_id=6019279794988915337>📞</emoji><b> Call: {call}\n</b><emoji document_id=5408832111773757273>🗑</emoji><b> TTL period: {ttl_period}\n</b><emoji document_id=5408846628763217930>👤</emoji><b> Recent requesters: {recent_requesters}</blockquote>\n</b><blockquote><emoji document_id=6021690418398239007>👥</emoji><b> Linked Chat ID: {linked_chat_id}\n</b><emoji document_id=6019328362479097179>🛡</emoji><b> Antispam: {antispam}\n</b><emoji document_id=6024008227564296298>👁</emoji><b> Participants hidden: {participants_hidden}</blockquote>\n</b><emoji document_id=6028171274939797252>🔗</emoji><b> Link: {link}</b>"
requests_checked: "<emoji document_id=5409029658794537988>✅</emoji> <b>Checked requests from {entities}</b>"
promoted: "<emoji document_id=5458614983212427372>👑</emoji> <b><a href='tg://user?id={id}'>{name}</a> is promoted!\n<emoji document_id=5409029658794537988>✅</emoji> Rights: {rights}</b>"
full_rights: "Full rights"
promote: "<b>Select rights for <a href='tg://user?id={id}'>{name}</a>!\nRank: {rank}</b>"
restrict: "<b>Restricting <a href='tg://user?id={id}'>{name}</a> for{time}\n\n<i>Select which actions to restrict. Options marked in green will be applied.</i></b>"
restricted: "<emoji document_id=5208491751639106607>🚫</emoji> <b><a href='tg://user?id={id}'>{name}</a> has been restricted for{time}</b>"
demoted: "<emoji document_id=5447183459602669338>🔽</emoji> <b><a href='tg://user?id={id}'>{name}</a> is demoted</b>"
userinfo: "<blockquote><emoji document_id=5985457743576698865>#️⃣</emoji><b> ID: {user_id}\n</b><emoji document_id=5408849420491962048>🔥</emoji><b> First name: {first_name}\n</b><emoji document_id=5408849420491962048>🔥</emoji><b> Last name: {last_name}\n</b><emoji document_id=5208889774848361126>📞</emoji><b> Phone number: {phone}\n</b><emoji document_id=5364052602357044385>🐶</emoji><b> Usernames: {usernames}</b></blockquote><b>\n</b><blockquote><emoji document_id=5985616786215669454></emoji><b> About: {about}</b></blockquote><b>\n</b><blockquote><emoji document_id=5408910404732595664>🕐</emoji><b> Work hours: </b>{business_work_hours}\n<emoji document_id=5408892365869952851>❤️</emoji><b> Emoji status: {emoji_status}</b></blockquote><b>\n</b><blockquote><emoji document_id=5409331062419502443>🔉</emoji><b> Personal channel: {personal_channel}\n</b><emoji document_id=6024041612345088787>🎂</emoji><b> Birthday: {birthday}\n</b><emoji document_id=6037175527846975726>🎁</emoji><b> Gifts count: {stargifts_count}</b></blockquote><b>\n</b><blockquote><emoji document_id=5208842667647061916>🚨</emoji><b> Common chats count: {common_chats_count}\n</b><emoji document_id=5985401861757210746>👥</emoji><b> Common chats: {common_chats}</b></blockquote>"
monday: "Monday"
tuesday: "Tuesday"
wednesday: "Wednesday"
thursday: "Thursday"
friday: "Friday"
saturday: "Saturday"
sunday: "Sunday"
owns: "<emoji document_id=5458614983212427372>👑</emoji><b> My kingdoms [{num}]:</b>\n<blockquote expandable>{owns}</blockquote>"
close: "❌ Close"
apply: "✅ Apply"
ru:
my_id: "<emoji document_id=5361912768045792571>👑</emoji><b> Мой ID: </b><code>{id}</code>"
chat_id: "<emoji document_id=5886436057091673541>💬</emoji> <b>ID чата:</b> <code>{id}</code>"
user_id: "<emoji document_id=6035084557378654059>👤</emoji> <b>ID пользователя:</b> <code>{id}</code>"
user_not_participant: "<emoji document_id=5019523782004441717>❌</emoji> <b>Пользователь не состоит в этом чате.</b>"
admin_rights: "<blockquote expandable><emoji document_id=6023985764885338464>📜</emoji> {name} <b>Права в этом чате:\n\n{rights}</b>\n\n<emoji document_id=5287734473775918473>🔼</emoji><b> Повысил: {promoter_name}</b> [{promoter_id}]</blockquote>"
not_an_admin: "<emoji document_id=5019523782004441717>❌</emoji><b> {user} не является админом.</b>"
no_rights: "<emoji document_id=5019523782004441717>❌</emoji> <b>У меня недостаточно прав :(</b>"
no_user: "<emoji document_id=5019523782004441717>❌</emoji> <b>Пользователь не найден.</b>"
change_info: "Изменение информации"
delete_messages: "Удаление сообщений"
other: "Другое"
ban_users: "Бан пользователей"
invite_users: "Приглашение пользователей"
pin_messages: "Закрепление сообщений"
add_admins: "Добавление админов"
manage_call: "Управление звонками"
post_stories: "Публикация историй"
edit_stories: "Редактирование историй"
delete_stories: "Удаление историй"
anonymous: "Анонимность"
manage_topics: "Управление темами"
post_messages: "Публикация сообщений"
edit_messages: "Редактирование сообщений"
view_messages: "Просмотр сообщений"
send_messages: "Отправка сообщений"
send_media: "Отправка медиа"
send_stickers: "Отправка стикеров"
send_gifs: "Отправка GIF"
send_games: "Отправка игр"
send_inline: "Использование инлайн-ботов"
embed_links: "Встраивание ссылок"
send_polls: "Отправка опросов"
send_photos: "Отправка фото"
send_videos: "Отправка видео"
send_roundvideos: "Отправка круговых видео"
send_audios: "Отправка аудио"
send_voices: "Отправка голосовых"
send_docs: "Отправка документов"
send_plain: "Отправка текстовых сообщений"
invalid_args: "<emoji document_id=5219776129669276751>❌</emoji> <b>Неверные аргументы.</b>"
error: "<emoji document_id=5458497936763676259>😖</emoji><b> Что-то пошло не так. Проверьте логи.</b>"
successful_delete: "<emoji document_id=5409029658794537988>✅</emoji> <b>Сущность успешно удалена</b>"
no_deleted_accounts: "<emoji document_id=5238020759900668600>😶‍🌫️</emoji> <b>Удалённых аккаунтов здесь нет</b>"
kicked_deleted_accounts: "<emoji document_id=5408832111773757273>🗑</emoji> <b>Удалённые аккаунты очищены из чата</b>"
admins_in_chat: "<emoji document_id=5276229330131772747>👑</emoji> <b>Админы в <code>{title}</code> ({count}):</b>\n"
no_admins_in_chat: "<emoji document_id=5458610095539645297>✖️</emoji> <b>В этом чате нет админов.</b>"
bot_list: "<blockquote expandable><emoji document_id=5355051922862653659>🤖</emoji><b> Боты ({count}):</b>\n{bots}</blockquote>"
no_bots_in_chat: "<emoji document_id=5458610095539645297>✖️</emoji> <b>В этом чате нет ботов.</b>"
user_list: "<blockquote expandable><emoji document_id=5408846628763217930>👤</emoji><b> Пользователи ({count}):</b>\n{users}</blockquote>"
no_user_in_chat: "<emoji document_id=5458610095539645297>✖️</emoji> <b>В этом чате нет пользователей.</b>"
user_is_banned: "<emoji document_id=5348402067947929537>🚫</emoji> <b>{name} [<code>{id}</code>] забанен на{time_info}.</b>"
user_is_unbanned: "<emoji document_id=5355277430120523169>👋</emoji> <b>{name} [<code>{id}</code>] разбанен.</b>"
user_is_kicked: "<emoji document_id=5983033346207256798>🚪</emoji> <b><code>{name}</code> [<code>{id}</code>] кикнут.</b>"
user_is_muted: "<emoji document_id=5409380965644514142>🔕</emoji> <b>{name} [<code>{id}</code>] замьючен на{time_info}.</b>"
reason: "<i>Причина: {reason}</i>"
forever: "всегда"
user_is_unmuted: "<emoji document_id=5409331062419502443>🔉</emoji> <b>{name} [<code>{id}</code>] размьючен.</b>"
title_changed: "<b>{type_of} название успешно изменено с <code>{old_title}</code> на <code>{new_title}</code>.</b>"
channel_created: "<emoji document_id=6296367896398399651>✅</emoji> <b>Канал <code>{title}</code> создан.\n</b><emoji document_id=5237918475254526196>🔗</emoji><b> Пригласительная ссылка: {link}</b>"
group_created: "<emoji document_id=6296367896398399651>✅</emoji> <b>Группа <code>{title}</code> создана.\n</b><emoji document_id=5237918475254526196>🔗</emoji><b> Пригласительная ссылка: {link}</b>"
user_blocked: "<emoji document_id=5019523782004441717>❌</emoji> <b><a href='tg://user?id={user_id}'>{user}</a> заблокирован.</b>"
user_privacy_restricted: "<emoji document_id=5019523782004441717>❌</emoji> <b><a href='tg://user?id={user_id}'>{user}</a> ограничил это действие настройками приватности.</b>"
user_not_mutual_contact: "<emoji document_id=5019523782004441717>❌</emoji> <b><a href='tg://user?id={user_id}'>{user}</a> не является взаимным контактом.</b>"
user_kicked: "<emoji document_id=5019523782004441717>❌</emoji> <b><a href='tg://user?id={user_id}'>{user}</a> исключён из чата.</b>"
user_invited: "<emoji document_id=5409029658794537988>✅</emoji> <b>Пользователь <a href='tg://user?id={id}'>{user}</a> приглашён в чат.</b>"
user_not_invited: "<emoji document_id=5019523782004441717>❌</emoji> <b>Не удалось пригласить пользователя.</b>"
admin_list: "<blockquote expandable><emoji document_id=5361912768045792571>👑</emoji> <b>Создатель: <a href='tg://user?id={id}'>{name}</a>\n\nАдмины ({admins_count}):</b>\n{admins}</blockquote>"
dnd: "<emoji document_id=5384262794306669858>🔕</emoji> <b>Чат заглушён и архивирован</b>"
dnd_failed: "<emoji document_id=5312383351217201533>⚠️</emoji> <b>Не удалось заглушить и архивировать чат</b>"
pinned: "<emoji document_id=6296367896398399651>✅</emoji> <b>Сообщение закреплено</b>"
pin_failed: "<emoji document_id=5458610095539645297>✖️</emoji><b> Не удалось закрепить сообщение</b>"
unpinned: "<emoji document_id=6296367896398399651>✅</emoji> <b>Сообщение откреплено</b>"
unpin_failed: "<emoji document_id=5458610095539645297>✖️</emoji><b> Не удалось открепить сообщение</b>"
type_group: "Группа"
type_channel: "Канал"
type_unknown: "Неизвестно"
yes: "<emoji document_id=5408909562919007848>✅</emoji> Да"
no: "<emoji document_id=5361566877149578396>✖️</emoji> Нет"
chatinfo: "<blockquote><emoji document_id=5983036958274752500>🔒</emoji><b> Тип: {type_of}\n</b><emoji document_id=5985457743576698865>#️⃣</emoji><b> ID чата: </b><code>{id}</code><b>\n</b><emoji document_id=5408849420491962048>🔥</emoji><b> Название: {title}\n</b><emoji document_id=5258328383183396223>📖</emoji><b> Форум:</b> {is_forum}</blockquote>\n<blockquote><emoji document_id=5870676941614354370>🖋</emoji><b> Описание: {about}</blockquote>\n</b><blockquote><emoji document_id=5805553606635559688>👑</emoji><b> Кол-во админов: {admins_count}\n</b><emoji document_id=5433648711982921307>✅</emoji><b> Онлайн: {online_count}\n</b><emoji document_id=6024039683904772353>👤</emoji><b> Кол-во участников: {participants_count}\n</b><emoji document_id=5816617137447376501>🚫</emoji><b> Кикнутых: {kicked_count}\n</b><emoji document_id=5431560533243346887>🔀</emoji><b> Запросов в ожидании: {requests_pending}</blockquote>\n</b><blockquote><emoji document_id=5408910404732595664>🕐</emoji><b> Слоумод: {slowmode_seconds}\n</b><emoji document_id=6019279794988915337>📞</emoji><b> Звонок: {call}\n</b><emoji document_id=5408832111773757273>🗑</emoji><b> Период TTL: {ttl_period}\n</b><emoji document_id=5408846628763217930>👤</emoji><b> Недавние запросы: {recent_requesters}</blockquote>\n</b><blockquote><emoji document_id=6021690418398239007>👥</emoji><b> Связанный чат: {linked_chat_id}\n</b><emoji document_id=6019328362479097179>🛡</emoji><b> Антиспам: {antispam}\n</b><emoji document_id=6024008227564296298>👁</emoji><b> Скрытые участники: {participants_hidden}</blockquote>\n</b><emoji document_id=6028171274939797252>🔗</emoji><b> Ссылка: {link}</b>"
requests_checked: "<emoji document_id=5409029658794537988>✅</emoji> <b>Рассмотрены заявки от {entities}</b>"
promoted: "<emoji document_id=5458614983212427372>👑</emoji> <b><a href='tg://user?id={id}'>{name}</a> назначен администратором!\n<emoji document_id=5409029658794537988>✅</emoji> Права: {rights}</b>"
full_rights: "Полные права"
promote: "<b>Выберите права для <a href='tg://user?id={id}'>{name}</a>!\nРанг: {rank}</b>"
restrict: "<b>Ограничение <a href='tg://user?id={id}'>{name}</a> на{time}\n\n<i>Выберите, что нужно запретить. Опции, отмеченные зелёным, будут применены.</i></b>"
restricted: "<emoji document_id=5208491751639106607>🚫</emoji> <b><a href='tg://user?id={id}'>{name}</a> был(а) ограничен(а) на{time}</b>"
demoted: "<emoji document_id=5447183459602669338>🔽</emoji> <b><a href='tg://user?id={id}'>{name}</a> понижен</b>"
userinfo: "<blockquote><emoji document_id=5985457743576698865>#️⃣</emoji><b> ID: {user_id}\n</b><emoji document_id=5408849420491962048>🔥</emoji><b> Имя: {first_name}\n</b><emoji document_id=5408849420491962048>🔥</emoji><b> Фамилия: {last_name}\n</b><emoji document_id=5208889774848361126>📞</emoji><b> Номер телефона: {phone}\n</b><emoji document_id=5364052602357044385>🐶</emoji><b> Имена пользователей: {usernames}</b></blockquote><b>\n</b><blockquote><emoji document_id=5985616786215669454></emoji><b> О себе: {about}</b></blockquote><b>\n</b><blockquote><emoji document_id=5408910404732595664>🕐</emoji><b> Рабочие часы: </b>{business_work_hours}\n<emoji document_id=5408892365869952851>❤️</emoji><b> Статус с эмодзи: {emoji_status}</b></blockquote><b>\n</b><blockquote><emoji document_id=5409331062419502443>🔉</emoji><b> Личный канал: {personal_channel}\n</b><emoji document_id=6024041612345088787>🎂</emoji><b> День рождения: {birthday}\n</b><emoji document_id=6037175527846975726>🎁</emoji><b> Количество подарков: {stargifts_count}</b></blockquote><b>\n</b><blockquote><emoji document_id=5208842667647061916>🚨</emoji><b> Количество общих чатов: {common_chats_count}\n</b><emoji document_id=5985401861757210746>👥</emoji><b> Общие чаты: {common_chats}</b></blockquote>"
monday: "Понедельник"
tuesday: "Вторник"
wednesday: "Среда"
thursday: "Четверг"
friday: "Пятница"
saturday: "Суббота"
sunday: "Воскресенье"
owns: "<emoji document_id=5458614983212427372>👑</emoji><b> Мои королевства [{num}]:</b>\n<blockquote expandable>{owns}</blockquote>"
close: "❌ Закрыть"
apply: "✅ Применить"

BIN
fajox1/famods/.DS_Store vendored Normal file

Binary file not shown.

BIN
fajox1/famods/assets/.DS_Store vendored Normal file

Binary file not shown.

BIN
fajox1/famods/assets/banners/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
fajox1/famods/assets/birds/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,9 @@
[
"blue",
"green",
"orange",
"pink",
"purple",
"white",
"yellow"
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

View File

@@ -43,3 +43,4 @@ evalaliases
spotify4ik
picme
hetsu
ptichki

156
fajox1/famods/ptichki.py Normal file
View File

@@ -0,0 +1,156 @@
# █▀▀ ▄▀█   █▀▄▀█ █▀█ █▀▄ █▀
# █▀░ █▀█   █░▀░█ █▄█ █▄▀ ▄█
# https://t.me/famods
# 🔒 Licensed under the GNU AGPLv3
# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
# ---------------------------------------------------------------------------------
# Name: Ptichki
# Description: Генератор птиц
# meta developer: @FAmods
# meta banner: https://github.com/FajoX1/FAmods/blob/main/assets/banners/ptichki.png?raw=true
# requires: aiohttp pillow
# ---------------------------------------------------------------------------------
import json
import random
import aiohttp
import logging
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds
class Ptichki(loader.Module):
"""Генератор птиц"""
strings = {
"name": "Ptichki",
"no_args": "<emoji document_id=5393175345367123686>🦅</emoji> <b>Нужно </b><code>{}{} {}</code>",
"generation": "<emoji document_id=5393165630151103839>🦅</emoji> <i>Генерирую птичку...</i>",
}
async def client_ready(self, client, db):
self.db = db
self._client = client
self.assets_link = "https://famods.fajox.one/assets"
self.font_url = f"{self.assets_link}/impact.ttf"
self.birds_url = f"{self.assets_link}/birds/birds.json"
async def fetch_bytes(self, url: str) -> bytes:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
resp.raise_for_status()
return await resp.read()
async def get_bird_url(self) -> str:
async with aiohttp.ClientSession() as session:
async with session.get(self.birds_url) as resp:
birds_list = json.loads(await resp.text())
return f"{self.assets_link}/birds/{random.choice(birds_list)}.png"
async def generate_bird(self, text: str, format: str) -> bytes:
text = text.upper()
img_bytes = await self.fetch_bytes(
await self.get_bird_url()
)
img = Image.open(BytesIO(img_bytes)).convert("RGBA")
img.thumbnail((512, 512))
width, height = img.size
draw = ImageDraw.Draw(img)
font_bytes = await self.fetch_bytes(self.font_url)
font_size = 55
min_font_size = 12
max_width_fraction = 0.9
font = ImageFont.truetype(BytesIO(font_bytes), font_size)
text_width = font.getlength(text)
if text_width > max_width_fraction * width:
scale = (max_width_fraction * width) / text_width
font_size = max(int(font_size * scale), min_font_size)
font = ImageFont.truetype(BytesIO(font_bytes), font_size)
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (width - text_width) / 2
y = height - text_height - (height * 0.05)
draw.text(
(x, y),
text,
font=font,
fill="white",
stroke_width=2,
stroke_fill="black"
)
output = BytesIO()
img.save(output, format=format.upper())
output.seek(0)
output.name = f"ptitchka.{format.lower()}"
return output
@loader.command()
async def ptichka(self, message):
"""[текст] - Сгенерировать стикер с птицей"""
text = utils.get_args_raw(message)
if not text:
return await utils.answer(
message,
self.strings["no_args"].format(
self.get_prefix(), "ptichka", "[текст]"
)
)
m = await utils.answer(message, self.strings['generation'])
await self.client.send_file(
message.peer_id,
mime_type="image/webp",
file=await self.generate_bird(text, format="webp"),
reply_to=getattr(message.reply_to, "reply_to_msg_id", None),
)
return await m.delete()
@loader.command()
async def ptichka_img(self, message):
"""[текст] - Сгенерировать фото с птицей"""
text = utils.get_args_raw(message)
if not text:
return await utils.answer(
message,
self.strings["no_args"].format(
self.get_prefix(), "ptichka_img", "[текст]"
)
)
m = await utils.answer(message, self.strings['generation'])
await self.client.send_file(
message.peer_id,
mime_type="image/png",
file=await self.generate_bird(text, format="png"),
reply_to=getattr(message.reply_to, "reply_to_msg_id", None),
)
return await m.delete()

View File

@@ -21,3 +21,4 @@ multiunloadmodule
tagall2.0
point
deviceinfo
mpi

View File

@@ -0,0 +1,229 @@
# ______ ___ ___ _ _
# ____ | ___ \ | \/ | | | | |
# / __ \| |_/ / _| . . | ___ __| |_ _| | ___
# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \
# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/
# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___|
# \____/ __/ |
# |___/
# На модуль распространяется лицензия "GNU General Public License v3.0"
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule
import aiohttp
import base64
import json
from datetime import datetime
from .. import loader, utils
async def download_image(session: aiohttp.ClientSession, url: str):
try:
async with session.get(url, timeout=35) as resp:
if resp.status == 200:
return await resp.read()
except Exception:
pass
return None
@loader.tds
class MinecraftPlayerInfo(loader.Module):
"""A module for obtaining information about a Minecraft player by nickname"""
strings = {
"name": "MinecraftPlayerInfo",
"no_args": "<b>❌ Specify the player's nickname</b>",
"not_found": "<b>❌ Player with this nickname not found</b>",
"loading": "<b>🔄 Loading information...</b>",
"no_media": "<b>❌ Failed to load any images</b>",
"partial_media": "<i>⚠️ Some images failed to load</i>\n\n",
"no_history": "No nickname history",
"model_steve": "Classic (Steve)",
"model_alex": "Slim (Alex)",
"cape_yes": "Yes ✅",
"cape_no": "No ❌",
"cape_failed": " (render failed to load)",
"history_current": "— current",
"history_changed": "— changed {}",
"history_original": "— original",
"info": (
"<b>🔍 Minecraft Player Information</b>\n\n"
"<b>Nickname:</b> <code>{name}</code>\n"
"<b>UUID:</b> <code>{uuid_dashed}</code>\n"
"<b>Skin Model:</b> {model}\n"
"<b>Cape:</b> {cape}\n\n"
"<b>Nickname History:</b>\n{history}\n\n"
"<a href=\"https://namemc.com/profile/{uuid_raw}\">🔗 Full profile on NameMC</a>"
)
}
strings_ru = {
"_cls_doc": "Модуль для получения информации о игроке Minecraft по никнейму",
"no_args": "<b>❌ Укажите никнейм игрока</b>",
"not_found": "<b>❌ Игрок с таким никнеймом не найден</b>",
"loading": "<b>🔄 Загружаю информацию...</b>",
"no_media": "<b>❌ Не удалось загрузить ни одного изображения</b>",
"partial_media": "<i>⚠️ Некоторые изображения не загрузились</i>\n\n",
"no_history": "Нет истории изменений",
"model_steve": "Classic (Steve)",
"model_alex": "Slim (Alex)",
"cape_yes": "Есть ✅",
"cape_no": "Нет ❌",
"cape_failed": " (рендер не загрузился)",
"history_current": "— текущий",
"history_changed": "— изменён {}",
"history_original": "— оригинальный",
"info": (
"<b>🔍 Информация о игроке Minecraft</b>\n\n"
"<b>Никнейм:</b> <code>{name}</code>\n"
"<b>UUID:</b> <code>{uuid_dashed}</code>\n"
"<b>Модель скина:</b> {model}\n"
"<b>Плащ:</b> {cape}\n\n"
"<b>История никнеймов:</b>\n{history}\n\n"
"<a href=\"https://namemc.com/profile/{uuid_raw}\">🔗 Полный профиль на NameMC</a>"
)
}
async def client_ready(self, client, db):
self.client = client
@loader.command(ru_doc="<никнейм> — отображает информацию об игроке Minecraft (3D-рендеринг, история, плащ)")
async def mcplayer(self, message):
"""<nickname> — show Minecraft player info (3D renders, history, cape)"""
args = utils.get_args_raw(message)
if not args:
return await utils.answer(message, self.strings("no_args"))
loading_msg = await utils.answer(message, self.strings("loading"))
nick = args.strip()
async with aiohttp.ClientSession() as session:
async with session.get(f"https://api.mojang.com/users/profiles/minecraft/{nick}") as resp:
if resp.status == 204 or resp.status != 200:
await loading_msg.edit(self.strings("not_found"))
return
data = await resp.json()
name = data["name"]
uuid = data["id"]
uuid_dashed = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
async with session.get(f"https://api.mojang.com/user/profiles/{uuid}/names") as resp:
names = await resp.json() if resp.status == 200 else [{"name": name}]
history_lines = []
for i, entry in enumerate(names):
uname = entry["name"]
if i == len(names) - 1:
history_lines.append(f"• <b>{uname}</b> {self.strings('history_current')}")
elif "changedToAt" in entry:
changed = datetime.utcfromtimestamp(entry["changedToAt"] / 1000).strftime("%d.%m.%Y")
history_lines.append(f"{uname} {self.strings('history_changed').format(changed)}")
else:
history_lines.append(f"{uname} {self.strings('history_original')}")
history = "\n".join(history_lines) or self.strings("no_history")
async with session.get(f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false") as resp:
profile = await resp.json() if resp.status == 200 else {}
cape_url = None
model = self.strings("model_steve")
if profile.get("properties"):
for prop in profile["properties"]:
if prop["name"] == "textures":
textures_b64 = prop["value"]
textures_json = json.loads(base64.b64decode(textures_b64).decode("utf-8"))
textures = textures_json.get("textures", {})
skin_data = textures.get("SKIN", {})
cape_url = textures.get("CAPE", {}).get("url")
if "metadata" in skin_data and skin_data["metadata"].get("model") == "slim":
model = self.strings("model_alex")
has_cape = bool(cape_url)
cape_text = self.strings("cape_yes") if has_cape else self.strings("cape_no")
body_urls = [
f"https://crafthead.net/body/{uuid}.png",
f"https://api.mineatar.io/body/{uuid}?scale=12",
f"https://mc-heads.net/body/{uuid}/500",
f"https://cravatar.eu/helmbody/{uuid}/500.png",
f"https://minotar.net/body/{uuid}/500.png",
]
head_urls = [
f"https://crafthead.net/avatar/{uuid}.png",
f"https://api.mineatar.io/head/{uuid}?scale=12",
f"https://mc-heads.net/avatar/{uuid}/500",
f"https://cravatar.eu/head/{uuid}/500.png",
f"https://minotar.net/avatar/{uuid}/500.png",
]
body_bytes = None
for url in body_urls:
body_bytes = await download_image(session, url)
if body_bytes:
break
head_bytes = None
for url in head_urls:
head_bytes = await download_image(session, url)
if head_bytes:
break
cape_bytes = await download_image(session, cape_url) if cape_url else None
if not cape_bytes and has_cape:
cape_fallbacks = [
f"https://crafthead.net/cape/{uuid}.png",
f"https://api.mineatar.io/cape/{uuid}.png",
f"https://mc-heads.net/cape/{uuid}",
]
for url in cape_fallbacks:
cape_bytes = await download_image(session, url)
if cape_bytes:
break
if not cape_bytes:
cape_text += self.strings("cape_failed")
uploaded_media = []
if body_bytes:
uploaded = await self.client.upload_file(body_bytes, file_name=f"{name}_body.png")
uploaded_media.append(uploaded)
if head_bytes:
uploaded = await self.client.upload_file(head_bytes, file_name=f"{name}_head.png")
uploaded_media.append(uploaded)
if cape_bytes:
uploaded = await self.client.upload_file(cape_bytes, file_name=f"{name}_cape.png")
uploaded_media.append(uploaded)
if not uploaded_media:
await loading_msg.edit(self.strings("no_media"))
return
caption = self.strings("info").format(
name=name,
uuid_dashed=uuid_dashed,
uuid_raw=uuid,
model=model,
cape=cape_text,
history=history
)
if not (body_bytes and head_bytes):
caption = self.strings("partial_media") + caption
await self.client.send_file(
message.peer_id,
file=uploaded_media,
caption=caption,
parse_mode="html",
reply_to=message.reply_to_msg_id or message.id
)
await loading_msg.delete()
if message.out:
await message.delete()

View File

@@ -26,241 +26,6 @@ from .. import loader, utils
__version__ = (2, 0, 0)
FLAGS = {
"ad": "🇦🇩", # Андорра
"ae": "🇦🇪", # ОАЭ
"af": "🇦🇫", # Афганистан
"ag": "🇦🇬", # Антигуа и Барбуда
"ai": "🇦🇮", # Ангилья
"al": "🇦🇱", # Албания
"am": "🇦🇲", # Армения
"ao": "🇦🇴", # Ангола
"aq": "🇦🇶", # Антарктика
"ar": "🇦🇷", # Аргентина
"at": "🇦🇹", # Австрия
"au": "🇦🇺", # Австралия
"aw": "🇦🇼", # Аруба
"ax": "🇦🇽", # Аландские острова
"az": "🇦🇿", # Азербайджан
"ba": "🇧🇦", # Босния и Герцеговина
"bb": "🇧🇧", # Барбадос
"bd": "🇧🇩", # Бангладеш
"be": "🇧🇪", # Бельгия
"bf": "🇧🇫", # Буркина-Фасо
"bg": "🇧🇬", # Болгария
"bh": "🇧🇭", # Бахрейн
"bi": "🇧🇮", # Бурунди
"bj": "🇧🇯", # Бенин
"bl": "🇧🇱", # Сен-Бартельми
"bm": "🇧🇲", # Бермудские острова
"bn": "🇧🇳", # Бруней
"bo": "🇧🇴", # Боливия
"bq": "🇧🇶", # Бонэйр, Синт-Эстатиус и Саба
"br": "🇧🇷", # Бразилия
"bs": "🇧🇸", # Багамы
"bt": "🇧🇹", # Бутан
"bv": "🇧🇻", # остров Буве
"bw": "🇧🇼", # Ботсвана
"by": "🇧🇾", # Беларусь
"bz": "🇧🇿", # Белиз
"ca": "🇨🇦", # Канада
"cc": "🇨🇨", # Кокосовые (Килинг) острова
"cd": "🇨🇩", # Конго - Киншаса
"cf": "🇨🇫", # Центральноафриканская Республика
"cg": "🇨🇬", # Конго - Браззавиль
"ch": "🇨🇭", # Швейцария
"ci": "🇨🇮", # Кот-д’Ивуар
"ck": "🇨🇰", # Острова Кука
"cl": "🇨🇱", # Чили
"cm": "🇨🇲", # Камерун
"cn": "🇨🇳", # Китай
"co": "🇨🇴", # Колумбия
"cr": "🇨🇷", # Коста-Рика
"cu": "🇨🇺", # Куба
"cv": "🇨🇻", # Кабо-Верде
"cw": "🇨🇼", # Кюрасао
"cx": "🇨🇽", # остров Рождества
"cy": "🇨🇾", # Кипр
"cz": "🇨🇿", # Чехия
"de": "🇩🇪", # Германия
"dj": "🇩🇯", # Джибути
"dk": "🇩🇰", # Дания
"dm": "🇩🇲", # Доминика
"do": "🇩🇴", # Доминиканская Республика
"dz": "🇩🇿", # Алжир
"ec": "🇪🇨", # Эквадор
"ee": "🇪🇪", # Эстония
"eg": "🇪🇬", # Египет
"eh": "🇪🇭", # Западная Сахара
"er": "🇪🇷", # Эритрея
"es": "🇪🇸", # Испания
"et": "🇪🇹", # Эфиопия
"fi": "🇫🇮", # Финляндия
"fj": "🇫🇯", # Фиджи
"fk": "🇫🇰", # Фолклендские острова
"fm": "🇫🇲", # Микронезия
"fo": "🇫🇴", # Фарерские острова
"fr": "🇫🇷", # Франция
"ga": "🇬🇦", # Габон
"gb": "🇬🇧", # Великобритания
"gd": "🇬🇩", # Гренада
"ge": "🇬🇪", # Грузия
"gf": "🇬🇫", # Французская Гвиана
"gg": "🇬🇬", # Гернси
"gh": "🇬🇭", # Гана
"gi": "🇬🇮", # Гибралтар
"gl": "🇬🇱", # Гренландия
"gm": "🇬🇲", # Гамбия
"gn": "🇬🇳", # Гвинея
"gp": "🇬🇵", # Гваделупа
"gq": "🇬🇶", # Экваториальная Гвинея
"gr": "🇬🇷", # Греция
"gs": "🇬🇸", # Южная Георгия и Южные Сандвичевы острова
"gt": "🇬🇹", # Гватемала
"gu": "🇬🇺", # Гуам
"gw": "🇬🇼", # Гвинея-Бисау
"gy": "🇬🇾", # Гайана
"hk": "🇭🇰", # Гонконг
"hm": "🇭🇲", # остров Херд и острова Макдональд
"hn": "🇭🇳", # Гондурас
"hr": "🇭🇷", # Хорватия
"ht": "🇭🇹", # Гаити
"hu": "🇭🇺", # Венгрия
"id": "🇮🇩", # Индонезия
"ie": "🇮🇪", # Ирландия
"il": "🇮🇱", # Израиль
"im": "🇮🇲", # остров Мэн
"in": "🇮🇳", # Индия
"io": "🇮🇴", # Британская территория в Индийском океане
"iq": "🇮🇶", # Ирак
"ir": "🇮🇷", # Иран
"is": "🇮🇸", # Исландия
"it": "🇮🇹", # Италия
"je": "🇯🇪", # Джерси
"jm": "🇯🇲", # Ямайка
"jo": "🇯🇴", # Иордания
"jp": "🇯🇵", # Япония
"ke": "🇰🇪", # Кения
"kg": "🇰🇬", # Киргизия
"kh": "🇰🇭", # Камбоджа
"ki": "🇰🇮", # Кирибати
"km": "🇰🇲", # Коморы
"kn": "🇰🇳", # Сент-Китс и Невис
"kp": "🇰🇵", # Корейская Народно-Демократическая Республика
"kr": "🇰🇷", # Республика Корея
"kw": "🇰🇼", # Кувейт
"ky": "🇰🇾", # Каймановы острова
"kz": "🇰🇿", # Казахстан
"la": "🇱🇦", # Лаос
"lb": "🇱🇧", # Ливан
"lc": "🇱🇨", # Сент-Люсия
"li": "🇱🇮", # Лихтенштейн
"lk": "🇱🇰", # Шри-Ланка
"lr": "🇱🇷", # Либерия
"ls": "🇱🇸", # Лесото
"lt": "🇱🇹", # Литва
"lu": "🇱🇺", # Люксембург
"lv": "🇱🇻", # Латвия
"ly": "🇱🇾", # Ливия
"my": "🇲🇾",
"md": "🇲🇩",
"mv": "🇲🇻",
"mw": "🇲🇼",
"mx": "🇲🇽",
"my": "🇲🇾",
"mz": "🇲🇿",
"na": "🇳🇦",
"nc": "🇳🇨",
"ne": "🇳🇪",
"nf": "🇳🇫",
"ng": "🇳🇬",
"ni": "🇳🇮",
"nl": "🇳🇱",
"no": "🇳🇴",
"np": "🇳🇵",
"nr": "🇳🇷",
"nu": "🇳🇺",
"nz": "🇳🇿",
"om": "🇴🇲",
"pa": "🇵🇦",
"pe": "🇵🇪",
"pf": "🇵🇫",
"pg": "🇵🇬",
"ph": "🇵🇭",
"pk": "🇵🇰",
"pl": "🇵🇱",
"pm": "🇵🇲",
"pn": "🇵🇳",
"pr": "🇵🇷",
"ps": "🇵🇸",
"pt": "🇵🇹",
"pw": "🇵🇼",
"py": "🇵🇾",
"qa": "🇶🇦",
"re": "🇷🇪",
"ro": "🇷🇴",
"rs": "🇷🇸",
"ru": "🇷🇺",
"rw": "🇷🇼",
"sa": "🇸🇦",
"sb": "🇸🇧",
"sc": "🇸🇨",
"sd": "🇸🇩",
"se": "🇸🇪",
"sg": "🇸🇬",
"sh": "🇸🇭",
"si": "🇸🇮",
"sj": "🇸🇯",
"sk": "🇸🇰",
"sl": "🇸🇱",
"sm": "🇸🇲",
"sn": "🇸🇳",
"so": "🇸🇴",
"sr": "🇸🇷",
"ss": "🇸🇸",
"st": "🇸🇹",
"sv": "🇸🇻",
"sx": "🇸🇽",
"sy": "🇸🇾",
"sz": "🇸🇿",
"tc": "🇹🇨",
"td": "🇹🇩",
"tf": "🇹🇫",
"tg": "🇹🇬",
"th": "🇹🇭",
"tj": "🇹🇯",
"tk": "🇹🇰",
"tl": "🇹🇱",
"tm": "🇹🇲",
"tn": "🇹🇳",
"to": "🇹🇴",
"tr": "🇹🇷",
"tt": "🇹🇹",
"tv": "🇹🇻",
"tw": "🇹🇼",
"tz": "🇹🇿",
"ua": "🇺🇦",
"ug": "🇺🇬",
"um": "🇺🇲",
"us": "🇺🇸",
"va": "🇻🇦",
"vc": "🇻🇨",
"ve": "🇻🇪",
"vg": "🇻🇬",
"vi": "🇻🇮",
"vn": "🇻🇳",
"vu": "🇻🇺",
"wf": "🇼🇫",
"ws": "🇼🇸",
"xk": "🇽🇰",
"ye": "🇾🇪",
"yt": "🇾🇹",
"za": "🇿🇦",
"zm": "🇿🇲",
"zw": "🇿🇼",
}
class Error(enum.Enum):
critical = 500
not_found = 404
@@ -382,6 +147,8 @@ class HostAPI(API):
route = f"{self._url}/{tg_id}/logs/{lines}"
return await self._request(route, method="GET", headers=self.auth_header)
def get_flag(country_code: str = None) -> str:
return ''.join( chr(ord(c.upper()) + 127397) for c in country_code)
@loader.tds
class HHMod(loader.Module):
@@ -389,16 +156,19 @@ class HHMod(loader.Module):
strings = {
"name": "HH",
"info": (
"_cfg_doc_hinfo_message": "Custom message text in hinfo. May contain keywords: {id}, {status}, {server}, {days_end}, {cpu_percent}, {ram_usage}, {warns}.",
"_cfg_doc_hinfo_banner_url": "Link to banner image or None.",
"default_info": (
"<emoji document_id=5413334818047940135>👤</emoji> <b>Info for</b> <code>{id}</code>\n\n"
"<emoji document_id=5418136591484865679>📶</emoji> <b>Status:</b> {status}\n"
"<emoji document_id=5415992848753379520>⚙️</emoji> <b>Server:</b> {server}\n"
"<emoji document_id=5416042764863293485>❤️</emoji> <b>The subscription expires after</b> <code>{days_end} days</code>\n"
"{stats}\n"
"<emoji document_id=5413394354884596702>💾</emoji> <b>Used now:</b> <code>{cpu_percent}%</code> CPU, <code>{ram_usage}MB</code> RAM\n\n"
"{warns}"
),
"logs": "<emoji document_id=5411608069396254249>📄</emoji> All docker container logs from the userbot\n\n<i>In t.me/hikkahost_bot/hhapp logs more readable</i>",
"stats": "<emoji document_id=5413394354884596702>💾</emoji> <b>Used now:</b> <code>{cpu_percent}%</code> CPU, <code>{memory}MB</code> RAM\n",
"loading_info": "<emoji document_id=5416094132672156295>⌛️</emoji> Loading...",
"no_apikey": "<emoji document_id=5411402525146370107>🚫</emoji> Not specified API Key, need get token:\n\n1. Go to the @hikkahost_bot\n2. Send /token\n3. Paste token to .config HH",
"warn_sub_left": "<emoji document_id=5411402525146370107>🚫</emoji> <i>There are less than 5 days left until the end of the subscription</i>\n",
@@ -413,16 +183,15 @@ class HHMod(loader.Module):
strings_ru = {
"name": "HH",
"info": (
"<emoji document_id=5413334818047940135>👤</emoji> <b>Информация о</b> <code>{id}</code>\n\n"
"<emoji document_id=5418136591484865679>📶</emoji> <b>Статус:</b> {status}\n"
"<emoji document_id=5415992848753379520>⚙️</emoji> <b>Сервер:</b> {server}\n"
"<emoji document_id=5416042764863293485>❤️</emoji> <b>Подписка истечёт через</b> <code>{days_end} дней</code>\n"
"{stats}\n"
"{warns}"
),
# "default_info": (
# "<emoji document_id=5413334818047940135>👤</emoji> <b>Информация о</b> <code>{id}</code>\n\n"
# "<emoji document_id=5418136591484865679>📶</emoji> <b>Статус:</b> {status}\n"
# "<emoji document_id=5415992848753379520>⚙️</emoji> <b>Сервер:</b> {server}\n"
# "<emoji document_id=5416042764863293485>❤️</emoji> <b>Подписка истечёт через</b> <code>{days_end} дней</code>\n"
# "<emoji document_id=5413394354884596702>💾</emoji> <b>Используется:</b> <code>{cpu_percent}%</code> CPU, <code>{ram_usage}MB</code> RAM\n\n"
# "{warns}"
# ),
"logs": "<emoji document_id=5411608069396254249>📄</emoji> Все логи docker контейнера от hikka\n\n<i>В t.me/hikkahost_bot/hhapp логи более читабельны</i>",
"stats": "<emoji document_id=5413394354884596702>💾</emoji> <b>Используется:</b> <code>{cpu_percent}%</code> CPU, <code>{memory}MB</code> RAM\n",
"loading_info": "<emoji document_id=5416094132672156295>⌛️</emoji> Загрузка...",
"no_apikey": "<emoji document_id=5411402525146370107>🚫</emoji> Не задан ключ API, нужно получить токен:\n\n1. Зайдите в @hikkahost_bot\n2. Отправьте /token\n3. Запишите токен в .config HH",
"warn_sub_left": "<emoji document_id=5411402525146370107>🚫</emoji> <i>Менее чем через 5 дней подписка истечёт</i>\n",
@@ -443,6 +212,16 @@ class HHMod(loader.Module):
None,
validator=loader.validators.Hidden(),
),
loader.ConfigValue(
"hinfo_message",
self.strings["default_info"],
self.strings['_cfg_doc_hinfo_message']
),
loader.ConfigValue(
"hinfo_banner_url",
"https://github.com/hikkahost/.github/blob/main/banners/main.jpg?raw=true",
self.strings['_cfg_doc_hinfo_banner_url']
),
)
async def client_ready(self, client, db):
@@ -485,19 +264,17 @@ class HHMod(loader.Module):
stats = (await api.get_stats(user_id))["stats"]
working = True if status["status"] == "running" else False
load = {}
if working:
cpu_stats = stats["cpu_stats"]
cpu_total_usage = cpu_stats['cpu_usage']['total_usage']
system_cpu_usage = cpu_stats['system_cpu_usage']
ram_usage = round(stats["memory_stats"]["usage"] / (1024 * 1024), 2)
cpu_percent = round((cpu_total_usage / system_cpu_usage) * 100.0, 2)
stats = self.strings["stats"].format(
cpu_percent=cpu_percent, memory=ram_usage
)
else:
stats = ""
load = {
"ram_usage": round(stats["memory_stats"]["usage"] / (1024 * 1024), 2),
"cpu_percent": round((cpu_total_usage / system_cpu_usage) * 100.0, 2)
}
end_date = host.end_date.replace(tzinfo=timezone.utc)
warns = ""
@@ -510,20 +287,22 @@ class HHMod(loader.Module):
server = servers_dict.get(host.server_id)
server = self.strings["server"].format(
flag=FLAGS[server["country_code"]],
flag=get_flag(server["country_code"]),
name=server["name"],
)
await utils.answer(
message,
self.strings["info"].format(
self.config["hinfo_message"].format(
id=user_id,
warns=warns,
stats=stats,
server=server,
days_end=days_end,
status=self.strings["statuses"][status["status"]],
ram_usage=load.get("ram_usage", "0.00"),
cpu_percent=load.get("cpu_percent", "0.00")
),
file=self.config['hinfo_banner_url']
)
@loader.command(