mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 22:34:19 +02:00
Compare commits
40 Commits
update-sub
...
71ed0c604c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71ed0c604c | ||
|
|
f335145976 | ||
| 6b6afb7493 | |||
|
|
2ed246b9ad | ||
|
|
837784206f | ||
|
|
811beb2b74 | ||
|
|
d279789b37 | ||
|
|
74dfe4caf8 | ||
|
|
18b8247e21 | ||
| 6394482213 | |||
| 43a0e578c1 | |||
| 3112e6d4bf | |||
| 18bb817239 | |||
| 24f1983e2a | |||
| 74638c14d0 | |||
|
|
db62a5aee1 | ||
|
|
4d2a2899f4 | ||
|
|
2a27c56d83 | ||
| 49956ba865 | |||
| f0d2a28105 | |||
| 13d091c56c | |||
| d7cf406b78 | |||
|
|
0f30a78990 | ||
|
|
16adfac8b5 | ||
|
|
7ddb190b35 | ||
| e50c7c1688 | |||
| f3682ed87a | |||
| be47e59d97 | |||
| eb71e39fcf | |||
|
|
7d713e36c0 | ||
|
|
a8fde5e498 | ||
|
|
4a03c6cb1a | ||
|
|
a424d6bac4 | ||
|
|
fc8344ca05 | ||
|
|
3e62dc0b69 | ||
| 41f253b471 | |||
|
|
59564f07b5 | ||
|
|
dfe2ae1103 | ||
|
|
4ca7279309 | ||
| 3a193cfb2f |
@@ -7,7 +7,7 @@ __version__ = (1, 0, 0)
|
||||
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/BSR/banner.png
|
||||
# meta developer: @FModules
|
||||
# meta developer: @NFModules
|
||||
# meta fhsdesc: brawlstars, game, funny
|
||||
|
||||
from .. import loader, utils
|
||||
@@ -42,6 +42,7 @@ async def to_code(n: int) -> str:
|
||||
n_shifted //= 31
|
||||
return "X" + "".join(reversed(res))
|
||||
|
||||
|
||||
@loader.tds
|
||||
class BSR(loader.Module):
|
||||
'''Module for finding nearby game rooms in BrawlStars.'''
|
||||
@@ -139,7 +140,7 @@ class BSR(loader.Module):
|
||||
'''(room code/link) (previous) (next) - find rooms.'''
|
||||
args = utils.get_args_raw(message).split()
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings("invalid_args").format(prefix=self.get_prefix()))
|
||||
return await utils.answer(message, self.strings["invalid_args"].format(prefix=self.get_prefix()))
|
||||
|
||||
raw_input = args[0]
|
||||
before = 0
|
||||
@@ -161,13 +162,13 @@ class BSR(loader.Module):
|
||||
nxt = max(0, min(nxt, 5000))
|
||||
|
||||
if before == 0 and nxt == 0:
|
||||
return await utils.answer(message, self.strings("at_least_one"))
|
||||
return await utils.answer(message, self.strings["at_least_one"])
|
||||
|
||||
clean_tag = await extract_code(raw_input)
|
||||
base_id = await to_id(clean_tag)
|
||||
|
||||
if base_id == 0:
|
||||
return await utils.answer(message, self.strings("invalid_code"))
|
||||
return await utils.answer(message, self.strings["invalid_code"])
|
||||
|
||||
text, page, total_pages = await self.get_page_content(base_id, before, nxt, 0)
|
||||
kb = self.build_keyboard(base_id, before, nxt, page, total_pages, clean_tag)
|
||||
@@ -205,10 +206,10 @@ class BSR(loader.Module):
|
||||
blocks = []
|
||||
|
||||
if prev_list:
|
||||
blocks.append(self.strings("prev_block").format(prev_list="\n".join(prev_list)))
|
||||
blocks.append(self.strings["prev_block"].format(prev_list="\n".join(prev_list)))
|
||||
|
||||
if next_list:
|
||||
blocks.append(self.strings("next_block").format(next_list="\n".join(next_list)))
|
||||
blocks.append(self.strings["next_block"].format(next_list="\n".join(next_list)))
|
||||
|
||||
res = "\n\n".join(blocks)
|
||||
if not res.strip():
|
||||
@@ -220,7 +221,7 @@ class BSR(loader.Module):
|
||||
kb = [
|
||||
[
|
||||
{
|
||||
"text": self.strings("btn_target"),
|
||||
"text": self.strings["btn_target"],
|
||||
"copy": clean_tag
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
__version__ = (9, 3, 9)
|
||||
|
||||
# meta developer: @FModules
|
||||
# meta developer: @NFModules
|
||||
# meta pic: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/logo.png
|
||||
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/logo.png
|
||||
# scope: hikka_min 2.0.0
|
||||
@@ -19,15 +19,19 @@ import re
|
||||
import sys
|
||||
import uuid
|
||||
import importlib
|
||||
from contextlib import suppress
|
||||
from typing import Optional, Dict, List, Union, Tuple, Any
|
||||
from urllib.parse import unquote
|
||||
from importlib.machinery import ModuleSpec
|
||||
|
||||
import telethon
|
||||
from .. import loader, utils
|
||||
from ..types import CoreOverwriteError
|
||||
from herokutl.tl.functions.contacts import UnblockRequest
|
||||
from aiogram.types import InlineQueryResultArticle, InputTextMessageContent, LinkPreviewOptions, ChosenInlineResult, CallbackQuery, Message
|
||||
|
||||
try:
|
||||
from aiogram.types import InlineQueryResultArticle, InputTextMessageContent, LinkPreviewOptions
|
||||
except ImportError:
|
||||
InlineQueryResultArticle = InputTextMessageContent = LinkPreviewOptions = Any
|
||||
|
||||
|
||||
class FHetaAPI:
|
||||
@@ -70,14 +74,14 @@ class FHetaAPI:
|
||||
return {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
class MInstaller:
|
||||
async def execute(self, plugin: 'loader.Module', url: str) -> Tuple[str, List[str]]:
|
||||
try:
|
||||
code = await plugin._storage.fetch(url, auth=plugin.config.get("basic_auth"))
|
||||
except Exception:
|
||||
return "error", []
|
||||
return "error",[]
|
||||
|
||||
for step in range(5):
|
||||
state = await self.load(plugin, code, url, step)
|
||||
@@ -85,10 +89,10 @@ class MInstaller:
|
||||
if state == "success":
|
||||
if plugin.fully_loaded:
|
||||
plugin.update_modules_in_db()
|
||||
return "success", []
|
||||
return "success",[]
|
||||
|
||||
if state == "overwrite":
|
||||
return "overwrite", []
|
||||
return "overwrite",[]
|
||||
|
||||
if isinstance(state, list):
|
||||
return "dependency", state
|
||||
@@ -98,47 +102,37 @@ class MInstaller:
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
return "dependency", []
|
||||
|
||||
async def pip(self, dependencies: List[str]) -> bool:
|
||||
virtualenv = hasattr(sys, 'real_prefix') or sys.prefix != getattr(sys, 'base_prefix', sys.prefix)
|
||||
flags = ["--user"] if loader.USER_INSTALL and not virtualenv else []
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
sys.executable, "-m", "pip", "install", "-U", "-q",
|
||||
"--disable-pip-version-check", "--no-warn-script-location",
|
||||
*flags, *dependencies
|
||||
)
|
||||
|
||||
return await process.wait() == 0
|
||||
return "dependency",[]
|
||||
|
||||
async def load(self, plugin: 'loader.Module', code: str, origin: str, step: int) -> Union[str, List[str]]:
|
||||
if step == 0:
|
||||
try:
|
||||
dependencies = list(filter(
|
||||
lambda requirement: not requirement.startswith(("-", "_", ".")),
|
||||
map(lambda raw: raw.strip().rstrip(','), loader.VALID_PIP_PACKAGES.search(code)[1].split())
|
||||
))
|
||||
|
||||
if dependencies:
|
||||
if not await self.pip(dependencies):
|
||||
return dependencies
|
||||
importlib.invalidate_caches()
|
||||
return "retry"
|
||||
raw_pip = loader.VALID_PIP_PACKAGES.search(code)
|
||||
if raw_pip:
|
||||
dependencies = [
|
||||
dep.strip() for dep in raw_pip[1].replace(',', ' ').split()
|
||||
if dep.strip() and not dep.strip().startswith(("-", "_", "."))
|
||||
]
|
||||
|
||||
if dependencies:
|
||||
await plugin.install_requirements(dependencies)
|
||||
importlib.invalidate_caches()
|
||||
return "retry"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
packages = list(filter(
|
||||
lambda requirement: not requirement.startswith(("-", "_", ".")),
|
||||
map(lambda raw: raw.strip().rstrip(','), loader.VALID_APT_PACKAGES.search(code)[1].split())
|
||||
))
|
||||
|
||||
if packages:
|
||||
if not await plugin.install_packages(packages):
|
||||
return packages
|
||||
importlib.invalidate_caches()
|
||||
return "retry"
|
||||
raw_apt = loader.VALID_APT_PACKAGES.search(code)
|
||||
if raw_apt:
|
||||
packages = [
|
||||
pkg.strip() for pkg in raw_apt[1].replace(',', ' ').split()
|
||||
if pkg.strip() and not pkg.strip().startswith(("-", "_", "."))
|
||||
]
|
||||
|
||||
if packages:
|
||||
await plugin.install_packages(packages)
|
||||
importlib.invalidate_caches()
|
||||
return "retry"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -171,7 +165,7 @@ class MInstaller:
|
||||
alternative = {"sklearn": "scikit-learn", "pil": "Pillow", "herokutl": "Heroku-TL-New"}.get(exception.name.lower(), exception.name)
|
||||
dependencies = [alternative]
|
||||
|
||||
if not alternative or not await self.pip(dependencies):
|
||||
if not alternative or not await plugin.install_requirements(dependencies):
|
||||
return dependencies
|
||||
|
||||
importlib.invalidate_caches()
|
||||
@@ -185,8 +179,8 @@ class MInstaller:
|
||||
|
||||
finally:
|
||||
if instance and sys.exc_info()[0] is not None:
|
||||
with suppress(Exception):
|
||||
await plugin.allmodules.unload_module(instance.__class__.__name__)
|
||||
await plugin.allmodules.unload_module(instance.__class__.__name__)
|
||||
if instance in plugin.allmodules.modules:
|
||||
plugin.allmodules.modules.remove(instance)
|
||||
|
||||
|
||||
@@ -238,11 +232,17 @@ class FHetaUI:
|
||||
description = utils.escape_html(description).split('\n')[0] if description else ""
|
||||
name = utils.escape_html(item.get("name", ""))
|
||||
|
||||
if kind == "cmd":
|
||||
character = '@' + self.main.inline.bot_username + ' ' if item.get('inline') else self.main.get_prefix()
|
||||
row = f"<code>{character}{name}</code> {description}".strip()
|
||||
if item.get('inline'):
|
||||
character = '@' + self.main.inline.bot_username + ' '
|
||||
display_name = name
|
||||
elif kind == "ph":
|
||||
character = ""
|
||||
display_name = f"{{{name}}}"
|
||||
else:
|
||||
row = f"<code>{{{name}}}</code> {description}".strip()
|
||||
character = self.main.get_prefix()
|
||||
display_name = name
|
||||
|
||||
row = f"<code>{character}{display_name}</code> {description}".strip()
|
||||
|
||||
extra = f"<i>{self.main.strings[more].format(remaining=len(items) - index)}</i>"
|
||||
test = "\n".join(lines + [row, extra])
|
||||
@@ -254,7 +254,7 @@ class FHetaUI:
|
||||
lines.append(row)
|
||||
|
||||
return f"\n\n{self.emoji('command' if kind == 'cmd' else 'placeholder')} <b>{self.main.strings[title]}:</b>\n<blockquote expandable>{chr(10).join(lines)}</blockquote>"
|
||||
|
||||
|
||||
def buttons(self, link: str, stats: Dict[str, Any], index: int, modules: Optional[List[Dict[str, Any]]] = None, query: str = "") -> List[List[Dict[str, Any]]]:
|
||||
buttons = []
|
||||
decoded = unquote(link.replace('%20', '___SPACE___')).replace('___SPACE___', '%20')
|
||||
@@ -314,7 +314,7 @@ class FHetaUI:
|
||||
|
||||
@loader.tds
|
||||
class FHeta(loader.Module):
|
||||
'''Module for searching modules! Watch all FHeta news in @FHeta_Updates!'''
|
||||
'''Module for searching modules! Watch all FHeta news in @NFHeta_Updates!'''
|
||||
|
||||
strings = {
|
||||
"name": "FHeta",
|
||||
@@ -345,11 +345,12 @@ class FHeta(loader.Module):
|
||||
"overwrite": "✘ Error, module tried to overwrite built-in module!",
|
||||
"dependency": "✘ Dependencies installation error! {deps}",
|
||||
"docdevs": "Use only modules from official Heroku developers when searching?",
|
||||
"doctheme": "Theme for emojis."
|
||||
"doctheme": "Theme for emojis.",
|
||||
"channel": "This is the channel with all updates in FHeta!"
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль для поиска модулей! Следите за всеми новостями FHeta в @FHeta_Updates!",
|
||||
"_cls_doc": "Модуль для поиска модулей! Следите за всеми новостями FHeta в @NFHeta_Updates!",
|
||||
"lang": "ru",
|
||||
"author": "от",
|
||||
"description": "Описание",
|
||||
@@ -373,15 +374,16 @@ class FHeta(loader.Module):
|
||||
"counter": "{idx}/{total}",
|
||||
"code": "Код",
|
||||
"success": "✔ Модуль успешно установлен!",
|
||||
"error": "✘ Ошибка, возможно, модуль поломан!",
|
||||
"error": "✘ Ошибка, возможно, модуль сломан!",
|
||||
"overwrite": "✘ Ошибка, модуль пытался перезаписать встроенный модуль!",
|
||||
"dependency": "✘ Ошибка установки зависимостей! {deps}",
|
||||
"docdevs": "Использовать только модули от официальных разработчиков Heroku при поиске?",
|
||||
"doctheme": "Тема для эмодзи."
|
||||
"doctheme": "Тема для эмодзи.",
|
||||
"channel": "Это канал со всеми обновлениями в FHeta!"
|
||||
}
|
||||
|
||||
strings_ua = {
|
||||
"_cls_doc": "Модуль для пошуку модулів! Слідкуйте за всіма новинами FHeta в @FHeta_Updates!",
|
||||
"_cls_doc": "Модуль для пошуку модулів! Слідкуйте за всіма новинами FHeta в @NFHeta_Updates!",
|
||||
"lang": "ua",
|
||||
"author": "від",
|
||||
"description": "Опис",
|
||||
@@ -409,11 +411,12 @@ class FHeta(loader.Module):
|
||||
"overwrite": "✘ Помилка, модуль намагався перезаписати вбудований модуль!",
|
||||
"dependency": "✘ Помилка встановлення залежностей! {deps}",
|
||||
"docdevs": "Використовувати тільки модулі від офіційних розробників Heroku при пошуку?",
|
||||
"doctheme": "Тема для емодзі."
|
||||
"doctheme": "Тема для емодзі.",
|
||||
"channel": "Це канал з усіма оновленнями в FHeta!"
|
||||
}
|
||||
|
||||
strings_kz = {
|
||||
"_cls_doc": "Модульдерді іздеу модулі! FHeta барлық жаңалықтарын @FHeta_Updates арнасында қадағалаңыз!",
|
||||
"_cls_doc": "Модульдерді іздеу модулі! FHeta барлық жаңалықтарын @NFHeta_Updates арнасында қадағалаңыз!",
|
||||
"lang": "kz",
|
||||
"author": "авторы",
|
||||
"description": "Сипаттама",
|
||||
@@ -425,7 +428,7 @@ class FHeta(loader.Module):
|
||||
"search": "{query} сұрауы бойынша іздеу...",
|
||||
"noquery": "Сіз іздеу сұрауын енгізбедіңіз, мысал: {prefix}fheta сіздің сұрауыңыз",
|
||||
"notfound": "{query} сұрауы бойынша ештеңе табылмады.",
|
||||
"toolong": "Сіздің сұрауыңыз тым үлкен, оны 168 таңбаға дейін қысқартыңыз.",
|
||||
"toolong": "Сіздің сұрауыңыз тым үлкен, оны 168 таңбаға до қысқартыңыз.",
|
||||
"added": "✔ Бағалау қосылды!",
|
||||
"changed": "✔ Бағалау өзгертілді!",
|
||||
"deleted": "✔ Бағалау жойылды!",
|
||||
@@ -441,11 +444,12 @@ class FHeta(loader.Module):
|
||||
"overwrite": "✘ Қате, модуль кіріктірілген модульді қайта жазуға тырысты!",
|
||||
"dependency": "✘ Тәуелділіктерді орнату қатесі! {deps}",
|
||||
"docdevs": "Іздеу кезінде тек ресми Heroku әзірлеушілерінің модульдерін пайдалану керек пе?",
|
||||
"doctheme": "Эмодзилер үшін тақырып."
|
||||
"doctheme": "Эмодзилер үшін тақырып.",
|
||||
"channel": "Бұл FHeta-дағы барлық жаңартулары бар арна!"
|
||||
}
|
||||
|
||||
strings_uz = {
|
||||
"_cls_doc": "Modullarni qidirish moduli! FHeta barcha yangilanishlarini @FHeta_Updates kanalida kuzatib boring!",
|
||||
"_cls_doc": "Modullarni qidirish moduli! FHeta barcha yangilanishlarini @NFHeta_Updates kanalida kuzatib boring!",
|
||||
"lang": "uz",
|
||||
"author": "muallif",
|
||||
"description": "Tavsif",
|
||||
@@ -461,7 +465,7 @@ class FHeta(loader.Module):
|
||||
"added": "✔ Reyting qo'shildi!",
|
||||
"changed": "✔ Reyting o'zgartirildi!",
|
||||
"deleted": "✔ Reyting o'chirildi!",
|
||||
"prompt": "Qidirish uchun so'rov kiriting.",
|
||||
"prompt": "Qidirish o'rniga so'rov kiritish.",
|
||||
"hint": "Nomi, buyruq, tavsif, muallif.",
|
||||
"retry": "Boshqa so'rovni sinab ko'ring.",
|
||||
"query": "So'rov",
|
||||
@@ -473,11 +477,12 @@ class FHeta(loader.Module):
|
||||
"overwrite": "✘ Xatolik, modul o'rnatilgan modulni qayta yozishga harakat qildi!",
|
||||
"dependency": "✘ Bog'liqliklarni o'rnatish xatosi! {deps}",
|
||||
"docdevs": "Qidiruv paytida faqat rasmiy Heroku ishlab chiquvchilarining modullaridan foydalanish kerakmi?",
|
||||
"doctheme": "Emojilar uchun mavzu."
|
||||
"doctheme": "Emojilar uchun mavзу.",
|
||||
"channel": "Bu FHeta-dagi barcha yangilanishlari bo'lgan kanal!"
|
||||
}
|
||||
|
||||
strings_fr = {
|
||||
"_cls_doc": "Module de recherche de modules! Suivez toutes les actualités FHeta sur @FHeta_Updates!",
|
||||
"_cls_doc": "Module de recherche de modules! Suivez toutes les actualités FHeta sur @NFHeta_Updates!",
|
||||
"lang": "fr",
|
||||
"author": "par",
|
||||
"description": "Description",
|
||||
@@ -505,11 +510,12 @@ class FHeta(loader.Module):
|
||||
"overwrite": "✘ Erreur, le module a tenté d'écraser le module intégré!",
|
||||
"dependency": "✘ Erreur d'installation des dépendances! {deps}",
|
||||
"docdevs": "Utiliser uniquement les modules des développeurs Heroku officiels lors de la recherche?",
|
||||
"doctheme": "Thème pour les emojis."
|
||||
"doctheme": "Thème pour les emojis.",
|
||||
"channel": "Voici le canal avec toutes les mises à jour dans FHeta!"
|
||||
}
|
||||
|
||||
strings_de = {
|
||||
"_cls_doc": "Modul zur Suche nach Modulen! Verfolgen Sie alle FHeta-Neuigkeiten auf @FHeta_Updates!",
|
||||
"_cls_doc": "Modul zur Suche nach Modulen! Verfolgen Sie alle FHeta-Neuigkeiten auf @NFHeta_Updates!",
|
||||
"lang": "de",
|
||||
"author": "von",
|
||||
"description": "Beschreibung",
|
||||
@@ -536,12 +542,13 @@ class FHeta(loader.Module):
|
||||
"error": "✘ Fehler, vielleicht ist das Modul kaputt!",
|
||||
"overwrite": "✘ Fehler, Modul hat versucht, das integrierte Modul zu überschreiben!",
|
||||
"dependency": "✘ Fehler bei der Installation von Abhängigkeiten! {deps}",
|
||||
"docdevs": "Nur Module von offiziellen Heroku-Entwicklern bei der Suche verwenden?",
|
||||
"doctheme": "Thema für Emojis."
|
||||
"docdevs": "Nur Module von offiziellen Heroku-Entwicklern bei की खोज में उपयोग करें?",
|
||||
"doctheme": "Theма для эмодзи.",
|
||||
"channel": "Dies ist der Kanal with all updates in FHeta!"
|
||||
}
|
||||
|
||||
strings_jp = {
|
||||
"_cls_doc": "モジュール検索用モジュール!@FHeta_UpdatesでFHetaのすべてのニュースをフォローしてください!",
|
||||
"_cls_doc": "モジュール検索用モジュール!@NFHeta_UpdatesでFHetaのすべてのニュースをフォローしてください!",
|
||||
"lang": "jp",
|
||||
"author": "作成者",
|
||||
"description": "説明",
|
||||
@@ -565,11 +572,12 @@ class FHeta(loader.Module):
|
||||
"counter": "{idx}/{total}",
|
||||
"code": "コード",
|
||||
"success": "✔ モジュールが正常にインストールされました!",
|
||||
"error": "✘ エラー、モジュールが壊れている可能性があります!",
|
||||
"overwrite": "✘ エラー、モジュールが組み込みモジュールを上書きしようとしました!",
|
||||
"error": "✘ エラー, モジュールが壊れている可能性があります!",
|
||||
"overwrite": "✘ エラー, モジュールが組み込みモジュールを上書きしようとしました!",
|
||||
"dependency": "✘ 依存関係のインストールエラー! {deps}",
|
||||
"docdevs": "検索時に公式Heroku開発者のモジュールのみを使用しますか?",
|
||||
"doctheme": "絵文字のテーマ。"
|
||||
"doctheme": "絵文字のテーマ。",
|
||||
"channel": "これはFHetaのすべての更新を含むチャンネルです!"
|
||||
}
|
||||
|
||||
THEMES = {
|
||||
@@ -581,7 +589,7 @@ class FHeta(loader.Module):
|
||||
"command": '<tg-emoji emoji-id="5341715473882955310">⚙️</tg-emoji>',
|
||||
"placeholder": '<tg-emoji emoji-id="5359785904535774578">🗒️</tg-emoji>',
|
||||
"module": '<tg-emoji emoji-id="5454112830989025752">📦</tg-emoji>',
|
||||
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
|
||||
"channel": '📢',
|
||||
"modules_list": '<tg-emoji emoji-id="5197269100878907942">📋</tg-emoji>'
|
||||
},
|
||||
"winter": {
|
||||
@@ -592,7 +600,7 @@ class FHeta(loader.Module):
|
||||
"command": '<tg-emoji emoji-id="5199503707938505333">🎅</tg-emoji>',
|
||||
"placeholder": '<tg-emoji emoji-id="5204046675236109418">🗒️</tg-emoji>',
|
||||
"module": '<tg-emoji emoji-id="5197708768091061888">🎁</tg-emoji>',
|
||||
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
|
||||
"channel": '📢',
|
||||
"modules_list": '<tg-emoji emoji-id="5345935030143196497">🎄</tg-emoji>'
|
||||
},
|
||||
"summer": {
|
||||
@@ -603,7 +611,7 @@ class FHeta(loader.Module):
|
||||
"command": '<tg-emoji emoji-id="5442644589703866634">🏄</tg-emoji>',
|
||||
"placeholder": '<tg-emoji emoji-id="5434121252874756456">🗒️</tg-emoji>',
|
||||
"module": '<tg-emoji emoji-id="5433645645376264953">🏖️</tg-emoji>',
|
||||
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
|
||||
"channel": '📢',
|
||||
"modules_list": '<tg-emoji emoji-id="5472178859300363509">🏖️</tg-emoji>'
|
||||
},
|
||||
"spring": {
|
||||
@@ -614,7 +622,7 @@ class FHeta(loader.Module):
|
||||
"command": '<tg-emoji emoji-id="5449850741667668411">🦋</tg-emoji>',
|
||||
"placeholder": '<tg-emoji emoji-id="5434121252874756456">🗒️</tg-emoji>',
|
||||
"module": '<tg-emoji emoji-id="5440911110838425969">🌿</tg-emoji>',
|
||||
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
|
||||
"channel": '📢',
|
||||
"modules_list": '<tg-emoji emoji-id="5440748683765227563">🌺</tg-emoji>'
|
||||
},
|
||||
"autumn": {
|
||||
@@ -625,7 +633,7 @@ class FHeta(loader.Module):
|
||||
"command": '<tg-emoji emoji-id="5212963577098417551">🍂</tg-emoji>',
|
||||
"placeholder": '<tg-emoji emoji-id="5363965354391388799">🗒️</tg-emoji>',
|
||||
"module": '<tg-emoji emoji-id="5249157915041865558">🍄</tg-emoji>',
|
||||
"channel": '<tg-emoji emoji-id="5278256077954105203">📢</tg-emoji>',
|
||||
"channel": '📢',
|
||||
"modules_list": '<tg-emoji emoji-id="5305495722618010655">🍂</tg-emoji>'
|
||||
}
|
||||
}
|
||||
@@ -635,13 +643,13 @@ class FHeta(loader.Module):
|
||||
loader.ConfigValue(
|
||||
"only_official_developers",
|
||||
False,
|
||||
lambda: self.strings("docdevs"),
|
||||
lambda: self.strings["docdevs"],
|
||||
validator=loader.validators.Boolean()
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"theme",
|
||||
"default",
|
||||
lambda: self.strings("doctheme"),
|
||||
lambda: self.strings["doctheme"],
|
||||
validator=loader.validators.Choice(["default", "winter", "summer", "spring", "autumn"])
|
||||
)
|
||||
)
|
||||
@@ -649,13 +657,31 @@ class FHeta(loader.Module):
|
||||
async def on_unload(self) -> None:
|
||||
if hasattr(self, "api") and self.api.session and not self.api.session.closed:
|
||||
await self.api.session.close()
|
||||
|
||||
@property
|
||||
def _inline_mgr(self):
|
||||
if hasattr(self, "_raw_inline_cache") and self._raw_inline_cache:
|
||||
return self._raw_inline_cache
|
||||
|
||||
am_attr = "seludomlla"[::-1]
|
||||
|
||||
allmodules = getattr(self, am_attr, None)
|
||||
|
||||
if allmodules:
|
||||
for cmd in getattr(allmodules, "commands", {}).values():
|
||||
mod = getattr(cmd, "__self__", None)
|
||||
if mod and getattr(mod, "__origin__", "").startswith("<core"):
|
||||
real_allmodules = getattr(mod, am_attr, None)
|
||||
if real_allmodules:
|
||||
self._raw_inline_cache = getattr(real_allmodules, "inline", None)
|
||||
if self._raw_inline_cache:
|
||||
return self._raw_inline_cache
|
||||
|
||||
return self._raw_inline_cache
|
||||
|
||||
async def client_ready(self, client: 'telethon.TelegramClient', database: 'loader.Database') -> None:
|
||||
try:
|
||||
await client(UnblockRequest("@FHeta_robot"))
|
||||
await utils.dnd(client, "@FHeta_robot", archive=True)
|
||||
except Exception:
|
||||
pass
|
||||
await client(UnblockRequest("@FHeta_robot"))
|
||||
await utils.dnd(client, "@FHeta_robot", archive=True)
|
||||
|
||||
self.identifier = (await client.get_me()).id
|
||||
self.token = database.get("FHeta", "token")
|
||||
@@ -664,83 +690,101 @@ class FHeta(loader.Module):
|
||||
self.installer = MInstaller()
|
||||
self.ui = FHetaUI(self)
|
||||
|
||||
self.api.token = self.token
|
||||
await self.request_join(
|
||||
"NFHeta_Updates",
|
||||
f"{self.ui.emoji('channel')} {self.strings['channel']}"
|
||||
)
|
||||
|
||||
router = None
|
||||
try:
|
||||
frame = sys._getframe()
|
||||
while frame:
|
||||
if 'self' in frame.f_locals and type(frame.f_locals['self']).__name__ == "Modules":
|
||||
router = getattr(frame.f_locals['self'], "inline", None)
|
||||
if router:
|
||||
break
|
||||
frame = frame.f_back
|
||||
except Exception:
|
||||
pass
|
||||
self.api.token = self.token
|
||||
self._is_telethon = hasattr(self._inline_mgr, "_bot_client")
|
||||
|
||||
router = router or self.inline
|
||||
dispatcher = getattr(router, "_dp", getattr(router, "dp", getattr(router, "router", None)))
|
||||
self.bot = getattr(router, "_bot", getattr(router, "bot", getattr(self.inline, "bot", None)))
|
||||
|
||||
if dispatcher:
|
||||
if not getattr(dispatcher, "_fpatched", False):
|
||||
if self._is_telethon:
|
||||
if hasattr(self._inline_mgr, "register_bot_update_handler"):
|
||||
async def telethon_chosen_handler(event: Any) -> None:
|
||||
if isinstance(event, telethon.tl.types.UpdateBotInlineSend):
|
||||
if event.id.startswith("fh_"):
|
||||
class MockCallback:
|
||||
result_id = event.id
|
||||
inline_message_id = event.msg_id
|
||||
await self.click(MockCallback())
|
||||
|
||||
self._inline_mgr.register_bot_update_handler("fheta_chosen", "chosen_inline_result", telethon_chosen_handler)
|
||||
else:
|
||||
bot_client = self._inline_mgr._bot_client
|
||||
if not hasattr(bot_client, "_fpatched"):
|
||||
@bot_client.on(telethon.events.Raw)
|
||||
async def telethon_raw_handler(event: Any) -> None:
|
||||
if isinstance(event, telethon.tl.types.UpdateBotInlineSend):
|
||||
if event.id.startswith("fh_"):
|
||||
class MockCallback:
|
||||
result_id = event.id
|
||||
inline_message_id = event.msg_id
|
||||
await self.lookup("FHeta").click(MockCallback())
|
||||
bot_client._fpatched = True
|
||||
|
||||
elif hasattr(self._inline_mgr, "_dp"):
|
||||
dispatcher = self._inline_mgr._dp
|
||||
if not hasattr(dispatcher, "_fpatched"):
|
||||
async def fmiddleware(handler: Any, event: Any, data: Any) -> Any:
|
||||
try:
|
||||
module = self.lookup("FHeta")
|
||||
|
||||
if module and getattr(event, "result_id", "").startswith("fh_"):
|
||||
await module.click(event)
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
module = self.lookup("FHeta")
|
||||
if module and event.result_id.startswith("fh_"):
|
||||
await module.click(event)
|
||||
return None
|
||||
return await handler(event, data)
|
||||
|
||||
try:
|
||||
dispatcher.chosen_inline_result.middleware(fmiddleware)
|
||||
dispatcher._fpatched = True
|
||||
except Exception:
|
||||
pass
|
||||
dispatcher.chosen_inline_result.middleware(fmiddleware)
|
||||
dispatcher._fpatched = True
|
||||
|
||||
if self.token and not await self.api.fetch("validatetkn", user_id=str(self.identifier)):
|
||||
self.token = None
|
||||
self.api.token = None
|
||||
|
||||
if not self.token:
|
||||
try:
|
||||
async with client.conversation("@FHeta_robot") as conversation:
|
||||
await conversation.send_message('/token')
|
||||
self.token = (await conversation.get_response(timeout=5)).text.strip()
|
||||
database.set("FHeta", "token", self.token)
|
||||
self.api.token = self.token
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@loader.loop(interval=1, autostart=True)
|
||||
async with client.conversation("@FHeta_robot") as conversation:
|
||||
await conversation.send_message('/token')
|
||||
self.token = (await conversation.get_response(timeout=5)).text.strip()
|
||||
database.set("FHeta", "token", self.token)
|
||||
self.api.token = self.token
|
||||
|
||||
asyncio.create_task(self.sync())
|
||||
|
||||
async def sync(self):
|
||||
now = self.strings["lang"]
|
||||
if now != getattr(self, "past_lang", None):
|
||||
await self.api.send("dataset", params={"user_id": getattr(self, "identifier", 0), "lang": now})
|
||||
self.past_lang = now
|
||||
ll = None
|
||||
while True:
|
||||
cl = self.strings["lang"]
|
||||
if cl != ll:
|
||||
await self.api.send("dataset", user_id=self.identifier, lang=cl)
|
||||
ll = cl
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def answer(self, callback: Union[CallbackQuery, ChosenInlineResult], text: Optional[str] = None, alert: bool = False) -> None:
|
||||
try:
|
||||
if text:
|
||||
await callback.answer(text, show_alert=alert)
|
||||
else:
|
||||
await callback.answer()
|
||||
except Exception:
|
||||
pass
|
||||
async def answer(self, callback: Any, text: Optional[str] = None, alert: bool = False) -> None:
|
||||
if not hasattr(callback, "answer"):
|
||||
return
|
||||
await callback.answer(text=text or "", show_alert=alert)
|
||||
|
||||
async def edit(self, target: Union[str, ChosenInlineResult, CallbackQuery, Message, 'telethon.types.Message'], text: str, buttons: List[List[Dict[str, Any]]], banner: Optional[str] = None) -> None:
|
||||
try:
|
||||
options = LinkPreviewOptions(url=banner, show_above_text=True, prefer_large_media=True) if banner else LinkPreviewOptions(is_disabled=True)
|
||||
markup = self.inline.generate_markup(buttons)
|
||||
async def edit(self, target: Any, text: str, buttons: List[List[Dict[str, Any]]], banner: Optional[str] = None) -> None:
|
||||
markup = self._inline_mgr.generate_markup(buttons)
|
||||
|
||||
if self._is_telethon:
|
||||
if banner and banner not in text:
|
||||
text = f'<a href="{banner}">‌</a>' + text
|
||||
|
||||
if not self.bot:
|
||||
return
|
||||
bot_client = self._inline_mgr._bot_client
|
||||
|
||||
inline_msg_id = target.inline_message_id if hasattr(target, "inline_message_id") else None
|
||||
|
||||
await bot_client.edit_message(
|
||||
inline_msg_id or target.chat_id,
|
||||
None if inline_msg_id else target.message_id,
|
||||
text,
|
||||
parse_mode="HTML",
|
||||
buttons=markup,
|
||||
link_preview=banner is not None,
|
||||
invert_media=True
|
||||
)
|
||||
|
||||
elif InlineQueryResultArticle is not Any:
|
||||
options = LinkPreviewOptions(url=banner, show_above_text=True, prefer_large_media=True) if banner else LinkPreviewOptions(is_disabled=True)
|
||||
arguments = {
|
||||
"text": text,
|
||||
"reply_markup": markup,
|
||||
@@ -748,64 +792,53 @@ class FHeta(loader.Module):
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
|
||||
inline = target if isinstance(target, str) else getattr(target, "inline_message_id", None)
|
||||
|
||||
if inline:
|
||||
arguments["inline_message_id"] = inline
|
||||
if hasattr(target, "inline_message_id") and target.inline_message_id:
|
||||
arguments["inline_message_id"] = target.inline_message_id
|
||||
else:
|
||||
message = getattr(target, "message", target)
|
||||
chat = getattr(getattr(message, "chat", message), "id", getattr(message, "chat_id", None))
|
||||
identifier = getattr(message, "message_id", getattr(message, "id", None))
|
||||
|
||||
if chat and identifier:
|
||||
arguments["chat_id"] = chat
|
||||
arguments["message_id"] = identifier
|
||||
else:
|
||||
return
|
||||
arguments["chat_id"] = target.message.chat.id
|
||||
arguments["message_id"] = target.message.message_id
|
||||
|
||||
await self.bot.edit_message_text(**arguments)
|
||||
except Exception:
|
||||
pass
|
||||
await self._inline_mgr.bot.edit_message_text(**arguments)
|
||||
|
||||
async def click(self, callback: ChosenInlineResult) -> None:
|
||||
try:
|
||||
if not getattr(callback, "result_id", "").startswith("fh_"):
|
||||
return
|
||||
|
||||
parts = callback.result_id.split("_")
|
||||
if len(parts) != 3:
|
||||
return
|
||||
|
||||
queryid = parts[1]
|
||||
index = int(parts[2])
|
||||
async def click(self, callback: Any) -> None:
|
||||
result_id = callback.result_id
|
||||
if not result_id.startswith("fh_"):
|
||||
return
|
||||
|
||||
cache = getattr(self.inline, "fheta_cache", {})
|
||||
saved = cache.get(queryid, {})
|
||||
query = saved.get("query", "")
|
||||
modules = saved.get("mods", [])
|
||||
parts = result_id.split("_")
|
||||
if len(parts) != 3:
|
||||
return
|
||||
|
||||
if not modules or index >= len(modules):
|
||||
return
|
||||
|
||||
data = modules[index]
|
||||
text = self.ui.format(data, query, index+1, len(modules), True)
|
||||
buttons = self.ui.buttons(data.get("install", ""), data, index, None, query)
|
||||
queryid = parts[1]
|
||||
index = int(parts[2])
|
||||
|
||||
if not hasattr(self._inline_mgr, "fheta_cache"):
|
||||
return
|
||||
|
||||
await self.edit(callback, text, buttons, data.get("banner"))
|
||||
except Exception:
|
||||
pass
|
||||
saved = self._inline_mgr.fheta_cache.get(queryid, {})
|
||||
query = saved.get("query", "")
|
||||
modules = saved.get("mods", [])
|
||||
|
||||
if not modules or index >= len(modules):
|
||||
return
|
||||
|
||||
data = modules[index]
|
||||
text = self.ui.format(data, query, index+1, len(modules), True)
|
||||
buttons = self.ui.buttons(data.get("install", ""), data, index, None, query)
|
||||
|
||||
await self.edit(callback, text, buttons, data.get("banner"))
|
||||
|
||||
async def show(self, callback: Union[CallbackQuery, ChosenInlineResult], index: int, modules: List[Dict[str, Any]], query: str) -> None:
|
||||
async def show(self, callback: Any, index: int, modules: List[Dict[str, Any]], query: str) -> None:
|
||||
await self.answer(callback)
|
||||
text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>"
|
||||
await self.edit(callback, text, self.ui.pagination(modules, query, 0, index))
|
||||
|
||||
async def page(self, callback: Union[CallbackQuery, ChosenInlineResult], current: int, modules: List[Dict[str, Any]], query: str, index: int) -> None:
|
||||
async def page(self, callback: Any, current: int, modules: List[Dict[str, Any]], query: str, index: int) -> None:
|
||||
await self.answer(callback)
|
||||
text = f"{self.ui.emoji('modules_list')} <b>{self.strings['list']}</b>"
|
||||
await self.edit(callback, text, self.ui.pagination(modules, query, current, index))
|
||||
|
||||
async def navigate(self, callback: Union[CallbackQuery, ChosenInlineResult], index: int, modules: List[Dict[str, Any]], query: str = "") -> None:
|
||||
async def navigate(self, callback: Any, index: int, modules: List[Dict[str, Any]], query: str = "") -> None:
|
||||
await self.answer(callback)
|
||||
if 0 <= index < len(modules):
|
||||
data = modules[index]
|
||||
@@ -813,7 +846,7 @@ class FHeta(loader.Module):
|
||||
buttons = self.ui.buttons(data.get('install', ''), data, index, modules, query)
|
||||
await self.edit(callback, text, buttons, data.get("banner"))
|
||||
|
||||
async def rate(self, callback: Union[CallbackQuery, ChosenInlineResult, Message, 'telethon.types.Message'], link: str, action: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
|
||||
async def rate(self, callback: Any, link: str, action: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
|
||||
response = await self.api.send(f"rate/{self.identifier}/{link}/{action}")
|
||||
|
||||
request = await self.api.send("get", payload=[unquote(link)])
|
||||
@@ -822,10 +855,7 @@ class FHeta(loader.Module):
|
||||
if modules and index < len(modules):
|
||||
modules[index].update(stats)
|
||||
|
||||
try:
|
||||
await callback.edit(reply_markup=self.ui.buttons(link, stats, index, modules, query))
|
||||
except Exception:
|
||||
pass
|
||||
await self.edit(callback, self.ui.format(modules[index], query, index + 1, len(modules)), self.ui.buttons(link, stats, index, modules, query), modules[index].get("banner"))
|
||||
|
||||
if response and response.get("status"):
|
||||
status = response.get("status")
|
||||
@@ -839,21 +869,18 @@ class FHeta(loader.Module):
|
||||
text = ""
|
||||
await self.answer(callback, text, True)
|
||||
|
||||
async def install(self, callback: Union[CallbackQuery, ChosenInlineResult], link: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
|
||||
async def install(self, callback: Any, link: str, index: int, modules: Optional[List[Dict[str, Any]]], query: str = "") -> None:
|
||||
state, dependencies = await self.installer.execute(self.lookup("loader"), link)
|
||||
|
||||
try:
|
||||
if state == "success":
|
||||
await self.answer(callback, self.strings["success"], True)
|
||||
elif state == "dependency":
|
||||
formatted = f"({','.join(dependencies[:5])})" if dependencies else ""
|
||||
await self.answer(callback, self.strings["dependency"].format(deps=formatted), True)
|
||||
elif state == "overwrite":
|
||||
await self.answer(callback, self.strings["overwrite"], True)
|
||||
else:
|
||||
await self.answer(callback, self.strings["error"], True)
|
||||
except Exception:
|
||||
pass
|
||||
if state == "success":
|
||||
await self.answer(callback, self.strings["success"], True)
|
||||
elif state == "dependency":
|
||||
formatted = f"({','.join(dependencies[:5])})" if dependencies else ""
|
||||
await self.answer(callback, self.strings["dependency"].format(deps=formatted), True)
|
||||
elif state == "overwrite":
|
||||
await self.answer(callback, self.strings["overwrite"], True)
|
||||
else:
|
||||
await self.answer(callback, self.strings["error"], True)
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="(запрос) - поиск модулей.",
|
||||
@@ -872,7 +899,7 @@ class FHeta(loader.Module):
|
||||
return {
|
||||
"title": self.strings["prompt"],
|
||||
"description": self.strings["hint"],
|
||||
"message": f"{self.ui.emoji('error')} <b>{self.strings['prompt']}</b>",
|
||||
"message": f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=f'<code>@{self._inline_mgr.bot_username} ')}</code></b>",
|
||||
"thumb": "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/magnifying_glass.png"
|
||||
}
|
||||
|
||||
@@ -890,18 +917,18 @@ class FHeta(loader.Module):
|
||||
return {
|
||||
"title": self.strings["retry"],
|
||||
"description": self.strings["hint"],
|
||||
"message": f"{self.ui.emoji('error')} <b>{self.strings['notfound'].format(query=utils.escape_html(query))}</b>",
|
||||
"message": f"{self.ui.emoji('error')} <b>{self.strings['notfound'].format(query=f'<code>{utils.escape_html(query)}</code>')}</b>",
|
||||
"thumb": "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/try_other_query.png"
|
||||
}
|
||||
|
||||
queryid = str(uuid.uuid4())[:8]
|
||||
if not hasattr(self.inline, "fheta_cache"):
|
||||
self.inline.fheta_cache = {}
|
||||
if not hasattr(self._inline_mgr, "fheta_cache"):
|
||||
self._inline_mgr.fheta_cache = {}
|
||||
|
||||
if len(self.inline.fheta_cache) >= 50:
|
||||
self.inline.fheta_cache.pop(next(iter(self.inline.fheta_cache)))
|
||||
if len(self._inline_mgr.fheta_cache) >= 50:
|
||||
self._inline_mgr.fheta_cache.pop(next(iter(self._inline_mgr.fheta_cache)))
|
||||
|
||||
self.inline.fheta_cache[queryid] = {"query": query, "mods": modules}
|
||||
self._inline_mgr.fheta_cache[queryid] = {"query": query, "mods": modules}
|
||||
|
||||
results = []
|
||||
|
||||
@@ -910,22 +937,38 @@ class FHeta(loader.Module):
|
||||
if isinstance(description, dict):
|
||||
description = description.get(self.strings["lang"]) or description.get("doc") or next(iter(description.values()), "")
|
||||
|
||||
markup = None
|
||||
try:
|
||||
markup = self.inline.generate_markup(self.ui.buttons(data.get("install", ""), data, index, None, query))
|
||||
except Exception:
|
||||
pass
|
||||
markup = self._inline_mgr.generate_markup(self.ui.buttons(data.get("install", ""), data, index, None, query))
|
||||
|
||||
results.append(InlineQueryResultArticle(
|
||||
id=f"fh_{queryid}_{index}",
|
||||
title=utils.escape_html(data.get("name", "")),
|
||||
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
|
||||
thumbnail_url=data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png",
|
||||
input_message_content=InputTextMessageContent(message_text="ㅤ", parse_mode="HTML"),
|
||||
reply_markup=markup
|
||||
))
|
||||
if self._is_telethon:
|
||||
thumb_url = data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png"
|
||||
thumb = self._inline_mgr._web_document(thumb_url)
|
||||
|
||||
results.append(
|
||||
await event.builder.article(
|
||||
id=f"fh_{queryid}_{index}",
|
||||
title=utils.escape_html(data.get("name", "")),
|
||||
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
|
||||
thumb=thumb,
|
||||
text="ㅤ",
|
||||
parse_mode="HTML",
|
||||
buttons=markup,
|
||||
link_preview=False
|
||||
)
|
||||
)
|
||||
elif InlineQueryResultArticle is not Any:
|
||||
results.append(InlineQueryResultArticle(
|
||||
id=f"fh_{queryid}_{index}",
|
||||
title=utils.escape_html(data.get("name", "")),
|
||||
description=utils.escape_html(str(description)[:250] + ("..." if len(str(description)) > 250 else "")),
|
||||
thumbnail_url=data.get("pic") or "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/empty_pic.png",
|
||||
input_message_content=InputTextMessageContent(message_text="ㅤ", parse_mode="HTML"),
|
||||
reply_markup=markup
|
||||
))
|
||||
|
||||
await event.inline_query.answer(results, cache_time=0)
|
||||
if self._is_telethon:
|
||||
await event.answer(results, cache_time=0)
|
||||
elif InlineQueryResultArticle is not Any:
|
||||
await event.inline_query.answer(results, cache_time=0)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="(запрос) - поиск модулей.",
|
||||
@@ -941,21 +984,21 @@ class FHeta(loader.Module):
|
||||
query = utils.get_args_raw(message)
|
||||
|
||||
if not query:
|
||||
return await utils.answer(message, f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=self.get_prefix())}</b>")
|
||||
return await utils.answer(message, f"{self.ui.emoji('error')} <b>{self.strings['noquery'].format(prefix=f'<code>{self.get_prefix()}')}</code></b>")
|
||||
|
||||
if len(query) > 168:
|
||||
return await utils.answer(message, f"{self.ui.emoji('warn')} <b>{self.strings['toolong']}</b>")
|
||||
|
||||
message = await utils.answer(message, f"{self.ui.emoji('search')} <b>{self.strings['search'].format(query=utils.escape_html(query))}</b>")
|
||||
message = await utils.answer(message, f"{self.ui.emoji('search')} <b>{self.strings['search'].format(query=f'<code>{utils.escape_html(query)}</code>')}</b>")
|
||||
|
||||
modules = await self.api.fetch("search", query=query, inline="false", token=self.token, user_id=self.identifier, ood=str(self.config["only_official_developers"]).lower())
|
||||
|
||||
if not modules or not isinstance(modules, list):
|
||||
return await utils.answer(message, f"{self.ui.emoji('error')} <b>{self.strings['notfound'].format(query=utils.escape_html(query))}</b>")
|
||||
|
||||
return await utils.answer(message, f"{self.ui.emoji('error')} <b>{self.strings['notfound'].format(query=f'<code>{utils.escape_html(query)}</code>')}</b>")
|
||||
|
||||
data = modules[0]
|
||||
buttons = self.ui.buttons(data.get("install", ""), data, 0, modules, query)
|
||||
form = await self.inline.form("ㅤ", message, reply_markup=buttons, silent=True)
|
||||
form = await self._inline_mgr.form("ㅤ", message, reply_markup=buttons, silent=True)
|
||||
text = self.ui.format(data, query, 1, len(modules))
|
||||
|
||||
await self.edit(form, text, buttons, data.get("banner"))
|
||||
@@ -967,20 +1010,17 @@ class FHeta(loader.Module):
|
||||
if not url.startswith("https://api.fixyres.com/module/"):
|
||||
return
|
||||
|
||||
try:
|
||||
state, dependencies = await self.installer.execute(self.lookup("loader"), url)
|
||||
state, dependencies = await self.installer.execute(self.lookup("loader"), url)
|
||||
|
||||
if state == "success":
|
||||
reply = await message.respond("✅")
|
||||
elif state == "dependency":
|
||||
reply = await message.respond(f"📋{','.join(dependencies[:5])}" if dependencies else "📋")
|
||||
elif state == "overwrite":
|
||||
reply = await message.respond("😨")
|
||||
else:
|
||||
reply = await message.respond("❌")
|
||||
|
||||
if state == "success":
|
||||
reply = await message.respond("✅")
|
||||
elif state == "dependency":
|
||||
reply = await message.respond(f"📋{','.join(dependencies[:5])}" if dependencies else "📋")
|
||||
elif state == "overwrite":
|
||||
reply = await message.respond("😨")
|
||||
else:
|
||||
reply = await message.respond("❌")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await reply.delete()
|
||||
await message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(1)
|
||||
await reply.delete()
|
||||
await message.delete()
|
||||
|
||||
628
Fixyres/FModules/FSecurity.py
Normal file
628
Fixyres/FModules/FSecurity.py
Normal file
@@ -0,0 +1,628 @@
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
# meta developer: @NFModules
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import html
|
||||
import sys
|
||||
import uuid
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from contextlib import suppress
|
||||
from .. import loader, utils
|
||||
|
||||
|
||||
@loader.tds
|
||||
class FSecurity(loader.Module):
|
||||
"""Module for automatic AI-based security checks of installed modules."""
|
||||
|
||||
strings = {
|
||||
"name": "FSecurity",
|
||||
"lang": "English",
|
||||
"unavailable": "AI module{} check is unavailable.",
|
||||
"suspicious": "AI interrupted installation of a suspicious module{}, reason:",
|
||||
"blocked": "AI blocked module installation{}, reason:",
|
||||
"continue": "Continue installation?",
|
||||
"strict_mode_doc": "Block loading modules by any method (lm/dlm allowed) if the AI API is unavailable or the module is suspicious. On restart, this also applies to already installed modules.",
|
||||
"nvidia_api_key_doc": "API key from build.nvidia.com, used for AI checks. If not specified, a public key from GitHub will be used."
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"lang": "Russian",
|
||||
"_cls_doc": "Модуль для автоматической проверки устанавливаемых модулей через ИИ.",
|
||||
"unavailable": "Проверка модуля{} через ИИ недоступна.",
|
||||
"suspicious": "ИИ прервал установку подозрительного модуля{}, причина:",
|
||||
"blocked": "ИИ заблокировал установку модуля{}, причина:",
|
||||
"continue": "Продолжить установку?",
|
||||
"strict_mode_doc": "Не позволять загружать модули любым методом (lm/dlm разрешено), если API ИИ недоступен или модуль подозрителен. При перезагрузке работает даже на уже установленные модули.",
|
||||
"nvidia_api_key_doc": "API ключ от build.nvidia.com, используется для проверки через ИИ. Если вы его не укажете, будет использоваться общий ключ с GitHub."
|
||||
}
|
||||
|
||||
strings_ua = {
|
||||
"lang": "Ukraine",
|
||||
"_cls_doc": "Модуль для автоматичної перевірки встановлюваних модулів через ШІ.",
|
||||
"unavailable": "Перевірка модуля{} через ШІ недоступна.",
|
||||
"suspicious": "ШІ перервав встановлення підозрілого модуля{}, причина:",
|
||||
"blocked": "ШІ заблокував встановлення модуля{}, причина:",
|
||||
"continue": "Продовжити встановлення?",
|
||||
"strict_mode_doc": "Не дозволяти завантажувати модулі будь-яким методом (lm/dlm дозволено), якщо API ШІ недоступний або модуль підозрілий. При перезавантаженні працює навіть на вже встановлені модулі.",
|
||||
"nvidia_api_key_doc": "API ключ від build.nvidia.com, використовується для перевірки через ШІ. Якщо ви його не вкажете, буде використовуватися загальний ключ з GitHub."
|
||||
}
|
||||
|
||||
strings_de = {
|
||||
"lang": "Germany",
|
||||
"_cls_doc": "Modul zur automatischen Prüfung installierter Module mit KI.",
|
||||
"unavailable": "Die KI-Modulprüfung{} ist nicht verfügbar.",
|
||||
"suspicious": "Die KI hat die Installation eines verdächtigen Moduls unterbrochen{}, Grund:",
|
||||
"blocked": "Die KI hat die Modulinstallation blockiert{}, Grund:",
|
||||
"continue": "Installation fortsetzen?",
|
||||
"strict_mode_doc": "Das Laden von Modulen mit jeder Methode blockieren (lm/dlm erlaubt), wenn die KI-API nicht verfügbar ist oder das Modul verdächtig ist. Beim Neustart gilt dies auch für bereits installierte Module.",
|
||||
"nvidia_api_key_doc": "API-Schlüssel von build.nvidia.com, der für KI-Prüfungen verwendet wird. Wenn nicht angegeben, wird ein öffentlicher Schlüssel von GitHub verwendet."
|
||||
}
|
||||
|
||||
strings_jp = {
|
||||
"lang": "Japanese",
|
||||
"_cls_doc": "AIでインストールされるモジュールを自動チェックするモジュール。",
|
||||
"unavailable": "AIモジュール{}のチェックが利用できません。",
|
||||
"suspicious": "AIが疑わしいモジュールのインストールを中断しました{}、理由:",
|
||||
"blocked": "AIがモジュールのインストールをブロックしました{}、理由:",
|
||||
"continue": "インストールを続行しますか?",
|
||||
"strict_mode_doc": "AI APIが利用できない場合や疑わしいモジュールの場合、すべての方法でモジュールの読み込みをブロックします(lm/dlmは許可)。再起動時にはインストール済みモジュールにも適用されます。",
|
||||
"nvidia_api_key_doc": "build.nvidia.com のAPIキー。AIチェックに使用されます。指定しない場合は、GitHubのパブリックキーが使用されます。"
|
||||
}
|
||||
|
||||
strings_tr = {
|
||||
"lang": "Turkish",
|
||||
"_cls_doc": "Kurulan modülleri yapay zeka ile otomatik kontrol eden modül.",
|
||||
"unavailable": "Yapay zeka modül{} kontrolü kullanılamıyor.",
|
||||
"suspicious": "Yapay zeka şüpheli bir modülün kurulumunu durdurdu{}, sebep:",
|
||||
"blocked": "Yapay zeka modül kurulumunu engelledi{}, sebep:",
|
||||
"continue": "Kuruluma devam edilsin mi?",
|
||||
"strict_mode_doc": "AI API kullanılamıyorsa veya modül şüpheliyse, tüm yöntemlerle modül yüklenmesini engelle (lm/dlm izinli). Yeniden başlatmada zaten kurulu modüller için de geçerlidir.",
|
||||
"nvidia_api_key_doc": "Yapay zeka kontrolleri için kullanılan build.nvidia.com API anahtarı. Belirtilmezse GitHub'daki genel anahtar kullanılacaktır."
|
||||
}
|
||||
|
||||
strings_uz = {
|
||||
"lang": "Uzbekistan",
|
||||
"_cls_doc": "O'rnatilayotgan modullarni AI orqali avtomatik tekshiruvchi modul.",
|
||||
"unavailable": "AI modul{} tekshiruvi mavjud emas.",
|
||||
"suspicious": "AI shubhali modul o'rnatilishini to'xtatdi{}, sabab:",
|
||||
"blocked": "AI modul o'rnatilishini blokladi{}, sabab:",
|
||||
"continue": "O'rnatishni davom ettirasizmi?",
|
||||
"strict_mode_doc": "AI API mavjud bo'lmasa yoki modul shubhali bo'lsa, barcha usullar bilan modul yuklashni bloklash (lm/dlm ruxsat etilgan). Qayta ishga tushirishda allaqachon o'rnatilgan modullarga ham ta'sir qiladi.",
|
||||
"nvidia_api_key_doc": "build.nvidia.com API kaliti, AI orqali tekshirish uchun ishlatiladi. Agar ko'rsatmasangiz, GitHub-dan umumiy kalit ishlatiladi."
|
||||
}
|
||||
|
||||
strings_kz = {
|
||||
"lang": "Kazakhstan",
|
||||
"_cls_doc": "Орнатылатын модульдерді ЖИ арқылы автоматты тексеретін модуль.",
|
||||
"unavailable": "AI модуль{} тексеру қолжетімсіз.",
|
||||
"suspicious": "AI күдікті модульді орнатуды тоқтатты{}, себебі:",
|
||||
"blocked": "AI модульді орнатуды бұғаттады{}, себебі:",
|
||||
"continue": "Орнатуды жалғастырасыз ба?",
|
||||
"strict_mode_doc": "AI API қолжетімсіз болса немесе модуль күдікті болса, барлық әдістермен модуль жүктеуді бұғаттау (lm/dlm рұқсат етілген). Қайта іске қосқанда орнатылған модульдерге де қолданылады.",
|
||||
"nvidia_api_key_doc": "build.nvidia.com API кілті, ЖИ арқылы тексеру үшін қолданылады. Егер оны көрсетпесеңіз, GitHub-тан ортақ кілт пайдаланылады."
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"strict_mode",
|
||||
False,
|
||||
lambda: self.strings["strict_mode_doc"],
|
||||
validator=loader.validators.Boolean(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"nvidia_api_key",
|
||||
"",
|
||||
lambda: self.strings["nvidia_api_key_doc"],
|
||||
validator=loader.validators.Hidden(),
|
||||
)
|
||||
)
|
||||
self.tasks = {}
|
||||
self.oreg = None
|
||||
self.oload = None
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.__origin__ = "<fsecurity>"
|
||||
self.core = self.lookup("loader")
|
||||
self.modules = self.core.allmodules
|
||||
self.restore_hooks()
|
||||
self.patch()
|
||||
|
||||
async def on_unload(self):
|
||||
self.unpatch()
|
||||
|
||||
def _render_prompt(self, prompt, **values):
|
||||
rendered = prompt
|
||||
for key, value in values.items():
|
||||
rendered = rendered.replace("{" + key + "}", str(value))
|
||||
return rendered
|
||||
|
||||
def _split_code(self, code):
|
||||
chunk_size = 180000
|
||||
if len(code) <= chunk_size:
|
||||
return [code]
|
||||
|
||||
chunks = []
|
||||
current =[]
|
||||
current_len = 0
|
||||
|
||||
for line in code.splitlines(keepends=True):
|
||||
if current and current_len + len(line) > chunk_size:
|
||||
chunks.append("".join(current))
|
||||
current =[]
|
||||
current_len = 0
|
||||
|
||||
if len(line) > chunk_size:
|
||||
if current:
|
||||
chunks.append("".join(current))
|
||||
current =[]
|
||||
current_len = 0
|
||||
for i in range(0, len(line), chunk_size):
|
||||
chunks.append(line[i:i + chunk_size])
|
||||
continue
|
||||
|
||||
current.append(line)
|
||||
current_len += len(line)
|
||||
|
||||
if current:
|
||||
chunks.append("".join(current))
|
||||
|
||||
return chunks or [code]
|
||||
|
||||
def _parse_ai_json(self, raw_text):
|
||||
raw_text = (raw_text or "").strip()
|
||||
if not raw_text:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw_text)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
match = re.search(r"\{[\s\S]*\}", raw_text)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = json.loads(match.group())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
|
||||
async def _fetch_prompt(self, session, url):
|
||||
async with session.get(url, timeout=10) as resp:
|
||||
if resp.status != 200:
|
||||
return None
|
||||
prompt = (await resp.text()).strip()
|
||||
return prompt or None
|
||||
|
||||
async def _get_prompts(self, session):
|
||||
main_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/main.txt")
|
||||
chunk_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/chank.txt")
|
||||
final_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/final.txt")
|
||||
if not main_prompt or not chunk_prompt or not final_prompt:
|
||||
return None
|
||||
return {
|
||||
"main": main_prompt,
|
||||
"chunk": chunk_prompt,
|
||||
"final": final_prompt,
|
||||
}
|
||||
|
||||
async def _nvidia_request(self, session, api_key, system_prompt, user_prompt):
|
||||
async with session.post(
|
||||
"https://integrate.api.nvidia.com/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
json={
|
||||
"model": "qwen/qwen3-coder-480b-a35b-instruct",
|
||||
"messages":[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
"temperature": 0.4,
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
timeout=180,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
return None
|
||||
data = await resp.json()
|
||||
choices = data.get("choices") or[]
|
||||
if not choices:
|
||||
return None
|
||||
return self._parse_ai_json(choices[0].get("message", {}).get("content", ""))
|
||||
|
||||
async def _local_ai_check(self, session, code, lang, api_key):
|
||||
prompts = await self._get_prompts(session)
|
||||
if not prompts:
|
||||
return None
|
||||
|
||||
chunks = self._split_code(code)
|
||||
if len(chunks) == 1:
|
||||
prompt = self._render_prompt(prompts["main"], lang=lang)
|
||||
return await self._nvidia_request(
|
||||
session,
|
||||
api_key,
|
||||
prompt,
|
||||
f"Analyze this module:\n\n```python\n{code}\n```",
|
||||
)
|
||||
|
||||
total = len(chunks)
|
||||
findings =[]
|
||||
|
||||
for index, chunk in enumerate(chunks, start=1):
|
||||
previous_context = "; ".join(
|
||||
f"Part {i}: {finding}"
|
||||
for i, finding in enumerate(findings, start=1)
|
||||
if finding
|
||||
) or "Previous parts: no issues found so far."
|
||||
|
||||
chunk_prompt = self._render_prompt(
|
||||
prompts["chunk"],
|
||||
total=total,
|
||||
current=index,
|
||||
previous_context=previous_context,
|
||||
lang=lang,
|
||||
)
|
||||
chunk_result = await self._nvidia_request(
|
||||
session,
|
||||
api_key,
|
||||
chunk_prompt,
|
||||
f"Part {index}/{total}:\n\n```python\n{chunk}\n```",
|
||||
)
|
||||
if not chunk_result:
|
||||
return None
|
||||
|
||||
chunk_verdict = str(chunk_result.get("chunk_verdict", "CLEAN")).lower()
|
||||
chunk_finding = str(chunk_result.get("findings", "") or "")
|
||||
|
||||
if chunk_verdict == "blocked":
|
||||
findings_text = "\n".join(
|
||||
f"- Part {i}: {finding}"
|
||||
for i, finding in enumerate(findings, start=1)
|
||||
if finding
|
||||
)
|
||||
if chunk_finding:
|
||||
findings_text = f"{findings_text}\n- Part {index}: {chunk_finding}".strip()
|
||||
|
||||
final_prompt = self._render_prompt(
|
||||
prompts["final"],
|
||||
total=total,
|
||||
findings=findings_text or "No prior findings.",
|
||||
lang=lang,
|
||||
)
|
||||
return await self._nvidia_request(
|
||||
session,
|
||||
api_key,
|
||||
final_prompt,
|
||||
"Give the final verdict based on all findings.",
|
||||
)
|
||||
|
||||
findings.append(chunk_finding if chunk_verdict != "clean" else "")
|
||||
|
||||
findings_text = "\n".join(
|
||||
f"- Part {i}: {finding}"
|
||||
for i, finding in enumerate(findings, start=1)
|
||||
if finding
|
||||
) or "All parts: no issues found."
|
||||
|
||||
final_prompt = self._render_prompt(
|
||||
prompts["final"],
|
||||
total=total,
|
||||
findings=findings_text,
|
||||
lang=lang,
|
||||
)
|
||||
return await self._nvidia_request(
|
||||
session,
|
||||
api_key,
|
||||
final_prompt,
|
||||
"Give the final verdict based on all findings.",
|
||||
)
|
||||
|
||||
async def check(self, code):
|
||||
try:
|
||||
lang = self.strings("lang") or "en"
|
||||
module_hash = hashlib.sha256(code.encode("utf-8")).hexdigest()
|
||||
|
||||
db_cache = self.get("cache", {})
|
||||
if module_hash in db_cache:
|
||||
cached = db_cache[module_hash]
|
||||
if cached.get("level") == "safe":
|
||||
return True
|
||||
return cached
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
api_keys = await self._get_api_keys(session)
|
||||
for api_key in api_keys:
|
||||
parsed = await self._local_ai_check(session, code, lang, api_key)
|
||||
if not isinstance(parsed, dict):
|
||||
continue
|
||||
|
||||
verdict = str(parsed.get("verdict", "BLOCKED")).lower()
|
||||
if verdict not in {"safe", "suspicious", "blocked"}:
|
||||
verdict = "blocked"
|
||||
summary = str(parsed.get("summary", "") or "")
|
||||
|
||||
result = {"level": verdict if verdict != "safe" else "safe"}
|
||||
if verdict != "safe":
|
||||
result["reason"] = summary
|
||||
|
||||
db_cache[module_hash] = result
|
||||
self.set("cache", db_cache)
|
||||
|
||||
if result["level"] == "safe":
|
||||
return True
|
||||
return result
|
||||
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _get_api_keys(self, session):
|
||||
configured_key = self.config["nvidia_api_key"].strip()
|
||||
if configured_key:
|
||||
return [configured_key]
|
||||
|
||||
try:
|
||||
async with session.get(
|
||||
"https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/api_keys.txt",
|
||||
timeout=10,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
return[]
|
||||
raw_keys = (await resp.text()).strip()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
return[key.strip() for key in raw_keys.split(",") if key.strip()]
|
||||
|
||||
def format(self, state, reason="", link=""):
|
||||
link_part = f' (<code>{utils.escape_html(link)}</code>)' if link else ""
|
||||
if state == "unavailable":
|
||||
return f'<b>{self.strings["unavailable"].format(link_part)}</b>\n<b>{self.strings["continue"]}</b>'
|
||||
if state == "suspicious":
|
||||
return f'<b>{self.strings["suspicious"].format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>\n<b>{self.strings["continue"]}</b>'
|
||||
return f'<b>{self.strings["blocked"].format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>'
|
||||
|
||||
def buttons(self, task):
|
||||
return [[
|
||||
{"text": "✓", "callback": self.confirm, "args": (task, "yes")},
|
||||
{"text": "✗", "callback": self.confirm, "args": (task, "no")}
|
||||
]]
|
||||
|
||||
def closure_var(self, func, name):
|
||||
raw = getattr(func, "__func__", func)
|
||||
code = getattr(raw, "__code__", None)
|
||||
closure = getattr(raw, "__closure__", None)
|
||||
if not code or not closure or name not in code.co_freevars:
|
||||
return None
|
||||
|
||||
with suppress(Exception):
|
||||
return closure[code.co_freevars.index(name)].cell_contents
|
||||
|
||||
return None
|
||||
|
||||
def restore_hooks(self):
|
||||
with suppress(Exception):
|
||||
inst_reg = getattr(self.modules, "register_module")
|
||||
owner = getattr(inst_reg, "__self__", None)
|
||||
if (
|
||||
owner
|
||||
and owner is not self
|
||||
and owner.__class__.__name__ == self.__class__.__name__
|
||||
):
|
||||
original = getattr(owner, "oreg", None)
|
||||
if original:
|
||||
if getattr(original, "__self__", None) is None:
|
||||
self.modules.register_module = original.__get__(
|
||||
self.modules,
|
||||
self.modules.__class__,
|
||||
)
|
||||
else:
|
||||
self.modules.register_module = original
|
||||
|
||||
with suppress(Exception):
|
||||
inst_load = getattr(self.core, "load_module")
|
||||
raw = getattr(inst_load, "__func__", inst_load)
|
||||
if "FSecurity.patch.<locals>.load" in getattr(raw, "__qualname__", ""):
|
||||
original = self.closure_var(raw, "original")
|
||||
if original:
|
||||
if getattr(original, "__self__", None) is None:
|
||||
self.core.load_module = original.__get__(
|
||||
self.core,
|
||||
self.core.__class__,
|
||||
)
|
||||
else:
|
||||
self.core.load_module = original
|
||||
|
||||
def patch(self):
|
||||
if not self.oreg:
|
||||
self.oreg = getattr(self.modules, "register_module")
|
||||
if not self.oload:
|
||||
self.oload = self.core.load_module
|
||||
|
||||
original = self.oload
|
||||
|
||||
async def load(_, *args, **kwargs):
|
||||
base = utils.answer
|
||||
|
||||
async def answer(message, response, *a, **k):
|
||||
if isinstance(response, str) and "😖</tg-emoji>" in response:
|
||||
body = response.split("😖</tg-emoji>", 1)[1].strip()
|
||||
if body in {"", "<b></b>", "<b> </b>"}:
|
||||
with suppress(Exception):
|
||||
if hasattr(message, "delete"):
|
||||
await message.delete()
|
||||
return message
|
||||
|
||||
if body.startswith("<b>") and body.endswith("</b>"):
|
||||
decoded = html.unescape(body[3:-4])
|
||||
response = response.split("😖</tg-emoji>", 1)[0] + f'😖</tg-emoji> {decoded}' if decoded else response.split("😖</tg-emoji>", 1)[0] + '😖</tg-emoji>'
|
||||
|
||||
try:
|
||||
return await base(message, response, *a, **k)
|
||||
except Exception:
|
||||
with suppress(Exception):
|
||||
return await self._client.send_message(
|
||||
utils.get_chat_id(message),
|
||||
response,
|
||||
reply_to=getattr(message, "reply_to_msg_id", None),
|
||||
buttons=k.get("reply_markup"),
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
utils.answer = answer
|
||||
try:
|
||||
if getattr(original, "__self__", None) is None:
|
||||
return await original(_, *args, **kwargs)
|
||||
return await original(*args, **kwargs)
|
||||
finally:
|
||||
if utils.answer is answer:
|
||||
utils.answer = base
|
||||
|
||||
self.core.load_module = load.__get__(self.core, self.core.__class__)
|
||||
self.modules.register_module = self.register
|
||||
|
||||
def unpatch(self):
|
||||
if self.oreg:
|
||||
self.modules.register_module = self.oreg
|
||||
if getattr(self, "core", None) and self.oload:
|
||||
self.core.load_module = self.oload
|
||||
|
||||
def context(self):
|
||||
frame = sys._getframe()
|
||||
msg = None
|
||||
fmsg = None
|
||||
is_dlm_lm = False
|
||||
|
||||
while frame:
|
||||
locals = frame.f_locals
|
||||
if (
|
||||
frame.f_code.co_name == "load_module"
|
||||
and locals.get("self") is self.core
|
||||
and 'message' in locals
|
||||
and hasattr(locals['message'], 'edit')
|
||||
):
|
||||
if not msg:
|
||||
msg = locals['message']
|
||||
fmsg = locals.get('msg')
|
||||
|
||||
if frame.f_code.co_name in {"dlmod", "loadmod"}:
|
||||
is_dlm_lm = True
|
||||
if not msg and 'message' in locals and hasattr(locals['message'], 'edit'):
|
||||
msg = locals['message']
|
||||
|
||||
if frame.f_code.co_name == "download_and_install":
|
||||
if not msg and 'message' in locals and hasattr(locals['message'], 'edit'):
|
||||
msg = locals['message']
|
||||
|
||||
frame = frame.f_back
|
||||
|
||||
return msg, fmsg, is_dlm_lm
|
||||
|
||||
def target_chat(self, msg=None, fmsg=None):
|
||||
if not msg:
|
||||
return None
|
||||
|
||||
if not fmsg:
|
||||
return msg
|
||||
|
||||
with suppress(Exception):
|
||||
target = copy.copy(msg)
|
||||
target.reply_to_msg_id = fmsg.id
|
||||
return target
|
||||
|
||||
return None
|
||||
|
||||
async def call_oreg(self, spec, name, origin="<core>", save_fs=False):
|
||||
if getattr(self.oreg, "__self__", None) is None:
|
||||
return await self.oreg(self.modules, spec, name, origin, save_fs=save_fs)
|
||||
return await self.oreg(spec, name, origin, save_fs=save_fs)
|
||||
|
||||
async def register(self, spec, name, origin="<core>", save_fs=False):
|
||||
if origin != "<core>":
|
||||
code = ""
|
||||
|
||||
if hasattr(spec.loader, "data") and spec.loader.data:
|
||||
code = spec.loader.data
|
||||
if isinstance(code, bytes):
|
||||
code = code.decode("utf-8", errors="ignore")
|
||||
elif origin and origin.endswith(".py"):
|
||||
with suppress(Exception):
|
||||
with open(origin, "r", encoding="utf-8") as f:
|
||||
code = f.read()
|
||||
|
||||
if code:
|
||||
check = await self.check(code)
|
||||
|
||||
if check is not True:
|
||||
msg, fmsg, is_dlm_lm = self.context()
|
||||
target = self.target_chat(msg, fmsg)
|
||||
|
||||
if isinstance(check, dict):
|
||||
status = check.get("level", "blocked")
|
||||
reason = check.get("reason", "")
|
||||
else:
|
||||
status = "unavailable"
|
||||
reason = ""
|
||||
|
||||
link = origin if origin.startswith("http") else ""
|
||||
|
||||
if status == "blocked":
|
||||
if msg and target:
|
||||
raise loader.LoadError(self.format("blocked", reason, link))
|
||||
raise loader.LoadError("")
|
||||
|
||||
should_block = is_dlm_lm or self.config["strict_mode"]
|
||||
|
||||
if should_block and not (msg and target):
|
||||
raise loader.LoadError("")
|
||||
|
||||
if should_block and msg and target:
|
||||
task = str(uuid.uuid4())
|
||||
event = asyncio.Event()
|
||||
self.tasks[task] = {"event": event, "decision": False}
|
||||
|
||||
try:
|
||||
form = await self.inline.form(
|
||||
text=self.format(status, reason, link),
|
||||
message=target,
|
||||
reply_markup=self.buttons(task)
|
||||
)
|
||||
|
||||
if not form:
|
||||
raise loader.LoadError(reason)
|
||||
|
||||
await asyncio.wait_for(event.wait(), timeout=180.0)
|
||||
|
||||
if not self.tasks.pop(task)["decision"]:
|
||||
with suppress(Exception):
|
||||
await form.delete()
|
||||
raise loader.LoadError("")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
self.tasks.pop(task, None)
|
||||
with suppress(Exception):
|
||||
await form.delete()
|
||||
raise loader.LoadError("")
|
||||
except loader.LoadError:
|
||||
raise
|
||||
except Exception:
|
||||
raise loader.LoadError("")
|
||||
|
||||
return await self.call_oreg(spec, name, origin, save_fs=save_fs)
|
||||
|
||||
async def confirm(self, call, task, action):
|
||||
if task in self.tasks:
|
||||
self.tasks[task]["decision"] = (action == "yes")
|
||||
self.tasks[task]["event"].set()
|
||||
with suppress(Exception):
|
||||
await call.delete()
|
||||
87
Fixyres/FModules/LFSecurity.py
Normal file
87
Fixyres/FModules/LFSecurity.py
Normal file
@@ -0,0 +1,87 @@
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
# meta developer: @NFModules
|
||||
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/banner.png
|
||||
# meta fhsdesc: security, guard, antiscam, antivirus
|
||||
# scope: hikka_min 2.0.0
|
||||
|
||||
# ©️ Fixyres, 2024-2030
|
||||
# 🌐 https://github.com/Fixyres/FModules
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
import aiohttp
|
||||
import os
|
||||
from .. import loader
|
||||
|
||||
|
||||
@loader.tds
|
||||
class FSecurity(loader.Module):
|
||||
"""Module for automatic AI-based security checks of installed modules."""
|
||||
|
||||
strings = {
|
||||
"name": "FSecurity"
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль для автоматической проверки устанавливаемых модулей через ИИ."
|
||||
}
|
||||
|
||||
strings_ua = {
|
||||
"_cls_doc": "Модуль для автоматичної перевірки встановлюваних модулів через ШІ."
|
||||
}
|
||||
|
||||
strings_de = {
|
||||
"_cls_doc": "Modul zur automatischen Prüfung installierter Module mit KI."
|
||||
}
|
||||
|
||||
strings_jp = {
|
||||
"_cls_doc": "AIでインストールされるモジュールを自動チェックするモジュール。"
|
||||
}
|
||||
|
||||
strings_tr = {
|
||||
"_cls_doc": "Kurulan modülleri yapay zeka ile otomatik kontrol eden modül."
|
||||
}
|
||||
|
||||
strings_uz = {
|
||||
"_cls_doc": "O'rnatilayotgan modullarni AI orqali avtomatik tekshiruvchi modul."
|
||||
}
|
||||
|
||||
strings_kz = {
|
||||
"_cls_doc": "Орнатылатын модульдерді ЖИ арқылы автоматты тексеретін модуль."
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
core = self.lookup("loader")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
"https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/FSecurity.py",
|
||||
timeout=aiohttp.ClientTimeout(total=15),
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
return
|
||||
source = await resp.text()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
target = os.path.join(
|
||||
os.path.dirname(loader.__file__),
|
||||
"modules",
|
||||
"FSecurity.py",
|
||||
)
|
||||
|
||||
try:
|
||||
with open(target, "w", encoding="utf-8") as f:
|
||||
f.write(source)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
await core.unload_module("FSecurity")
|
||||
try:
|
||||
await core.load_module(source, None, "FSecurity", target, save_fs=False)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -7,7 +7,8 @@ __version__ = (1, 0, 0)
|
||||
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/SCD/banner.png
|
||||
# meta developer: @FModules
|
||||
# meta developer: @NFModules
|
||||
# meta fhsdesc: SoundCloud, Music, Music downloader, Downloader
|
||||
|
||||
# requires: curl_cffi
|
||||
|
||||
@@ -105,15 +106,15 @@ class SCD(loader.Module):
|
||||
'''(link) - download a song from SoundCloud.'''
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("no_args").format(prefix=self.get_prefix()))
|
||||
await utils.answer(message, self.strings["no_args"].format(prefix=self.get_prefix()))
|
||||
return
|
||||
|
||||
m = re.search(r"(https?://(?:[a-zA-Z0-9-]+\.)?soundcloud\.com/[^\s]+)", args)
|
||||
if not m:
|
||||
await utils.answer(message, self.strings("not_found"))
|
||||
await utils.answer(message, self.strings["not_found"])
|
||||
return
|
||||
|
||||
msg = await utils.answer(message, self.strings("downloading"))
|
||||
msg = await utils.answer(message, self.strings["downloading"])
|
||||
|
||||
try:
|
||||
async with requests.AsyncSession(impersonate="chrome120") as ses:
|
||||
@@ -194,4 +195,4 @@ class SCD(loader.Module):
|
||||
await msg.delete()
|
||||
|
||||
except:
|
||||
await utils.answer(msg, self.strings("not_found"))
|
||||
await utils.answer(msg, self.strings["not_found"])
|
||||
|
||||
@@ -7,7 +7,7 @@ __version__ = (1, 1, 0)
|
||||
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/banner.png
|
||||
# meta developer: @FModules
|
||||
# meta developer: @NFModules
|
||||
# meta fhsdesc: game, funny, guess, question game
|
||||
|
||||
# requires: curl_cffi
|
||||
@@ -308,7 +308,7 @@ class Akinator(loader.Module):
|
||||
loader.ConfigValue(
|
||||
"child_mode",
|
||||
False,
|
||||
lambda: self.strings("child_mode"),
|
||||
lambda: self.strings["child_mode"],
|
||||
validator=loader.validators.Boolean()
|
||||
)
|
||||
)
|
||||
@@ -339,17 +339,17 @@ class Akinator(loader.Module):
|
||||
async def akinator(self, message):
|
||||
'''- start the game.'''
|
||||
try:
|
||||
aki = AsyncAki(self.strings("lang"), self.config["child_mode"])
|
||||
aki = AsyncAki(self.strings["lang"], self.config["child_mode"])
|
||||
await aki.start()
|
||||
|
||||
self.games.setdefault(message.chat_id, {})[message.id] = aki
|
||||
|
||||
await self.inline.form(
|
||||
text=self.strings("text"),
|
||||
text=self.strings["text"],
|
||||
message=message,
|
||||
photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/banner.png",
|
||||
reply_markup={
|
||||
"text": self.strings("start"),
|
||||
"text": self.strings["start"],
|
||||
"callback": self._cb,
|
||||
"args": (message,)
|
||||
}
|
||||
@@ -369,12 +369,12 @@ class Akinator(loader.Module):
|
||||
question = aki.q
|
||||
|
||||
markup = [[
|
||||
{"text": self.strings("yes"), "callback": self._ans, "args": (0, message)},
|
||||
{"text": self.strings("no"), "callback": self._ans, "args": (1, message)},
|
||||
{"text": self.strings("idk"), "callback": self._ans, "args": (2, message)}
|
||||
{"text": self.strings["yes"], "callback": self._ans, "args": (0, message)},
|
||||
{"text": self.strings["no"], "callback": self._ans, "args": (1, message)},
|
||||
{"text": self.strings["idk"], "callback": self._ans, "args": (2, message)}
|
||||
],[
|
||||
{"text": self.strings("probably"), "callback": self._ans, "args": (3, message)},
|
||||
{"text": self.strings("probably_not"), "callback": self._ans, "args": (4, message)}
|
||||
{"text": self.strings["probably"], "callback": self._ans, "args": (3, message)},
|
||||
{"text": self.strings["probably_not"], "callback": self._ans, "args": (4, message)}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -393,13 +393,13 @@ class Akinator(loader.Module):
|
||||
desc = aki.desc
|
||||
|
||||
if desc:
|
||||
text = self.strings("this_is").format(name=name, description=desc)
|
||||
text = self.strings["this_is"].format(name=name, description=desc)
|
||||
else:
|
||||
text = self.strings("this_is_no_desc").format(name=name)
|
||||
text = self.strings["this_is_no_desc"].format(name=name)
|
||||
|
||||
markup = [[
|
||||
{"text": self.strings("yes"), "callback": self._fin, "args": (True, message, text, aki.photo)},
|
||||
{"text": self.strings("not_right"), "callback": self._rej, "args": (message,)}
|
||||
{"text": self.strings["yes"], "callback": self._fin, "args": (True, message, text, aki.photo)},
|
||||
{"text": self.strings["not_right"], "callback": self._rej, "args": (message,)}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -450,7 +450,7 @@ class Akinator(loader.Module):
|
||||
await call.edit(text, photo=photo, reply_markup=[])
|
||||
else:
|
||||
await call.edit(
|
||||
self.strings("failed"),
|
||||
self.strings["failed"],
|
||||
photo="https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/akinator/idk.png",
|
||||
reply_markup=[]
|
||||
)
|
||||
)
|
||||
|
||||
1
Fixyres/FModules/assets/FSecurity/api_keys.txt
Normal file
1
Fixyres/FModules/assets/FSecurity/api_keys.txt
Normal file
@@ -0,0 +1 @@
|
||||
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
|
||||
BIN
Fixyres/FModules/assets/FSecurity/banner.png
Normal file
BIN
Fixyres/FModules/assets/FSecurity/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
25
Fixyres/FModules/assets/FSecurity/prompts/chank.txt
Normal file
25
Fixyres/FModules/assets/FSecurity/prompts/chank.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
You must strictly follow these rules with no exceptions.
|
||||
|
||||
Analyze only part {current}/{total} of a Hikka userbot Python module. Do NOT give a final verdict for the whole module.
|
||||
Previous context: {previous_context}
|
||||
|
||||
BLOCKED: encrypted/obfuscated code (base64, marshal, zlib, rot13, encoded exec, or any technique hiding real logic), account deletion, mass scam/spam/ads to all chats on load, session/auth_key/2FA exfiltration, bulk message/dialog dump to external destination, string "FSecurity" (if found → findings must be ONLY: "Attempted interaction with FSecurity." translated to {lang}, nothing else).
|
||||
SUSPICIOUS: watcher/scheduler/client_ready auto-installing modules without owner confirmation, download + exec of remote Python code without confirmation, runtime pip install or library download, third-party OAuth redirect.
|
||||
CLEAN: no security issue in this chunk.
|
||||
|
||||
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs CLEAN → CLEAN.
|
||||
@loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default = not a threat.
|
||||
Owner-triggered exec/eval/shell = not a threat.
|
||||
A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always CLEAN, never SUSPICIOUS. This is a standard userbot feature.
|
||||
|
||||
Respond ONLY with valid JSON:
|
||||
{"chunk_verdict":"CLEAN|SUSPICIOUS|BLOCKED","findings":"..."}
|
||||
|
||||
Findings rules (when not CLEAN):
|
||||
- Write in {lang}. Max 1000 chars.
|
||||
- Technical analysis for reading, not a reply. No "I found", no "you should". Third person only.
|
||||
- Do NOT mention which rule was triggered. Just describe what the code does.
|
||||
- Only the key threats in this chunk. Reference approximate line numbers within the chunk.
|
||||
- Use <code>text</code> for code references: function names, variables, URLs, string literals.
|
||||
- For obfuscation chains, wrap the whole chain in one <code> block: <code>base64.b64decode → zlib.decompress → exec</code>.
|
||||
- If CLEAN → findings must be empty string "".
|
||||
29
Fixyres/FModules/assets/FSecurity/prompts/final.txt
Normal file
29
Fixyres/FModules/assets/FSecurity/prompts/final.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
You must strictly follow these rules with no exceptions.
|
||||
|
||||
A Hikka userbot module was split into {total} parts. Chunk findings:
|
||||
{findings}
|
||||
|
||||
Give the final verdict for the entire module based on all findings above.
|
||||
|
||||
BLOCKED: encrypted/obfuscated code, account deletion, mass scam/spam on load, session/auth_key theft, bulk message dump, string "FSecurity" (if found → summary must be ONLY: "Attempted interaction with FSecurity." translated to {lang}, nothing else).
|
||||
SUSPICIOUS: auto-install modules without confirmation, remote code download + exec without confirmation, runtime pip/library install, third-party OAuth redirect.
|
||||
SAFE: no real security issue across all parts.
|
||||
|
||||
Auto-install = SUSPICIOUS, never BLOCKED.
|
||||
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs SAFE → SAFE.
|
||||
@loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default = not a threat.
|
||||
Owner-triggered exec/eval/shell = not a threat.
|
||||
A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always SAFE, never SUSPICIOUS. This is a standard userbot feature.
|
||||
|
||||
Respond ONLY with valid JSON:
|
||||
{"verdict":"SAFE|SUSPICIOUS|BLOCKED","summary":"..."}
|
||||
|
||||
Summary rules (when not SAFE):
|
||||
- Write in {lang}. Max 1000 chars.
|
||||
- Combine the most important findings into one coherent technical analysis.
|
||||
- This is a report for reading, NOT a reply to a person. No "I found", no "you should". Third person only.
|
||||
- Do NOT mention which rule was triggered or explain criteria. Just describe what the code does.
|
||||
- Only the key threats. Reference line numbers from findings where available.
|
||||
- Use <code>text</code> for all code references: function names, variables, URLs, string literals.
|
||||
- For obfuscation, show the full chain in one <code> block: <code>base64.b64decode → zlib.decompress → exec</code>.
|
||||
- If SAFE → summary must be empty string "".
|
||||
37
Fixyres/FModules/assets/FSecurity/prompts/main.txt
Normal file
37
Fixyres/FModules/assets/FSecurity/prompts/main.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
You must strictly follow these classification rules with no exceptions.
|
||||
|
||||
Classify a Hikka userbot Python module as BLOCKED, SUSPICIOUS, or SAFE.
|
||||
|
||||
BLOCKED (any single match):
|
||||
- Code is encrypted or obfuscated (base64, marshal, zlib, rot13, compile+exec of encoded data, or any technique that hides real logic).
|
||||
- Attempts to delete Telegram account (DeleteAccountRequest, client.delete_account, or equivalent).
|
||||
- On load (client_ready, __init__) automatically sends scam, spam, or ads to all chats/dialogs/contacts without owner action.
|
||||
- Steals and sends session string, auth_key, or 2FA password anywhere outside the device.
|
||||
- Collects and forwards all messages or dialogs to any external destination.
|
||||
- Contains the string "FSecurity" → summary must be ONLY: "Attempted interaction with FSecurity." translated to {lang}. Nothing else, no extra text.
|
||||
|
||||
SUSPICIOUS (any single match, only if BLOCKED did not trigger):
|
||||
- Watcher, scheduler, or client_ready auto-installs modules from any URL without per-action owner confirmation.
|
||||
- Downloads and executes remote Python code (exec/eval on fetched content) without owner confirmation.
|
||||
- Installs pip packages or downloads Python libraries at runtime from the internet.
|
||||
- OAuth or auth flow redirected through a non-official third-party domain.
|
||||
|
||||
SAFE: everything that does not match any rule above.
|
||||
- Owner-triggered exec/eval/shell = always SAFE.
|
||||
- A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always SAFE, never SUSPICIOUS. This is a standard feature of userbots and poses no threat.
|
||||
- @loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default (no public access without explicit permission) = SAFE.
|
||||
|
||||
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs SAFE → SAFE.
|
||||
|
||||
Respond ONLY with valid JSON:
|
||||
{"verdict":"SAFE|SUSPICIOUS|BLOCKED","summary":"..."}
|
||||
|
||||
Summary rules (when not SAFE):
|
||||
- Write in {lang}. Max 1000 chars.
|
||||
- This is a technical analysis meant to be read, NOT a reply to a person. Never write "I found", "you should", "I recommend". Write in third person.
|
||||
- Do NOT mention which rule was triggered or explain the classification criteria. Just describe what the code does.
|
||||
- Point out ONLY the key threats. Do NOT describe what the module does overall or list safe parts.
|
||||
- Reference the approximate line number where dangerous code appears: "line NN —".
|
||||
- Use <code>text</code> for every code reference: function names, variables, URLs, string literals.
|
||||
- For obfuscation, show the full decoding chain inside one <code> block: <code>base64.b64decode → zlib.decompress → marshal.loads → exec</code>.
|
||||
- If SAFE → summary must be empty string "".
|
||||
@@ -2,3 +2,4 @@ akinator
|
||||
FHeta
|
||||
BSR
|
||||
SCD
|
||||
LFSecurity
|
||||
|
||||
596
Limoka.py
596
Limoka.py
@@ -1,5 +1,5 @@
|
||||
# meta developer: @limokanews
|
||||
# requires: whoosh cryptography
|
||||
# requires: whoosh cryptography filetype
|
||||
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
@@ -24,16 +24,37 @@ from telethon.errors.rpcerrorlist import WebpageMediaEmptyError
|
||||
from telethon import TelegramClient
|
||||
from telethon.errors.rpcerrorlist import YouBlockedUserError
|
||||
from telethon import functions
|
||||
import filetype
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
import ast
|
||||
|
||||
|
||||
from aiogram.exceptions import TelegramBadRequest as BadRequest
|
||||
|
||||
try:
|
||||
from aiogram.utils.exceptions import BadRequest
|
||||
except ImportError:
|
||||
from aiogram.exceptions import TelegramBadRequest as BadRequest
|
||||
from .. import utils, loader
|
||||
from ..types import InlineCall
|
||||
from ..types import BotInlineCall, InlineCall
|
||||
|
||||
logger = logging.getLogger("Limoka")
|
||||
__version__ = (1, 4, 5)
|
||||
__version__ = (1, 5, 5)
|
||||
|
||||
|
||||
def _parse_version_from_source(source: str):
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
return None
|
||||
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == "__version__":
|
||||
try:
|
||||
return ast.literal_eval(node.value)
|
||||
except (ValueError, SyntaxError):
|
||||
return None
|
||||
return None
|
||||
|
||||
WEIGHTS = {
|
||||
"inline.token_obtainment": 15,
|
||||
@@ -236,9 +257,11 @@ class ModuleContentBuilder:
|
||||
module_info: Dict[str, Any],
|
||||
query: str,
|
||||
filters: Dict[str, List[str]],
|
||||
url: str,
|
||||
include_categories: bool = True,
|
||||
module_path: Optional[str] = None,
|
||||
lang: str = "en",
|
||||
|
||||
) -> tuple:
|
||||
"""
|
||||
Build complete formatted module content.
|
||||
@@ -260,39 +283,48 @@ class ModuleContentBuilder:
|
||||
|
||||
categories_text = self._build_categories_text(filters)
|
||||
commands = self.formatter.format_commands(module_info, lang)
|
||||
header = self._build_header(query, name, description, dev_username, module_path)
|
||||
header = self._build_header(query, name, description, dev_username, module_path, url)
|
||||
footer = self._build_footer(module_path)
|
||||
body_pages = self._paginate_commands(commands)
|
||||
|
||||
return header, body_pages, footer, categories_text
|
||||
|
||||
def _build_header(self, query: str, name: str, description: str, dev_username: str, module_path: Optional[str]) -> str:
|
||||
def _build_header(self, query: str, name: str, description: str, dev_username: str, module_path: Optional[str], url: str) -> str:
|
||||
"""Build message header with module info and tags."""
|
||||
tags_list = self.repository.get_tags_for_module(module_path) if module_path else []
|
||||
tags_text = ", ".join(self.strings["tags"].get(tag, tag) for tag in tags_list)
|
||||
|
||||
header_template = self.strings["found_header"]
|
||||
if not tags_text:
|
||||
# Replace the tags line but keep blockquote closure at the end
|
||||
header_template = header_template.replace(
|
||||
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Tags:</b> {tags}</blockquote>\n\n",
|
||||
""
|
||||
"\n\n<b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Tags:</b> {tags}</blockquote>\n\n",
|
||||
"</blockquote>\n\n"
|
||||
)
|
||||
header_template = header_template.replace(
|
||||
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Теги:</b> {tags}</blockquote>\n\n",
|
||||
""
|
||||
"\n\n<b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Теги:</b> {tags}</blockquote>\n\n",
|
||||
"</blockquote>\n\n"
|
||||
)
|
||||
|
||||
return header_template.format(
|
||||
header = header_template.format(
|
||||
query=html.escape(query),
|
||||
name=name,
|
||||
description=description,
|
||||
username=dev_username,
|
||||
tags=tags_text,
|
||||
module_path=module_path,
|
||||
url=url
|
||||
)
|
||||
|
||||
# Remove extra newlines
|
||||
header = re.sub(r'\n+', '\n', header)
|
||||
|
||||
return header
|
||||
|
||||
def _build_footer(self, module_path: Optional[str]) -> str:
|
||||
"""Build message footer with download command."""
|
||||
clean_path = (module_path or "").replace("\\", "/")
|
||||
return "" # unused for now, but may be used in the future for additional info
|
||||
return self.strings["found_footer"].format(
|
||||
url=html.escape(self.formatter.strings.get("limokaurl", "")),
|
||||
module_path=html.escape(clean_path),
|
||||
@@ -351,22 +383,20 @@ class Limoka(loader.Module):
|
||||
"found_header": (
|
||||
"<blockquote><tg-emoji emoji-id=5413334818047940135>🔍</tg-emoji> Found module <b>{name}</b> "
|
||||
"by query: <b>{query}</b>\n\n"
|
||||
"<b><tg-emoji emoji-id=5413350219800661019>🌐</tg-emoji> <a href='{url}{module_path}'>Source</a></b>\n"
|
||||
"<b><tg-emoji emoji-id=5418376169055602355>ℹ️</tg-emoji> Description:</b> {description}\n"
|
||||
"<b><tg-emoji emoji-id=5418299289141004396>🧑💻</tg-emoji> Developer:</b> {username}\n\n"
|
||||
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Tags:</b> {tags}</blockquote>\n\n"
|
||||
"<b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Tags:</b> {tags}</blockquote>\n\n"
|
||||
),
|
||||
"found_body": ("{commands}"),
|
||||
"found_footer": (
|
||||
"<blockquote>\n<tg-emoji emoji-id=5411143117711624172>🪄</tg-emoji> <code>{prefix}dlm {url}{module_path}</code></blockquote>"
|
||||
),
|
||||
"caption_short": (
|
||||
"<blockquote><tg-emoji emoji-id=5413334818047940135>🔍</tg-emoji> <b>{safe_name}</b>\n"
|
||||
"<b><tg-emoji emoji-id=5413350219800661019>🌐</tg-emoji> <a href='{url}{module_path}'>Source</a></b>\n"
|
||||
"<b><tg-emoji emoji-id=5418376169055602355>ℹ️</tg-emoji> Description:</b> {safe_desc}\n"
|
||||
"<b><tg-emoji emoji-id=5418299289141004396>🧑💻</tg-emoji> Dev:</b> {dev_username}\n"
|
||||
"<tg-emoji emoji-id=5411143117711624172>🪄</tg-emoji> <code>{prefix}dlm {module_path}</code></blockquote>"
|
||||
"<b><tg-emoji emoji-id=5418299289141004396>🧑💻</tg-emoji> Dev:</b> {dev_username}</blockquote>\n"
|
||||
),
|
||||
"command_template": "{emoji} <code>{prefix}{command}</code> — {description}\n",
|
||||
"inline_handler_template": "{inline_bot} {command} — {description}\n",
|
||||
"command_template": "<blockquote>{emoji} <code>{prefix}{command}</code> — {description}</blockquote>\n",
|
||||
"inline_handler_template": "<blockquote>{inline_bot} {command} — {description}</blockquote>\n",
|
||||
"emojis": {
|
||||
1: "<tg-emoji emoji-id=5416037945909987712>1️⃣</tg-emoji>",
|
||||
2: "<tg-emoji emoji-id=5413855071731470617>2️⃣</tg-emoji>",
|
||||
@@ -381,7 +411,7 @@ class Limoka(loader.Module):
|
||||
"404": "<blockquote><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> <b>Not found by query: <i>{query}</i></b></blockquote>",
|
||||
"noargs": "<blockquote><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> <b>No args</b></blockquote>",
|
||||
"?": "<blockquote><tg-emoji emoji-id=5951895176908640647>🔎</tg-emoji> Request too short / not found</blockquote>",
|
||||
"no_info": "<blockquote>No information</blockquote>",
|
||||
"no_info": "No information",
|
||||
"facts": [
|
||||
"<blockquote><tg-emoji emoji-id=5472193350520021357>🛡</tg-emoji> The limoka catalog is carefully moderated!</blockquote>",
|
||||
"<blockquote><tg-emoji emoji-id=5940434198413184876>🚀</tg-emoji> Limoka performance allows you to search for modules quickly!</blockquote>",
|
||||
@@ -414,6 +444,7 @@ class Limoka(loader.Module):
|
||||
"global_button": "🌍 Results",
|
||||
"filtered_button": "🏷️ Filtered search",
|
||||
"inline_search": "🔍 Search in Limoka",
|
||||
"install_button": "🪄 Install",
|
||||
"inline_no_results": "<blockquote>❌ No modules found</blockquote>",
|
||||
"inline_error": "<blockquote>❌ Search error occurred</blockquote>",
|
||||
"inline_short_query": "<blockquote>❌ Query too short (min 2 chars)</blockquote>",
|
||||
@@ -450,7 +481,19 @@ class Limoka(loader.Module):
|
||||
"If error persists again, report to developers</blockquote>"
|
||||
),
|
||||
"body_page": "Commands",
|
||||
"limokaurl": "https://raw.githubusercontent.com/MuRuLOSE/limoka/refs/heads/main/"
|
||||
"install_failed": "Installation failed. Check logs for details.",
|
||||
"install_succeeded": "Module installed successfully!",
|
||||
"update_available": (
|
||||
"🔔 <b>New update available!</b>\n\n"
|
||||
"New Limoka Version {version} already available. Please update for better performance, bug fixes, and new features.\n"
|
||||
"Press the button below to update the module."
|
||||
),
|
||||
"no_updates_available": "<blockquote>❌ No updates available. You are using the latest version of Limoka.</blockquote>",
|
||||
"module_update_available": "<blockquote>🔔 Notification about module update has been sent, check @{bot}.</blockquote>",
|
||||
"index_update_started": "<blockquote>🔄 Limoka module index update has started. This may take a few minutes. Please wait...</blockquote>",
|
||||
"index_update_failed": "<blockquote>❌ Failed to update Limoka module index. Please try again later. If the error persists, report to developers</blockquote>",
|
||||
"index_update_success": "<blockquote>✅ Limoka module index updated successfully!</blockquote>",
|
||||
"update_check_started": "<blockquote>🔍 Checking for Limoka updates...</blockquote>",
|
||||
}
|
||||
strings_ru = {
|
||||
"name": "Limoka",
|
||||
@@ -461,23 +504,21 @@ class Limoka(loader.Module):
|
||||
),
|
||||
"found_header": (
|
||||
"<blockquote><tg-emoji emoji-id=5413334818047940135>🔍</tg-emoji> Найден модуль <b>{name}</b> "
|
||||
"по запросу: <b>{query}</b></blockquote>\n\n"
|
||||
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>ℹ️</tg-emoji> Описание:</b> {description}</blockquote>\n"
|
||||
"<blockquote><b><tg-emoji emoji-id=5418299289141004396>🧑💻</tg-emoji> Разработчик:</b> {username}</blockquote>\n\n"
|
||||
"<blockquote><b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Теги:</b> {tags}</blockquote>\n\n"
|
||||
"по запросу: <b>{query}</b>\n\n"
|
||||
"<b><tg-emoji emoji-id=5413350219800661019>🌐</tg-emoji> <a href='{url}{module_path}'>Исходный код</a></b>\n"
|
||||
"<b><tg-emoji emoji-id=5418376169055602355>ℹ️</tg-emoji> Описание:</b> {description}\n"
|
||||
"<b><tg-emoji emoji-id=5418299289141004396>🧑💻</tg-emoji> Разработчик:</b> {username}\n\n"
|
||||
"<b><tg-emoji emoji-id=5418376169055602355>🏷</tg-emoji> Теги:</b> {tags}</blockquote>\n\n"
|
||||
),
|
||||
"found_body": ("{commands}"),
|
||||
"found_footer": (
|
||||
"\n<blockquote><tg-emoji emoji-id=5411143117711624172>🪄</tg-emoji> <code>{prefix}dlm {url}{module_path}</code></blockquote>"
|
||||
),
|
||||
"caption_short": (
|
||||
"<blockquote><tg-emoji emoji-id=5413334818047940135>🔍</tg-emoji> <b>{safe_name}</b>\n"
|
||||
"<b><tg-emoji emoji-id=5413350219800661019>🌐</tg-emoji> <a href='{url}{module_path}'>Исходный код</a></b>\n"
|
||||
"<b><tg-emoji emoji-id=5418376169055602355>ℹ️</tg-emoji> Описание:</b> {safe_desc}\n"
|
||||
"<b><tg-emoji emoji-id=5418299289141004396>🧑💻</tg-emoji> Разработчик:</b> {dev_username}\n"
|
||||
"<tg-emoji emoji-id=5411143117711624172>🪄</tg-emoji> <code>{prefix}dlm {module_path}</code></blockquote>"
|
||||
"<b><tg-emoji emoji-id=5418299289141004396>🧑💻</tg-emoji> Разработчик:</b> {dev_username}</blockquote>\n"
|
||||
),
|
||||
"command_template": "<blockquote>{emoji} <code>{prefix}{command}</code> — {description}</blockquote>\n",
|
||||
"inline_handler_template": "{inline_bot} {command} — {description}\n",
|
||||
"inline_handler_template": "<blockquote>{inline_bot} {command} — {description}</blockquote>\n",
|
||||
"emojis": {
|
||||
1: "<tg-emoji emoji-id=5416037945909987712>1️⃣</tg-emoji>",
|
||||
2: "<tg-emoji emoji-id=5413855071731470617>2️⃣</tg-emoji>",
|
||||
@@ -493,7 +534,7 @@ class Limoka(loader.Module):
|
||||
"404": "<blockquote><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> <b>Не найдено по запросу: <i>{query}</i></b></blockquote>",
|
||||
"noargs": "<blockquote><tg-emoji emoji-id=5210952531676504517>❌</tg-emoji> <b>Нет аргументов</b></blockquote>",
|
||||
"?": "<blockquote><tg-emoji emoji-id=5951895176908640647>🔎</tg-emoji> Запрос слишком короткий / не найден</blockquote>",
|
||||
"no_info": "<blockquote>Нет информации</blockquote>",
|
||||
"no_info": "Нет информации",
|
||||
"facts": [
|
||||
"<blockquote><tg-emoji emoji-id=5472193350520021357>🛡</tg-emoji> Каталог Limoka тщательно модерируется!</blockquote>",
|
||||
"<blockquote><tg-emoji emoji-id=5940434198413184876>🚀</tg-emoji> Limoka позволяет искать модули с невероятной скоростью!</blockquote>",
|
||||
@@ -530,6 +571,7 @@ class Limoka(loader.Module):
|
||||
"global_button": "🌍 Результаты",
|
||||
"filtered_button": "🏷️ Поиск с фильтрами",
|
||||
"inline_search": "🔍 Поиск в Limoka",
|
||||
"install_button": "🪄 Установить",
|
||||
"inline_no_results": "<blockquote>❌ Модули не найдены</blockquote>",
|
||||
"inline_error": "<blockquote>❌ Ошибка поиска</blockquote>",
|
||||
"inline_short_query": "<blockquote>❌ Запрос слишком короткий (мин. 2 символа)</blockquote>",
|
||||
@@ -566,6 +608,19 @@ class Limoka(loader.Module):
|
||||
"Если ошибка сохраняется снова, сообщите разработчикам</blockquote>"
|
||||
),
|
||||
"body_page": "Команды",
|
||||
"install_failed": "Установка не удалась. Проверьте логи для деталей.",
|
||||
"install_succeeded": "Модуль успешно установлен!",
|
||||
"update_available": (
|
||||
"🔔 <b>Доступно новое обновление!</b>\n\n"
|
||||
"Новая версия Limoka {version} уже доступна. Пожалуйста, обновитесь для лучшей производительности, исправления багов и новых функций.\n"
|
||||
"Нажмите кнопку ниже, чтобы обновить модуль."
|
||||
),
|
||||
"no_updates_available": "<blockquote>❌ Нет доступных обновлений. У вас установлена последняя версия Limoka.</blockquote>",
|
||||
"module_update_available": "<blockquote>🔔 Уведомление об обновлении модуля было отправлено, проверьте @{bot}.</blockquote>",
|
||||
"index_update_started": "<blockquote>🔄 Обновление индекса модулей Limoka началось. Это может занять несколько минут. Пожалуйста, подождите...</blockquote>",
|
||||
"index_update_failed": "<blockquote>❌ Не удалось обновить индекс модулей Limoka. Пожалуйста, попробуйте снова позже. Если ошибка сохраняется, сообщите разработчикам</blockquote>",
|
||||
"index_update_success": "<blockquote>✅ Индекс модулей Limoka успешно обновлен!</blockquote>",
|
||||
"update_check_started": "<blockquote>🔍 Проверка обновлений Limoka...</blockquote>",
|
||||
"_cls_doc": "Модули теперь в одном месте с простым и удобным поиском!",
|
||||
}
|
||||
|
||||
@@ -590,11 +645,18 @@ class Limoka(loader.Module):
|
||||
lambda: "If enabled, modules from developers with newbies tag will be not shown.",
|
||||
validator=loader.validators.Boolean(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"auto_update_check",
|
||||
True,
|
||||
lambda: "If enabled, Limoka will periodically check for updates and notify you when a new version is available.",
|
||||
validator=loader.validators.Boolean(),
|
||||
)
|
||||
)
|
||||
self.name = self.strings["name"]
|
||||
self._invalid_banners = set()
|
||||
self._bot_username = "limoka_bbot"
|
||||
self._base_url = self.config["limokaurl"]
|
||||
self._self_bot_username = None
|
||||
|
||||
self.SEARCH_STATES = {
|
||||
"no_banner": "no_banner",
|
||||
@@ -613,6 +675,50 @@ class Limoka(loader.Module):
|
||||
def _filter_newbies(self, modules: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""[DEPRECATED] Use ModuleRepository.apply_newbie_filter instead."""
|
||||
return self.repository.apply_newbie_filter(self.config.get("filter_newbies_modules", False))
|
||||
|
||||
@loader.loop(interval=3600*24)
|
||||
async def periodic_update_check(self):
|
||||
"""Periodically check for module updates if auto_update_check is enabled."""
|
||||
if self.config["auto_update_check"]:
|
||||
await self.check_for_module_update()
|
||||
|
||||
async def check_for_module_update(self):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
async with session.get(self._base_url + "Limoka.py", timeout=10) as response:
|
||||
if response.status == 200:
|
||||
version = _parse_version_from_source(await response.text())
|
||||
if version is not None and version > __version__:
|
||||
markup = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=self.strings.get("install_button", "Install"),
|
||||
callback_data="limoka:update_module"
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
await self.inline.bot.send_message(
|
||||
self._tg_id,
|
||||
self.strings["update_available"].format(version='.'.join(str(v) for v in version)),
|
||||
reply_markup=markup
|
||||
)
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking for module update: {e}")
|
||||
|
||||
@loader.callback_handler()
|
||||
async def callback_handler(self, call: BotInlineCall):
|
||||
if call.data == "limoka:update_module":
|
||||
result = await self._install_module_limoka()
|
||||
call.as_(self.inline.bot)
|
||||
if result:
|
||||
await call.answer(f"✅ {self.strings['install_succeeded']}")
|
||||
else:
|
||||
await call.answer(f"❌ {self.strings['install_failed']}")
|
||||
|
||||
|
||||
def _create_search_session(
|
||||
self,
|
||||
@@ -668,8 +774,8 @@ class Limoka(loader.Module):
|
||||
self.repository = ModuleRepository(raw_modules, repositories)
|
||||
self.modules = self.repository.apply_newbie_filter(self.config["filter_newbies_modules"])
|
||||
|
||||
self._userbot_bot_username = (await self.inline.bot.get_me()).username
|
||||
self.formatter = CommandFormatter(self.strings, self._userbot_bot_username, self.get_prefix())
|
||||
self._self_bot_username = (await self.inline.bot.get_me()).username
|
||||
self.formatter = CommandFormatter(self.strings, self._self_bot_username, self.get_prefix())
|
||||
self.content_builder = ModuleContentBuilder(self.strings, self.formatter, self.repository)
|
||||
|
||||
self._service_bot_id = (await self.client.get_entity(self._bot_username)).id
|
||||
@@ -699,7 +805,7 @@ class Limoka(loader.Module):
|
||||
@loader.loop(interval=3600)
|
||||
async def _update_modules_loop(self):
|
||||
"""Periodically update modules list and rebuild index."""
|
||||
raw_modules = await self.api.fetch_json(self._base_url, "modules.json")
|
||||
await self.api.fetch_json(self._base_url, "modules.json")
|
||||
self.modules = self.repository.apply_newbie_filter(
|
||||
self.config.get("filter_newbies_modules", False)
|
||||
)
|
||||
@@ -740,21 +846,82 @@ class Limoka(loader.Module):
|
||||
logger.error(f"Skipping unsafe rmtree for {folder}")
|
||||
|
||||
async def _validate_url(self, url: str) -> Optional[str]:
|
||||
if not url or url in self._invalid_banners:
|
||||
logger.debug(f"_validate_url called with: {url}")
|
||||
if not url:
|
||||
logger.warning("_validate_url: URL is empty, returning None")
|
||||
return None
|
||||
if url in self._invalid_banners:
|
||||
logger.debug(f"_validate_url: URL already in invalid_banners: {url}, returning None")
|
||||
return None
|
||||
|
||||
# Headers to mimic a browser request
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
}
|
||||
|
||||
try:
|
||||
logger.debug(f"_validate_url: Starting validation for {url}")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.head(
|
||||
url, timeout=5, allow_redirects=True
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
ct = response.headers.get("Content-Type", "").lower()
|
||||
if not ct.startswith("image/"):
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
ct = None
|
||||
response_status = None
|
||||
|
||||
# Try HEAD first (more efficient)
|
||||
try:
|
||||
logger.debug(f"_validate_url: Attempting HEAD request for {url}")
|
||||
async with session.head(
|
||||
url, timeout=5, allow_redirects=True, headers=headers
|
||||
) as response:
|
||||
response_status = response.status
|
||||
logger.debug(f"_validate_url: HEAD request returned status {response.status} for {url}")
|
||||
if response.status == 200:
|
||||
ct = response.headers.get("Content-Type", "").lower()
|
||||
logger.debug(f"_validate_url: Content-Type from HEAD: '{ct}' for {url}")
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as head_error:
|
||||
logger.debug(f"_validate_url: HEAD failed ({type(head_error).__name__}), will try GET for {url}")
|
||||
|
||||
# If HEAD didn't work or returned non-200, try GET
|
||||
if ct is None:
|
||||
max_retries = 2
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
async with session.get(
|
||||
url, timeout=10, headers=headers, allow_redirects=True
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
ct = response.headers.get("Content-Type", "").lower()
|
||||
|
||||
# Try to get MIME if Content-Type is missing
|
||||
if not ct:
|
||||
try:
|
||||
data = await response.content.read(2048)
|
||||
mime = filetype.guess_mime(data, mime=True)
|
||||
if mime and mime.startswith("image/"):
|
||||
return url
|
||||
else:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
except Exception as mime_error:
|
||||
logger.error(f"_validate_url: Error reading content for MIME detection: {mime_error}")
|
||||
break # Success, exit retry loop
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as get_error:
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(1) # Wait before retry
|
||||
else:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
|
||||
# Check Content-Type from successful request
|
||||
if ct and ct.startswith("image/"):
|
||||
return url
|
||||
elif ct:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
else:
|
||||
self._invalid_banners.add(url)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
if url:
|
||||
self._invalid_banners.add(url)
|
||||
@@ -805,20 +972,20 @@ class Limoka(loader.Module):
|
||||
module_info: Dict[str, Any],
|
||||
query: str,
|
||||
filters: Dict[str, List[str]],
|
||||
url: str,
|
||||
include_categories: bool = True,
|
||||
module_path: Optional[str] = None,
|
||||
lang: str = "en",
|
||||
lang: str = "en"
|
||||
|
||||
) -> tuple:
|
||||
"""[DEPRECATED] Use ModuleContentBuilder.build_content instead."""
|
||||
return self.content_builder.build_content(
|
||||
module_info, query, filters, include_categories, module_path, lang
|
||||
module_info, query, filters, url, include_categories, module_path, lang
|
||||
)
|
||||
|
||||
def _build_navigation_markup(self, session: Dict[str, Any]) -> list:
|
||||
result = session["results"]
|
||||
index = session["current_index"]
|
||||
query = session["query"]
|
||||
filters = session["filters"]
|
||||
|
||||
page = index + 1
|
||||
markup = [
|
||||
@@ -872,8 +1039,6 @@ class Limoka(loader.Module):
|
||||
) -> list:
|
||||
result = session["results"]
|
||||
index = session["current_index"]
|
||||
query = session["query"]
|
||||
filters = session["filters"]
|
||||
|
||||
markup = []
|
||||
if len(body_pages) > 1:
|
||||
@@ -951,6 +1116,15 @@ class Limoka(loader.Module):
|
||||
},
|
||||
]
|
||||
)
|
||||
markup.append(
|
||||
[
|
||||
{
|
||||
"text": self.strings["install_button"],
|
||||
"callback": self._install_module,
|
||||
"args": (session,),
|
||||
},
|
||||
]
|
||||
)
|
||||
markup.append(
|
||||
[{"text": self.strings.get("close", "❌ Close"), "action": "close", "style": "danger"}]
|
||||
)
|
||||
@@ -1024,6 +1198,7 @@ class Limoka(loader.Module):
|
||||
include_categories=True,
|
||||
module_path=module_path,
|
||||
lang=lang,
|
||||
url=self._base_url
|
||||
)
|
||||
current_body = body_pages[min(page_body, len(body_pages) - 1)]
|
||||
full_message = header + current_body + footer + categories_text
|
||||
@@ -1070,12 +1245,36 @@ class Limoka(loader.Module):
|
||||
include_categories=True,
|
||||
module_path=module_path,
|
||||
lang=self.user_lang,
|
||||
url=self.config["limokaurl"]
|
||||
)
|
||||
new_page_body = min(page_body + 1, len(body_pages) - 1)
|
||||
await self._display_module(
|
||||
call, module_info, module_path, session, page_body=new_page_body
|
||||
)
|
||||
|
||||
async def _install_module(self, call: InlineCall, session: Dict[str, Any]):
|
||||
try:
|
||||
loader = self.lookup("Loader")
|
||||
await loader.download_and_install(f"{self.config['limokaurl']}{session['results'][session['current_index']]}")
|
||||
if getattr(loader, "fully_loaded", False):
|
||||
loader.update_modules_in_db()
|
||||
|
||||
except Exception:
|
||||
await call.answer(f"❌ {self.strings['install_failed']}", alert=True)
|
||||
else:
|
||||
await call.answer(f"✅ {self.strings['install_succeeded']}", alert=True)
|
||||
|
||||
async def _install_module_limoka(self):
|
||||
try:
|
||||
loader = self.lookup("Loader")
|
||||
await loader.download_and_install(f"{self.config['limokaurl']}Limoka.py")
|
||||
if getattr(loader, "fully_loaded", False):
|
||||
loader.update_modules_in_db()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception(f"Error updating Limoka module: {e}")
|
||||
return False
|
||||
|
||||
async def _display_filter_menu(self, call: InlineCall, session: Dict[str, Any]):
|
||||
query = session["query"]
|
||||
current_filters = session["filters"]
|
||||
@@ -1217,6 +1416,7 @@ class Limoka(loader.Module):
|
||||
result = searcher.search()
|
||||
except Exception:
|
||||
await call.edit(self.strings["?"], reply_markup=[])
|
||||
|
||||
return
|
||||
if not result:
|
||||
markup = (
|
||||
@@ -1570,7 +1770,8 @@ class Limoka(loader.Module):
|
||||
)
|
||||
try:
|
||||
result = SearchIndex(args.lower(), self.ix).search()
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception(f"Error occurred while searching: {e}")
|
||||
return await utils.answer(message, self.strings["?"])
|
||||
if not result:
|
||||
return await utils.answer(message, self.strings["404"].format(query=args))
|
||||
@@ -1589,6 +1790,29 @@ class Limoka(loader.Module):
|
||||
message, module_info, module_path, display_session, 0
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="— Обновить индекс ")
|
||||
async def updateindex(self, message: Message):
|
||||
"""— Update search index"""
|
||||
await utils.answer(message, self.strings["index_update_started"])
|
||||
try:
|
||||
await self._update_index()
|
||||
except Exception as e:
|
||||
logger.exception(f"Error updating index: {e}")
|
||||
await utils.answer(message, self.strings["index_update_failed"])
|
||||
else:
|
||||
await utils.answer(message, self.strings["index_update_success"])
|
||||
|
||||
@loader.command(ru_doc="— Проверить наличие обновлений модуля")
|
||||
async def limokaupdatecmd(self, message: Message):
|
||||
"""— Check for module updates"""
|
||||
await utils.answer(message, self.strings["checking_for_updates"])
|
||||
|
||||
is_update_available = await self.check_for_module_update()
|
||||
if is_update_available:
|
||||
await utils.answer(message, self.strings["module_update_available"].format(bot=self._self_bot_username))
|
||||
else:
|
||||
await utils.answer(message, self.strings["no_updates_available"])
|
||||
|
||||
async def _show_global_form(self, call: InlineCall, message: Message):
|
||||
markup = [
|
||||
[
|
||||
@@ -1742,135 +1966,135 @@ class Limoka(loader.Module):
|
||||
self.strings["history"].format(history="\n".join(formatted_history)),
|
||||
)
|
||||
|
||||
@loader.watcher(from_dl=False)
|
||||
async def secure_install_watcher(self, message: Message):
|
||||
if not message.text:
|
||||
return
|
||||
if not hasattr(message, "from_id") or not message.from_id:
|
||||
return
|
||||
sender_id = None
|
||||
if hasattr(message.from_id, "user_id"):
|
||||
sender_id = message.from_id.user_id
|
||||
elif hasattr(message.from_id, "channel_id"):
|
||||
sender_id = message.from_id.channel_id
|
||||
if sender_id != self._service_bot_id:
|
||||
# logger.debug("Message not from official bot, ignoring")
|
||||
return
|
||||
if not self.config["external_install_allowed"]:
|
||||
return
|
||||
try:
|
||||
clean_text = (
|
||||
getattr(message, "raw_text", None)
|
||||
or getattr(message, "message", None)
|
||||
or message.text
|
||||
or ""
|
||||
)
|
||||
if message.entities:
|
||||
from html import unescape
|
||||
# @loader.watcher(from_dl=False)
|
||||
# async def secure_install_watcher(self, message: Message):
|
||||
# if not message.text:
|
||||
# return
|
||||
# if not hasattr(message, "from_id") or not message.from_id:
|
||||
# return
|
||||
# sender_id = None
|
||||
# if hasattr(message.from_id, "user_id"):
|
||||
# sender_id = message.from_id.user_id
|
||||
# elif hasattr(message.from_id, "channel_id"):
|
||||
# sender_id = message.from_id.channel_id
|
||||
# if sender_id != self._service_bot_id:
|
||||
# # logger.debug("Message not from official bot, ignoring")
|
||||
# return
|
||||
# if not self.config["external_install_allowed"]:
|
||||
# return
|
||||
# try:
|
||||
# clean_text = (
|
||||
# getattr(message, "raw_text", None)
|
||||
# or getattr(message, "message", None)
|
||||
# or message.text
|
||||
# or ""
|
||||
# )
|
||||
# if message.entities:
|
||||
# from html import unescape
|
||||
|
||||
clean_text = unescape(clean_text)
|
||||
clean_text = re.sub(r"<[^>]+>", "", clean_text)
|
||||
match = re.search(r"#limoka:([^\s\"'<>]+)", clean_text)
|
||||
if not match:
|
||||
logger.debug(
|
||||
"No #limoka tag found in cleaned text; leaving original message intact"
|
||||
)
|
||||
return
|
||||
tag_content = match.group(1)
|
||||
parts = tag_content.split(":", 1)
|
||||
if len(parts) != 2:
|
||||
logger.error("Invalid tag format after cleaning")
|
||||
await utils.answer(message, self.strings["watcher_invalid_format"])
|
||||
return
|
||||
module_path, signature_hex = parts
|
||||
module_path = re.sub(r"[<>\"']", "", module_path).strip()
|
||||
if module_path.startswith("href="):
|
||||
module_path = module_path[5:].strip('"').strip("'")
|
||||
if module_path not in self.modules:
|
||||
found = False
|
||||
for db_path in self.modules.keys():
|
||||
if module_path in db_path or db_path in module_path:
|
||||
module_path = db_path
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
logger.warning(f"Module not found after cleanup: {module_path}")
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["watcher_module_not_found"].format(
|
||||
path=html.escape(module_path)
|
||||
),
|
||||
)
|
||||
return
|
||||
try:
|
||||
import base64
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
# clean_text = unescape(clean_text)
|
||||
# clean_text = re.sub(r"<[^>]+>", "", clean_text)
|
||||
# match = re.search(r"#limoka:([^\s\"'<>]+)", clean_text)
|
||||
# if not match:
|
||||
# logger.debug(
|
||||
# "No #limoka tag found in cleaned text; leaving original message intact"
|
||||
# )
|
||||
# return
|
||||
# tag_content = match.group(1)
|
||||
# parts = tag_content.split(":", 1)
|
||||
# if len(parts) != 2:
|
||||
# logger.error("Invalid tag format after cleaning")
|
||||
# await utils.answer(message, self.strings["watcher_invalid_format"])
|
||||
# return
|
||||
# module_path, signature_hex = parts
|
||||
# module_path = re.sub(r"[<>\"']", "", module_path).strip()
|
||||
# if module_path.startswith("href="):
|
||||
# module_path = module_path[5:].strip('"').strip("'")
|
||||
# if module_path not in self.modules:
|
||||
# found = False
|
||||
# for db_path in self.modules.keys():
|
||||
# if module_path in db_path or db_path in module_path:
|
||||
# module_path = db_path
|
||||
# found = True
|
||||
# break
|
||||
# if not found:
|
||||
# logger.warning(f"Module not found after cleanup: {module_path}")
|
||||
# await utils.answer(
|
||||
# message,
|
||||
# self.strings["watcher_module_not_found"].format(
|
||||
# path=html.escape(module_path)
|
||||
# ),
|
||||
# )
|
||||
# return
|
||||
# try:
|
||||
# import base64
|
||||
# from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
|
||||
PUB_KEY_B64 = (
|
||||
"MCowBQYDK2VwAyEA1ltSnqtf3pGBuctuAYqHivCXsaRtKOVxavai7yin7ZE="
|
||||
)
|
||||
der_bytes = base64.b64decode(PUB_KEY_B64)
|
||||
raw_pubkey = der_bytes[-32:]
|
||||
module_url = self.config["limokaurl"] + module_path
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(module_url, timeout=10) as resp:
|
||||
if resp.status != 200:
|
||||
logger.error(
|
||||
f"Failed to fetch module for verification: {module_url} (HTTP {resp.status})"
|
||||
)
|
||||
await utils.answer(
|
||||
message, self.strings["watcher_loader_missing"]
|
||||
)
|
||||
return
|
||||
module_bytes = await resp.read()
|
||||
sha256 = hashlib.sha256(module_bytes).hexdigest()
|
||||
public_key = ed25519.Ed25519PublicKey.from_public_bytes(
|
||||
raw_pubkey
|
||||
)
|
||||
signature = bytes.fromhex(signature_hex)
|
||||
signed_payload = f"{module_path}|{sha256}".encode()
|
||||
public_key.verify(signature, signed_payload)
|
||||
except Exception as e:
|
||||
logger.error(f"Signature verification failed for {module_path}: {e}")
|
||||
await utils.answer(message, self.strings["watcher_signature_invalid"])
|
||||
return
|
||||
loader_mod = self.lookup("loader")
|
||||
if not loader_mod:
|
||||
logger.error("Loader module not found")
|
||||
await utils.answer(message, self.strings["watcher_loader_missing"])
|
||||
return
|
||||
module_url = self.config["limokaurl"] + module_path
|
||||
status = await loader_mod.download_and_install(module_url, None)
|
||||
if getattr(loader_mod, "fully_loaded", False):
|
||||
loader_mod.update_modules_in_db()
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete message: {e}")
|
||||
if status:
|
||||
try:
|
||||
bot_peer = await self.client.get_entity(self._service_bot_id)
|
||||
await self.client.send_message(
|
||||
bot_peer, f"#limoka:sucsess:{message.id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send success confirmation: {e}")
|
||||
else:
|
||||
logger.error(f"Installation failed with status: {status}")
|
||||
try:
|
||||
bot_peer = await self.client.get_entity(self._service_bot_id)
|
||||
await self.client.send_message(
|
||||
bot_peer, f"#limoka:failed:{message.id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send failure notification: {e}")
|
||||
except Exception as e:
|
||||
logger.exception(f"CRITICAL ERROR in secure_install_watcher: {e}")
|
||||
try:
|
||||
await utils.answer(
|
||||
message, self.strings["watcher_critical"].format(error=str(e)[:100])
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
await message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
# PUB_KEY_B64 = (
|
||||
# "MCowBQYDK2VwAyEA1ltSnqtf3pGBuctuAYqHivCXsaRtKOVxavai7yin7ZE="
|
||||
# )
|
||||
# der_bytes = base64.b64decode(PUB_KEY_B64)
|
||||
# raw_pubkey = der_bytes[-32:]
|
||||
# module_url = self.config["limokaurl"] + module_path
|
||||
# async with aiohttp.ClientSession() as session:
|
||||
# async with session.get(module_url, timeout=10) as resp:
|
||||
# if resp.status != 200:
|
||||
# logger.error(
|
||||
# f"Failed to fetch module for verification: {module_url} (HTTP {resp.status})"
|
||||
# )
|
||||
# await utils.answer(
|
||||
# message, self.strings["watcher_loader_missing"]
|
||||
# )
|
||||
# return
|
||||
# module_bytes = await resp.read()
|
||||
# sha256 = hashlib.sha256(module_bytes).hexdigest()
|
||||
# public_key = ed25519.Ed25519PublicKey.from_public_bytes(
|
||||
# raw_pubkey
|
||||
# )
|
||||
# signature = bytes.fromhex(signature_hex)
|
||||
# signed_payload = f"{module_path}|{sha256}".encode()
|
||||
# public_key.verify(signature, signed_payload)
|
||||
# except Exception as e:
|
||||
# logger.error(f"Signature verification failed for {module_path}: {e}")
|
||||
# await utils.answer(message, self.strings["watcher_signature_invalid"])
|
||||
# return
|
||||
# loader_mod = self.lookup("loader")
|
||||
# if not loader_mod:
|
||||
# logger.error("Loader module not found")
|
||||
# await utils.answer(message, self.strings["watcher_loader_missing"])
|
||||
# return
|
||||
# module_url = self.config["limokaurl"] + module_path
|
||||
# status = await loader_mod.download_and_install(module_url, None)
|
||||
# if getattr(loader_mod, "fully_loaded", False):
|
||||
# loader_mod.update_modules_in_db()
|
||||
# try:
|
||||
# await message.delete()
|
||||
# except Exception as e:
|
||||
# logger.error(f"Failed to delete message: {e}")
|
||||
# if status:
|
||||
# try:
|
||||
# bot_peer = await self.client.get_entity(self._service_bot_id)
|
||||
# await self.client.send_message(
|
||||
# bot_peer, f"#limoka:sucsess:{message.id}"
|
||||
# )
|
||||
# except Exception as e:
|
||||
# logger.error(f"Failed to send success confirmation: {e}")
|
||||
# else:
|
||||
# logger.error(f"Installation failed with status: {status}")
|
||||
# try:
|
||||
# bot_peer = await self.client.get_entity(self._service_bot_id)
|
||||
# await self.client.send_message(
|
||||
# bot_peer, f"#limoka:failed:{message.id}"
|
||||
# )
|
||||
# except Exception as e:
|
||||
# logger.error(f"Failed to send failure notification: {e}")
|
||||
# except Exception as e:
|
||||
# logger.exception(f"CRITICAL ERROR in secure_install_watcher: {e}")
|
||||
# try:
|
||||
# await utils.answer(
|
||||
# message, self.strings["watcher_critical"].format(error=str(e)[:100])
|
||||
# )
|
||||
# await asyncio.sleep(5)
|
||||
# await message.delete()
|
||||
# except Exception:
|
||||
# pass
|
||||
@@ -760,7 +760,7 @@ class Limoka(loader.Module):
|
||||
),
|
||||
},
|
||||
{
|
||||
"text": f"{self.strings["body_page"]} {page_body + 1}/{len(body_pages)}",
|
||||
"text": f"{self.strings['body_page']} {page_body + 1}/{len(body_pages)}",
|
||||
"callback": self._inline_void,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#Midga3
|
||||
#Placeholder system is the best
|
||||
|
||||
# meta banner: https://github.com/Midga3/heroku-modules/blob/main/new_module.jpg?raw=true
|
||||
# meta developer: @midga3_modules
|
||||
__version__ = (1, 0, 0)
|
||||
__version__ = (1, 1, 2)
|
||||
|
||||
import logging
|
||||
import aiohttp
|
||||
@@ -17,13 +16,20 @@ class PingEmoji(loader.Module):
|
||||
strings = {
|
||||
"name": "PingEmoji"
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"emoji",
|
||||
"<tg-emoji emoji-id=5276307163529092252>🔴</tg-emoji>",
|
||||
"Ping Emoji",
|
||||
)
|
||||
)
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
utils.register_placeholder("ping_emoji", self.get_emoji)
|
||||
|
||||
async def get_emoji(self, data):
|
||||
if data['ping'] > 300:
|
||||
return "<tg-emoji emoji-id=5276307163529092252>🔴</tg-emoji>"
|
||||
return self.config['emoji']
|
||||
else:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
157
Midga3/Heroku-modules/wordle.py
Normal file
157
Midga3/Heroku-modules/wordle.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# Midga3
|
||||
|
||||
# I AM NOT AFFICIATED WITH WORDLE
|
||||
|
||||
# meta developer: @midga3_modules
|
||||
|
||||
import requests
|
||||
import random
|
||||
import logging
|
||||
from .. import loader, utils
|
||||
from herokutl.tl.types import Message
|
||||
__verison__ = (0, 1, 1)
|
||||
logger = logging.getLogger(__name__)
|
||||
@loader.tds
|
||||
class wordle(loader.Module):
|
||||
"""Wordle!"""
|
||||
strings = {
|
||||
"name": "Wordle",
|
||||
"loading": "Loading...",
|
||||
"language": "Language of the wordle",
|
||||
"have_a_good_game": "Have a good game! Try to guess the 5 letter word in {}",
|
||||
"attempts_left": "WRONG! Attempts left: {}",
|
||||
"gg": "GG! YOU DIDN'T GUESS THE WORD {}",
|
||||
"win": "GG! YOU WON! THE WORD WAS {}",
|
||||
"already_playing": "ALREADY PLAYING! type .stopwordle to stop the current game",
|
||||
"length": "Must be 5 letters",
|
||||
"no_game": "No game is currently running",
|
||||
"ok": "Game stopped",
|
||||
"ad": "I tried to Guess word {}. Check out my result:\n{}",
|
||||
"real_word": "This word is not in the word list"
|
||||
}
|
||||
strings_ru ={
|
||||
"name": "Wordle",
|
||||
"loading": "Загрузка...",
|
||||
"language": "Язык вордла",
|
||||
"have_a_good_game": "Хорошей игры! Попытайтесь угадать слово из 5 букв на {}",
|
||||
"attempts_left": "НВЕВЕРНО! Осталось попыток: {}",
|
||||
"gg": "ГГ! ВЫ НЕ УГАДАЛИ СЛОВО {}",
|
||||
"win": "ГГ! ВЫ ВЫИГРАЛИ! СЛОВО БЫЛО {}",
|
||||
"already_playing": "УЖЕ ИГРАЕТЕ! напишите .stopwordle чтобы остановить текущую игру",
|
||||
"length": "Должно содержать 5 букв",
|
||||
"no_game": "Сейчас нет активной игры",
|
||||
"ok": "Игра остановлена",
|
||||
"ad": "Я попытался угадать слово {}. Чекайте мой результат:\n{}",
|
||||
"real_word": "Такого слова нет в списке слов"
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"language",
|
||||
"en",
|
||||
self.strings["language"],
|
||||
validator=loader.validators.Choice(["en", "ru"])
|
||||
),
|
||||
)
|
||||
async def handler(self, call, data):
|
||||
guess = data.upper()
|
||||
word = self._db.get("wordle", "word", "")
|
||||
attempts = self._db.get("wordle", "attempts", 0)
|
||||
buttons = self._db.get("wordle", "buttons", [])
|
||||
markup = buttons + [[{"text":"Введите слово","input":self.strings("length"),"handler": self.handler}]]
|
||||
|
||||
if len(guess) != 5:
|
||||
await call.edit(self.strings("length"), reply_markup=markup)
|
||||
return
|
||||
|
||||
if guess not in self._db.get("wordle", "words", []):
|
||||
await call.edit(self.strings("real_word"), reply_markup=markup)
|
||||
return
|
||||
buttons2 = []
|
||||
for i in range(5):
|
||||
if guess[i] == word[i]:
|
||||
buttons2.append({"text": guess[i], "data": "custom/data", "style": "success"})
|
||||
elif guess[i] in word:
|
||||
buttons2.append({"text": guess[i], "data": "custom/data", "style": "primary"})
|
||||
else:
|
||||
buttons2.append({"text": guess[i], "data": "custom/data", "style": "danger"})
|
||||
|
||||
buttons.append(buttons2)
|
||||
|
||||
if guess == word:
|
||||
self._db.set("wordle", "buttons", buttons)
|
||||
self._db.set("wordle", "now_playing", False)
|
||||
result = ""
|
||||
for btn in buttons:
|
||||
for b in btn:
|
||||
if b["style"] == "success":
|
||||
result += "🟩"
|
||||
elif b["style"] == "primary":
|
||||
result += "🟨"
|
||||
else:
|
||||
result += "⬛"
|
||||
result += "\n"
|
||||
buttons.append([{"text":"Поделится резултатом","copy":self.strings("ad").format(word, result)}])
|
||||
await call.edit(f"{self.strings('win').format(word)}", reply_markup=buttons)
|
||||
return
|
||||
|
||||
self._db.set("wordle", "buttons", buttons)
|
||||
attempts -= 1
|
||||
self._db.set("wordle", "attempts", attempts)
|
||||
|
||||
if attempts == 0:
|
||||
result = ""
|
||||
for btn in buttons:
|
||||
for b in btn:
|
||||
if b["style"] == "success":
|
||||
result += "🟩"
|
||||
elif b["style"] == "primary":
|
||||
result += "🟨"
|
||||
else:
|
||||
result += "⬛"
|
||||
result += "\n"
|
||||
buttons.append([{"text":"Поделится резултатом","copy":self.strings("ad").format(word, result)}])
|
||||
await call.edit(f"{self.strings('gg').format(word)}", reply_markup=buttons)
|
||||
self._db.set("wordle", "now_playing", False)
|
||||
else:
|
||||
await call.edit(f"{self.strings('attempts_left').format(attempts)}", reply_markup=markup)
|
||||
|
||||
@loader.command()
|
||||
async def wordle(self, message: Message):
|
||||
"""Play wordle!"""
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
if self._db.get("wordle", "now_playing", False):
|
||||
await utils.answer(message, self.strings("already_playing"))
|
||||
return
|
||||
args = utils.get_args(message)
|
||||
if args and args[0].lower():
|
||||
if args[0].lower() == "--no-sec":
|
||||
self._db.set("wordle", "nosec", True)
|
||||
return
|
||||
else:
|
||||
self._db.set("wordle", "nosec", False)
|
||||
try:
|
||||
response = requests.get(f"https://raw.githubusercontent.com/mimimishka449/Worlde/refs/heads/main/words_{self.config['language']}.txt")
|
||||
if response.status_code == 200:
|
||||
words = response.text.splitlines()
|
||||
word = random.choice(words).upper()
|
||||
self._db.set("wordle", "now_playing", True)
|
||||
self._db.set("wordle", "attempts", 6)
|
||||
self._db.set("wordle", "word", word)
|
||||
self._db.set("wordle", "words", words)
|
||||
self._db.set("wordle", "buttons", [])
|
||||
await self.inline.form(self.strings("have_a_good_game").format("английском" if self.config['language'] == "en" else "русском"), message, reply_markup=[[{"text":"Введите слово","input":self.strings("length"),"handler": self.handler}]])
|
||||
else:
|
||||
await utils.answer(message, "Error fetching wordle data.")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error: {e}")
|
||||
await utils.answer(message, "An error occurred while fetching wordle data.")
|
||||
@loader.command()
|
||||
async def stopwordle(self, message: Message):
|
||||
"""Stop the wordle game."""
|
||||
if not self._db.get("wordle", "now_playing", False):
|
||||
await utils.answer(message, self.strings("no_game"))
|
||||
return
|
||||
self._db.set("wordle", "now_playing", False)
|
||||
await utils.answer(message, self.strings("ok"))
|
||||
@@ -1,150 +1,122 @@
|
||||
#meta developer: @matubuntu
|
||||
import requests, bs4
|
||||
from datetime import datetime
|
||||
from .. import loader, utils
|
||||
import lxml
|
||||
# meta developer: @matubuntu
|
||||
|
||||
# requires: lxml requests bs4
|
||||
import time
|
||||
from datetime import datetime
|
||||
import aiohttp
|
||||
from .. import loader, utils
|
||||
|
||||
_FLAGS = {
|
||||
"AUD": "🇦🇺",
|
||||
"AZN": "🇦🇿",
|
||||
"GBP": "🇬🇧",
|
||||
"AMD": "🇦🇲",
|
||||
"BYN": "🇧🇾",
|
||||
"BGN": "🇧🇬",
|
||||
"BRL": "🇧🇷",
|
||||
"HUF": "🇭🇺",
|
||||
"VND": "🇻🇳",
|
||||
"HKD": "🇭🇰",
|
||||
"GEL": "🇬🇪",
|
||||
"DKK": "🇩🇰",
|
||||
"AED": "🇦🇪",
|
||||
"USD": "🇺🇸",
|
||||
"EUR": "🇪🇺",
|
||||
"EGP": "🇪🇬",
|
||||
"INR": "🇮🇳",
|
||||
"IDR": "🇮🇩",
|
||||
"KZT": "🇰🇿",
|
||||
"CAD": "🇨🇦",
|
||||
"QAR": "🇶🇦",
|
||||
"KGS": "🇰🇬",
|
||||
"CNY": "🇨🇳",
|
||||
"MDL": "🇲🇩",
|
||||
"NZD": "🇳🇿",
|
||||
"NOK": "🇳🇴",
|
||||
"PLN": "🇵🇱",
|
||||
"RON": "🇷🇴",
|
||||
"SGD": "🇸🇬",
|
||||
"TJS": "🇹🇯",
|
||||
"THB": "🇹🇭",
|
||||
"TRY": "🇹🇷",
|
||||
"TMT": "🇹🇲",
|
||||
"UZS": "🇺🇿",
|
||||
"UAH": "🇺🇦",
|
||||
"CZK": "🇨🇿",
|
||||
"SEK": "🇸🇪",
|
||||
"CHF": "🇨🇭",
|
||||
"RSD": "🇷🇸",
|
||||
"ZAR": "🇿🇦",
|
||||
"KRW": "🇰🇷",
|
||||
"JPY": "🇯🇵",
|
||||
"AUD": "🇦🇺", "AZN": "🇦🇿", "GBP": "🇬🇧", "AMD": "🇦🇲",
|
||||
"BYN": "🇧🇾", "BGN": "🇧🇬", "BRL": "🇧🇷", "HUF": "🇭🇺",
|
||||
"VND": "🇻🇳", "HKD": "🇭🇰", "GEL": "🇬🇪", "DKK": "🇩🇰",
|
||||
"AED": "🇦🇪", "USD": "🇺🇸", "EUR": "🇪🇺", "EGP": "🇪🇬",
|
||||
"INR": "🇮🇳", "IDR": "🇮🇩", "KZT": "🇰🇿", "CAD": "🇨🇦",
|
||||
"QAR": "🇶🇦", "KGS": "🇰🇬", "CNY": "🇨🇳", "MDL": "🇲🇩",
|
||||
"NZD": "🇳🇿", "NOK": "🇳🇴", "PLN": "🇵🇱", "RON": "🇷🇴",
|
||||
"SGD": "🇸🇬", "TJS": "🇹🇯", "THB": "🇹🇭", "TRY": "🇹🇷",
|
||||
"TMT": "🇹🇲", "UZS": "🇺🇿", "UAH": "🇺🇦", "CZK": "🇨🇿",
|
||||
"SEK": "🇸🇪", "CHF": "🇨🇭", "RSD": "🇷🇸", "ZAR": "🇿🇦",
|
||||
"KRW": "🇰🇷", "JPY": "🇯🇵",
|
||||
}
|
||||
|
||||
_CRYPTO_EMOJIS = {
|
||||
"BTC": "<emoji document_id=5289519973285257969>💰</emoji>",
|
||||
"ETH": "<emoji document_id=5287735049301550386>💰</emoji>",
|
||||
"SOL": "<emoji document_id=5251712673258697260>💰</emoji>",
|
||||
"TON": "<emoji document_id=5289648693455119919>💰</emoji>",
|
||||
"USDT": "<emoji document_id=5289904548951911168>💰</emoji>",
|
||||
"XRP": "<emoji document_id=5373312921214401986>💰</emoji>",
|
||||
"USDC": "<emoji document_id=5372958453268497353>💰</emoji>",
|
||||
"ADA": "<emoji document_id=5373076801092338046>💰</emoji>",
|
||||
"DOGE": "<emoji document_id=5375192042420842380>💰</emoji>",
|
||||
"TRX": "<emoji document_id=5375187081733616165>💰</emoji>",
|
||||
"AVAX": "<emoji document_id=5375311275007947936>💰</emoji>",
|
||||
"LTC": "<emoji document_id=5373035462032113888>💰</emoji>",
|
||||
"BCH": "<emoji document_id=5375596920397903962>💰</emoji>",
|
||||
"ATOM": "<emoji document_id=5375468745688889977>💰</emoji>",
|
||||
"XLM": "<emoji document_id=5372823290647690288>💰</emoji>",
|
||||
"SHIB": "<emoji document_id=5375231036428924778>💰</emoji>",
|
||||
"UNI": "<emoji document_id=5372953110329180525>💰</emoji>",
|
||||
"XMR": "<emoji document_id=5375507073977038661>💰</emoji>",
|
||||
"LINK": "<emoji document_id=5375149651093633217>💰</emoji>",
|
||||
"ETC": "<emoji document_id=5375543306321146693>💰</emoji>",
|
||||
"SUI": "<emoji document_id=5391002164929772708>💰</emoji>",
|
||||
"NEAR": "<emoji document_id=5391181990915487346>💰</emoji>",
|
||||
"VET": "<emoji document_id=5391091302681033446>💰</emoji>",
|
||||
"FIL": "<emoji document_id=5373117173784919811>💰</emoji>",
|
||||
"XTZ": "<emoji document_id=5390985478981829698>💰</emoji>",
|
||||
"ALGO": "<emoji document_id=5391337713544738420>💰</emoji>",
|
||||
"BTC": "<emoji document_id=5289519973285257969>💰</emoji>",
|
||||
"ETH": "<emoji document_id=5287735049301550386>💰</emoji>",
|
||||
"SOL": "<emoji document_id=5251712673258697260>💰</emoji>",
|
||||
"TON": "<emoji document_id=5289648693455119919>💰</emoji>",
|
||||
"USDT": "<emoji document_id=5289904548951911168>💰</emoji>",
|
||||
"XRP": "<emoji document_id=5373312921214401986>💰</emoji>",
|
||||
"USDC": "<emoji document_id=5372958453268497353>💰</emoji>",
|
||||
"ADA": "<emoji document_id=5373076801092338046>💰</emoji>",
|
||||
"DOGE": "<emoji document_id=5375192042420842380>💰</emoji>",
|
||||
"TRX": "<emoji document_id=5375187081733616165>💰</emoji>",
|
||||
"AVAX": "<emoji document_id=5375311275007947936>💰</emoji>",
|
||||
"LTC": "<emoji document_id=5373035462032113888>💰</emoji>",
|
||||
"BCH": "<emoji document_id=5375596920397903962>💰</emoji>",
|
||||
"ATOM": "<emoji document_id=5375468745688889977>💰</emoji>",
|
||||
"XLM": "<emoji document_id=5372823290647690288>💰</emoji>",
|
||||
"SHIB": "<emoji document_id=5375231036428924778>💰</emoji>",
|
||||
"UNI": "<emoji document_id=5372953110329180525>💰</emoji>",
|
||||
"XMR": "<emoji document_id=5375507073977038661>💰</emoji>",
|
||||
"LINK": "<emoji document_id=5375149651093633217>💰</emoji>",
|
||||
"ETC": "<emoji document_id=5375543306321146693>💰</emoji>",
|
||||
"SUI": "<emoji document_id=5391002164929772708>💰</emoji>",
|
||||
"NEAR": "<emoji document_id=5391181990915487346>💰</emoji>",
|
||||
"VET": "<emoji document_id=5391091302681033446>💰</emoji>",
|
||||
"FIL": "<emoji document_id=5373117173784919811>💰</emoji>",
|
||||
"XTZ": "<emoji document_id=5390985478981829698>💰</emoji>",
|
||||
"ALGO": "<emoji document_id=5391337713544738420>💰</emoji>",
|
||||
"THETA": "<emoji document_id=5391256014676833736>💰</emoji>",
|
||||
"FTM": "<emoji document_id=5393179395521263785>💰</emoji>",
|
||||
"XDAI": "<emoji document_id=5391325992578988886>💰</emoji>",
|
||||
"RUNE": "<emoji document_id=5391347570494684983>💰</emoji>",
|
||||
"DOT": "<emoji document_id=5375224568208177973>💰</emoji>",
|
||||
"FTM": "<emoji document_id=5393179395521263785>💰</emoji>",
|
||||
"XDAI": "<emoji document_id=5391325992578988886>💰</emoji>",
|
||||
"RUNE": "<emoji document_id=5391347570494684983>💰</emoji>",
|
||||
"DOT": "<emoji document_id=5375224568208177973>💰</emoji>",
|
||||
}
|
||||
|
||||
_CRYPTO_LIST = {
|
||||
"BTC": "Bitcoin",
|
||||
"ETH": "Ethereum",
|
||||
"XMR": "Monero",
|
||||
"LTC": "Litecoin",
|
||||
"XRP": "XRP",
|
||||
"ADA": "Cardano",
|
||||
"DOGE": "Dogecoin",
|
||||
"SOL": "Solana",
|
||||
"DOT": "Polkadot",
|
||||
"USDT": "Tether",
|
||||
"TON": "Toncoin",
|
||||
"USDC": "USD Coin",
|
||||
"TRX": "TRON",
|
||||
"AVAX": "Avalanche",
|
||||
"BCH": "Bitcoin Cash",
|
||||
"ATOM": "Cosmos",
|
||||
"XLM": "Stellar",
|
||||
"SHIB": "Shiba Inu",
|
||||
"UNI": "Uniswap",
|
||||
"LINK": "Chainlink",
|
||||
"ETC": "Ethereum Classic",
|
||||
"SUI": "Sui",
|
||||
"NEAR": "NEAR Protocol",
|
||||
"VET": "VeChain",
|
||||
"FIL": "Filecoin",
|
||||
"XTZ": "Tezos",
|
||||
"ALGO": "Algorand",
|
||||
"THETA": "Theta Network",
|
||||
"FTM": "Fantom",
|
||||
"XDAI": "xDai",
|
||||
_CRYPTO_NAMES = {
|
||||
"BTC": "Bitcoin", "ETH": "Ethereum", "XMR": "Monero",
|
||||
"LTC": "Litecoin", "XRP": "XRP", "ADA": "Cardano",
|
||||
"DOGE": "Dogecoin", "SOL": "Solana", "DOT": "Polkadot",
|
||||
"USDT": "Tether", "TON": "Toncoin", "USDC": "USD Coin",
|
||||
"TRX": "TRON", "AVAX": "Avalanche", "BCH": "Bitcoin Cash",
|
||||
"ATOM": "Cosmos", "XLM": "Stellar", "SHIB": "Shiba Inu",
|
||||
"UNI": "Uniswap", "LINK": "Chainlink", "ETC": "Ethereum Classic",
|
||||
"SUI": "Sui", "NEAR": "NEAR Protocol", "VET": "VeChain",
|
||||
"FIL": "Filecoin", "XTZ": "Tezos", "ALGO": "Algorand",
|
||||
"THETA": "Theta Network", "FTM": "Fantom", "XDAI": "xDai",
|
||||
"RUNE": "THORChain",
|
||||
}
|
||||
|
||||
def _fmt_num(v, d=3):
|
||||
p = f"{v:,.{d}f}".replace(",", " ").split(".")
|
||||
i = p[0]
|
||||
d = p[1].rstrip("0") if len(p) > 1 else ""
|
||||
return f"{i},{d}" if d else i
|
||||
_CBR_URL = "https://www.cbr.ru/scripts/XML_daily.asp"
|
||||
_CRYPTO_URL = "https://api.coinlore.net/api/tickers/?limit=100"
|
||||
|
||||
CACHE_TTL = 300 # seconds
|
||||
|
||||
|
||||
def _fmt_num(value: float, decimals: int = 3) -> str:
|
||||
if decimals == 0:
|
||||
return f"{int(value):,}".replace(",", " ")
|
||||
rounded = round(value, decimals)
|
||||
int_part = int(rounded)
|
||||
dec_part = str(rounded - int_part)[2:2 + decimals].rstrip("0")
|
||||
int_str = f"{int_part:,}".replace(",", " ")
|
||||
return f"{int_str},{dec_part}" if dec_part else int_str
|
||||
|
||||
|
||||
def _parse_cbr_xml(xml_bytes: bytes) -> tuple[str | None, dict]:
|
||||
"""Parse CBR XML without bs4/lxml — pure stdlib ElementTree."""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
root = ET.fromstring(xml_bytes)
|
||||
date_str = root.attrib.get("Date", "")
|
||||
try:
|
||||
date = datetime.strptime(date_str, "%d.%m.%Y").strftime("%d.%m.%Y")
|
||||
except ValueError:
|
||||
date = date_str
|
||||
|
||||
rates: dict[str, dict] = {}
|
||||
for valute in root.findall("Valute"):
|
||||
code = valute.findtext("CharCode", "").strip()
|
||||
if not code or code == "XDR":
|
||||
continue
|
||||
try:
|
||||
nominal = float(valute.findtext("Nominal", "1").replace(",", "."))
|
||||
value = float(valute.findtext("Value", "0").replace(",", "."))
|
||||
except ValueError:
|
||||
continue
|
||||
rates[code] = {
|
||||
"name": valute.findtext("Name", code).strip(),
|
||||
"nominal": nominal,
|
||||
"rub": value / nominal,
|
||||
}
|
||||
return date, rates
|
||||
|
||||
|
||||
@loader.tds
|
||||
class FinanceMod(loader.Module):
|
||||
strings = {
|
||||
"name": "FinanceMod",
|
||||
"valute_description": "<кол-во> <код> - курс валюты\n<кол-во> - список",
|
||||
"valute_no_args": (
|
||||
"💵 <b>Курс валюты с сайта </b><a href='https://www.cbr.ru/'>ЦБ(РФ)</a>\n"
|
||||
"<b>Актуально на</b> <i>{}</i>\n\n<blockquote expandable>{}</blockquote>"
|
||||
),
|
||||
"valute_specific": (
|
||||
"💵 <b>Курс валюты с сайта </b><a href='https://www.cbr.ru/'>ЦБ(РФ)</a>\n"
|
||||
"<b>Актуально на</b> <i>{}</i>\n\n{}"
|
||||
),
|
||||
"valute_not_found": "🚫 Валюта {} не найдена",
|
||||
"crypto_description": "<кол-во> <код> - курс крипты\n<кол-во> - список",
|
||||
"crypto_no_args": "💎 <b>Курсы криптовалют</b>\n\n<blockquote expandable>{}</blockquote>",
|
||||
"crypto_specific": "💎 <b>Курс криптовалюты</b>\n\n{}",
|
||||
"crypto_not_found": "🚫 Криптовалюта {} не найдена",
|
||||
"error": "🚫 Ошибка получения данных",
|
||||
}
|
||||
"""Курсы валют (ЦБ РФ) и криптовалют (CoinLore)"""
|
||||
|
||||
strings = {"name": "FinanceMod"}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
@@ -152,149 +124,194 @@ class FinanceMod(loader.Module):
|
||||
"crypto_currency",
|
||||
"USD",
|
||||
lambda: "Валюта для отображения крипты (USD, RUB, EUR)",
|
||||
validator=loader.validators.Choice(["USD", "RUB", "EUR"])
|
||||
validator=loader.validators.Choice(["USD", "RUB", "EUR"]),
|
||||
)
|
||||
)
|
||||
# Simple in-process cache
|
||||
self._cbr_cache: tuple[float, str, dict] | None = None # (ts, date, rates)
|
||||
self._crypto_cache: tuple[float, list] | None = None # (ts, data)
|
||||
|
||||
async def _get_curr_data(self):
|
||||
# ──────────────────────────── HTTP helpers ────────────────────────────
|
||||
|
||||
async def _fetch(self, url: str, *, as_json: bool = False):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
|
||||
resp.raise_for_status()
|
||||
return await resp.json() if as_json else await resp.read()
|
||||
|
||||
# ──────────────────────────── CBR data ────────────────────────────────
|
||||
|
||||
async def _cbr_data(self) -> tuple[str | None, dict]:
|
||||
now = time.monotonic()
|
||||
if self._cbr_cache and now - self._cbr_cache[0] < CACHE_TTL:
|
||||
return self._cbr_cache[1], self._cbr_cache[2]
|
||||
try:
|
||||
r = requests.get("https://www.cbr.ru/scripts/XML_daily.asp")
|
||||
s = bs4.BeautifulSoup(r.content, 'xml')
|
||||
d = datetime.strptime(s.ValCurs['Date'], "%d.%m.%Y").strftime("%d.%m.%Y")
|
||||
return d, s.find_all('Valute')
|
||||
except:
|
||||
return None, None
|
||||
raw = await self._fetch(_CBR_URL)
|
||||
date, rates = _parse_cbr_xml(raw)
|
||||
self._cbr_cache = (now, date, rates)
|
||||
return date, rates
|
||||
except Exception:
|
||||
if self._cbr_cache:
|
||||
return self._cbr_cache[1], self._cbr_cache[2]
|
||||
return None, {}
|
||||
|
||||
async def _get_rates(self):
|
||||
# ──────────────────────────── Crypto data ─────────────────────────────
|
||||
|
||||
async def _crypto_data(self) -> list:
|
||||
now = time.monotonic()
|
||||
if self._crypto_cache and now - self._crypto_cache[0] < CACHE_TTL:
|
||||
return self._crypto_cache[1]
|
||||
try:
|
||||
r = requests.get("https://www.cbr.ru/scripts/XML_daily.asp")
|
||||
s = bs4.BeautifulSoup(r.content, 'xml')
|
||||
rt = {'USD': None, 'EUR': None}
|
||||
for v in s.find_all('Valute'):
|
||||
if v.CharCode.text in ['USD', 'EUR']:
|
||||
n = float(v.Nominal.text.replace(',', '.'))
|
||||
vl = float(v.Value.text.replace(',', '.'))
|
||||
rt[v.CharCode.text] = vl / n
|
||||
if rt['USD'] and rt['EUR']:
|
||||
rt['EUR_USD'] = rt['USD'] / rt['EUR']
|
||||
else:
|
||||
rt['EUR_USD'] = None
|
||||
return rt
|
||||
except:
|
||||
return None
|
||||
js = await self._fetch(_CRYPTO_URL, as_json=True)
|
||||
data = js.get("data", [])
|
||||
self._crypto_cache = (now, data)
|
||||
return data
|
||||
except Exception:
|
||||
return self._crypto_cache[1] if self._crypto_cache else []
|
||||
|
||||
async def _fmt_curr(self, v, a=1):
|
||||
if v.CharCode.text == "XDR":
|
||||
return None
|
||||
c = v.CharCode.text
|
||||
n = v.Name.text
|
||||
v = float(v.Value.text.replace(',', '.')) / float(v.Nominal.text.replace(',', '.'))
|
||||
t = v * a
|
||||
ts = _fmt_num(t, 3)
|
||||
return f"{_FLAGS.get(c, '🏳')} [{a}] {n} ({c}) - {ts} руб."
|
||||
# ──────────────────────────── Formatters ──────────────────────────────
|
||||
|
||||
async def _get_crypto(self):
|
||||
def _fmt_valute(self, code: str, info: dict, amount: float = 1.0) -> str:
|
||||
total = info["rub"] * amount
|
||||
flag = _FLAGS.get(code, "🏳")
|
||||
return f"{flag} [{_fmt_num(amount, 0)}] {info['name']} ({code}) — {_fmt_num(total, 3)} ₽"
|
||||
|
||||
def _fmt_crypto(self, coin: dict, rates: dict, amount: float = 1.0) -> str:
|
||||
symbol = coin["symbol"].upper()
|
||||
try:
|
||||
return requests.get("https://api.coinlore.net/api/tickers/").json().get('data', [])
|
||||
except:
|
||||
return None
|
||||
price_usd = float(coin["price_usd"])
|
||||
except (KeyError, ValueError, TypeError):
|
||||
return ""
|
||||
|
||||
async def _fmt_crypto(self, c, a=1):
|
||||
r = await self._get_rates()
|
||||
if not r:
|
||||
return "🚫 Ошибка получения курсов валют"
|
||||
cr = self.config["crypto_currency"]
|
||||
try:
|
||||
p = float(c['price_usd'])
|
||||
except:
|
||||
return "🚫 Ошибка данных криптовалюты"
|
||||
if cr == "RUB":
|
||||
if not r['USD']:
|
||||
return "🚫 Курс USD не найден"
|
||||
p *= r['USD']
|
||||
elif cr == "EUR":
|
||||
if not r['EUR_USD']:
|
||||
return "🚫 Курс EUR/USD не рассчитан"
|
||||
p *= r['EUR_USD']
|
||||
t = p * a
|
||||
ts = _fmt_num(t)
|
||||
s = c['symbol'].upper()
|
||||
e = _CRYPTO_EMOJIS.get(s, "💠")
|
||||
n = _CRYPTO_LIST.get(s, c['name'])
|
||||
cs = {"USD": "$", "RUB": "₽", "EUR": "€"}.get(cr, "$")
|
||||
return f"{e} [{a}] {n} ({s}) - {ts}{cs}"
|
||||
currency = self.config["crypto_currency"]
|
||||
if currency == "RUB":
|
||||
usd_rate = rates.get("USD", {}).get("rub")
|
||||
if not usd_rate:
|
||||
return ""
|
||||
price = price_usd * usd_rate
|
||||
sign = "₽"
|
||||
elif currency == "EUR":
|
||||
usd_rate = rates.get("USD", {}).get("rub")
|
||||
eur_rate = rates.get("EUR", {}).get("rub")
|
||||
if not usd_rate or not eur_rate:
|
||||
return ""
|
||||
price = price_usd * (usd_rate / eur_rate)
|
||||
sign = "€"
|
||||
else:
|
||||
price = price_usd
|
||||
sign = "$"
|
||||
|
||||
@loader.command()
|
||||
async def valutecmd(self, m):
|
||||
"""[count] [usd, eur, ...]"""
|
||||
a = utils.get_args(m)
|
||||
d, v = await self._get_curr_data()
|
||||
if not d or not v:
|
||||
return await utils.answer(m, self.strings["error"])
|
||||
if len(a) == 0:
|
||||
l = []
|
||||
for x in v:
|
||||
if (n := await self._fmt_curr(x)):
|
||||
l.append(n)
|
||||
await utils.answer(m, self.strings["valute_no_args"].format(d, "\n".join(l)))
|
||||
elif len(a) == 1:
|
||||
total = price * amount
|
||||
emoji = _CRYPTO_EMOJIS.get(symbol, "💠")
|
||||
name = _CRYPTO_NAMES.get(symbol, coin.get("name", symbol))
|
||||
return f"{emoji} [{_fmt_num(amount, 0)}] {name} ({symbol}) — {_fmt_num(total, 3)}{sign}"
|
||||
|
||||
# ──────────────────────────── Commands ────────────────────────────────
|
||||
|
||||
@loader.command(ru_doc="[кол-во] [код] — курс валюты по ЦБ РФ")
|
||||
async def valutecmd(self, message):
|
||||
"""[amount] [code] — exchange rates from CBR"""
|
||||
args = utils.get_args(message)
|
||||
date, rates = await self._cbr_data()
|
||||
|
||||
if not rates:
|
||||
return await utils.answer(message, "🚫 Не удалось получить данные ЦБ РФ")
|
||||
|
||||
header = (
|
||||
f"💵 <b>Курс валюты</b> · <a href='https://www.cbr.ru/'>ЦБ РФ</a>\n"
|
||||
f"<b>Актуально на</b> <i>{date}</i>\n\n"
|
||||
)
|
||||
|
||||
# .valute — список всех, кол-во = 1
|
||||
if not args:
|
||||
lines = [self._fmt_valute(c, i) for c, i in rates.items()]
|
||||
return await utils.answer(
|
||||
message,
|
||||
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
|
||||
)
|
||||
|
||||
# Первый аргумент: число или код валюты?
|
||||
amount = 1.0
|
||||
code = None
|
||||
arg0 = args[0].upper()
|
||||
|
||||
if len(args) >= 2:
|
||||
# .valute 100 USD
|
||||
try:
|
||||
am = float(a[0])
|
||||
l = []
|
||||
for x in v:
|
||||
if (n := await self._fmt_curr(x, am)):
|
||||
l.append(n)
|
||||
await utils.answer(m, self.strings["valute_no_args"].format(d, "\n".join(l)))
|
||||
except:
|
||||
await utils.answer(m, "🚫 Некорректное число")
|
||||
elif len(a) == 2:
|
||||
amount = float(args[0].replace(",", "."))
|
||||
except ValueError:
|
||||
return await utils.answer(message, "🚫 Некорректное число")
|
||||
code = args[1].upper()
|
||||
else:
|
||||
# .valute USD или .valute 100
|
||||
try:
|
||||
am = float(a[0])
|
||||
c = a[1].upper()
|
||||
for x in v:
|
||||
if x.CharCode.text == c:
|
||||
if (n := await self._fmt_curr(x, am)):
|
||||
return await utils.answer(m, self.strings["valute_specific"].format(d, n))
|
||||
await utils.answer(m, self.strings["valute_not_found"].format(c))
|
||||
except:
|
||||
await utils.answer(m, "🚫 Некорректное число")
|
||||
amount = float(arg0.replace(",", "."))
|
||||
# число без кода — список с умножением
|
||||
except ValueError:
|
||||
code = arg0
|
||||
|
||||
@loader.command()
|
||||
async def cryptocmd(self, m):
|
||||
"""[count] [ton, btc, ...]"""
|
||||
a = utils.get_args(m)
|
||||
c = await self._get_crypto()
|
||||
if not c:
|
||||
return await utils.answer(m, self.strings["error"])
|
||||
try:
|
||||
if len(a) == 0:
|
||||
f = [x for x in c if x['symbol'].upper() in _CRYPTO_LIST]
|
||||
l = []
|
||||
for x in f:
|
||||
if (n := await self._fmt_crypto(x)):
|
||||
l.append(n)
|
||||
await utils.answer(m, self.strings["crypto_no_args"].format("\n".join(l)))
|
||||
elif len(a) == 1:
|
||||
am = float(a[0])
|
||||
f = [x for x in c if x['symbol'].upper() in _CRYPTO_LIST]
|
||||
l = []
|
||||
for x in f:
|
||||
if (n := await self._fmt_crypto(x, am)):
|
||||
l.append(n)
|
||||
await utils.answer(m, self.strings["crypto_no_args"].format("\n".join(l)))
|
||||
elif len(a) == 2:
|
||||
am = float(a[0])
|
||||
t = a[1].upper()
|
||||
f = False
|
||||
for x in c:
|
||||
if x['symbol'].upper() == t:
|
||||
if (n := await self._fmt_crypto(x, am)):
|
||||
f = True
|
||||
await utils.answer(m, self.strings["crypto_specific"].format(n))
|
||||
break
|
||||
if not f:
|
||||
await utils.answer(m, self.strings["crypto_not_found"].format(t))
|
||||
except ValueError:
|
||||
await utils.answer(m, "🚫 Некорректное число")
|
||||
except Exception as e:
|
||||
await utils.answer(m, f"🚫 Ошибка: {str(e)}")
|
||||
if code:
|
||||
if code not in rates:
|
||||
return await utils.answer(message, f"🚫 Валюта <b>{code}</b> не найдена")
|
||||
line = self._fmt_valute(code, rates[code], amount)
|
||||
return await utils.answer(message, header + line)
|
||||
|
||||
# список с кол-вом
|
||||
lines = [self._fmt_valute(c, i, amount) for c, i in rates.items()]
|
||||
await utils.answer(
|
||||
message,
|
||||
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="[кол-во] [код] — курс крипты")
|
||||
async def cryptocmd(self, message):
|
||||
"""[amount] [symbol] — crypto rates from CoinLore"""
|
||||
args = utils.get_args(message)
|
||||
coins = await self._crypto_data()
|
||||
_, rates = await self._cbr_data()
|
||||
|
||||
if not coins:
|
||||
return await utils.answer(message, "🚫 Не удалось получить данные крипты")
|
||||
|
||||
header = f"💎 <b>Курсы криптовалют</b> · <i>{self.config['crypto_currency']}</i>\n\n"
|
||||
|
||||
amount = 1.0
|
||||
symbol = None
|
||||
|
||||
if not args:
|
||||
pass # список, amount=1
|
||||
elif len(args) == 1:
|
||||
try:
|
||||
amount = float(args[0].replace(",", "."))
|
||||
except ValueError:
|
||||
symbol = args[0].upper()
|
||||
else:
|
||||
try:
|
||||
amount = float(args[0].replace(",", "."))
|
||||
except ValueError:
|
||||
return await utils.answer(message, "🚫 Некорректное число")
|
||||
symbol = args[1].upper()
|
||||
|
||||
if symbol:
|
||||
coin = next((c for c in coins if c["symbol"].upper() == symbol), None)
|
||||
if not coin:
|
||||
return await utils.answer(message, f"🚫 Крипта <b>{symbol}</b> не найдена")
|
||||
line = self._fmt_crypto(coin, rates, amount)
|
||||
if not line:
|
||||
return await utils.answer(message, "🚫 Ошибка форматирования")
|
||||
return await utils.answer(message, header + line)
|
||||
|
||||
# список только известных монет
|
||||
known = {c["symbol"].upper(): c for c in coins if c["symbol"].upper() in _CRYPTO_NAMES}
|
||||
# сортируем по порядку _CRYPTO_NAMES
|
||||
lines = []
|
||||
for sym in _CRYPTO_NAMES:
|
||||
if sym in known:
|
||||
line = self._fmt_crypto(known[sym], rates, amount)
|
||||
if line:
|
||||
lines.append(line)
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
header + f"<blockquote expandable>{chr(10).join(lines)}</blockquote>",
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5
SenkoGuardian/SenModules/full.txt
Normal file
5
SenkoGuardian/SenModules/full.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
ChatCopy.py
|
||||
Gemini.py
|
||||
GiftFinder.py
|
||||
MaillingChatGT99.py
|
||||
NekoEditorMod.py
|
||||
BIN
archquise/H.Modules/.DS_Store
vendored
BIN
archquise/H.Modules/.DS_Store
vendored
Binary file not shown.
@@ -1,59 +0,0 @@
|
||||
name: Generate Index Page
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
- name: Generate index.html
|
||||
run: |
|
||||
cat <<EOF > _site/index.html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>H:Mods</title>
|
||||
<link type=text/css href="https://github.com/C0dwiz/H.Modules/raw/assets/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>H:Mods modules</h1>
|
||||
<ul class="module-list">
|
||||
$(for file in *.py; do echo " <li class="module-item"><a href=\"$file\" class="module-link">$file</a></li>"; done)
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: .
|
||||
7
archquise/H.Modules/.gitignore
vendored
7
archquise/H.Modules/.gitignore
vendored
@@ -1,7 +0,0 @@
|
||||
full.py
|
||||
autocleaner.py
|
||||
silent.py
|
||||
|
||||
# Ruff Format
|
||||
.ruff_cache/
|
||||
.idea
|
||||
@@ -1,136 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: ASCIIArt
|
||||
# Description: Converting images to ASCII art
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: ASCIIArt
|
||||
# scope: ASCIIArt 0.0.1
|
||||
# requires: pillow
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class ASCIIArtMod(loader.Module):
|
||||
"""Converting images to ASCII art"""
|
||||
|
||||
strings = {
|
||||
"name": "ASCIIArt",
|
||||
"no_media_reply": "<b>Please reply to the image!</b>",
|
||||
"loading": "<emoji document_id=5116240346656801621>❓</emoji> <b>Converting an image to ASCII...</b>",
|
||||
"error": "<emoji document_id=5121063440311386962>👎</emoji> <b>Error when converting an image.</b>",
|
||||
"done": "<emoji document_id=5123163417326126159>✅</emoji> <b>Here is your ASCII art:</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_media_reply": "<b>Пожалуйста, ответьте на изображение!</b>",
|
||||
"loading": "<emoji document_id=5116240346656801621>❓</emoji> <b>Конвертирую изображение в ASCII...</b>",
|
||||
"error": "<emoji document_id=5121063440311386962>👎</emoji> <b>Ошибка при конвертации изображения.</b>",
|
||||
"done": "<emoji document_id=5123163417326126159>✅</emoji> <b>Вот ваш ASCII-арт:</b>",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<реплай на изображение> сделать ascii art",
|
||||
en_doc="<replay on image> make ascii art",
|
||||
)
|
||||
async def cascii(self, message):
|
||||
reply = await message.get_reply_message()
|
||||
if not self._is_image(reply):
|
||||
await utils.answer(message, self.strings("no_media_reply"))
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
ascii_art = await self._generate_ascii_art(reply)
|
||||
|
||||
if ascii_art:
|
||||
await self._send_ascii_file(message, ascii_art)
|
||||
await message.delete()
|
||||
else:
|
||||
await utils.answer(message, self.strings("error"))
|
||||
|
||||
def _is_image(self, reply):
|
||||
"""Проверка, является ли ответ изображением"""
|
||||
return reply and (
|
||||
reply.photo
|
||||
or (reply.document and reply.file.mime_type.startswith("image/"))
|
||||
)
|
||||
|
||||
async def _generate_ascii_art(self, reply):
|
||||
"""Генерирует ASCII-арт из изображения"""
|
||||
try:
|
||||
image_path = await reply.download_media(tempfile.gettempdir())
|
||||
if not image_path:
|
||||
return None
|
||||
with Image.open(image_path) as img:
|
||||
img = img.convert("L")
|
||||
img = img.resize(self._get_new_dimensions(img), Image.NEAREST)
|
||||
|
||||
chars = "@#S%?*+;:,. "
|
||||
pixels = img.getdata()
|
||||
|
||||
ascii_str = "".join(chars[pixel // 25] for pixel in pixels)
|
||||
return "\n".join(
|
||||
ascii_str[i : i + img.width]
|
||||
for i in range(0, len(ascii_str), img.width)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating ASCII art: {e}")
|
||||
return None
|
||||
finally:
|
||||
if image_path and os.path.exists(image_path):
|
||||
os.remove(image_path)
|
||||
|
||||
def _get_new_dimensions(self, img):
|
||||
"""Получаем новые размеры для изображения"""
|
||||
new_width = 100
|
||||
aspect_ratio = img.height / img.width
|
||||
new_height = int(aspect_ratio * new_width * 0.55)
|
||||
return new_width, new_height
|
||||
|
||||
async def _send_ascii_file(self, message, ascii_art):
|
||||
"""Сохраняет ASCII-арт во временный файл и отправляет его"""
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", encoding="utf-8", suffix=".txt", delete=False
|
||||
) as tmp_file:
|
||||
tmp_file_path = tmp_file.name
|
||||
tmp_file.write(ascii_art)
|
||||
|
||||
await message.client.send_file(
|
||||
message.chat_id,
|
||||
tmp_file_path,
|
||||
caption=self.strings("done"),
|
||||
force_document=True,
|
||||
reply_to=getattr(message, "reply_to_msg_id", None),
|
||||
)
|
||||
finally:
|
||||
if tmp_file_path and os.path.exists(tmp_file_path):
|
||||
os.remove(tmp_file_path)
|
||||
@@ -1,134 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: AccountData
|
||||
# Description: Find out the approximate date of registration of the telegram account
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api AccountData
|
||||
# scope: Api AccountData 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class AccountData(loader.Module):
|
||||
"""Find out the approximate date of registration of the telegram account"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"api_token",
|
||||
"7518491974:1ea2284eec9dc40a9838cfbcb48a2b36",
|
||||
"API token for datereg.pro",
|
||||
validator=loader.validators.Hidden(),
|
||||
)
|
||||
)
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
strings = {
|
||||
"name": "AccountData",
|
||||
"_cls_doc": "Find out the approximate date of registration of the telegram account",
|
||||
"date_text": "<emoji document_id=5983150113483134607>⏰️</emoji> Date of registration of this account: {data} (Accuracy: {accuracy}%)",
|
||||
"date_text_ps": "<emoji document_id=6028435952299413210>ℹ</emoji> <i>Tip: To increase accuracy, the person whose registration date is being checked can write any message to</i> @mewpl2.\n\nDon't worry, this account is not run by a person, but by a userbot just like yours, which will check the registration date using Telegram's built-in tool.",
|
||||
"no_reply": "<emoji document_id=6030512294109122096>💬</emoji> You did not reply to the user's message",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"date_text": "<emoji document_id=5983150113483134607>⏰️</emoji> Дата регистрации этого аккаунта: {data} (Точность: {accuracy}%)",
|
||||
"_cls_doc": "Узнайте примерную дату регистрации Telegram-аккаунта",
|
||||
"date_text_ps": "<emoji document_id=6028435952299413210>ℹ</emoji> <i>Совет: Для повышения точности, человек, дата регистрации которого проверяется, может написать любое сообщение</i> @mewpl2.\n\nНе бойтесь, на этом аккаунте сидит не человек, а такой же юзербот, как и у вас, который проверит дату регистрации при помощи встроенного инструмента Telegram.",
|
||||
"no_reply": "<emoji document_id=6030512294109122096>💬</emoji> Вы не ответили на сообщение пользователя",
|
||||
}
|
||||
|
||||
async def get_creation_date(self, user_id: int) -> dict:
|
||||
api_token = self.config["api_token"]
|
||||
if not api_token:
|
||||
return {"error": "API token not configured"}
|
||||
|
||||
url = "https://api.datereg.pro/api/v1/users/getCreationDateFast"
|
||||
params = {"token": api_token, "user_id": user_id}
|
||||
|
||||
session = await self._get_session()
|
||||
try:
|
||||
async with session.get(url, params=params) as response:
|
||||
if response.status == 200:
|
||||
json_response = await response.json()
|
||||
if json_response["success"]:
|
||||
return {
|
||||
"creation_date": json_response["creation_date"],
|
||||
"accuracy_percent": json_response["accuracy_percent"],
|
||||
}
|
||||
else:
|
||||
return {"error": json_response["error"]["message"]}
|
||||
else:
|
||||
return {"error": f"HTTP {response.status}"}
|
||||
except asyncio.TimeoutError:
|
||||
return {"error": "Request timed out"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Узнать примерную дату регистрации Telergam-аккаунта",
|
||||
en_doc="Find out the approximate date of registration of the telegram account",
|
||||
)
|
||||
async def accdata(self, message):
|
||||
if reply := await message.get_reply_message():
|
||||
result = await self.get_creation_date(user_id=reply.sender.id)
|
||||
|
||||
if "error" in result or not result.get("creation_date"):
|
||||
error_msg = result.get("error", "Unknown error occurred")
|
||||
await utils.answer(message, f"Ошибка: {error_msg}")
|
||||
return
|
||||
|
||||
try:
|
||||
month, year = map(int, result["creation_date"].split("."))
|
||||
date_object = datetime(year, month, 1)
|
||||
formatted = date_object.strftime("%B %Y")
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
f"{self.strings('date_text').format(data=formatted, accuracy=result['accuracy_percent'])}\n\n{self.strings('date_text_ps')}",
|
||||
)
|
||||
except (ValueError, KeyError) as e:
|
||||
await utils.answer(message, f"Ошибка обработки данных: {str(e)}")
|
||||
else:
|
||||
await utils.answer(message, self.strings("no_reply"))
|
||||
@@ -1,273 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Aniliberty
|
||||
# Description: Searches and gives random anime on the Aniliberty database.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: dacite
|
||||
# scope: AniLiberty
|
||||
# scope: AniLiberty 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram.types import CallbackQuery, InlineQueryResultPhoto
|
||||
from dataclasses import dataclass
|
||||
from json import JSONDecodeError
|
||||
from dacite import from_dict
|
||||
from typing import Optional
|
||||
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader
|
||||
from ..inline.types import InlineQuery
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BASE_API_URL = "https://aniliberty.top/api/v1"
|
||||
|
||||
|
||||
# Датаклассы для парсинга и хранения json
|
||||
@dataclass
|
||||
class Genre:
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Name:
|
||||
main: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Type:
|
||||
description: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Poster:
|
||||
preview: str
|
||||
thumbnail: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReleaseInfo:
|
||||
id: int
|
||||
genres: Optional[list[Genre]]
|
||||
name: Name
|
||||
is_ongoing: bool
|
||||
type: Type
|
||||
description: str
|
||||
added_in_users_favorites: int
|
||||
alias: str
|
||||
poster: Poster
|
||||
|
||||
|
||||
@loader.tds
|
||||
class AniLibertyMod(loader.Module):
|
||||
"""Ищет и возвращает случайное аниме из базы Aniliberty"""
|
||||
|
||||
strings = {
|
||||
"name": "AniLiberty",
|
||||
"announce": "<b>The announcement</b>:",
|
||||
"ongoing": "<b>Ongoing</b>:",
|
||||
"type": "<b>Type</b>:",
|
||||
"genres": "<b>Genres</b>:",
|
||||
"favorite": "<b>Favourites <3</b>:", # < == <
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"announce": "<b>Анонс</b>:",
|
||||
"ongoing": "<b>Онгоинг</b>:",
|
||||
"type": "<b>Тип</b>:",
|
||||
"genres": "<b>Жанры</b>:",
|
||||
"favorite": "<b>Избранное <3</b>:", # < == <
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
async def search_title(self, query):
|
||||
session = await self._get_session()
|
||||
async with session.get(
|
||||
f"{BASE_API_URL}/app/search/releases?query={query}&include=id%2Cname.main%2Cis_ongoing%2Ctype.description%2Cdescription%2Cadded_in_users_favorites%2Calias%2Cposter.preview%2Cposter.thumbnail"
|
||||
) as resp:
|
||||
json_answer = await resp.json()
|
||||
results = []
|
||||
for i in json_answer:
|
||||
obj = from_dict(data_class=ReleaseInfo, data=i)
|
||||
results.append(obj)
|
||||
return results
|
||||
|
||||
async def get_title(self, release_id):
|
||||
session = await self._get_session()
|
||||
async with session.get(
|
||||
f"{BASE_API_URL}/anime/releases/{release_id}?include=id%2Cgenres.name%2Cname.main%2Cis_ongoing%2Ctype.description%2Cdescription%2Cadded_in_users_favorites%2Calias%2Cposter.preview%2Cposter.thumbnail"
|
||||
) as resp:
|
||||
try:
|
||||
json_answer = await resp.json()
|
||||
data = from_dict(data_class=ReleaseInfo, data=json_answer)
|
||||
return data
|
||||
except JSONDecodeError:
|
||||
logger.error("Ошибка парсинга JSON!")
|
||||
|
||||
async def get_random_title(self):
|
||||
session = await self._get_session()
|
||||
async with session.get(
|
||||
f"{BASE_API_URL}/anime/releases/random?limit=1&include=id"
|
||||
) as resp:
|
||||
randid = await resp.json()
|
||||
data = await self.get_title(randid[0]["id"])
|
||||
return data
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Возвращает случайный релиз из базы",
|
||||
en_doc="Returns a random release from the database",
|
||||
)
|
||||
async def arandom(self, message) -> None:
|
||||
anime_release = await self.get_random_title()
|
||||
genres_str = ""
|
||||
for genre in anime_release.genres[:-1]:
|
||||
genres_str += f"{genre.name}, "
|
||||
genres_str += anime_release.genres[-1].name
|
||||
|
||||
text = f"{anime_release.name.main} \n"
|
||||
text += f"{self.strings['ongoing']} {'Да' if anime_release.is_ongoing else 'Нет'}\n\n"
|
||||
text += f"{self.strings['type']} {anime_release.type.description}\n"
|
||||
text += f"{self.strings['genres']} {genres_str}\n\n"
|
||||
|
||||
text += f"<code>{anime_release.description}</code>\n\n"
|
||||
text += (
|
||||
f"{self.strings['favorite']} {str(anime_release.added_in_users_favorites)}"
|
||||
)
|
||||
|
||||
kb = [
|
||||
[
|
||||
{
|
||||
"text": "Ссылка",
|
||||
"url": f"https://aniliberty.top/anime/releases/release/{anime_release.alias}/episodes",
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
kb.append([{"text": "🔃 Обновить", "callback": self.inline__update}])
|
||||
kb.append([{"text": "🚫 Закрыть", "callback": self.inline__close}])
|
||||
|
||||
await self.inline.form(
|
||||
text=text,
|
||||
photo=f"https://aniliberty.top{anime_release.poster.preview}",
|
||||
message=message,
|
||||
reply_markup=kb,
|
||||
silent=True,
|
||||
)
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="Возвращает список найденных по названию тайтлов",
|
||||
en_doc="Returns a list of titles found by name",
|
||||
)
|
||||
async def asearch_inline_handler(self, query: InlineQuery) -> None:
|
||||
text = query.args
|
||||
|
||||
if not text:
|
||||
return
|
||||
|
||||
anime_releases = await self.search_title(text)
|
||||
|
||||
inline_query = []
|
||||
for anime_release in anime_releases:
|
||||
"""
|
||||
Приходится запрашивать по второму кругу, т.к. API в поиске не отдает жанры, даже если попросить через include
|
||||
"""
|
||||
release_genres = await self.get_title(anime_release.id)
|
||||
genres_str = ""
|
||||
for genre in release_genres.genres[:-1]:
|
||||
genres_str += f"{genre.name}, "
|
||||
genres_str += release_genres.genres[-1].name
|
||||
release_text = (
|
||||
f"{anime_release.name.main}\n"
|
||||
f"{self.strings['ongoing']} {'Да' if anime_release.is_ongoing else 'Нет'}\n\n"
|
||||
f"{self.strings['type']} {anime_release.type.description}\n"
|
||||
f"{self.strings['genres']} {genres_str}\n\n"
|
||||
f"<code>{anime_release.description}</code>\n\n"
|
||||
f"{self.strings['favorite']} {anime_release.added_in_users_favorites}"
|
||||
)
|
||||
|
||||
inline_query.append(
|
||||
InlineQueryResultPhoto(
|
||||
id=str(anime_release.id),
|
||||
title=anime_release.name.main,
|
||||
description=anime_release.type.description,
|
||||
caption=release_text,
|
||||
thumbnail_url=f"https://aniliberty.top{anime_release.poster.thumbnail}",
|
||||
photo_url=f"https://aniliberty.top{anime_release.poster.preview}",
|
||||
parse_mode="html",
|
||||
)
|
||||
)
|
||||
method = query.answer(inline_query, cache_time=0)
|
||||
await method.as_(self.inline.bot)
|
||||
|
||||
async def inline__close(self, call: CallbackQuery) -> None:
|
||||
await call.delete()
|
||||
|
||||
async def inline__update(self, call: CallbackQuery) -> None:
|
||||
anime_release = await self.get_random_title()
|
||||
genres_str = ""
|
||||
for genre in anime_release.genres[:-1]:
|
||||
genres_str += f"{genre.name}, "
|
||||
genres_str += anime_release.genres[-1].name
|
||||
|
||||
text = f"{anime_release.name.main} \n"
|
||||
text += f"{self.strings['ongoing']} {'Да' if anime_release.is_ongoing else 'Нет'}\n\n"
|
||||
text += f"{self.strings['type']} {anime_release.type.description}\n"
|
||||
text += f"{self.strings['genres']} {genres_str}\n\n"
|
||||
|
||||
text += f"<code>{anime_release.description}</code>\n\n"
|
||||
text += (
|
||||
f"{self.strings['favorite']} {str(anime_release.added_in_users_favorites)}"
|
||||
)
|
||||
|
||||
kb = [
|
||||
[
|
||||
{
|
||||
"text": "Ссылка",
|
||||
"url": f"https://aniliberty.top/anime/releases/release/{anime_release.alias}/episodes",
|
||||
}
|
||||
]
|
||||
]
|
||||
kb.append([{"text": "🔃 Обновить", "callback": self.inline__update}])
|
||||
kb.append([{"text": "🚫 Закрыть", "callback": self.inline__close}])
|
||||
|
||||
await call.edit(
|
||||
text=text,
|
||||
photo=f"https://aniliberty.top{anime_release.poster.preview}",
|
||||
reply_markup=kb,
|
||||
)
|
||||
@@ -1,100 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: AnimeQuotes
|
||||
# Description: A module for sending random quotes from anime
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: AnimeQuotes
|
||||
# scope: AnimeQuotes 0.0.1
|
||||
# requires: requests
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class AnimeQuotesMod(loader.Module):
|
||||
"""A module for sending random quotes from anime"""
|
||||
|
||||
strings = {
|
||||
"name": "AnimeQuotes",
|
||||
"quote_template": (
|
||||
'<b>Quote:</b> "{quote}"\n\n'
|
||||
"<b>Character:</b> {character}\n"
|
||||
"<b>Anime:</b> {anime}"
|
||||
),
|
||||
"error": "<b>Couldn't get a quote. Try again later!</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"quote_template": (
|
||||
'<b>Цитата:</b> "{quote}"\n\n'
|
||||
"<b>Персонаж:</b> {character}\n"
|
||||
"<b>Аниме:</b> {anime}"
|
||||
),
|
||||
"error": "<b>Не удалось получить цитату. Попробуйте позже!</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Получить случайную цитату из аниме",
|
||||
en_doc="Get a random quote from the anime",
|
||||
)
|
||||
async def quote(self, message):
|
||||
url = "https://api.animechan.io/v1/quotes/random"
|
||||
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
|
||||
quote_content = data["data"]["content"]
|
||||
character_name = data["data"]["character"]["name"]
|
||||
anime_name = data["data"]["anime"]["name"]
|
||||
|
||||
quote = self.strings("quote_template").format(
|
||||
quote=quote_content, character=character_name, anime=anime_name
|
||||
)
|
||||
await utils.answer(message, quote)
|
||||
|
||||
except aiohttp.ClientError:
|
||||
await utils.answer(message, self.strings("error"))
|
||||
@@ -1,76 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Article
|
||||
# Description: Displays your article Criminal Code of the Russian Federation
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Article
|
||||
# scope: Article 0.0.1
|
||||
# requires: requests
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict
|
||||
|
||||
import requests
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class ArticleMod(loader.Module):
|
||||
"""Displays your article Criminal Code of the Russian Federation"""
|
||||
|
||||
strings = {
|
||||
"name": "Article",
|
||||
"article": "<emoji document_id=5226512880362332956>📖</emoji> <b>Your article of the Criminal Code of the Russian Federation</b>:\n\n<blockquote>Number {}\n\n{}</blockquote>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"article": "<emoji document_id=5226512880362332956>📖</emoji> <b>Твоя статья УК РФ</b>:\n\n<blockquote>Номер {}\n\n{}</blockquote>",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Отображается ваша статья Уголовного кодекса Российской Федерации",
|
||||
en_doc="Displays your article Criminal Code of the Russian Federation",
|
||||
)
|
||||
async def arccmd(self, message):
|
||||
if values := self._load_values():
|
||||
random_key = random.choice(list(values.keys()))
|
||||
random_value = values[random_key]
|
||||
await utils.answer(
|
||||
message, self.strings("article").format(random_key, random_value)
|
||||
)
|
||||
|
||||
def _load_values(self) -> Dict[str, str]:
|
||||
url = "https://raw.githubusercontent.com/Codwizer/ReModules/main/assets/zakon.json"
|
||||
try:
|
||||
response = requests.get(url)
|
||||
if response.ok:
|
||||
data = json.loads(response.text)
|
||||
return data
|
||||
except (requests.RequestException, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
return {}
|
||||
@@ -1,205 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: AutofarmCookies
|
||||
# Description: Autofarm in the bot @cookies_game_bot
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: AutofarmCookies
|
||||
# scope: AutofarmCookies 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import random
|
||||
from datetime import timedelta
|
||||
|
||||
from telethon import functions
|
||||
from telethon.tl.custom import Message
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
__version__ = (1, 0, 0)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class AutofarmCookiesMod(loader.Module):
|
||||
"""Autofarm in the bot @cookies_game_bot"""
|
||||
|
||||
strings = {
|
||||
"name": "AutofarmCookies",
|
||||
"farmon": (
|
||||
"<i>The deferred task has been created, autofarming has been started, everything will start in 10 minutes"
|
||||
" seconds...</i>"
|
||||
),
|
||||
"farmon_already": "<i>It has already been launched :)</i>",
|
||||
"farmoff": "<i>The autopharm is stopped\nSelected:</i> <b>%coins% Cookies</b>",
|
||||
"farm": "<i>I typed:</i> <b>%coins% Cookies</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"farmon": (
|
||||
"<i>Отложенная задача создана, автофарминг запущен, всё начнётся через 10"
|
||||
" секунд...</i>"
|
||||
),
|
||||
"farmon_already": "<i>Уже запущено :)</i>",
|
||||
"farmoff": "<i>Автофарм остановлен.\nНвброно:</i> <b>%coins% Cookies</b>",
|
||||
"farm": "<i>Я набрал:</i> <b>%coins% Cookies</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.name = self.strings["name"]
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
self.myid = (await client.get_me()).id
|
||||
self.cookies = "@cookies_game_bot"
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Запустить автофарминг",
|
||||
en_doc="Launch auto-farming",
|
||||
)
|
||||
async def cookon(self, message):
|
||||
status = self.db.get(self.name, "status", False)
|
||||
if status:
|
||||
return await message.edit(self.strings["farmon_already"])
|
||||
self.db.set(self.name, "status", True)
|
||||
await self.client.send_message(
|
||||
self.cookies, "/cookie", schedule=timedelta(seconds=10)
|
||||
)
|
||||
await message.edit(self.strings["farmon"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Остановить автофарминг",
|
||||
en_doc="Stop auto-farming",
|
||||
)
|
||||
async def cookoff(self, message):
|
||||
self.db.set(self.name, "status", False)
|
||||
coins = self.db.get(self.name, "coins", 0)
|
||||
if coins:
|
||||
self.db.set(self.name, "coins", 0)
|
||||
await message.edit(self.strings["farmoff"].replace("%coins%", str(coins)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Вывод кол-ва коинов, добытых этим модулем",
|
||||
en_doc="Output of the number of coins mined by this module",
|
||||
)
|
||||
async def cookies(self, message):
|
||||
coins = self.db.get(self.name, "coins", 0)
|
||||
await message.edit(self.strings["farm"].replace("%coins%", str(coins)))
|
||||
|
||||
async def watcher(self, event):
|
||||
if not isinstance(event, Message): # noqa: F821
|
||||
return
|
||||
chat = utils.get_chat_id(event)
|
||||
if chat != self.cookies:
|
||||
return
|
||||
status = self.db.get(self.name, "status", False)
|
||||
if not status:
|
||||
return
|
||||
if event.raw_text == "/cookie":
|
||||
return await self.client.send_message(
|
||||
self.cookies, "/cookie", schedule=timedelta(hours=2)
|
||||
)
|
||||
if event.sender_id != self.cookies:
|
||||
return
|
||||
if "🙅♂️!" in event.raw_text:
|
||||
args = [int(x) for x in event.raw_text.split() if x.isnumeric()]
|
||||
randelta = random.randint(20, 60)
|
||||
if len(args) == 4:
|
||||
delta = timedelta(
|
||||
hours=args[1], minutes=args[2], seconds=args[3] + randelta
|
||||
)
|
||||
elif len(args) == 3:
|
||||
delta = timedelta(minutes=args[1], seconds=args[2] + randelta)
|
||||
elif len(args) == 2:
|
||||
delta = timedelta(seconds=args[1] + randelta)
|
||||
else:
|
||||
return
|
||||
sch = (
|
||||
await self.client(
|
||||
functions.messages.GetScheduledHistoryRequest(self.cookies, 1488)
|
||||
)
|
||||
).messages
|
||||
await self.client(
|
||||
functions.messages.DeleteScheduledMessagesRequest(
|
||||
self.cookies, id=[x.id for x in sch]
|
||||
)
|
||||
)
|
||||
return await self.client.send_message(
|
||||
self.cookies, "/cookie", schedule=delta
|
||||
)
|
||||
if "✨" in event.raw_text:
|
||||
args = event.raw_text.split()
|
||||
for x in args:
|
||||
if x[0] == "+":
|
||||
return self.db.set(
|
||||
self.name,
|
||||
"coins",
|
||||
self.db.get(self.name, "coins", 0) + int(x[1:]),
|
||||
)
|
||||
|
||||
async def message_q(
|
||||
self,
|
||||
text: str,
|
||||
user_id: int,
|
||||
mark_read: bool = False,
|
||||
delete: bool = False,
|
||||
):
|
||||
async with self.client.conversation(user_id) as conv:
|
||||
msg = await conv.send_message(text)
|
||||
response = await conv.get_response()
|
||||
if mark_read:
|
||||
await conv.mark_read()
|
||||
|
||||
if delete:
|
||||
await msg.delete()
|
||||
await response.delete()
|
||||
|
||||
return response
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Показывает ваш мешок",
|
||||
en_doc="Shows your bag",
|
||||
)
|
||||
async def me(self, message):
|
||||
bot = "@cookies_game_bot"
|
||||
bags = await self.message_q(
|
||||
"/me",
|
||||
bot,
|
||||
delete=True,
|
||||
)
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
|
||||
if not args:
|
||||
await utils.answer(message, bags.text)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Помощь по модулю AutofarmCookies",
|
||||
en_doc="Help with the AutofarmCookies module",
|
||||
)
|
||||
async def ckies(self, message):
|
||||
chelp = """
|
||||
🍀| <b>Помощь по командам:</b>
|
||||
.cookon - Включает авто-фарм.
|
||||
.cookoff - Выключает авто-фарм.
|
||||
.me - Показывает ваш мешок"""
|
||||
await utils.answer(message, chelp)
|
||||
@@ -1,252 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: BirthdayTime
|
||||
# Description: Counting down to your birthday
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: BirthdayTime
|
||||
# scope: Api BirthdayTime 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import calendar
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from telethon.errors.rpcerrorlist import UserPrivacyRestrictedError
|
||||
from telethon.tl.functions.account import UpdateProfileRequest
|
||||
from telethon.tl.functions.users import GetFullUserRequest
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
D_MSG = [
|
||||
"Ждешь его?",
|
||||
"Осталось немного)",
|
||||
"Дни пролетят, даже не заметишь",
|
||||
"Уже знаешь что хочешь получить в подарок?)",
|
||||
"Сколько исполняется?",
|
||||
"Жду не дождусь уже",
|
||||
]
|
||||
|
||||
|
||||
@loader.tds
|
||||
class DaysToMyBirthday(loader.Module):
|
||||
"""Counting down to your birthday"""
|
||||
|
||||
strings = {
|
||||
"name": "BirthdayTime",
|
||||
"date_error": "<emoji document_id=5422840512681877946>❗️</emoji> <b>Your birthdate is not specified in the config, please correct this :)</b>",
|
||||
"msg": (
|
||||
"<emoji document_id=5377476217698001788>🎉</emoji> <b>"
|
||||
"There are {} days, {} hours, {} minutes, and {} seconds left until your birthday. \n<emoji document_id=5377442914521588226>"
|
||||
"💙</emoji> {}</b>"
|
||||
),
|
||||
"conf": "<i>Open config...</i>",
|
||||
"name_changed": "<b>Name updated!</b>",
|
||||
"name_not_changed": "<b>Name was not updated.</b>",
|
||||
"name_privacy_error": "<b>Unable to change name due to privacy settings.</b>",
|
||||
"error": "<b>An error occurred. Please check the logs.</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"date_error": "<emoji document_id=5422840512681877946>❗️</emoji> <b>В конфиге не указан день вашего рождения, пожалуйста, исправь это :)</b>",
|
||||
"msg": (
|
||||
"<emoji document_id=5377476217698001788>🎉</emoji> <b>"
|
||||
"До вашего дня рождения осталось {} дней, {} часов, {} "
|
||||
"минут, {} секунд. \n<emoji document_id=5377442914521588226>"
|
||||
"💙</emoji> {}</b>"
|
||||
),
|
||||
"conf": "<i>Открываю конфиг...</i>",
|
||||
"btname_yes": (
|
||||
"<b><emoji document_id=6327560044845991305>😶</emoji> Хорошо, теперь я "
|
||||
"буду изменять ваше имя в зависимости от количества дней до дня рождения</b>"
|
||||
),
|
||||
"btname_no": "<emoji document_id=6325696222313055607>😶</emoji>Хорошо, я больше не буду изменять ваше имя",
|
||||
"name_changed": "<b>Имя обновлено!</b>",
|
||||
"name_not_changed": "<b>Имя не было обновлено.</b>",
|
||||
"name_privacy_error": "<b>Не удалось изменить имя из-за настроек приватности.</b>",
|
||||
"error": "<b>Произошла ошибка. Пожалуйста, проверьте логи.</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"birthday_date",
|
||||
None,
|
||||
lambda: "Дата вашего рождения. Указывать только день",
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"birthday_month",
|
||||
None,
|
||||
"Месяц вашего рождения",
|
||||
validator=loader.validators.Choice(
|
||||
[
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
self._task = None
|
||||
|
||||
async def client_ready(self):
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
|
||||
self._task = asyncio.create_task(self.checker())
|
||||
|
||||
async def checker(self):
|
||||
while True:
|
||||
if not self.db.get(__name__, "change_name", False):
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
try:
|
||||
now = datetime.now()
|
||||
day = self.config["birthday_date"]
|
||||
monthy = self.config["birthday_month"]
|
||||
month = list(calendar.month_name).index(monthy)
|
||||
birthday = datetime(now.year, month, day)
|
||||
|
||||
if now.month > month or (now.month == month and now.day > day):
|
||||
birthday = datetime(now.year + 1, month, day)
|
||||
|
||||
time_to_birthday = abs(birthday - now)
|
||||
days = time_to_birthday.days
|
||||
|
||||
user = await self.client(GetFullUserRequest(self.client.hikka_me.id))
|
||||
if not user or not user.users:
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
|
||||
name = user.users[0].last_name or ""
|
||||
|
||||
ln = f"{self.db.get(__name__, 'last_name', '')} • {days} d."
|
||||
if name == ln:
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
else:
|
||||
await self.client(UpdateProfileRequest(last_name=ln))
|
||||
self.db.set(__name__, "last_name", name)
|
||||
except UserPrivacyRestrictedError:
|
||||
self.db.set(__name__, "change_name", False)
|
||||
logger.error("Error: Can't change name due to privacy settings.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in checker: {e}")
|
||||
finally:
|
||||
await asyncio.sleep(60)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Включить таймер дней в ник (нестабильно)",
|
||||
en_doc="Enable timer of days in nickname (unstable)",
|
||||
)
|
||||
async def btnameon(self, message):
|
||||
try:
|
||||
user = await self.client(GetFullUserRequest(self.client.hikka_me.id))
|
||||
name = user.users[0].last_name or ""
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user info: {e}")
|
||||
await utils.answer(message, self.strings("error"))
|
||||
return
|
||||
|
||||
self.db.set(__name__, "last_name", name)
|
||||
self.db.set(__name__, "change_name", True)
|
||||
await utils.answer(message, self.strings("btname_yes"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выключить таймер дней в ник",
|
||||
en_doc="Disable timer of days in nickname",
|
||||
)
|
||||
async def btnameoff(self, message):
|
||||
change_name = self.db.get(__name__, "change_name", False)
|
||||
|
||||
if not change_name:
|
||||
await utils.answer(message, self.strings("btname_no"))
|
||||
return
|
||||
|
||||
self.db.set(__name__, "change_name", False)
|
||||
await utils.answer(message, self.strings("btname_no"))
|
||||
try:
|
||||
await self.client(
|
||||
UpdateProfileRequest(last_name=self.db.get(__name__, "last_name"))
|
||||
)
|
||||
await utils.answer(message, self.strings("name_not_changed"))
|
||||
except UserPrivacyRestrictedError:
|
||||
await utils.answer(message, self.strings("name_privacy_error"))
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing name: {e}")
|
||||
await utils.answer(message, self.strings("error"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Вывести таймер",
|
||||
en_doc="Display the timer",
|
||||
)
|
||||
async def bt(self, message):
|
||||
if (
|
||||
self.config["birthday_date"] is None
|
||||
or self.config["birthday_month"] is None
|
||||
):
|
||||
await utils.answer(message, self.strings("date_error"))
|
||||
msg = await self.client.send_message(message.chat_id, self.strings("conf"))
|
||||
await self.allmodules.commands["config"](
|
||||
await utils.answer(msg, f"{self.get_prefix()}config BirthdayTime")
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
now = datetime.now()
|
||||
day = self.config["birthday_date"]
|
||||
monthy = self.config["birthday_month"]
|
||||
month = list(calendar.month_name).index(monthy)
|
||||
birthday = datetime(now.year, month, day)
|
||||
|
||||
if now.month > month or (now.month == month and now.day > day):
|
||||
birthday = datetime(now.year + 1, month, day)
|
||||
|
||||
time_to_birthday = abs(birthday - now)
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("msg").format(
|
||||
time_to_birthday.days,
|
||||
(time_to_birthday.seconds // 3600),
|
||||
(time_to_birthday.seconds // 60 % 60),
|
||||
(time_to_birthday.seconds % 60),
|
||||
random.choice(D_MSG),
|
||||
),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in bt command: {e}")
|
||||
await utils.answer(message, self.strings("error"))
|
||||
@@ -1 +0,0 @@
|
||||
mods.archquise.ru
|
||||
@@ -1,54 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: CheckSpamBan
|
||||
# Description: Check spam ban for your account.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: CheckSpamBan
|
||||
# scope: CheckSpamBan 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class SpamBanCheckMod(loader.Module):
|
||||
"""Checks spam ban for your account."""
|
||||
|
||||
strings = {
|
||||
"name": "CheckSpamBan",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Проверяет вашу учетную запись на спам-бан с помощью бота @SpamBot",
|
||||
en_doc="Checks your account for spam ban via @SpamBot bot",
|
||||
)
|
||||
async def spambot(self, message):
|
||||
async with self.client.conversation(178220800) as conv:
|
||||
user_message = await conv.send_message("/start")
|
||||
await user_message.delete()
|
||||
spam_message = await conv.get_response()
|
||||
await utils.answer(message, spam_message.text)
|
||||
await spam_message.delete()
|
||||
@@ -1,104 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2026-2029 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: CodeShare
|
||||
# Description: Uploads your code at the kmi.aeza.net (Pastebin and GitHub Gist alternative)
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: aiofiles
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .. import loader, utils
|
||||
from telethon.types import MessageMediaDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class CodeShareMod(loader.Module):
|
||||
"""Uploads your code at the kmi.aeza.net (Pastebin and GitHub Gist alternative)"""
|
||||
|
||||
strings = {
|
||||
"name": "CodeShare",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or reply with a file, or they are invalid",
|
||||
"_cls_doc": "Uploads your code at the kmi.aeza.net (Pastebin and GitHub Gist alternative)",
|
||||
"link_ready": "<emoji document_id=5854762571659218443>✅</emoji> <b>Code uploaded! Link:</b> <code>{}</code>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Загружает ваш код на kmi.aeza.net (альтернатива Pastebin и GitHub Gist)",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или реплая с файлом, или они неверны",
|
||||
"link_ready": "<emoji document_id=5854762571659218443>✅</emoji> <b>Код загружен! Ссылка:</b> <code>{}</code>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
async def upload_to_kmi(self, content: str) -> Optional[str]:
|
||||
url = "https://kmi.aeza.net"
|
||||
data = aiohttp.FormData()
|
||||
data.add_field("kmi", content)
|
||||
|
||||
session = await self._get_session()
|
||||
async with session.post(url, data=data) as response:
|
||||
if response.status == 200:
|
||||
link = await response.text()
|
||||
return link
|
||||
else:
|
||||
logger.error(f"Error occurred! Status code: {response.status}")
|
||||
return None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Загрузка кода на сайт",
|
||||
en_doc="Upload code to the site",
|
||||
)
|
||||
async def codesharecmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
reply = await message.get_reply_message()
|
||||
if args:
|
||||
link = await self.upload_to_kmi(args)
|
||||
await utils.answer(message, self.strings["link_ready"].format(link))
|
||||
return
|
||||
if reply and isinstance(reply.media, MessageMediaDocument):
|
||||
file_name = await reply.download_media()
|
||||
async with aiofiles.open(file_name, mode="r") as f:
|
||||
content = await f.read()
|
||||
link = await self.upload_to_kmi(content)
|
||||
await os.remove(file_name)
|
||||
await utils.answer(message, self.strings["link_ready"].format(link))
|
||||
return
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
@@ -1,126 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: CryptoCurrency
|
||||
# Description: Module for displaying current cryptocurrency exchange rates.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api CryptoCurrency
|
||||
# scope: Api CryptoCurrency 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class CryptoCurrencyMod(loader.Module):
|
||||
"""Module for displaying current cryptocurrency exchange rates."""
|
||||
|
||||
strings = {
|
||||
"name": "CryptoCurrency",
|
||||
"query_missing": "Please specify a cryptocurrency ticker or name.",
|
||||
"coin_not_found": "Cryptocurrency '{query}' not found.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"query_missing": "Пожалуйста, укажите тикер или название криптовалюты.",
|
||||
"coin_not_found": "Криптовалюта '{query}' не найдена.",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
async def fetch_json(self, url):
|
||||
"""Fetch JSON data from a given URL."""
|
||||
session = await self._get_session()
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
return await response.json()
|
||||
|
||||
async def get_exchange_rates(self):
|
||||
"""Get exchange rates for RUB and EUR based on USD."""
|
||||
data = await self.fetch_json("https://open.er-api.com/v6/latest/USD")
|
||||
return data["rates"]["RUB"], data["rates"]["EUR"]
|
||||
|
||||
async def find_coin(self, query):
|
||||
"""Find a cryptocurrency by its name or symbol."""
|
||||
data = await self.fetch_json(
|
||||
"https://api.coinlore.net/api/tickers/?start=0&limit=100"
|
||||
)
|
||||
return next(
|
||||
(
|
||||
item
|
||||
for item in data["data"]
|
||||
if query.lower() in item["name"].lower()
|
||||
or query.lower() in item["symbol"].lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Отображает текущий курс криптовалюты в рублях, долларах США и евро",
|
||||
en_doc="Displays the current cryptocurrency rate in RUB, USD, and EUR",
|
||||
)
|
||||
async def crypto(self, message):
|
||||
query = utils.get_args_raw(message)
|
||||
if not query:
|
||||
return await utils.answer(message, self.strings("query_missing"))
|
||||
|
||||
coin = await self.find_coin(query)
|
||||
if not coin:
|
||||
return await utils.answer(
|
||||
message, self.strings("coin_not_found").format(query=query)
|
||||
)
|
||||
|
||||
price_usd = float(coin["price_usd"])
|
||||
usd_rub_rate, usd_eur_rate = await self.get_exchange_rates()
|
||||
|
||||
price_rub = price_usd * usd_rub_rate
|
||||
price_eur = price_usd * usd_eur_rate
|
||||
|
||||
response = self.format_response(coin, price_usd, price_rub, price_eur)
|
||||
await utils.answer(message, response)
|
||||
|
||||
def format_response(self, coin, price_usd, price_rub, price_eur):
|
||||
"""Format the response message with cryptocurrency information."""
|
||||
return (
|
||||
f"💰 {coin['name']} ({coin['symbol']})\n"
|
||||
f"USD: ${price_usd:.2f}\n"
|
||||
f"RUB: ₽{price_rub:.2f}\n"
|
||||
f"EUR: €{price_eur:.2f}\n"
|
||||
)
|
||||
@@ -1,360 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: EmojiStickerBlocker
|
||||
# Description: Block emojis, stickers and sticker packs
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: EmojiStickerBlocker
|
||||
# scope: EmojiStickerBlocker0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional, Set
|
||||
|
||||
from telethon.errors import FloodWaitError, MessageDeleteForbiddenError
|
||||
from telethon.tl.types import Message, MessageMediaDocument
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class EmojiStickerBlocker(loader.Module):
|
||||
"""Block emojis, stickers and sticker packs with enhanced functionality"""
|
||||
|
||||
strings = {
|
||||
"name": "EmojiStickerBlocker",
|
||||
"no_permission": "<emoji document_id=5854929766146118183>❌</emoji> Need delete messages permission",
|
||||
"pack_blocked": "<emoji document_id=5854762571659218443>✅</emoji> Pack blocked",
|
||||
"pack_not_found": "<emoji document_id=5854929766146118183>❌</emoji> Pack not found",
|
||||
"sticker_blocked": "<emoji document_id=5854929766146118183>❌</emoji> Sticker blocked",
|
||||
"emoji_blocked": "<emoji document_id=5854929766146118183>❌</emoji> Emoji blocked",
|
||||
"pack_unblocked": "<emoji document_id=5854762571659218443>✅</emoji> Pack unblocked",
|
||||
"item_unblocked": "<emoji document_id=5854929766146118183>❌</emoji> Item unblocked",
|
||||
"not_found": "<emoji document_id=5854929766146118183>❌</emoji> Not in blocklist",
|
||||
"no_reply": "<emoji document_id=5854929766146118183>❌</emoji> Reply to a sticker or emoji",
|
||||
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Specify pack link or name",
|
||||
"list_packs": "📦 Blocked packs: {}",
|
||||
"list_stickers": "🖼 Blocked stickers: {}",
|
||||
"list_emojis": "😀 Blocked emojis: {}",
|
||||
"all_cleared": "✅ All blocks cleared",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_permission": "<emoji document_id=5854929766146118183>❌</emoji> Нужны права на удаление сообщений",
|
||||
"pack_blocked": "<emoji document_id=5188311512791393083>✅</emoji> Пак заблокирован",
|
||||
"pack_not_found": "<emoji document_id=5854929766146118183>❌</emoji> Пак не найден",
|
||||
"sticker_blocked": "<emoji document_id=5854929766146118183>❌</emoji> Стикер заблокирован",
|
||||
"emoji_blocked": "<emoji document_id=5854929766146118183>❌</emoji> Эмодзи заблокирован",
|
||||
"pack_unblocked": "<emoji document_id=5854762571659218443>✅</emoji> Пак разблокирован",
|
||||
"item_unblocked": "<emoji document_id=5854929766146118183>❌</emoji> Элемент разблокирован",
|
||||
"not_found": "<emoji document_id=5854929766146118183>❌</emoji> Не найден в блоклисте",
|
||||
"no_reply": "<emoji document_id=5854929766146118183>❌</emoji> Ответьте на стикер или эмодзи",
|
||||
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Укажите ссылку или название пака",
|
||||
"list_packs": "📦 Заблокированные паки: {}",
|
||||
"list_stickers": "🖼 Заблокированные стикеры: {}",
|
||||
"list_emojis": "😀 Заблокированные эмодзи: {}",
|
||||
"all_cleared": "✅ Все блоки очищены",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.blocked_packs: Set[str] = set()
|
||||
self.blocked_stickers: Set[str] = set()
|
||||
self.blocked_emojis: Set[str] = set()
|
||||
self._client = None
|
||||
self._db = None
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
self._db = db
|
||||
self._load_blocklists()
|
||||
|
||||
def _load_blocklists(self):
|
||||
self.blocked_packs = set(self._db.get(__name__, "blocked_packs", []))
|
||||
self.blocked_stickers = set(self._db.get(__name__, "blocked_stickers", []))
|
||||
self.blocked_emojis = set(self._db.get(__name__, "blocked_emojis", []))
|
||||
|
||||
def _save_blocklists(self):
|
||||
self._db.set(__name__, "blocked_packs", list(self.blocked_packs))
|
||||
self._db.set(__name__, "blocked_stickers", list(self.blocked_stickers))
|
||||
self._db.set(__name__, "blocked_emojis", list(self.blocked_emojis))
|
||||
|
||||
def _extract_pack_name(self, message: Message) -> Optional[str]:
|
||||
"""Extract pack name from sticker or emoji"""
|
||||
if not message.media:
|
||||
return None
|
||||
|
||||
if message.sticker:
|
||||
if hasattr(message.sticker, "set_name") and message.sticker.set_name:
|
||||
return message.sticker.set_name.lower()
|
||||
|
||||
if isinstance(message.media, MessageMediaDocument):
|
||||
if hasattr(message.media, "document") and hasattr(
|
||||
message.media.document, "attributes"
|
||||
):
|
||||
for attr in message.media.document.attributes:
|
||||
if (
|
||||
hasattr(attr, "stickerset")
|
||||
and hasattr(attr.stickerset, "title")
|
||||
and attr.stickerset.title
|
||||
):
|
||||
return attr.stickerset.title.lower()
|
||||
|
||||
return None
|
||||
|
||||
def _extract_emoji_text(self, message: Message) -> Optional[str]:
|
||||
"""Extract emoji text from message"""
|
||||
if not message.message:
|
||||
return None
|
||||
|
||||
emoji_pattern = re.compile(
|
||||
r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F1E0-\U0001F1FF\U00002702-\U000027B0\U000024C2-\U0001F251]"
|
||||
)
|
||||
emojis = emoji_pattern.findall(message.message)
|
||||
|
||||
if emojis:
|
||||
return emojis[0]
|
||||
return None
|
||||
|
||||
async def _delete_message(self, message: Message) -> bool:
|
||||
"""Delete message with error handling"""
|
||||
try:
|
||||
await self._client.delete_messages(message.to_id, [message.id])
|
||||
return True
|
||||
except MessageDeleteForbiddenError:
|
||||
logger.warning("No permission to delete message")
|
||||
return False
|
||||
except FloodWaitError as e:
|
||||
logger.warning(f"Flood wait when deleting message: {e.seconds}s")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting message: {e}")
|
||||
return False
|
||||
|
||||
async def _should_block_message(self, message: Message) -> tuple[bool, str]:
|
||||
"""Check if message should be blocked and return reason"""
|
||||
try:
|
||||
pack_name = self._extract_pack_name(message)
|
||||
emoji_text = self._extract_emoji_text(message)
|
||||
|
||||
if pack_name and pack_name in self.blocked_packs:
|
||||
return True, f"pack: {pack_name}"
|
||||
|
||||
if message.sticker:
|
||||
sticker_id = str(message.sticker.id)
|
||||
if sticker_id in self.blocked_stickers:
|
||||
return True, f"sticker: {sticker_id}"
|
||||
|
||||
if emoji_text and emoji_text in self.blocked_emojis:
|
||||
return True, f"emoji: {emoji_text}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking message: {e}")
|
||||
|
||||
return False, ""
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[link/название пака] — блокирует эмодзипак/стикерпак в личных сообщениях",
|
||||
en_doc="[link/pack name] — block emoji pack/sticker pack in private messages",
|
||||
)
|
||||
async def packblock(self, message: Message):
|
||||
"""Block emoji pack/sticker pack"""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
pack_name = args.lower().strip()
|
||||
|
||||
if pack_name in self.blocked_packs:
|
||||
return await utils.answer(message, self.strings["not_found"])
|
||||
|
||||
self.blocked_packs.add(pack_name)
|
||||
self._save_blocklists()
|
||||
|
||||
await utils.answer(message, self.strings["pack_blocked"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[reply] — блокирует определенный стикер",
|
||||
en_doc="[reply] — block specific sticker",
|
||||
)
|
||||
async def stickblock(self, message: Message):
|
||||
"""Block sticker from reply"""
|
||||
if not message.is_reply:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
reply_msg = await message.get_reply_message()
|
||||
if not reply_msg or not reply_msg.sticker:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
sticker_id = str(reply_msg.sticker.id)
|
||||
|
||||
if sticker_id in self.blocked_stickers:
|
||||
return await utils.answer(message, self.strings["not_found"])
|
||||
|
||||
self.blocked_stickers.add(sticker_id)
|
||||
self._save_blocklists()
|
||||
|
||||
await utils.answer(message, self.strings["sticker_blocked"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[reply/enter] — блокирует определенное эмодзи",
|
||||
en_doc="[reply/enter] — block specific emoji",
|
||||
)
|
||||
async def emojiblock(self, message: Message):
|
||||
"""Block emoji from reply or input"""
|
||||
args = utils.get_args_raw(message)
|
||||
emoji_text = None
|
||||
|
||||
if args:
|
||||
emoji_text = args.strip()
|
||||
if not emoji_text:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
else:
|
||||
if not message.is_reply:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
reply_msg = await message.get_reply_message()
|
||||
if not reply_msg:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
emoji_text = self._extract_emoji_text(reply_msg)
|
||||
if not emoji_text:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
if emoji_text in self.blocked_emojis:
|
||||
return await utils.answer(message, self.strings["not_found"])
|
||||
|
||||
self.blocked_emojis.add(emoji_text)
|
||||
self._save_blocklists()
|
||||
|
||||
await utils.answer(message, self.strings["emoji_blocked"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="— снимает блокировку с эмодзипака/стикерпака",
|
||||
en_doc="— unblock emoji pack/sticker pack",
|
||||
)
|
||||
async def ublpack(self, message: Message):
|
||||
"""Unblock emoji pack/sticker pack"""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
pack_name = args.lower().strip()
|
||||
|
||||
if pack_name in self.blocked_packs:
|
||||
self.blocked_packs.remove(pack_name)
|
||||
self._save_blocklists()
|
||||
await utils.answer(message, self.strings["pack_unblocked"])
|
||||
else:
|
||||
await utils.answer(message, self.strings["not_found"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[reply/enter] — снимает блокировку с определенного эмодзи/стикера",
|
||||
en_doc="[reply/enter] — unblock specific emoji/sticker",
|
||||
)
|
||||
async def ublthis(self, message: Message):
|
||||
"""Unblock emoji/sticker from reply or input"""
|
||||
args = utils.get_args_raw(message)
|
||||
|
||||
if args:
|
||||
item = args.strip()
|
||||
if not item:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
else:
|
||||
if not message.is_reply:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
reply_msg = await message.get_reply_message()
|
||||
if not reply_msg:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
if reply_msg.sticker:
|
||||
item = str(reply_msg.sticker.id)
|
||||
else:
|
||||
item = self._extract_emoji_text(reply_msg)
|
||||
|
||||
if not item:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
unblocked = False
|
||||
if item in self.blocked_stickers:
|
||||
self.blocked_stickers.remove(item)
|
||||
unblocked = True
|
||||
if item in self.blocked_emojis:
|
||||
self.blocked_emojis.remove(item)
|
||||
unblocked = True
|
||||
|
||||
if unblocked:
|
||||
self._save_blocklists()
|
||||
await utils.answer(message, self.strings["item_unblocked"])
|
||||
else:
|
||||
await utils.answer(message, self.strings["not_found"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="— показать список заблокированных паков/стикеров/эмодзи",
|
||||
en_doc="— show list of blocked packs/stickers/emojis",
|
||||
)
|
||||
async def blocklist(self, message: Message):
|
||||
"""Show blocklist"""
|
||||
packs_list = ", ".join(self.blocked_packs) if self.blocked_packs else "нет"
|
||||
stickers_list = (
|
||||
", ".join(self.blocked_stickers) if self.blocked_stickers else "нет"
|
||||
)
|
||||
emojis_list = ", ".join(self.blocked_emojis) if self.blocked_emojis else "нет"
|
||||
|
||||
result = []
|
||||
if packs_list:
|
||||
result.append(self.strings["list_packs"].format(packs_list))
|
||||
if stickers_list:
|
||||
result.append(self.strings["list_stickers"].format(stickers_list))
|
||||
if emojis_list:
|
||||
result.append(self.strings["list_emojis"].format(emojis_list))
|
||||
|
||||
if result:
|
||||
await utils.answer(message, "\n".join(result))
|
||||
else:
|
||||
await utils.answer(message, self.strings["all_cleared"])
|
||||
|
||||
@loader.command(ru_doc="— очистить все блокировки", en_doc="— clear all blocks")
|
||||
async def clearblocks(self, message: Message):
|
||||
"""Clear all blocks"""
|
||||
self.blocked_packs.clear()
|
||||
self.blocked_stickers.clear()
|
||||
self.blocked_emojis.clear()
|
||||
self._save_blocklists()
|
||||
|
||||
await utils.answer(message, self.strings["all_cleared"])
|
||||
|
||||
async def watcher(self, message: Message):
|
||||
"""Monitor messages and block unwanted content"""
|
||||
if not self._client or not self._db:
|
||||
return
|
||||
|
||||
if message.is_group or message.is_channel:
|
||||
return
|
||||
|
||||
should_block, reason = await self._should_block_message(message)
|
||||
|
||||
if should_block:
|
||||
logger.info(f"Blocking message: {reason}")
|
||||
await self._delete_message(message)
|
||||
@@ -1,90 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: FakeActions
|
||||
# Description: Module for simulating various actions in chat
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api FakeActions
|
||||
# scope: Api FakeActions 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class FakeActionsMod(loader.Module):
|
||||
"""Module for simulating various actions in chat"""
|
||||
|
||||
strings = {"name": "FakeActions"}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
"DEFAULT_DURATION", 5, "Default duration for actions in seconds"
|
||||
)
|
||||
|
||||
async def ftcmd(self, message):
|
||||
"""<seconds> - Simulates typing in chat for the specified number of seconds."""
|
||||
await self._simulate_action_command(message, "typing")
|
||||
|
||||
async def ffcmd(self, message):
|
||||
"""<seconds> - Simulates sending a file."""
|
||||
await self._simulate_action_command(message, "document")
|
||||
|
||||
async def fgcmd(self, message):
|
||||
"""<seconds> - Simulates recording a voice message."""
|
||||
await self._simulate_action_command(message, "record-audio")
|
||||
|
||||
async def fvgcmd(self, message):
|
||||
"""<seconds> - Simulates recording a video message."""
|
||||
await self._simulate_action_command(message, "record-round")
|
||||
|
||||
async def fpgcmd(self, message):
|
||||
"""<seconds> - Simulates playing a game."""
|
||||
await self._simulate_action_command(message, "game")
|
||||
|
||||
async def _simulate_action_command(self, message, action):
|
||||
"""General function for handling action simulation commands."""
|
||||
duration = self._parse_duration(message)
|
||||
if duration is None:
|
||||
await utils.answer(
|
||||
message,
|
||||
f"Usage: {self.get_prefix()}{message.raw_text.split()[0][1:]} <seconds>",
|
||||
)
|
||||
return
|
||||
|
||||
await message.delete()
|
||||
await self._simulate_action(message, action, duration)
|
||||
|
||||
def _parse_duration(self, message):
|
||||
"""Parse the duration from the message."""
|
||||
args = message.raw_text.split()
|
||||
if len(args) == 2 and args[1].isdigit():
|
||||
return int(args[1])
|
||||
return self.config["DEFAULT_DURATION"]
|
||||
|
||||
async def _simulate_action(self, message, action, duration):
|
||||
"""Simulate the specified action in chat."""
|
||||
async with message.client.action(message.chat_id, action):
|
||||
await asyncio.sleep(duration)
|
||||
@@ -1,179 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: FakeWallet
|
||||
# Description: Fun joke - fake crypto wallet. You can change cryptocurrency values using .cfg FakeWallet.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# -----------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: hikka_only
|
||||
# scope: hikka_min 1.4.2
|
||||
# -----------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class FakeWallet(loader.Module):
|
||||
"""Fun joke - fake crypto wallet. You can change cryptocurrency values using .cfg FakeWallet."""
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"Toncoin",
|
||||
0,
|
||||
lambda: self.strings("ton"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"Tether",
|
||||
0,
|
||||
lambda: self.strings("tether"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"Bitcoin",
|
||||
0,
|
||||
lambda: self.strings("btc"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"Etherium",
|
||||
0,
|
||||
lambda: self.strings("ether"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"Binance",
|
||||
0,
|
||||
lambda: self.strings("binc"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"Tron",
|
||||
0,
|
||||
lambda: self.strings("tron"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"USDT",
|
||||
0,
|
||||
lambda: self.strings("usdt"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"Gram",
|
||||
0,
|
||||
lambda: self.strings("gram"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"Litecoin",
|
||||
0,
|
||||
lambda: self.strings("lite"),
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
)
|
||||
|
||||
strings = {
|
||||
"name": "FakeWallet",
|
||||
"crypto": "Enter a value for your cryptovalute",
|
||||
"wallet": "<emoji document_id=5438626338560810621>👛</emoji> <b>Wallet</b>\n\n"
|
||||
"<emoji document_id=5215276644620586569>☺️</emoji> <a href='https://ton.org'>Toncoin</a>: {} TON\n\n"
|
||||
"<emoji document_id=5215699136258524363>☺️</emoji> <a href='https://tether.to'>Tether</a>: {} USDT\n\n"
|
||||
"<emoji document_id=5215590800003451651>☺️</emoji> <a href='https://bitcoin.org'>Bitcoin</a>: {} BTC\n\n"
|
||||
"<emoji document_id=5217867240044512715>☺️</emoji> <a href='https://etherium.org'>Etherium</a>: {} ETH\n\n"
|
||||
"<emoji document_id=5215595550237279768>☺️</emoji> <a href='https://binance.org'>Binance coin</a>: {} BNB\n\n"
|
||||
"<emoji document_id=5215437796088499410>☺️</emoji> <a href='https://tron.network'>TRON</a>: {} TRX\n\n"
|
||||
"<emoji document_id=5215440441788351459>☺️</emoji> <a href='https://www.centre.io/usdc'>USD Coin</a>: {} USDC\n\n"
|
||||
"<emoji document_id=5215267041073711005>☺️</emoji> <a href='https://gramcoin.org'>Gram</a>: {} GRAM\n\n"
|
||||
"<emoji document_id=5217877586620729050>☺️</emoji> <a href='https://litecoin.org'>Litecoin</a>: {} LTC",
|
||||
"ton": "Enter a value for Toncoin",
|
||||
"teth": "Enter a value for Tethcoin",
|
||||
"btc": "Enter a value for Bitcoin",
|
||||
"ether": "Enter a value for Etherium",
|
||||
"binc": "Enter a value for Binance coin",
|
||||
"tron": "Enter a value for Tron",
|
||||
"usdt": "Enter a value for USDT coin",
|
||||
"gram": "Enter a value for Gramcoin",
|
||||
"lite": "Enter a value for Litecoin",
|
||||
"info": "<b><emoji document_id=5305467350064047192>🫥</emoji><i>Attention!</b>\n\n"
|
||||
"<i><emoji document_id=5915991028430542030>☝️</emoji>This module is strictly prohibited from being used for the purposes of <b>scam, fraud and advertising</b>.\n\n"
|
||||
"<emoji document_id=5787190061644647815>🗣</emoji>The module is provided solely for entertainment purposes, and any violation of the <b>Rules for using the module</b>, if detected, will be subject <b>to appropriate punishment</i>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"wallet": "<emoji document_id=5438626338560810621>👛</emoji> <b>Кошелёк</b>\n\n"
|
||||
"<emoji document_id=5215276644620586569>☺️</emoji> <a href='https://ton.org'>Toncoin</a>: {} TON\n\n"
|
||||
"<emoji document_id=5215699136258524363>☺️</emoji> <a href='https://tether.to'>Tether</a>: {} USDT\n\n"
|
||||
"<emoji document_id=5215590800003451651>☺️</emoji> <a href='https://bitcoin.org'>Bitcoin</a>: {} BTC\n\n"
|
||||
"<emoji document_id=5217867240044512715>☺️</emoji> <a href='https://etherium.org'>Etherium</a>: {} ETH\n\n"
|
||||
"<emoji document_id=5215595550237279768>☺️</emoji> <a href='https://binance.org'>Binance coin</a>: {} BNB\n\n"
|
||||
"<emoji document_id=5215437796088499410>☺️</emoji> <a href='https://tron.network'>TRON</a>: {} TRX\n\n"
|
||||
"<emoji document_id=5215440441788351459>☺️</emoji> <a href='https://www.centre.io/usdc'>USD Coin</a>: {} USDC\n\n"
|
||||
"<emoji document_id=5215267041073711005>☺️</emoji> <a href='https://gramcoin.org'>Gram</a>: {} GRAM\n\n"
|
||||
"<emoji document_id=5217877586620729050>☺️</emoji> <a href='https://litecoin.org'>Litecoin</a>: {} LTC",
|
||||
"ton": "Введите количество валюты для Toncoin",
|
||||
"teth": "Введите количество валюты для Tethcoin",
|
||||
"btc": "Введите количество валюты для Bitcoin",
|
||||
"ether": "Введите количество валюты для Etherium",
|
||||
"binc": "Введите количество валюты для Binance coin",
|
||||
"tron": "Введите количество валюты для Tron",
|
||||
"usdt": "Введите количество валюты для USDT coin",
|
||||
"gram": "Введите количество валюты для Gramcoin",
|
||||
"lite": "Введите количество валюты для Litecoin",
|
||||
"info": "<b><emoji document_id=5305467350064047192>🫥</emoji><i> Внимание!</b>\n\n"
|
||||
"<i><emoji document_id=5915991028430542030>☝️</emoji> Использование этого модуля в целях <b>скама, обмана и рекламы</b> строго запрещено.\n\n"
|
||||
"<emoji document_id=5787190061644647815>🗣</emoji> Модуль предоставлен исключительно в развлекательных целях, и любое нарушение <b>Правил использования модуля</b>, если его обнаружат, будет подлежать соответствующему наказанию.</i>",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Чтобы заполучить поддельный кошелек",
|
||||
en_doc="To get a fake wallet",
|
||||
)
|
||||
@loader.command()
|
||||
async def fwalletcmd(self, message):
|
||||
ton = self.config["Toncoin"]
|
||||
teth = self.config["Tether"]
|
||||
btc = self.config["Bitcoin"]
|
||||
ether = self.config["Etherium"]
|
||||
binc = self.config["Binance"]
|
||||
tron = self.config["Tron"]
|
||||
usdt = self.config["USDT"]
|
||||
gram = self.config["Gram"]
|
||||
lite = self.config["Litecoin"]
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("wallet").format(
|
||||
ton, teth, btc, ether, binc, tron, usdt, gram, lite
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Информация о FakeModule",
|
||||
en_doc="Info about FakeModule",
|
||||
)
|
||||
@loader.command()
|
||||
async def fwinfocmd(self, message):
|
||||
await utils.answer(message, self.strings("info"))
|
||||
@@ -1,154 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: FolderAutoRead
|
||||
# Description: Automatically reads chats in selected folders
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from telethon import functions
|
||||
from telethon.tl.types import DialogFilter, InputPeerChannel
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class FolderAutoReadMod(loader.Module):
|
||||
"""Automatically reads chats in selected folders"""
|
||||
|
||||
strings = {
|
||||
"name": "FolderAutoRead",
|
||||
"not_exists_or_already_added": "<emoji document_id=5278578973595427038>🚫</emoji> <b>This folder does not exists or it is already added for tracking!</b>",
|
||||
"_cls_doc": "Automatically reads chats in selected folders every 60 seconds!",
|
||||
"_cmd_doc_addfolder": "Adds folder to the tracking list by it's name. Usage: .addfolder FolderName",
|
||||
"_cmd_doc_listfolders": "Prints list of tracked folders",
|
||||
"_cmd_doc_delfolder": "Deletes folder from the tracking list",
|
||||
"wrong_args": "<emoji document_id=5278578973595427038>🚫</emoji> <b>Wrong arguments!</b> Usage: .addfolder/delfolder FolderName\n\n<i>Tip: If you trying to delete the folder from the tracking list, double-check that it really still tracking using .listfolders</i>",
|
||||
"listfolders": "<emoji document_id=5278227821364275264>📁</emoji> <b>List of tracked folders:</b>\n",
|
||||
"delfolder": "<emoji document_id=5276384644739129761>🗑</emoji> <b>Folder is successfully deleted from the tracking list!</b>",
|
||||
"addfolder": "<emoji document_id=5278227821364275264>📁</emoji> <b>Folder is successfully added to the tracking list!</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"not_exists_or_already_added": "<emoji document_id=5278578973595427038>🚫</emoji> <b>Такой папки не существует, или она уже добавлена для отслеживания!</b>",
|
||||
"_cls_doc": "Автоматически читает чаты в выбранных папках каждые 60 секунд!",
|
||||
"_cmd_doc_addfolder": "Добавляет папки в список отслеживания по их названию. Использование: .addfolder НазваниеПапки",
|
||||
"_cmd_doc_listfolders": "Выводит список отслеживаемых папок",
|
||||
"_cmd_doc_delfolder": "Удаляет папку из списка для отслежнивания",
|
||||
"wrong_args": "<emoji document_id=5278578973595427038>🚫</emoji> <b>Неверные аргументы!</b> Использование: .addfolder/delfolder НазваниеПапки\n\n<i>Совет: Если вы пытаетесь удалить папку из списка отслеживания, проверьте, что она вообще отслеживается, используя .listfolders</i>",
|
||||
"listfolders": "<emoji document_id=5278227821364275264>📁</emoji> <b>Список отслеживаемых папок:</b>\n",
|
||||
"delfolder": "<emoji document_id=5276384644739129761>🗑</emoji> <b>Папка успешно удалена из листа отслеживания!</b>",
|
||||
"addfolder": "<emoji document_id=5278227821364275264>📁</emoji> <b>Папка успешно добавлена в лист отслеживания!</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.tracked_folders = []
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.tracked_folders = self.pointer("tracked_folders", [])
|
||||
|
||||
async def _read_peers(self, peers):
|
||||
for peer in peers:
|
||||
try:
|
||||
await self._client(functions.messages.ReadMentionsRequest(peer=peer))
|
||||
await self._client(functions.messages.ReadReactionsRequest(peer=peer))
|
||||
if isinstance(peer, InputPeerChannel):
|
||||
await self._client(
|
||||
functions.channels.ReadHistoryRequest(channel=peer, max_id=0)
|
||||
)
|
||||
else:
|
||||
await self._client(
|
||||
functions.messages.ReadHistoryRequest(peer=peer, max_id=0)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to read peer {peer}: {e}")
|
||||
|
||||
@loader.loop(interval=60, autostart=True)
|
||||
async def read_chats_in_folders(self):
|
||||
if self.tracked_folders:
|
||||
all_folders = await self._client(
|
||||
functions.messages.GetDialogFiltersRequest()
|
||||
)
|
||||
for folder_name in self.tracked_folders:
|
||||
match = next(
|
||||
(
|
||||
f
|
||||
for f in all_folders.filters
|
||||
if isinstance(f, DialogFilter) and f.title.text == folder_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if match is None:
|
||||
continue
|
||||
await self._read_peers(match.pinned_peers)
|
||||
await self._read_peers(match.include_peers)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Добавить папку в список отслеживания",
|
||||
en_doc="Add folder to the tracking list",
|
||||
)
|
||||
async def addfolder(self, message):
|
||||
arg = utils.get_args_raw(message)
|
||||
if arg:
|
||||
all_folders = await self._client(
|
||||
functions.messages.GetDialogFiltersRequest()
|
||||
)
|
||||
match = next(
|
||||
(
|
||||
f
|
||||
for f in all_folders.filters
|
||||
if isinstance(f, DialogFilter) and f.title.text == arg
|
||||
),
|
||||
None,
|
||||
)
|
||||
if match and arg not in self.tracked_folders:
|
||||
self.tracked_folders.append(arg)
|
||||
await utils.answer(message, self.strings("addfolder"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("not_exists_or_already_added"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("wrong_args"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Удалить папку из списка отслеживания",
|
||||
en_doc="Delete folder from the tracking list",
|
||||
)
|
||||
async def delfolder(self, message):
|
||||
arg = utils.get_args_raw(message)
|
||||
if arg and arg in self.tracked_folders:
|
||||
self.tracked_folders.remove(arg)
|
||||
await utils.answer(message, self.strings("delfolder"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("wrong_args"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Список отслеживаемых папок",
|
||||
en_doc="List tracked folders",
|
||||
)
|
||||
async def listfolders(self, message):
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("listfolders")
|
||||
+ "\n".join(f"• {folder}" for folder in self.tracked_folders),
|
||||
)
|
||||
@@ -1,124 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: GigaChat
|
||||
# Description: Module for using GigaChat
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api GigaChat
|
||||
# scope: Api GigaChat 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class GigaChatMod(loader.Module):
|
||||
"""Module for using GigaChat"""
|
||||
|
||||
strings = {
|
||||
"name": "GigaChat",
|
||||
"api_key_missing": "Please set the API key in the module configuration.",
|
||||
"query_missing": "Please enter a query after the command.",
|
||||
"response_error": "Failed to get a response from GigaChat.",
|
||||
"error_occurred": "An error occurred: {}",
|
||||
"formatted_response": (
|
||||
"<emoji document_id=6030848053177486888>❓</emoji> Query: {}\n"
|
||||
"<emoji document_id=6030400221232501136>🤖</emoji> GigaChat: {}"
|
||||
),
|
||||
"giga_model": "List of GigaChat models:\n{}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"api_key_missing": "Пожалуйста, установите API ключ в конфигурации модуля.",
|
||||
"query_missing": "Пожалуйста, введите запрос после команды.",
|
||||
"response_error": "Не удалось получить ответ от GigaChat.",
|
||||
"error_occurred": "Произошла ошибка: {}",
|
||||
"formatted_response": (
|
||||
"<emoji document_id=6030848053177486888>❓</emoji> Запрос: {}\n"
|
||||
"<emoji document_id=6030400221232501136>🤖</emoji> GigaChat: {}"
|
||||
),
|
||||
"giga_model": "Список моделей GigaChat:\n{}",
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.hmodslib = await self.import_lib(
|
||||
"https://files.archquise.ru/HModsLibrary.py"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"GIGACHAT_API_KEY",
|
||||
None,
|
||||
"Введите ваш API ключ для GigaChat, Чтобы получить ключ API, перейдите сюда: https://developers.sber.ru/studio/workspaces",
|
||||
validator=loader.validators.Hidden(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"GIGACHAT_MODEL",
|
||||
"GigaChat",
|
||||
"Введите модель, ее можно получить при команде .gigamodel",
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Получите исчерпывающий ответ на свой вопрос",
|
||||
en_doc="Get GigaResponse to your question",
|
||||
)
|
||||
async def giga(self, message):
|
||||
api_key = self.config["GIGACHAT_API_KEY"]
|
||||
if not api_key:
|
||||
return await utils.answer(message, self.strings("api_key_missing"))
|
||||
|
||||
query = utils.get_args_raw(message)
|
||||
if not query:
|
||||
return await utils.answer(message, self.strings("query_missing"))
|
||||
|
||||
try:
|
||||
response = await self.hmodslib.get_giga_response(api_key, query)
|
||||
if response:
|
||||
await utils.answer(
|
||||
message, self.strings("formatted_response").format(query, response)
|
||||
)
|
||||
else:
|
||||
await utils.answer(message, self.strings("response_error"))
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings("error_occurred").format(str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Получить список моделей",
|
||||
en_doc="Get a list of models",
|
||||
)
|
||||
async def gigamodel(self, message):
|
||||
api_key = self.config["GIGACHAT_API_KEY"]
|
||||
if not api_key:
|
||||
return await utils.answer(message, self.strings("api_key_missing"))
|
||||
|
||||
try:
|
||||
response = await self.hmodslib.get_giga_models(api_key)
|
||||
if response:
|
||||
await utils.answer(message, self.strings("giga_model").format(response))
|
||||
else:
|
||||
await utils.answer(message, self.strings("response_error"))
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings("error_occurred").format(str(e)))
|
||||
@@ -1,44 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: H
|
||||
# Description: H.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: H
|
||||
# scope: H 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
|
||||
@loader.tds
|
||||
class H(loader.Module):
|
||||
"""H"""
|
||||
|
||||
strings = {"name": "H", "h": "H"}
|
||||
strings_ru = {"h": "H"}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="H",
|
||||
)
|
||||
async def h(self, message):
|
||||
"""H"""
|
||||
await utils.answer(message, self.strings("h"))
|
||||
@@ -1,306 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: HAFK
|
||||
# Description: Your personal assistant while you are in AFK mode
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: HAFK
|
||||
# scope: HAFK 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
from telethon import types
|
||||
from telethon.utils import get_peer_id
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class HAFK(loader.Module):
|
||||
strings = {
|
||||
"name": "HAFK",
|
||||
"afk_on": "<emoji document_id=5357107687484038897>🫶</emoji> <b>AFK mode is on!</b>",
|
||||
"afk_on_reason": "<emoji document_id=5357107687484038897>🫶</emoji> <b>AFK mode is on!</b>\n\n<b>Reason:</b> <i>{}</i>",
|
||||
"afk_here_on": "<emoji document_id=5357107687484038897>🫶</emoji> <b>AFK mode is on in this chat!</b>",
|
||||
"afk_here_on_reason": "<emoji document_id=5357107687484038897>🫶</emoji> <b>AFK mode is on in this chat!</b>\n\n<b>Reason:</b> <i>{}</i>",
|
||||
"afk_off": "<emoji document_id=5472234792659458002>🤌</emoji> <b>AFK mode is off!</b>",
|
||||
"afk_off_time": "<emoji document_id=5472234792659458002>🤌</emoji> <b>AFK mode is off!</b>\n\n<b>You were AFK for:</b> {}",
|
||||
"afk_off_here_time": "<emoji document_id=5472234792659458002>🤌</emoji> <b>AFK mode is off in this chat!</b>\n\n<b>You were AFK for:</b> {}",
|
||||
"already_afk": "<emoji document_id=5465665476971471368>❌</emoji> <b>You are already in AFK mode!</b>",
|
||||
"already_afk_here": "<emoji document_id=5465665476971471368>❌</emoji> <b>You are already in AFK mode in this chat!</b>",
|
||||
"not_afk": "<emoji document_id=5330365133745038003>😐</emoji> <b>AFK mode is already off.</b>",
|
||||
"not_afk_here": "<emoji document_id=5330365133745038003>😐</emoji> <b>AFK mode is already off in this chat.</b>",
|
||||
"afk_message": "<emoji document_id=5330130448142049118>🫤</emoji> <b>I'm currently not accepting messages!</b>\n<b>Reason:</b> <i>{}</i>\n\n<i>Inactive mode has been on for:</i> {}",
|
||||
"afk_message_reason": "<emoji document_id=5330130448142049118>🫤</emoji> <b>I'm currently not accepting messages!</b>\n<b>Reason:</b> <i>{}</i>\n\n<i>Inactive mode has been on for:</i> {}",
|
||||
"afk_set_failed": "<b>Failed to set AFK status. Check logs.</b>",
|
||||
"added_excluded_chat": "<b>Chat {} added to excluded chats.</b>",
|
||||
"removed_excluded_chat": "<b>Chat {} removed from excluded chats.</b>",
|
||||
"excluded_chats_list": "<b>Excluded chats:</b>\n{}",
|
||||
"no_excluded_chats": "<b>No chats are excluded.</b>",
|
||||
"invalid_chat_id": "<b>Invalid chat ID.</b>",
|
||||
"already_excluded": "<b>Chat {} is already excluded.</b>",
|
||||
"not_excluded": "<b>Chat {} is not excluded.</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"afk_on": "<emoji document_id=5357107687484038897>🫶</emoji> <b>AFK-режим включен!</b>",
|
||||
"afk_on_reason": "<emoji document_id=5357107687484038897>🫶</emoji> <b>AFK-режим включен!</b>\n\n<b>Причина:</b> <i>{}</i>",
|
||||
"afk_here_on": "<emoji document_id=5357107687484038897>🫶</emoji> <b>AFK-режим включен в этом чате!</b>",
|
||||
"afk_here_on_reason": "<emoji document_id=5357107687484038897>🫶</emoji> <b>AFK-режим включен в этом чате!</b>\n\n<b>Причина:</b> <i>{}</i>",
|
||||
"afk_off": "<emoji document_id=5472234792659458002>🤌</emoji> <b>AFK-режим отключен!</b>",
|
||||
"afk_off_time": "<emoji document_id=5472234792659458002>🤌</emoji> <b>AFK-режим отключен!</b>\n\n<b>Вы были AFK:</b> {}",
|
||||
"afk_off_here_time": "<emoji document_id=5472234792659458002>🤌</emoji> <b>AFK-режим отключен в этом чате!</b>\n\n<b>Вы были AFK:</b> {}",
|
||||
"already_afk": "<emoji document_id=5465665476971471368>❌</emoji> <b>Вы уже находитесь в AFK-режиме!</b>",
|
||||
"already_afk_here": "<emoji document_id=5465665476971471368>❌</emoji> <b>Вы уже находитесь в AFK-режиме в этом чате!</b>",
|
||||
"not_afk": "<emoji document_id=5330365133745038003>😐</emoji> <b>AFK-режим уже отключён.</b>",
|
||||
"not_afk_here": "<emoji document_id=5330365133745038003>😐</emoji> <b>AFK-режим уже отключен в этом чате.</b>",
|
||||
"afk_message": "<emoji document_id=5330130448142049118>🫤</emoji> <b>На данный момент я не принимаю сообщения!</b>\n<b>Причина:</b> <i>{}</i>\n\n<i>С момента включения режима неактивности:</i> {}",
|
||||
"afk_message_reason": "<emoji document_id=5330130448142049118>🫤</emoji> <b>На данный момент я не принимаю сообщения!</b>\n<b>Причина:</b> <i>{}</i>\n\n<i>С момента включения режима неактивности:</i> {}",
|
||||
"afk_set_failed": "<b>Не удалось установить AFK-статус. Проверьте логи.</b>",
|
||||
"added_excluded_chat": "<b>Чат {} добавлен в список исключений.</b>",
|
||||
"removed_excluded_chat": "<b>Чат {} удален из списка исключений.</b>",
|
||||
"excluded_chats_list": "<b>Список исключенных чатов:</b>\n{}",
|
||||
"no_excluded_chats": "<b>Нет исключенных чатов.</b>",
|
||||
"invalid_chat_id": "<b>Неверный ID чата.</b>",
|
||||
"already_excluded": "<b>Чат {} уже исключен.</b>",
|
||||
"not_excluded": "<b>Чат {} не исключен.</b>",
|
||||
}
|
||||
|
||||
DEFAULT_AFK_TIMEOUT = 60
|
||||
DEFAULT_DELETE_TIMEOUT = 5
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"excluded_chats",
|
||||
[],
|
||||
lambda: "List of chat IDs where AFK mode will not be activated",
|
||||
validator=loader.validators.Series(
|
||||
validator=loader.validators.Integer()
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
self._me = await client.get_me()
|
||||
self._ratelimit_cache = {}
|
||||
|
||||
self.global_afk = self.db.get(__name__, "afk", False)
|
||||
self.global_afk_reason = self.db.get(__name__, "afk_reason", None)
|
||||
self.global_gone_time = self.db.get(__name__, "gone_afk", None)
|
||||
|
||||
logger.debug(
|
||||
f"Initial global AFK state: afk={self.global_afk}, reason={self.global_afk_reason}, gone_time={self.global_gone_time}"
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[reason / none] – Установить режим AFK",
|
||||
en_doc="[reason / none] – Set AFK mode globally",
|
||||
)
|
||||
async def afk(self, message):
|
||||
await self._afk_toggle(message, global_afk=True)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[reason / none] – Установить режим AFK только в этом чате.",
|
||||
en_doc="[reason / none] – Set AFK mode in current chat only.",
|
||||
)
|
||||
async def afkhere(self, message):
|
||||
await self._afk_toggle(message, global_afk=False)
|
||||
|
||||
async def _afk_toggle(self, message, global_afk: bool):
|
||||
chat_id = utils.get_chat_id(message)
|
||||
already_afk_string = "already_afk" if global_afk else "already_afk_here"
|
||||
afk_on_string = "afk_on" if global_afk else "afk_here_on"
|
||||
afk_on_reason_string = "afk_on_reason" if global_afk else "afk_here_on_reason"
|
||||
|
||||
if self._is_afk_enabled(chat_id, global_afk):
|
||||
await utils.answer(message, self.strings(already_afk_string, message))
|
||||
return
|
||||
|
||||
reason = utils.get_args_raw(message) or None
|
||||
success = self._set_afk(
|
||||
True, reason=reason, chat_id=chat_id if not global_afk else None
|
||||
)
|
||||
|
||||
if not success:
|
||||
await utils.answer(message, self.strings("afk_set_failed", message))
|
||||
return
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings(afk_on_reason_string, message).format(reason)
|
||||
if reason
|
||||
else self.strings(afk_on_string, message),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выйти из режима AFK",
|
||||
en_doc="Exit AFK mode",
|
||||
)
|
||||
async def unafk(self, message):
|
||||
await self._unafk_toggle(message, global_afk=True)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выйти из режима AFK в этом чате",
|
||||
en_doc="Exit AFK mode in this chat",
|
||||
)
|
||||
async def unafkhere(self, message):
|
||||
await self._unafk_toggle(message, global_afk=False)
|
||||
|
||||
async def _unafk_toggle(self, message, global_afk: bool):
|
||||
chat_id = utils.get_chat_id(message)
|
||||
not_afk_string = "not_afk" if global_afk else "not_afk_here"
|
||||
afk_off_time_string = "afk_off_time" if global_afk else "afk_off_here_time"
|
||||
|
||||
if not self._is_afk_enabled(chat_id, global_afk):
|
||||
await utils.answer(message, self.strings(not_afk_string, message))
|
||||
return
|
||||
|
||||
total_gone_time = self._calculate_total_afk_time(
|
||||
datetime.datetime.now().replace(microsecond=0),
|
||||
chat_id=chat_id if not global_afk else None,
|
||||
)
|
||||
|
||||
self._set_afk(False, chat_id=chat_id if not global_afk else None)
|
||||
|
||||
await self.allmodules.log("unafk" if global_afk else "unafkhere")
|
||||
await utils.answer(
|
||||
message, self.strings(afk_off_time_string, message).format(total_gone_time)
|
||||
)
|
||||
|
||||
async def watcher(self, message):
|
||||
if not isinstance(message, types.Message):
|
||||
return
|
||||
|
||||
chat_id = get_peer_id(message.peer_id)
|
||||
user_id = getattr(message.to_id, "user_id", None)
|
||||
is_mentioned = message.mentioned or user_id == self._me.id
|
||||
is_private = isinstance(message.to_id, types.PeerUser)
|
||||
|
||||
if not (is_mentioned or is_private) or chat_id in self.config["excluded_chats"]:
|
||||
return
|
||||
|
||||
reason = None
|
||||
gone_time = None
|
||||
|
||||
if self._is_afk_enabled(chat_id, False):
|
||||
reason = self.db.get(__name__, f"afk_here_{chat_id}_reason", None)
|
||||
gone_time = self.db.get(__name__, f"gone_afk_here_{chat_id}", None)
|
||||
elif self.global_afk:
|
||||
reason = self.global_afk_reason
|
||||
gone_time = self.global_gone_time
|
||||
else:
|
||||
return
|
||||
|
||||
if gone_time is None:
|
||||
logger.warning(f"No 'gone' time found for chat {chat_id}. Cannot send AFK.")
|
||||
return
|
||||
|
||||
afk_message = await self._send_afk_message(message, reason, gone_time)
|
||||
if afk_message:
|
||||
await self._delete_message(afk_message, self.DEFAULT_DELETE_TIMEOUT)
|
||||
|
||||
def _set_afk(self, value: bool, reason: str = None, chat_id: int = None) -> bool:
|
||||
try:
|
||||
key_prefix = f"afk_here_{chat_id}" if chat_id else "afk"
|
||||
self.db.set(__name__, key_prefix, value)
|
||||
self.db.set(__name__, f"{key_prefix}_reason", reason)
|
||||
|
||||
gone_key = f"gone_{key_prefix}"
|
||||
gone_time = time.time() if value else None
|
||||
self.db.set(__name__, gone_key, gone_time)
|
||||
logger.debug(f"AFK status updated. {gone_key} set to {gone_time}")
|
||||
|
||||
if chat_id is None:
|
||||
self.global_afk = value
|
||||
self.global_afk_reason = reason
|
||||
self.global_gone_time = gone_time
|
||||
logger.debug("Updated global AFK vars")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception(f"Error setting AFK status: {e}")
|
||||
return False
|
||||
|
||||
def _calculate_total_afk_time(
|
||||
self, now: datetime.datetime, chat_id: int = None
|
||||
) -> datetime.timedelta:
|
||||
key_prefix = f"afk_here_{chat_id}" if chat_id else "afk"
|
||||
gone_time = self.db.get(__name__, f"gone_{key_prefix}")
|
||||
|
||||
if gone_time is None:
|
||||
return datetime.timedelta(seconds=0)
|
||||
|
||||
try:
|
||||
gone = datetime.datetime.fromtimestamp(gone_time).replace(microsecond=0)
|
||||
return now - gone
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating AFK time: {e}")
|
||||
return datetime.timedelta(seconds=0)
|
||||
|
||||
async def _send_afk_message(self, message, reason, gone_time):
|
||||
user = await utils.get_user(message)
|
||||
if user.is_self or user.bot or user.verified:
|
||||
logger.debug("User is self, bot, or verified. Not sending AFK message.")
|
||||
return None
|
||||
|
||||
now = datetime.datetime.now().replace(microsecond=0)
|
||||
try:
|
||||
gone = datetime.datetime.fromtimestamp(gone_time).replace(microsecond=0)
|
||||
except (TypeError, ValueError) as e:
|
||||
logger.error(f"Error converting timestamp: {e}")
|
||||
return None
|
||||
|
||||
time_string = str(now - gone)
|
||||
ret = (
|
||||
self.strings("afk_message_reason", message).format(reason, time_string)
|
||||
if reason
|
||||
else self.strings("afk_message", message).format(time_string)
|
||||
)
|
||||
|
||||
try:
|
||||
reply = await utils.answer(message, ret, reply_to=message)
|
||||
return reply
|
||||
except Exception as e:
|
||||
logger.exception(f"Error sending AFK message: {e}")
|
||||
return None
|
||||
|
||||
def _is_afk_enabled(self, chat_id: int = None, global_afk: bool = False) -> bool:
|
||||
return (
|
||||
self.global_afk
|
||||
if global_afk
|
||||
else self.db.get(__name__, f"afk_here_{chat_id}", False)
|
||||
)
|
||||
|
||||
async def _delete_message(self, message, delay):
|
||||
await asyncio.sleep(delay)
|
||||
try:
|
||||
await self.client.delete_messages(message.chat_id, message.id)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error deleting message: {e}")
|
||||
@@ -1,107 +0,0 @@
|
||||
# 🔐 Licensed under the GNU AGPLv3.
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: HInstall
|
||||
# Description: Provides H:Mods modules installation trough buttons
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: PyCryptodome
|
||||
# ---------------------------------------------------------------------------------
|
||||
# #################################################################################
|
||||
# ########## This module is based on @hikariatama 's hikkamods_socket!! ###########
|
||||
# #################################################################################
|
||||
|
||||
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.Signature import pkcs1_15
|
||||
|
||||
from telethon.tl.types import Message
|
||||
from telethon import functions, types
|
||||
from typing import Optional
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
pubkey_data = """
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvekpGqKiD2HZwY/J7jZv
|
||||
PwGRobAS2TaC9HU5LUNRDg90jA/r8xgoFhlCBJocq8+XvJIWpgmIEYWJCz0KpCXu
|
||||
Meu42bAXvLqniDOqnOt8FjXFapGZvEMLen1CLCRr1OQhVNpRlPjjWo7PM+YpUnbw
|
||||
giqEZ9nA5DQ5Gi0vsSHXAnBa+ZIsxaY3EwosHMvUUhnnijcbBpkyYRJ8atvsT9AX
|
||||
cNS+NjDE4Kj8jSnArQ1D1Ct1pcZEXD6DUk2k3HAD4OlZS5nY5IFchWEcpLT/Fjbt
|
||||
BzGBZCJZ+rp8qR1tCVvVTV3itACc8O0Pirmptkrxb3A4pC0S8oxYBFQcnZAlIiw3
|
||||
uX36O90AkRwbsdnsp2JVg5AAPUYvdsMoCGG+cSGZC73arqcrvn0VFo7EhsYq/1Ds
|
||||
CevorFI4TiLVbSlFSVnX5baqmTj+XNhgaWWmiY/+mhErzsWtpCOHYFitf1xqp3zD
|
||||
9O2Vs7lQIxMsHFISAEhn8BqQxvlwslfcjmbuJxkYriqAHXQGS3IZDXhEZXwouOUV
|
||||
HGN2YD5aLK0L8OuTNY5cf1TN8C5xgVZoEodAKqAva/i1v/F6IQk3iEo0ncgypeyg
|
||||
NM1TUudkQ+f1wXqLj2YaVKqRdKswl9vgYpUCHjGZfN+WYT4DbOMrJm1OFeen6geo
|
||||
xqON1/xeRBgkE3tna3RuhmUCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
pubkey = RSA.importKey(pubkey_data.strip())
|
||||
|
||||
|
||||
@loader.tds
|
||||
class HInstallMod(loader.Module):
|
||||
"""Provides H:Mods modules installation trough buttons"""
|
||||
|
||||
strings = {
|
||||
"name": "HInstall",
|
||||
"_cls_doc": "Provides H:Mods modules installation trough buttons",
|
||||
"module_downloaded": "Module downloaded!"
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Позволяет устанавливать модули от H:Mods через кнопки",
|
||||
"module_downloaded": "Модуль загружен!"
|
||||
}
|
||||
|
||||
async def on_dlmod(self, client, db):
|
||||
ent = await self.client(functions.users.GetFullUserRequest('@hinstall_bot'))
|
||||
if ent.full_user.blocked:
|
||||
await self.client(functions.contacts.UnblockRequest('@hinstall_bot'))
|
||||
await self.client.send_message('@hinstall_bot', '/start')
|
||||
await self.client.delete_dialog('@hinstall_bot')
|
||||
|
||||
|
||||
async def _load_module(self, url: str, message: Optional[Message] = None):
|
||||
loader_m = self.lookup("loader")
|
||||
|
||||
await loader_m.download_and_install(url, None)
|
||||
|
||||
if getattr(loader_m, "_fully_loaded", getattr(loader_m, "fully_loaded", False)):
|
||||
getattr(
|
||||
loader_m,
|
||||
"_update_modules_in_db",
|
||||
getattr(loader_m, "update_modules_in_db", lambda: None),
|
||||
)()
|
||||
|
||||
|
||||
async def watcher(self, message: Message):
|
||||
if not isinstance(message, Message):
|
||||
return
|
||||
if message.sender_id == 8104671142 and message.raw_text.startswith("#install"):
|
||||
await message.delete()
|
||||
fileref = (
|
||||
message.raw_text.split("#install:")[1].strip().splitlines()[0].strip()
|
||||
)
|
||||
sig = base64.b64decode(message.raw_text.splitlines()[1].strip().encode())
|
||||
try:
|
||||
h = SHA256.new(fileref.encode("utf-8"))
|
||||
pkcs1_15.new(pubkey).verify(h, sig)
|
||||
except (ValueError, TypeError):
|
||||
logger.error(f"Got message with non-verified signature ({fileref=})")
|
||||
return
|
||||
await self._load_module(f"https://raw.githubusercontent.com/archquise/H.Modules/refs/heads/main/{fileref}", message)
|
||||
await self.client.send_message('@hinstall_bot', self.strings['module_downloaded'])
|
||||
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: InfoBannersManager
|
||||
# Description: Автоматически меняет баннеры на случайные из выбранного списка через заданный промежуток времени
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
|
||||
import logging
|
||||
import random
|
||||
|
||||
from .. import loader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class InfoBannersManagerMod(loader.Module):
|
||||
"""Автоматически меняет баннеры на случайные из выбранного списка через заданный промежуток времени"""
|
||||
|
||||
strings = {"name": "InfoBannersManager"}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"enabled",
|
||||
False,
|
||||
"Включить автоматическую смену баннеров",
|
||||
validator=loader.validators.Boolean(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"delay",
|
||||
60,
|
||||
"Задержка между изменениями баннеров в секундах",
|
||||
validator=loader.validators.Integer(minimum=1),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"bannerslist",
|
||||
None,
|
||||
"Список ссылок на баннеры",
|
||||
validator=loader.validators.Series(validator=loader.validators.Link()),
|
||||
),
|
||||
)
|
||||
|
||||
async def banner_changer(self):
|
||||
"""Change banner periodically"""
|
||||
try:
|
||||
if not self.config["bannerslist"]:
|
||||
logger.warning("Banners list is empty!")
|
||||
return
|
||||
|
||||
banner = random.choice(self.config["bannerslist"])
|
||||
instance = self.lookup("HerokuInfo")
|
||||
if not instance:
|
||||
instance = self.lookup("HikkaInfo")
|
||||
|
||||
if instance:
|
||||
instance.config["banner_url"] = banner
|
||||
logger.info(f"Banner changed to: {banner}")
|
||||
else:
|
||||
logger.warning("Info module not found!")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error changing banner: {e}")
|
||||
|
||||
@loader.loop(interval=60, autostart=False)
|
||||
async def banner_loop(self):
|
||||
"""Main banner changing loop"""
|
||||
if not self.config["enabled"]:
|
||||
return
|
||||
|
||||
await self.banner_changer()
|
||||
|
||||
# Update interval from config
|
||||
self.banner_loop.set_interval(self.config["delay"])
|
||||
|
||||
async def client_ready(self):
|
||||
"""Initialize the banner changer loop"""
|
||||
if self.config["enabled"]:
|
||||
self.banner_loop.start()
|
||||
|
||||
def on_config_update(self, config_key, new_value):
|
||||
"""Handle config updates"""
|
||||
if config_key == "enabled":
|
||||
if new_value:
|
||||
self.banner_loop.start()
|
||||
else:
|
||||
self.banner_loop.stop()
|
||||
elif config_key == "delay":
|
||||
# Update interval immediately
|
||||
self.banner_loop.set_interval(new_value)
|
||||
@@ -1,88 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: InlineButton
|
||||
# Description: Create inline button
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: InlineButton
|
||||
# scope: InlineButton 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from .. import loader, utils
|
||||
from ..inline.types import InlineQuery
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class InlineButtonMod(loader.Module):
|
||||
"""Create inline buttons with enhanced functionality"""
|
||||
|
||||
strings = {
|
||||
"name": "InlineButton",
|
||||
"titles": "🔘 Create message with Inline Button",
|
||||
"error_title": "<emoji document_id=5854929766146118183>❌</emoji> Error",
|
||||
"error_description": "<emoji document_id=5854929766146118183>❌</emoji> Invalid input format. Please provide exactly three comma-separated values: message, name, url.",
|
||||
"error_message": "<emoji document_id=5854929766146118183>❌</emoji> Make sure your input is formatted as: message, name, url.",
|
||||
"button_created": "<emoji document_id=5854762571659218443>✅</emoji> Button created successfully!",
|
||||
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Please provide arguments: message, name, url.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"titles": "🔘 Создать сообщение с Inline Кнопкой",
|
||||
"error_title": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка",
|
||||
"error_description": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат ввода. Пожалуйста, укажите ровно три значения, разделенных запятыми: сообщение, имя, url.",
|
||||
"error_message": "<emoji document_id=5854929766146118183>❌</emoji> Убедитесь, что ваш ввод имеет следующий формат: сообщение, имя, url.",
|
||||
"button_created": "<emoji document_id=5854762571659218443>✅</emoji> Кнопка успешно создана!",
|
||||
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Укажите аргументы: сообщение, имя, url.",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Создать inline кнопку\nНапример: @username_bot crinl Текст сообщения, Текст кнопки, Ссылка в кнопке",
|
||||
en_doc="Create an inline button\nexample: @username_bot crinl Message text, Button text, Link in the button",
|
||||
)
|
||||
async def crinl_inline_handler(self, query: InlineQuery):
|
||||
args = utils.get_args_raw(query.query)
|
||||
|
||||
if not args:
|
||||
return {
|
||||
"title": self.strings("error_title"),
|
||||
"description": self.strings("error_description"),
|
||||
"message": self.strings("no_args"),
|
||||
}
|
||||
|
||||
args_list = [arg.strip() for arg in args.split(",")]
|
||||
|
||||
if len(args_list) != 3:
|
||||
return {
|
||||
"title": self.strings("error_title"),
|
||||
"description": self.strings("error_description"),
|
||||
"message": self.strings("error_message"),
|
||||
}
|
||||
|
||||
message, name, url = args_list
|
||||
return True, {
|
||||
"message": message,
|
||||
"reply_markup": [{"text": name, "url": url}],
|
||||
"description": self.strings("button_created"),
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: InlineCoin
|
||||
# Description: Mini game heads or tails.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: InlineCoin
|
||||
# scope: InlineCoin 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict
|
||||
|
||||
from .. import loader
|
||||
from ..inline.types import InlineQuery
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class CoinFlipMod(loader.Module):
|
||||
"""Mini coin flip game"""
|
||||
|
||||
strings = {
|
||||
"name": "InlineCoin",
|
||||
"titles": "🪙 Heads or Tails?",
|
||||
"description": "🎲 Let's find out!",
|
||||
"heads": "🦅 An eagle fell out!",
|
||||
"tails": "🪙 Tails fell out!",
|
||||
"edge": "🙀 Miraculously, the coin remained on its edge!",
|
||||
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Please provide a command to flip.",
|
||||
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> An error occurred: {error}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"titles": "🪙 Орёл или решка?",
|
||||
"description": "🎲 Давай узнаем!",
|
||||
"heads": "🦅 Выпал орёл!",
|
||||
"tails": "🪙 Выпала решка!",
|
||||
"edge": "🙀 Чудо, монетка осталась на ребре!",
|
||||
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Укажите команду для подбрасывания монетки.",
|
||||
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка: {error}",
|
||||
}
|
||||
|
||||
def get_coin_flip_result(self) -> Dict[str, str]:
|
||||
"""Get coin flip result with better formatting"""
|
||||
return {
|
||||
"title": self.strings["titles"],
|
||||
"description": self.strings["description"],
|
||||
"message": f"<b>{random.choice([self.strings['heads'], self.strings['tails']])}</b>",
|
||||
"thumb": "https://github.com/Codwizer/ReModules/blob/main/assets/images.png",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Подбросить монетку",
|
||||
en_doc="Flip a coin",
|
||||
)
|
||||
async def coin_inline_handler(self, query: InlineQuery):
|
||||
"""Handle coin flip inline query"""
|
||||
if not query.args:
|
||||
return {
|
||||
"title": self.strings["titles"],
|
||||
"description": self.strings["no_args"],
|
||||
"message": self.strings["no_args"],
|
||||
}
|
||||
|
||||
result = self.get_coin_flip_result()
|
||||
return {
|
||||
"title": self.strings["titles"],
|
||||
"description": self.strings["description"],
|
||||
"message": result["message"],
|
||||
"thumb": result["thumb"],
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: InlineHelper
|
||||
# Description: Basic management of the UB in case only the inline works
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: InlineHelper
|
||||
# scope: InlineHelper 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
from .. import loader, main, utils
|
||||
from ..inline.types import InlineQuery
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class InlineHelperMod(loader.Module):
|
||||
"""Basic management of the UB in case only the inline works"""
|
||||
|
||||
strings = {
|
||||
"name": "InlineHelper",
|
||||
"call_restart": "<emoji document_id=5188311512791393083>🔄</emoji> Restarting...",
|
||||
"call_update": "<emoji document_id=5188311512791393083>🔄</emoji> Updating...",
|
||||
"res_prefix": "<emoji document_id=5854762571659218443>✅</emoji> Prefix successfully reset to default",
|
||||
"restart_inline_handler_title": "🔄 Restart Userbot",
|
||||
"restart_inline_handler_description": "Restart your userbot via inline",
|
||||
"restart_inline_handler_message": "🔄 Restart",
|
||||
"update_inline_handler_title": "🔄 Update Userbot",
|
||||
"update_inline_handler_description": "Update your userbot via inline",
|
||||
"update_inline_handler_message": "🔄 Update",
|
||||
"terminal_inline_handler_title": "💻 Command Executed",
|
||||
"terminal_inline_handler_description": "Command executed successfully",
|
||||
"terminal_inline_handler_message": "Command <code>{text}</code> executed successfully in terminal",
|
||||
"modules_inline_handler_title": "📦 Modules",
|
||||
"modules_inline_handler_description": "List all installed modules",
|
||||
"modules_inline_handler_result": "📦 All installed modules:\n\n",
|
||||
"resetprefix_inline_handler_title": "⚠️ Reset Prefix",
|
||||
"resetprefix_inline_handler_description": "Reset your prefix back to default (be careful!)",
|
||||
"resetprefix_inline_handler_message": "Are you sure you want to reset your prefix to default dot?",
|
||||
"resetprefix_inline_handler_reply_text_yes": "Yes, reset it",
|
||||
"resetprefix_inline_handler_reply_text_no": "No, cancel",
|
||||
"error_no_module": "<emoji document_id=5854929766146118183>❌</emoji> Module not found: {module}",
|
||||
"error_command_failed": "<emoji document_id=5854929766146118183>❌</emoji> Command execution failed: {error}",
|
||||
"error_git_failed": "<emoji document_id=5854929766146118183>❌</emoji> Git operation failed: {error}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"call_restart": "<emoji document_id=5188311512791393083>🔄</emoji> Перезагружаю...",
|
||||
"call_update": "<emoji document_id=5188311512791393083>🔄</emoji> Обновляю...",
|
||||
"res_prefix": "<emoji document_id=5854762571659218443>✅</emoji> Префикс успешно сброшен по умолчанию",
|
||||
"restart_inline_handler_title": "<emoji document_id=5188311512791393083>🔄</emoji> Перезагрузить юзербота",
|
||||
"restart_inline_handler_description": "Перезагрузить юзербота через инлайн",
|
||||
"restart_inline_handler_message": "<emoji document_id=5188311512791393083>🔄</emoji> Перезагрузка",
|
||||
"update_inline_handler_title": "<emoji document_id=5188311512791393083>🔄</emoji> Обновить юзербота",
|
||||
"update_inline_handler_description": "Обновить юзербота через инлайн",
|
||||
"update_inline_handler_message": "<emoji document_id=5188311512791393083>🔄</emoji> Обновить",
|
||||
"terminal_inline_handler_title": "<emoji document_id=5854762571659218443>💻</emoji> Команда выполнена!",
|
||||
"terminal_inline_handler_description": "Команда успешно выполнена.",
|
||||
"terminal_inline_handler_message": "Команда <code>{text}</code> была успешно выполнена в терминале",
|
||||
"modules_inline_handler_title": "<emoji document_id=5854762571659218443>📦</emoji> Модули",
|
||||
"modules_inline_handler_description": "Вывести список установленных модулей",
|
||||
"modules_inline_handler_result": "<emoji document_id=5854762571659218443>📦</emoji> Все установленные модули:\n\n",
|
||||
"resetprefix_inline_handler_title": "<emoji document_id=5854929766146118183>⚠️</emoji> Сбросить префикс",
|
||||
"resetprefix_inline_handler_description": "Сбросить префикс по умолчанию (осторожно!)",
|
||||
"resetprefix_inline_handler_message": "Вы действительно хотите сбросить ваш префикс и установить стандартную точку?",
|
||||
"resetprefix_inline_handler_reply_text_yes": "Да, сбросить",
|
||||
"resetprefix_inline_handler_reply_text_no": "Нет, отменить",
|
||||
"error_no_module": "<emoji document_id=5854929766146118183>❌</emoji> Модуль не найден: {module}",
|
||||
"error_command_failed": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка выполнения команды: {error}",
|
||||
"error_git_failed": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка git операции: {error}",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.client = None
|
||||
self.db = None
|
||||
self._base_dir = utils.get_base_dir()
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
|
||||
async def restart(self, call):
|
||||
"""Restart callback"""
|
||||
logger.info("InlineHelper: Restarting userbot...")
|
||||
try:
|
||||
await call.edit(self.strings["call_restart"])
|
||||
|
||||
await asyncio.create_subprocess_exec(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
f"cd {self._base_dir} && git reset --hard HEAD && git pull",
|
||||
],
|
||||
cwd=self._base_dir,
|
||||
)
|
||||
await call.edit(self.strings["call_update"])
|
||||
await asyncio.sleep(2)
|
||||
await asyncio.create_subprocess_exec(
|
||||
[sys.executable, "-c", f"cd {self._base_dir} && git pull"],
|
||||
cwd=self._base_dir,
|
||||
)
|
||||
await call.edit(self.strings["res_prefix"])
|
||||
except Exception as e:
|
||||
logger.error(f"Restart failed: {e}")
|
||||
await call.edit(self.strings["error_git_failed"].format(error=str(e)))
|
||||
|
||||
async def update(self, call):
|
||||
"""Update callback"""
|
||||
logger.info("InlineHelper: Updating userbot...")
|
||||
try:
|
||||
await call.edit(self.strings["call_update"])
|
||||
|
||||
await asyncio.create_subprocess_exec(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
f"cd {self._base_dir} && git reset --hard HEAD && git pull",
|
||||
],
|
||||
cwd=self._base_dir,
|
||||
)
|
||||
await call.edit(self.strings["res_prefix"])
|
||||
except Exception as e:
|
||||
logger.error(f"Update failed: {e}")
|
||||
await call.edit(self.strings["error_git_failed"].format(error=str(e)))
|
||||
|
||||
async def reset_prefix(self, call):
|
||||
"""Reset prefix callback"""
|
||||
try:
|
||||
self.db.set(main.__name__, "command_prefix", ".")
|
||||
await call.edit(self.strings["res_prefix"])
|
||||
except Exception as e:
|
||||
logger.error(f"Reset prefix failed: {e}")
|
||||
await call.edit(self.strings["error_command_failed"].format(error=str(e)))
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="Перезагрузить юзербота",
|
||||
en_doc="Reboot the userbot",
|
||||
)
|
||||
async def restart_inline_handler(self, _: InlineQuery):
|
||||
return {
|
||||
"title": self.strings("restart_inline_handler_title"),
|
||||
"description": self.strings("restart_inline_handler_description"),
|
||||
"message": self.strings("restart_inline_handler_message"),
|
||||
"reply_markup": [
|
||||
{
|
||||
"text": self.strings("restart_inline_handler_reply_text"),
|
||||
"callback": self.restart,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="Обновить юзербота",
|
||||
en_doc="Update the userbot",
|
||||
)
|
||||
async def update_inline_handler(self, _: InlineQuery):
|
||||
return {
|
||||
"title": self.strings("update_inline_handler_title"),
|
||||
"description": self.strings("update_inline_handler_description"),
|
||||
"message": self.strings("update_inline_handler_message"),
|
||||
"reply_markup": [
|
||||
{
|
||||
"text": self.strings("update_inline_handler_reply_text"),
|
||||
"callback": self.update,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="Выполнить команду в терминале (лучше сразу подготовить команду и просто вставить)",
|
||||
en_doc="Execute the command in the terminal (it is better to prepare the command immediately and just paste it)",
|
||||
)
|
||||
async def terminal_inline_handler(self, query: InlineQuery):
|
||||
"""Execute terminal command safely"""
|
||||
if not query.args:
|
||||
return {
|
||||
"title": self.strings["terminal_inline_handler_title"],
|
||||
"description": self.strings["terminal_inline_handler_description"],
|
||||
"message": self.strings["terminal_inline_handler_message"].format(
|
||||
text="No command provided"
|
||||
),
|
||||
}
|
||||
|
||||
command_text = query.args.strip()
|
||||
if not command_text:
|
||||
return {
|
||||
"title": self.strings["terminal_inline_handler_title"],
|
||||
"description": self.strings["terminal_inline_handler_description"],
|
||||
"message": self.strings["terminal_inline_handler_message"].format(
|
||||
text="No command provided"
|
||||
),
|
||||
}
|
||||
|
||||
if any(char in command_text for char in ["&", "|", ";", "`", "$"]):
|
||||
return {
|
||||
"title": self.strings["terminal_inline_handler_title"],
|
||||
"description": self.strings["terminal_inline_handler_description"],
|
||||
"message": self.strings["error_command_failed"].format(
|
||||
error="Invalid characters in command"
|
||||
),
|
||||
}
|
||||
|
||||
try:
|
||||
args = shlex.split(command_text)
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
args,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=self._base_dir,
|
||||
text=True,
|
||||
)
|
||||
|
||||
stdout, stderr = await process.communicate()
|
||||
stdout.decode().strip() if stdout else ""
|
||||
error = stderr.decode().strip() if stderr else ""
|
||||
|
||||
if error:
|
||||
return {
|
||||
"title": self.strings["terminal_inline_handler_title"],
|
||||
"description": self.strings["terminal_inline_handler_description"],
|
||||
"message": self.strings["error_command_failed"].format(error=error),
|
||||
}
|
||||
|
||||
return {
|
||||
"title": self.strings["terminal_inline_handler_title"],
|
||||
"description": self.strings["terminal_inline_handler_description"],
|
||||
"message": self.strings["terminal_inline_handler_message"].format(
|
||||
text=command_text
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"title": self.strings["terminal_inline_handler_title"],
|
||||
"description": self.strings["terminal_inline_handler_description"],
|
||||
"message": self.strings["error_command_failed"].format(error=str(e)),
|
||||
}
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="Вывести список установленных модулей через инлайн",
|
||||
en_doc="Display a list of installed modules via the inline",
|
||||
)
|
||||
async def modules_inline_handler(self, query: InlineQuery):
|
||||
"""List all installed modules"""
|
||||
try:
|
||||
result = self.strings["modules_inline_handler_result"]
|
||||
|
||||
for mod in self.allmodules.modules:
|
||||
try:
|
||||
name = mod.strings["name"]
|
||||
except KeyError:
|
||||
name = mod.__class__.__name__
|
||||
result += f"• {name}\n"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing modules: {e}")
|
||||
result = f"Error listing modules: {str(e)}"
|
||||
|
||||
return {
|
||||
"title": self.strings["modules_inline_handler_title"],
|
||||
"description": self.strings["modules_inline_handler_description"],
|
||||
"message": result,
|
||||
}
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="Сбросить префикс (осторожнее, сбрасывает ваш префикс на . )",
|
||||
en_doc="Reset the prefix (be careful, resets your prefix to . )",
|
||||
)
|
||||
async def resetprefix_inline_handler(self, _: InlineQuery):
|
||||
return {
|
||||
"title": self.strings("resetprefix_inline_handler_title"),
|
||||
"description": self.strings("resetprefix_inline_handler_description"),
|
||||
"message": self.strings("resetprefix_inline_handler_message"),
|
||||
"reply_markup": [
|
||||
{
|
||||
"text": self.strings("resetprefix_inline_handler_reply_text_yes"),
|
||||
"callback": self.reset_prefix,
|
||||
},
|
||||
{
|
||||
"text": self.strings("resetprefix_inline_handler_reply_text_no"),
|
||||
"action": "close",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: IrisSimpleMod
|
||||
# Description: Module for basic interaction with Iris.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: IrisSimpleMod
|
||||
# scope: IrisSimpleMod 1.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
__version__ = (1, 0, 1)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class IrisSimpleMod(loader.Module):
|
||||
"""Module for basic interaction with Iris bot"""
|
||||
|
||||
strings = {
|
||||
"name": "IrisSimpleMod",
|
||||
"checking_bag": "<emoji document_id=5188311512791393083>🌎</emoji> Checking bag...",
|
||||
"bag_result": "<emoji document_id=5854762571659218443>✅</emoji> Your bag: <code>{}</code>",
|
||||
"farming": "<emoji document_id=5188311512791393083>🌎</emoji> Farming iris-coins...",
|
||||
"farm_result": "<emoji document_id=5854762571659218443>✅</emoji> Farm result: <code>{}</code>",
|
||||
"getting_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Getting user stats...",
|
||||
"stats_result": "<emoji document_id=5854762571659218443>✅</emoji> User stats: <code>{}</code>",
|
||||
"bot_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Getting bot stats...",
|
||||
"bot_stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Bot stats: <code>{}</code>",
|
||||
"error_no_response": "<emoji document_id=5854929766146118183>❌</emoji> No response from bot. Please try again.",
|
||||
"error_timeout": "<emoji document_id=5854929766146118183>❌</emoji> Request timeout. Please try again.",
|
||||
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> An error occurred: {error}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"checking_bag": "<emoji document_id=5188311512791393083>🌎</emoji> Проверка мешка...",
|
||||
"bag_result": "<emoji document_id=5854762571659218443>✅</emoji> Ваш мешок: <code>{}</code>",
|
||||
"farming": "<emoji document_id=5188311512791393083>🌎</emoji> Фарм ирис-коинов...",
|
||||
"farm_result": "<emoji document_id=5854762571659218443>✅</emoji> Результат фарма: <code>{}</code>",
|
||||
"getting_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Получение статистики пользователя...",
|
||||
"stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Статистика пользователя: <code>{}</code>",
|
||||
"bot_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Получение статистики ботов...",
|
||||
"bot_stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Статистика ботов: <code>{}</code>",
|
||||
"error_no_response": "<emoji document_id=5854929766146118183>❌</emoji> Нет ответа от бота. Попробуйте еще раз.",
|
||||
"error_timeout": "<emoji document_id=5854929766146118183>❌</emoji> Таймаут запроса. Попробуйте еще раз.",
|
||||
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка: {error}",
|
||||
}
|
||||
|
||||
async def _send_and_delete(
|
||||
self, message, command_message: str, response_timeout: int = 15
|
||||
) -> Optional[str]:
|
||||
"""Send command to Iris and get response with timeout"""
|
||||
try:
|
||||
async with self.client.conversation(
|
||||
self._iris_user_id, timeout=self._timeout
|
||||
) as conv:
|
||||
await conv.send_message(command_message)
|
||||
await message.delete()
|
||||
|
||||
response_msg = await conv.get_response()
|
||||
if response_msg:
|
||||
await utils.answer(message, response_msg.text)
|
||||
return response_msg.text
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error in conversation: {e}")
|
||||
await utils.answer(
|
||||
message, self.strings["error_general"].format(error=str(e))
|
||||
)
|
||||
return None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Проверить мешок",
|
||||
en_doc="Check bag",
|
||||
)
|
||||
async def bag(self, message):
|
||||
"""Check bag"""
|
||||
await utils.answer(message, self.strings["checking_bag"])
|
||||
|
||||
result = await self._send_and_delete(message, "мешок", response_timeout=20)
|
||||
|
||||
if result:
|
||||
await utils.answer(message, self.strings["bag_result"].format(result))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Зафармить ирис-коины",
|
||||
en_doc="Farm iris-coins",
|
||||
)
|
||||
async def farm(self, message):
|
||||
"""Farm iris-coins"""
|
||||
await utils.answer(message, self.strings["farming"])
|
||||
|
||||
result = await self._send_and_delete(message, "ферма", response_timeout=25)
|
||||
|
||||
if result:
|
||||
await utils.answer(message, self.strings["farm_result"].format(result))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Вывести анкету",
|
||||
en_doc="Display user stats",
|
||||
)
|
||||
async def irisstats(self, message):
|
||||
"""Display user stats"""
|
||||
await utils.answer(message, self.strings["getting_stats"])
|
||||
|
||||
result = await self._send_and_delete(message, "анкета", response_timeout=20)
|
||||
|
||||
if result:
|
||||
await utils.answer(message, self.strings["stats_result"].format(result))
|
||||
@@ -1,138 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: KBSwapper
|
||||
# Description: KBSwapper is a module for changing the keyboard layout
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: KBSwapper
|
||||
# scope: KBSwapper 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import string
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EN_TO_RU = str.maketrans(
|
||||
"qwertyuiop[]asdfghjkl;'zxcvbnm,./`" + 'QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?~',
|
||||
"йцукенгшщзхъфывапролджэячсмитьбю.ё" + "ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,Ё",
|
||||
)
|
||||
|
||||
RU_TO_EN = str.maketrans(
|
||||
"йцукенгшщзхъфывапролджэячсмитьбю.ё" + "ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,Ё",
|
||||
"qwertyuiop[]asdfghjkl;'zxcvbnm,./`" + 'QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?~',
|
||||
)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class KBSwapperMod(loader.Module):
|
||||
"""KBSwapper is a module for changing the keyboard layout"""
|
||||
|
||||
strings = {
|
||||
"name": "KBSwapper",
|
||||
"no_reply": "<emoji document_id=5774077015388852135>❌</emoji> <b>Please reply to a message.</b>",
|
||||
"no_text": "<emoji document_id=5774077015388852135>❌</emoji> <b>The replied message does not contain text.</b>",
|
||||
"original_message": "<emoji document_id=5260450573768990626>➡️</emoji> <b>Original message:</b>\n<code>{original}</code>",
|
||||
"fixed_message": "<emoji document_id=5774022692642492953>✅</emoji> <b>Fixed message:</b>\n<code>{fixed}</code>",
|
||||
"error": "<emoji document_id=5774077015388852135>❌</emoji> <b>An error occurred while processing the message.</b>",
|
||||
}
|
||||
strings_ru = {
|
||||
"no_reply": "<emoji document_id=5774077015388852135>❌</emoji> <b>Пожалуйста, ответьте на сообщение.</b>",
|
||||
"no_text": "<emoji document_id=5774077015388852135>❌</emoji> <b>Отвеченное сообщение не содержит текста.</b>",
|
||||
"original_message": "<emoji document_id=5260450573768990626>➡️</emoji> <b>Оригинальное сообщение:</b>\n<code>{original}</code>",
|
||||
"fixed_message": "<emoji document_id=5774022692642492953>✅</emoji> <b>Исправленное сообщение:</b>\n<code>{fixed}</code>",
|
||||
"error": "<emoji document_id=5774077015388852135>❌</emoji> <b>Произошла ошибка при обработке сообщения.</b>",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="При ответе на своё сообщение меняет раскладку путем редактирования, на чужое — в отдельном сообщении.",
|
||||
en_doc="Change keyboard layout for the replied message.",
|
||||
)
|
||||
async def swap(self, message):
|
||||
reply = await message.get_reply_message()
|
||||
if not reply:
|
||||
await utils.answer(message, self.strings("no_reply"))
|
||||
return
|
||||
|
||||
original_text = reply.text
|
||||
if not original_text or original_text.isspace():
|
||||
await utils.answer(message, self.strings("no_text"))
|
||||
return
|
||||
|
||||
try:
|
||||
trimmed_text = original_text.strip()
|
||||
|
||||
has_russian = any(
|
||||
char
|
||||
in "йцукенгшщзхъфывапролджэячсмитьбюёЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮЁ"
|
||||
for char in trimmed_text
|
||||
)
|
||||
has_english = any(char in string.ascii_letters for char in trimmed_text)
|
||||
|
||||
logger.debug(
|
||||
f"Text analysis - Russian: {has_russian}, English: {has_english}, Text: {trimmed_text[:50]}..."
|
||||
)
|
||||
|
||||
if has_russian and not has_english:
|
||||
fixed_text = original_text.translate(RU_TO_EN)
|
||||
logger.debug("Detected Russian text, translating to English")
|
||||
elif has_english and not has_russian:
|
||||
fixed_text = original_text.translate(EN_TO_RU)
|
||||
logger.debug("Detected English text, translating to Russian")
|
||||
else:
|
||||
first_char = (
|
||||
trimmed_text[0].lower()
|
||||
if trimmed_text
|
||||
else original_text[0].lower()
|
||||
)
|
||||
logger.debug(
|
||||
f"Mixed/other characters detected, first char: {first_char}"
|
||||
)
|
||||
if first_char in string.ascii_lowercase:
|
||||
fixed_text = original_text.translate(EN_TO_RU)
|
||||
logger.debug("Using first char detection: English to Russian")
|
||||
elif first_char in "йцукенгшщзхъфывапролджэячсмитьбюё":
|
||||
fixed_text = original_text.translate(RU_TO_EN)
|
||||
logger.debug("Using first char detection: Russian to English")
|
||||
else:
|
||||
fixed_text = original_text
|
||||
logger.debug("No recognizable letters, returning as is")
|
||||
|
||||
if fixed_text != original_text:
|
||||
logger.debug(
|
||||
f"Text changed: {original_text[:30]}... → {fixed_text[:30]}..."
|
||||
)
|
||||
else:
|
||||
logger.debug("Text unchanged")
|
||||
|
||||
if message.sender_id == reply.sender_id:
|
||||
await reply.edit(fixed_text)
|
||||
else:
|
||||
await utils.answer(
|
||||
message,
|
||||
f"{self.strings('original_message').format(original=original_text)}\n"
|
||||
f"{self.strings('fixed_message').format(fixed=fixed_text)}",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during swap: {e}")
|
||||
await utils.answer(message, self.strings("error"))
|
||||
@@ -1,17 +0,0 @@
|
||||
Proprietary License Agreement
|
||||
|
||||
Copyright (c) 2024-29 Archquise
|
||||
|
||||
Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
For any inquiries or requests for permissions, please contact archquise@gmail.com.
|
||||
@@ -1,112 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Meme
|
||||
# Description: Random memes
|
||||
# Author: @hikka_mods
|
||||
# Commands:
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Meme
|
||||
# scope: Meme 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import random # noqa: F401
|
||||
|
||||
import aiohttp # noqa: F401
|
||||
from bs4 import BeautifulSoup # noqa: F401
|
||||
|
||||
from .. import loader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class MemesMod(loader.Module):
|
||||
"""Random memes"""
|
||||
|
||||
strings = {
|
||||
"name": "Memes",
|
||||
"done": "☄️ Catch the meme",
|
||||
"still": "🔄 Update",
|
||||
"dell": "❌ Close",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"done": "☄️ Лови мем",
|
||||
"still": "🔄 Обновить",
|
||||
"dell": "❌ Закрыть",
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.hmodslib = await self.import_lib(
|
||||
"https://files.archquise.ru/HModsLibrary.py"
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="",
|
||||
en_doc="",
|
||||
)
|
||||
async def memescmd(self, message):
|
||||
img = await self.hmodslib.get_random_image()
|
||||
await self.inline.form(
|
||||
text=self.strings("done"),
|
||||
photo=img,
|
||||
message=message,
|
||||
reply_markup=[
|
||||
[
|
||||
{
|
||||
"text": self.strings("still"),
|
||||
"callback": self.ladno,
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": self.strings("dell"),
|
||||
"callback": self.dell,
|
||||
}
|
||||
],
|
||||
],
|
||||
silent=True,
|
||||
)
|
||||
|
||||
async def ladno(self, call):
|
||||
img = await self.hmodslib.get_random_image()
|
||||
await call.edit(
|
||||
text=self.strings("done"),
|
||||
photo=img,
|
||||
reply_markup=[
|
||||
[
|
||||
{
|
||||
"text": self.strings("still"),
|
||||
"callback": self.ladno,
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": self.strings("dell"),
|
||||
"callback": self.dell,
|
||||
}
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
async def dell(self, call):
|
||||
"""Callback button"""
|
||||
await call.delete()
|
||||
@@ -1,305 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: MessageMonitor
|
||||
# Description: Monitor messages for trigger words in all chats.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: MessageMonitor
|
||||
# scope: MessageMonitor 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Optional
|
||||
|
||||
from telethon.types import Message
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class MessageMonitor(loader.Module):
|
||||
"""
|
||||
Monitor messages for trigger words in all chats.
|
||||
"""
|
||||
|
||||
strings = {
|
||||
"name": "MessageMonitor",
|
||||
"triggers_set": "<emoji document_id=5854762571659218443>✅</emoji> Trigger words have been set: <code>{}</code>",
|
||||
"triggers_not_set": "<emoji document_id=5854929766146118183>❌</emoji> Trigger words have not been set",
|
||||
"target_set": "<emoji document_id=5854762571659218443>✅</emoji> Target chat for notifications has been set",
|
||||
"target_not_set": "<emoji document_id=5854929766146118183>❌</emoji> Target chat for notifications has not been set",
|
||||
"monitoring_started": "<emoji document_id=5188311512791393083>🌎</emoji> Monitoring has started",
|
||||
"monitoring_stopped": "<emoji document_id=5854929766146118183>❌</emoji> Monitoring has stopped",
|
||||
"monitoring_status": "<emoji document_id=5188311512791393083>🌎</emoji> Monitoring <b>{}</b>",
|
||||
"triggers_example": "<emoji document_id=5854929766146118183>❌</emoji> Example: <code>.triggers word1 word2</code>",
|
||||
"monitoring_status_on": "<emoji document_id=5854762571659218443>✅</emoji> enabled",
|
||||
"monitoring_status_off": "<emoji document_id=5854929766146118183>❌</emoji> disabled",
|
||||
"ignore_set": "<emoji document_id=5854762571659218443>✅</emoji> Ignored chats have been set: <code>{}</code>",
|
||||
"ignore_none": "<emoji document_id=5854929766146118183>❌</emoji> Ignored chats have not been set",
|
||||
"ignore_example": "<emoji document_id=5854929766146118183>❌</emoji> Example: <code>.ignore 123456789 -987654321</code> (chat IDs)",
|
||||
"no_reply": "<emoji document_id=5854929766146118183>❌</emoji> Reply to a message in the desired chat or specify its ID",
|
||||
"monitoring_msg": (
|
||||
"<emoji document_id=5854929766146118183>🚨</emoji> <b>Trigger word detected!</b> <emoji document_id=5854929766146118183>🚨</emoji>\n\n"
|
||||
"<b>Chat:</b> <code>{}</code>\n"
|
||||
"<b>User:</b> {}\n"
|
||||
"<b>Link:</b> <a href='{}'>{}</a>\n\n"
|
||||
"<b>Message:</b>\n{}"
|
||||
),
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"triggers_set": "<emoji document_id=5854762571659218443>✅</emoji> Триггерные слова установлены: <code>{}</code>",
|
||||
"triggers_not_set": "<emoji document_id=5854929766146118183>❌</emoji> Триггерные слова не установлены",
|
||||
"target_set": "<emoji document_id=5854762571659218443>✅</emoji> Целевой чат для уведомлений установлен",
|
||||
"target_not_set": "<emoji document_id=5854929766146118183>❌</emoji> Целевой чат для уведомлений не установлен",
|
||||
"monitoring_started": "<emoji document_id=5188311512791393083>🌎</emoji> Мониторинг запущен",
|
||||
"monitoring_stopped": "<emoji document_id=5854929766146118183>❌</emoji> Мониторинг остановлен",
|
||||
"monitoring_status": "<emoji document_id=5188311512791393083>🌎</emoji> Мониторинг <b>{}</b>",
|
||||
"triggers_example": "<emoji document_id=5854929766146118183>❌</emoji> Пример: <code>.triggers слово1 слово2</code>",
|
||||
"monitoring_status_on": "<emoji document_id=5854762571659218443>✅</emoji> включен",
|
||||
"monitoring_status_off": "<emoji document_id=5854929766146118183>❌</emoji> выключен",
|
||||
"ignore_set": "<emoji document_id=5854762571659218443>✅</emoji> Игнорируемые чаты установлены: <code>{}</code>",
|
||||
"ignore_none": "<emoji document_id=5854929766146118183>❌</emoji> Игнорируемые чаты не установлены",
|
||||
"ignore_example": "<emoji document_id=5854929766146118183>❌</emoji> Пример: <code>.ignore 123456789 -987654321</code> (ID чатов)",
|
||||
"no_reply": "<emoji document_id=5854929766146118183>❌</emoji> Ответьте на сообщение в нужном чате или укажите его ID",
|
||||
"monitoring_msg": (
|
||||
"<emoji document_id=5854929766146118183>🚨</emoji> <b>Обнаружено триггерное слово!</b> <emoji document_id=5854929766146118183>🚨</emoji>\n\n"
|
||||
"<b>Чат:</b> <code>{}</code>\n"
|
||||
"<b>Пользователь:</b> {}\n"
|
||||
"<b>Ссылка:</b> <a href='{}'>{}</a>\n\n"
|
||||
"<b>Сообщение:</b>\n{}"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"triggers",
|
||||
[],
|
||||
"List of trigger words to monitor",
|
||||
validator=loader.validators.Series(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"target_chat",
|
||||
None,
|
||||
"Target chat ID for notifications",
|
||||
validator=loader.validators.Integer(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"ignore_chats",
|
||||
[],
|
||||
"List of chat IDs to ignore",
|
||||
validator=loader.validators.Series(),
|
||||
),
|
||||
)
|
||||
self._triggers: List[str] = []
|
||||
self._target_chat: Optional[int] = None
|
||||
self._ignore_chats: List[int] = []
|
||||
self._compiled_patterns: List[re.Pattern] = []
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
"""Initialize module when client is ready"""
|
||||
await self._update_config()
|
||||
self.client = client
|
||||
|
||||
async def _update_config(self):
|
||||
"""Update internal configuration and compile regex patterns"""
|
||||
self._triggers = [trigger.lower() for trigger in self.config["triggers"]]
|
||||
self._target_chat = self.config["target_chat"]
|
||||
self._ignore_chats = [
|
||||
int(chat_id)
|
||||
for chat_id in self.config["ignore_chats"]
|
||||
if str(chat_id).lstrip("-").isdigit()
|
||||
]
|
||||
|
||||
self._compiled_patterns = [
|
||||
re.compile(r"\b" + re.escape(trigger) + r"\b", re.IGNORECASE)
|
||||
for trigger in self._triggers
|
||||
]
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Показать статус мониторинга",
|
||||
en_doc="Show monitoring status",
|
||||
)
|
||||
async def status(self, message: Message):
|
||||
"""Show current monitoring status"""
|
||||
status_text = (
|
||||
self.strings["monitoring_status_on"]
|
||||
if self._target_chat and self._triggers
|
||||
else self.strings["monitoring_status_off"]
|
||||
)
|
||||
await utils.answer(
|
||||
message, self.strings["monitoring_status"].format(status_text)
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Установить триггерные слова. Пример: .triggers слово1 слово2",
|
||||
en_doc="Set trigger words. Example: .triggers word1 word2",
|
||||
)
|
||||
async def triggers(self, message: Message):
|
||||
"""Set trigger words"""
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings["triggers_example"])
|
||||
return
|
||||
|
||||
self._triggers = [arg.lower() for arg in args]
|
||||
self.config["triggers"] = self._triggers
|
||||
await self._update_config()
|
||||
await utils.answer(
|
||||
message, self.strings["triggers_set"].format(", ".join(self._triggers))
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Установить целевой чат для уведомлений. Ответьте на сообщение или укажите ID",
|
||||
en_doc="Set target chat for notifications. Reply to a message or provide its ID",
|
||||
)
|
||||
async def settarget(self, message: Message):
|
||||
"""Set target chat"""
|
||||
args = utils.get_args_raw(message)
|
||||
chat_id = None
|
||||
|
||||
if getattr(message, "is_reply", False):
|
||||
reply_message = await message.get_reply_message()
|
||||
if reply_message and hasattr(reply_message, "chat_id"):
|
||||
chat_id = reply_message.chat_id
|
||||
elif args and (args.isdigit() or (args.startswith("-") and args[1:].isdigit())):
|
||||
chat_id = int(args)
|
||||
|
||||
if chat_id:
|
||||
self.config["target_chat"] = chat_id
|
||||
self._target_chat = chat_id
|
||||
await utils.answer(message, self.strings["target_set"])
|
||||
else:
|
||||
await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Установить игнорируемые чаты. Укажите ID чатов через пробел.",
|
||||
en_doc="Set ignored chats. Provide chat IDs separated by space.",
|
||||
)
|
||||
async def ignore(self, message: Message):
|
||||
"""Set ignored chats"""
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings["ignore_example"])
|
||||
return
|
||||
|
||||
valid_ids = []
|
||||
for arg in args:
|
||||
if arg.isdigit() or (arg.startswith("-") and arg[1:].isdigit()):
|
||||
valid_ids.append(int(arg))
|
||||
|
||||
self.config["ignore_chats"] = valid_ids
|
||||
await self._update_config()
|
||||
|
||||
if valid_ids:
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["ignore_set"].format(", ".join(map(str, valid_ids))),
|
||||
)
|
||||
else:
|
||||
await utils.answer(message, self.strings["ignore_none"])
|
||||
|
||||
@loader.watcher(out=False, only_messages=True)
|
||||
async def message_watcher(self, message: Message):
|
||||
"""Watch for messages containing trigger words"""
|
||||
if not self._target_chat or not self._triggers:
|
||||
return
|
||||
|
||||
chat_id = getattr(message, "chat_id", None)
|
||||
if chat_id and chat_id in self._ignore_chats:
|
||||
logger.debug(f"Message in ignored chat: {chat_id}. Skipping monitoring.")
|
||||
return
|
||||
|
||||
text = getattr(message, "text", "")
|
||||
if not text:
|
||||
return
|
||||
|
||||
found_triggers = [
|
||||
trigger
|
||||
for pattern, trigger in zip(self._compiled_patterns, self._triggers)
|
||||
if pattern.search(text)
|
||||
]
|
||||
|
||||
if not found_triggers:
|
||||
return
|
||||
|
||||
try:
|
||||
chat = await message.get_chat()
|
||||
chat_title = getattr(
|
||||
chat,
|
||||
"title",
|
||||
"Личные сообщения"
|
||||
if getattr(message, "is_private", False)
|
||||
else f"Чат с ID {chat_id}",
|
||||
)
|
||||
|
||||
sender = await message.get_sender()
|
||||
if sender:
|
||||
sender_name = sender.first_name
|
||||
if getattr(sender, "last_name", None):
|
||||
sender_name += f" {sender.last_name}"
|
||||
if not sender_name:
|
||||
sender_name = getattr(
|
||||
sender, "username", "Неизвестный пользователь"
|
||||
)
|
||||
else:
|
||||
sender_name = "Неизвестный пользователь"
|
||||
|
||||
link = await self._get_message_link(message, sender)
|
||||
|
||||
await self.client.send_message(
|
||||
self._target_chat,
|
||||
self.strings["monitoring_msg"].format(
|
||||
chat_title,
|
||||
chat_id,
|
||||
sender_name,
|
||||
link,
|
||||
text,
|
||||
),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
logger.debug(
|
||||
f"Sent notification about trigger word(s) {found_triggers} to chat {self._target_chat}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing message: {e}")
|
||||
|
||||
async def _get_message_link(self, message: Message, sender) -> str:
|
||||
"""Generate message link based on message type"""
|
||||
message_id = message.id
|
||||
|
||||
if getattr(message, "to_id", None):
|
||||
to_id_obj = getattr(message, "to_id")
|
||||
if getattr(to_id_obj, "channel_id", None):
|
||||
return f"https://t.me/c/{to_id_obj.channel_id}/{message_id}"
|
||||
|
||||
if (
|
||||
getattr(message, "is_private", False)
|
||||
and sender
|
||||
and getattr(sender, "username", None)
|
||||
):
|
||||
return f"https://t.me/{sender.username}/{message_id}"
|
||||
|
||||
return f"https://t.me/c/{message_id}"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,126 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Music
|
||||
# Description: Searches for music using Telegram music bots
|
||||
# Author: @hikka_mods
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Music
|
||||
# scope: Music 0.0.2
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
# Thanks to @murpizz for the search code yandex
|
||||
|
||||
import logging
|
||||
|
||||
from telethon.errors.rpcerrorlist import (
|
||||
BotMethodInvalidError,
|
||||
FloodWaitError,
|
||||
MessageNotModifiedError,
|
||||
)
|
||||
from telethon.tl.types import Message
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class MusicMod(loader.Module):
|
||||
strings = {
|
||||
"name": "Music",
|
||||
"no_query": "<emoji document_id=5337117114392127164>🤷♂</emoji> <b>Provide a search query!</b>",
|
||||
"searching": "<emoji document_id=4918235297679934237>⌨️</emoji> <b>Searching...</b>",
|
||||
"found": "<emoji document_id=5336965905773504919>🗣</emoji> <b>Possible match:</b>",
|
||||
"not_found": "<emoji document_id=5228947933545635555>😫</emoji> <b>Track not found: <code>{}</code></b>",
|
||||
"usage": "<b>Usage:</b> <code>.music [track name]</code>",
|
||||
"error": "<emoji document_id=5228947933545635555>⚠️</emoji> <b>Error:</b> <code>{}</code>",
|
||||
"no_results": "<emoji document_id=5228947933545635555>😫</emoji> <b>No results: <code>{}</code></b>",
|
||||
"flood_wait": "<emoji document_id=5462295343642956603>⏳</emoji> <b>Wait {}s (Telegram limits)</b>",
|
||||
"bot_error": "<emoji document_id=5228947933545635555>🤖</emoji> <b>Bot error: <code>{}</code></b>",
|
||||
"no_audio": "<emoji document_id=5228947933545635555>🎵</emoji> <b>No audio</b>",
|
||||
"generic_result": "<emoji document_id=5336965905773504919>ℹ️</emoji> <b>Non-media result. Check the bot's chat</b>",
|
||||
"yafind_searching": "<emoji document_id=5258396243666681152>🔎</emoji> <b>Searching Yandex.Music...</b>",
|
||||
"yafind_not_found": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Track not found on Yandex.Music</b>",
|
||||
"yafind_error": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Error (Yandex): {}</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"name": "Music",
|
||||
"no_query": "<emoji document_id=5337117114392127164>🤷♂</emoji> <b>Укажите запрос!</b>",
|
||||
"searching": "<emoji document_id=4918235297679934237>⌨️</emoji> <b>Поиск...</b>",
|
||||
"found": "<emoji document_id=5336965905773504919>🗣</emoji> <b>Возможно, это оно:</b>",
|
||||
"not_found": "<emoji document_id=5228947933545635555>😫</emoji> <b>Трек не найден: <code>{}</code></b>",
|
||||
"usage": "<b>Использование:</b> <code>.music [название трека]</code>",
|
||||
"error": "<emoji document_id=5228947933545635555>⚠️</emoji> <b>Ошибка:</b> <code>{}</code>",
|
||||
"no_results": "<emoji document_id=5228947933545635555>😫</emoji> <b>Нет результатов: <code>{}</code></b>",
|
||||
"flood_wait": "<emoji document_id=5462295343642956603>⏳</emoji> <b>Подождите {}с (лимиты Telegram)</b>",
|
||||
"bot_error": "<emoji document_id=5228947933545635555>🤖</emoji> <b>Ошибка бота: <code>{}</code></b>",
|
||||
"no_audio": "<emoji document_id=5228947933545635555>🎵</emoji> <b>Нет аудио</b>",
|
||||
"generic_result": "<emoji document_id=5336965905773504919>ℹ️</emoji> <b>Немедийный результат. Проверьте чат с ботом</b>",
|
||||
"yafind_searching": "<emoji document_id=5258396243666681152>🔎</emoji> <b>Поиск в Яндекс.Музыке...</b>",
|
||||
"yafind_not_found": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Трек не найден в Яндекс.Музыке</b>",
|
||||
"yafind_error": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Ошибка (Яндекс): {}</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.murglar_bot = "@murglar_bot"
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Найти трек в Yandex.Music: `.music {название}`",
|
||||
en_doc="Find a track in Yandex.Music: `.music yandex {name}`",
|
||||
)
|
||||
async def music(self, message):
|
||||
args = utils.get_args(message)
|
||||
|
||||
if not args:
|
||||
if reply := await message.get_reply_message():
|
||||
await self._yafind(message, reply.raw_text.strip())
|
||||
else:
|
||||
await utils.answer(message, self.strings("usage", message))
|
||||
return
|
||||
|
||||
await self._yafind(message, query=args)
|
||||
|
||||
async def _yafind(self, message: Message, query: str):
|
||||
if not query:
|
||||
return await utils.answer(message, self.strings("no_query", message))
|
||||
|
||||
await utils.answer(message, self.strings("yafind_searching", message))
|
||||
|
||||
try:
|
||||
results = await message.client.inline_query(
|
||||
self.murglar_bot, f"s:ynd {query}"
|
||||
)
|
||||
|
||||
if not results:
|
||||
return await utils.answer(
|
||||
message, self.strings("yafind_not_found", message)
|
||||
)
|
||||
|
||||
await results[0].click(
|
||||
entity=message.chat_id,
|
||||
hide_via=True,
|
||||
reply_to=message.reply_to_msg_id if message.reply_to_msg_id else None,
|
||||
)
|
||||
await message.delete()
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Yandex search error:")
|
||||
await utils.answer(message, self.strings("yafind_error", message).format(e))
|
||||
@@ -1,184 +0,0 @@
|
||||
<div align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/Codwizer/ReModules/blob/main/assets/Vector.png">
|
||||
<img src="https://github.com/Codwizer/ReModules/blob/main/assets/Vector.png" alt="H:Mods Logo" width="400">
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
<h1 align="center">
|
||||
<a href="https://github.com/archquise/H.Modules">
|
||||
<img src="https://readme-typing-svg.herokuapp.com/?lines=H:Mods;Hikka+Modules+Collection¢er=true&vCenter=true&width=500&height=80&color=00D9FF¢er=true&vCenter=true">
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/hikariatama/Hikka">
|
||||
<img src="https://img.shields.io/badge/Hikka-Userbot-blue?style=for-the-badge&logo=python" alt="Hikka">
|
||||
</a>
|
||||
<a href="https://github.com/coddrago/Heroku">
|
||||
<img src="https://img.shields.io/badge/Heroku-Userbot-purple?style=for-the-badge&logo=heroku" alt="Heroku">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://t.me/hikka_mods">
|
||||
<img src="https://img.shields.io/badge/Telegram%20Channel-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram Channel">
|
||||
</a>
|
||||
<a href="https://github.com/archquise/H.Modules">
|
||||
<img src="https://img.shields.io/badge/GitHub-Repository-181717?style=for-the-badge&logo=github" alt="GitHub">
|
||||
</a>
|
||||
<a href="https://github.com/archquise/H.Modules/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/archquise/H.Modules?style=for-the-badge&logo=github&color=yellow" alt="Stars">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 📦 Repository Installation (Recommended)
|
||||
|
||||
The easiest way to install and manage all modules:
|
||||
|
||||
```bash
|
||||
.addrepo https://github.com/archquise/H.Modules/raw/main
|
||||
```
|
||||
|
||||
After adding the repository, install any module:
|
||||
|
||||
```bash
|
||||
.dlm <module_name>
|
||||
```
|
||||
|
||||
### 🎯 Direct Installation
|
||||
|
||||
Install a specific module directly:
|
||||
|
||||
```bash
|
||||
.dlm https://raw.githubusercontent.com/archquise/H.Modules/main/<module_name>.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Installation Guide
|
||||
|
||||
### Step 1: Add Repository
|
||||
|
||||
```bash
|
||||
.addrepo https://github.com/archquise/H.Modules/raw/main
|
||||
```
|
||||
|
||||
### Step 2: Install Modules
|
||||
|
||||
```bash
|
||||
# Install specific module
|
||||
.dlm TelegraphComic
|
||||
|
||||
|
||||
# Install from direct URL
|
||||
.dlm https://raw.githubusercontent.com/archquise/H.Modules/main/TelegraphComic.py
|
||||
```
|
||||
|
||||
### Step 4: Configure Modules
|
||||
|
||||
Most modules have configurable settings:
|
||||
|
||||
```bash
|
||||
# View module configuration
|
||||
.config TelegraphComic
|
||||
|
||||
# Update configuration
|
||||
.config TelegraphComic upload_service catbox
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
<details>
|
||||
<summary>❌ Module Installation Failed</summary>
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Check repository URL is correct
|
||||
2. Ensure you have internet connection
|
||||
3. Try restarting Hikka/Heroku
|
||||
4. Use direct installation method
|
||||
|
||||
```bash
|
||||
.dlm https://raw.githubusercontent.com/archquise/H.Modules/main/<module_name>.py
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>⚠️ Module Not Working</summary>
|
||||
|
||||
**Solution:**
|
||||
2. Check configuration: `.cfg <module_name>`
|
||||
3. Check dependencies are installed
|
||||
4. Update the module: `.dlm <module_name>`
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🔧 Configuration Issues</summary>
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Reset to defaults: `.cfg <module_name> reset`
|
||||
2. Check syntax: `.cfg <module_name>`
|
||||
3. View help: `.help <module_name>`
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
<div align="center">
|
||||
|
||||
**🔒 Proprietary License**
|
||||
|
||||
This project is licensed under a proprietary license. By using this software, you agree to the following terms:
|
||||
|
||||
</div>
|
||||
|
||||
### 📋 License Terms
|
||||
|
||||
> **✅ What You CAN Do:**
|
||||
|
||||
> - Use the software for personal and non-commercial purposes
|
||||
> - Install and use modules in Hikka/Heroku
|
||||
> - Modify configuration settings
|
||||
> - Report issues and suggest improvements
|
||||
|
||||
> **❌ What You CANNOT Do:**
|
||||
|
||||
> - Modify, alter, or change the software without explicit permission
|
||||
> - Redistribute the software in original or modified form
|
||||
> - Use for commercial purposes without permission
|
||||
> - Remove copyright notices or attribution
|
||||
|
||||
### 📧 Contact
|
||||
|
||||
For inquiries or permission requests:
|
||||
|
||||
- **Email:** `archquise@gmail.com`
|
||||
- **Telegram:** [@hikka_mods](https://t.me/hikka_mods)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<picture>
|
||||
<img src="https://raw.githubusercontent.com/trinib/trinib/82213791fa9ff58d3ca768ddd6de2489ec23ffca/images/footer.svg" alt="Footer" width="100%">
|
||||
</picture>
|
||||
|
||||
<p>
|
||||
<sub>Built with ❤️ for the Hikka/Heroku community</sub>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="#top">⬆️ Back to Top</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,81 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: VowelReplacer
|
||||
# Description: Replaces vowel letters with ё
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: VowelReplacer
|
||||
# scope: VowelReplacer 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from telethon.tl.types import Message
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class VowelReplacer(loader.Module):
|
||||
"""Replaces vowel letters with ё"""
|
||||
|
||||
strings = {
|
||||
"name": "Vowel Replacer",
|
||||
"on": "✅ Vowel substitution for ё has been successfully enabled.",
|
||||
"off": "🚫 Vowel substitution for ё is disabled.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"on": "✅ Замена гласных на ё успешно включена.",
|
||||
"off": "🚫 Замена гласных на ё отключена.",
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.db = db
|
||||
self._client = client
|
||||
self.enabled = self.db.get("vowel_replacer", "enabled", False)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Включить или отключить замену гласных на ё.",
|
||||
en_doc="Enable or disable vowel substitution for ё.",
|
||||
)
|
||||
async def vowelreplace(self, message):
|
||||
self.enabled = not self.enabled
|
||||
self.db.set("vowel_replacer", "enabled", self.enabled)
|
||||
|
||||
if self.enabled:
|
||||
response = self.strings("on")
|
||||
else:
|
||||
response = self.strings("off")
|
||||
|
||||
await utils.answer(message, response)
|
||||
|
||||
async def watcher(self, message: Message):
|
||||
"""Автоматическая замена гласных на ё при получении собственного сообщения."""
|
||||
if self.enabled and message.out:
|
||||
vowels = "аеёиоуыэюяАЕЁИОУЫЭЮЯ"
|
||||
message_text = message.text
|
||||
replaced_text = "".join(
|
||||
"ё" if char in vowels else char for char in message_text
|
||||
)
|
||||
|
||||
await message.edit(replaced_text)
|
||||
@@ -1,153 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: SMArchiver
|
||||
# Description: unloads all messages from Favorites
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: SMArchiver
|
||||
# scope: SMArchiver 0.0.1
|
||||
# requires: zipfile
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class SMArchiver(loader.Module):
|
||||
"""unloads all messages from Favorites"""
|
||||
|
||||
strings = {
|
||||
"name": "SMArchiver",
|
||||
"archive_created": "🎉 Archive with messages has been successfully created: {filename}",
|
||||
"no_messages": "⚠️ There are no messages in Saved Messages.",
|
||||
"error": "❌ An error occurred: {error}",
|
||||
"processing": "🛠️ Processing messages... Please wait.\n\nP.S: Be careful, if you have a lot of messages, you may get flooding, and if you have a lot of heavy files, the download will be slower than usual.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"archive_created": "🎉 Архив с сообщениями успешно создан: {filename}",
|
||||
"no_messages": "⚠️ В Сохраненных сообщениях нет сообщений.",
|
||||
"error": "❌ Произошла ошибка: {error}",
|
||||
"processing": "🛠️ Обработка сообщений... Пожалуйста, подождите.\n\nP.S: Будьте осторожны, если у вас много сообщений, вы можете получить флуд, а если у вас много тяжелых файлов, загрузка будет медленнее обычного.",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="выгружает все сообщения из Избранного / Saved Messages и собирает их в одном архиве.",
|
||||
en_doc="downloads all messages from Favorites / Saved Messages and collects them in one archive.",
|
||||
)
|
||||
async def smdump(self, message):
|
||||
await utils.answer(message, self.strings["processing"])
|
||||
saved_messages = await message.client.get_messages("me", limit=None)
|
||||
|
||||
if not saved_messages:
|
||||
await utils.answer(message, self.strings["no_messages"])
|
||||
return
|
||||
|
||||
archive_path = await self.create_archive(saved_messages)
|
||||
|
||||
try:
|
||||
await message.client.send_file(
|
||||
message.chat_id,
|
||||
archive_path,
|
||||
caption=self.strings["archive_created"].format(
|
||||
filename=os.path.basename(archive_path)
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(error=str(e)))
|
||||
finally:
|
||||
self.cleanup(archive_path)
|
||||
|
||||
async def create_archive(self, saved_messages):
|
||||
current_month = datetime.now().strftime("%B %Y")
|
||||
archive_path = "saved_messages.zip"
|
||||
|
||||
with zipfile.ZipFile(archive_path, "w") as archive:
|
||||
self.initialize_archive_structure(archive, current_month)
|
||||
for msg in saved_messages:
|
||||
await self.add_message_to_archive(msg, archive, current_month)
|
||||
|
||||
return archive_path
|
||||
|
||||
def initialize_archive_structure(self, archive, current_month):
|
||||
month_folder = f"{current_month}/"
|
||||
archive.writestr(month_folder, "")
|
||||
message_folders = {
|
||||
"Text Messages": f"{month_folder}Text Messages/",
|
||||
"Voice Messages": f"{month_folder}Voice Messages/",
|
||||
"Video Messages": f"{month_folder}Video Messages/",
|
||||
"Videos": f"{month_folder}Videos/",
|
||||
"Audios": f"{month_folder}Audios/",
|
||||
"GIFs": f"{month_folder}GIFs/",
|
||||
"Files": f"{month_folder}Files/",
|
||||
}
|
||||
|
||||
for folder in message_folders.values():
|
||||
archive.writestr(folder, "")
|
||||
|
||||
async def add_message_to_archive(self, msg, archive, current_month):
|
||||
"""Обрабатывает отдельное сообщение и добавляет его в архив."""
|
||||
if msg.message:
|
||||
await self.add_text_message_to_archive(msg, archive, current_month)
|
||||
|
||||
if msg.media:
|
||||
await self.add_media_to_archive(msg, archive, current_month)
|
||||
|
||||
async def add_text_message_to_archive(self, msg, archive, current_month):
|
||||
timestamp = datetime.fromtimestamp(msg.date.timestamp()).strftime(
|
||||
"%Y%m%d_%H%M%S"
|
||||
)
|
||||
safe_name = f"message_{timestamp}.txt"
|
||||
archive.writestr(
|
||||
os.path.join(f"{current_month}/Text Messages/", safe_name), msg.message
|
||||
)
|
||||
|
||||
async def add_media_to_archive(self, msg, archive, current_month):
|
||||
media_file = await msg.client.download_media(msg.media)
|
||||
if media_file:
|
||||
mime_type = (
|
||||
msg.media.document.mime_type if hasattr(msg.media, "document") else None
|
||||
)
|
||||
folder = self.get_media_folder(mime_type, current_month)
|
||||
archive.write(
|
||||
media_file, os.path.join(folder, os.path.basename(media_file))
|
||||
)
|
||||
|
||||
def get_media_folder(self, mime_type, current_month):
|
||||
if mime_type:
|
||||
if mime_type.startswith("audio/"):
|
||||
return f"{current_month}/Audios/"
|
||||
elif mime_type.startswith("video/"):
|
||||
return f"{current_month}/Videos/"
|
||||
elif mime_type.startswith("image/gif"):
|
||||
return f"{current_month}/GIFs/"
|
||||
return f"{current_month}/Files/"
|
||||
|
||||
def cleanup(self, archive_path):
|
||||
if os.path.exists(archive_path):
|
||||
os.remove(archive_path)
|
||||
@@ -1,386 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TaskManager
|
||||
# Description: Manages tasks with Telegram commands and inline keyboards.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: TaskManager
|
||||
# scope: TaskManager 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
"""Represents a task."""
|
||||
|
||||
description: str
|
||||
due_date: Optional[datetime.datetime] = None
|
||||
completed: bool = False
|
||||
created_at: datetime.datetime = field(default_factory=datetime.datetime.now)
|
||||
id: str = field(default_factory=lambda: f"{datetime.datetime.now().timestamp()}")
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert task to dictionary for JSON serialization."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"description": self.description,
|
||||
"due_date": self.due_date.isoformat() if self.due_date else None,
|
||||
"completed": self.completed,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "Task":
|
||||
"""Create task from dictionary."""
|
||||
return cls(
|
||||
id=data.get("id", f"{datetime.datetime.now().timestamp()}"),
|
||||
description=data["description"],
|
||||
due_date=datetime.datetime.fromisoformat(data["due_date"])
|
||||
if data.get("due_date")
|
||||
else None,
|
||||
completed=data["completed"],
|
||||
created_at=datetime.datetime.fromisoformat(data["created_at"]),
|
||||
)
|
||||
|
||||
|
||||
class TaskManager:
|
||||
"""Manages tasks, storing them in a JSON file."""
|
||||
|
||||
def __init__(self, data_file: str):
|
||||
self.data_file = Path(data_file)
|
||||
self.tasks: Dict[int, List[Task]] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self.load_data()
|
||||
|
||||
def load_data(self):
|
||||
"""Loads task data from the JSON file."""
|
||||
if not self.data_file.exists():
|
||||
self.tasks = {}
|
||||
logger.info("Task data file not found. Starting empty.")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.data_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
self.tasks = {
|
||||
int(user_id): [Task.from_dict(task) for task in task_list]
|
||||
for user_id, task_list in data.items()
|
||||
}
|
||||
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
||||
logger.warning(f"Failed to load task data: {e}. Starting empty.")
|
||||
self.tasks = {}
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error loading task data: {e}")
|
||||
self.tasks = {}
|
||||
|
||||
async def save_data(self):
|
||||
"""Saves task data to the JSON file."""
|
||||
async with self._lock:
|
||||
try:
|
||||
self.data_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = {
|
||||
str(user_id): [task.to_dict() for task in task_list]
|
||||
for user_id, task_list in self.tasks.items()
|
||||
}
|
||||
with open(self.data_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
except IOError as e:
|
||||
logger.error(f"Failed to save task data: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error saving task data: {e}")
|
||||
|
||||
async def add_task(self, user_id: int, task: Task):
|
||||
self.tasks.setdefault(user_id, []).append(task)
|
||||
await self.save_data()
|
||||
|
||||
async def remove_task(self, user_id: int, index: int) -> bool:
|
||||
if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
|
||||
del self.tasks[user_id][index]
|
||||
await self.save_data()
|
||||
return True
|
||||
logger.warning(f"Invalid index for removal: {index}, user: {user_id}")
|
||||
return False
|
||||
|
||||
async def complete_task(self, user_id: int, index: int) -> bool:
|
||||
if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
|
||||
self.tasks[user_id][index].completed = True
|
||||
await self.save_data()
|
||||
return True
|
||||
logger.warning(f"Invalid index for completion: {index}, user: {user_id}")
|
||||
return False
|
||||
|
||||
def get_tasks(self, user_id: int, include_completed: bool = True) -> List[Task]:
|
||||
tasks = self.tasks.get(user_id, [])
|
||||
if not include_completed:
|
||||
tasks = [task for task in tasks if not task.completed]
|
||||
return tasks
|
||||
|
||||
async def clear_tasks(self, user_id: int) -> bool:
|
||||
if user_id in self.tasks:
|
||||
self.tasks[user_id] = []
|
||||
await self.save_data()
|
||||
return True
|
||||
logger.info(f"No tasks to clear for user: {user_id}")
|
||||
return False
|
||||
|
||||
def get_task(self, user_id: int, index: int) -> Optional[Task]:
|
||||
if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
|
||||
return self.tasks[user_id][index]
|
||||
return None
|
||||
|
||||
def get_overdue_tasks(self, user_id: int) -> List[Task]:
|
||||
"""Get overdue tasks for a user."""
|
||||
now = datetime.datetime.now()
|
||||
return [
|
||||
task
|
||||
for task in self.get_tasks(user_id)
|
||||
if task.due_date and task.due_date < now and not task.completed
|
||||
]
|
||||
|
||||
async def update_task(self, user_id: int, index: int, **kwargs) -> bool:
|
||||
"""Update task properties."""
|
||||
task = self.get_task(user_id, index)
|
||||
if not task:
|
||||
return False
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(task, key):
|
||||
setattr(task, key, value)
|
||||
|
||||
await self.save_data()
|
||||
return True
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TaskManagerModule(loader.Module):
|
||||
"""Manages tasks with Telegram commands and inline keyboards."""
|
||||
|
||||
strings = {
|
||||
"name": "TaskManager",
|
||||
"task_added": "<emoji document_id=5776375003280838798>✅</emoji> Task added.",
|
||||
"task_removed": "<emoji document_id=5776375003280838798>✅</emoji> Task removed.",
|
||||
"task_completed": "<emoji document_id=5776375003280838798>✅</emoji> Task completed.",
|
||||
"task_not_found": "<emoji document_id=5778527486270770928>❌</emoji> Task not found.",
|
||||
"no_tasks": "<emoji document_id=5956561916573782596>📄</emoji> No active tasks.",
|
||||
"task_list": "<emoji document_id=5956561916573782596>📄</emoji> Your tasks:\n{}",
|
||||
"invalid_index": "<emoji document_id=5778527486270770928>❌</emoji> Invalid index. Provide valid integer.",
|
||||
"description_required": "<emoji document_id=5879813604068298387>❗️</emoji> Provide task description.",
|
||||
"clear_confirmation": "⚠️ Delete all tasks?",
|
||||
"tasks_cleared": "✅ All tasks deleted.",
|
||||
"due_date_format": "<emoji document_id=5778527486270770928>❌</emoji> Invalid date. Use YYYY-MM-DD HH:MM.",
|
||||
"task_info": "<emoji document_id=6028435952299413210>ℹ</emoji> Task: {description}\n<emoji document_id=5967412305338568701>📅</emoji> Due: {due_date}\n<emoji document_id=5825794181183836432>✔️</emoji> Completed: {completed}\n<emoji document_id=5936170807716745162>🎛</emoji> Created: {created_at}",
|
||||
"confirm_clear": "Confirm",
|
||||
"cancel_clear": "Cancel",
|
||||
"clear_cancelled": "❌ Deletion cancelled.",
|
||||
"index_required": "⚠️ Provide task index.",
|
||||
"clear_confirmation_text": "Are you sure you want to clear all tasks?",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"task_added": "<emoji document_id=5776375003280838798>✅</emoji> Задача добавлена.",
|
||||
"task_removed": "<emoji document_id=5776375003280838798>✅</emoji> Задача удалена.",
|
||||
"task_completed": "<emoji document_id=5776375003280838798>✅</emoji> Задача выполнена.",
|
||||
"task_not_found": "<emoji document_id=5778527486270770928>❌</emoji> Задача не найдена.",
|
||||
"no_tasks": "<emoji document_id=5956561916573782596>📄</emoji> Нет активных задач.",
|
||||
"task_list": "<emoji document_id=5956561916573782596>📄</emoji> Ваши задачи:\n{}",
|
||||
"invalid_index": "<emoji document_id=5778527486270770928>❌</emoji> Неверный индекс. Укажите целое число.",
|
||||
"description_required": "<emoji document_id=5879813604068298387>❗️</emoji> Укажите описание задачи.",
|
||||
"clear_confirmation": "⚠️ Удалить все задачи?",
|
||||
"tasks_cleared": "✅ Все задачи удалены.",
|
||||
"due_date_format": "<emoji document_id=5778527486270770928>❌</emoji> Неверный формат даты. Используйте ГГГГ-ММ-ДД ЧЧ:ММ.",
|
||||
"task_info": "<emoji document_id=6028435952299413210>ℹ</emoji> Задача: {description}\n<emoji document_id=5967412305338568701>📅</emoji> Срок: {due_date}\n<emoji document_id=5825794181183836432>✔️</emoji> Выполнена: {completed}\n<emoji document_id=5936170807716745162>🎛</emoji> Создана: {created_at}",
|
||||
"confirm_clear": "Подтвердить",
|
||||
"cancel_clear": "Отменить",
|
||||
"clear_cancelled": "❌ Удаление отменено.",
|
||||
"index_required": "⚠️ Укажите индекс задачи.",
|
||||
"clear_confirmation_text": "Вы уверены, что хотите удалить все задачи?",
|
||||
"confirm": "Подтвердить",
|
||||
"cancel": "Отменить",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.task_manager: Optional[TaskManager] = None
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
data_dir = Path.cwd() / "data"
|
||||
data_dir.mkdir(exist_ok=True)
|
||||
self.task_manager = TaskManager(str(data_dir / "tasks.json"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Добавить задачу:\n.taskadd <описание> | <дата (необязательно)>",
|
||||
en_doc="Add task:\n.taskadd <description> | <date (opt)>",
|
||||
)
|
||||
async def taskadd(self, message):
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("description_required"))
|
||||
return
|
||||
|
||||
try:
|
||||
description, due_date_str = (
|
||||
args.split("|", 1) if "|" in args else (args, None)
|
||||
)
|
||||
description = description.strip()
|
||||
due_date_str = due_date_str.strip() if due_date_str else None
|
||||
due_date = (
|
||||
datetime.datetime.fromisoformat(due_date_str) if due_date_str else None
|
||||
)
|
||||
except ValueError:
|
||||
await utils.answer(message, self.strings("due_date_format"))
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding task: {e}")
|
||||
await utils.answer(
|
||||
message, f"<emoji document_id=5778527486270770928>❌</emoji> Error: {e}"
|
||||
)
|
||||
return
|
||||
|
||||
task = Task(description=description, due_date=due_date)
|
||||
await self.task_manager.add_task(message.sender_id, task)
|
||||
await utils.answer(message, self.strings("task_added"))
|
||||
|
||||
@loader.command(ru_doc="[index] - удалить задачу", en_doc="[index] - remove task")
|
||||
async def taskremove(self, message):
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("index_required"))
|
||||
return
|
||||
|
||||
try:
|
||||
index = int(args) - 1
|
||||
except ValueError:
|
||||
await utils.answer(message, self.strings("invalid_index"))
|
||||
return
|
||||
|
||||
if await self.task_manager.remove_task(message.sender_id, index):
|
||||
await utils.answer(message, self.strings("task_removed"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("task_not_found"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[index] - Завершите задачу", en_doc="[index] - Complete task"
|
||||
)
|
||||
async def taskcomplete(self, message):
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("index_required"))
|
||||
return
|
||||
|
||||
try:
|
||||
index = int(args) - 1
|
||||
except ValueError:
|
||||
await utils.answer(message, self.strings("invalid_index"))
|
||||
return
|
||||
|
||||
if await self.task_manager.complete_task(message.sender_id, index):
|
||||
await utils.answer(message, self.strings("task_completed"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("task_not_found"))
|
||||
|
||||
@loader.command(ru_doc="Список задач", en_doc="List tasks")
|
||||
async def tasklist(self, message):
|
||||
tasks = self.task_manager.get_tasks(message.sender_id)
|
||||
|
||||
if not tasks:
|
||||
await utils.answer(message, self.strings("no_tasks"))
|
||||
return
|
||||
|
||||
task_list_str = "\n".join(
|
||||
[
|
||||
f" {i + 1}. {'<emoji document_id=5776375003280838798>✅</emoji>' if task.completed else '<emoji document_id=5778527486270770928>❌</emoji>'} {task.description} (Due: {task.due_date.strftime('%Y-%m-%d %H:%M') if task.due_date else 'None'})"
|
||||
for i, task in enumerate(tasks)
|
||||
]
|
||||
)
|
||||
await utils.answer(message, self.strings("task_list").format(task_list_str))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[index] - Посмотреть информацию о задаче",
|
||||
en_doc="[index] - Show task info",
|
||||
)
|
||||
async def taskinfo(self, message):
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("index_required"))
|
||||
return
|
||||
try:
|
||||
index = int(args) - 1
|
||||
except ValueError:
|
||||
await utils.answer(message, self.strings("invalid_index"))
|
||||
return
|
||||
|
||||
task = self.task_manager.get_task(message.sender_id, index)
|
||||
if not task:
|
||||
await utils.answer(message, self.strings("task_not_found"))
|
||||
return
|
||||
|
||||
due_date_str = (
|
||||
task.due_date.strftime("%Y-%m-%d %H:%M") if task.due_date else "None"
|
||||
)
|
||||
created_at_str = task.created_at.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("task_info").format(
|
||||
description=task.description,
|
||||
due_date=due_date_str,
|
||||
completed="Yes" if task.completed else "No",
|
||||
created_at=created_at_str,
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="Удалить все задачи", en_doc="Clear all tasks")
|
||||
async def taskclear(self, message):
|
||||
await self.inline.form(
|
||||
text=self.strings("clear_confirmation_text"),
|
||||
message=message,
|
||||
reply_markup=[
|
||||
[
|
||||
{"text": self.strings("confirm"), "callback": self.clear_confirm},
|
||||
{"text": self.strings("cancel"), "callback": self.clear_cancel},
|
||||
]
|
||||
],
|
||||
silent=True,
|
||||
)
|
||||
|
||||
async def clear_confirm(self, call):
|
||||
"""Callback for confirming task clearing."""
|
||||
if await self.task_manager.clear_tasks(call.from_user.id):
|
||||
await call.edit(self.strings("tasks_cleared"))
|
||||
else:
|
||||
await call.edit(self.strings("no_tasks"))
|
||||
|
||||
async def clear_cancel(self, call):
|
||||
"""Callback for canceling task clearing."""
|
||||
await call.edit(self.strings("clear_cancelled"))
|
||||
@@ -1,200 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TelegramStatusCodes
|
||||
# Description: Dictionary of telegram status codes
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api TelegramStatusCodes
|
||||
# scope: Api TelegramStatusCodes 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
responses = {
|
||||
300: (
|
||||
"⛔ SEE_OTHER",
|
||||
"The request must be repeated, but directed to a different data center.",
|
||||
),
|
||||
400: (
|
||||
"⛔ BAD_REQUEST",
|
||||
"The query contains errors. In the event that a request was created using a form and contains user generated data, the user should be notified that the data must be corrected before the query is repeated.",
|
||||
),
|
||||
401: (
|
||||
"⛔ UNAUTHORIZED",
|
||||
"There was an unauthorized attempt to use functionality available only to authorized users.",
|
||||
),
|
||||
403: (
|
||||
"⛔ FORBIDDEN",
|
||||
"Privacy violation. For example, an attempt to write a message to someone who has blacklisted the current user.",
|
||||
),
|
||||
404: (
|
||||
"⛔ NOT_FOUND",
|
||||
"An attempt to invoke a non-existent object, such as a method",
|
||||
),
|
||||
406: (
|
||||
"⛔ NOT_ACCEPTABLE",
|
||||
"""
|
||||
Similar to <b>400 BAD_REQUESTS</b>, but the app must display the error to the user a bit differently.
|
||||
Do not display any visible error to the user when receiving the <b>rpc_error</b> constructor: instead, wait for an <a href="https://core.telegram.org/constructor/updateServiceNotification ">updateServiceNotification</a> update, and handle it as usual.
|
||||
Basically, an <a href="https://core.telegram.org/constructor/updateServiceNotification"updateServiceNotification</a> <b>pop-up</b> update will be emitted independently (ie NOT as an <a href="https://core.telegram.org/type/Updates">Updates</a> constructor inside <b>rpc_result</b> but as a normal update) immediately after emission of a 406 <b>rpc_error</b>: the update will contain the actual localized error message to show to the user with a UI popup.
|
||||
|
||||
An exception to this is the <b>AUTH_KEY_DUPLICATED</b> error, which is only emitted if any of the non-media DC detects that an authorized session is sending requests in parallel from two separate TCP connections, from the same or different IP addresses.
|
||||
Note that parallel connections are still allowed and actually recommended for media DCs.
|
||||
Also note that by session we mean a logged-in session identified by an <a href="https://core.telegram.org/constructor/authorization">authorization</a> constructor, fetchable using <a href="https://core.telegram.org/method/account.getAuthorizations">account.getAuthorizations</a>, not an MTProto session.
|
||||
|
||||
If the client receives an <b>AUTH_KEY_DUPLICATED</b> error, the session was already invalidated by the server and the user must generate a new auth key and login again.""",
|
||||
),
|
||||
420: (
|
||||
"⛔ FLOOD",
|
||||
"The maximum allowed number of attempts to invoke the given method with the given input parameters has been exceeded. For example, in an attempt to request a large number of text messages (SMS) for the same phone number.",
|
||||
),
|
||||
500: (
|
||||
"⛔ INTERNAL",
|
||||
"""An internal server error occurred while a request was being processed; for example, there was a disruption while accessing a database or file storage.
|
||||
|
||||
If a client receives a 500 error, or you believe this error should not have occurred, please collect as much information as possible about the query and error and send it to the developers""",
|
||||
),
|
||||
}
|
||||
|
||||
responses_ru = {
|
||||
300: (
|
||||
"⛔ SEE_OTHER",
|
||||
"Запрос должен быть повторен, но направлен в другой дата-центр.",
|
||||
),
|
||||
400: (
|
||||
"⛔ BAD_REQUEST",
|
||||
"Запрос содержит ошибки. В случае, если запрос был создан с помощью формы и содержит данные, введенные пользователем, пользователю следует сообщить, что данные должны быть исправлены перед повторным выполнением запроса.",
|
||||
),
|
||||
401: (
|
||||
"⛔ UNAUTHORIZED",
|
||||
"Была совершена неавторизованная попытка использовать функциональность, доступную только авторизованным пользователям.",
|
||||
),
|
||||
403: (
|
||||
"⛔ FORBIDDEN",
|
||||
"Нарушение конфиденциальности. Например, попытка написать сообщение пользователю, который добавил текущего пользователя в черный список.",
|
||||
),
|
||||
404: (
|
||||
"⛔ NOT_FOUND",
|
||||
"Попытка обращения к несуществующему объекту, например, к методу.",
|
||||
),
|
||||
406: (
|
||||
"⛔ NOT_ACCEPTABLE",
|
||||
"""
|
||||
Аналогично <b>400 BAD_REQUESTS</b>, но приложение должно отображать ошибку пользователю немного иначе.
|
||||
Не показывайте пользователю видимую ошибку при получении конструктора <b>rpc_error</b>: вместо этого дождитесь обновления <a href="https://core.telegram.org/constructor/updateServiceNotification">updateServiceNotification</a> и обработайте его как обычно.
|
||||
По сути, обновление-всплывающее окно <b>updateServiceNotification</b> будет отправлено независимо (т.е. НЕ как конструктор <b>Updates</b> внутри <b>rpc_result</b>, а как обычное обновление) сразу после выдачи 406 <b>rpc_error</b>: обновление будет содержать актуальное локализованное сообщение об ошибке для показа пользователю в интерфейсе.
|
||||
|
||||
Исключением является ошибка <b>AUTH_KEY_DUPLICATED</b>, которая возникает только в том случае, если любой из не-медиа DC обнаруживает, что авторизованная сессия отправляет запросы параллельно из двух отдельных TCP-соединений с одного или разных IP-адресов.
|
||||
Обратите внимание, что параллельные соединения по-прежнему разрешены и фактически рекомендуются для медиа-DC.
|
||||
Также обратите внимание, что под сессией понимается авторизованная сессия, идентифицируемая конструктором <a href="https://core.telegram.org/constructor/authorization">authorization</a>, которую можно получить с помощью <a href="https://core.telegram.org/method/account.getAuthorizations">account.getAuthorizations</a>, а не сессия MTProto.
|
||||
|
||||
Если клиент получает ошибку <b>AUTH_KEY_DUPLICATED</b>, сессия уже была аннулирована сервером, и пользователю необходимо сгенерировать новый ключ авторизации и войти снова.""",
|
||||
),
|
||||
420: (
|
||||
"⛔ FLOOD",
|
||||
"Превышено максимально допустимое количество попыток вызова данного метода с указанными входными параметрами. Например, при попытке запросить большое количество текстовых сообщений (SMS) для одного и того же номера телефона.",
|
||||
),
|
||||
500: (
|
||||
"⛔ INTERNAL",
|
||||
"""Произошла внутренняя ошибка сервера во время обработки запроса; например, произошел сбой при доступе к базе данных или файловому хранилищу.
|
||||
|
||||
Если клиент получает ошибку 500 или вы считаете, что эта ошибка не должна была возникнуть, пожалуйста, соберите как можно больше информации о запросе и ошибке и отправьте ее разработчикам.""",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TelegramStatusCodes(loader.Module):
|
||||
"""Dictionary of telegram status codes"""
|
||||
|
||||
strings = {
|
||||
"name": "TelegramStatusCodes",
|
||||
"args_incorrect": "<b>Incorrect args</b>",
|
||||
"not_found": "<b>Code not found</b>",
|
||||
"syntax_error": "<b>Args are mandatory</b>",
|
||||
"scode": "<b>{} {}</b>\n⚜️ Code Description: <i>{}</i>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"args_incorrect": "<b>Неверные аргументы</b>",
|
||||
"not_found": "<b>Код не найден</b>",
|
||||
"syntax_error": "<b>Аргументы обязательны</b>",
|
||||
"_cmd_doc_httpsc": "<код> - Получить информацию о статус-коде",
|
||||
"_cmd_doc_httpscs": "Показать все доступные коды",
|
||||
"_cls_doc": "Словарь статус-кодов Telegram",
|
||||
"scode": "<b>{} {}</b>\n⚜️ Описание статус-кода: <i>{}</i>",
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.ub_lang = self._db.get("hikka.translations", "lang")
|
||||
if not self.ub_lang:
|
||||
self.ub_lang = self._db.get("heroku.translations", "lang")
|
||||
|
||||
@loader.unrestricted
|
||||
@loader.command(
|
||||
ru_doc="<код состояния> - Получение информации о статус-коде",
|
||||
en_doc="<statuscode> - Get status code info",
|
||||
)
|
||||
async def tgccmd(self, message):
|
||||
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("syntax_error", message))
|
||||
return
|
||||
|
||||
try:
|
||||
if int(args[0]) not in responses:
|
||||
await utils.answer(message, self.strings("not_found", message))
|
||||
except ValueError:
|
||||
await utils.answer(message, self.strings("args_incorrect", message))
|
||||
|
||||
if self.ub_lang != "ru":
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("scode", message).format(
|
||||
responses[int(args[0])][0], args[0], responses[int(args[0])][1]
|
||||
),
|
||||
)
|
||||
else:
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("scode", message).format(
|
||||
responses[int(args[0])][0], args[0], responses_ru[int(args[0])][1]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@loader.unrestricted
|
||||
@loader.command(
|
||||
ru_doc="Получите все статус-коды Telegram",
|
||||
en_doc="Get all Telegram status codes",
|
||||
)
|
||||
async def tgcscmd(self, message):
|
||||
await utils.answer(
|
||||
message,
|
||||
"\n".join(
|
||||
[f"<b>{str(sc)}: {text}</b>" for sc, (text, _) in responses.items()]
|
||||
),
|
||||
)
|
||||
@@ -1,536 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2026-2029 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TelegraphComics
|
||||
# Description: Create comics on Telegraph from ZIP/RAR archives
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: aiohttp, zipfile, telegraph
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from typing import List, Optional
|
||||
import zipfile
|
||||
|
||||
import aiohttp
|
||||
from telethon.types import MessageMediaDocument, Message
|
||||
|
||||
from telegraph import Telegraph
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TelegraphComicMod(loader.Module):
|
||||
"""Create comics on Telegraph from ZIP/CBZ/RAR archives"""
|
||||
|
||||
strings = {
|
||||
"name": "TelegraphComic",
|
||||
"invalid_args": "<emoji document_id=5388785832956016892>❌</emoji> Invalid arguments. Usage: .telegraphcomics <title> | <cover_url> (optional)",
|
||||
"no_reply": "<emoji document_id=5388785832956016892>❌</emoji> Reply to a message with ZIP/CBZ/RAR file",
|
||||
"unsupported_format": "<emoji document_id=5388785832956016892>❌</emoji> Unsupported file format. Only ZIP/CBZ/RAR files are supported",
|
||||
"processing": "<emoji document_id=5256094480498436162>⏳</emoji> Processing archive...",
|
||||
"uploading": "<emoji document_id=5854762571659218443>⏳</emoji> Uploading images...",
|
||||
"creating_article": "<emoji document_id=5854762571659218443>⏳</emoji> Creating Telegraph article...",
|
||||
"archive_extracted": "<emoji document_id=5854762571659218443>📦</emoji> Archive successfully extracted: <emoji document_id=5208422125924275090>✅</emoji>",
|
||||
"upload_files": "<emoji document_id=5854762571659218443>📦</emoji> Upload image files:",
|
||||
"creating_telegraph": "<emoji document_id=5854762571659218443>📝</emoji> Creating Telegraph article:",
|
||||
"success": '<emoji document_id=5208422125924275090>✅</emoji> <b>Telegraph article created!</b>\n\n<emoji document_id=5256094480498436162>📦</emoji> Archive successfully extracted: <emoji document_id=5208422125924275090>✅</emoji>\n\n<emoji document_id=5256094480498436162>📦</emoji> Upload image files:\n{upload_status}\n\n<emoji document_id=5256230583717079814>📝</emoji> Creating Telegraph article:\n{article_status}\n\n<emoji document_id=5271604874419647061>🔗</emoji> <a href="{url}">{url}</a>',
|
||||
"error": "<emoji document_id=5854929766146118183>❌</emoji> <b>Error:</b> {}",
|
||||
"_cls_doc": "Create comics on Telegraph from ZIP/CBZ/RAR archives",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Создание комиксов на Telegraph из ZIP/CBZ/RAR архивов",
|
||||
"invalid_args": "<emoji document_id=5388785832956016892>❌</emoji> Неверные аргументы. Использование: .telegraphcomics <название> | <ссылка_на_обложку>(необязательно)",
|
||||
"no_reply": "<emoji document_id=5388785832956016892>❌</emoji> Ответьте на сообщение с ZIP/CBZ/RAR файлом",
|
||||
"unsupported_format": "<emoji document_id=5388785832956016892>❌</emoji> Неподдерживаемый формат. Только ZIP/CBZ/RAR файлы",
|
||||
"processing": "<emoji document_id=5256094480498436162>⏳</emoji> Обработка архива...",
|
||||
"uploading": "<emoji document_id=5256094480498436162>⏳</emoji> Загрузка изображений...",
|
||||
"creating_article": "<emoji document_id=5854762571659218443>⏳</emoji> Создание Telegraph статьи...",
|
||||
"archive_extracted": "<emoji document_id=5256094480498436162>📦</emoji> Архив успешно распакован: <emoji document_id=5208422125924275090>✅</emoji>",
|
||||
"upload_files": "<emoji document_id=5256094480498436162>📦</emoji> Загрузка файлов изображений:",
|
||||
"creating_telegraph": "<emoji document_id=5854762571659218443>📝</emoji> Создание Telegraph статьи:",
|
||||
"success": '<emoji document_id=5208422125924275090>✅</emoji> <b>Telegraph статья создана!</b>\n\n<emoji document_id=5256094480498436162>📦</emoji> Архив успешно распакован: <emoji document_id=5208422125924275090>✅</emoji>\n\n<emoji document_id=5256094480498436162>📦</emoji> Загрузка файлов изображений:\n{upload_status}\n\n<emoji document_id=5256230583717079814>📝</emoji> Создание Telegraph статьи:\n{article_status}\n\n<emoji document_id=5271604874419647061>🔗</emoji> <a href="{url}">{url}</a>',
|
||||
"error": "<emoji document_id=5388785832956016892>❌</emoji> <b>Ошибка:</b> {}",
|
||||
"available_services": "Доступные сервисы: catbox, bashupload, kappa, x0, tmpfiles, pomf",
|
||||
"current_service": "Текущий сервис: {}",
|
||||
"invalid_service": "❌ Неизвестный сервис: {}\n\n{}",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"upload_service",
|
||||
"catbox",
|
||||
"Upload service to use",
|
||||
validator=loader.validators.Choice(
|
||||
["catbox", "bashupload", "kappa", "x0", "tmpfiles", "pomf"]
|
||||
),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"short_name",
|
||||
"HikkaMods",
|
||||
"short name for the article",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"author_name",
|
||||
"HikkaMods",
|
||||
"nickname of the author of the article",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"author_url",
|
||||
"https://t.me/hikka_mods",
|
||||
"link to author",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
self.telegraph = Telegraph()
|
||||
self.telegraph.create_account(
|
||||
short_name=self.config["short_name"],
|
||||
author_name=self.config["author_name"],
|
||||
author_url=self.config["author_url"],
|
||||
)
|
||||
|
||||
async def _upload_file_to_service(
|
||||
self,
|
||||
session: aiohttp.ClientSession,
|
||||
url: str,
|
||||
file_path: str,
|
||||
field_name: str,
|
||||
**extra_fields,
|
||||
) -> Optional[str]:
|
||||
"""Generic file upload method"""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
data = aiohttp.FormData()
|
||||
data.add_field(field_name, f, filename=os.path.basename(file_path))
|
||||
|
||||
for key, value in extra_fields.items():
|
||||
data.add_field(key, value)
|
||||
|
||||
async with session.post(url, data=data) as response:
|
||||
if response.status == 200:
|
||||
result = await response.text()
|
||||
return result.strip() if result else None
|
||||
else:
|
||||
logger.info(
|
||||
f"Upload failed with status {response.status}: {await response.text()}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info(f"Error uploading to {url}: {e}")
|
||||
return None
|
||||
|
||||
async def upload_to_catbox(self, file_path: str) -> Optional[str]:
|
||||
"""Upload file to catbox.moe"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
result = await self._upload_file_to_service(
|
||||
session,
|
||||
"https://catbox.moe/user/api.php",
|
||||
file_path,
|
||||
"fileToUpload",
|
||||
reqtype="fileupload",
|
||||
)
|
||||
return (
|
||||
result
|
||||
if result and result.startswith("https://files.catbox.moe/")
|
||||
else None
|
||||
)
|
||||
|
||||
async def upload_to_bashupload(self, file_path: str) -> Optional[str]:
|
||||
"""Upload file to bashupload.com"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
data = aiohttp.FormData()
|
||||
data.add_field("file", f, filename=os.path.basename(file_path))
|
||||
|
||||
async with session.post(
|
||||
"https://bashupload.com", data=data
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
result = await response.text()
|
||||
|
||||
lines = result.strip().split("\n")
|
||||
for line in lines:
|
||||
if line.startswith("https://"):
|
||||
return line
|
||||
|
||||
if "wget" in result:
|
||||
urls = [
|
||||
line
|
||||
for line in result.split("\n")
|
||||
if "wget" in line
|
||||
]
|
||||
if urls:
|
||||
parts = urls[0].split()
|
||||
for part in parts:
|
||||
if part.startswith("https://"):
|
||||
return part
|
||||
except Exception as e:
|
||||
logger.info(f"Error uploading to bashupload: {e}")
|
||||
return None
|
||||
|
||||
async def upload_to_kappa(self, file_path: str) -> Optional[str]:
|
||||
"""Upload file to kappa.lol"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
data = aiohttp.FormData()
|
||||
data.add_field("file", f, filename=os.path.basename(file_path))
|
||||
|
||||
async with session.post(
|
||||
"https://kappa.lol/api/upload", data=data
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
result = await response.json()
|
||||
if result and "id" in result:
|
||||
return f"https://kappa.lol/{result['id']}"
|
||||
except Exception as e:
|
||||
logger.info(f"Error uploading to kappa: {e}")
|
||||
return None
|
||||
|
||||
async def upload_to_x0(self, file_path: str) -> Optional[str]:
|
||||
"""Upload file to x0.at"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
data = aiohttp.FormData()
|
||||
data.add_field("file", f, filename=os.path.basename(file_path))
|
||||
|
||||
async with session.post("https://x0.at", data=data) as response:
|
||||
if response.status == 200:
|
||||
result = await response.text()
|
||||
return (
|
||||
result.strip()
|
||||
if result and "https://" in result
|
||||
else None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info(f"Error uploading to x0: {e}")
|
||||
return None
|
||||
|
||||
async def upload_to_tmpfiles(self, file_path: str) -> Optional[str]:
|
||||
"""Upload file to tmpfiles.org"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
data = aiohttp.FormData()
|
||||
data.add_field("file", f, filename=os.path.basename(file_path))
|
||||
|
||||
async with session.post(
|
||||
"https://tmpfiles.org/api/v1/upload", data=data
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
result = await response.json()
|
||||
if result and "data" in result and "url" in result["data"]:
|
||||
return result["data"]["url"]
|
||||
except Exception as e:
|
||||
logger.info(f"Error uploading to tmpfiles: {e}")
|
||||
return None
|
||||
|
||||
async def upload_to_pomf(self, file_path: str) -> Optional[str]:
|
||||
"""Upload file to pomf.lain.la"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
data = aiohttp.FormData()
|
||||
data.add_field("files[]", f, filename=os.path.basename(file_path))
|
||||
|
||||
async with session.post(
|
||||
"https://pomf.lain.la/upload.php", data=data
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
result = await response.json()
|
||||
if result and "files" in result and result["files"]:
|
||||
return result["files"][0].get("url")
|
||||
except Exception as e:
|
||||
logger.info(f"Error uploading to pomf: {e}")
|
||||
return None
|
||||
|
||||
async def upload_file(self, file_path: str) -> Optional[str]:
|
||||
"""Upload file to selected service"""
|
||||
service_name = self.config["upload_service"]
|
||||
|
||||
service_map = {
|
||||
"catbox": self.upload_to_catbox,
|
||||
"bashupload": self.upload_to_bashupload,
|
||||
"kappa": self.upload_to_kappa,
|
||||
"x0": self.upload_to_x0,
|
||||
"tmpfiles": self.upload_to_tmpfiles,
|
||||
"pomf": self.upload_to_pomf,
|
||||
}
|
||||
|
||||
service_func = service_map.get(service_name)
|
||||
if not service_func:
|
||||
return await self.upload_to_catbox(file_path)
|
||||
|
||||
try:
|
||||
result = await service_func(file_path)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Upload to {service_name} failed: {e}")
|
||||
return None
|
||||
|
||||
async def extract_zip_archive(self, zip_path: str, extract_dir: str) -> List[str]:
|
||||
"""Extract ZIP archive and return sorted list of image files"""
|
||||
image_extensions = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".avif"}
|
||||
image_files = []
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
|
||||
for root, _, files in os.walk(extract_dir):
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1].lower() in image_extensions:
|
||||
image_files.append(os.path.join(root, file))
|
||||
|
||||
image_files.sort(key=lambda x: os.path.basename(x).lower())
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Error extracting ZIP archive: {e}")
|
||||
|
||||
return image_files
|
||||
|
||||
async def create_telegraph_article(
|
||||
self, title: str, image_urls: List[str], cover_url: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""Create Telegraph article with images"""
|
||||
try:
|
||||
if cover_url:
|
||||
content = f'<img src="{cover_url}"/><br>'
|
||||
content += "<br>".join(f'<img src="{url}"/>' for url in image_urls)
|
||||
else:
|
||||
content = "<br>".join(f'<img src="{url}"/>' for url in image_urls)
|
||||
|
||||
response = await asyncio.to_thread(
|
||||
lambda: self.telegraph.create_page(
|
||||
title=title,
|
||||
html_content=content,
|
||||
author_name=self.config["author_name"],
|
||||
author_url=self.config["author_url"],
|
||||
)
|
||||
)
|
||||
|
||||
return response["url"]
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Error creating Telegraph article: {e}")
|
||||
return None
|
||||
|
||||
async def _process_cover_url(self, cover_url: str) -> Optional[str]:
|
||||
"""Process cover URL - handle Telegram message links and direct URLs"""
|
||||
if not cover_url:
|
||||
return None
|
||||
|
||||
cover_url = cover_url.strip()
|
||||
|
||||
if "t.me/" in cover_url and "/" in cover_url.split("t.me/")[1]:
|
||||
try:
|
||||
parts = cover_url.split("/")
|
||||
if len(parts) >= 4:
|
||||
chat_username = parts[-3]
|
||||
message_id = int(parts[-1])
|
||||
|
||||
message = await self.client.get_messages(
|
||||
chat_username, ids=message_id
|
||||
)
|
||||
if message and message.media:
|
||||
media_path = await message.download_media()
|
||||
if media_path:
|
||||
uploaded_url = await self.upload_file(media_path)
|
||||
os.remove(media_path)
|
||||
return uploaded_url
|
||||
except Exception as e:
|
||||
logger.info(f"Error processing Telegram cover link: {e}")
|
||||
return cover_url
|
||||
|
||||
return cover_url
|
||||
|
||||
async def _process_comics_request(self, message, create_func) -> None:
|
||||
"""Common logic for processing comics requests"""
|
||||
args = utils.get_args_raw(message)
|
||||
reply = await message.get_reply_message()
|
||||
|
||||
if not args or not reply:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
|
||||
if not isinstance(reply.media, MessageMediaDocument):
|
||||
await utils.answer(message, self.strings["no_reply"])
|
||||
return
|
||||
|
||||
if "|" in args:
|
||||
title, cover_url = args.split("|", 1)
|
||||
else:
|
||||
title = args
|
||||
cover_url = None
|
||||
|
||||
title = title.strip()
|
||||
cover_url = (
|
||||
await self._process_cover_url(cover_url.strip()) if cover_url else None
|
||||
)
|
||||
|
||||
await utils.answer(message, self.strings["processing"])
|
||||
|
||||
file_path = await reply.download_media()
|
||||
if not file_path:
|
||||
await utils.answer(
|
||||
message, self.strings["error"].format("Failed to download file")
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
if not (file_path.lower().endswith((".zip", ".cbz"))):
|
||||
await utils.answer(message, self.strings["unsupported_format"])
|
||||
return
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
archive_path = file_path
|
||||
if file_path.lower().endswith(".cbz"):
|
||||
import shutil
|
||||
|
||||
zip_path = file_path[:-4] + ".zip"
|
||||
shutil.copy2(file_path, zip_path)
|
||||
archive_path = zip_path
|
||||
|
||||
image_files = await self.extract_zip_archive(archive_path, temp_dir)
|
||||
|
||||
if archive_path != file_path and os.path.exists(archive_path):
|
||||
os.remove(archive_path)
|
||||
if not image_files:
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["error"].format("No images found in archive"),
|
||||
)
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings["archive_extracted"])
|
||||
|
||||
await utils.answer(message, self.strings["uploading"])
|
||||
|
||||
upload_tasks = [self.upload_file(img_file) for img_file in image_files]
|
||||
upload_results = await asyncio.gather(
|
||||
*upload_tasks, return_exceptions=True
|
||||
)
|
||||
image_urls = []
|
||||
failed_uploads = 0
|
||||
upload_errors = []
|
||||
upload_status_lines = []
|
||||
|
||||
for i, (img_file, result) in enumerate(
|
||||
zip(image_files, upload_results)
|
||||
):
|
||||
filename = os.path.basename(img_file)
|
||||
if isinstance(result, Exception):
|
||||
error_str = str(result)
|
||||
logger.info(f"Upload failed: {error_str}")
|
||||
failed_uploads += 1
|
||||
upload_errors.append(error_str)
|
||||
upload_status_lines.append(
|
||||
f"{filename} - <emoji document_id=5388785832956016892>❌</emoji>"
|
||||
)
|
||||
elif result and "https://" in result:
|
||||
image_urls.append(result)
|
||||
upload_status_lines.append(
|
||||
f"{filename} - <emoji document_id=5208422125924275090>✅</emoji>"
|
||||
)
|
||||
else:
|
||||
failed_uploads += 1
|
||||
upload_errors.append("Invalid response from upload service")
|
||||
upload_status_lines.append(
|
||||
f"{filename} - <emoji document_id=5388785832956016892>❌</emoji>"
|
||||
)
|
||||
|
||||
if not image_urls:
|
||||
error_details = []
|
||||
error_details.append(f"Failed uploads: {failed_uploads}")
|
||||
|
||||
if upload_errors:
|
||||
unique_errors = list(set(upload_errors))[:3]
|
||||
error_details.append("Errors: " + "; ".join(unique_errors))
|
||||
|
||||
error_msg = " | ".join(error_details)
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["error"].format(error_msg),
|
||||
)
|
||||
return
|
||||
|
||||
upload_status = (
|
||||
self.strings["upload_files"] + "\n" + "\n".join(upload_status_lines)
|
||||
)
|
||||
await utils.answer(message, upload_status)
|
||||
|
||||
await utils.answer(message, self.strings["creating_article"])
|
||||
|
||||
article_url = await create_func(title, image_urls, cover_url)
|
||||
if article_url:
|
||||
article_status_lines = []
|
||||
for i, (img_file, url) in enumerate(zip(image_files, image_urls)):
|
||||
filename = os.path.basename(img_file)
|
||||
article_status_lines.append(
|
||||
f"{filename} - <emoji document_id=5208422125924275090>✅</emoji>"
|
||||
)
|
||||
|
||||
upload_status = "\n".join(upload_status_lines)
|
||||
article_status = "\n".join(article_status_lines)
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["success"].format(
|
||||
upload_status=upload_status,
|
||||
article_status=article_status,
|
||||
url=article_url,
|
||||
),
|
||||
)
|
||||
|
||||
else:
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["error"].format("Failed to create article"),
|
||||
)
|
||||
except Exception as e:
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["error"].format(f"Processing error: {e}"),
|
||||
)
|
||||
finally:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Создать комикс на Telegraph из ZIP/CBZ/RAR архива\nАргументы: <название> | <ссылка_на_обложку>(необязательно)\nИспользование: .telegraphcomics <title> | <cover_url>(optional)",
|
||||
en_doc="Create Telegraph comic from ZIP/CBZ/RAR archive\nArguments: <title> | <cover_url>(optional)\nUsage: .telegraphcomics <title> | <cover_url>(optional)",
|
||||
)
|
||||
async def telegraphcomicscmd(self, message):
|
||||
await self._process_comics_request(message, self.create_telegraph_article)
|
||||
@@ -1,158 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TempChat
|
||||
# Description: Creates a temporary private chat with a message forwarding restriction and adds the specified user to it.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: TempChat
|
||||
# scope: TempChat 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from hikkatl import functions
|
||||
from datetime import datetime as dt
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TempChatMod(loader.Module):
|
||||
"""Creates a temporary private chat with a message forwarding restriction and adds the specified user to it."""
|
||||
|
||||
strings = {
|
||||
"name": "TempChat",
|
||||
"selfchat": "You can't create a chat with yourself.",
|
||||
"wrongargs": "<emoji document_id=5980953710157632545>❌</emoji> <b>Wrong arguments. Use </b><code>.tmpchat [@user/reply] [time]</code><b>",
|
||||
"alreadychatting": "<emoji document_id=5980953710157632545>❌</emoji> <b>You already have an active conversation with this person.</b>",
|
||||
"invalidtime": "<emoji document_id=5980953710157632545>❌</emoji> <b>Invalid time format. Use combinations like 1h30m.</b>",
|
||||
"invitemsg": "<emoji document_id=5818967120213445821>🛡</emoji> You've been invited to a temporary private chat!\n\n<emoji document_id=5451646226975955576>⌛️</emoji> Auto-deletes in ",
|
||||
"joinlink": "🔗 Join link: ",
|
||||
"chatcreated": "<emoji document_id=5980930633298350051>✅</emoji> The temporary chat has been successfully created!",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"selfchat": "Ты не можешь создать чат сам с собой.",
|
||||
"wrongargs": "<emoji document_id=5980953710157632545>❌</emoji> <b>Неверные аргументы. Используй </b><code>.tmpchat [@user/reply] [время]</code>",
|
||||
"alreadychatting": "<emoji document_id=5980953710157632545>❌</emoji> <b>У вас уже есть открытая переписка с этим человеком.</b>",
|
||||
"invalidtime": "<emoji document_id=5980953710157632545>❌</emoji> <b>Неверный формат времени. Убедитесь, что вы вводите время в формате 1h, 2h30m.</b>",
|
||||
"invitemsg": "<emoji document_id=5818967120213445821>🛡</emoji> Вы были приглашены во временный приватный чат!\n\n<emoji document_id=5451646226975955576>⌛️</emoji> Авто-удаление через ",
|
||||
"joinlink": "🔗 Ссылка: ",
|
||||
"chatcreated": "<emoji document_id=5980930633298350051>✅</emoji> Временный чат успешно создан!",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.temp_chats = {}
|
||||
|
||||
@loader.loop(interval=30, autostart=True)
|
||||
async def check_expired_chats(self):
|
||||
now = dt.now().timestamp()
|
||||
for chat_id in list(self.temp_chats.keys()):
|
||||
if self.temp_chats[chat_id][1] <= now:
|
||||
try:
|
||||
await self.client(
|
||||
functions.channels.DeleteChannelRequest(chat_id)
|
||||
)
|
||||
del self.temp_chats[chat_id]
|
||||
self.set("temp_chats", self.temp_chats)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting chat {chat_id}: {e}")
|
||||
try:
|
||||
self.client(
|
||||
functions.channels.GetFullChannelRequest(
|
||||
channel=chat_id
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
del self.temp_chats[chat_id]
|
||||
self.set("temp_chats", self.temp_chats)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.hmodslib = await self.import_lib(
|
||||
"https://files.archquise.ru/HModsLibrary.py"
|
||||
)
|
||||
self.temp_chats = self.get("temp_chats", {})
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Создает временный чат. Использование: .tmpchat [@user/reply] [time]"
|
||||
)
|
||||
async def tmpchat(self, message):
|
||||
"""Create temporary chat. Usage: .tmpchat [@user/reply] [time]"""
|
||||
args = utils.get_args_raw(message)
|
||||
reply = await message.get_reply_message()
|
||||
|
||||
if reply:
|
||||
user = await self.client.get_entity(reply.sender_id)
|
||||
time_str = args.strip() if args else None
|
||||
else:
|
||||
parts = args.split(",", 1) if "," in args else args.rsplit(" ", 1)
|
||||
if len(parts) != 2:
|
||||
return await utils.answer(message, self.strings["wrongargs"])
|
||||
user_str, time_str = parts[0].strip(), parts[1].strip()
|
||||
try:
|
||||
user = await self.client.get_entity(user_str)
|
||||
except Exception:
|
||||
return await utils.answer(message, self.strings["wrongargs"])
|
||||
|
||||
if not time_str:
|
||||
return await utils.answer(message, self.strings["wrongargs"])
|
||||
seconds = await self.hmodslib.parse_time(time_str)
|
||||
if not seconds:
|
||||
return await utils.answer(message, self.strings["invalidtime"])
|
||||
|
||||
if any(user.id == uid for uid, _ in self.temp_chats.values()):
|
||||
return await utils.answer(message, self.strings["alreadychatting"])
|
||||
|
||||
try:
|
||||
created = await self.client(
|
||||
functions.channels.CreateChannelRequest(
|
||||
title=f"TempChat #{user.id}",
|
||||
about=f"Temporary private chat with {user.id} | Expires after: {time_str}",
|
||||
megagroup=True,
|
||||
)
|
||||
)
|
||||
chat_id = created.chats[0].id
|
||||
expires_at = dt.now().timestamp() + seconds
|
||||
|
||||
await self.client(
|
||||
functions.messages.ToggleNoForwardsRequest(peer=chat_id, enabled=True)
|
||||
)
|
||||
|
||||
self.temp_chats[chat_id] = (user.id, expires_at)
|
||||
self.set("temp_chats", self.temp_chats)
|
||||
|
||||
invite = await self.client(
|
||||
functions.messages.ExportChatInviteRequest(peer=chat_id, usage_limit=1)
|
||||
)
|
||||
invite_message = (
|
||||
self.strings["invitemsg"]
|
||||
+ time_str
|
||||
+ f"\n{self.strings['joinlink']} {invite.link}"
|
||||
)
|
||||
await self.client.send_message(user.id, invite_message)
|
||||
await utils.answer(message, self.strings["chatcreated"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating temp chat: {e}")
|
||||
await utils.answer(message, "❌ Error! Check log-chat.")
|
||||
@@ -1,80 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Text2File
|
||||
# Description: Module for convertation your text to file
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Text2File
|
||||
# scope: Text2File 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import io
|
||||
import logging
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class Text2File(loader.Module):
|
||||
"""Module for convertation your text to file"""
|
||||
|
||||
strings = {
|
||||
"name": "Text2File",
|
||||
"no_args": "Don't have any args! Use .ttf text/code",
|
||||
"cfg_name": "You can change the extension and file name",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_args": "Недостаточно аргументов! Используйте: .ttf текст/код",
|
||||
"cfg_name": "Вы можете выбрать расширение и название для файла",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"name",
|
||||
"file.txt",
|
||||
lambda: self.strings("cfg_name"),
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Создать файл с вашим текстом или кодом",
|
||||
en_doc="Create a file with your text or code",
|
||||
)
|
||||
async def ttfcmd(self, message):
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("no_args"))
|
||||
return
|
||||
|
||||
text = args
|
||||
by = io.BytesIO(text.encode("utf-8"))
|
||||
by.name = self.config["name"]
|
||||
|
||||
await utils.send_file(
|
||||
message.chat_id,
|
||||
by,
|
||||
caption=None,
|
||||
reply_to=message.reply_to_msg_id,
|
||||
)
|
||||
@@ -1,282 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TikTokDownloader
|
||||
# Description: A module for downloading videos and photos from TikTok without watermark
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api TikTokDownloader
|
||||
# scope: Api TikTokDownloader 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import List, Optional, Union
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import aiohttp
|
||||
from tqdm import tqdm
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class data:
|
||||
dir_name: str
|
||||
media: Union[str, List[str]]
|
||||
type: str
|
||||
|
||||
|
||||
class TikTok:
|
||||
def __init__(self, host: Optional[str] = None):
|
||||
self.headers = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) "
|
||||
"AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 "
|
||||
"Mobile/7B334b Safari/531.21.10"
|
||||
)
|
||||
}
|
||||
self.host = host or "https://www.tikwm.com/"
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
self.data_endpoint = "api"
|
||||
self.search_videos_keyword_endpoint = "api/feed/search"
|
||||
self.search_videos_hashtag_endpoint = "api/challenge/search"
|
||||
|
||||
self.link = None
|
||||
self.result = None
|
||||
|
||||
self.logger = logging.getLogger("damirtag-TikTok")
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
"[damirtag-TikTok:%(funcName)s]: %(levelname)s - %(message)s"
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
self.logger.addHandler(handler)
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
async def close_session(self):
|
||||
await self.session.close()
|
||||
|
||||
async def __ensure_data(self, link: str):
|
||||
if self.link != link:
|
||||
self.link = link
|
||||
self.result = await self._fetch_data(link)
|
||||
self.logger.info("Successfully ensured data from the link")
|
||||
|
||||
async def __get_images(self, download_dir: Optional[str] = None):
|
||||
download_dir = download_dir or self.result["id"]
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
tasks = [
|
||||
self._download_file(url, os.path.join(download_dir, f"image_{i + 1}.jpg"))
|
||||
for i, url in enumerate(self.result["images"])
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
self.logger.info(f"Images - Downloaded and saved photos to {download_dir}")
|
||||
|
||||
return data(
|
||||
dir_name=download_dir,
|
||||
media=[
|
||||
os.path.join(download_dir, f"image_{i + 1}.jpg")
|
||||
for i in range(len(self.result["images"]))
|
||||
],
|
||||
type="images",
|
||||
)
|
||||
|
||||
async def __get_video(self, video_filename: Optional[str] = None, hd: bool = False):
|
||||
video_url = self.result["hdplay"] if hd else self.result["play"]
|
||||
video_filename = video_filename or f"{self.result['id']}.mp4"
|
||||
|
||||
async with self.session.get(video_url) as response:
|
||||
response.raise_for_status()
|
||||
total_size = int(response.headers.get("content-length", 0))
|
||||
with open(video_filename, "wb") as file:
|
||||
with tqdm(
|
||||
total=total_size, unit="B", unit_scale=True, desc=video_filename
|
||||
) as pbar:
|
||||
async for chunk in response.content.iter_any():
|
||||
file.write(chunk)
|
||||
pbar.update(len(chunk))
|
||||
|
||||
self.logger.info(f"Video - Downloaded and saved video as {video_filename}")
|
||||
|
||||
return data(
|
||||
dir_name=os.path.dirname(video_filename), media=video_filename, type="video"
|
||||
)
|
||||
|
||||
async def _fetch_data(self, link: str) -> dict:
|
||||
url = self.get_url(link)
|
||||
params = {"url": url, "hd": 1}
|
||||
return await self._make_request(self.data_endpoint, params=params)
|
||||
|
||||
async def _download_file(self, url: str, path: str):
|
||||
async with self.session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
with open(path, "wb") as file:
|
||||
while chunk := await response.content.read(1024):
|
||||
file.write(chunk)
|
||||
|
||||
async def download_sound(
|
||||
self,
|
||||
link: str,
|
||||
audio_filename: Optional[str] = None,
|
||||
audio_ext: Optional[str] = ".mp3",
|
||||
):
|
||||
await self.__ensure_data(link)
|
||||
|
||||
if not audio_filename:
|
||||
audio_filename = f"{self.result['music_info']['title']}{audio_ext}"
|
||||
else:
|
||||
audio_filename += audio_ext
|
||||
|
||||
await self._download_file(self.result["music_info"]["play"], audio_filename)
|
||||
self.logger.info(f"Sound - Downloaded and saved sound as {audio_filename}")
|
||||
return audio_filename
|
||||
|
||||
async def download(
|
||||
self, link: str, video_filename: Optional[str] = None, hd: bool = True
|
||||
):
|
||||
await self.__ensure_data(link)
|
||||
|
||||
if "images" in self.result:
|
||||
return await self.__get_images(video_filename)
|
||||
|
||||
if "hdplay" in self.result or "play" in self.result:
|
||||
return await self.__get_video(video_filename, hd)
|
||||
|
||||
self.logger.error("No downloadable content found in the provided link.")
|
||||
raise Exception("No downloadable content found in the provided link.")
|
||||
|
||||
async def _make_request(self, endpoint: str, params: dict) -> dict:
|
||||
async with self.session.get(
|
||||
urljoin(self.host, endpoint), params=params, headers=self.headers
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
return data.get("data", {})
|
||||
|
||||
@staticmethod
|
||||
def get_url(text: str) -> Optional[str]:
|
||||
urls = re.findall(r"http[s]?://[^\s]+", text)
|
||||
return urls[0] if urls else None
|
||||
|
||||
@staticmethod
|
||||
def _get_video_link(unique_id: str, aweme_id: str) -> str:
|
||||
return f"https://www.tiktok.com/@{unique_id}/video/{aweme_id}"
|
||||
|
||||
@staticmethod
|
||||
def _get_uploader_link(unique_id: str) -> str:
|
||||
return f"https://www.tiktok.com/@{unique_id}"
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TikTokDownloader(loader.Module):
|
||||
"""TikTok Downloader module"""
|
||||
|
||||
strings = {
|
||||
"name": "TikTokDownloader",
|
||||
"downloading": "<emoji document_id=5436024756610546212>⚡</emoji> <b>Downloading…</b>",
|
||||
"success_photo": "<emoji document_id=5436246187944460315>❤️</emoji> <b>The photo(s) has/have been successfully downloaded!</b>!",
|
||||
"success_video": "<emoji document_id=5436246187944460315>❤️</emoji> <b>The video has been successfully downloaded!</b>",
|
||||
"success_sound": "<emoji document_id=5436246187944460315>❤️</emoji> <b>The sound has been successfully downloaded!</b>",
|
||||
"error": "Error occurred while downloading.\n{}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"downloading": "<emoji document_id=5436024756610546212>⚡</emoji> <b>Загружаем…</b>",
|
||||
"success_photo": "<emoji document_id=5436246187944460315>❤️</emoji> <b>Фотография(-и) была(-и) успешно загружены!</b>!",
|
||||
"success_video": "<emoji document_id=5436246187944460315>❤️</emoji> <b>Видео было успешно загружено!</b>",
|
||||
"success_sound": "<emoji document_id=5436246187944460315>❤️</emoji> <b>Звук был успешно загружен!</b>",
|
||||
"error": "Во время загрузки произошла ошибка.\n{}",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Скачать звук с TikTok",
|
||||
en_doc="Download sound from TikTok",
|
||||
)
|
||||
async def ttsound(self, message):
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
await utils.answer(message, "Please provide a TikTok URL.")
|
||||
return
|
||||
|
||||
url = args[0]
|
||||
await utils.answer(message, self.strings("downloading"))
|
||||
|
||||
tiktok_downloader = TikTok()
|
||||
|
||||
try:
|
||||
download_result = await tiktok_downloader.download_sound(url)
|
||||
await message.client.send_file(
|
||||
message.to_id, download_result, caption=self.strings("success_sound")
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
await utils.answer(
|
||||
message,
|
||||
f"{self.strings('error').format(e)}\n Убедитесь, что ссылка ведет именно на видео или фото с нужным звуком, прямая ссылка на звук не сработает!",
|
||||
)
|
||||
finally:
|
||||
await tiktok_downloader.close_session()
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Скачать видео или фото с TikTok",
|
||||
en_doc="Download videos or photos from TikTok",
|
||||
)
|
||||
async def tt(self, message):
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
await utils.answer(message, "Please provide a TikTok URL.")
|
||||
return
|
||||
|
||||
url = args[0]
|
||||
await utils.answer(message, self.strings("downloading"))
|
||||
|
||||
tiktok_downloader = TikTok()
|
||||
|
||||
try:
|
||||
download_result = await tiktok_downloader.download(url)
|
||||
|
||||
if download_result.type == "video":
|
||||
await message.client.send_file(
|
||||
message.to_id,
|
||||
download_result.media,
|
||||
caption=self.strings("success_video"),
|
||||
)
|
||||
await message.delete()
|
||||
elif download_result.type == "images":
|
||||
await message.client.send_file(
|
||||
message.to_id,
|
||||
download_result.media,
|
||||
caption=self.strings("success_photo"),
|
||||
)
|
||||
await message.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings("error").format(e))
|
||||
finally:
|
||||
await tiktok_downloader.close_session()
|
||||
@@ -1,538 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TimedEmojiStatus
|
||||
# Description: Temporary emoji status with auto-revert
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: TimedEmojiStatus
|
||||
# scope: TimedEmojiStatus 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional
|
||||
|
||||
from telethon.tl.functions.account import UpdateEmojiStatusRequest
|
||||
from telethon.tl.types import EmojiStatus, MessageEntityCustomEmoji, Message
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TimedEmojiStatusMod(loader.Module):
|
||||
"""Temporary emoji status with auto-revert using scheduler"""
|
||||
|
||||
strings = {
|
||||
"name": "TimedEmojiStatus",
|
||||
"no_emoji": "<emoji document_id=5337117114392127164>❌</emoji> <b>Specify emoji or emoji document_id</b>",
|
||||
"no_time": "<emoji document_id=5337117114392127164>❌</emoji> <b>Specify time (ex: 1h, 30m, 2d)</b>",
|
||||
"invalid_time": "<emoji document_id=5337117114392127164>❌</emoji> <b>Invalid time format (ex: 30m, 2h, 1d, 1w)</b>",
|
||||
"status_set": "<emoji document_id=5336965905773504919>✅</emoji> <b>Status set:</b>\n<b>Current:</b> {}\n<b>Final:</b> {}\n<b>For:</b> {} ({})",
|
||||
"status_updated": "<emoji document_id=5336965905773504919>✅</emoji> <b>Status updated: {}</b>",
|
||||
"no_status": "<emoji document_id=5337117114392127164>❌</emoji> <b>No active status</b>",
|
||||
"status_removed": "<emoji document_id=5336965905773504919>✅</emoji> <b>Status removed</b>",
|
||||
"current_status": "<emoji document_id=5348186233610711303>📊</emoji> <b>Active status:</b>\n<b>Current:</b> {}\n<b>Final:</b> {}\n<b>Until:</b> {} ({})",
|
||||
"no_premium": "<emoji document_id=5337117114392127164>❌</emoji> <b>Premium required for emoji status</b>",
|
||||
"error": "<emoji document_id=5337117114392127164>❌</emoji> <b>Error: {}</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_emoji": "<emoji document_id=5337117114392127164>❌</emoji> <b>Укажите эмодзи или document_id</b>",
|
||||
"no_time": "<emoji document_id=5337117114392127164>❌</emoji> <b>Укажите время (напр: 1h, 30m, 2d)</b>",
|
||||
"invalid_time": "<emoji document_id=5337117114392127164>❌</emoji> <b>Неверный формат времени (напр: 30m, 2h, 1d, 1w)</b>",
|
||||
"status_set": "<emoji document_id=5336965905773504919>✅</emoji> <b>Статус установлен:</b>\n<b>Текущий:</b> {}\n<b>Финальный:</b> {}\n<b>На:</b> {} ({})",
|
||||
"status_updated": "<emoji document_id=5336965905773504919>✅</emoji> <b>Статус обновлён: {}</b>",
|
||||
"no_status": "<emoji document_id=5337117114392127164>❌</emoji> <b>Нет активного статуса</b>",
|
||||
"status_removed": "<emoji document_id=5336965905773504919>✅</emoji> <b>Статус удалён</b>",
|
||||
"current_status": "<emoji document_id=5348186233610711303>📊</emoji> <b>Активный статус:</b>\n<b>Текущий:</b> {}\n<b>Финальный:</b> {}\n<b>До:</b> {} ({})",
|
||||
"no_premium": "<emoji document_id=5337117114392127164>❌</emoji> <b>Требуется Premium для эмодзи статуса</b>",
|
||||
"error": "<emoji document_id=5337117114392127164>❌</emoji> <b>Ошибка: {}</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.status_data: Dict[int, Dict] = {}
|
||||
self.scheduler_tasks: Dict[int, asyncio.Task] = {}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
self._db = db
|
||||
|
||||
if not self._client.hikka_me.premium:
|
||||
logger.warning("Premium required for emoji status functionality")
|
||||
|
||||
await self._restore_active_statuses()
|
||||
|
||||
async def _restore_active_statuses(self):
|
||||
"""Restore and reschedule active statuses after restart"""
|
||||
saved = self._db.get(__name__, "statuses", {})
|
||||
current_time = time.time()
|
||||
|
||||
for user_id, data in saved.items():
|
||||
end_time = data.get("end_time", 0)
|
||||
if end_time > current_time:
|
||||
remaining_time = end_time - current_time
|
||||
logger.info(
|
||||
f"Restoring status for user {user_id}, remaining: {remaining_time}s"
|
||||
)
|
||||
|
||||
task = asyncio.create_task(
|
||||
self._schedule_revert_sleep(user_id, remaining_time)
|
||||
)
|
||||
self.scheduler_tasks[user_id] = task
|
||||
|
||||
self.status_data[user_id] = data
|
||||
else:
|
||||
logger.info(f"Removing expired status for user {user_id}")
|
||||
del saved[user_id]
|
||||
|
||||
if saved != self._db.get(__name__, "statuses", {}):
|
||||
self._db.set(__name__, "statuses", saved)
|
||||
|
||||
def _parse_time(self, time_str: str) -> Optional[timedelta]:
|
||||
"""Parse time string like 1h30m, 2d, 1w, 1mth"""
|
||||
pattern = r"(\d+)([smhdwmth]+)"
|
||||
matches = re.findall(pattern, time_str.lower())
|
||||
|
||||
if not matches:
|
||||
return None
|
||||
|
||||
total_seconds = 0
|
||||
for value, unit in matches:
|
||||
value = int(value)
|
||||
if unit == "s":
|
||||
total_seconds += value
|
||||
elif unit == "m":
|
||||
total_seconds += value * 60
|
||||
elif unit == "h":
|
||||
total_seconds += value * 3600
|
||||
elif unit == "d":
|
||||
total_seconds += value * 86400
|
||||
elif unit == "w":
|
||||
total_seconds += value * 604800
|
||||
elif unit in ["mth", "month"]:
|
||||
total_seconds += value * 2592000 # 30 days
|
||||
|
||||
return timedelta(seconds=total_seconds)
|
||||
|
||||
def _format_time(self, td: timedelta) -> str:
|
||||
"""Format timedelta to human readable string"""
|
||||
total_days = td.days
|
||||
months = total_days // 30
|
||||
remaining_days = total_days % 30
|
||||
|
||||
if months > 0:
|
||||
if remaining_days > 0:
|
||||
return f"{months}mth {remaining_days}d"
|
||||
return f"{months}mth"
|
||||
elif total_days > 0:
|
||||
return f"{total_days}d {td.seconds // 3600}h"
|
||||
elif td.seconds >= 3600:
|
||||
return f"{td.seconds // 3600}h {(td.seconds % 3600) // 60}m"
|
||||
else:
|
||||
return f"{td.seconds // 60}m"
|
||||
|
||||
def _extract_document_id(self, emoji_input: str) -> Optional[int]:
|
||||
"""Extract document_id from emoji string"""
|
||||
|
||||
pattern = r"<emoji\s+document_id=(\d+)>.*?</emoji>"
|
||||
match = re.search(pattern, emoji_input)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
|
||||
if emoji_input.isdigit():
|
||||
return int(emoji_input)
|
||||
|
||||
return None
|
||||
|
||||
def _extract_document_id_from_entities(self, message: Message) -> Optional[int]:
|
||||
"""Extract document_id from message entities"""
|
||||
if not message.entities:
|
||||
return None
|
||||
|
||||
for entity in message.entities:
|
||||
if isinstance(entity, MessageEntityCustomEmoji):
|
||||
return entity.document_id
|
||||
return None
|
||||
|
||||
def _safe_emoji_display(
|
||||
self, emoji_str: str, document_id: Optional[int] = None
|
||||
) -> str:
|
||||
"""Safely display emoji without causing errors"""
|
||||
if not emoji_str:
|
||||
return "❌"
|
||||
|
||||
if document_id:
|
||||
return f"[Custom Emoji ID: {document_id}]"
|
||||
|
||||
if emoji_str.isdigit():
|
||||
return f"[Custom Emoji ID: {emoji_str}]"
|
||||
|
||||
if "<emoji document_id=" in emoji_str:
|
||||
|
||||
import re
|
||||
match = re.search(r'document_id=(\d+)', emoji_str)
|
||||
if match:
|
||||
return f"[Custom Emoji ID: {match.group(1)}]"
|
||||
return "[Custom Emoji]"
|
||||
|
||||
if len(emoji_str) == 1 or (
|
||||
len(emoji_str) <= 4 and all(ord(c) >= 0x1F000 for c in emoji_str)
|
||||
):
|
||||
return emoji_str
|
||||
|
||||
return emoji_str[:10] + "..." if len(emoji_str) > 10 else emoji_str
|
||||
|
||||
async def _set_emoji_status(
|
||||
self, emoji_input: str, until: datetime | None = None, message: Message = None
|
||||
) -> tuple[bool, Optional[int]]:
|
||||
"""Set emoji status (requires Premium). Returns (success, document_id)"""
|
||||
try:
|
||||
logger.info(f"Setting emoji status for: {emoji_input}")
|
||||
|
||||
if not self._client.hikka_me.premium:
|
||||
logger.warning("Premium required for emoji status")
|
||||
return False, None
|
||||
|
||||
if not emoji_input:
|
||||
logger.info("Removing emoji status")
|
||||
await self._client(UpdateEmojiStatusRequest(emoji_status=None))
|
||||
return True, None
|
||||
|
||||
document_id = None
|
||||
|
||||
if message:
|
||||
document_id = self._extract_document_id_from_entities(message)
|
||||
if document_id:
|
||||
logger.info(
|
||||
f"Found document_id from message entities: {document_id}"
|
||||
)
|
||||
|
||||
if not document_id:
|
||||
document_id = self._extract_document_id(emoji_input)
|
||||
if document_id:
|
||||
logger.info(f"Extracted document_id from text: {document_id}")
|
||||
|
||||
if not document_id:
|
||||
try:
|
||||
logger.info("Trying to get document_id from test message")
|
||||
test_msg = await self._client.send_message("me", emoji_input)
|
||||
document_id = self._extract_document_id_from_entities(test_msg)
|
||||
await self._client.delete_messages("me", [test_msg.id])
|
||||
|
||||
if document_id:
|
||||
logger.info(
|
||||
f"Found document_id from test message: {document_id}"
|
||||
)
|
||||
else:
|
||||
logger.warning("No document_id found in test message")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting document_id from test message: {e}")
|
||||
|
||||
if document_id:
|
||||
try:
|
||||
emoji_status = EmojiStatus(document_id=document_id, until=until)
|
||||
await self._client(
|
||||
UpdateEmojiStatusRequest(emoji_status=emoji_status)
|
||||
)
|
||||
logger.info(
|
||||
f"Status set successfully with document_id: {document_id}"
|
||||
)
|
||||
return True, document_id
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting status: {e}")
|
||||
if "PREMIUM" in str(e).upper():
|
||||
return False, None
|
||||
return False, None
|
||||
|
||||
logger.warning("No document_id found, all methods failed")
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"General error setting emoji status: {e}")
|
||||
return False, None
|
||||
|
||||
async def _revert_status(self, user_id: int):
|
||||
"""Revert status to final emoji or remove"""
|
||||
logger.info(f"Starting revert status for user {user_id}")
|
||||
|
||||
if user_id in self.scheduler_tasks:
|
||||
del self.scheduler_tasks[user_id]
|
||||
|
||||
if user_id in self.status_data:
|
||||
data = self.status_data[user_id]
|
||||
final_emoji = data.get("final_emoji", "")
|
||||
final_doc_id = data.get("final_doc_id")
|
||||
|
||||
logger.info(
|
||||
f"Reverting status for user {user_id} to: '{final_emoji}' (saved doc_id: {final_doc_id})"
|
||||
)
|
||||
|
||||
try:
|
||||
if final_emoji and final_doc_id:
|
||||
logger.info(
|
||||
f"Setting final emoji using saved document_id: {final_doc_id}"
|
||||
)
|
||||
try:
|
||||
emoji_status = EmojiStatus(document_id=final_doc_id)
|
||||
await self._client(
|
||||
UpdateEmojiStatusRequest(emoji_status=emoji_status)
|
||||
)
|
||||
logger.info(
|
||||
f"Successfully set final emoji with document_id: {final_doc_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting final emoji with document_id: {e}")
|
||||
|
||||
success, _ = await self._set_emoji_status(final_emoji)
|
||||
if not success:
|
||||
await self._set_emoji_status("")
|
||||
elif final_emoji:
|
||||
logger.info(f"Attempting to set final emoji: '{final_emoji}'")
|
||||
success, final_doc_id = await self._set_emoji_status(final_emoji)
|
||||
if success:
|
||||
logger.info(
|
||||
f"Successfully reverted to final emoji: '{final_emoji}' (doc_id: {final_doc_id})"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Failed to set final emoji '{final_emoji}', removing status instead"
|
||||
)
|
||||
await self._set_emoji_status("")
|
||||
else:
|
||||
logger.info("No final emoji specified, removing status")
|
||||
await self._set_emoji_status("")
|
||||
except Exception as e:
|
||||
logger.error(f"Error reverting status: {e}")
|
||||
|
||||
try:
|
||||
await self._set_emoji_status("")
|
||||
except Exception as e2:
|
||||
logger.error(f"Error removing status: {e2}")
|
||||
|
||||
logger.info(f"Removing status data for user {user_id}")
|
||||
del self.status_data[user_id]
|
||||
|
||||
saved = self._db.get(__name__, "statuses", {})
|
||||
if user_id in saved:
|
||||
logger.info(f"Removing saved status for user {user_id}")
|
||||
del saved[user_id]
|
||||
self._db.set(__name__, "statuses", saved)
|
||||
|
||||
logger.info(f"Revert status completed for user {user_id}")
|
||||
|
||||
async def _schedule_revert_sleep(self, user_id: int, delay: float):
|
||||
"""Schedule status revert using asyncio.sleep"""
|
||||
try:
|
||||
logger.info(f"Scheduling revert for user {user_id} in {delay} seconds")
|
||||
await asyncio.sleep(delay)
|
||||
await self._revert_status(user_id)
|
||||
except asyncio.CancelledError:
|
||||
logger.info(f"Revert task cancelled for user {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in scheduled revert for user {user_id}: {e}")
|
||||
|
||||
async def _schedule_revert(self, user_id: int, data: Dict):
|
||||
"""Schedule status revert"""
|
||||
end_time = data.get("end_time", 0)
|
||||
delay = max(0, end_time - time.time())
|
||||
|
||||
self.status_data[user_id] = data
|
||||
|
||||
await self._schedule_revert_sleep(user_id, delay)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<время> <эмодзи/document_id> [финальный_эмодзи/document_id] - установить временный статус",
|
||||
en_doc="<time> <emoji/document_id> [final_emoji/document_id] - set temporary status",
|
||||
)
|
||||
async def setmoji(self, message: Message):
|
||||
"""Set timed emoji status"""
|
||||
args = utils.get_args_raw(message)
|
||||
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_time"])
|
||||
|
||||
parts = args.split(maxsplit=2)
|
||||
if len(parts) < 2:
|
||||
return await utils.answer(message, self.strings["no_emoji"])
|
||||
|
||||
time_str, initial_emoji = parts[0], parts[1]
|
||||
final_emoji = parts[2] if len(parts) > 2 else ""
|
||||
|
||||
td = self._parse_time(time_str)
|
||||
if not td:
|
||||
return await utils.answer(message, self.strings["invalid_time"])
|
||||
|
||||
if message.sender_id in self.scheduler_tasks:
|
||||
self.scheduler_tasks[message.sender_id].cancel()
|
||||
del self.scheduler_tasks[message.sender_id]
|
||||
|
||||
try:
|
||||
success, initial_doc_id = await self._set_emoji_status(
|
||||
initial_emoji, message=message
|
||||
)
|
||||
if not success:
|
||||
return await utils.answer(message, self.strings["no_premium"])
|
||||
except Exception as e:
|
||||
return await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
final_doc_id = None
|
||||
if final_emoji:
|
||||
try:
|
||||
final_doc_id = self._extract_document_id(final_emoji)
|
||||
if not final_doc_id:
|
||||
if message and len(parts) > 2:
|
||||
emoji_entities = [
|
||||
e
|
||||
for e in message.entities
|
||||
if isinstance(e, MessageEntityCustomEmoji)
|
||||
]
|
||||
if len(emoji_entities) >= 2:
|
||||
final_doc_id = emoji_entities[1].document_id
|
||||
|
||||
if not final_doc_id:
|
||||
try:
|
||||
test_msg = await self._client.send_message("me", final_emoji)
|
||||
final_doc_id = self._extract_document_id_from_entities(test_msg)
|
||||
await self._client.delete_messages("me", [test_msg.id])
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Could not get document_id for final emoji: {e}"
|
||||
)
|
||||
|
||||
if final_doc_id:
|
||||
logger.info(f"Final emoji document_id: {final_doc_id}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Could not resolve document_id for final emoji: {final_emoji}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting final emoji document_id: {e}")
|
||||
|
||||
end_time = time.time() + td.total_seconds()
|
||||
user_id = message.sender_id
|
||||
|
||||
data = {
|
||||
"initial_emoji": initial_emoji,
|
||||
"final_emoji": final_emoji,
|
||||
"initial_doc_id": initial_doc_id,
|
||||
"final_doc_id": final_doc_id,
|
||||
"end_time": end_time,
|
||||
"set_time": time.time(),
|
||||
}
|
||||
|
||||
self.status_data[user_id] = data
|
||||
|
||||
saved = self._db.get(__name__, "statuses", {})
|
||||
saved[user_id] = data
|
||||
self._db.set(__name__, "statuses", saved)
|
||||
|
||||
task = asyncio.create_task(
|
||||
self._schedule_revert_sleep(user_id, td.total_seconds())
|
||||
)
|
||||
self.scheduler_tasks[user_id] = task
|
||||
|
||||
end_dt = datetime.fromtimestamp(end_time)
|
||||
time_str = self._format_time(td)
|
||||
|
||||
logger.info(
|
||||
f"Display formatting - initial: '{initial_emoji}' (doc_id: {initial_doc_id}), final: '{final_emoji}' (doc_id: {final_doc_id})"
|
||||
)
|
||||
current_display = self._safe_emoji_display(initial_emoji, initial_doc_id)
|
||||
final_display = (
|
||||
self._safe_emoji_display(final_emoji, final_doc_id)
|
||||
if final_emoji
|
||||
else "❌ (удалить)"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Display results - current: '{current_display}', final: '{final_display}'"
|
||||
)
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["status_set"].format(
|
||||
current_display, final_display, time_str, f"{end_dt:%H:%M:%S}"
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="Показать текущий статус", en_doc="Show current status")
|
||||
async def showmoji(self, message: Message):
|
||||
"""Show current emoji status"""
|
||||
user_id = message.sender_id
|
||||
|
||||
if user_id not in self.status_data:
|
||||
return await utils.answer(message, self.strings["no_status"])
|
||||
|
||||
data = self.status_data[user_id]
|
||||
end_time = data.get("end_time", 0)
|
||||
initial_emoji = data.get("initial_emoji", "")
|
||||
final_emoji = data.get("final_emoji", "")
|
||||
initial_doc_id = data.get("initial_doc_id")
|
||||
final_doc_id = data.get("final_doc_id")
|
||||
|
||||
if end_time <= time.time():
|
||||
return await utils.answer(message, self.strings["no_status"])
|
||||
|
||||
end_dt = datetime.fromtimestamp(end_time)
|
||||
remaining = timedelta(seconds=end_time - time.time())
|
||||
remaining_str = self._format_time(remaining)
|
||||
|
||||
current_display = self._safe_emoji_display(initial_emoji, initial_doc_id)
|
||||
final_display = (
|
||||
self._safe_emoji_display(final_emoji, final_doc_id)
|
||||
if final_emoji
|
||||
else "❌ (удалить)"
|
||||
)
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["current_status"].format(
|
||||
current_display, final_display, f"{end_dt:%H:%M:%S}", remaining_str
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(ru_doc="Удалить статус", en_doc="Remove status")
|
||||
async def removemoji(self, message: Message):
|
||||
"""Remove emoji status"""
|
||||
user_id = message.sender_id
|
||||
|
||||
if user_id not in self.status_data:
|
||||
return await utils.answer(message, self.strings["no_status"])
|
||||
|
||||
if user_id in self.scheduler_tasks:
|
||||
self.scheduler_tasks[user_id].cancel()
|
||||
del self.scheduler_tasks[user_id]
|
||||
|
||||
await self._revert_status(user_id)
|
||||
await utils.answer(message, self.strings["status_removed"])
|
||||
|
||||
async def on_unload(self):
|
||||
"""Cancel all scheduled tasks on unload"""
|
||||
for task in self.scheduler_tasks.values():
|
||||
task.cancel()
|
||||
self.scheduler_tasks.clear()
|
||||
@@ -1,963 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: UserbotAvast
|
||||
# Description: A module for checking modules for security.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: UserbotAvast
|
||||
# scope: UserbotAvast 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import ast
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
import zlib
|
||||
|
||||
import requests
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import g4f
|
||||
|
||||
G4F_AVAILABLE = True
|
||||
except ImportError:
|
||||
G4F_AVAILABLE = False
|
||||
logger.warning("g4f is not installed. AI analysis will be disabled.")
|
||||
|
||||
|
||||
class SecurityAnalyzer:
|
||||
"""
|
||||
Продвинутый анализатор безопасности Python-кода с эвристическим анализом.
|
||||
"""
|
||||
|
||||
SECURITY_KEYWORDS = {
|
||||
"critical": [
|
||||
{
|
||||
"keyword": "DeleteAccountRequest",
|
||||
"description": "Удаление аккаунта",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "ResetAuthorizationRequest",
|
||||
"description": "Сброс авторизации",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "client.export_session_string",
|
||||
"description": "Экспорт сессии",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "edit_2fa",
|
||||
"description": "Изменение 2FA",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "os.system",
|
||||
"description": "Системные команды",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "subprocess.Popen",
|
||||
"description": "Внешние процессы",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "eval",
|
||||
"description": "Выполнение кода (eval)",
|
||||
"relevance": "Критическая",
|
||||
},
|
||||
{
|
||||
"keyword": "exec",
|
||||
"description": "Выполнение кода (exec)",
|
||||
"relevance": "Критическая",
|
||||
},
|
||||
{
|
||||
"keyword": "MessagePacker.append",
|
||||
"description": "Патч MessagePacker",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "MessagePacker.extend",
|
||||
"description": "Патч MessagePacker",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "Scrypt",
|
||||
"description": "Класс Scrypt (подозрительно)",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "socket.socket",
|
||||
"description": "Создание сокета",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "shell=True",
|
||||
"description": "Использование shell=True в subprocess",
|
||||
"relevance": "Критическая",
|
||||
},
|
||||
{
|
||||
"keyword": "codecs.decode",
|
||||
"description": "Декодирование с использованием codecs",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "pickle.loads",
|
||||
"description": "Десериализация (pickle)",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "marshal.loads",
|
||||
"description": "Десериализация (marshal)",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "__import__",
|
||||
"description": "Динамический импорт",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "ctypes.CDLL",
|
||||
"description": "Загрузка динамической библиотеки",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "create_connection",
|
||||
"description": "Установка соединения",
|
||||
"relevance": "Высокая",
|
||||
},
|
||||
{
|
||||
"keyword": "http.server",
|
||||
"description": "Запуск веб-сервера",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "asyncio.create_subprocess_shell",
|
||||
"description": "Асинхронный запуск процесса через shell",
|
||||
"relevance": "Критическая",
|
||||
},
|
||||
],
|
||||
"warning": [
|
||||
{
|
||||
"keyword": "requests",
|
||||
"description": "HTTP-запросы",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "aiohttp",
|
||||
"description": "Асинхронные HTTP-запросы",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "os.remove",
|
||||
"description": "Удаление файлов",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "os.mkdir",
|
||||
"description": "Создание каталогов",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "json.loads",
|
||||
"description": "Парсинг JSON",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "open(..., 'w')",
|
||||
"description": "Открытие файла на запись",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "open(..., 'a')",
|
||||
"description": "Открытие файла на добавление",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "telnetlib.Telnet",
|
||||
"description": "Telnet соединение",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "ftplib.FTP",
|
||||
"description": "FTP соединение",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "shutil.move",
|
||||
"description": "Перемещение файлов",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "shutil.copy",
|
||||
"description": "Копирование файлов",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "threading.Thread",
|
||||
"description": "Создание потока",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "multiprocessing.Process",
|
||||
"description": "Создание процесса",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "queue.Queue",
|
||||
"description": "Использование очереди",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "subprocess.check_output",
|
||||
"description": "Запуск процесса с захватом вывода",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "subprocess.run",
|
||||
"description": "Запуск процесса",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
{
|
||||
"keyword": "codecs.encode",
|
||||
"description": "Кодирование с использованием codecs",
|
||||
"relevance": "Средняя",
|
||||
},
|
||||
],
|
||||
"info": [
|
||||
{
|
||||
"keyword": "telethon",
|
||||
"description": "Использование Telethon",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "pyrogram",
|
||||
"description": "Использование Pyrogram",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "import",
|
||||
"description": "Импорт модулей",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "print",
|
||||
"description": "Вывод в консоль",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
{
|
||||
"keyword": "logging.info",
|
||||
"description": "Логирование",
|
||||
"relevance": "Низкая",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, ai_enabled: bool = False):
|
||||
"""Инициализация анализатора."""
|
||||
self.results = {"critical": [], "warning": [], "info": []}
|
||||
self.reported_issues = set()
|
||||
self.code_lines = []
|
||||
self.is_decoded = False
|
||||
self.ai_enabled = ai_enabled
|
||||
|
||||
def reset(self):
|
||||
"""Сброс результатов анализа."""
|
||||
self.results = {"critical": [], "warning": [], "info": []}
|
||||
self.reported_issues = set()
|
||||
self.code_lines = []
|
||||
self.is_decoded = False
|
||||
|
||||
async def analyze(self, code: str, strings: dict) -> str:
|
||||
"""
|
||||
Выполняет анализ предоставленного Python-кода.
|
||||
|
||||
Args:
|
||||
code: Python-код для анализа.
|
||||
strings: Словарь строк для локализации.
|
||||
|
||||
Returns:
|
||||
Форматированный отчет об анализе.
|
||||
"""
|
||||
self.reset()
|
||||
original_code = code
|
||||
|
||||
try:
|
||||
code = self._try_decode(code)
|
||||
self.is_decoded = True
|
||||
except Exception:
|
||||
logger.warning("Не удалось расшифровать код, анализ как есть.")
|
||||
|
||||
self.code_lines = code.splitlines()
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
self._visit_tree(tree)
|
||||
self._heuristic_analysis(code, tree)
|
||||
|
||||
if self.ai_enabled and G4F_AVAILABLE:
|
||||
ai_analysis_result = await self._ai_analysis(code)
|
||||
if ai_analysis_result:
|
||||
self.results["critical"].append(
|
||||
{
|
||||
"keyword": "AI Analysis",
|
||||
"description": ai_analysis_result,
|
||||
"relevance": "Критическая",
|
||||
"line": 0,
|
||||
"col": 0,
|
||||
}
|
||||
)
|
||||
|
||||
except SyntaxError as e:
|
||||
logger.error(f"Ошибка синтаксиса в коде: {e}")
|
||||
return strings["syntax_error"].format(error=e)
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error during analysis")
|
||||
return strings["syntax_error"].format(error=str(e))
|
||||
|
||||
return self._format_report(strings, original_code)
|
||||
|
||||
async def _ai_analysis(self, code: str) -> str or None:
|
||||
"""
|
||||
Использует g4f для анализа кода и выявления потенциальных угроз.
|
||||
"""
|
||||
try:
|
||||
prompt = f"Проанализируйте следующий Python-код на предмет потенциальных угроз безопасности, уязвимостей и вредоносных действий. Предоставьте подробное объяснение, если что-то будет обнаружено:\n\n{code}"
|
||||
response = await utils.run_sync(
|
||||
g4f.ChatCompletion.create,
|
||||
model=g4f.models.default,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
return str(response)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при анализе с помощью g4f: {e}")
|
||||
return None
|
||||
|
||||
def _try_decode(self, code):
|
||||
"""Попытка расшифровать base64 + zlib код."""
|
||||
if re.search(r"__import__\('zlib'\).decompress\(", code) and re.search(
|
||||
r"__import__\('base64'\).b64decode\(", code
|
||||
):
|
||||
try:
|
||||
match = re.search(r"b'([A-Za-z0-9+/=]+)'", code)
|
||||
if match:
|
||||
encoded_string = match.group(1)
|
||||
decoded_code = self._decode_base64_zlib(encoded_string)
|
||||
logger.info("Код успешно расшифрован.")
|
||||
return decoded_code
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при расшифровке кода: {e}")
|
||||
raise
|
||||
return code
|
||||
|
||||
def _decode_base64_zlib(self, encoded_string):
|
||||
"""Расшифровывает base64 + zlib код."""
|
||||
try:
|
||||
decoded_bytes = base64.b64decode(encoded_string)
|
||||
decompressed_bytes = zlib.decompress(decoded_bytes)
|
||||
return decompressed_bytes.decode("utf-8")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при расшифровке base64+zlib: {e}")
|
||||
raise
|
||||
|
||||
def _get_line_from_code(self, lineno):
|
||||
"""Получает строку кода по номеру строки."""
|
||||
try:
|
||||
return self.code_lines[lineno - 1]
|
||||
except IndexError:
|
||||
return ""
|
||||
|
||||
def _visit_tree(self, tree):
|
||||
"""Рекурсивно обходит AST-дерево."""
|
||||
for node in ast.walk(tree):
|
||||
self._analyze_node(node)
|
||||
|
||||
def _analyze_node(self, node):
|
||||
"""Анализирует отдельный узел AST."""
|
||||
if isinstance(node, ast.Name):
|
||||
self._check_keyword(node.id, node)
|
||||
elif isinstance(node, ast.Call):
|
||||
self._check_call(node)
|
||||
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
||||
self._check_import(node)
|
||||
elif isinstance(node, ast.FunctionDef):
|
||||
self._check_function_def(node)
|
||||
elif isinstance(node, ast.ClassDef):
|
||||
self._check_class_def(node)
|
||||
elif isinstance(node, ast.Assign):
|
||||
self._check_assign(node)
|
||||
|
||||
def _check_keyword(self, keyword, node):
|
||||
"""Проверяет ключевые слова."""
|
||||
for severity, keywords in self.SECURITY_KEYWORDS.items():
|
||||
for item in keywords:
|
||||
if item["keyword"] == keyword:
|
||||
issue_key = (
|
||||
item["keyword"],
|
||||
node.lineno,
|
||||
node.col_offset,
|
||||
)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results[severity].append(
|
||||
{
|
||||
"keyword": item["keyword"],
|
||||
"description": item["description"],
|
||||
"relevance": item["relevance"],
|
||||
"line": node.lineno,
|
||||
"col": node.col_offset,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
def _check_call(self, node):
|
||||
"""Анализирует вызовы функций."""
|
||||
if isinstance(node.func, ast.Name):
|
||||
self._check_keyword(node.func.id, node)
|
||||
elif isinstance(node.func, ast.Attribute):
|
||||
full_attr = ""
|
||||
if isinstance(node.func.value, ast.Name):
|
||||
full_attr = node.func.value.id + "." + node.func.attr
|
||||
self._check_keyword(full_attr, node)
|
||||
else:
|
||||
self._check_keyword(node.func.attr, node)
|
||||
elif isinstance(node.func, ast.Subscript):
|
||||
if isinstance(node.func.value, ast.Attribute):
|
||||
full_attr = ""
|
||||
if isinstance(node.func.value.value, ast.Name):
|
||||
full_attr = node.func.value.value.id + "." + node.func.value.attr
|
||||
self._check_keyword(full_attr, node)
|
||||
|
||||
def _check_function_def(self, node):
|
||||
self._check_keyword(node.name, node)
|
||||
|
||||
def _check_class_def(self, node):
|
||||
self._check_keyword(node.name, node)
|
||||
|
||||
def _check_import(self, node):
|
||||
"""Анализирует импорты."""
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
self._check_keyword(alias.name, node)
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
self._check_keyword(node.module, node)
|
||||
for alias in node.names:
|
||||
self._check_keyword(alias.name, node)
|
||||
|
||||
def _check_assign(self, node):
|
||||
"""Анализирует присваивания."""
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name):
|
||||
self._check_keyword(target.id, node)
|
||||
|
||||
def _heuristic_analysis(self, code: str, tree: ast.AST):
|
||||
"""
|
||||
Эвристический анализ для обнаружения подозрительного кода.
|
||||
"""
|
||||
self._check_obfuscation(code, tree)
|
||||
self._check_dynamic_code_generation(code, tree)
|
||||
self._check_url_patterns(code)
|
||||
self._check_api_abuse(tree)
|
||||
self._check_reverse_shell(code)
|
||||
self._check_file_operations(code)
|
||||
|
||||
def _check_obfuscation(self, code: str, tree: ast.AST):
|
||||
"""Обнаружение обфускации кода."""
|
||||
if len(re.findall(r"[A-Za-z0-9+/]{30,}", code)) > 2:
|
||||
issue_key = ("Base64", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": "Base64",
|
||||
"description": "Подозрительные строки Base64",
|
||||
"relevance": "Средняя",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
if "zlib.decompress" in code:
|
||||
issue_key = ("zlib.decompress", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": "zlib.decompress",
|
||||
"description": "Использование zlib декомпрессии",
|
||||
"relevance": "Средняя",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.Call)):
|
||||
if isinstance(node.func, ast.Name) and node.func.id in ("eval", "exec"):
|
||||
if len(node.args) > 0 and isinstance(node.args[0], ast.Str):
|
||||
obfuscated_string = node.args[0].s
|
||||
if (
|
||||
len(re.findall(r"[A-Za-z0-9+/]{30,}", obfuscated_string))
|
||||
> 0
|
||||
):
|
||||
issue_key = (
|
||||
"eval/exec+Base64",
|
||||
node.lineno,
|
||||
node.col_offset,
|
||||
)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["critical"].append(
|
||||
{
|
||||
"keyword": "eval/exec+Base64",
|
||||
"description": "eval/exec с обфускацией Base64",
|
||||
"relevance": "Критическая",
|
||||
"line": node.lineno,
|
||||
"col": node.col_offset,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
elif isinstance(node.func, ast.Name) and node.func.id in (
|
||||
"eval",
|
||||
"exec",
|
||||
):
|
||||
if len(node.args) > 0 and isinstance(node.args[0], ast.Name):
|
||||
issue_key = ("eval/exec+Variable", node.lineno, node.col_offset)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["critical"].append(
|
||||
{
|
||||
"keyword": "eval/exec+Variable",
|
||||
"description": "eval/exec с переменной",
|
||||
"relevance": "Критическая",
|
||||
"line": node.lineno,
|
||||
"col": node.col_offset,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
hash_functions = ["md5", "sha1", "sha256", "sha512"]
|
||||
for hash_func in hash_functions:
|
||||
if f"hashlib.{hash_func}" in code:
|
||||
issue_key = (f"hashlib.{hash_func}", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["info"].append(
|
||||
{
|
||||
"keyword": f"hashlib.{hash_func}",
|
||||
"description": f"Использование {hash_func} хеширования",
|
||||
"relevance": "Низкая",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
if any(
|
||||
x in code
|
||||
for x in [
|
||||
"hashlib.md5(password.encode()).hexdigest()",
|
||||
"hashlib.sha256(password.encode()).hexdigest()",
|
||||
]
|
||||
):
|
||||
issue_key = ("Weak Hashing", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": "Weak Hashing",
|
||||
"description": "Использование хеширования без соли",
|
||||
"relevance": "Средняя",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
def _check_dynamic_code_generation(self, code, tree: ast.AST):
|
||||
"""Обнаружение динамической генерации кода."""
|
||||
if "compile(" in code:
|
||||
issue_key = ("compile", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": "compile",
|
||||
"description": "Использование compile() для генерации кода",
|
||||
"relevance": "Средняя",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if (
|
||||
isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Name)
|
||||
and node.func.id == "type"
|
||||
):
|
||||
if (
|
||||
len(node.args) == 3
|
||||
and isinstance(node.args[0], ast.Str)
|
||||
and isinstance(node.args[1], ast.Tuple)
|
||||
and isinstance(node.args[2], ast.Dict)
|
||||
):
|
||||
issue_key = ("type() class", node.lineno, node.col_offset)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": "type() class",
|
||||
"description": "Динамическое создание классов через type()",
|
||||
"relevance": "Средняя",
|
||||
"line": node.lineno,
|
||||
"col": node.col_offset,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
def _check_url_patterns(self, code: str):
|
||||
"""Обнаружение подозрительных URL-паттернов."""
|
||||
short_url_domains = [
|
||||
"bit.ly",
|
||||
"goo.gl",
|
||||
"t.co",
|
||||
"tinyurl.com",
|
||||
"is.gd",
|
||||
"ow.ly",
|
||||
"github.com",
|
||||
"raw.githubusercontent.com",
|
||||
]
|
||||
for domain in short_url_domains:
|
||||
if domain in code:
|
||||
issue_key = (f"Short URL ({domain})", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
line = self._get_line_from_code(1)
|
||||
match = re.search(r"(https?://\S+)", line)
|
||||
url = (
|
||||
match.group(1)
|
||||
if match
|
||||
else f"Не удалось извлечь URL ({domain})"
|
||||
)
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": f"Short URL ({domain})",
|
||||
"description": f"Обнаружен сокращенный URL: {url}",
|
||||
"relevance": "Низкая",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
webhook_patterns = ["discord.com/api/webhooks", "api.telegram.org/bot"]
|
||||
for pattern in webhook_patterns:
|
||||
if pattern in code:
|
||||
issue_key = (f"Webhook ({pattern})", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
line = self._get_line_from_code(1)
|
||||
match = re.search(pattern, line)
|
||||
url = (
|
||||
match.group(0)
|
||||
if match
|
||||
else f"Не удалось извлечь Webhook ({pattern})"
|
||||
)
|
||||
|
||||
self.results["critical"].append(
|
||||
{
|
||||
"keyword": f"Webhook ({pattern})",
|
||||
"description": f"Обнаружен Webhook: {url}",
|
||||
"relevance": "Критическая",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
def _check_api_abuse(self, tree: ast.AST):
|
||||
"""Обнаружение потенциального злоупотребления Telegram API."""
|
||||
send_methods = ["send_message", "send_file", "send_photo"]
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.For):
|
||||
for send_method in send_methods:
|
||||
if send_method in ast.unparse(node):
|
||||
issue_key = (
|
||||
f"Mass {send_method}",
|
||||
node.lineno,
|
||||
node.col_offset,
|
||||
)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": f"Mass {send_method}",
|
||||
"description": f"Подозрение на массовую рассылку ({send_method})",
|
||||
"relevance": "Средняя",
|
||||
"line": node.lineno,
|
||||
"col": node.col_offset,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
if "time.sleep(" in ast.unparse(tree):
|
||||
sleep_calls = re.findall(r"time\.sleep\((.*?)\)", ast.unparse(tree))
|
||||
for sleep_time in sleep_calls:
|
||||
try:
|
||||
sleep_value = float(sleep_time)
|
||||
if sleep_value < 1:
|
||||
issue_key = ("Short Sleep Time", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": "Short Sleep Time",
|
||||
"description": "Обнаружена короткая задержка (менее 1 секунды)",
|
||||
"relevance": "Средняя",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def _check_reverse_shell(self, code: str):
|
||||
"""Обнаружение попыток создания обратного шелла."""
|
||||
try:
|
||||
reverse_shell_patterns = [
|
||||
r"socket\.socket\(\s*socket\.AF_INET",
|
||||
r"os\.dup2\(",
|
||||
r"subprocess\.Popen\(\s*\[.+?\]\s*,\s*shell=True",
|
||||
r"/bin/bash -i",
|
||||
r"/bin/sh -i",
|
||||
r"nc -e /bin/bash",
|
||||
r"nc -e /bin/sh",
|
||||
r"> /dev/tcp/",
|
||||
r"python -c 'import socket,subprocess,os;s=socket.socket",
|
||||
r"python3 -c 'import socket,subprocess,os;s=socket.socket",
|
||||
]
|
||||
for pattern in reverse_shell_patterns:
|
||||
if re.search(pattern, code):
|
||||
issue_key = ("Reverse Shell", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["critical"].append(
|
||||
{
|
||||
"keyword": "Reverse Shell",
|
||||
"description": "Обнаружена попытка создания обратного шелла",
|
||||
"relevance": "Критическая",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in _check_reverse_shell: {e}")
|
||||
|
||||
def _check_file_operations(self, code: str):
|
||||
"""Обнаружение потенциально опасных операций с файлами."""
|
||||
dangerous_file_paths = [
|
||||
"/etc/passwd",
|
||||
"/etc/shadow",
|
||||
"/etc/hosts",
|
||||
"/etc/sudoers",
|
||||
]
|
||||
for file_path in dangerous_file_paths:
|
||||
if file_path in code:
|
||||
issue_key = ("File Override", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["critical"].append(
|
||||
{
|
||||
"keyword": "File Override",
|
||||
"description": f"Попытка записи в критический файл: {file_path}",
|
||||
"relevance": "Критическая",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
if "shutil.rmtree" in code:
|
||||
issue_key = ("Recursive Delete", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": "Recursive Delete",
|
||||
"description": "Обнаружено рекурсивное удаление каталога",
|
||||
"relevance": "Средняя",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
executable_extensions = [".py", ".sh", ".bat", ".exe"]
|
||||
for ext in executable_extensions:
|
||||
if f"open(..., '{ext}'" in code or f"open(... + '{ext}'" in code:
|
||||
issue_key = ("Executable File Creation", 1, 1)
|
||||
if issue_key not in self.reported_issues:
|
||||
self.results["warning"].append(
|
||||
{
|
||||
"keyword": "Executable File Creation",
|
||||
"description": f"Обнаружено создание файла с расширением {ext}",
|
||||
"relevance": "Средняя",
|
||||
"line": 1,
|
||||
"col": 1,
|
||||
}
|
||||
)
|
||||
self.reported_issues.add(issue_key)
|
||||
|
||||
def _format_report(self, strings: dict, original_code: str) -> str:
|
||||
"""Форматирует отчет об анализе."""
|
||||
report = strings["report_header"]
|
||||
|
||||
if self.is_decoded:
|
||||
report += "<b>⚠️ Код был расшифрован перед анализом.</b>\n\n"
|
||||
else:
|
||||
report += "<b>⚠️ Анализ проводился над исходным кодом, расшифровка не удалась.</b>\n\n"
|
||||
|
||||
total_issues = 0
|
||||
for severity, issues in self.results.items():
|
||||
if issues:
|
||||
report += strings[f"{severity}_header"]
|
||||
total_issues += len(issues)
|
||||
for issue in issues:
|
||||
report += strings["issue_format"].format(
|
||||
keyword=issue["keyword"],
|
||||
description=issue["description"],
|
||||
relevance=issue["relevance"],
|
||||
line=issue["line"],
|
||||
col=issue["col"],
|
||||
)
|
||||
report += "\n"
|
||||
|
||||
if total_issues == 0:
|
||||
report += strings["no_issues"]
|
||||
else:
|
||||
report += strings["report_footer"].format(count=total_issues)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
@loader.tds
|
||||
class UserbotAvast(loader.Module):
|
||||
"""A module for checking modules for security."""
|
||||
|
||||
strings = {
|
||||
"name": "UserbotAvast",
|
||||
"cfg_ai_enabled": "Включить анализ с помощью AI (g4f)",
|
||||
"cfg_lingva_url": "Анализирует Python-код модуля на предмет потенциальных угроз безопасности, включая обфускацию и эвристические признаки.",
|
||||
"report_header": "<b>🛡️ Отчет об анализе безопасности модуля:</b>\n\n",
|
||||
"critical_header": "<b>🔴 Критические угрозы:</b>\n",
|
||||
"warning_header": "<b>🟠 Предупреждения:</b>\n",
|
||||
"info_header": "<b>🔵 Информация:</b>\n",
|
||||
"issue_format": " - ⚠️ <code>{keyword}</code>: {description} (Важность: {relevance}, Строка: {line}, Позиция: {col})\n",
|
||||
"no_issues": "✅ Не обнаружено проблем безопасности.\n",
|
||||
"report_footer": "\nВсего обнаружено {count} проблем.\n",
|
||||
"syntax_error": "❌ Ошибка синтаксиса в коде: {error}\n",
|
||||
"loading": "⏳ Запуск анализатора безопасности...",
|
||||
"no_module": "⚠️ Не удалось получить код модуля. Убедитесь, что ссылка верна или прикрепите файл к сообщению.",
|
||||
"decoding_error": "⚠️ Обнаружен зашифрованный код, но не удалось его расшифровать.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"cfg_ai_enabled": "Включить анализ с помощью AI (g4f)",
|
||||
"cfg_lingva_url": "Анализирует Python-код модуля на предмет потенциальных угроз безопасности, включая обфускацию и эвристические признаки.",
|
||||
"report_header": "<b>🛡️ Отчет об анализе безопасности модуля:</b>\n\n",
|
||||
"critical_header": "<b>🔴 Критические угрозы:</b>\n",
|
||||
"warning_header": "<b>🟠 Предупреждения:</b>\n",
|
||||
"info_header": "<b>🔵 Информация:</b>\n",
|
||||
"issue_format": " - ⚠️ <code>{keyword}</code>: {description} (Важность: {relevance}, Строка: {line}, Позиция: {col})\n",
|
||||
"no_issues": "✅ Не обнаружено проблем безопасности.\n",
|
||||
"report_footer": "\nВсего обнаружено {count} проблем.\n",
|
||||
"syntax_error": "❌ Ошибка синтаксиса в коде: {error}\n",
|
||||
"loading": "⏳ Запуск анализатора безопасности...",
|
||||
"no_module": "⚠️ Не удалось получить код модуля. Убедитесь, что ссылка верна или прикрепите файл к сообщению.",
|
||||
"decoding_error": "⚠️ Обнаружен зашифрованный код, но не удалось его расшифровать.",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"ai_enabled",
|
||||
False,
|
||||
lambda: self.strings["cfg_ai_enabled"],
|
||||
validator=loader.validators.Boolean(),
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
"""Вызывается при готовности клиента."""
|
||||
self.client = client
|
||||
self.db = db
|
||||
self.security_analyzer = SecurityAnalyzer(self.config["ai_enabled"])
|
||||
|
||||
@loader.unrestricted
|
||||
@loader.ratelimit
|
||||
async def checkmodcmd(self, message):
|
||||
"""
|
||||
[module_link] или [reply file] или [send file] - выполняет проверку модуля на безопасность.
|
||||
"""
|
||||
await utils.answer(message, self.strings["loading"])
|
||||
args = utils.get_args_raw(message)
|
||||
code = None
|
||||
|
||||
if args:
|
||||
code = await self._get_code_from_url(args)
|
||||
|
||||
if not code:
|
||||
code = await self._get_code_from_message(message)
|
||||
|
||||
if not code:
|
||||
await utils.answer(message, self.strings["no_module"])
|
||||
return
|
||||
|
||||
try:
|
||||
result = await self.security_analyzer.analyze(code, self.strings)
|
||||
await utils.answer(message, result)
|
||||
except Exception as e:
|
||||
logger.exception("Error during analysis")
|
||||
await utils.answer(message, f"An error occurred during analysis: {e}")
|
||||
|
||||
async def _get_code_from_url(self, url: str) -> str or None:
|
||||
"""Получает код модуля по URL."""
|
||||
try:
|
||||
response = await utils.run_sync(requests.get, url)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Ошибка при получении кода из URL: {e}")
|
||||
return None
|
||||
|
||||
async def _get_code_from_message(self, message) -> str or None:
|
||||
"""Получает код модуля из прикрепленного файла или ответа на сообщение."""
|
||||
try:
|
||||
if message.media:
|
||||
code = (await self.client.download_file(message.media, bytes)).decode(
|
||||
"utf-8"
|
||||
)
|
||||
return code
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if reply and reply.media:
|
||||
code = (await self.client.download_file(reply.media, bytes)).decode(
|
||||
"utf-8"
|
||||
)
|
||||
return code
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении кода из сообщения: {e}")
|
||||
return None
|
||||
@@ -1,132 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Video2GIF
|
||||
# Description: Converts video to GIF
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Video2GIF
|
||||
# scope: Video2GIF 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class Video2GIFMod(loader.Module):
|
||||
"""Convert video to high quality GIF"""
|
||||
|
||||
strings = {
|
||||
"name": "Video2GIF",
|
||||
"success": "✅ GIF created",
|
||||
"error": "❌ Conversion failed",
|
||||
"no_video": "❌ Reply to a video",
|
||||
"no_ffmpeg": "❌ FFmpeg not installed. Install: apt install ffmpeg",
|
||||
"processing": "🔄 Processing video...",
|
||||
"compressing": "📦 Optimizing GIF...",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"success": "✅ GIF создан",
|
||||
"error": "❌ Ошибка конвертации",
|
||||
"no_video": "❌ Ответьте на видео",
|
||||
"no_ffmpeg": "❌ FFmpeg не установлен. Установите: apt install ffmpeg",
|
||||
"processing": "🔄 Обрабатываю видео...",
|
||||
"compressing": "📦 Оптимизирую GIF...",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._ffmpeg_check = None
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
self._db = db
|
||||
self._check_ffmpeg()
|
||||
|
||||
def _check_ffmpeg(self):
|
||||
self._ffmpeg_check = shutil.which("ffmpeg") is not None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[ответ] [fps] [ширина] - конвертировать видео в GIF",
|
||||
en_doc="[reply] [fps] [width] - convert video to GIF",
|
||||
)
|
||||
async def gifc(self, message):
|
||||
"""Convert video to GIF"""
|
||||
if not self._ffmpeg_check:
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.video:
|
||||
return await utils.answer(message, self.strings["no_video"])
|
||||
|
||||
args = utils.get_args_raw(message).split()
|
||||
fps = 15 if len(args) < 1 else min(int(args[0]), 30)
|
||||
width = 480 if len(args) < 2 else min(int(args[1]), 1024)
|
||||
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
gif_path = await self._convert_to_gif(reply, fps, width)
|
||||
|
||||
await self._client.send_file(
|
||||
message.chat_id,
|
||||
gif_path,
|
||||
caption=self.strings["success"],
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
os.remove(gif_path)
|
||||
await msg.delete()
|
||||
|
||||
except Exception:
|
||||
await utils.answer(message, self.strings["error"])
|
||||
|
||||
async def _convert_to_gif(self, reply, fps: int, width: int) -> str:
|
||||
"""Convert video to optimized GIF"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
video_path = os.path.join(tmpdir, "video.mp4")
|
||||
gif_path = os.path.join(tmpdir, "output.gif")
|
||||
|
||||
await reply.download_media(video_path)
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
video_path,
|
||||
"-vf",
|
||||
f"fps={fps},scale={width}:-1:flags=lanczos",
|
||||
"-lavfi",
|
||||
"[0:v]split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
|
||||
"-y",
|
||||
gif_path,
|
||||
]
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(*cmd)
|
||||
await proc.communicate()
|
||||
|
||||
return gif_path
|
||||
@@ -1,240 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: VirusTotal
|
||||
# Description: Checks files for viruses using VirusTotal
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api VirusTotal
|
||||
# scope: Api VirusTotal 0.0.1
|
||||
# requires: json aiohttp tempfile
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class VirusTotalMod(loader.Module):
|
||||
"""Professional file scanning with VirusTotal"""
|
||||
|
||||
strings = {
|
||||
"name": "VirusTotal",
|
||||
"no_file": "🚫 Reply to a file",
|
||||
"downloading": "📥 Downloading file...",
|
||||
"uploading": "📤 Uploading to VirusTotal...",
|
||||
"scanning": "🔍 Scanning in progress...",
|
||||
"waiting": "⏳ Waiting for analysis...",
|
||||
"no_key": "🚫 Set VirusTotal API key in config",
|
||||
"error": "❌ Error during scan",
|
||||
"size_limit": "📁 File exceeds 32MB limit",
|
||||
"timeout": "⏰ Scan timeout",
|
||||
"clean": "✅ File is clean",
|
||||
"suspicious": "⚠️ Suspicious file",
|
||||
"malicious": "⛔ Malicious file",
|
||||
"view_report": "📊 View full report",
|
||||
"close": "❌ Close",
|
||||
"engines": "Scan engines",
|
||||
"detections": "Detections",
|
||||
"status": "Status",
|
||||
"completed": "Completed",
|
||||
"queued": "Queued",
|
||||
"scan_date": "Scan date",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_file": "🚫 Ответьте на файл",
|
||||
"downloading": "📥 Скачиваю файл...",
|
||||
"uploading": "📤 Загружаю на VirusTotal...",
|
||||
"scanning": "🔍 Сканирую...",
|
||||
"waiting": "⏳ Жду анализа...",
|
||||
"no_key": "🚫 Укажите API ключ в конфиге",
|
||||
"error": "❌ Ошибка при сканировании",
|
||||
"size_limit": "📁 Файл больше 32МБ",
|
||||
"timeout": "⏰ Таймаут сканирования",
|
||||
"clean": "✅ Файл чистый",
|
||||
"suspicious": "⚠️ Подозрительный файл",
|
||||
"malicious": "⛔ Вредоносный файл",
|
||||
"view_report": "📊 Полный отчёт",
|
||||
"close": "❌ Закрыть",
|
||||
"engines": "Антивирусов",
|
||||
"detections": "Обнаружено",
|
||||
"status": "Статус",
|
||||
"completed": "Завершён",
|
||||
"queued": "В очереди",
|
||||
"scan_date": "Дата сканирования",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"api_key",
|
||||
None,
|
||||
"VirusTotal API key from https://virustotal.com",
|
||||
validator=loader.validators.Hidden(),
|
||||
)
|
||||
)
|
||||
self.session: Optional[aiohttp.ClientSession] = None
|
||||
self.MAX_SIZE = 32 * 1024 * 1024 # 32MB
|
||||
self.TIMEOUT = 120 # seconds
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
self._db = db
|
||||
|
||||
async def on_unload(self):
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
def _get_session(self) -> aiohttp.ClientSession:
|
||||
"""Get or create aiohttp session with API key"""
|
||||
if not self.session:
|
||||
headers = {"x-apikey": self.config["api_key"]}
|
||||
self.session = aiohttp.ClientSession(headers=headers)
|
||||
return self.session
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[ответ] - просканировать файл через VirusTotal",
|
||||
en_doc="[reply] - scan file with VirusTotal",
|
||||
)
|
||||
async def vt(self, message):
|
||||
"""Scan file with VirusTotal"""
|
||||
api_key = self.config["api_key"]
|
||||
if not api_key:
|
||||
return await utils.answer(message, self.strings["no_key"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.document:
|
||||
return await utils.answer(message, self.strings["no_file"])
|
||||
|
||||
async with self._get_session() as session:
|
||||
try:
|
||||
msg = await utils.answer(message, self.strings["downloading"])
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
file_path = os.path.join(tmpdir, reply.file.name)
|
||||
await reply.download_media(file_path)
|
||||
|
||||
file_size = os.path.getsize(file_path)
|
||||
if file_size > self.MAX_SIZE:
|
||||
return await msg.edit(self.strings["size_limit"])
|
||||
|
||||
await msg.edit(self.strings["uploading"])
|
||||
analysis_id = await self._upload_file(session, file_path)
|
||||
|
||||
await msg.edit(self.strings["waiting"])
|
||||
result = await self._wait_for_analysis(session, analysis_id)
|
||||
|
||||
await self._show_results(msg, analysis_id, result)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
await utils.answer(message, self.strings["timeout"])
|
||||
except Exception as e:
|
||||
error_text = f"{self.strings['error']}: {str(e)[:100]}"
|
||||
await utils.answer(message, error_text)
|
||||
|
||||
async def _upload_file(self, session: aiohttp.ClientSession, path: str) -> str:
|
||||
"""Upload file to VirusTotal and return analysis ID"""
|
||||
with open(path, "rb") as f:
|
||||
form = aiohttp.FormData()
|
||||
form.add_field("file", f, filename=os.path.basename(path))
|
||||
|
||||
async with session.post(
|
||||
"https://www.virustotal.com/api/v3/files", data=form
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
return data["data"]["id"]
|
||||
|
||||
async def _wait_for_analysis(
|
||||
self, session: aiohttp.ClientSession, analysis_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Poll analysis results until completion"""
|
||||
url = f"https://www.virustotal.com/api/v3/analyses/{analysis_id}"
|
||||
|
||||
for _ in range(20):
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
|
||||
status = data["data"]["attributes"]["status"]
|
||||
if status == "completed":
|
||||
return data
|
||||
|
||||
await asyncio.sleep(3)
|
||||
|
||||
raise asyncio.TimeoutError()
|
||||
|
||||
async def _show_results(self, message, analysis_id: str, result: Dict[str, Any]):
|
||||
"""Display scan results in inline form"""
|
||||
stats = result["data"]["attributes"]["stats"]
|
||||
date = result["data"]["attributes"]["date"]
|
||||
|
||||
malicious = stats.get("malicious", 0)
|
||||
suspicious = stats.get("suspicious", 0)
|
||||
undetected = stats.get("undetected", 0)
|
||||
harmless = stats.get("harmless", 0)
|
||||
total = malicious + suspicious + undetected + harmless
|
||||
|
||||
if malicious > 0:
|
||||
verdict = self.strings["malicious"]
|
||||
emoji = "⛔"
|
||||
elif suspicious > 0:
|
||||
verdict = self.strings["suspicious"]
|
||||
emoji = "⚠️"
|
||||
else:
|
||||
verdict = self.strings["clean"]
|
||||
emoji = "✅"
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
scan_date = datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
text = (
|
||||
f"{emoji} <b>VirusTotal Scan Results</b>\n\n"
|
||||
f"<b>{self.strings['status']}:</b> {verdict}\n"
|
||||
f"<b>{self.strings['detections']}:</b> {malicious}\n"
|
||||
f"<b>{self.strings['engines']}:</b> {total}\n"
|
||||
f"<b>{self.strings['scan_date']}:</b> {scan_date}\n\n"
|
||||
f"<code>Malicious: {malicious}/{total}</code>\n"
|
||||
f"<code>Suspicious: {suspicious}/{total}</code>\n"
|
||||
f"<code>Harmless: {harmless}/{total}</code>\n"
|
||||
f"<code>Undetected: {undetected}/{total}</code>"
|
||||
)
|
||||
|
||||
vt_url = f"https://www.virustotal.com/gui/file-analysis/{analysis_id}"
|
||||
|
||||
await self.inline.form(
|
||||
text=text,
|
||||
message=message,
|
||||
reply_markup=[
|
||||
[{"text": f"🔗 {self.strings['view_report']}", "url": vt_url}],
|
||||
[{"text": self.strings["close"], "action": "close"}],
|
||||
],
|
||||
ttl=300, # 5 minutes timeout
|
||||
)
|
||||
@@ -1,118 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: VoiceDL
|
||||
# Description: Voice Downloader module
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: VoiceDL
|
||||
# scope: VoiceDL 0.0.1
|
||||
# requires: tempfile
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class VoiceDLMod(loader.Module):
|
||||
"""Download voice messages as MP3"""
|
||||
|
||||
strings = {
|
||||
"name": "VoiceDL",
|
||||
"success": "✅ Voice downloaded as MP3",
|
||||
"error": "❌ Error downloading voice",
|
||||
"no_voice": "❌ Reply to a voice message",
|
||||
"no_ffmpeg": "❌ FFmpeg not found. Install: apt install ffmpeg",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"success": "✅ Голосовое скачано как MP3",
|
||||
"error": "❌ Ошибка скачивания",
|
||||
"no_voice": "❌ Ответьте на голосовое",
|
||||
"no_ffmpeg": "❌ FFmpeg не установлен. Установите: apt install ffmpeg",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._ffmpeg_check = None
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
self._db = db
|
||||
self._check_ffmpeg()
|
||||
|
||||
def _check_ffmpeg(self):
|
||||
self._ffmpeg_check = shutil.which("ffmpeg") is not None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[ответ] - скачать голосовое как MP3",
|
||||
en_doc="[reply] - download voice as MP3",
|
||||
)
|
||||
async def voicedl(self, message):
|
||||
if not self._ffmpeg_check:
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.voice:
|
||||
return await utils.answer(message, self.strings["no_voice"])
|
||||
|
||||
await self._process_voice(message, reply)
|
||||
|
||||
async def _process_voice(self, message, reply):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
try:
|
||||
ogg_path = os.path.join(tmpdir, "voice.ogg")
|
||||
mp3_path = os.path.join(tmpdir, "voice.mp3")
|
||||
|
||||
await reply.download_media(file=ogg_path)
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
ogg_path,
|
||||
"-codec:a",
|
||||
"libmp3lame",
|
||||
"-q:a",
|
||||
"2",
|
||||
mp3_path,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
await proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
raise Exception("FFmpeg error")
|
||||
|
||||
await message.client.send_file(
|
||||
message.chat.id,
|
||||
mp3_path,
|
||||
caption=self.strings["success"],
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
await utils.answer(message, self.strings["error"])
|
||||
@@ -1,333 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Weather
|
||||
# Description: Advanced weather module with detailed information
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: api Weather
|
||||
# scope: api Weather 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import requests
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_FORECAST_DAYS = 3
|
||||
DEFAULT_HOURLY_INDEX = 4
|
||||
WEATHER_API_URL = "https://wttr.in/{city}?format=j1&lang=en"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeatherCondition:
|
||||
"""Represents a weather condition with its emoji."""
|
||||
|
||||
condition: str
|
||||
emoji: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class WindDirection:
|
||||
"""Represents a wind direction with its description."""
|
||||
|
||||
direction: str
|
||||
description: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForecastDay:
|
||||
"""Represents a single day's weather forecast."""
|
||||
|
||||
date: str
|
||||
emoji: str
|
||||
condition: str
|
||||
temp_min: str
|
||||
temp_max: str
|
||||
wind_speed: str
|
||||
wind_direction: str
|
||||
|
||||
|
||||
WEATHER_EMOJI: List[WeatherCondition] = [
|
||||
WeatherCondition("clear", "<emoji document_id=5402477260982731644>☀️</emoji>"),
|
||||
WeatherCondition("sunny", "<emoji document_id=5402477260982731644>☀️</emoji>"),
|
||||
WeatherCondition(
|
||||
"partly cloudy", "<emoji document_id=5350424168615649565>⛅️</emoji>"
|
||||
),
|
||||
WeatherCondition("cloudy", "☁️<emoji document_id=5208563370218762357>☁️</emoji>"),
|
||||
WeatherCondition("overcast", "<emoji document_id=5208563370218762357>☁️</emoji>"),
|
||||
WeatherCondition("mist", "<emoji document_id=5449510395574229527>😶🌫️</emoji>"),
|
||||
WeatherCondition("fog", "<emoji document_id=5449510395574229527>😶🌫️</emoji>"),
|
||||
WeatherCondition("light rain", "<emoji document_id=5283097055852503586>🌦</emoji>"),
|
||||
WeatherCondition("rain", "<emoji document_id=5283243028905994049>🌧</emoji>"),
|
||||
WeatherCondition("heavy rain", "<emoji document_id=5282939632416206153>⛈</emoji>"),
|
||||
WeatherCondition(
|
||||
"thunderstorm", "<emoji document_id=5282939632416206153>⛈</emoji>"
|
||||
),
|
||||
WeatherCondition("snow", "<emoji document_id=5282833267551117457>🌨</emoji>"),
|
||||
WeatherCondition("heavy snow", "<emoji document_id=5449449325434266744>❄️</emoji>"),
|
||||
WeatherCondition("sleet", "<emoji document_id=5282833267551117457>🌨</emoji>"),
|
||||
WeatherCondition("wind", "💨"),
|
||||
]
|
||||
|
||||
WIND_DIRECTIONS: List[WindDirection] = [
|
||||
WindDirection("N", "⬆️ North"),
|
||||
WindDirection("NE", "↗️ Northeast"),
|
||||
WindDirection("E", "➡️ East"),
|
||||
WindDirection("SE", "↘️ Southeast"),
|
||||
WindDirection("S", "⬇️ South"),
|
||||
WindDirection("SW", "↙️ Southwest"),
|
||||
WindDirection("W", "⬅️ West"),
|
||||
WindDirection("NW", "↖️ Northwest"),
|
||||
]
|
||||
|
||||
WIND_DIRECTIONS_RU: List[WindDirection] = [
|
||||
WindDirection("N", "⬆️ Северный"),
|
||||
WindDirection("NE", "↗️ Северо-восточный"),
|
||||
WindDirection("E", "➡️ Восточный"),
|
||||
WindDirection("SE", "↘️ Юго-восточный"),
|
||||
WindDirection("S", "⬇️ Южный"),
|
||||
WindDirection("SW", "↙️ Юго-западный"),
|
||||
WindDirection("W", "⬅️ Западный"),
|
||||
WindDirection("NW", "↖️ Северо-западный"),
|
||||
]
|
||||
|
||||
|
||||
@loader.tds
|
||||
class Weather(loader.Module):
|
||||
"""Advanced weather module with detailed information"""
|
||||
|
||||
strings = {
|
||||
"name": "Weather",
|
||||
"no_city": "🚫 <b>Please specify a city</b>",
|
||||
"invalid_city": "🚫 <b>City not found</b>",
|
||||
"loading": "🔄 <b>Fetching weather data for {}</b>...",
|
||||
"error": "<emoji document_id=5980953710157632545>❌</emoji> <b>Error retrieving weather data</b>",
|
||||
"default_city": "<emoji document_id=5980930633298350051>✅</emoji> Default city set to: <code>{city}</code>",
|
||||
"weather_text": """<b>{emoji} Weather: {location}</b>
|
||||
|
||||
<b>📊 Current conditions:</b>
|
||||
├ 🌡 Temperature: <code>{temp}°C</code>
|
||||
├– <i>Feels like:</i> <code>{feels_like}°C</code>
|
||||
├ 💧 Humidity: <code>{humidity}%</code>
|
||||
├ 💨 Wind: <code>{wind_speed} km/h</code> {wind_direction}
|
||||
├ 🌪 Pressure: <code>{pressure} mmHg</code>
|
||||
├ 👁 Visibility: <code>{visibility} km</code>
|
||||
└ ☁️ Cloudiness: <code>{clouds}</code>
|
||||
|
||||
<b>🌅 Time:</b>
|
||||
├ 🌅 Sunrise: <code>{sunrise}</code>
|
||||
├ 🌇 Sunset: <code>{sunset}</code>
|
||||
└ ⏱ Local time: <code>{local_time}</code>
|
||||
|
||||
<b>📅 Forecast for {forecast_days} days:</b>
|
||||
{forecast}
|
||||
⏰ Updated: <code>{updated}</code>""",
|
||||
"forecast_day": """<b>{date}</b> {emoji}
|
||||
├ 🌡 Temperature: {temp_min}°C ... {temp_max}°C
|
||||
└ 💨 Wind: {wind_speed} km/h {wind_direction}
|
||||
|
||||
""",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_city": "🚫 <b>Пожалуйста, укажите город</b>",
|
||||
"invalid_city": "🚫 <b>Город не найден</b>",
|
||||
"loading": "🔄 <b>Получаю метеоданные для {}</b>...",
|
||||
"default_city": "<emoji document_id=5980930633298350051>✅</emoji> Город по умолчанию установлен: <code>{city}</code>",
|
||||
"error": "<emoji document_id=5980953710157632545>❌</emoji> <b>Ошибка при получении данных о погоде</b>",
|
||||
"weather_text": """<b>{emoji} Погода: {location}</b>
|
||||
|
||||
<b>📊 Текущие условия:</b>
|
||||
├ 🌡 Температура: <code>{temp}°C</code>
|
||||
├– <i>Ощущается как:</i> <code>{feels_like}°C</code>
|
||||
├ 💧 Влажность: <code>{humidity}%</code>
|
||||
├ 💨 Ветер: <code>{wind_speed} км/ч</code> {wind_direction}
|
||||
├ 🌪 Давление: <code>{pressure} мм.рт.ст</code>
|
||||
├ 👁 Видимость: <code>{visibility} км</code>
|
||||
└ ☁️ Облачность: <code>{clouds}</code>
|
||||
|
||||
<b>🌅 Время:</b>
|
||||
├ 🌅 Восход: <code>{sunrise}</code>
|
||||
├ 🌇 Закат: <code>{sunset}</code>
|
||||
└ ⏱ Местное время: <code>{local_time}</code>
|
||||
|
||||
<b>📅 Прогноз на {forecast_days} дня:</b>
|
||||
{forecast}
|
||||
⏰ Обновлено: <code>{updated}</code>""",
|
||||
"forecast_day": """<b>{date}</b> {emoji}
|
||||
├ 🌡 Температура: {temp_min}°C ... {temp_max}°C
|
||||
└ 💨 Ветер: {wind_speed} км/ч {wind_direction}
|
||||
|
||||
""",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"default_city",
|
||||
None,
|
||||
lambda: "Default city for weather command",
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"language",
|
||||
"ru",
|
||||
lambda: "Language for weather output (en/ru)",
|
||||
),
|
||||
)
|
||||
|
||||
def get_weather_emoji(self, condition: str) -> str:
|
||||
"""Get emoji for weather conditions"""
|
||||
condition = condition.lower()
|
||||
for item in WEATHER_EMOJI:
|
||||
if item.condition in condition:
|
||||
return item.emoji
|
||||
return "🌡"
|
||||
|
||||
def get_wind_direction(self, direction: str) -> str:
|
||||
"""Get wind direction description"""
|
||||
lang = self.config["language"]
|
||||
directions = WIND_DIRECTIONS_RU if lang == "ru" else WIND_DIRECTIONS
|
||||
for item in directions:
|
||||
if item.direction == direction.upper():
|
||||
return item.description
|
||||
return direction
|
||||
|
||||
async def get_weather_data(self, city: str) -> Union[Dict, None]:
|
||||
"""Get weather data from wttr.in"""
|
||||
lang = self.config["language"]
|
||||
url = WEATHER_API_URL.format(city=city)
|
||||
if lang == "ru":
|
||||
url = f"https://wttr.in/{city}?format=j1&lang=ru"
|
||||
try:
|
||||
response = await utils.run_sync(requests.get, url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to fetch weather data for {city}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.exception(f"Error fetching weather data: {e}")
|
||||
return None
|
||||
|
||||
def format_forecast(self, forecast_data: list) -> str:
|
||||
"""Format weather forecast for multiple days."""
|
||||
forecast_text = ""
|
||||
for day in forecast_data:
|
||||
hourly = day["hourly"][DEFAULT_HOURLY_INDEX]
|
||||
forecast_day = ForecastDay(
|
||||
date=day["date"],
|
||||
emoji=self.get_weather_emoji(hourly["weatherDesc"][0]["value"]),
|
||||
condition=hourly["weatherDesc"][0]["value"],
|
||||
temp_min=day["mintempC"],
|
||||
temp_max=day["maxtempC"],
|
||||
wind_speed=hourly["windspeedKmph"],
|
||||
wind_direction=self.get_wind_direction(hourly["winddir16Point"]),
|
||||
)
|
||||
|
||||
forecast_text += self.strings("forecast_day").format(
|
||||
date=forecast_day.date,
|
||||
emoji=forecast_day.emoji,
|
||||
condition=forecast_day.condition,
|
||||
temp_min=forecast_day.temp_min,
|
||||
temp_max=forecast_day.temp_max,
|
||||
wind_speed=forecast_day.wind_speed,
|
||||
wind_direction=forecast_day.wind_direction,
|
||||
)
|
||||
return forecast_text
|
||||
|
||||
async def process_weather_data(self, weather_data: Dict) -> str:
|
||||
"""Process weather data and format the text."""
|
||||
current = weather_data["current_condition"][0]
|
||||
forecast = weather_data["weather"]
|
||||
location = (
|
||||
f"{weather_data['nearest_area'][0]['areaName'][0]['value']}, "
|
||||
f"{weather_data['nearest_area'][0]['country'][0]['value']}"
|
||||
)
|
||||
|
||||
forecast_text = self.format_forecast(forecast[:DEFAULT_FORECAST_DAYS])
|
||||
|
||||
return self.strings("weather_text").format(
|
||||
location=location,
|
||||
emoji=self.get_weather_emoji(current["weatherDesc"][0]["value"]),
|
||||
temp=current["temp_C"],
|
||||
feels_like=current["FeelsLikeC"],
|
||||
humidity=current["humidity"],
|
||||
wind_speed=current["windspeedKmph"],
|
||||
wind_direction=self.get_wind_direction(current["winddir16Point"]),
|
||||
pressure=current["pressure"],
|
||||
visibility=current["visibility"],
|
||||
clouds=current["weatherDesc"][0]["value"],
|
||||
sunrise=forecast[0]["astronomy"][0]["sunrise"],
|
||||
sunset=forecast[0]["astronomy"][0]["sunset"],
|
||||
local_time=current["observation_time"],
|
||||
forecast=forecast_text,
|
||||
forecast_days=DEFAULT_FORECAST_DAYS,
|
||||
updated=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Узнайте погоду для указанного города",
|
||||
en_doc="Get the weather for the specified city",
|
||||
)
|
||||
async def weather(self, message):
|
||||
city = utils.get_args_raw(message) or self.config["default_city"]
|
||||
if not city:
|
||||
await utils.answer(message, self.strings("no_city"))
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings("loading").format(city))
|
||||
|
||||
weather_data = await self.get_weather_data(city)
|
||||
if not weather_data:
|
||||
await utils.answer(message, self.strings("error"))
|
||||
return
|
||||
|
||||
try:
|
||||
weather_text = await self.process_weather_data(weather_data)
|
||||
await utils.answer(message, weather_text)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error processing weather data: {e}")
|
||||
await utils.answer(message, self.strings("error"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Установите город по умолчанию для определения погоды",
|
||||
en_doc="Set the default city for weather",
|
||||
)
|
||||
async def weatherset(self, message):
|
||||
city = utils.get_args_raw(message)
|
||||
if not city:
|
||||
await utils.answer(message, self.strings("no_city"))
|
||||
return
|
||||
|
||||
weather_data = await self.get_weather_data(city)
|
||||
if not weather_data:
|
||||
await utils.answer(message, self.strings("invalid_city"))
|
||||
return
|
||||
|
||||
self.config["default_city"] = city
|
||||
await utils.answer(message, self.strings("default_city").format(city=city))
|
||||
@@ -1,135 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: WindowsKeys
|
||||
# Description: Provides you Windows activation keys
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: WindowsKeys
|
||||
# scope: WindowsKeys 0.0.1
|
||||
# requires: requests
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class WindowsKeysMod(loader.Module):
|
||||
"""Windows activation keys"""
|
||||
|
||||
strings = {
|
||||
"name": "WindowsKeys",
|
||||
"winkey": "✅ Key: <code>{}</code>\n\n⚠ For KMS activation only",
|
||||
"error": "❌ Failed to get key",
|
||||
"select": "🔓 Select version:",
|
||||
"close": "🎈 Close",
|
||||
"loading": "⌛ Loading...",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"winkey": "✅ Ключ: <code>{}</code>\n\n⚠ Только для KMS активации",
|
||||
"error": "❌ Ошибка получения",
|
||||
"select": "🔓 Выберите версию:",
|
||||
"close": "🎈 Закрыть",
|
||||
"loading": "⌛ Загрузка...",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.cache = None
|
||||
self.cache_time = 0
|
||||
self.CACHE_TTL = 3600
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
|
||||
@loader.command(ru_doc="Меню ключей Windows", en_doc="Windows keys menu")
|
||||
async def winkey(self, message):
|
||||
await self.inline.form(
|
||||
self.strings["select"],
|
||||
message=message,
|
||||
reply_markup=[
|
||||
[
|
||||
{
|
||||
"text": "Win 10/11 Pro",
|
||||
"callback": self._key,
|
||||
"args": ("win10_11pro",),
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "Win 10/11 LTSC",
|
||||
"callback": self._key,
|
||||
"args": ("win10_11enterpriseLTSC",),
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "Win 8.1 Pro",
|
||||
"callback": self._key,
|
||||
"args": ("win8.1pro",),
|
||||
}
|
||||
],
|
||||
[{"text": "Win 8 Pro", "callback": self._key, "args": ("win8pro",)}],
|
||||
[{"text": "Win 7 Pro", "callback": self._key, "args": ("win7pro",)}],
|
||||
[
|
||||
{
|
||||
"text": "Vista Business",
|
||||
"callback": self._key,
|
||||
"args": ("winvistabusiness",),
|
||||
}
|
||||
],
|
||||
[{"text": self.strings["close"], "action": "close"}],
|
||||
],
|
||||
)
|
||||
|
||||
async def _key(self, call, version):
|
||||
await call.edit(self.strings["loading"])
|
||||
keys = await self._get_keys()
|
||||
key = keys.get(version) if keys else None
|
||||
await call.edit(
|
||||
self.strings["winkey"].format(key) if key else self.strings["error"],
|
||||
reply_markup=[
|
||||
[{"text": "← Back", "callback": self.winkey}],
|
||||
[{"text": self.strings["close"], "action": "close"}],
|
||||
],
|
||||
)
|
||||
|
||||
async def _get_keys(self):
|
||||
if time.time() - self.cache_time < self.CACHE_TTL:
|
||||
return self.cache
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(10)
|
||||
) as session:
|
||||
async with session.get("https://files.archquise.ru/winkeys.json") as r:
|
||||
self.cache = await r.json()
|
||||
self.cache_time = time.time()
|
||||
return self.cache
|
||||
except Exception: # noqa: E722
|
||||
return None
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,100 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: animals
|
||||
# Description: Random cats and dogs
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api animals
|
||||
# scope: Api animals 0.0.1
|
||||
# requires: requests
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class animals(loader.Module):
|
||||
"""Random cats and dogs"""
|
||||
|
||||
strings = {
|
||||
"name": "animals",
|
||||
"loading": "<b>Generation is underway</b> <emoji document_id=5215484787325676090>🕐</emoji>",
|
||||
"done": "<b>Here is your salute</b> <emoji document_id=5436246187944460315>❤️</emoji>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"loading": "<b>Генерация идет полным ходом</b> <emoji document_id=5215484787325676090>🕐</emoji>",
|
||||
"done": "<b>Вот ваш результат</b> <emoji document_id=5436246187944460315>❤️</emoji>",
|
||||
}
|
||||
|
||||
# thanks https://github.com/C0dwiz/H.Modules/pull/1
|
||||
async def get_photo(self, prefix: str) -> str:
|
||||
response = requests.get(f"https://api.{prefix}.com/v1/images/search")
|
||||
return response.json()[0]["url"]
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Файлы случайных фотографий кошек",
|
||||
en_doc="Random photos of cats files",
|
||||
)
|
||||
async def fcatcmd(self, message):
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
cat_url = await self.get_photo("thecatapi")
|
||||
await utils.answer_file(
|
||||
message, cat_url, self.strings("done"), force_document=True
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Случайные фотографии собачьих файлов",
|
||||
en_doc="Random photos of dog files",
|
||||
)
|
||||
async def fdogcmd(self, message):
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
dog_url = await self.get_photo("thedogapi")
|
||||
await utils.answer_file(
|
||||
message, dog_url, self.strings("done"), force_document=True
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Случайные фотографии кошек",
|
||||
en_doc="Random photos of cats",
|
||||
)
|
||||
async def catcmd(self, message):
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
cat_url = await self.get_photo("thecatapi")
|
||||
await utils.answer_file(
|
||||
message, cat_url, self.strings("done"), force_document=False
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Случайные фотографии собаки",
|
||||
en_doc="Random photos of dog",
|
||||
)
|
||||
async def dogcmd(self, message):
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
dog_url = await self.get_photo("thedogapi")
|
||||
await utils.answer_file(
|
||||
message, dog_url, self.strings("done"), force_document=False
|
||||
)
|
||||
@@ -1,223 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: CAliases
|
||||
# Description: Module for custom aliases
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: CAliases
|
||||
# scope: CAliases 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
from telethon import types
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class CustomAliasesMod(loader.Module):
|
||||
"""Module for custom aliases"""
|
||||
|
||||
strings = {
|
||||
"name": "CAliases",
|
||||
"c404": "<emoji document_id=5312526098750252863>❌</emoji> <b>Command <code>{}</code> not found!</b>",
|
||||
"a404": "<emoji document_id=5312526098750252863>❌</emoji> <b>Custom alias <code>{}</code> not found!</b>",
|
||||
"no_args": "<emoji document_id=5312526098750252863>❌</emoji> <b>You must specify two args: alias name and command</b>",
|
||||
"added": (
|
||||
"<emoji document_id=5314250708508220914>✅</emoji> <b>Custom alias <i>{alias}</i> for command "
|
||||
"<code>{prefix}{cmd}</code> successfully added!</b>\n<b>Use it like:</b> <code>{prefix}{alias}{args}</code>"
|
||||
),
|
||||
"argsopt": " [args (optional)]",
|
||||
"deleted": "<emoji document_id=5314250708508220914>✅</emoji> <b>Custom alias <code>{}</code> successfully deleted</b>",
|
||||
"list": "<emoji document_id=5974492756494519709>🔗</emoji> <b>Custom aliases ({len}):</b>\n",
|
||||
"no_aliases": "<emoji document_id=5312526098750252863>❌</emoji> <b>You have no custom aliases!</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"c404": "<emoji document_id=5312526098750252863>❌</emoji> <b>Команда <code>{}</code> не найдена!</b>",
|
||||
"a404": "<emoji document_id=5312526098750252863>❌</emoji> <b>Кастомный алиас <code>{}</code> не найден!</b>",
|
||||
"no_args": "<emoji document_id=5312526098750252863>❌</emoji> <b>Вы должны указать как минимум два аргумента: имя алиаса и команду</b>",
|
||||
"added": (
|
||||
"<emoji document_id=5314250708508220914>✅</emoji> <b>Успешно добавил алиас с названием <i>{alias}</i> "
|
||||
"для команды <code>{prefix}{cmd}</code></b>\n<b>Используй его так:</b> <code>{prefix}{alias}{args}</code>"
|
||||
),
|
||||
"argsopt": " [аргументы (необязательно)]",
|
||||
"deleted": "<emoji document_id=5314250708508220914>✅</emoji> <b>Кастомный алиас <code>{}</code> успешно удалён</b>",
|
||||
"list": "<emoji document_id=5974492756494519709>🔗</emoji> <b>Кастомные алиасы (всего {len}):</b>\n",
|
||||
"no_aliases": "<emoji document_id=5312526098750252863>❌</emoji> <b>У вас нет кастомных алиасов!</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._aliases_cache: Optional[Dict[str, Dict[str, str]]] = None
|
||||
self._prefix_cache: Optional[str] = None
|
||||
|
||||
def _get_aliases(self) -> Dict[str, Dict[str, str]]:
|
||||
if self._aliases_cache is None:
|
||||
self._aliases_cache = self.get("aliases", {})
|
||||
return self._aliases_cache
|
||||
|
||||
def _save_aliases(self, aliases: Dict[str, Dict[str, str]]) -> None:
|
||||
self.set("aliases", aliases)
|
||||
self._aliases_cache = aliases
|
||||
|
||||
def _get_prefix(self) -> str:
|
||||
if self._prefix_cache is None:
|
||||
self._prefix_cache = self.get_prefix()
|
||||
return self._prefix_cache
|
||||
|
||||
def _format_alias_list(self) -> str:
|
||||
"""Format aliases list for display"""
|
||||
aliases = self._get_aliases()
|
||||
if not aliases:
|
||||
return self.strings["no_aliases"]
|
||||
|
||||
lines = [self.strings["list"].format(len=len(aliases))]
|
||||
|
||||
for alias_name, alias_data in aliases.items():
|
||||
cmd = alias_data["command"]
|
||||
if alias_data.get("args"):
|
||||
cmd += f" {alias_data['args']}"
|
||||
|
||||
lines.append(
|
||||
f" <emoji document_id=5280726938279749656>▪️</emoji> <code>{alias_name}</code> "
|
||||
f"<emoji document_id=5960671702059848143>👈</emoji> <code>{cmd}</code>"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _validate_command(self, cmd: str) -> bool:
|
||||
"""Check if command exists"""
|
||||
return cmd in self.allmodules.commands
|
||||
|
||||
def _parse_alias_args(self, message: types.Message) -> tuple:
|
||||
"""Parse alias command arguments"""
|
||||
raw_args = utils.get_args_raw(message)
|
||||
if not raw_args:
|
||||
return None, None, None
|
||||
|
||||
parts = raw_args.split(" ", 2)
|
||||
if len(parts) < 2:
|
||||
return None, None, None
|
||||
|
||||
alias_name = parts[0]
|
||||
command = parts[1]
|
||||
cmd_args = parts[2] if len(parts) > 2 else ""
|
||||
|
||||
return alias_name, command, cmd_args
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Получить список всех алиасов",
|
||||
en_doc=" Get list of all aliases"
|
||||
)
|
||||
async def caliasescmd(self, message: types.Message):
|
||||
"""Get all aliases"""
|
||||
await utils.answer(message, self._format_alias_list())
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<имя> Удалить алиас",
|
||||
en_doc="<name> Remove alias"
|
||||
)
|
||||
async def rmcaliascmd(self, message: types.Message):
|
||||
"""Remove alias"""
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
alias_name = args[0]
|
||||
aliases = self._get_aliases()
|
||||
|
||||
if alias_name not in aliases:
|
||||
return await utils.answer(message, self.strings["a404"].format(alias_name))
|
||||
|
||||
del aliases[alias_name]
|
||||
self._save_aliases(aliases)
|
||||
await utils.answer(message, self.strings["deleted"].format(alias_name))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<имя> <команда> [аргументы] Добавить новый алиас (может содержать ключевое слово {args})",
|
||||
en_doc="<name> <command> [arguments] Add new alias (may contain {args} keyword)",
|
||||
)
|
||||
async def caliascmd(self, message: types.Message):
|
||||
"""Add new alias (may contain {args} keyword)"""
|
||||
alias_name, command, cmd_args = self._parse_alias_args(message)
|
||||
|
||||
if not alias_name or not command:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
if not self._validate_command(command):
|
||||
return await utils.answer(message, self.strings["c404"].format(command))
|
||||
|
||||
aliases = self._get_aliases()
|
||||
aliases[alias_name] = {"command": command, "args": cmd_args}
|
||||
self._save_aliases(aliases)
|
||||
|
||||
prefix = self._get_prefix()
|
||||
full_cmd = f"{command} {cmd_args}" if cmd_args else command
|
||||
args_display = self.strings["argsopt"] if "{args}" in cmd_args else ""
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["added"].format(
|
||||
alias=alias_name,
|
||||
prefix=prefix,
|
||||
cmd=full_cmd,
|
||||
args=args_display,
|
||||
),
|
||||
)
|
||||
|
||||
@loader.tag(only_messages=True, no_media=True, no_inline=True, out=True)
|
||||
async def watcher(self, message: types.Message):
|
||||
"""Handle alias execution"""
|
||||
if not message.raw_text:
|
||||
return
|
||||
|
||||
aliases = self._get_aliases()
|
||||
prefix = self._get_prefix()
|
||||
text = message.raw_text
|
||||
first_word = text.split()[0].lower()
|
||||
|
||||
if not first_word.startswith(prefix):
|
||||
return
|
||||
|
||||
alias_name = first_word[len(prefix) :]
|
||||
if alias_name not in aliases:
|
||||
return
|
||||
|
||||
alias_data = aliases[alias_name]
|
||||
command = alias_data["command"]
|
||||
template_args = alias_data.get("args", "")
|
||||
|
||||
user_args = utils.get_args_raw(message)
|
||||
if user_args and template_args:
|
||||
final_command = f"{command} {template_args}".format(args=user_args)
|
||||
else:
|
||||
final_command = f"{command} {template_args}" if template_args else command
|
||||
|
||||
try:
|
||||
await self.allmodules.commands[command](
|
||||
await utils.answer(message, f"{prefix}{final_command}")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing alias '{alias_name}': {e}")
|
||||
@@ -1,369 +0,0 @@
|
||||
# ---------------------------------------------------------------------------------
|
||||
# ░█▀▄░▄▀▀▄░█▀▄░█▀▀▄░█▀▀▄░█▀▀▀░▄▀▀▄░░░█▀▄▀█
|
||||
# ░█░░░█░░█░█░█░█▄▄▀░█▄▄█░█░▀▄░█░░█░░░█░▀░█
|
||||
# ░▀▀▀░░▀▀░░▀▀░░▀░▀▀░▀░░▀░▀▀▀▀░░▀▀░░░░▀░░▒▀
|
||||
# Name: DelMessTools
|
||||
# Description: Module to manage and delete your messages in the current chat
|
||||
# Author: @codrago_m
|
||||
# ---------------------------------------------------------------------------------
|
||||
# 🔒 Licensed under the GNU AGPLv3
|
||||
# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Author: @codrago
|
||||
# Commands: nopurge, purgetime, purgelength, purgekeyword, purge
|
||||
# scope: hikka_only
|
||||
# meta developer: @codrago_m
|
||||
# meta banner: https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/banner.png
|
||||
# meta pic: https://envs.sh/HJx.webp
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
__version__ = (1, 1, 0)
|
||||
|
||||
from telethon.tl.types import Message, DocumentAttributeFilename
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
|
||||
class DelMessTools(loader.Module):
|
||||
"""Module to manage and delete your messages in the current chat"""
|
||||
|
||||
strings = {
|
||||
"name": "DelMessTools",
|
||||
"purge_complete": "All your messages have been deleted.",
|
||||
"purge_reply_complete": "Messages up to the replied message have been deleted.",
|
||||
"purge_keyword_complete": "Messages containing the keyword have been deleted.",
|
||||
"purge_time_complete": "Messages within the specified time range have been deleted.",
|
||||
"purge_media_complete": "All your media messages have been deleted.",
|
||||
"purge_length_complete": "Messages with the specified length have been deleted.",
|
||||
"purge_type_complete": "Messages of the specified type have been deleted.",
|
||||
"enabled": "It's not operational now anyway.",
|
||||
"disabled": "Operation status changed to disabled.",
|
||||
"interrupted": "The deletion was interrupted because you changed your mind.",
|
||||
"none": "You didn't even intend to delete anything here, but anyway it's disabled now.",
|
||||
"no_args": "Please specify arguments for the command.",
|
||||
"no_keyword": "Please specify a keyword to delete messages.",
|
||||
"no_length": "Please specify a valid length.",
|
||||
"invalid_time": "Invalid time format. Please use the format: YYYY-MM-DD HH:MM:SS",
|
||||
"invalid_length": "Please specify a valid number for length.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"purge_complete": "Все ваши сообщения были удалены.",
|
||||
"purge_reply_complete": "Сообщения до указанного ответа были удалены.",
|
||||
"purge_keyword_complete": "Сообщения, содержащие ключевое слово, были удалены.",
|
||||
"purge_time_complete": "Сообщения в указанном временном диапазоне были удалены.",
|
||||
"purge_media_complete": "Все ваши медиа-сообщения были удалены.",
|
||||
"purge_length_complete": "Сообщения указанной длины были удалены.",
|
||||
"purge_type_complete": "Сообщения указанного типа были удалены.",
|
||||
"enabled": "Оно итак сейчас не работает.",
|
||||
"disabled": "Режим работы изменен на выключено.",
|
||||
"interrupted": "Удаление было прервано т.к вы передумали.",
|
||||
"none": "Вы даже не пытались ничего здесь удалить, в любом случае сейчас оно выключено.",
|
||||
"no_args": "Пожалуйста, укажите аргументы для команды.",
|
||||
"no_keyword": "Пожалуйста, укажите ключевое слово для удаления сообщений.",
|
||||
"no_length": "Пожалуйста, укажите корректную длину.",
|
||||
"invalid_time": "Неверный формат времени. Используйте формат: YYYY-MM-DD HH:MM:SS",
|
||||
"invalid_length": "Пожалуйста, укажите корректное число для длины.",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.client = None
|
||||
self.db = None
|
||||
self.tg_id = None
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
self.tg_id = (await client.get_me()).id
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[reply] [-img] [-voice] [-file] [-all] - удалить все ваши сообщения в текущем чате или только до сообщения, на которое ответили\n -all - удалять сообщения в каждой теме, если это форум, иначе флаг игнорируется",
|
||||
en_doc="[reply] [-img] [-voice] [-file] [-all] - delete all your messages in current chat or only ones up to the message you replied to\n -all - to delete messages in each topic if this is a forum otherwise the flag'll just be ignored",
|
||||
)
|
||||
async def purge(self, message: Message):
|
||||
if not self.client or not self.db or not self.tg_id:
|
||||
return
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
is_last = False
|
||||
args, types_filter, is_each = self.get_types_filter(message)
|
||||
|
||||
try:
|
||||
entity = await self.client.get_entity(message.chat.id)
|
||||
is_forum = getattr(entity, "forum", False)
|
||||
except Exception:
|
||||
is_forum = False
|
||||
|
||||
status = self.db.get(__name__, "status", {})
|
||||
status[message.chat.id] = True
|
||||
self.db.set(__name__, "status", status)
|
||||
|
||||
deleted_count = 0
|
||||
async for i in self.client.iter_messages(message.peer_id):
|
||||
status = self.db.get(__name__, "status", {})
|
||||
if status.get(message.chat.id, None) is not True:
|
||||
return await utils.answer(message, self.strings["interrupted"])
|
||||
|
||||
if is_forum and not is_each:
|
||||
try:
|
||||
if utils.get_topic(message) != utils.get_topic(i):
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if i.from_id == self.tg_id and self.is_valid_type(i, types_filter):
|
||||
if reply:
|
||||
if is_last:
|
||||
break
|
||||
if i.id == reply.id:
|
||||
is_last = True
|
||||
try:
|
||||
await self.client.delete_messages(message.peer_id, [i.id])
|
||||
deleted_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if reply:
|
||||
await utils.answer(message, self.strings["purge_reply_complete"])
|
||||
else:
|
||||
await utils.answer(message, self.strings["purge_complete"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<ключевое слово> [-img] [-voice] [-file] [-all] - удалить все ваши сообщения с указанным ключевым словом в текущем чате\n -all - удалять сообщения в каждой теме, если это форум, иначе флаг игнорируется",
|
||||
en_doc="<keyword> [-img] [-voice] [-file] [-all] - delete all your messages containing the specified keyword in the current chat\n -all - to delete messages in each topic if this is a forum otherwise the flag'll just be ignored",
|
||||
)
|
||||
async def purgekeyword(self, message: Message):
|
||||
if not self.client or not self.db or not self.tg_id:
|
||||
return
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
args, types_filter, is_each = self.get_types_filter(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_keyword"])
|
||||
|
||||
try:
|
||||
entity = await self.client.get_entity(message.chat.id)
|
||||
is_forum = getattr(entity, "forum", False)
|
||||
except Exception:
|
||||
is_forum = False
|
||||
|
||||
status = self.db.get(__name__, "status", {})
|
||||
status[message.chat.id] = True
|
||||
self.db.set(__name__, "status", status)
|
||||
|
||||
deleted_count = 0
|
||||
async for i in self.client.iter_messages(message.peer_id):
|
||||
status = self.db.get(__name__, "status", {})
|
||||
if status.get(message.chat.id, None) is not True:
|
||||
return await utils.answer(message, self.strings["interrupted"])
|
||||
|
||||
if is_forum and not is_each:
|
||||
try:
|
||||
if utils.get_topic(message) != utils.get_topic(i):
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if (
|
||||
i.from_id == self.tg_id
|
||||
and args.lower() in (i.text or "").lower()
|
||||
and self.is_valid_type(i, types_filter)
|
||||
):
|
||||
try:
|
||||
await self.client.delete_messages(message.chat.id, [i.id])
|
||||
deleted_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await utils.answer(message, self.strings["purge_keyword_complete"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<начальное время> <конечное время> [-img] [-voice] [-file] [-all] - удалить все ваши сообщения в указанном временном диапазоне в текущем чате\n -all - удалять сообщения в каждой теме, если это форум, иначе флаг игнорируется\n Формат времени: YYYY-MM-DD HH:MM:SS",
|
||||
en_doc="<start_time> <end_time> [-img] [-voice] [-file] [-all] - delete all your messages within the specified time range in the current chat\n -all - to delete messages in each topic if this is a forum otherwise the flag'll just be ignored\n Time format: YYYY-MM-DD HH:MM:SS",
|
||||
)
|
||||
async def purgetime(self, message: Message):
|
||||
if not self.client or not self.db or not self.tg_id:
|
||||
return
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
args, types_filter, is_each = self.get_types_filter(message)
|
||||
args = args.split()
|
||||
|
||||
if not args or len(args) < 2:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
start_time = datetime.strptime(args[0], "%Y-%m-%d %H:%M:%S")
|
||||
end_time = datetime.strptime(args[1], "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
return await utils.answer(message, self.strings["invalid_time"])
|
||||
|
||||
try:
|
||||
entity = await self.client.get_entity(message.chat.id)
|
||||
is_forum = getattr(entity, "forum", False)
|
||||
except Exception:
|
||||
is_forum = False
|
||||
|
||||
status = self.db.get(__name__, "status", {})
|
||||
status[message.chat.id] = True
|
||||
self.db.set(__name__, "status", status)
|
||||
|
||||
deleted_count = 0
|
||||
async for i in self.client.iter_messages(message.peer_id):
|
||||
status = self.db.get(__name__, "status", {})
|
||||
if status.get(message.chat.id, None) is not True:
|
||||
return await utils.answer(message, self.strings["interrupted"])
|
||||
|
||||
if is_forum and not is_each:
|
||||
try:
|
||||
if utils.get_topic(message) != utils.get_topic(i):
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if (
|
||||
i.from_id == self.tg_id
|
||||
and start_time <= i.date <= end_time
|
||||
and self.is_valid_type(i, types_filter)
|
||||
):
|
||||
try:
|
||||
await self.client.delete_messages(message.peer_id, [i.id])
|
||||
deleted_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await utils.answer(message, self.strings["purge_time_complete"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<длина> [-img] [-voice] [-file] [-all] - удалить все ваши сообщения указанной длины в текущем чате\n -all - удалять сообщения в каждой теме, если это форум, иначе флаг игнорируется",
|
||||
en_doc="<length> [-img] [-voice] [-file] [-all] - delete all your messages with the specified length in the current chat\n -all - to delete messages in each topic if this is a forum otherwise the flag'll just be ignored",
|
||||
)
|
||||
async def purgelength(self, message: Message):
|
||||
if not self.client or not self.db or not self.tg_id:
|
||||
return
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
args, types_filter, is_each = self.get_types_filter(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings["no_length"])
|
||||
|
||||
try:
|
||||
length = int(args)
|
||||
except ValueError:
|
||||
return await utils.answer(message, self.strings["invalid_length"])
|
||||
|
||||
try:
|
||||
entity = await self.client.get_entity(message.chat.id)
|
||||
is_forum = getattr(entity, "forum", False)
|
||||
except Exception:
|
||||
is_forum = False
|
||||
|
||||
status = self.db.get(__name__, "status", {})
|
||||
status[message.chat.id] = True
|
||||
self.db.set(__name__, "status", status)
|
||||
|
||||
deleted_count = 0
|
||||
async for i in self.client.iter_messages(message.peer_id):
|
||||
status = self.db.get(__name__, "status", {})
|
||||
if status.get(message.chat.id, None) is not True:
|
||||
return await utils.answer(message, self.strings["interrupted"])
|
||||
|
||||
if is_forum and not is_each:
|
||||
try:
|
||||
if utils.get_topic(message) != utils.get_topic(i):
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if (
|
||||
i.from_id == self.tg_id
|
||||
and len(i.text or "") == length
|
||||
and self.is_valid_type(i, types_filter)
|
||||
):
|
||||
try:
|
||||
await self.client.delete_messages(message.peer_id, [i.id])
|
||||
deleted_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await utils.answer(message, self.strings["purge_length_complete"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Прервать процесс удаления\nИспользуйте в чате, где вы ранее начали удаление",
|
||||
en_doc="Interrupt the deletion process\nUse in the chat where you've previously started deletion",
|
||||
)
|
||||
async def nopurge(self, message: Message):
|
||||
if not self.db:
|
||||
return
|
||||
|
||||
chat_id = utils.get_chat_id(message)
|
||||
|
||||
status = self.db.get(__name__, "status", {})
|
||||
_status = status.get(chat_id, None)
|
||||
status[chat_id] = False
|
||||
self.db.set(__name__, "status", status)
|
||||
|
||||
if _status is True:
|
||||
await utils.answer(message, self.strings["disabled"])
|
||||
elif _status is False:
|
||||
await utils.answer(message, self.strings["enabled"])
|
||||
else:
|
||||
await utils.answer(message, self.strings["none"])
|
||||
|
||||
def get_types_filter(self, message: Message):
|
||||
"""Get the types filter from the command arguments."""
|
||||
args_raw = utils.get_args_raw(message)
|
||||
if not args_raw:
|
||||
return "", [], False
|
||||
|
||||
args = args_raw.split()
|
||||
types_filter = []
|
||||
valid_types = ["-img", "-voice", "-file", "-all"]
|
||||
is_each = "-all" in args
|
||||
|
||||
_args = args_raw
|
||||
args_ = ""
|
||||
|
||||
for i, arg in enumerate(args):
|
||||
if arg in valid_types:
|
||||
_args = " ".join(args[:i])
|
||||
args_ = " ".join(args[i:])
|
||||
break
|
||||
|
||||
if "-img" in args_:
|
||||
types_filter.append("img")
|
||||
if "-voice" in args_:
|
||||
types_filter.append("voice")
|
||||
if "-file" in args_:
|
||||
types_filter.append("file")
|
||||
if "-all" in args_:
|
||||
is_each = True
|
||||
|
||||
return _args, types_filter, is_each
|
||||
|
||||
def is_valid_type(self, message: Message, types_filter):
|
||||
"""Check if the message matches the specified types filter."""
|
||||
if not types_filter:
|
||||
return True # No filtering means all types are valid
|
||||
|
||||
if "img" in types_filter and hasattr(message, "photo") and message.photo:
|
||||
return True
|
||||
if "voice" in types_filter and hasattr(message, "voice") and message.voice:
|
||||
return True
|
||||
if "file" in types_filter and hasattr(message, "document") and message.document:
|
||||
for attr in getattr(message.document, "attributes", []):
|
||||
if isinstance(attr, DocumentAttributeFilename):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1,100 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: face
|
||||
# Description: Random face
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api face
|
||||
# scope: Api face 0.0.1
|
||||
# requires: aiohttp
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import re
|
||||
import random
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class face(loader.Module):
|
||||
"""random face"""
|
||||
|
||||
strings = {
|
||||
"name": "face",
|
||||
"loading": (
|
||||
"<emoji document_id=5348399448017871250>🔍</emoji> I'm looking for you kaomoji"
|
||||
),
|
||||
"random_face": (
|
||||
"<emoji document_id=5208878706717636743>🗿</emoji> Here is your random one kaomoji\n<code>{}</code>"
|
||||
),
|
||||
"error": "An error has occurred!",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"loading": (
|
||||
"<emoji document_id=5348399448017871250>🔍</emoji> Ищю вам kaomoji"
|
||||
),
|
||||
"random_face": (
|
||||
"<emoji document_id=5208878706717636743>🗿</emoji> Вот ваш рандомный kaomoji\n<code>{}</code>"
|
||||
),
|
||||
"error": "Произошла ошибка!",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Рандом kaomoji",
|
||||
en_doc="Random kaomoji",
|
||||
)
|
||||
async def rfacecmd(self, message):
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
|
||||
url = "https://files.archquise.ru/kaomoji.txt"
|
||||
|
||||
session = await self._get_session()
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
data = await response.text()
|
||||
kaomoji_list = [
|
||||
s.strip() for s in re.split(r"[\t\r\n]+", data) if s.strip()
|
||||
]
|
||||
kaomoji = random.choice(kaomoji_list)
|
||||
await utils.answer(message, self.strings("random_face").format(kaomoji))
|
||||
else:
|
||||
await utils.answer(message, self.strings("error"))
|
||||
@@ -1,54 +0,0 @@
|
||||
ASCIIArt
|
||||
AccountData
|
||||
AniLiberty
|
||||
AnimeQuotes
|
||||
Article
|
||||
AutofarmCookies
|
||||
BirthdayTime
|
||||
CheckSpamBan
|
||||
CodeShare
|
||||
CryptoCurrency
|
||||
FakeActions
|
||||
FakeWallet
|
||||
FolderAutoRead
|
||||
GigaChat
|
||||
H
|
||||
HAFK
|
||||
HInstall
|
||||
InfoBannersManager
|
||||
InlineButton
|
||||
InlineCoin
|
||||
InlineHelper
|
||||
IrisSimpleMod
|
||||
KBSwapper
|
||||
Memes
|
||||
MessageMonitor
|
||||
MooFarmRC1
|
||||
Music
|
||||
ReplaceVowels
|
||||
SMAcrhiver
|
||||
TaskManager
|
||||
TelegramStatusCodes
|
||||
TempChat
|
||||
Text2File
|
||||
TikTokDownloader
|
||||
TimedEmojiStatus
|
||||
UserbotAvast
|
||||
Video2GIF
|
||||
VirusTotal
|
||||
VoiceDL
|
||||
Weather
|
||||
WindowsKeys
|
||||
animals
|
||||
face
|
||||
globalrestrict
|
||||
hikkahost
|
||||
mediatools
|
||||
novoice
|
||||
nsfwart
|
||||
passgen
|
||||
profile
|
||||
search
|
||||
shortener
|
||||
timezone
|
||||
ytdl
|
||||
@@ -1,684 +0,0 @@
|
||||
# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀
|
||||
# █▀█ █ █ █ █▀█ █▀▄ █
|
||||
# © Copyright 2022
|
||||
# https://t.me/hikariatama
|
||||
#
|
||||
# 🔒 Licensed under the GNU AGPLv3
|
||||
# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
# Some functions took from Hikarichat by Hikariatama
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: GlobalRestrict
|
||||
# Description: Global mutation or ban
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api GlobalRestrict
|
||||
# scope: Api GlobalRestrict 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import typing
|
||||
|
||||
from telethon.tl.types import (
|
||||
Channel,
|
||||
Chat,
|
||||
Message,
|
||||
User,
|
||||
)
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BANNED_RIGHTS = {
|
||||
"view_messages": False,
|
||||
"send_messages": False,
|
||||
"send_media": False,
|
||||
"send_stickers": False,
|
||||
"send_gifs": False,
|
||||
"send_games": False,
|
||||
"send_inline": False,
|
||||
"send_polls": False,
|
||||
"change_info": False,
|
||||
"invite_users": False,
|
||||
}
|
||||
|
||||
MUTES_RIGHTS = {
|
||||
"view_messages": True,
|
||||
"send_messages": False,
|
||||
"send_media": False,
|
||||
"send_stickers": False,
|
||||
"send_gifs": False,
|
||||
"send_games": False,
|
||||
"send_inline": False,
|
||||
"send_polls": False,
|
||||
"change_info": False,
|
||||
"invite_users": False,
|
||||
}
|
||||
|
||||
|
||||
def get_full_name(user: typing.Union[User, Channel]) -> str:
|
||||
return utils.escape_html(
|
||||
user.title
|
||||
if isinstance(user, Channel)
|
||||
else (
|
||||
f"{user.first_name} "
|
||||
+ (user.last_name if getattr(user, "last_name", False) else "")
|
||||
)
|
||||
).strip()
|
||||
|
||||
|
||||
@loader.tds
|
||||
class GlobalRestrict(loader.Module):
|
||||
"""Global mutation or ban"""
|
||||
|
||||
strings = {
|
||||
"name": "GlobalRestrict",
|
||||
"no_reason": "Not specified",
|
||||
"args": (
|
||||
"<emoji document_id=5300759756669984376>🚫</emoji> <b>Incorrect arguments</b>"
|
||||
),
|
||||
"glban": (
|
||||
'<emoji document_id=5301059317753979286>🖕</emoji> <b><a href="{}">{}</a>'
|
||||
" has been globally banned.</b>\n<b>Reason: </b><i>{}</i>\n\n{}"
|
||||
),
|
||||
"glbanning": (
|
||||
"<emoji document_id=5301059317753979286>🖕</emoji> <b>Globally banning <a"
|
||||
' href="{}">{}</a>...</b>'
|
||||
),
|
||||
"gunban": (
|
||||
'<emoji document_id=6334872157947955302>🤗</emoji> <b><a href="{}">{}</a>'
|
||||
" has been globally unbanned.</b>\n\n{}"
|
||||
),
|
||||
"gunbanning": (
|
||||
"<emoji document_id=6334872157947955302>🤗</emoji> <b>Global unbanning <a"
|
||||
' href="{}">{}</a>...</b>'
|
||||
),
|
||||
"in_n_chats": (
|
||||
"<emoji document_id=5379568936218009290>👎</emoji> <b>Banned in {}"
|
||||
" chat(s)</b>"
|
||||
),
|
||||
"unbanned_in_n_chats": (
|
||||
"<emoji document_id=5461129450341014019>✋️</emoji> <b>Unbanned in {}"
|
||||
" chat(s)</b>"
|
||||
),
|
||||
"glmute": (
|
||||
'<emoji document_id=5301059317753979286>🖕</emoji> <b><a href="{}">{}</a>'
|
||||
" has been globally muted.</b>\n<b>Reason: </b><i>{}</i>\n\n{}"
|
||||
),
|
||||
"glmutes": (
|
||||
"<emoji document_id=5301059317753979286>🖕</emoji> <b>Global mute <a"
|
||||
' href="{}">{}</a>...</b>'
|
||||
),
|
||||
"gunmute": (
|
||||
'<emoji document_id=6334872157947955302>🤗</emoji> <b><a href="{}">{}</a>'
|
||||
" has been globally unmuted.</b>\n\n{}"
|
||||
),
|
||||
"gunmutes": (
|
||||
"<emoji document_id=6334872157947955302>🤗</emoji> <b>Global unmute <a"
|
||||
' href="{}">{}</a>...</b>'
|
||||
),
|
||||
"in_m_chats": (
|
||||
"<emoji document_id=5379568936218009290>👎</emoji> <b>Muted in {}"
|
||||
" chat(s)</b>"
|
||||
),
|
||||
"unmute_in_n_chats": (
|
||||
"<emoji document_id=5461129450341014019>✋️</emoji> <b>Unmuted in {}"
|
||||
" chat(s)</b>"
|
||||
),
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_reason": "Не указана",
|
||||
"args": (
|
||||
"<emoji document_id=5300759756669984376>🚫</emoji> <b>Неверные"
|
||||
" аргументы</b>"
|
||||
),
|
||||
"glban": (
|
||||
'<emoji document_id=5301059317753979286>🖕</emoji> <b><a href="{}">{}</a>'
|
||||
" был гзабанен.</b>\n<b>Причина: </b><i>{}</i>\n\n{}"
|
||||
),
|
||||
"glbanning": (
|
||||
"<emoji document_id=5301059317753979286>🖕</emoji> <b>Гбан <a"
|
||||
' href="{}">{}</a>...</b>'
|
||||
),
|
||||
"gunban": (
|
||||
'<emoji document_id=6334872157947955302>🤗</emoji> <b><a href="{}">{}</a>'
|
||||
" был гразбанен.</b>\n\n{}"
|
||||
),
|
||||
"gunbanning": (
|
||||
"<emoji document_id=6334872157947955302>🤗</emoji> <b>Гразбан <a"
|
||||
' href="{}">{}</a>...</b>'
|
||||
),
|
||||
"in_n_chats": (
|
||||
"<emoji document_id=5379568936218009290>👎</emoji> <b>Забанил в {}"
|
||||
" чат(-ах)</b>"
|
||||
),
|
||||
"unbanned_in_n_chats": (
|
||||
"<emoji document_id=5461129450341014019>✋️</emoji> <b>Разбанил in {}"
|
||||
" чат(-ах)</b>"
|
||||
),
|
||||
"glmute": (
|
||||
'<emoji document_id=5301059317753979286>🖕</emoji> <b><a href="{}">{}</a>'
|
||||
" был замучен.</b>\n<b>Причина: </b><i>{}</i>\n\n{}"
|
||||
),
|
||||
"glmutes": (
|
||||
"<emoji document_id=5301059317753979286>🖕</emoji> <b>Гмут <a"
|
||||
' href="{}">{}</a>...</b>'
|
||||
),
|
||||
"gunmute": (
|
||||
'<emoji document_id=6334872157947955302>🤗</emoji> <b><a href="{}">{}</a>'
|
||||
" был размучен.</b>\n\n{}"
|
||||
),
|
||||
"gunmutes": (
|
||||
"<emoji document_id=6334872157947955302>🤗</emoji> <b>Гразмут <a"
|
||||
' href="{}">{}</a>...</b>'
|
||||
),
|
||||
"in_m_chats": (
|
||||
"<emoji document_id=5379568936218009290>👎</emoji> <b>Мут в {} чат(-ах)</b>"
|
||||
),
|
||||
"unmute_in_n_chats": (
|
||||
"<emoji document_id=5461129450341014019>✋️</emoji> <b>Размут in {}"
|
||||
" чат(-ах)</b>"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._gban_cache = {}
|
||||
self._gmute_cache = {}
|
||||
|
||||
@staticmethod
|
||||
def convert_time(t: str) -> int:
|
||||
"""
|
||||
Tries to export time from text
|
||||
"""
|
||||
try:
|
||||
if not str(t)[:-1].isdigit():
|
||||
return 0
|
||||
|
||||
if "d" in str(t):
|
||||
t = int(t[:-1]) * 60 * 60 * 24
|
||||
|
||||
if "h" in str(t):
|
||||
t = int(t[:-1]) * 60 * 60
|
||||
|
||||
if "m" in str(t):
|
||||
t = int(t[:-1]) * 60
|
||||
|
||||
if "s" in str(t):
|
||||
t = int(t[:-1])
|
||||
|
||||
t = int(re.sub(r"[^0-9]", "", str(t)))
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
return t
|
||||
|
||||
async def args_parser(
|
||||
self,
|
||||
message: Message,
|
||||
include_force: bool = False,
|
||||
include_silent: bool = False,
|
||||
) -> tuple:
|
||||
"""Get args from message"""
|
||||
args = " " + utils.get_args_raw(message)
|
||||
if include_force and " -f" in args:
|
||||
force = True
|
||||
args = args.replace(" -f", "")
|
||||
else:
|
||||
force = False
|
||||
|
||||
if include_silent and " -s" in args:
|
||||
silent = True
|
||||
args = args.replace(" -s", "")
|
||||
else:
|
||||
silent = False
|
||||
|
||||
args = args.strip()
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
|
||||
if reply and not args:
|
||||
return (
|
||||
(await self._client.get_entity(reply.sender_id)),
|
||||
0,
|
||||
utils.escape_html(self.strings("no_reason")).strip(),
|
||||
*((force,) if include_force else []),
|
||||
*((silent,) if include_silent else []),
|
||||
)
|
||||
|
||||
try:
|
||||
a = args.split()[0]
|
||||
if str(a).isdigit():
|
||||
a = int(a)
|
||||
user = await self._client.get_entity(a)
|
||||
except Exception:
|
||||
try:
|
||||
user = await self._client.get_entity(reply.sender_id)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
t = ([arg for arg in args.split() if self.convert_time(arg)] or ["0"])[0]
|
||||
args = args.replace(t, "").replace(" ", " ")
|
||||
t = self.convert_time(t)
|
||||
|
||||
if not reply:
|
||||
try:
|
||||
args = " ".join(args.split()[1:])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if time.time() + t >= 2208978000: # 01.01.2040 00:00:00
|
||||
t = 0
|
||||
|
||||
return (
|
||||
user,
|
||||
t,
|
||||
utils.escape_html(args or self.strings("no_reason")).strip(),
|
||||
*((force,) if include_force else []),
|
||||
*((silent,) if include_silent else []),
|
||||
)
|
||||
|
||||
async def ban(
|
||||
self,
|
||||
chat: typing.Union[Chat, int],
|
||||
user: typing.Union[User, Channel, int],
|
||||
period: int = 0,
|
||||
reason: str = None,
|
||||
message: typing.Optional[Message] = None,
|
||||
silent: bool = False,
|
||||
):
|
||||
"""Ban user in chat"""
|
||||
if str(user).isdigit():
|
||||
user = int(user)
|
||||
|
||||
if reason is None:
|
||||
reason = self.strings("no_reason")
|
||||
|
||||
try:
|
||||
await self.inline.bot.kick_chat_member(
|
||||
int(f"-100{getattr(chat, 'id', chat)}"),
|
||||
int(getattr(user, "id", user)),
|
||||
)
|
||||
except Exception:
|
||||
await self._client.edit_permissions(
|
||||
chat,
|
||||
user,
|
||||
until_date=(time.time() + period) if period else 0,
|
||||
**BANNED_RIGHTS,
|
||||
)
|
||||
|
||||
if silent:
|
||||
return
|
||||
|
||||
async def mute(
|
||||
self,
|
||||
chat: typing.Union[Chat, int],
|
||||
user: typing.Union[User, Channel, int],
|
||||
period: int = 0,
|
||||
reason: str = None,
|
||||
message: typing.Optional[Message] = None,
|
||||
silent: bool = False,
|
||||
):
|
||||
"""Mute user in chat"""
|
||||
if str(user).isdigit():
|
||||
user = int(user)
|
||||
|
||||
if reason is None:
|
||||
reason = self.strings("no_reason")
|
||||
|
||||
try:
|
||||
await self.inline.bot.restrict_chat_member(
|
||||
int(f"-100{getattr(chat, 'id', chat)}"),
|
||||
int(getattr(user, "id", user)),
|
||||
)
|
||||
except Exception:
|
||||
await self._client.edit_permissions(
|
||||
chat,
|
||||
user,
|
||||
until_date=(time.time() + period) if period else 0,
|
||||
**MUTES_RIGHTS,
|
||||
)
|
||||
|
||||
if silent:
|
||||
return
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<реплай | юзер> [причина] [-s] - Забанить пользователя во всех чатах где ты админ",
|
||||
en_doc="<replay | user> [reason] [-s] - Ban the user in all chats where you are the admin",
|
||||
)
|
||||
async def glban(self, message):
|
||||
reply = await message.get_reply_message()
|
||||
args = utils.get_args_raw(message)
|
||||
if not reply and not args:
|
||||
await utils.answer(message, self.strings("args"))
|
||||
return
|
||||
|
||||
a = await self.args_parser(message, include_silent=True)
|
||||
|
||||
if not a:
|
||||
await utils.answer(message, self.strings("args"))
|
||||
return
|
||||
|
||||
user, t, reason, silent = a
|
||||
|
||||
message = await utils.answer(
|
||||
message,
|
||||
self.strings("glbanning").format(
|
||||
utils.get_entity_url(user),
|
||||
utils.escape_html(get_full_name(user)),
|
||||
),
|
||||
)
|
||||
|
||||
if not self._gban_cache or self._gban_cache["exp"] < time.time():
|
||||
self._gban_cache = {
|
||||
"exp": int(time.time()) + 10 * 60,
|
||||
"chats": [
|
||||
chat.entity.id
|
||||
async for chat in self._client.iter_dialogs()
|
||||
if (
|
||||
(
|
||||
isinstance(chat.entity, Chat)
|
||||
or (
|
||||
isinstance(chat.entity, Channel)
|
||||
and getattr(chat.entity, "megagroup", False)
|
||||
)
|
||||
)
|
||||
and chat.entity.admin_rights
|
||||
and chat.entity.participants_count > 5
|
||||
and chat.entity.admin_rights.ban_users
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
chats = ""
|
||||
counter = 0
|
||||
|
||||
for chat in self._gban_cache["chats"]:
|
||||
try:
|
||||
await self.ban(chat, user, 0, reason, silent=True)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
chats += '▫️ <b><a href="{}">{}</a></b>\n'.format(
|
||||
utils.get_entity_url(await self._client.get_entity(chat, exp=0)),
|
||||
utils.escape_html(
|
||||
get_full_name(await self._client.get_entity(chat, exp=0))
|
||||
),
|
||||
)
|
||||
counter += 1
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("glban").format(
|
||||
utils.get_entity_url(user),
|
||||
utils.escape_html(get_full_name(user)),
|
||||
reason,
|
||||
self.strings("in_n_chats").format(counter) if silent else chats,
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<реплай | юзер> [причина] [-s] - Разбанить пользователя во всех где ты админ",
|
||||
en_doc="<replay | user> [reason] [-s] - To unban the user in all where you are the admin",
|
||||
)
|
||||
async def glunban(self, message: Message):
|
||||
reply = await message.get_reply_message()
|
||||
args = utils.get_args_raw(message)
|
||||
if not reply and not args:
|
||||
await utils.answer(message, self.strings("args"))
|
||||
return
|
||||
|
||||
a = await self.args_parser(message, include_silent=True)
|
||||
|
||||
if not a:
|
||||
await utils.answer(message, self.strings("args"))
|
||||
return
|
||||
|
||||
user, t, reason, silent = a
|
||||
|
||||
message = await utils.answer(
|
||||
message,
|
||||
self.strings("gunbanning").format(
|
||||
utils.get_entity_url(user),
|
||||
utils.escape_html(get_full_name(user)),
|
||||
),
|
||||
)
|
||||
|
||||
if not self._gban_cache or self._gban_cache["exp"] < time.time():
|
||||
self._gban_cache = {
|
||||
"exp": int(time.time()) + 10 * 60,
|
||||
"chats": [
|
||||
chat.entity.id
|
||||
async for chat in self._client.iter_dialogs()
|
||||
if (
|
||||
(
|
||||
isinstance(chat.entity, Chat)
|
||||
or (
|
||||
isinstance(chat.entity, Channel)
|
||||
and getattr(chat.entity, "megagroup", False)
|
||||
)
|
||||
)
|
||||
and chat.entity.admin_rights
|
||||
and chat.entity.participants_count > 5
|
||||
and chat.entity.admin_rights.ban_users
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
chats = ""
|
||||
counter = 0
|
||||
|
||||
for chat in self._gban_cache["chats"]:
|
||||
try:
|
||||
await self._client.edit_permissions(
|
||||
chat,
|
||||
user,
|
||||
until_date=0,
|
||||
**{right: True for right in BANNED_RIGHTS.keys()},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
chats += '▫️ <b><a href="{}">{}</a></b>\n'.format(
|
||||
utils.get_entity_url(await self._client.get_entity(chat, exp=0)),
|
||||
utils.escape_html(
|
||||
get_full_name(await self._client.get_entity(chat, exp=0))
|
||||
),
|
||||
)
|
||||
counter += 1
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("gunban").format(
|
||||
utils.get_entity_url(user),
|
||||
utils.escape_html(get_full_name(user)),
|
||||
(
|
||||
self.strings("unbanned_in_n_chats").format(counter)
|
||||
if silent
|
||||
else chats
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<реплай | юзер> [причина] [-s] - Замутить пользователя во всех чатах где ты админ",
|
||||
en_doc="<replay | user> [reason] [-s] - To hook up the user in all chats where you are the admin",
|
||||
)
|
||||
async def glmute(self, message):
|
||||
reply = await message.get_reply_message()
|
||||
args = utils.get_args_raw(message)
|
||||
if not reply and not args:
|
||||
await utils.answer(message, self.strings("args"))
|
||||
return
|
||||
|
||||
a = await self.args_parser(message, include_silent=True)
|
||||
|
||||
if not a:
|
||||
await utils.answer(message, self.strings("args"))
|
||||
return
|
||||
|
||||
user, t, reason, silent = a
|
||||
|
||||
message = await utils.answer(
|
||||
message,
|
||||
self.strings("glmutes").format(
|
||||
utils.get_entity_url(user),
|
||||
utils.escape_html(get_full_name(user)),
|
||||
),
|
||||
)
|
||||
|
||||
if not self._gmute_cache or self._gmute_cache["exp"] < time.time():
|
||||
self._gmute_cache = {
|
||||
"exp": int(time.time()) + 10 * 60,
|
||||
"chats": [
|
||||
chat.entity.id
|
||||
async for chat in self._client.iter_dialogs()
|
||||
if (
|
||||
(
|
||||
isinstance(chat.entity, Chat)
|
||||
or (
|
||||
isinstance(chat.entity, Channel)
|
||||
and getattr(chat.entity, "megagroup", False)
|
||||
)
|
||||
)
|
||||
and chat.entity.admin_rights
|
||||
and chat.entity.participants_count > 5
|
||||
and chat.entity.admin_rights.ban_users
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
chats = ""
|
||||
counter = 0
|
||||
|
||||
for chat in self._gmute_cache["chats"]:
|
||||
try:
|
||||
await self.mute(chat, user, 0, reason, silent=True)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
chats += '▫️ <b><a href="{}">{}</a></b>\n'.format(
|
||||
utils.get_entity_url(await self._client.get_entity(chat, exp=0)),
|
||||
utils.escape_html(
|
||||
get_full_name(await self._client.get_entity(chat, exp=0))
|
||||
),
|
||||
)
|
||||
counter += 1
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("glmute").format(
|
||||
utils.get_entity_url(user),
|
||||
utils.escape_html(get_full_name(user)),
|
||||
reason,
|
||||
self.strings("in_m_chats").format(counter) if silent else chats,
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<реплай | юзер> [причина] [-s] - Размутит пользователя во всех где ты админ",
|
||||
en_doc="<replay | user> [reason] [-s] - Will confuse the user in all where you are the admin",
|
||||
)
|
||||
async def glunmute(self, message: Message):
|
||||
reply = await message.get_reply_message()
|
||||
args = utils.get_args_raw(message)
|
||||
if not reply and not args:
|
||||
await utils.answer(message, self.strings("args"))
|
||||
return
|
||||
|
||||
a = await self.args_parser(message, include_silent=True)
|
||||
|
||||
if not a:
|
||||
await utils.answer(message, self.strings("args"))
|
||||
return
|
||||
|
||||
user, t, reason, silent = a
|
||||
|
||||
message = await utils.answer(
|
||||
message,
|
||||
self.strings("gunmutes").format(
|
||||
utils.get_entity_url(user),
|
||||
utils.escape_html(get_full_name(user)),
|
||||
),
|
||||
)
|
||||
|
||||
if not self._gmute_cache or self._gmute_cache["exp"] < time.time():
|
||||
self._gmute_cache = {
|
||||
"exp": int(time.time()) + 10 * 60,
|
||||
"chats": [
|
||||
chat.entity.id
|
||||
async for chat in self._client.iter_dialogs()
|
||||
if (
|
||||
(
|
||||
isinstance(chat.entity, Chat)
|
||||
or (
|
||||
isinstance(chat.entity, Channel)
|
||||
and getattr(chat.entity, "megagroup", False)
|
||||
)
|
||||
)
|
||||
and chat.entity.admin_rights
|
||||
and chat.entity.participants_count > 5
|
||||
and chat.entity.admin_rights.ban_users
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
chats = ""
|
||||
counter = 0
|
||||
|
||||
for chat in self._gmute_cache["chats"]:
|
||||
try:
|
||||
await self._client.edit_permissions(
|
||||
chat,
|
||||
user,
|
||||
until_date=0,
|
||||
**{right: True for right in MUTES_RIGHTS.keys()},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
chats += '▫️ <b><a href="{}">{}</a></b>\n'.format(
|
||||
utils.get_entity_url(await self._client.get_entity(chat, exp=0)),
|
||||
utils.escape_html(
|
||||
get_full_name(await self._client.get_entity(chat, exp=0))
|
||||
),
|
||||
)
|
||||
counter += 1
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("gunmute").format(
|
||||
utils.get_entity_url(user),
|
||||
utils.escape_html(get_full_name(user)),
|
||||
(
|
||||
self.strings("unmute_in_n_chats").format(counter)
|
||||
if silent
|
||||
else chats
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1,326 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: HikkaHost
|
||||
# Description: Hikkahost manager.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: api HikkaHost
|
||||
# scope: api HikkaHost 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HostApi:
|
||||
"""
|
||||
A class for interacting with a Host API.
|
||||
|
||||
Args:
|
||||
token (str): The API token.
|
||||
"""
|
||||
|
||||
def __init__(self, token: str):
|
||||
self.token = token
|
||||
|
||||
async def _request(self, path: str, method: str = "GET") -> dict:
|
||||
"""
|
||||
Sends a request to the API.
|
||||
|
||||
Args:
|
||||
path (str): The API path.
|
||||
method (str, optional): The HTTP method. Defaults to "GET".
|
||||
|
||||
Returns:
|
||||
dict: The API response as a dictionary.
|
||||
"""
|
||||
url = "https://api.hikka.host" + path
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True,
|
||||
timeout=aiohttp.ClientTimeout(total=15),
|
||||
) as session:
|
||||
async with session.request(
|
||||
method,
|
||||
url,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"token": self.token,
|
||||
},
|
||||
) as response:
|
||||
return await response.json()
|
||||
|
||||
async def stats(self, user_id: int) -> dict:
|
||||
"""
|
||||
Gets the host stats.
|
||||
|
||||
Args:
|
||||
user_id (int): The user ID.
|
||||
|
||||
Returns:
|
||||
dict: The host stats.
|
||||
"""
|
||||
url = f"/api/host/{user_id}/stats"
|
||||
return await self._request(url)
|
||||
|
||||
async def host_info(self, user_id: int) -> dict:
|
||||
"""
|
||||
Gets the host information.
|
||||
|
||||
Args:
|
||||
user_id (int): The user ID.
|
||||
|
||||
Returns:
|
||||
dict: The host information.
|
||||
"""
|
||||
url = f"/api/host/{user_id}"
|
||||
return await self._request(url)
|
||||
|
||||
async def status(self, user_id: int) -> dict:
|
||||
"""
|
||||
Gets the host status.
|
||||
|
||||
Args:
|
||||
user_id (int): The user ID.
|
||||
|
||||
Returns:
|
||||
dict: The host status.
|
||||
"""
|
||||
url = f"/api/host/{user_id}/status"
|
||||
return await self._request(url)
|
||||
|
||||
async def logs(self, user_id: int) -> dict:
|
||||
"""
|
||||
Gets the host logs.
|
||||
|
||||
Args:
|
||||
user_id (int): The user ID.
|
||||
|
||||
Returns:
|
||||
dict: The host logs.
|
||||
"""
|
||||
url = f"/api/host/{user_id}/logs/all"
|
||||
return await self._request(url)
|
||||
|
||||
async def action(self, user_id: int, action: str = "restart") -> dict:
|
||||
"""
|
||||
Performs an action on the host.
|
||||
|
||||
Args:
|
||||
user_id (int): The user ID.
|
||||
action (str, optional): The action to perform. Defaults to "restart".
|
||||
|
||||
Returns:
|
||||
dict: The action result.
|
||||
"""
|
||||
url = f"/api/host/{user_id}?action={action}"
|
||||
return await self._request(url, method="PUT")
|
||||
|
||||
|
||||
def bytes_to_megabytes(b: int):
|
||||
"""
|
||||
Converts bytes to megabytes.
|
||||
|
||||
Args:
|
||||
b (int): The number of bytes.
|
||||
|
||||
Returns:
|
||||
float: The number of megabytes.
|
||||
"""
|
||||
return round(b / 1024 / 1024, 1)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class HikkahostMod(loader.Module):
|
||||
"""Hikkahost manager."""
|
||||
|
||||
MAX_RAM = 750
|
||||
|
||||
strings = {
|
||||
"name": "HikkaHost",
|
||||
"info": (
|
||||
"<emoji document_id=5879770735999717115>👤</emoji> <b>Information panel</b>\n\n"
|
||||
"<emoji document_id=5974526806995242353>🆔</emoji> <b>Server ID:</b> <code>{server_id}</code>\n"
|
||||
"<emoji document_id=6005570495603282482>🔑</emoji> <b>ID:</b> <code>{id}</code>\n"
|
||||
"<emoji document_id=5874986954180791957>📶</emoji> <b>Status:</b> <code>{status}</code>\n"
|
||||
"<emoji document_id=5451646226975955576>⌛️</emoji> <b>Subscription ends:</b> <code>{end_dates}</code> | <code>{days_end} days</code>\n\n"
|
||||
"<emoji document_id=5877260593903177342>⚙️</emoji> <b>CPU:</b> <code>{cpu_percent} %</code>\n"
|
||||
"<emoji document_id=5379652232813750191>💾</emoji> <b>RAM:</b> <code>{memory} / {max_ram} MB</code> <b>{ram_percent} %</b>"
|
||||
),
|
||||
"logs": (
|
||||
"<emoji document_id=5188377234380954537>🌘</emoji> <b>Here are your logs</b>"
|
||||
),
|
||||
"restart": (
|
||||
"<emoji document_id=5789886476472815477>✅</emoji> <b>Restart request sent</b>\n"
|
||||
"This message remains unchanged after the restart"
|
||||
),
|
||||
"loading_info": "<emoji document_id=5451646226975955576>⌛️</emoji> Loading...",
|
||||
"no_apikey": "<emoji document_id=5260342697075416641>🚫</emoji> You have not specified an API Key\nTo get a token.\n\n1. Go to the @hikkahost_bot\n2. Write /token\n3. Paste it into the config",
|
||||
"condition": "works",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"info": (
|
||||
"<emoji document_id=5879770735999717115>👤</emoji> <b>Панель информации</b>\n\n"
|
||||
"<emoji document_id=5974526806995242353>🆔</emoji> <b>Server ID:</b> <code>{server_id}</code>\n"
|
||||
"<emoji document_id=6005570495603282482>🔑</emoji> <b>ID:</b> <code>{id}</code>\n"
|
||||
"<emoji document_id=5874986954180791957>📶</emoji> <b>Статус:</b> <code>{status}</code>\n"
|
||||
"<emoji document_id=5451646226975955576>⌛️</emoji> <b>Подписка закончится:</b> <code>{end_dates}</code> | <code>{days_end} дней</code>\n\n"
|
||||
"<emoji document_id=5877260593903177342>⚙️</emoji> <b>CPU:</b> <code>{cpu_percent} %</code>\n"
|
||||
"<emoji document_id=5379652232813750191>💾</emoji> <b>RAM:</b> <code>{memory} / {max_ram} MB</code> <b>{ram_percent} %</b>"
|
||||
),
|
||||
"logs": (
|
||||
"<emoji document_id=5188377234380954537>🌘</emoji> <b>Вот ваши логи</b>"
|
||||
),
|
||||
"restart": (
|
||||
"<emoji document_id=5789886476472815477>✅</emoji> <b>Запрос на рестарт отправил</b>\n"
|
||||
"Это сообщение не изменяется после рестарта"
|
||||
),
|
||||
"loading_info": "<emoji document_id=5451646226975955576>⌛️</emoji> Загрузка...",
|
||||
"no_apikey": "<emoji document_id=5260342697075416641>🚫</emoji> Вы не указали Api Key\nЧтобы получить token.\n\n1. Перейдите в бота @hikkahost_bot\n2. Напишите /token\n3. Вставьте его в конфиг",
|
||||
"condition": "работает",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.name = self.strings["name"]
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"token",
|
||||
None,
|
||||
validator=loader.validators.Hidden(),
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Статус HikkaHost",
|
||||
en_doc="Status HikkaHost",
|
||||
)
|
||||
async def hinfocmd(self, message):
|
||||
message = await utils.answer(message, self.strings("loading_info"))
|
||||
if self.config["token"] is None:
|
||||
await utils.answer(message, self.strings("no_apikey"))
|
||||
return
|
||||
|
||||
token = self.config["token"]
|
||||
user_id = token.split(":")[0]
|
||||
api = HostApi(token)
|
||||
|
||||
stats_data = await api.stats(user_id)
|
||||
host_data = await api.host_info(user_id)
|
||||
datas = await api.status(user_id)
|
||||
|
||||
memory = bytes_to_megabytes(stats_data["stats"]["memory_stats"]["usage"])
|
||||
cpu_percent = (
|
||||
round(
|
||||
(
|
||||
stats_data["stats"]["cpu_stats"]["cpu_usage"]["total_usage"]
|
||||
/ stats_data["stats"]["cpu_stats"]["system_cpu_usage"]
|
||||
)
|
||||
* 100.0,
|
||||
2,
|
||||
)
|
||||
if stats_data["stats"]["cpu_stats"]["cpu_usage"]["total_usage"]
|
||||
and stats_data["stats"]["cpu_stats"]["system_cpu_usage"]
|
||||
else None
|
||||
)
|
||||
ram_percent = round(
|
||||
bytes_to_megabytes(
|
||||
stats_data["stats"]["memory_stats"]["usage"] / self.MAX_RAM
|
||||
)
|
||||
* 100,
|
||||
2,
|
||||
)
|
||||
|
||||
server_id = host_data["host"]["server_id"]
|
||||
target_data = datetime.fromisoformat(
|
||||
host_data["host"]["end_date"].replace("Z", "+00:00")
|
||||
).replace(tzinfo=timezone.utc)
|
||||
current_data = datetime.now(timezone.utc)
|
||||
days_end = (target_data - current_data).days
|
||||
end_dates = (current_data + timedelta(days=days_end)).strftime("%d-%m-%Y")
|
||||
|
||||
if "status" in datas and datas["status"] == "running":
|
||||
status = self.strings("condition")
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("info").format(
|
||||
server_id=server_id,
|
||||
id=user_id,
|
||||
status=status,
|
||||
end_dates=end_dates,
|
||||
days_end=days_end,
|
||||
cpu_percent=cpu_percent,
|
||||
memory=memory,
|
||||
max_ram=self.MAX_RAM,
|
||||
ram_percent=ram_percent,
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Логи HikkaHost",
|
||||
en_doc="Logs HikkaHost",
|
||||
)
|
||||
async def hlogscmd(self, message):
|
||||
if self.config["token"] is None:
|
||||
await utils.answer(message, self.strings("no_apikey"))
|
||||
return
|
||||
|
||||
token = self.config["token"]
|
||||
user_id = token.split(":")[0]
|
||||
api = HostApi(token)
|
||||
data = await api.logs(user_id)
|
||||
|
||||
files_log = data["logs"]
|
||||
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
fd, tmp_path = tempfile.mkstemp(suffix=".txt", prefix="hikkahost_log_")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as log_file:
|
||||
json.dump(files_log, log_file)
|
||||
await utils.answer_file(message, tmp_path, self.strings("logs"))
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Рестарт HikkaHost",
|
||||
en_doc="Restart HikkaHost",
|
||||
)
|
||||
async def hrestartcmd(self, message):
|
||||
await utils.answer(message, self.strings("restart"))
|
||||
|
||||
if self.config["token"] is None:
|
||||
await utils.answer(message, self.strings("no_apikey"))
|
||||
return
|
||||
|
||||
token = self.config["token"]
|
||||
user_id = token.split(":")[0]
|
||||
api = HostApi(token)
|
||||
|
||||
await api.action(user_id)
|
||||
@@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>H:Mods</title>
|
||||
<link type=text/css href="https://github.com/C0dwiz/H.Modules/raw/assets/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>H:Mods modules</h1>
|
||||
<ul class="module-list">
|
||||
<li class=module-item><a href="ASCIIArt.py" class=module-link>ASCIIArt.py</a></li>
|
||||
<li class=module-item><a href="AccountData.py" class=module-link>AccountData.py</a></li>
|
||||
<li class=module-item><a href="AniLibria.py" class=module-link>AniLibria.py</a></li>
|
||||
<li class=module-item><a href="AnimeQuotes.py" class=module-link>AnimeQuotes.py</a></li>
|
||||
<li class=module-item><a href="Article.py" class=module-link>Article.py</a></li>
|
||||
<li class=module-item><a href="AutofarmCookies.py" class=module-link>AutofarmCookies.py</a></li>
|
||||
<li class=module-item><a href="BirthdayTime.py" class=module-link>BirthdayTime.py</a></li>
|
||||
<li class=module-item><a href="CheckSpamBan.py" class=module-link>CheckSpamBan.py</a></li>
|
||||
<li class=module-item><a href="CryptoCurrency.py" class=module-link>CryptoCurrency.py</a></li>
|
||||
<li class=module-item><a href="EnvsSH.py" class=module-link>EnvsSH.py</a></li>
|
||||
<li class=module-item><a href="FakeActions.py" class=module-link>FakeActions.py</a></li>
|
||||
<li class=module-item><a href="FakeWallet.py" class=module-link>FakeWallet.py</a></li>
|
||||
<li class=module-item><a href="GigaChat.py" class=module-link>GigaChat.py</a></li>
|
||||
<li class=module-item><a href="H.py" class=module-link>H.py</a></li>
|
||||
<li class=module-item><a href="HAFK.py" class=module-link>HAFK.py</a></li>
|
||||
<li class=module-item><a href="HModsLibrary.py" class=module-link>HModsLibrary.py</a></li>
|
||||
<li class=module-item><a href="InfoBannersManager.py" class=module-link>InfoBannersManager.py</a></li>
|
||||
<li class=module-item><a href="InlineButton.py" class=module-link>InlineButton.py</a></li>
|
||||
<li class=module-item><a href="InlineCoin.py" class=module-link>InlineCoin.py</a></li>
|
||||
<li class=module-item><a href="InlineHelper.py" class=module-link>InlineHelper.py</a></li>
|
||||
<li class=module-item><a href="IrisSimpleMod.py" class=module-link>IrisSimpleMod.py</a></li>
|
||||
<li class=module-item><a href="KBSwapper.py" class=module-link>KBSwapper.py</a></li>
|
||||
<li class=module-item><a href="Memes.py" class=module-link>Memes.py</a></li>
|
||||
<li class=module-item><a href="MooFarmRC1.py" class=module-link>MooFarmRC1.py</a></li>
|
||||
<li class=module-item><a href="Music.py" class=module-link>Music.py</a></li>
|
||||
<li class=module-item><a href="PastebinAPI.py" class=module-link>PastebinAPI.py</a></li>
|
||||
<li class=module-item><a href="ReplaceVowels.py" class=module-link>ReplaceVowels.py</a></li>
|
||||
<li class=module-item><a href="SMAcrhiver.py" class=module-link>SMAcrhiver.py</a></li>
|
||||
<li class=module-item><a href="SafetyMod.py" class=module-link>SafetyMod.py</a></li>
|
||||
<li class=module-item><a href="TaskManager.py" class=module-link>TaskManager.py</a></li>
|
||||
<li class=module-item><a href="TelegramStatusCodes.py" class=module-link>TelegramStatusCodes.py</a></li>
|
||||
<li class=module-item><a href="TempChat.py" class=module-link>TempChat.py</a></li>
|
||||
<li class=module-item><a href="Text2File.py" class=module-link>Text2File.py</a></li>
|
||||
<li class=module-item><a href="Text_Sticker.py" class=module-link>Text_Sticker.py</a></li>
|
||||
<li class=module-item><a href="TikTokDownloader.py" class=module-link>TikTokDownloader.py</a></li>
|
||||
<li class=module-item><a href="UserbotAvast.py" class=module-link>UserbotAvast.py</a></li>
|
||||
<li class=module-item><a href="Video2GIF.py" class=module-link>Video2GIF.py</a></li>
|
||||
<li class=module-item><a href="VirusTotal.py" class=module-link>VirusTotal.py</a></li>
|
||||
<li class=module-item><a href="VoiceDL.py" class=module-link>VoiceDL.py</a></li>
|
||||
<li class=module-item><a href="Weather.py" class=module-link>Weather.py</a></li>
|
||||
<li class=module-item><a href="WindowsKeys.py" class=module-link>WindowsKeys.py</a></li>
|
||||
<li class=module-item><a href="animals.py" class=module-link>animals.py</a></li>
|
||||
<li class=module-item><a href="face.py" class=module-link>face.py</a></li>
|
||||
<li class=module-item><a href="globalrestrict.py" class=module-link>globalrestrict.py</a></li>
|
||||
<li class=module-item><a href="hikkahost.py" class=module-link>hikkahost.py</a></li>
|
||||
<li class=module-item><a href="jacques.py" class=module-link>jacques.py</a></li>
|
||||
<li class=module-item><a href="novoice.py" class=module-link>novoice.py</a></li>
|
||||
<li class=module-item><a href="nsfwart.py" class=module-link>nsfwart.py</a></li>
|
||||
<li class=module-item><a href="numbersapi.py" class=module-link>numbersapi.py</a></li>
|
||||
<li class=module-item><a href="profile.py" class=module-link>profile.py</a></li>
|
||||
<li class=module-item><a href="search.py" class=module-link>search.py</a></li>
|
||||
<li class=module-item><a href="shortener.py" class=module-link>shortener.py</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,130 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Жаконизатор
|
||||
# Description: Жаконизатор
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Жаконизатор
|
||||
# scope: Жаконизатор 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import io
|
||||
import logging
|
||||
from textwrap import wrap
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class JacquesMod(loader.Module):
|
||||
"""Жаконизатор"""
|
||||
|
||||
strings = {"name": "Жаконизатор", "usage": "Write <code>.help Жаконизатор</code>"}
|
||||
|
||||
strings_ru = {"usage": "Напиши <code>.help Жаконизатор</code>"}
|
||||
|
||||
def __init__(self):
|
||||
self.name = self.strings["name"]
|
||||
self._me = None
|
||||
self._ratelimit = []
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"font",
|
||||
"https://github.com/Codwizer/ReModules/blob/main/assets/OpenSans-Light.ttf?raw=true",
|
||||
lambda: "добавьте ссылку на нужный вам шрифт",
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"location",
|
||||
"center",
|
||||
"Можно указать left, right или center",
|
||||
validator=loader.validators.Choice(["left", "right", "center"]),
|
||||
),
|
||||
)
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<реплай на сообщение/свой текст>",
|
||||
en_doc="<reply to the message/your own text>",
|
||||
)
|
||||
async def ionicmd(self, message):
|
||||
reply = await message.get_reply_message()
|
||||
args = utils.get_args_raw(message)
|
||||
|
||||
if not args:
|
||||
if not reply:
|
||||
await utils.answer(message, self.strings("usage", message))
|
||||
return
|
||||
else:
|
||||
txt = reply.raw_text
|
||||
else:
|
||||
txt = args
|
||||
|
||||
session = await self._get_session()
|
||||
async with session.get(self.config["font"]) as font_response:
|
||||
font_data = await font_response.read()
|
||||
|
||||
async with session.get(
|
||||
"https://raw.githubusercontent.com/Codwizer/ReModules/main/assets/IMG_20231128_152538.jpg"
|
||||
) as pic_response:
|
||||
pic_data = await pic_response.read()
|
||||
|
||||
img = Image.open(io.BytesIO(pic_data)).convert("RGB")
|
||||
|
||||
wrapped_text = "\n".join(wrap(txt, 19)) + "\n"
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype(io.BytesIO(font_data), 32, encoding="UTF-8")
|
||||
|
||||
text_bbox = draw.multiline_textbbox((0, 0), wrapped_text, font=font)
|
||||
text_size = (text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1])
|
||||
imtext = Image.new("RGBA", (text_size[0] + 10, text_size[1] + 10), (0, 0, 0, 0))
|
||||
draw_imtext = ImageDraw.Draw(imtext)
|
||||
draw_imtext.multiline_text(
|
||||
(10, 10), wrapped_text, (0, 0, 0), font=font, align=self.config["location"]
|
||||
)
|
||||
|
||||
imtext.thumbnail((350, 195))
|
||||
img.paste(imtext, (10, 10), imtext)
|
||||
|
||||
out = io.BytesIO()
|
||||
out.name = "hikka_mods.jpg"
|
||||
img.save(out)
|
||||
out.seek(0)
|
||||
|
||||
await message.client.send_file(message.to_id, out, reply_to=reply)
|
||||
await message.delete()
|
||||
@@ -1,770 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: MediaTools
|
||||
# Description: Powerful tools for working with media files
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: MediaTools
|
||||
# scope: MediaTools 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from typing import Optional
|
||||
|
||||
from telethon.types import Message
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def check_ffmpeg():
|
||||
return shutil.which("ffmpeg") is not None
|
||||
|
||||
|
||||
@loader.tds
|
||||
class MediaToolsMod(loader.Module):
|
||||
"""Powerful tools for working with media files"""
|
||||
|
||||
strings = {
|
||||
"name": "MediaTools",
|
||||
"no_reply": "🚫 Reply to a media file!",
|
||||
"no_ffmpeg": "❌ FFmpeg is not installed! Install: apt-get install ffmpeg",
|
||||
"processing": "⚙️ Processing...",
|
||||
"converted": "✅ Converted to {}",
|
||||
"downloaded": "✅ Voice message saved",
|
||||
"gif_created": "✅ GIF created",
|
||||
"cut_done": "✅ Trimmed",
|
||||
"circle_done": "✅ Video circle created",
|
||||
"audio_extracted": "✅ Audio extracted",
|
||||
"compressed": "✅ Compressed to {}",
|
||||
"split_done": "✅ Split into {} parts",
|
||||
"merged": "✅ Merged",
|
||||
"metadata_removed": "✅ Metadata removed",
|
||||
"invalid_args": "❌ Invalid arguments",
|
||||
"error": "❌ Error: {}",
|
||||
"available_formats": "Available formats:\n🎵 Audio: mp3, flac, wav, aac, ogg, m4a, opus\n🎬 Video: mp4, avi, mkv, mov, wmv, flv, webm, 3gp, hevc, h264",
|
||||
"cut_usage": "Usage: .cut 20s6ms:8m16s3ms",
|
||||
"compress_usage": "Available qualities: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p",
|
||||
"split_time_usage": "Example: .split 10m (10 minutes)",
|
||||
"split_size_usage": "Usage: .split 10m or .split 5MB",
|
||||
"merge_usage": "Reply to first video/audio",
|
||||
"min_files": "Need at least 2 media files in chain",
|
||||
"downloading": "Downloading {} files...",
|
||||
"part": "Part {}/{}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_reply": "🚫 Ответьте на медиафайл!",
|
||||
"no_ffmpeg": "❌ FFmpeg не установлен! Установите: apt-get install ffmpeg",
|
||||
"processing": "⚙️ Обрабатываю...",
|
||||
"converted": "✅ Конвертировано в {}",
|
||||
"downloaded": "✅ Голосовое сохранено",
|
||||
"gif_created": "✅ GIF создан",
|
||||
"cut_done": "✅ Обрезано",
|
||||
"circle_done": "✅ Видео в кружок",
|
||||
"audio_extracted": "✅ Аудио извлечено",
|
||||
"compressed": "✅ Сжато до {}",
|
||||
"split_done": "✅ Разделено на {} частей",
|
||||
"merged": "✅ Объединено",
|
||||
"metadata_removed": "✅ Метаданные удалены",
|
||||
"invalid_args": "❌ Неверные аргументы",
|
||||
"error": "❌ Ошибка: {}",
|
||||
"available_formats": "Доступные форматы:\n🎵 Аудио: mp3, flac, wav, aac, ogg, m4a, opus\n🎬 Видео: mp4, avi, mkv, mov, wmv, flv, webm, 3gp, hevc, h264",
|
||||
"cut_usage": "Используйте: .cut 20с6мс:8м16с3мс",
|
||||
"compress_usage": "Доступные качества: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p",
|
||||
"split_time_usage": "Пример: .split 10m (10 минут)",
|
||||
"split_size_usage": "Используйте: .split 10m или .split 5MB",
|
||||
"merge_usage": "Ответьте на первое видео/аудио",
|
||||
"min_files": "Нужно как минимум 2 медиафайла в цепочке",
|
||||
"downloading": "Скачиваю {} файлов...",
|
||||
"part": "Часть {}/{}",
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
self._db = db
|
||||
if not check_ffmpeg():
|
||||
self.logger.warning(self.strings["no_ffmpeg"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<формат> - конвертировать медиа в указанный формат",
|
||||
en_doc="<format> - convert media to specified format",
|
||||
)
|
||||
async def convert(self, message: Message):
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.media:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
args = utils.get_args_raw(message).lower()
|
||||
formats = {
|
||||
"mp3": "audio",
|
||||
"flac": "audio",
|
||||
"wav": "audio",
|
||||
"aac": "audio",
|
||||
"ogg": "audio",
|
||||
"m4a": "audio",
|
||||
"opus": "audio",
|
||||
"mp4": "video",
|
||||
"avi": "video",
|
||||
"mkv": "video",
|
||||
"mov": "video",
|
||||
"wmv": "video",
|
||||
"flv": "video",
|
||||
"webm": "video",
|
||||
"3gp": "video",
|
||||
"hevc": "video",
|
||||
"h264": "video",
|
||||
}
|
||||
|
||||
if not args or args not in formats:
|
||||
return await utils.answer(message, self.strings["available_formats"])
|
||||
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/")
|
||||
output = f"{file.rsplit('.', 1)[0]}_converted.{args}"
|
||||
|
||||
cmd = ["ffmpeg", "-i", file, "-y"]
|
||||
if formats[args] == "audio":
|
||||
if args == "mp3":
|
||||
cmd.extend(["-codec:a", "libmp3lame", "-q:a", "2"])
|
||||
elif args == "flac":
|
||||
cmd.extend(["-codec:a", "flac", "-compression_level", "12"])
|
||||
elif args == "opus":
|
||||
cmd.extend(["-codec:a", "libopus", "-b:a", "128k"])
|
||||
elif args == "aac":
|
||||
cmd.extend(["-codec:a", "aac", "-b:a", "192k"])
|
||||
elif formats[args] == "video":
|
||||
if args in ["hevc", "h264"]:
|
||||
codec = "libx265" if args == "hevc" else "libx264"
|
||||
cmd.extend(["-codec:v", codec, "-preset", "medium", "-crf", "23"])
|
||||
if args == "webm":
|
||||
cmd.extend(["-codec:v", "libvpx-vp9", "-b:v", "1M"])
|
||||
|
||||
cmd.append(output)
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["converted"].format(args),
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
os.remove(file)
|
||||
if os.path.exists(output):
|
||||
os.remove(output)
|
||||
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Скачать голосовое сообщение как файл",
|
||||
en_doc="Download voice message as file",
|
||||
)
|
||||
async def voicedl(self, message: Message):
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.voice:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/voice.ogg")
|
||||
new_file = file.replace(".ogg", ".opus")
|
||||
os.rename(file, new_file)
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
new_file,
|
||||
caption=self.strings["downloaded"],
|
||||
reply_to=reply.id,
|
||||
voice_note=False,
|
||||
)
|
||||
|
||||
os.remove(new_file)
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
@loader.command(ru_doc="Преобразовать видео в GIF", en_doc="Convert video to GIF")
|
||||
async def gif(self, message: Message):
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.video:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/")
|
||||
output = f"{file.rsplit('.', 1)[0]}.gif"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
file,
|
||||
"-vf",
|
||||
"fps=10,scale=480:-1:flags=lanczos",
|
||||
"-gifflags",
|
||||
"+transdiff",
|
||||
"-y",
|
||||
output,
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(*cmd)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["gif_created"],
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
os.remove(file)
|
||||
os.remove(output)
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
def parse_time(self, time_str: str) -> Optional[float]:
|
||||
time_str = time_str.lower()
|
||||
total = 0
|
||||
pattern = r"(\d+\.?\d*)([мm]?[сc]|[мm][сc]?)"
|
||||
matches = re.findall(pattern, time_str)
|
||||
|
||||
for value, unit in matches:
|
||||
value = float(value)
|
||||
if "м" in unit or "m" in unit:
|
||||
total += value * 60
|
||||
elif "с" in unit or "c" in unit:
|
||||
total += value
|
||||
|
||||
return total if total > 0 else None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<начало:конец> - обрезать медиа по времени",
|
||||
en_doc="<start:end> - trim media by time",
|
||||
)
|
||||
async def cut(self, message: Message):
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.media:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
if not args or ":" not in args:
|
||||
return await utils.answer(message, self.strings["cut_usage"])
|
||||
|
||||
start_str, end_str = args.split(":", 1)
|
||||
start = self.parse_time(start_str)
|
||||
end = self.parse_time(end_str)
|
||||
|
||||
if start is None or end is None or start >= end:
|
||||
return await utils.answer(message, self.strings["invalid_args"])
|
||||
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/")
|
||||
output = f"{file.rsplit('.', 1)[0]}_cut.{file.rsplit('.', 1)[1]}"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
file,
|
||||
"-ss",
|
||||
str(start),
|
||||
"-to",
|
||||
str(end),
|
||||
"-c",
|
||||
"copy",
|
||||
"-avoid_negative_ts",
|
||||
"make_zero",
|
||||
"-y",
|
||||
output,
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(*cmd)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["cut_done"],
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
os.remove(file)
|
||||
os.remove(output)
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[начало:конец] - Видео в кружок",
|
||||
en_doc="[start:end] - Convert video to circle",
|
||||
)
|
||||
async def vircle(self, message: Message):
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not (reply.video or reply.gif):
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
filter_args = ""
|
||||
|
||||
if args and ":" in args:
|
||||
start_str, end_str = args.split(":", 1)
|
||||
start = self.parse_time(start_str)
|
||||
end = self.parse_time(end_str)
|
||||
|
||||
if start is not None and end is not None and start < end:
|
||||
filter_args = f",trim=start={start}:end={end},setpts=PTS-STARTPTS"
|
||||
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/")
|
||||
output = f"{file.rsplit('.', 1)[0]}_circle.mp4"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
file,
|
||||
"-vf",
|
||||
f"scale=720:720:force_original_aspect_ratio=increase,crop=720:720{filter_args},format=rgba,geq='if(gt(X,360),if(gt(Y,360),if(lt(sqrt((X-360)^2+(Y-360)^2),360),p(X,Y),0),0),0)'",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"fast",
|
||||
"-crf",
|
||||
"23",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-y",
|
||||
output,
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(*cmd)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["circle_done"],
|
||||
reply_to=reply.id,
|
||||
video_note=True,
|
||||
)
|
||||
|
||||
os.remove(file)
|
||||
os.remove(output)
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[начало:конец] - Извлечь аудио из видео",
|
||||
en_doc="[start:end] - Extract audio from video",
|
||||
)
|
||||
async def vsound(self, message: Message):
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.video:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/")
|
||||
output = f"{file.rsplit('.', 1)[0]}_audio.mp3"
|
||||
|
||||
cmd = ["ffmpeg", "-i", file]
|
||||
if args and ":" in args:
|
||||
start_str, end_str = args.split(":", 1)
|
||||
start = self.parse_time(start_str)
|
||||
end = self.parse_time(end_str)
|
||||
|
||||
if start is not None and end is not None and start < end:
|
||||
cmd.extend(["-ss", str(start), "-to", str(end)])
|
||||
|
||||
cmd.extend(["-q:a", "2", "-map", "a", "-y", output])
|
||||
|
||||
process = await asyncio.create_subprocess_exec(*cmd)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["audio_extracted"],
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
os.remove(file)
|
||||
os.remove(output)
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<качество> - Сжать видео", en_doc="<quality> - Compress video"
|
||||
)
|
||||
async def compress(self, message: Message):
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.video:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
args = utils.get_args_raw(message).lower()
|
||||
resolutions = {
|
||||
"144p": "256x144",
|
||||
"240p": "426x240",
|
||||
"360p": "640x360",
|
||||
"480p": "854x480",
|
||||
"720p": "1280x720",
|
||||
"1080p": "1920x1080",
|
||||
"1440p": "2560x1440",
|
||||
"2160p": "3840x2160",
|
||||
}
|
||||
|
||||
if not args or args not in resolutions:
|
||||
return await utils.answer(message, self.strings["compress_usage"])
|
||||
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/")
|
||||
output = f"{file.rsplit('.', 1)[0]}_compressed.mp4"
|
||||
|
||||
probe_cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=bit_rate",
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
file,
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*probe_cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, _ = await process.communicate()
|
||||
original_bitrate = stdout.decode().strip()
|
||||
|
||||
scale_factor = {
|
||||
"144p": 0.1,
|
||||
"240p": 0.2,
|
||||
"360p": 0.3,
|
||||
"480p": 0.4,
|
||||
"720p": 0.6,
|
||||
"1080p": 0.8,
|
||||
"1440p": 0.9,
|
||||
"2160p": 1.0,
|
||||
}
|
||||
|
||||
target_bitrate = "500k"
|
||||
if original_bitrate and original_bitrate.isdigit():
|
||||
original_br = int(original_bitrate)
|
||||
target_br = int(original_br * scale_factor[args] / 1000)
|
||||
target_bitrate = f"{max(200, target_br)}k"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
file,
|
||||
"-vf",
|
||||
f"scale={resolutions[args]}:force_original_aspect_ratio=decrease",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"medium",
|
||||
"-b:v",
|
||||
target_bitrate,
|
||||
"-maxrate",
|
||||
target_bitrate,
|
||||
"-bufsize",
|
||||
f"{int(target_bitrate[:-1]) * 2}k",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"128k",
|
||||
"-y",
|
||||
output,
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(*cmd)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["compressed"].format(args),
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
os.remove(file)
|
||||
os.remove(output)
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<время/размер> - Разделить медиа на части",
|
||||
en_doc="<time/size> - Split media into parts",
|
||||
)
|
||||
async def split(self, message: Message):
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.media:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
args = utils.get_args_raw(message).lower()
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/")
|
||||
file_ext = file.rsplit(".", 1)[1]
|
||||
|
||||
if "m" in args or "м" in args:
|
||||
duration = self.parse_time(args)
|
||||
if not duration:
|
||||
await msg.edit(self.strings["split_time_usage"])
|
||||
os.remove(file)
|
||||
return
|
||||
|
||||
probe_cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
file,
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*probe_cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, _ = await process.communicate()
|
||||
total_duration = float(stdout.decode().strip())
|
||||
|
||||
parts = math.ceil(total_duration / duration)
|
||||
|
||||
for i in range(parts):
|
||||
start = i * duration
|
||||
end = min((i + 1) * duration, total_duration)
|
||||
output = f"{file.rsplit('.', 1)[0]}_part{i + 1}.{file_ext}"
|
||||
|
||||
split_cmd = [
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
file,
|
||||
"-ss",
|
||||
str(start),
|
||||
"-to",
|
||||
str(end),
|
||||
"-c",
|
||||
"copy",
|
||||
"-avoid_negative_ts",
|
||||
"make_zero",
|
||||
"-y",
|
||||
output,
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(*split_cmd)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["part"].format(i + 1, parts),
|
||||
reply_to=reply.id if i == 0 else None,
|
||||
)
|
||||
|
||||
os.remove(output)
|
||||
|
||||
await msg.edit(self.strings["split_done"].format(parts))
|
||||
|
||||
elif "mb" in args or "мб" in args:
|
||||
await utils.answer(
|
||||
message, "Size splitting requires additional implementation"
|
||||
)
|
||||
else:
|
||||
await msg.edit(self.strings["split_size_usage"])
|
||||
|
||||
os.remove(file)
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Объединить несколько медиафайлов", en_doc="Merge multiple media files"
|
||||
)
|
||||
async def merge(self, message: Message):
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply:
|
||||
return await utils.answer(message, self.strings["merge_usage"])
|
||||
|
||||
messages = []
|
||||
current = reply
|
||||
|
||||
while current and current.media:
|
||||
messages.append(current)
|
||||
current = await current.get_reply_message()
|
||||
|
||||
if len(messages) < 2:
|
||||
return await utils.answer(message, self.strings["min_files"])
|
||||
|
||||
msg = await utils.answer(
|
||||
message, self.strings["downloading"].format(len(messages))
|
||||
)
|
||||
|
||||
try:
|
||||
files = []
|
||||
file_list = "temp/filelist.txt"
|
||||
|
||||
with open(file_list, "w") as f:
|
||||
for i, msg_file in enumerate(messages):
|
||||
filename = f"temp/merge_{i}.{msg_file.file.ext if msg_file.file else 'mp4'}"
|
||||
await msg_file.download_media(file=filename)
|
||||
files.append(filename)
|
||||
f.write(f"file '{os.path.abspath(filename)}'\n")
|
||||
|
||||
output = "temp/merged.mp4"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
file_list,
|
||||
"-c",
|
||||
"copy",
|
||||
"-y",
|
||||
output,
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(*cmd)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["merged"],
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
for file in files:
|
||||
if os.path.exists(file):
|
||||
os.remove(file)
|
||||
os.remove(file_list)
|
||||
os.remove(output)
|
||||
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Удалить метаданные из медиа", en_doc="Remove metadata from media"
|
||||
)
|
||||
async def removemetadata(self, message: Message):
|
||||
if not check_ffmpeg():
|
||||
return await utils.answer(message, self.strings["no_ffmpeg"])
|
||||
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.media:
|
||||
return await utils.answer(message, self.strings["no_reply"])
|
||||
|
||||
msg = await utils.answer(message, self.strings["processing"])
|
||||
|
||||
try:
|
||||
file = await reply.download_media(file="temp/")
|
||||
file_ext = file.rsplit(".", 1)[1]
|
||||
output = f"{file.rsplit('.', 1)[0]}_nometa.{file_ext}"
|
||||
|
||||
cmd = ["ffmpeg", "-i", file, "-map_metadata", "-1", "-y", output]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(*cmd)
|
||||
await process.communicate()
|
||||
|
||||
await message.client.send_file(
|
||||
message.peer_id,
|
||||
output,
|
||||
caption=self.strings["metadata_removed"],
|
||||
reply_to=reply.id,
|
||||
)
|
||||
|
||||
os.remove(file)
|
||||
os.remove(output)
|
||||
await msg.delete()
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["error"].format(str(e)))
|
||||
@@ -1,160 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: NoVoice
|
||||
# Description: A module for prohibiting the sending of voice and video messages
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: NoVoice
|
||||
# scope: NoVoice 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from telethon.tl.custom import Message
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class NoVoiceMod(loader.Module):
|
||||
"""A module for prohibiting the sending of voice and video messages"""
|
||||
|
||||
strings = {
|
||||
"name": "NoVoice",
|
||||
"novoice_true": "❌ Voice messages are disabled for all users!",
|
||||
"novoice_false": "✅ Voice messages are allowed for all users again!",
|
||||
"novoice_no_args": "Usage: .novoice [on/off]",
|
||||
"novoiceuser_no_reply": "Usage: .novoiceuser [username/reply]",
|
||||
"novoiceuser_true": "❌ User {user_id} is now forbidden to send voice messages!",
|
||||
"novoicerm_no_reply": "Usage: .novoicerm [username/reply]",
|
||||
"novoicerm_yes": "✅ User {user_id} is now allowed to send voice messages again!",
|
||||
"novoicerm_no": "⚠️ User {user_id} not found in the banned list.",
|
||||
"text": "❌ I do not accept voice messages!",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"novoice_true": "❌ Голосовые сообщения отключены для всех пользователей!",
|
||||
"novoice_false": "✅ Голосовые сообщения снова разрешены для всех пользователей!",
|
||||
"novoice_no_args": "Использование: .novoice [on/off]",
|
||||
"novoiceuser_no_reply": "Использование: .novoiceuser [username/reply]",
|
||||
"novoiceuser_true": "❌ Пользователю {user_id} запрещено отправлять голосовые сообщения!",
|
||||
"novoicerm_no_reply": "Использование: .novoicerm [username/reply]",
|
||||
"novoicerm_yes": "✅ Пользователю {user_id} снова разрешено отправлять голосовые сообщения!",
|
||||
"novoicerm_no": "⚠ Пользователь {user_id} не найден в списке запрещенных.",
|
||||
"text": "❌ Я не принимаю голосовые сообщения!",
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
self.novoice_global = self.db.get("NoVoice", "global", False)
|
||||
self.banned_users = self.db.get("NoVoice", "banned_users", {})
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[on/off] — запрещает/разрешает всем пользователям отправку голосовых и видеосообщений.",
|
||||
en_doc="[on/off] — prohibits/allows all users to send voice and video messages.",
|
||||
)
|
||||
async def novoice(self, message):
|
||||
args = utils.get_args_raw(message)
|
||||
if args == "on":
|
||||
self.novoice_global = True
|
||||
self.db.set("NoVoice", "global", self.novoice_global)
|
||||
await utils.answer(message, self.strings("novoice_true"))
|
||||
elif args == "off":
|
||||
self.novoice_global = False
|
||||
self.db.set("NoVoice", "global", self.novoice_global)
|
||||
await utils.answer(message, self.strings("novoice_false"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("novoice_no_args"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[username/reply] — запрещает пользователю отправку голосовых и видеосообщений.",
|
||||
en_doc="[username/reply] — prohibits the user from sending voice and video messages.",
|
||||
)
|
||||
async def novoiceuser(self, message):
|
||||
args = utils.get_args_raw(message)
|
||||
reply = await message.get_reply_message()
|
||||
if not args and not reply:
|
||||
return await utils.answer(message, self.strings("novoiceuser_no_reply"))
|
||||
|
||||
if reply:
|
||||
user_id = reply.from_id
|
||||
else:
|
||||
user = await self.client.get_entity(args)
|
||||
user_id = user.id
|
||||
|
||||
self.banned_users[user_id] = True
|
||||
self.db.set("NoVoice", "banned_users", self.banned_users)
|
||||
await utils.answer(
|
||||
message, self.strings("novoiceuser_true").format(user_id=user_id)
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[username/reply] — разрешает пользователю отправку голосовых и видеосообщений.",
|
||||
en_doc="[username/reply] — allows the user to send voice and video messages.",
|
||||
)
|
||||
async def novoicerm(self, message):
|
||||
args = utils.get_args_raw(message)
|
||||
reply = await message.get_reply_message()
|
||||
|
||||
if not args and not reply:
|
||||
return await utils.answer(message, self.strings("novoicerm_no_reply"))
|
||||
|
||||
user_id = None
|
||||
if reply:
|
||||
user_id = reply.sender_id
|
||||
else:
|
||||
try:
|
||||
user = await self.client.get_entity(args)
|
||||
user_id = user.id
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get entity for {args}: {e}")
|
||||
|
||||
if user_id in self.banned_users:
|
||||
del self.banned_users[user_id]
|
||||
self.db.set("NoVoice", "banned_users", self.banned_users)
|
||||
await utils.answer(
|
||||
message, self.strings("novoicerm_yes").format(user_id=user_id)
|
||||
)
|
||||
else:
|
||||
await utils.answer(
|
||||
message, self.strings("novoicerm_no").format(user_id=user_id)
|
||||
)
|
||||
|
||||
async def watcher(self, message: Message):
|
||||
"""Обрабатывает входящие сообщения"""
|
||||
if (
|
||||
isinstance(message, Message)
|
||||
and not message.out
|
||||
and message.is_private
|
||||
and (self.novoice_global or message.sender_id in self.banned_users)
|
||||
and (message.voice or message.video_note)
|
||||
):
|
||||
await message.delete()
|
||||
await utils.answer(message, self.strings("text"))
|
||||
|
||||
logger.debug(
|
||||
"Deleted voice/video message from user %s in chat %s",
|
||||
message.sender_id,
|
||||
message.chat_id,
|
||||
)
|
||||
@@ -1,135 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: NSFWArt
|
||||
# Description: Sends cute anime nsfw-art
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api NSFWArt
|
||||
# scope: Api NSFWArt 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class NSFWArtMod(loader.Module):
|
||||
"""Sends cute anime nsfw-art"""
|
||||
|
||||
strings = {
|
||||
"name": "NSFWArt",
|
||||
"fetching": "<emoji document_id=5188311512791393083>🌎</emoji> Fetching NSFW art...",
|
||||
"no_results": "<emoji document_id=5854929766146118183>❌</emoji> No results found for this tag.",
|
||||
"api_error": "<emoji document_id=5854929766146118183>❌</emoji> API error: {error}",
|
||||
"network_error": "<emoji document_id=5854929766146118183>❌</emoji> Network error. Please try again later.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"fetching": "<emoji document_id=5188311512791393083>🌎</emoji> Получение NSFW арта...",
|
||||
"no_results": "<emoji document_id=5854929766146118183>❌</emoji> Ничего не найдено для этого тега.",
|
||||
"api_error": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка API: {error}",
|
||||
"network_error": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка сети. Попробуйте позже.",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"tags",
|
||||
"drool",
|
||||
lambda: "Tag for NSFW art (e.g., drool, masturbation, yuri, etc.)",
|
||||
)
|
||||
)
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
"""Get or create aiohttp session"""
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def _fetch_photos(self, tags: str, quantity: int = 15) -> Optional[List[str]]:
|
||||
"""Fetch photos from API"""
|
||||
session = await self._get_session()
|
||||
|
||||
try:
|
||||
url = f"https://api.lolicon.app/setu/v2?tag={tags}"
|
||||
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
if data.get("data") and len(data["data"]) > 0:
|
||||
return data["data"][0].get("urls", {}).get("original", [])
|
||||
return None
|
||||
else:
|
||||
logger.error(f"API error: {response.status}")
|
||||
return None
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("API timeout")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Fetch error: {e}")
|
||||
return None
|
||||
|
||||
async def _handle_error(self, message, error: Exception):
|
||||
"""Handle different types of errors"""
|
||||
if isinstance(error, asyncio.TimeoutError):
|
||||
await utils.answer(message, self.strings("network_error"))
|
||||
else:
|
||||
await utils.answer(
|
||||
message, self.strings("api_error").format(error=str(error))
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Отправить симпатичный NSFW-арт",
|
||||
en_doc="Send cute NSFW-art",
|
||||
)
|
||||
async def nsfwartcmd(self, message):
|
||||
"""Send NSFW art based on configured tags"""
|
||||
tags = self.config["tags"]
|
||||
|
||||
if not tags:
|
||||
await utils.answer(message, self.strings("no_results"))
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings("fetching"))
|
||||
|
||||
try:
|
||||
photos = await self._fetch_photos(tags)
|
||||
if not photos:
|
||||
await utils.answer(message, self.strings("no_results"))
|
||||
return
|
||||
|
||||
await self.inline.gallery(
|
||||
message=message,
|
||||
media=photos[:15],
|
||||
caption=f"<i>{utils.ascii_face()}</i>",
|
||||
)
|
||||
except Exception as e:
|
||||
await self._handle_error(message, e)
|
||||
@@ -1,167 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: NumbersAPI
|
||||
# Description: Many interesting facts about numbers.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: NumbersAPI
|
||||
# scope: NumbersAPI 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class NumbersAPI(loader.Module):
|
||||
"""Many interesting facts about numbers."""
|
||||
|
||||
strings = {
|
||||
"name": "NumbersAPI",
|
||||
"usage": "<emoji document_id=5854929766146118183>❌</emoji> Usage: .num <number or date> <type>\nExamples: .num 42 math, .num 01.15 date",
|
||||
"error_date_format": "<emoji document_id=5854929766146118183>❌</emoji> Invalid date format. Use: month.day (e.g., 01.15)",
|
||||
"error_number_format": "<emoji document_id=5854929766146118183>❌</emoji> Invalid number format.",
|
||||
"error_invalid_type": "<emoji document_id=5854929766146118183>❌</emoji> Invalid fact type. Available: math, trivia, date",
|
||||
"error_api": "<emoji document_id=5854929766146118183>❌</emoji> Failed to get fact. Please try again later.",
|
||||
"fetching": "<emoji document_id=5188311512791393083>🌎</emoji> Fetching fact...",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"usage": "<emoji document_id=5854929766146118183>❌</emoji> Использование: .num <число или дата> <тип>\nПримеры: .num 42 math, .num 01.15 date",
|
||||
"error_date_format": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат даты. Используйте: месяц.день (например, 01.15)",
|
||||
"error_number_format": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат числа.",
|
||||
"error_invalid_type": "<emoji document_id=5854929766146118183>❌</emoji> Неверный тип факта. Доступны: math, trivia, date",
|
||||
"error_api": "<emoji document_id=5854929766146118183>❌</emoji> Не удалось получить факт. Попробуйте позже.",
|
||||
"fetching": "<emoji document_id=5188311512791393083>🌎</emoji> Получение факта...",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
self.valid_fact_types = ["math", "trivia", "date"]
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
"""Get or create aiohttp session"""
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
def _parse_date(self, date_str: str) -> Optional[tuple[int, int]]:
|
||||
"""Parse date string in format MM.DD"""
|
||||
try:
|
||||
parts = date_str.split(".")
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
|
||||
month, day = map(int, parts)
|
||||
if not (1 <= month <= 12 and 1 <= day <= 31):
|
||||
return None
|
||||
|
||||
return month, day
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _parse_number(self, num_str: str) -> Optional[int]:
|
||||
"""Parse number string"""
|
||||
try:
|
||||
return int(num_str)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
async def _fetch_fact(self, url: str) -> str:
|
||||
"""Fetch fact from Numbers API"""
|
||||
session = await self._get_session()
|
||||
|
||||
try:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
return await response.text()
|
||||
else:
|
||||
logger.error(f"Numbers API error: {response.status}")
|
||||
return self.strings("error_api")
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Numbers API timeout")
|
||||
return self.strings("error_api")
|
||||
except Exception as e:
|
||||
logger.error(f"Numbers API error: {e}")
|
||||
return self.strings("error_api")
|
||||
|
||||
async def _get_number_fact(self, number: int, fact_type: str) -> str:
|
||||
"""Get fact about number"""
|
||||
url = f"https://numbersapi.com/{number}/{fact_type}"
|
||||
return await self._fetch_fact(url)
|
||||
|
||||
async def _get_date_fact(self, month: int, day: int) -> str:
|
||||
"""Get fact about date"""
|
||||
date_str = f"{month:02d}/{day:02d}"
|
||||
url = f"https://numbersapi.com/{date_str}/date"
|
||||
return await self._fetch_fact(url)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Дает интересный факт про число или дату\nНапример: .num 10 math или .num 01.01 date",
|
||||
en_doc="Gives an interesting fact about a number or date\nexample: .num 10 math or .num 01.01 date",
|
||||
)
|
||||
async def num(self, message):
|
||||
"""Get interesting fact about number or date"""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("usage"))
|
||||
return
|
||||
|
||||
parts = args.split(maxsplit=1)
|
||||
if len(parts) < 2:
|
||||
await utils.answer(message, self.strings("usage"))
|
||||
return
|
||||
|
||||
input_value = parts[0].strip()
|
||||
fact_type = parts[1].strip().lower()
|
||||
|
||||
if fact_type not in self.valid_fact_types:
|
||||
await utils.answer(message, self.strings("error_invalid_type"))
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings("fetching"))
|
||||
|
||||
if "." in input_value:
|
||||
date_parts = self._parse_date(input_value)
|
||||
if date_parts is None:
|
||||
await utils.answer(message, self.strings("error_date_format"))
|
||||
return
|
||||
|
||||
month, day = date_parts
|
||||
result = await self._get_date_fact(month, day)
|
||||
else:
|
||||
number = self._parse_number(input_value)
|
||||
if number is None:
|
||||
await utils.answer(message, self.strings("error_number_format"))
|
||||
return
|
||||
|
||||
result = await self._get_number_fact(number, fact_type)
|
||||
|
||||
await utils.answer(message, result)
|
||||
@@ -1,105 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: PassgenMod
|
||||
# Description: Generates random password
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api SafetyMod
|
||||
# scope: Api SafetyMod 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import secrets
|
||||
import string
|
||||
import logging
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def generate_password(
|
||||
length: int, letters: bool = True, numbers: bool = True, symbols: bool = True
|
||||
) -> str:
|
||||
"""Generates a random password with customizable options.
|
||||
|
||||
Args:
|
||||
length: The desired length of the password.
|
||||
letters: Include lowercase and uppercase letters (default: True).
|
||||
numbers: Include digits (default: True).
|
||||
symbols: Include common symbols (default: True).
|
||||
|
||||
Returns:
|
||||
A randomly generated password string.
|
||||
|
||||
Raises:
|
||||
ValueError: If all character sets are disabled (letters, numbers, symbols).
|
||||
"""
|
||||
character_sets = []
|
||||
if letters:
|
||||
character_sets.append(string.ascii_letters)
|
||||
if numbers:
|
||||
character_sets.append(string.digits)
|
||||
if symbols:
|
||||
character_sets.append(string.punctuation)
|
||||
|
||||
if not character_sets:
|
||||
raise ValueError("At least one of letters, numbers, or symbols must be True")
|
||||
|
||||
combined_characters = "".join(character_sets)
|
||||
password = "".join(secrets.choice(combined_characters) for _ in range(length))
|
||||
return password
|
||||
|
||||
|
||||
@loader.tds
|
||||
class PassgenMod(loader.Module):
|
||||
"""generate random password"""
|
||||
|
||||
strings = {
|
||||
"name": "Passgen",
|
||||
"pass": "<emoji document_id=5472287483318245416>*⃣</emoji> <b>Here is your secure password:</b> <code>{}</code>",
|
||||
}
|
||||
strings_ru = {
|
||||
"pass": "<emoji document_id=5472287483318245416>*⃣</emoji> <b>Вот ваш безопасный пароль:</b> <code>{}</code>"
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Случайный пароль\n-n - цифры\n-s - символы \n -l - буквы",
|
||||
en_doc="Random password\n-n - numbers\n-s - symbols \n -l - letters",
|
||||
)
|
||||
async def password(self, message):
|
||||
"""random password\n-n - numbers\n-s - symbols \n -l - letters"""
|
||||
text = message.text.split()
|
||||
length = 10
|
||||
letters = True
|
||||
numbers = False
|
||||
symbols = False
|
||||
for i in text:
|
||||
if i.startswith("password"):
|
||||
length = int(i.split("password")[1])
|
||||
elif i == "-n":
|
||||
numbers = True
|
||||
elif i == "-s":
|
||||
symbols = True
|
||||
elif i == "-l":
|
||||
letters = True
|
||||
password = generate_password(
|
||||
length=length, letters=letters, numbers=numbers, symbols=symbols
|
||||
)
|
||||
await utils.answer(message, self.strings("pass").format(password))
|
||||
@@ -1,350 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Privacy
|
||||
# Description: Module for fast privacy settings management
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Privacy
|
||||
# scope: Privacy 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import telethon
|
||||
from telethon import types
|
||||
|
||||
from .. import inline, loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class PrivacyMod(loader.Module):
|
||||
"""Module for fast privacy settings management"""
|
||||
|
||||
_PRIVACY_TYPES = {
|
||||
"phone": types.InputPrivacyKeyPhoneNumber,
|
||||
"add_by_phone": types.InputPrivacyKeyAddedByPhone,
|
||||
"online": types.InputPrivacyKeyStatusTimestamp,
|
||||
"photos": types.InputPrivacyKeyProfilePhoto,
|
||||
"forwards": types.InputPrivacyKeyForwards,
|
||||
"calls": types.InputPrivacyKeyPhoneCall,
|
||||
"p2p": types.InputPrivacyKeyPhoneP2P,
|
||||
"voices": types.InputPrivacyKeyVoiceMessages,
|
||||
"bio": getattr(types, "InputPrivacyKeyAbout", None),
|
||||
"invites": types.InputPrivacyKeyChatInvite,
|
||||
}
|
||||
|
||||
_PRIVACY_RULES = {
|
||||
types.PrivacyValueAllowAll: types.InputPrivacyValueAllowAll,
|
||||
types.PrivacyValueAllowChatParticipants: types.InputPrivacyValueAllowChatParticipants,
|
||||
types.PrivacyValueAllowContacts: types.InputPrivacyValueAllowContacts,
|
||||
types.PrivacyValueAllowUsers: types.InputPrivacyValueAllowUsers,
|
||||
types.PrivacyValueDisallowAll: types.InputPrivacyValueDisallowAll,
|
||||
types.PrivacyValueDisallowChatParticipants: types.InputPrivacyValueDisallowChatParticipants,
|
||||
types.PrivacyValueDisallowContacts: types.InputPrivacyValueDisallowContacts,
|
||||
types.PrivacyValueDisallowUsers: types.InputPrivacyValueDisallowUsers,
|
||||
}
|
||||
|
||||
strings = {
|
||||
"name": "Privacy",
|
||||
"privacy_types": (
|
||||
"<emoji document_id=5974492756494519709>🔗</emoji> <b>Available privacy types:</b>\n"
|
||||
),
|
||||
"no_user": "<emoji document_id=5312383351217201533>⚠️</emoji> <b>User not specified</b>",
|
||||
"u_silly": (
|
||||
"<emoji document_id=5449682572223194186>🥺</emoji> <b>You can't set privacy exceptions for yourself</b>"
|
||||
),
|
||||
"choose_type": "🔑 <b>Select privacy type to manage</b>",
|
||||
"not_supported_type": (
|
||||
"<emoji document_id=5312383351217201533>⚠️</emoji> <b>Privacy type '{}' not supported</b>"
|
||||
),
|
||||
"allowed": (
|
||||
"<emoji document_id=5298609004551887592>💕</emoji> <b>{user} added to allowed users for [{type}]</b>"
|
||||
),
|
||||
"disallowed": (
|
||||
"<emoji document_id=5224379368242965520>💔</emoji> <b>{user} added to disallowed users for [{type}]</b>"
|
||||
),
|
||||
"privacy": {
|
||||
"phone": "Phone Number",
|
||||
"add_by_phone": "Find by Phone",
|
||||
"p2p": "P2P Calls",
|
||||
"online": "Last Seen",
|
||||
"photos": "Profile Photos",
|
||||
"forwards": "Message Forwards",
|
||||
"calls": "Phone Calls",
|
||||
"voices": "Voice Messages",
|
||||
"bio": "Bio",
|
||||
"invites": "Chat Invites",
|
||||
},
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль для быстрого управления настройками приватности",
|
||||
"privacy_types": (
|
||||
"<emoji document_id=5974492756494519709>🔗</emoji> <b>Доступные типы приватности:</b>\n"
|
||||
),
|
||||
"no_user": "<emoji document_id=5312383351217201533>⚠️</emoji> <b>Пользователь не указан</b>",
|
||||
"u_silly": (
|
||||
"<emoji document_id=5449682572223194186>🥺</emoji> <b>Нельзя установить исключения приватности для самого себя</b>"
|
||||
),
|
||||
"choose_type": "🔑 <b>Выберите тип настроек приватности</b>",
|
||||
"not_supported_type": (
|
||||
"<emoji document_id=5312383351217201533>⚠️</emoji> <b>Тип приватности '{}' не поддерживается</b>"
|
||||
),
|
||||
"allowed": (
|
||||
"<emoji document_id=5298609004551887592>💕</emoji> <b>{user} добавлен в разрешённые для [{type}]</b>"
|
||||
),
|
||||
"disallowed": (
|
||||
"<emoji document_id=5224379368242965520>💔</emoji> <b>{user} добавлен в запрещённые для [{type}]</b>"
|
||||
),
|
||||
"privacy": {
|
||||
"phone": "Номер телефона",
|
||||
"add_by_phone": "Поиск по номеру",
|
||||
"p2p": "P2P звонки",
|
||||
"online": "Последний вход",
|
||||
"photos": "Фотографии профиля",
|
||||
"forwards": "Пересылка сообщений",
|
||||
"calls": "Звонки",
|
||||
"voices": "Голосовые сообщения",
|
||||
"bio": "О себе",
|
||||
"invites": "Приглашения в чаты",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._client: Optional[telethon.TelegramClient] = None
|
||||
self._me: Optional[types.User] = None
|
||||
|
||||
async def client_ready(self, client: telethon.TelegramClient, db) -> None:
|
||||
"""Initialize client and get current user"""
|
||||
self._client = client
|
||||
self._me = await client.get_me()
|
||||
|
||||
async def _get_user_id(self, message: types.Message) -> Optional[int]:
|
||||
"""Extract user ID from reply or username"""
|
||||
reply = await message.get_reply_message()
|
||||
if reply:
|
||||
return reply.sender_id
|
||||
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
return None
|
||||
|
||||
username = args[0]
|
||||
match = re.search(r"(?:t\.me/|@|^(\w+)\.t\.me$)([a-zA-Z0-9_.]+)", username)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
username = match.group(1) or match.group(2)
|
||||
try:
|
||||
entity = await self._client.get_entity(username)
|
||||
return getattr(entity, "user_id", getattr(entity, "id", None))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _format_privacy_list(self) -> str:
|
||||
"""Format privacy types list"""
|
||||
lines = [self.strings["privacy_types"]]
|
||||
|
||||
for key, name in self.strings["privacy"].items():
|
||||
if key in self._PRIVACY_TYPES:
|
||||
lines.append(f" <code>{key}</code> — {name}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _create_privacy_keyboard(
|
||||
self, user: types.User, action: str
|
||||
) -> List[List[Dict]]:
|
||||
"""Create inline keyboard for privacy type selection"""
|
||||
buttons = []
|
||||
|
||||
for key, name in self.strings["privacy"].items():
|
||||
if key not in self._PRIVACY_TYPES:
|
||||
continue
|
||||
|
||||
buttons.append(
|
||||
[
|
||||
{
|
||||
"text": name,
|
||||
"callback": self._privacy_callback_handler,
|
||||
"args": (user, key, action),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
return [buttons[i : i + 2] for i in range(0, len(buttons), 2)]
|
||||
|
||||
async def _privacy_callback_handler(self, call: inline.types.InlineCall) -> None:
|
||||
"""Handle privacy type selection callback"""
|
||||
user, privacy_key, action = call.data
|
||||
|
||||
if privacy_key not in self._PRIVACY_TYPES:
|
||||
await call.answer(self.strings["not_supported_type"].format(privacy_key))
|
||||
return
|
||||
|
||||
privacy_type = self._PRIVACY_TYPES[privacy_key]
|
||||
await self._update_privacy_settings(user, privacy_type, action)
|
||||
|
||||
action_text = "allowed" if action == "allow" else "disallowed"
|
||||
await call.edit(
|
||||
self.strings[action_text].format(
|
||||
user=telethon.utils.get_display_name(user),
|
||||
type=self.strings["privacy"][privacy_key],
|
||||
)
|
||||
)
|
||||
|
||||
async def _update_privacy_settings(
|
||||
self, user: types.User, privacy_key: types.TypeInputPrivacyKey, action: str
|
||||
) -> None:
|
||||
"""Update privacy settings for specified user"""
|
||||
try:
|
||||
current_rules = await self._client(
|
||||
telethon.functions.account.GetPrivacyRequest(key=privacy_key)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting privacy rules: {e}")
|
||||
return
|
||||
|
||||
new_rules = []
|
||||
existing_user_ids = set()
|
||||
|
||||
for rule in current_rules.rules:
|
||||
rule_type = type(rule)
|
||||
|
||||
if rule_type == types.PrivacyValueAllowUsers:
|
||||
for user_id in rule.users:
|
||||
existing_user_ids.add(user_id)
|
||||
if user_id != user.id:
|
||||
new_rules.append(rule)
|
||||
elif rule_type == types.PrivacyValueDisallowUsers:
|
||||
for user_id in rule.users:
|
||||
existing_user_ids.add(user_id)
|
||||
if user_id != user.id:
|
||||
new_rules.append(rule)
|
||||
|
||||
if action == "allow" and user.id not in existing_user_ids:
|
||||
new_rules.append(
|
||||
types.InputPrivacyValueAllowUsers(
|
||||
[types.InputUser(user.id, user.access_hash)]
|
||||
)
|
||||
)
|
||||
elif action == "disallow" and user.id in existing_user_ids:
|
||||
for rule in new_rules[:]:
|
||||
if type(rule) in [
|
||||
types.PrivacyValueAllowUsers,
|
||||
types.PrivacyValueDisallowUsers,
|
||||
]:
|
||||
user_list = getattr(rule, "users", [])
|
||||
if user.id in user_list:
|
||||
user_list.remove(user.id)
|
||||
if not user_list:
|
||||
new_rules.remove(rule)
|
||||
break
|
||||
|
||||
try:
|
||||
await self._client(
|
||||
telethon.functions.account.SetPrivacyRequest(
|
||||
key=privacy_key, rules=new_rules
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating privacy settings: {e}")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Список типов настроек приватности",
|
||||
en_doc="List of privacy types"
|
||||
)
|
||||
async def privacytypescmd(self, message: types.Message) -> None:
|
||||
"""Show available privacy types"""
|
||||
await utils.answer(message, self._format_privacy_list())
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<user> Добавить в разрешённые",
|
||||
en_doc="<user> Add to allowed list",
|
||||
)
|
||||
async def allowusercmd(self, message: types.Message) -> None:
|
||||
"""Add user to privacy exceptions"""
|
||||
user_id = await self._get_user_id(message)
|
||||
if not user_id:
|
||||
return await utils.answer(message, self.strings["no_user"])
|
||||
|
||||
if user_id == self._me.id:
|
||||
return await utils.answer(message, self.strings["u_silly"])
|
||||
|
||||
try:
|
||||
user_entity = await self._client.get_entity(user_id)
|
||||
except Exception:
|
||||
return await utils.answer(message, self.strings["no_user"])
|
||||
|
||||
await self.inline.form(
|
||||
message=message,
|
||||
text=self.strings["choose_type"],
|
||||
reply_markup=self._create_privacy_keyboard(user_entity, "allow"),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="<user> Добавить в запрещённые",
|
||||
en_doc="<user> Add to forbidden list",
|
||||
)
|
||||
async def disallowuser(self, message: types.Message) -> None:
|
||||
"""Add user to privacy restrictions"""
|
||||
user_id = await self._get_user_id(message)
|
||||
if not user_id:
|
||||
return await utils.answer(message, self.strings["no_user"])
|
||||
|
||||
if user_id == self._me.id:
|
||||
return await utils.answer(message, self.strings["u_silly"])
|
||||
|
||||
try:
|
||||
user_entity = await self._client.get_entity(user_id)
|
||||
except Exception:
|
||||
return await utils.answer(message, self.strings["no_user"])
|
||||
|
||||
await self.inline.form(
|
||||
message=message,
|
||||
text=self.strings["choose_type"],
|
||||
reply_markup=self._create_privacy_keyboard(user_entity, "disallow"),
|
||||
)
|
||||
|
||||
async def _create_privacy_keyboard(
|
||||
self, user: types.User, action: str
|
||||
) -> List[List[Dict]]:
|
||||
"""Create inline keyboard for privacy type selection"""
|
||||
buttons = []
|
||||
|
||||
for key, name in self.strings["privacy"].items():
|
||||
if key not in self._PRIVACY_TYPES:
|
||||
continue
|
||||
|
||||
buttons.append(
|
||||
[
|
||||
{
|
||||
"text": name,
|
||||
"callback": self._privacy_callback_handler,
|
||||
"args": (user, key, action),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
return [buttons[i : i + 2] for i in range(0, len(buttons), 2)]
|
||||
@@ -1,180 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Profile
|
||||
# Description: This module can change your Telegram profile
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Profile
|
||||
# scope: Profile 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from telethon.errors.rpcerrorlist import UsernameOccupiedError, FloodWaitError
|
||||
from telethon.tl.functions.account import UpdateProfileRequest, UpdateUsernameRequest
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class ProfileEditorMod(loader.Module):
|
||||
"""This module can change your Telegram profile."""
|
||||
|
||||
strings = {
|
||||
"name": "Profile",
|
||||
"error_format": "<emoji document_id=5854929766146118183>❌</emoji> Incorrect format. Try again.",
|
||||
"done_name": "<emoji document_id=5854762571659218443>✅</emoji> Name successfully updated!",
|
||||
"done_bio": "<emoji document_id=5854762571659218443>✅</emoji> Bio successfully updated!",
|
||||
"done_username": "<emoji document_id=5854762571659218443>✅</emoji> Username successfully updated!",
|
||||
"error_occupied": "<emoji document_id=5854929766146118183>❌</emoji> Username is already occupied!",
|
||||
"error_invalid_username": "<emoji document_id=5854929766146118183>❌</emoji> Invalid username format!",
|
||||
"error_flood": "<emoji document_id=5854929766146118183>❌</emoji> Too many requests. Try again later.",
|
||||
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> An error occurred: {error}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"error_format": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат. Попробуйте еще раз.",
|
||||
"done_name": "<emoji document_id=5854762571659218443>✅</emoji> Имя успешно обновлено!",
|
||||
"done_bio": "<emoji document_id=5854762571659218443>✅</emoji> Био успешно обновлено!",
|
||||
"done_username": "<emoji document_id=5854762571659218443>✅</emoji> Имя пользователя успешно обновлено!",
|
||||
"error_occupied": "<emoji document_id=5854929766146118183>❌</emoji> Имя пользователя уже занято!",
|
||||
"error_invalid_username": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат имени пользователя!",
|
||||
"error_flood": "<emoji document_id=5854929766146118183>❌</emoji> Слишком много запросов. Попробуйте позже.",
|
||||
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка: {error}",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _validate_username(self, username: str) -> bool:
|
||||
"""Validate username format"""
|
||||
if not username:
|
||||
return False
|
||||
|
||||
username = username.strip("@")
|
||||
if len(username) < 5 or len(username) > 32:
|
||||
return False
|
||||
|
||||
return re.match(r"^[a-zA-Z0-9_]+$", username) is not None
|
||||
|
||||
def _sanitize_name(self, name: str) -> str:
|
||||
"""Sanitize name input"""
|
||||
if not name:
|
||||
return ""
|
||||
|
||||
return " ".join(name.split())[:64]
|
||||
|
||||
def _sanitize_bio(self, bio: str) -> str:
|
||||
"""Sanitize bio input"""
|
||||
if not bio:
|
||||
return ""
|
||||
|
||||
bio = bio.strip()
|
||||
limit = 70 if not self._client.hikka_me.premium else 140
|
||||
if len(bio) < limit:
|
||||
return bio[:limit]
|
||||
else:
|
||||
return bio[: limit - 3] + "..."
|
||||
|
||||
async def _handle_error(self, message, error: Exception):
|
||||
"""Handle common errors"""
|
||||
if isinstance(error, UsernameOccupiedError):
|
||||
await utils.answer(message, self.strings("error_occupied"))
|
||||
elif isinstance(error, FloodWaitError):
|
||||
await utils.answer(message, self.strings("error_flood"))
|
||||
else:
|
||||
await utils.answer(
|
||||
message, self.strings("error_general").format(error=str(error))
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="для того, чтобы сменить свое имя/отчество",
|
||||
en_doc="for change your first/second name",
|
||||
)
|
||||
async def namecmd(self, message):
|
||||
"""Change first name and last name"""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings("error_format"))
|
||||
|
||||
if "/" in args:
|
||||
parts = args.split("/", 1)
|
||||
else:
|
||||
parts = args.split(" ", 1)
|
||||
|
||||
if len(parts) < 1:
|
||||
return await utils.answer(message, self.strings("error_format"))
|
||||
|
||||
firstname = self._sanitize_name(parts[0])
|
||||
lastname = self._sanitize_name(parts[1]) if len(parts) > 1 else ""
|
||||
|
||||
if not firstname:
|
||||
return await utils.answer(message, self.strings("error_format"))
|
||||
|
||||
try:
|
||||
await message.client(
|
||||
UpdateProfileRequest(first_name=firstname, last_name=lastname)
|
||||
)
|
||||
await utils.answer(message, self.strings("done_name"))
|
||||
except Exception as e:
|
||||
await self._handle_error(message, e)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="для изменения вашего имени пользователя. Введите значение без '@'",
|
||||
en_doc="for change your username. Enter value without '@'",
|
||||
)
|
||||
async def aboutcmd(self, message):
|
||||
"""Change profile bio"""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings("error_format"))
|
||||
|
||||
bio = self._sanitize_bio(args)
|
||||
|
||||
try:
|
||||
await message.client(UpdateProfileRequest(about=bio))
|
||||
await utils.answer(message, self.strings("done_bio"))
|
||||
except Exception as e:
|
||||
await self._handle_error(message, e)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="для изменения вашего имени пользователя. Введите значение без '@'",
|
||||
en_doc="for change your username. Enter value without '@'",
|
||||
)
|
||||
async def usercmd(self, message):
|
||||
"""Change username"""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
return await utils.answer(message, self.strings("error_format"))
|
||||
|
||||
username = args.strip("@")
|
||||
|
||||
if not self._validate_username(username):
|
||||
return await utils.answer(message, self.strings("error_invalid_username"))
|
||||
|
||||
try:
|
||||
await message.client(UpdateUsernameRequest(username))
|
||||
await utils.answer(message, self.strings("done_username"))
|
||||
except Exception as e:
|
||||
await self._handle_error(message, e)
|
||||
@@ -1,116 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: SDSaver
|
||||
# Description: The module for automatically saving self-destructing mediat
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: SDSaver
|
||||
# scope: SDSaver 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
import aiogram
|
||||
import telethon
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class SDSaverMod(loader.Module):
|
||||
"""The module for automatically saving self-destructing media"""
|
||||
|
||||
strings = {
|
||||
"name": "SDSaver",
|
||||
"sdmode_on": "<emoji document_id=5769230088960741619>🔥</emoji> <b>Automatic saving self-destructing media is enabled</b>",
|
||||
"sdmode_off": "<emoji document_id=5769230088960741619>🔥</emoji> <b>Automatic saving self-destructing media is disabled</b>",
|
||||
"sd": '🔥 <b><a href="{link}">{name}</a> sent self-destructing media:</b>\n{caption}',
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль для автоматического сохранения самоуничтожающихся медиа",
|
||||
"sdmode_on": "<emoji document_id=5769230088960741619>🔥</emoji> <b>Автоматическое сохранение самоуничтожающихся медиа включено</b>",
|
||||
"sdmode_off": "<emoji document_id=5769230088960741619>🔥</emoji> <b>Автоматическое сохранение самоуничтожающихся медиа выключено</b>",
|
||||
"sd": '🔥 <b><a href="{link}">{name}</a> отправил(а) самоуничтожающееся медиа:</b>\n{caption}',
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
self._db = db
|
||||
|
||||
channel, _ = await utils.asset_channel(
|
||||
self._client,
|
||||
"heroku-sd",
|
||||
"Self-destruction media will appear there",
|
||||
invite_bot=True,
|
||||
avatar="https://i.pinimg.com/originals/6c/1e/cf/6c1ecf3afca663a9ebc0b18788b337ee.jpg",
|
||||
_folder="heroku",
|
||||
)
|
||||
self._channel = int(f"-100{channel.id}")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Включить/Выключить автоматическое сохранение самоуничтожающихся медиа",
|
||||
en_doc="Enable/Disable automatic saving self-destructing media",
|
||||
)
|
||||
async def sdmodecmd(self, message: telethon.types.Message):
|
||||
need_mode = not self.get("save_sd", True)
|
||||
self.set("save_sd", need_mode)
|
||||
await utils.answer(
|
||||
message, self.strings(f"sdmode_{'on' if need_mode else 'off'}")
|
||||
)
|
||||
|
||||
@loader.watcher("in", only_messages=True)
|
||||
async def watcher(self, message: telethon.types.Message):
|
||||
if (
|
||||
(not self.get("save_sd", True))
|
||||
or (not message.media)
|
||||
or (not getattr(message.media, "ttl_seconds", None))
|
||||
):
|
||||
return
|
||||
|
||||
try:
|
||||
sender = await self.client.get_entity(message.sender_id, exp=0)
|
||||
except Exception:
|
||||
sender = await message.get_sender()
|
||||
|
||||
media = await self.client.download_media(message.media, bytes)
|
||||
args = {
|
||||
"chat_id": self._channel,
|
||||
"caption": self.strings("sd").format(
|
||||
link=utils.get_entity_url(sender),
|
||||
name=utils.escape_html(telethon.utils.get_display_name(sender)),
|
||||
caption=message.text if message.text else "",
|
||||
),
|
||||
}
|
||||
if message.photo:
|
||||
args["photo"] = aiogram.types.BufferedInputFile(media, "sd.png")
|
||||
method = self.inline.bot.send_photo
|
||||
if message.video or message.video_note:
|
||||
args["video"] = aiogram.types.BufferedInputFile(media, "sd.mp4")
|
||||
method = self.inline.bot.send_video
|
||||
if message.voice:
|
||||
args["voice"] = aiogram.types.BufferedInputFile(media, "sd.ogg")
|
||||
method = self.inline.bot.send_voice
|
||||
|
||||
await method(**args)
|
||||
@@ -1,188 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Search
|
||||
# Description: Search for your question on the Internet
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api Search
|
||||
# scope: Api Search 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class Search(loader.Module):
|
||||
"""Поисковик"""
|
||||
|
||||
strings = {
|
||||
"name": "Search",
|
||||
"search": "<emoji document_id=5188311512791393083>🌎</emoji><b> I searched for information for you</b>",
|
||||
"isearch": "🔎<b> I searched for information for you</b> ",
|
||||
"link": "🗂️ Link to your request",
|
||||
"close": "❌ Close",
|
||||
"no_query": "<emoji document_id=5854929766146118183>❌</emoji> Please provide a search query.",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"search": "<emoji document_id=5188311512791393083>🌎</emoji><b> Я поискал информацию за тебя</b>",
|
||||
"isearch": "🔎<b> Я поискал информацию за тебя</b> ",
|
||||
"link": "🗂️ Ссылка на ваш запрос",
|
||||
"close": "❌ Закрыть",
|
||||
"no_query": "<emoji document_id=5854929766146118183>❌</emoji> Пожалуйста, укажите поисковый запрос.",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.search_engines = {
|
||||
"google": "https://google.com/search?q=",
|
||||
"yandex": "https://yandex.ru/?q=",
|
||||
"duckduckgo": "https://duckduckgo.com/?q=",
|
||||
"bing": "https://bing.com/?q=",
|
||||
"you": "https://you.com/?q=",
|
||||
}
|
||||
|
||||
def _create_search_url(self, engine: str, query: str) -> str:
|
||||
"""Create search URL with proper encoding"""
|
||||
if not query.strip():
|
||||
return None
|
||||
|
||||
base_url = self.search_engines.get(engine)
|
||||
if not base_url:
|
||||
return None
|
||||
|
||||
encoded_query = urllib.parse.quote_plus(query.strip())
|
||||
return f"{base_url}{encoded_query}"
|
||||
|
||||
def _create_inline_markup(self, search_url: str):
|
||||
"""Create inline keyboard markup"""
|
||||
return [
|
||||
[
|
||||
{
|
||||
"text": self.strings("link"),
|
||||
"url": search_url,
|
||||
}
|
||||
],
|
||||
[{"text": self.strings("close"), "action": "close"}],
|
||||
]
|
||||
|
||||
async def _search_command(self, message, engine: str, inline: bool = False):
|
||||
"""Universal search command handler"""
|
||||
query = utils.get_args_raw(message)
|
||||
|
||||
if not query.strip():
|
||||
await utils.answer(message, self.strings("no_query"))
|
||||
return
|
||||
|
||||
search_url = self._create_search_url(engine, query)
|
||||
if not search_url:
|
||||
await utils.answer(message, self.strings("no_query"))
|
||||
return
|
||||
|
||||
if inline:
|
||||
await self.inline.form(
|
||||
text=self.strings("isearch"),
|
||||
message=message,
|
||||
reply_markup=self._create_inline_markup(search_url),
|
||||
silent=True,
|
||||
)
|
||||
else:
|
||||
await utils.answer(
|
||||
message, self.strings("search") + f": <a href={search_url}>link</a>"
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в Google",
|
||||
en_doc="Search on Google",
|
||||
)
|
||||
async def google(self, message):
|
||||
await self._search_command(message, "google")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в Yandex",
|
||||
en_doc="Search on Yandex",
|
||||
)
|
||||
async def yandex(self, message):
|
||||
await self._search_command(message, "yandex")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в Duckduckgo",
|
||||
en_doc="Search on Duckduckgo",
|
||||
)
|
||||
async def duckduckgo(self, message):
|
||||
await self._search_command(message, "duckduckgo")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в Bing",
|
||||
en_doc="Search on Bing",
|
||||
)
|
||||
async def bing(self, message):
|
||||
await self._search_command(message, "bing")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в You",
|
||||
en_doc="Search on You",
|
||||
)
|
||||
async def you(self, message):
|
||||
await self._search_command(message, "you")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в Google инлайн",
|
||||
en_doc="Search on Google inline",
|
||||
)
|
||||
async def igoogle(self, message):
|
||||
await self._search_command(message, "google", inline=True)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в Yandex инлайн",
|
||||
en_doc="Search on Yandex inline",
|
||||
)
|
||||
async def iyandex(self, message):
|
||||
await self._search_command(message, "yandex", inline=True)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в Duckduckgo инлайн",
|
||||
en_doc="Search on Duckduckgo inline",
|
||||
)
|
||||
async def iduckduckgo(self, message):
|
||||
await self._search_command(message, "duckduckgo", inline=True)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в Bing инлайн",
|
||||
en_doc="Search on Bing inline",
|
||||
)
|
||||
async def ibing(self, message):
|
||||
await self._search_command(message, "bing", inline=True)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Поискать в You инлайн",
|
||||
en_doc="Search on You inline",
|
||||
)
|
||||
async def iyou(self, message):
|
||||
await self._search_command(message, "you", inline=True)
|
||||
|
||||
async def close(self, call):
|
||||
"""Callback button"""
|
||||
await call.delete()
|
||||
@@ -1,197 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2026-2029 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Shortener
|
||||
# Description: Module for using bit.ly API
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Shortener
|
||||
# scope: Shortener 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class Shortener(loader.Module):
|
||||
"""Module for using bit.ly API"""
|
||||
|
||||
strings = {
|
||||
"name": "Shortener",
|
||||
"no_api": "<emoji document_id=5854929766146118183>❌</emoji> You have not specified an API token from the site <a href='https://app.bitly.com/settings/api/'>bit.ly</a>",
|
||||
"statclcmd": "<emoji document_id=5787384838411522455>📊</emoji> <b>Statistics on clicks for this link:</b> {c}",
|
||||
"shortencmd": "<emoji document_id=5854762571659218443>✅</emoji> <b>Your shortened link is ready:</b> <code>{c}</code>",
|
||||
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Please provide a URL to shorten.",
|
||||
"invalid_url": "<emoji document_id=5854929766146118183>❌</emoji> Invalid URL format.",
|
||||
"api_error": "<emoji document_id=5854929766146118183>❌</emoji> API error: {error}",
|
||||
"_cls_doc": "Module for using bit.ly API",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_api": "<emoji document_id=5854929766146118183>❌</emoji> Вы не указали api токен с сайта <a href='https://app.bitly.com/settings/api/'>bit.ly</a>",
|
||||
"statclcmd": "<emoji document_id=5787384838411522455>📊</emoji> <b>Статистика о переходе по этой ссылке:</b> {c}",
|
||||
"shortencmd": "<emoji document_id=5854762571659218443>✅</emoji> <b>Ваша сокращённая ссылка готова:</b> <code>{c}</code>",
|
||||
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Пожалуйста, укажите URL для сокращения.",
|
||||
"invalid_url": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат URL.",
|
||||
"api_error": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка API: {error}",
|
||||
"_cls_doc": "Модуль для использования API bit.ly",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"token",
|
||||
None,
|
||||
lambda: "Need a token with https://app.bitly.com/settings/api/",
|
||||
validator=loader.validators.Hidden(),
|
||||
)
|
||||
)
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
def _validate_url(self, url: str) -> bool:
|
||||
"""Validate URL format"""
|
||||
if not url:
|
||||
return False
|
||||
|
||||
url_pattern = re.compile(
|
||||
r"^https?://"
|
||||
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|"
|
||||
r"localhost|"
|
||||
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
|
||||
r"(?::\d+)?"
|
||||
r"(?:/?|[/?]\S+)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
return url_pattern.match(url) is not None
|
||||
|
||||
async def shorten_url(self, url: str, token: str) -> Optional[str]:
|
||||
session = await self._get_session()
|
||||
async with session.post(
|
||||
"https://api-ssl.bitly.com/v4/shorten",
|
||||
json={"long_url": url},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
) as resp:
|
||||
if resp.status == 201:
|
||||
json_response = await resp.json()
|
||||
return json_response["link"]
|
||||
else:
|
||||
logger.error(f"Error occurred! Status code: {resp.status}")
|
||||
return None
|
||||
|
||||
async def get_bitlink_stats(self, bitlink: str, token: str) -> Optional[int]:
|
||||
session = await self._get_session()
|
||||
async with session.get(
|
||||
f"https://api-ssl.bitly.com/v4/bitlinks/{bitlink}/clicks/summary",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
json_response = await resp.json()
|
||||
return json_response["total_clicks"]
|
||||
else:
|
||||
logger.error(f"Error occurred! Status code: {resp.status}")
|
||||
return None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Сократить ссылку через bit.ly (ссылка с https://)",
|
||||
en_doc="Shorten the link via bit.ly (url with https://)",
|
||||
)
|
||||
async def shortencmd(self, message):
|
||||
"""Shorten URL using bit.ly API"""
|
||||
if self.config["token"] is None:
|
||||
await utils.answer(message, self.strings("no_api"))
|
||||
return
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("no_args"))
|
||||
return
|
||||
|
||||
if not self._validate_url(args):
|
||||
await utils.answer(message, self.strings("invalid_url"))
|
||||
return
|
||||
|
||||
try:
|
||||
short_url = await self.shorten_url(url=args, token=self.config["token"])
|
||||
if short_url is None:
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("api_error").format(error="Failed to shorten URL"),
|
||||
)
|
||||
return
|
||||
await utils.answer(message, self.strings("shortencmd").format(c=short_url))
|
||||
except Exception as e:
|
||||
logger.error(f"Error shortening URL: {e}")
|
||||
await utils.answer(message, self.strings("api_error").format(error=str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Посмотреть статистику ссылки через bit.ly (ссылка без https:// | Доступно только на платных аккаунтах)",
|
||||
en_doc="View link statistics via bit.ly (link without https:// | Works only on paid accounts)",
|
||||
)
|
||||
async def statclcmd(self, message):
|
||||
"""Get click statistics for shortened URL"""
|
||||
if self.config["token"] is None:
|
||||
await utils.answer(message, self.strings("no_api"))
|
||||
return
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("no_args"))
|
||||
return
|
||||
|
||||
try:
|
||||
if not args.startswith("bit.ly/"):
|
||||
await utils.answer(message, self.strings("invalid_url"))
|
||||
return
|
||||
else:
|
||||
clicks = await self.get_bitlink_stats(
|
||||
bitlink=args, token=self.config["token"]
|
||||
)
|
||||
if clicks is None:
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("api_error").format(
|
||||
error="Failed to get statistics"
|
||||
),
|
||||
)
|
||||
return
|
||||
await utils.answer(message, self.strings("statclcmd").format(c=clicks))
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting statistics: {e}")
|
||||
await utils.answer(message, self.strings("api_error").format(error=str(e)))
|
||||
@@ -1,419 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Silent T&R
|
||||
# Description: Silent tags and reactions
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Silent T&R
|
||||
# scope: Silent T&R 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
from telethon.types import Message
|
||||
from telethon import events
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
|
||||
@loader.tds
|
||||
class SilentTRMod(loader.Module):
|
||||
"""Silent tags and reactions"""
|
||||
|
||||
strings = {
|
||||
"name": "Silent T&R",
|
||||
"global_reactions_on": "✅ Global silent reactions enabled",
|
||||
"global_reactions_off": "❌ Global silent reactions disabled",
|
||||
"global_tags_on": "✅ Global silent tags enabled",
|
||||
"global_tags_off": "❌ Global silent tags disabled",
|
||||
"chat_reactions_on": "✅ Silent reactions enabled in this chat",
|
||||
"chat_reactions_off": "❌ Silent reactions disabled in this chat",
|
||||
"chat_tags_on": "✅ Silent tags enabled in this chat",
|
||||
"chat_tags_off": "❌ Silent tags disabled in this chat",
|
||||
"ignore_added": "✅ User added to global ignore list",
|
||||
"ignore_removed": "❌ User removed from global ignore list",
|
||||
"hignore_added": "✅ User added to ignore list in this chat",
|
||||
"hignore_removed": "❌ User removed from ignore list in this chat",
|
||||
"no_reply": "❌ Reply to a user or specify username",
|
||||
"user_not_found": "❌ User not found",
|
||||
"args_error": "❌ Use: .sreacts on/off or .sreacts",
|
||||
"chat_args_error": "❌ Use: .hsreacts on/off or .hsreacts",
|
||||
"status": "📊 Silent T&R status:\n\nGlobal:\n Reactions: {}\n Tags: {}\n\nThis chat:\n Reactions: {}\n Tags: {}\n\nGlobal ignore: {}\nChat ignore: {}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"global_reactions_on": "✅ Глобальные тихие реакции включены",
|
||||
"global_reactions_off": "❌ Глобальные тихие реакции выключены",
|
||||
"global_tags_on": "✅ Глобальные тихие упоминания включены",
|
||||
"global_tags_off": "❌ Глобальные тихие упоминания выключены",
|
||||
"chat_reactions_on": "✅ Тихие реакции включены в этом чате",
|
||||
"chat_reactions_off": "❌ Тихие реакции выключены в этом чате",
|
||||
"chat_tags_on": "✅ Тихие упоминания включены в этом чате",
|
||||
"chat_tags_off": "❌ Тихие упоминания выключены в этом чате",
|
||||
"ignore_added": "✅ Пользователь добавлен в глобальный игнор-лист",
|
||||
"ignore_removed": "❌ Пользователь удален из глобального игнор-листа",
|
||||
"hignore_added": "✅ Пользователь добавлен в игнор-лист этого чата",
|
||||
"hignore_removed": "❌ Пользователь удален из игнор-листа этого чата",
|
||||
"no_reply": "❌ Ответьте на пользователя или укажите username",
|
||||
"user_not_found": "❌ Пользователь не найден",
|
||||
"args_error": "❌ Используйте: .sreacts on/off или .sreacts",
|
||||
"chat_args_error": "❌ Используйте: .hsreacts on/off или .hsreacts",
|
||||
"status": "📊 Статус Silent T&R:\n\nГлобально:\n Реакции: {}\n Упоминания: {}\n\nВ этом чате:\n Реакции: {}\n Упоминания: {}\n\nГлобальный игнор: {}\nИгнор в чате: {}",
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self._client = client
|
||||
self._db = db
|
||||
self._me = await client.get_me()
|
||||
|
||||
self._global_settings = self._db.get(
|
||||
__name__, "global_settings", {"reactions": False, "tags": False}
|
||||
)
|
||||
self._chat_settings = self._db.get(__name__, "chat_settings", {})
|
||||
self._global_ignore = self._db.get(__name__, "global_ignore", [])
|
||||
self._chat_ignore = self._db.get(__name__, "chat_ignore", {})
|
||||
|
||||
client.add_event_handler(
|
||||
self._on_message_reaction_updated, events.MessageReactionsUpdated
|
||||
)
|
||||
client.add_event_handler(self._on_new_message, events.NewMessage)
|
||||
|
||||
async def on_unload(self):
|
||||
self._client.remove_event_handler(self._on_message_reaction_updated)
|
||||
self._client.remove_event_handler(self._on_new_message)
|
||||
|
||||
def _save_settings(self):
|
||||
self._db.set(__name__, "global_settings", self._global_settings)
|
||||
self._db.set(__name__, "chat_settings", self._chat_settings)
|
||||
self._db.set(__name__, "global_ignore", self._global_ignore)
|
||||
self._db.set(__name__, "chat_ignore", self._chat_ignore)
|
||||
|
||||
async def _on_message_reaction_updated(self, event):
|
||||
try:
|
||||
message = await self._client.get_messages(
|
||||
event.chat_id, ids=event.message_id
|
||||
)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if message.sender_id != self._me.id:
|
||||
return
|
||||
|
||||
chat_id = str(event.chat_id)
|
||||
user_id = event.user_id
|
||||
|
||||
if user_id in self._global_ignore or (
|
||||
chat_id in self._chat_ignore and user_id in self._chat_ignore[chat_id]
|
||||
):
|
||||
return
|
||||
|
||||
chat_settings = self._chat_settings.get(
|
||||
chat_id, {"reactions": None, "tags": None}
|
||||
)
|
||||
reactions_enabled = (
|
||||
chat_settings["reactions"]
|
||||
if chat_settings["reactions"] is not None
|
||||
else self._global_settings["reactions"]
|
||||
)
|
||||
|
||||
if reactions_enabled:
|
||||
await self._client.read_messages(event.chat_id, event.message_id)
|
||||
|
||||
async def _on_new_message(self, event):
|
||||
if event.out or not event.mentioned:
|
||||
return
|
||||
|
||||
chat_id = str(event.chat_id)
|
||||
user_id = event.sender_id
|
||||
|
||||
if user_id in self._global_ignore or (
|
||||
chat_id in self._chat_ignore and user_id in self._chat_ignore[chat_id]
|
||||
):
|
||||
return
|
||||
|
||||
chat_settings = self._chat_settings.get(
|
||||
chat_id, {"reactions": None, "tags": None}
|
||||
)
|
||||
tags_enabled = (
|
||||
chat_settings["tags"]
|
||||
if chat_settings["tags"] is not None
|
||||
else self._global_settings["tags"]
|
||||
)
|
||||
|
||||
if tags_enabled:
|
||||
await event.mark_read()
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[on/off] - тихие реакции во всех чатах",
|
||||
en_doc="[on/off] - silent reactions in all chats",
|
||||
)
|
||||
async def _toggle_setting(
|
||||
self, message: Message, setting_type: str, scope: str = "global"
|
||||
):
|
||||
args = utils.get_args_raw(message).lower()
|
||||
chat_id = str(message.chat_id) if scope == "chat" else None
|
||||
|
||||
if args not in ["on", "off", ""]:
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["args_error"]
|
||||
if scope == "global"
|
||||
else self.strings["chat_args_error"],
|
||||
)
|
||||
return
|
||||
|
||||
if scope == "global":
|
||||
if args == "on":
|
||||
self._global_settings[setting_type] = True
|
||||
elif args == "off":
|
||||
self._global_settings[setting_type] = False
|
||||
else:
|
||||
status = "on" if self._global_settings[setting_type] else "off"
|
||||
await utils.answer(message, f"Global silent {setting_type}: {status}")
|
||||
return
|
||||
else:
|
||||
chat_settings = self._chat_settings.get(
|
||||
chat_id, {"reactions": None, "tags": None}
|
||||
)
|
||||
if args == "on":
|
||||
chat_settings[setting_type] = True
|
||||
elif args == "off":
|
||||
chat_settings[setting_type] = False
|
||||
else:
|
||||
status = chat_settings[setting_type]
|
||||
if status is None:
|
||||
status = f"global ({'on' if self._global_settings[setting_type] else 'off'})"
|
||||
else:
|
||||
status = "on" if status else "off"
|
||||
await utils.answer(
|
||||
message, f"Silent {setting_type} in this chat: {status}"
|
||||
)
|
||||
return
|
||||
self._chat_settings[chat_id] = chat_settings
|
||||
|
||||
self._save_settings()
|
||||
status_key = "on" if args == "on" else "off"
|
||||
if scope == "global":
|
||||
key = f"global_{setting_type}_{status_key}"
|
||||
else:
|
||||
key = f"chat_{setting_type}_{status_key}"
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings.get(
|
||||
key,
|
||||
f"✅ {scope.title()} silent {setting_type} {'enabled' if args == 'on' else 'disabled'}",
|
||||
),
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[on/off] - тихие реакции во всех чатах",
|
||||
en_doc="[on/off] - silent reactions in all chats",
|
||||
)
|
||||
async def sreacts(self, message: Message):
|
||||
await self._toggle_setting(message, "reactions", "global")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[on/off] - тихие упоминания во всех чатах",
|
||||
en_doc="[on/off] - silent tags in all chats",
|
||||
)
|
||||
async def stags(self, message: Message):
|
||||
await self._toggle_setting(message, "tags", "global")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[on/off] - тихие реакции и упоминания во всех чатах",
|
||||
en_doc="[on/off] - silent reactions and tags in all chats",
|
||||
)
|
||||
async def sall(self, message: Message):
|
||||
args = utils.get_args_raw(message).lower()
|
||||
if args == "on":
|
||||
self._global_settings["reactions"] = True
|
||||
self._global_settings["tags"] = True
|
||||
elif args == "off":
|
||||
self._global_settings["reactions"] = False
|
||||
self._global_settings["tags"] = False
|
||||
elif args == "":
|
||||
status_r = "on" if self._global_settings["reactions"] else "off"
|
||||
status_t = "on" if self._global_settings["tags"] else "off"
|
||||
await utils.answer(
|
||||
message, f"Global silent reactions: {status_r}, tags: {status_t}"
|
||||
)
|
||||
return
|
||||
else:
|
||||
await utils.answer(message, self.strings["args_error"])
|
||||
return
|
||||
|
||||
self._save_settings()
|
||||
await utils.answer(
|
||||
message,
|
||||
f"{'✅' if args == 'on' else '❌'} Global silent reactions and tags {'enabled' if args == 'on' else 'disabled'}",
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[on/off] - тихие реакции в этом чате",
|
||||
en_doc="[on/off] - silent reactions in this chat",
|
||||
)
|
||||
async def hsreacts(self, message: Message):
|
||||
await self._toggle_setting(message, "reactions", "chat")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[on/off] - тихие упоминания в этом чате",
|
||||
en_doc="[on/off] - silent tags in this chat",
|
||||
)
|
||||
async def hstags(self, message: Message):
|
||||
await self._toggle_setting(message, "tags", "chat")
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[on/off] - тихие реакции и упоминания в этом чате",
|
||||
en_doc="[on/off] - silent reactions and tags in this chat",
|
||||
)
|
||||
async def hsall(self, message: Message):
|
||||
args = utils.get_args_raw(message).lower()
|
||||
chat_id = str(message.chat_id)
|
||||
chat_settings = self._chat_settings.get(
|
||||
chat_id, {"reactions": None, "tags": None}
|
||||
)
|
||||
|
||||
if args == "on":
|
||||
chat_settings["reactions"] = True
|
||||
chat_settings["tags"] = True
|
||||
elif args == "off":
|
||||
chat_settings["reactions"] = False
|
||||
chat_settings["tags"] = False
|
||||
elif args == "":
|
||||
status_r = chat_settings["reactions"]
|
||||
status_t = chat_settings["tags"]
|
||||
|
||||
def format_status(status, setting_type):
|
||||
if status is None:
|
||||
return f"global ({'on' if self._global_settings[setting_type] else 'off'})"
|
||||
return "on" if status else "off"
|
||||
|
||||
status_r = format_status(status_r, "reactions")
|
||||
status_t = format_status(status_t, "tags")
|
||||
await utils.answer(
|
||||
message, f"Silent reactions: {status_r}, tags: {status_t} in this chat"
|
||||
)
|
||||
return
|
||||
else:
|
||||
await utils.answer(message, self.strings["chat_args_error"])
|
||||
return
|
||||
|
||||
self._chat_settings[chat_id] = chat_settings
|
||||
self._save_settings()
|
||||
await utils.answer(
|
||||
message,
|
||||
f"{'✅' if args == 'on' else '❌'} Silent reactions and tags {'enabled' if args == 'on' else 'disabled'} in this chat",
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[ответ/username] - игнорировать пользователя глобально",
|
||||
en_doc="[reply/username] - ignore user globally",
|
||||
)
|
||||
async def _get_user_id(self, message: Message):
|
||||
reply = await message.get_reply_message()
|
||||
args = utils.get_args_raw(message)
|
||||
|
||||
if reply:
|
||||
return reply.sender_id
|
||||
if args:
|
||||
try:
|
||||
user = await self._client.get_entity(args)
|
||||
return user.id
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="[ответ/username] - игнорировать пользователя в этом чате",
|
||||
en_doc="[reply/username] - ignore user in this chat",
|
||||
)
|
||||
async def ignore(self, message: Message):
|
||||
user_id = await self._get_user_id(message)
|
||||
if not user_id:
|
||||
await utils.answer(message, self.strings["no_reply"])
|
||||
return
|
||||
|
||||
if user_id in self._global_ignore:
|
||||
self._global_ignore.remove(user_id)
|
||||
await utils.answer(message, self.strings["ignore_removed"])
|
||||
else:
|
||||
self._global_ignore.append(user_id)
|
||||
await utils.answer(message, self.strings["ignore_added"])
|
||||
|
||||
self._save_settings()
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Показать статус Silent T&R", en_doc="Show Silent T&R status"
|
||||
)
|
||||
async def hignore(self, message: Message):
|
||||
user_id = await self._get_user_id(message)
|
||||
if not user_id:
|
||||
await utils.answer(message, self.strings["no_reply"])
|
||||
return
|
||||
|
||||
chat_id = str(message.chat_id)
|
||||
if chat_id not in self._chat_ignore:
|
||||
self._chat_ignore[chat_id] = []
|
||||
|
||||
if user_id in self._chat_ignore[chat_id]:
|
||||
self._chat_ignore[chat_id].remove(user_id)
|
||||
await utils.answer(message, self.strings["hignore_removed"])
|
||||
else:
|
||||
self._chat_ignore[chat_id].append(user_id)
|
||||
await utils.answer(message, self.strings["hignore_added"])
|
||||
|
||||
self._save_settings()
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Показать статус Silent T&R", en_doc="Show Silent T&R status"
|
||||
)
|
||||
async def strstatus(self, message: Message):
|
||||
global_reactions = "on" if self._global_settings["reactions"] else "off"
|
||||
global_tags = "on" if self._global_settings["tags"] else "off"
|
||||
|
||||
chat_id = str(message.chat_id)
|
||||
chat_settings = self._chat_settings.get(
|
||||
chat_id, {"reactions": None, "tags": None}
|
||||
)
|
||||
chat_reactions = chat_settings["reactions"]
|
||||
if chat_reactions is None:
|
||||
chat_reactions = "global"
|
||||
else:
|
||||
chat_reactions = "on" if chat_reactions else "off"
|
||||
chat_tags = chat_settings["tags"]
|
||||
if chat_tags is None:
|
||||
chat_tags = "global"
|
||||
else:
|
||||
chat_tags = "on" if chat_tags else "off"
|
||||
|
||||
global_ignore_count = len(self._global_ignore)
|
||||
chat_ignore_count = len(self._chat_ignore.get(chat_id, []))
|
||||
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["status"].format(
|
||||
global_reactions,
|
||||
global_tags,
|
||||
chat_reactions,
|
||||
chat_tags,
|
||||
global_ignore_count,
|
||||
chat_ignore_count,
|
||||
),
|
||||
)
|
||||
@@ -1,875 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: SoundCloud
|
||||
# Description: Card with the currently playing track on SoundCloud
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: SoundCloud
|
||||
# scope: SoundCloud 0.0.2
|
||||
# requires: requests pillow yt-dlp
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import functools
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import requests
|
||||
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
|
||||
from telethon.tl.types import Message
|
||||
from yt_dlp import YoutubeDL
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_API = "https://api-v2.soundcloud.com"
|
||||
_COVER_HQ = "-t500x500"
|
||||
|
||||
_ORANGE = (255, 85, 0)
|
||||
_DIM = (155, 155, 170)
|
||||
_FADED = (100, 100, 115)
|
||||
_CARD_BG = (255, 255, 255, 14)
|
||||
_CARD_ACTIVE = (255, 255, 255, 26)
|
||||
_BAR_MUTED = (255, 255, 255, 16)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class TrackInfo:
|
||||
"""Parsed SoundCloud track metadata."""
|
||||
|
||||
track_id: int
|
||||
title: str
|
||||
artist: str
|
||||
duration_ms: int
|
||||
permalink: str
|
||||
cover_url: str
|
||||
genre: str
|
||||
plays: int
|
||||
likes: int
|
||||
reposts: int
|
||||
comments: int
|
||||
|
||||
@classmethod
|
||||
def parse(cls, raw: dict) -> "TrackInfo":
|
||||
u = raw.get("user") or {}
|
||||
return cls(
|
||||
track_id=raw.get("id", 0),
|
||||
title=raw.get("title") or "Unknown",
|
||||
artist=u.get("username") or "Unknown",
|
||||
duration_ms=raw.get("duration") or raw.get("full_duration") or 0,
|
||||
permalink=raw.get("permalink_url") or "",
|
||||
cover_url=raw.get("artwork_url") or u.get("avatar_url") or "",
|
||||
genre=raw.get("genre") or "",
|
||||
plays=raw.get("playback_count") or 0,
|
||||
likes=raw.get("likes_count") or raw.get("favoritings_count") or 0,
|
||||
reposts=raw.get("reposts_count") or 0,
|
||||
comments=raw.get("comment_count") or 0,
|
||||
)
|
||||
|
||||
@property
|
||||
def duration_fmt(self) -> str:
|
||||
s = self.duration_ms // 1000
|
||||
return f"{s // 60}:{s % 60:02d}"
|
||||
|
||||
@property
|
||||
def hq_cover(self) -> str:
|
||||
return self.cover_url.replace("-large", _COVER_HQ)
|
||||
|
||||
|
||||
def _compact(n: int) -> str:
|
||||
"""Format large numbers: 12500 → 12.5K."""
|
||||
if n >= 1_000_000:
|
||||
return f"{n / 1_000_000:.1f}M"
|
||||
if n >= 1_000:
|
||||
return f"{n / 1_000:.1f}K"
|
||||
return str(n)
|
||||
|
||||
|
||||
class _Fonts:
|
||||
"""Cached font loader from raw bytes."""
|
||||
|
||||
__slots__ = ("_raw", "_loaded")
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
self._raw = data
|
||||
self._loaded: Dict[int, ImageFont.FreeTypeFont] = {}
|
||||
|
||||
def __call__(self, size: int) -> ImageFont.FreeTypeFont:
|
||||
if size not in self._loaded:
|
||||
self._loaded[size] = ImageFont.truetype(io.BytesIO(self._raw), size)
|
||||
return self._loaded[size]
|
||||
|
||||
def fit(self, text: str, max_w: int, hi: int, lo: int) -> ImageFont.FreeTypeFont:
|
||||
for s in range(hi, lo - 1, -2):
|
||||
f = self(s)
|
||||
if f.getlength(text) <= max_w:
|
||||
return f
|
||||
return self(lo)
|
||||
|
||||
|
||||
def _ellipsis(text: str, font: ImageFont.FreeTypeFont, max_w: int) -> str:
|
||||
"""Truncate text with '…' using binary search."""
|
||||
if font.getlength(text) <= max_w:
|
||||
return text
|
||||
lo, hi = 0, len(text)
|
||||
while lo < hi:
|
||||
mid = (lo + hi + 1) // 2
|
||||
if font.getlength(text[:mid] + "…") <= max_w:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid - 1
|
||||
return text[:lo] + "…"
|
||||
|
||||
|
||||
def _center_text(draw, text, font, y, canvas_w, fill="white"):
|
||||
bb = draw.textbbox((0, 0), text, font=font)
|
||||
draw.text(((canvas_w - bb[2] + bb[0]) // 2, y), text, font=font, fill=fill)
|
||||
|
||||
|
||||
def _frosted_bg(src: bytes, w: int, h: int, dim: float = 0.25) -> Image.Image:
|
||||
"""Blurred & dimmed background from cover art."""
|
||||
img = Image.open(io.BytesIO(src)).convert("RGBA")
|
||||
small = img.resize((max(w // 5, 1), max(h // 5, 1)), Image.Resampling.BILINEAR)
|
||||
small = small.filter(ImageFilter.GaussianBlur(12))
|
||||
result = small.resize((w, h), Image.Resampling.BILINEAR)
|
||||
return ImageEnhance.Brightness(result).enhance(dim)
|
||||
|
||||
|
||||
def _gradient(
|
||||
w: int, h: int, vertical: bool = True, c_from=(0, 0, 0, 160), c_to=(0, 0, 0, 40)
|
||||
) -> Image.Image:
|
||||
"""Fast linear gradient via 1px strip resize."""
|
||||
length = h if vertical else w
|
||||
strip = Image.new("RGBA", (1, length) if vertical else (length, 1))
|
||||
px = strip.load()
|
||||
for i in range(length):
|
||||
t = i / max(length - 1, 1)
|
||||
rgba = tuple(int(c_from[c] + (c_to[c] - c_from[c]) * t) for c in range(4))
|
||||
if vertical:
|
||||
px[0, i] = rgba
|
||||
else:
|
||||
px[i, 0] = rgba
|
||||
return strip.resize((w, h), Image.Resampling.BILINEAR)
|
||||
|
||||
|
||||
def _round_corners(img: Image.Image, r: int) -> Image.Image:
|
||||
mask = Image.new("L", img.size, 0)
|
||||
ImageDraw.Draw(mask).rounded_rectangle((0, 0, *img.size), r, fill=255)
|
||||
out = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
||||
out.paste(img, mask=mask)
|
||||
return out
|
||||
|
||||
|
||||
def _rounded_cover(data: bytes, size: int, r: int) -> Image.Image:
|
||||
img = Image.open(io.BytesIO(data)).convert("RGBA")
|
||||
img = img.resize((size, size), Image.Resampling.LANCZOS)
|
||||
return _round_corners(img, r)
|
||||
|
||||
|
||||
def _place_cover(
|
||||
base: Image.Image,
|
||||
cover_data: bytes,
|
||||
size: int,
|
||||
radius: int,
|
||||
pos: tuple,
|
||||
shadow_blur: int = 20,
|
||||
shadow_alpha: int = 50,
|
||||
):
|
||||
"""Place cover with colored drop shadow (offset downward)."""
|
||||
cover = _rounded_cover(cover_data, size, radius)
|
||||
avg = cover.resize((1, 1), Image.Resampling.BILINEAR).getpixel((0, 0))
|
||||
|
||||
pad = shadow_blur * 2
|
||||
offset_y = 8
|
||||
canvas = Image.new(
|
||||
"RGBA", (size + pad * 2, size + pad * 2 + offset_y), (0, 0, 0, 0)
|
||||
)
|
||||
shadow_shape = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
ImageDraw.Draw(shadow_shape).rounded_rectangle(
|
||||
(0, 0, size, size), radius, fill=(*avg[:3], shadow_alpha)
|
||||
)
|
||||
canvas.paste(shadow_shape, (pad, pad + offset_y), shadow_shape)
|
||||
canvas = canvas.filter(ImageFilter.GaussianBlur(shadow_blur))
|
||||
canvas.paste(cover, (pad, pad), cover)
|
||||
|
||||
base.paste(canvas, (pos[0] - pad, pos[1] - pad), canvas)
|
||||
|
||||
|
||||
def _waveform(draw, x, y, w, h, bars=45, color=_ORANGE, muted=_BAR_MUTED, prog=0.0):
|
||||
"""Waveform visualization bars with sha256-seeded heights."""
|
||||
bw = max(w // (bars * 2), 2)
|
||||
gap = (w - bw * bars) // max(bars - 1, 1)
|
||||
seed = hashlib.sha256(f"sc{bars}".encode()).digest()
|
||||
for i in range(bars):
|
||||
bx = x + i * (bw + gap)
|
||||
amp = seed[i % len(seed)] / 255
|
||||
bh = int(h * (0.25 + amp * 0.75))
|
||||
by = y + (h - bh) // 2
|
||||
c = color if i / bars <= prog else muted
|
||||
draw.rounded_rectangle((bx, by, bx + bw, by + bh), bw // 2, fill=c)
|
||||
|
||||
|
||||
def _badge(
|
||||
draw, text, font, x, y, fg="white", bg=(255, 255, 255, 18), px=12, py=5
|
||||
) -> int:
|
||||
"""Rounded pill badge. Returns width."""
|
||||
bb = font.getbbox(text)
|
||||
tw, th = bb[2] - bb[0], bb[3] - bb[1]
|
||||
pw, ph = tw + px * 2, th + py * 2
|
||||
draw.rounded_rectangle((x, y, x + pw, y + ph), ph // 2, fill=bg)
|
||||
draw.text((x + px, y + py), text, font=font, fill=fg)
|
||||
return pw
|
||||
|
||||
|
||||
def _export(img: Image.Image, name: str = "soundcloud.png") -> io.BytesIO:
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, "PNG", optimize=True)
|
||||
buf.seek(0)
|
||||
buf.name = name
|
||||
return buf
|
||||
|
||||
|
||||
class CardFactory:
|
||||
"""Generates visual cards for SoundCloud tracks."""
|
||||
|
||||
def __init__(self, fonts: _Fonts):
|
||||
self._f = fonts
|
||||
|
||||
def square(self, track: TrackInfo, cover: bytes) -> io.BytesIO:
|
||||
"""Square now-playing card (800×800)."""
|
||||
S = 800
|
||||
p = 45
|
||||
|
||||
bg = _frosted_bg(cover, S, S, 0.22)
|
||||
bg = Image.alpha_composite(
|
||||
bg, _gradient(S, S, True, (0, 0, 0, 50), (0, 0, 0, 190))
|
||||
)
|
||||
draw = ImageDraw.Draw(bg)
|
||||
|
||||
bf = self._f(12)
|
||||
draw.text((p, p), "SOUNDCLOUD", font=bf, fill=_ORANGE)
|
||||
lw = bf.getlength("SOUNDCLOUD")
|
||||
draw.line([(p, p + 17), (p + lw, p + 17)], fill=(*_ORANGE, 100), width=2)
|
||||
|
||||
cs = 310
|
||||
cx, cy = (S - cs) // 2, p + 32
|
||||
_place_cover(bg, cover, cs, 14, (cx, cy), shadow_blur=25, shadow_alpha=50)
|
||||
draw = ImageDraw.Draw(bg)
|
||||
|
||||
wy = cy + cs + 30
|
||||
_waveform(draw, p + 35, wy, S - p * 2 - 70, 26, bars=50)
|
||||
|
||||
tf = self._f(13)
|
||||
draw.text((p + 35, wy + 30), "0:00", font=tf, fill=_FADED)
|
||||
ds = track.duration_fmt
|
||||
draw.text((S - p - 35 - tf.getlength(ds), wy + 30), ds, font=tf, fill=_FADED)
|
||||
|
||||
tw = S - p * 2
|
||||
ty = wy + 56
|
||||
title_f = self._f.fit(track.title, tw, 36, 20)
|
||||
_center_text(draw, _ellipsis(track.title, title_f, tw), title_f, ty, S)
|
||||
|
||||
af = self._f.fit(track.artist, tw, 24, 16)
|
||||
_center_text(draw, _ellipsis(track.artist, af, tw), af, ty + 44, S, _DIM)
|
||||
|
||||
sy = ty + 92
|
||||
sf = self._f(14)
|
||||
parts = []
|
||||
if track.genre:
|
||||
parts.append(track.genre)
|
||||
if track.plays:
|
||||
parts.append(f"▶ {_compact(track.plays)}")
|
||||
if track.likes:
|
||||
parts.append(f"♥ {_compact(track.likes)}")
|
||||
if not parts:
|
||||
parts.append(track.duration_fmt)
|
||||
_center_text(draw, " · ".join(parts), sf, sy, S, _FADED)
|
||||
|
||||
return _export(_round_corners(bg, 22))
|
||||
|
||||
def horizontal(self, track: TrackInfo, cover: bytes) -> io.BytesIO:
|
||||
"""Wide now-playing card (1200×400)."""
|
||||
W, H = 1200, 400
|
||||
p = 40
|
||||
cs = 280
|
||||
|
||||
bg = _frosted_bg(cover, W, H, 0.22)
|
||||
bg = Image.alpha_composite(
|
||||
bg, _gradient(W, H, False, (0, 0, 0, 180), (0, 0, 0, 60))
|
||||
)
|
||||
|
||||
cvy = (H - cs) // 2
|
||||
_place_cover(bg, cover, cs, 14, (p, cvy), shadow_blur=20, shadow_alpha=40)
|
||||
draw = ImageDraw.Draw(bg)
|
||||
|
||||
bf = self._f(11)
|
||||
draw.text((p, p - 6), "SOUNDCLOUD", font=bf, fill=_ORANGE)
|
||||
|
||||
if track.genre:
|
||||
gf = self._f(12)
|
||||
gt = track.genre.upper()
|
||||
draw.text((W - p - gf.getlength(gt), p - 6), gt, font=gf, fill=_FADED)
|
||||
|
||||
tx = p + cs + 50
|
||||
tw = W - tx - p
|
||||
|
||||
tty = cvy + 10
|
||||
title_f = self._f.fit(track.title, tw, 36, 22)
|
||||
draw.text(
|
||||
(tx, tty),
|
||||
_ellipsis(track.title, title_f, tw),
|
||||
font=title_f,
|
||||
fill="white",
|
||||
)
|
||||
|
||||
af = self._f(22)
|
||||
draw.text(
|
||||
(tx, tty + 50),
|
||||
_ellipsis(track.artist, af, tw),
|
||||
font=af,
|
||||
fill=_DIM,
|
||||
)
|
||||
|
||||
by = tty + 98
|
||||
bx = tx
|
||||
pill_f = self._f(14)
|
||||
bw = _badge(
|
||||
draw,
|
||||
track.duration_fmt,
|
||||
pill_f,
|
||||
bx,
|
||||
by,
|
||||
fg=_ORANGE,
|
||||
bg=(*_ORANGE, 35),
|
||||
)
|
||||
bx += bw + 8
|
||||
if track.plays:
|
||||
bw = _badge(draw, f"▶ {_compact(track.plays)}", pill_f, bx, by, fg=_DIM)
|
||||
bx += bw + 8
|
||||
if track.likes:
|
||||
_badge(draw, f"♥ {_compact(track.likes)}", pill_f, bx, by, fg=_DIM)
|
||||
|
||||
wy = cvy + cs - 50
|
||||
_waveform(draw, tx, wy, tw, 22, bars=55)
|
||||
|
||||
wf = self._f(12)
|
||||
draw.text((tx, wy + 26), "0:00", font=wf, fill=_FADED)
|
||||
ds = track.duration_fmt
|
||||
draw.text((tx + tw - wf.getlength(ds), wy + 26), ds, font=wf, fill=_FADED)
|
||||
|
||||
return _export(_round_corners(bg, 20))
|
||||
|
||||
def history(self, tracks: List[TrackInfo], fetch_cover) -> io.BytesIO:
|
||||
"""History card with dynamic height based on track count."""
|
||||
W = 1200
|
||||
p = 36
|
||||
row_h = 120
|
||||
gap = 8
|
||||
hdr = 55
|
||||
n = len(tracks)
|
||||
H = p * 2 + hdr + n * row_h + (n - 1) * gap
|
||||
|
||||
bg_data = fetch_cover(tracks[0].hq_cover)
|
||||
bg = _frosted_bg(bg_data, W, H, 0.18)
|
||||
bg = Image.alpha_composite(bg, Image.new("RGBA", (W, H), (0, 0, 0, 150)))
|
||||
draw = ImageDraw.Draw(bg)
|
||||
|
||||
hf = self._f(14)
|
||||
draw.text((p, p), "SOUNDCLOUD", font=hf, fill=_ORANGE)
|
||||
thf = self._f(22)
|
||||
draw.text((p, p + 20), "Listening History", font=thf, fill="white")
|
||||
|
||||
lw = hf.getlength("SOUNDCLOUD")
|
||||
draw.rounded_rectangle((p, p + 48, p + lw, p + 50), 1, fill=_ORANGE)
|
||||
|
||||
ct = f"{n} tracks"
|
||||
draw.text((W - p - hf.getlength(ct), p + 22), ct, font=hf, fill=_FADED)
|
||||
|
||||
title_f = self._f(22)
|
||||
artist_f = self._f(16)
|
||||
time_f = self._f(14)
|
||||
num_f = self._f(12)
|
||||
cp = 12
|
||||
cvsz = row_h - cp * 2
|
||||
card_w = W - p * 2
|
||||
|
||||
yo = p + hdr + 8
|
||||
for idx, trk in enumerate(tracks):
|
||||
ry = int(yo)
|
||||
|
||||
card = Image.new("RGBA", (card_w, row_h), (0, 0, 0, 0))
|
||||
cd = ImageDraw.Draw(card)
|
||||
cd.rounded_rectangle(
|
||||
(0, 0, card_w, row_h),
|
||||
12,
|
||||
fill=_CARD_ACTIVE if idx == 0 else _CARD_BG,
|
||||
)
|
||||
if idx == 0:
|
||||
cd.rounded_rectangle((0, 0, 4, row_h), 2, fill=_ORANGE)
|
||||
region = bg.crop((p, ry, p + card_w, ry + row_h))
|
||||
bg.paste(Image.alpha_composite(region, card), (p, ry))
|
||||
|
||||
try:
|
||||
cv_data = fetch_cover(trk.hq_cover)
|
||||
cv = _rounded_cover(cv_data, cvsz, 8)
|
||||
bg.paste(cv, (p + cp + 6, ry + cp), cv)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
draw = ImageDraw.Draw(bg)
|
||||
|
||||
nt = f"{idx + 1:02d}"
|
||||
nw = num_f.getlength(nt)
|
||||
nx = p + cp + 6 + (cvsz - nw) // 2
|
||||
ny = ry + cp + cvsz - 18
|
||||
draw.rounded_rectangle(
|
||||
(nx - 3, ny - 1, nx + nw + 3, ny + 14), 3, fill=(0, 0, 0, 170)
|
||||
)
|
||||
draw.text((nx, ny - 1), nt, font=num_f, fill=_ORANGE)
|
||||
|
||||
txt_x = p + cp + cvsz + 24
|
||||
txt_w = card_w - cvsz - cp * 3 - 24 - 70
|
||||
ty_center = ry + (row_h - 58) // 2
|
||||
|
||||
draw.text(
|
||||
(txt_x, ty_center),
|
||||
_ellipsis(trk.title, title_f, txt_w),
|
||||
font=title_f,
|
||||
fill="white",
|
||||
)
|
||||
draw.text(
|
||||
(txt_x, ty_center + 30),
|
||||
_ellipsis(trk.artist, artist_f, txt_w),
|
||||
font=artist_f,
|
||||
fill=_DIM,
|
||||
)
|
||||
|
||||
dt = trk.duration_fmt
|
||||
dw = time_f.getlength(dt)
|
||||
draw.text(
|
||||
(p + card_w - cp - dw - 8, ty_center + 4),
|
||||
dt,
|
||||
font=time_f,
|
||||
fill=_FADED,
|
||||
)
|
||||
|
||||
if trk.plays:
|
||||
pt = f"▶ {_compact(trk.plays)}"
|
||||
pw = time_f.getlength(pt)
|
||||
draw.text(
|
||||
(p + card_w - cp - pw - 8, ty_center + 24),
|
||||
pt,
|
||||
font=time_f,
|
||||
fill=_FADED,
|
||||
)
|
||||
|
||||
yo += row_h + gap
|
||||
|
||||
return _export(_round_corners(bg, 20), "soundcloud_history.png")
|
||||
|
||||
|
||||
def _require_token(func):
|
||||
"""Decorator: ensure oauth_token is configured."""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapper(self, message, *a, **kw):
|
||||
if not self.config["oauth_token"]:
|
||||
return await utils.answer(message, self.strings("no_token"))
|
||||
return await func(self, message, *a, **kw)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _catch_errors(func):
|
||||
"""Decorator: log & report exceptions to user."""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapper(self, message, *a, **kw):
|
||||
try:
|
||||
return await func(self, message, *a, **kw)
|
||||
except Exception:
|
||||
logger.exception("SoundCloud: %s failed", func.__name__)
|
||||
with contextlib.suppress(Exception):
|
||||
import traceback
|
||||
|
||||
await utils.answer(
|
||||
message, self.strings("error").format(traceback.format_exc())
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@loader.tds
|
||||
class SoundCloudMod(loader.Module):
|
||||
"""Display the currently playing SoundCloud track as a stylized card."""
|
||||
|
||||
strings = {
|
||||
"name": "SoundCloud",
|
||||
"no_token": (
|
||||
"<emoji document_id=5778527486270770928>\u274c</emoji>"
|
||||
" <b>Set </b><code>oauth_token</code><b> in module config</b>\n\n"
|
||||
"\U0001f511 Get it via extension:\n"
|
||||
"\u2022 <a href='https://chromewebstore.google.com/detail/"
|
||||
"jgocamehhjhbhomfnhknmiljlhjbaldg'>Chromium</a>\n"
|
||||
"\u2022 <a href='https://addons.mozilla.org/en-US/firefox/addon/"
|
||||
"playinnowbot/'>Firefox</a>\n"
|
||||
"\u2022 Or via DevTools: Application \u2192 Cookies \u2192 "
|
||||
"<code>oauth_token</code>"
|
||||
),
|
||||
"nothing": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Nothing is playing right now</b>"
|
||||
),
|
||||
"error": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Error</b>\n<code>{}</code>"
|
||||
),
|
||||
"wait_card": (
|
||||
"\n\n<emoji document_id=5841359499146825803>🕔</emoji>"
|
||||
" <i>Generating card…</i>"
|
||||
),
|
||||
"wait_dl": (
|
||||
"\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>Downloading…</i>"
|
||||
),
|
||||
"dl_fail": (
|
||||
"\n\n<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <i>Download failed</i>"
|
||||
),
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"no_token": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Установи </b><code>oauth_token</code>"
|
||||
"<b> в конфиге модуля</b>\n\n"
|
||||
"🔑 Получить токен:\n"
|
||||
"• <a href='https://chromewebstore.google.com/detail/"
|
||||
"jgocamehhjhbhomfnhknmiljlhjbaldg'>Chromium</a>\n"
|
||||
"• <a href='https://addons.mozilla.org/en-US/firefox/addon/"
|
||||
"playinnowbot/'>Firefox</a>\n"
|
||||
"• Или через DevTools: Application → Cookies → "
|
||||
"<code>oauth_token</code>"
|
||||
),
|
||||
"nothing": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Сейчас ничего не играет</b>"
|
||||
),
|
||||
"error": (
|
||||
"<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <b>Ошибка</b>\n<code>{}</code>"
|
||||
),
|
||||
"wait_card": (
|
||||
"\n\n<emoji document_id=5841359499146825803>🕔</emoji>"
|
||||
" <i>Генерация карточки…</i>"
|
||||
),
|
||||
"wait_dl": (
|
||||
"\n\n<emoji document_id=5841359499146825803>🕔</emoji> <i>Скачивание…</i>"
|
||||
),
|
||||
"dl_fail": (
|
||||
"\n\n<emoji document_id=5778527486270770928>❌</emoji>"
|
||||
" <i>Ошибка скачивания</i>"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._font_data: Optional[bytes] = None
|
||||
self._font_src: Optional[str] = None
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"show_banner",
|
||||
True,
|
||||
"Generate image card",
|
||||
validator=loader.validators.Boolean(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"banner_type",
|
||||
"square",
|
||||
"Card layout",
|
||||
validator=loader.validators.Choice(["square", "horizontal"]),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"template",
|
||||
(
|
||||
"<emoji document_id=6007938409857815902>🎧</emoji>"
|
||||
" <b>Now playing:</b> {artist} — {track}\n"
|
||||
"<emoji document_id=5776213190387961618>🕓</emoji>"
|
||||
" {duration}{genre}\n"
|
||||
"<emoji document_id=5877465816030515018>🔗</emoji>"
|
||||
" <b><a href='{url}'>SoundCloud</a></b>"
|
||||
),
|
||||
"Message template. Placeholders: {track}, {artist},"
|
||||
" {url}, {duration}, {genre}, {stats}",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"font",
|
||||
"https://github.com/web-fonts/ttf/raw/refs/heads/master/alk-sanet-webfont.ttf",
|
||||
"URL to .ttf font file",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"oauth_token",
|
||||
"",
|
||||
"SoundCloud OAuth token",
|
||||
validator=loader.validators.String(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"history_count",
|
||||
5,
|
||||
"Tracks in history (3–5)",
|
||||
validator=loader.validators.Integer(minimum=3, maximum=5),
|
||||
),
|
||||
)
|
||||
|
||||
def _headers(self) -> dict:
|
||||
return {
|
||||
"Authorization": f"OAuth {self.config['oauth_token']}",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
),
|
||||
}
|
||||
|
||||
async def _get(self, path: str, **params) -> Optional[dict]:
|
||||
try:
|
||||
r = await utils.run_sync(
|
||||
requests.get,
|
||||
f"{_API}{path}",
|
||||
headers=self._headers(),
|
||||
params=params,
|
||||
timeout=5,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json()
|
||||
except Exception:
|
||||
logger.debug("SC API %s failed", path)
|
||||
return None
|
||||
|
||||
async def _load_font(self) -> bytes:
|
||||
url = self.config["font"]
|
||||
if self._font_data and self._font_src == url:
|
||||
return self._font_data
|
||||
data = await utils.run_sync(lambda: requests.get(url, timeout=10).content)
|
||||
self._font_data = data
|
||||
self._font_src = url
|
||||
return data
|
||||
|
||||
async def _load_cover(self, url: str) -> Optional[bytes]:
|
||||
try:
|
||||
hq = url.replace("-large", _COVER_HQ)
|
||||
r = await utils.run_sync(requests.get, hq, timeout=10)
|
||||
if r.status_code == 200:
|
||||
return r.content
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _current(self) -> Optional[TrackInfo]:
|
||||
for ep in ("/me/play-history/tracks", "/me/activities", "/stream"):
|
||||
data = await self._get(ep, limit=3)
|
||||
if not data:
|
||||
continue
|
||||
for item in data.get("collection", []):
|
||||
raw = item.get("track") or item
|
||||
if raw and "title" in raw and (raw.get("duration") or 0) > 0:
|
||||
return TrackInfo.parse(raw)
|
||||
return None
|
||||
|
||||
async def _recent(self, count: int) -> List[TrackInfo]:
|
||||
data = await self._get("/me/play-history/tracks", limit=count)
|
||||
if not data:
|
||||
return []
|
||||
return [
|
||||
TrackInfo.parse(it["track"])
|
||||
for it in data.get("collection", [])
|
||||
if it.get("track") and "title" in it["track"]
|
||||
]
|
||||
|
||||
async def _download(self, url: str) -> Optional[bytes]:
|
||||
try:
|
||||
token = self.config["oauth_token"]
|
||||
opts = {
|
||||
"format": "best[ext=mp3]/best",
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"http_headers": {
|
||||
"Authorization": f"OAuth {token}",
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
def _run():
|
||||
with YoutubeDL(opts) as ydl:
|
||||
info = ydl.extract_info(url, download=False)
|
||||
audio = info.get("url")
|
||||
if audio:
|
||||
r = requests.get(audio, timeout=60)
|
||||
if r.status_code == 200:
|
||||
return r.content
|
||||
return None
|
||||
|
||||
return await utils.run_sync(_run)
|
||||
except Exception as e:
|
||||
logger.error("Download failed: %s", e)
|
||||
return None
|
||||
|
||||
def _format_message(self, t: TrackInfo) -> str:
|
||||
genre_part = f" | {utils.escape_html(t.genre)}" if t.genre else ""
|
||||
stats = []
|
||||
if t.plays:
|
||||
stats.append(f"▶ {_compact(t.plays)}")
|
||||
if t.likes:
|
||||
stats.append(f"♥ {_compact(t.likes)}")
|
||||
return self.config["template"].format(
|
||||
track=utils.escape_html(t.title),
|
||||
artist=utils.escape_html(t.artist),
|
||||
duration=t.duration_fmt,
|
||||
url=t.permalink,
|
||||
genre=genre_part,
|
||||
stats=" · ".join(stats),
|
||||
)
|
||||
|
||||
def _format_detail(self, t: TrackInfo) -> str:
|
||||
parts = [t.duration_fmt]
|
||||
if t.genre:
|
||||
parts.append(utils.escape_html(t.genre))
|
||||
if t.plays:
|
||||
parts.append(f"▶ {_compact(t.plays)}")
|
||||
if t.likes:
|
||||
parts.append(f"♥ {_compact(t.likes)}")
|
||||
info = " | ".join(parts)
|
||||
return (
|
||||
f"<emoji document_id=6007938409857815902>🎧</emoji>"
|
||||
f" <b>{utils.escape_html(t.artist)} — {utils.escape_html(t.title)}</b>\n"
|
||||
f"<emoji document_id=5776213190387961618>🕓</emoji> {info}\n"
|
||||
f"<emoji document_id=5877465816030515018>🔗</emoji>"
|
||||
f" <b><a href='{t.permalink}'>SoundCloud</a></b>"
|
||||
)
|
||||
|
||||
@_catch_errors
|
||||
@_require_token
|
||||
@loader.command(
|
||||
ru_doc="— Показать карточку текущего трека",
|
||||
en_doc="— Show current track card",
|
||||
)
|
||||
async def scnow(self, message: Message):
|
||||
track = await self._current()
|
||||
if not track:
|
||||
return await utils.answer(message, self.strings("nothing"))
|
||||
|
||||
text = self._format_message(track)
|
||||
|
||||
if not (self.config["show_banner"] and track.cover_url):
|
||||
return await utils.answer(message, text)
|
||||
|
||||
msg = await utils.answer(message, text + self.strings("wait_card"))
|
||||
|
||||
cover = await self._load_cover(track.cover_url)
|
||||
if not cover:
|
||||
return await utils.answer(msg, text)
|
||||
|
||||
font_data = await self._load_font()
|
||||
factory = CardFactory(_Fonts(font_data))
|
||||
|
||||
render = (
|
||||
factory.square
|
||||
if self.config["banner_type"] == "square"
|
||||
else factory.horizontal
|
||||
)
|
||||
card = await utils.run_sync(render, track, cover)
|
||||
await utils.answer(msg, text, file=card)
|
||||
|
||||
@_catch_errors
|
||||
@_require_token
|
||||
@loader.command(
|
||||
ru_doc="— Скачать текущий трек",
|
||||
en_doc="— Download current track",
|
||||
)
|
||||
async def scnowt(self, message: Message):
|
||||
track = await self._current()
|
||||
if not track:
|
||||
return await utils.answer(message, self.strings("nothing"))
|
||||
|
||||
text = self._format_detail(track)
|
||||
msg = await utils.answer(message, text + self.strings("wait_dl"))
|
||||
|
||||
audio = await self._download(track.permalink)
|
||||
if not audio:
|
||||
return await utils.answer(msg, text + self.strings("dl_fail"))
|
||||
|
||||
buf = io.BytesIO(audio)
|
||||
buf.name = f"{track.artist} - {track.title}.mp3"
|
||||
await utils.answer(msg, text, file=buf)
|
||||
|
||||
@_catch_errors
|
||||
@_require_token
|
||||
@loader.command(
|
||||
ru_doc="— История прослушивания",
|
||||
en_doc="— Listening history",
|
||||
)
|
||||
async def schistory(self, message: Message):
|
||||
tracks = await self._recent(self.config["history_count"])
|
||||
if not tracks:
|
||||
return await utils.answer(message, self.strings("nothing"))
|
||||
|
||||
text = (
|
||||
"<emoji document_id=5776213190387961618>📜</emoji>"
|
||||
" <b>История прослушивания:</b>\n\n"
|
||||
)
|
||||
for i, t in enumerate(tracks, 1):
|
||||
parts = [t.duration_fmt]
|
||||
if t.genre:
|
||||
parts.append(utils.escape_html(t.genre))
|
||||
if t.plays:
|
||||
parts.append(f"▶ {_compact(t.plays)}")
|
||||
meta = " | ".join(parts)
|
||||
text += (
|
||||
f"{i}. <b>{utils.escape_html(t.artist)} —"
|
||||
f" {utils.escape_html(t.title)}</b>\n"
|
||||
f" <emoji document_id=5776213190387961618>🕓</emoji>"
|
||||
f" {meta} | <a href='{t.permalink}'>Link</a>\n\n"
|
||||
)
|
||||
|
||||
if not self.config["show_banner"]:
|
||||
return await utils.answer(message, text)
|
||||
|
||||
msg = await utils.answer(message, text + self.strings("wait_card"))
|
||||
try:
|
||||
font_data = await self._load_font()
|
||||
|
||||
def _render():
|
||||
factory = CardFactory(_Fonts(font_data))
|
||||
|
||||
def fetcher(u):
|
||||
return requests.get(u, timeout=10).content
|
||||
|
||||
return factory.history(tracks, fetcher)
|
||||
|
||||
card = await utils.run_sync(_render)
|
||||
await utils.answer(msg, text, file=card)
|
||||
except Exception:
|
||||
await utils.answer(msg, text)
|
||||
@@ -1,92 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2026-2029 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TimeZone
|
||||
# Description: Prints current time in selected timezone (UTC+n and tzdata formats supported)
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: tzdata
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import tzdata
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TimeZoneMod(loader.Module):
|
||||
"""Prints current time in selected timezone (UTC+n and tzdata formats supported)"""
|
||||
|
||||
strings = {
|
||||
"name": "TimeZone",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or they are invalid",
|
||||
"_cls_doc": "Prints current time in selected timezone (UTC+n and tzdata formats supported)",
|
||||
"time_utc": "<emoji document_id=5276412364458059956>🕓</emoji> Current time by UTC+{}: {}",
|
||||
"time_tzdata": "<emoji document_id=5276412364458059956>🕓</emoji> Current time in {}: {}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Выводит текущее время в выбранном часовом поясе (поддерживаются форматы UTC+n и tzdata)",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или они неверны",
|
||||
"tzdata_error": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка при получении времени по tzdata: {}\n\nУбедитесь, что часовой пояс указан верно",
|
||||
"time_utc": "<emoji document_id=5276412364458059956>🕓</emoji> Текущее время по UTC+{}: {}",
|
||||
"time_tzdata": "<emoji document_id=5276412364458059956>🕓</emoji> Текущее время в {}: {}",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выводит время по UTC+n | Использование: .utc 4",
|
||||
en_doc="Prints UTC+n time | Usage: .utc 4",
|
||||
)
|
||||
async def utccmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
if not args or not args[0].isdigit() or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
offset = timedelta(hours=int(args[0]))
|
||||
tz = timezone(offset)
|
||||
time = datetime.now(tz)
|
||||
await utils.answer(
|
||||
message, self.strings["time_utc"].format(args[0], time.strftime("%H:%M:%S"))
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выводит время по часовому поясу tzdata | Использование: .tzdata Europe/Moscow",
|
||||
en_doc="Prints time by tzdata timezone | Usage: .tzdata Europe/Moscow",
|
||||
)
|
||||
async def tzdatacmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
if args[0].isdigit() or not args or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
try:
|
||||
time = datetime.now(ZoneInfo(args[0]))
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["tzdata_error"].format(e))
|
||||
logger.error(self.strings["tzdata_error"].format(e))
|
||||
return
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["time_tzdata"].format(args[0], time.strftime("%H:%M:%S")),
|
||||
)
|
||||
@@ -1,238 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2026-2029
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: YTDL
|
||||
# Description: Downloads and sends audio/video from YouTube
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: yt_dlp ffmpeg
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import shutil
|
||||
import platform
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import zipfile
|
||||
import os
|
||||
import re
|
||||
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from yt_dlp import YoutubeDL
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class YTDLMod(loader.Module):
|
||||
"""Downloads and sends audio/video from YouTube"""
|
||||
|
||||
strings = {
|
||||
"name": "YTDL",
|
||||
"_cls_doc": "Downloads and sends audio/video from YouTube",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or they are invalid",
|
||||
"downloading": "<emoji document_id=5215484787325676090>🕐</emoji> Downloading...",
|
||||
"done": "<emoji document_id=5854762571659218443>✅</emoji> Done!",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Скачивает и отправляет аудио/видео с Ютуба",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или они неверны",
|
||||
"downloading": "<emoji document_id=5215484787325676090>🕐</emoji> Скачиваю...",
|
||||
"done": "<emoji document_id=5854762571659218443>✅</emoji> Готово!",
|
||||
}
|
||||
|
||||
def _validate_url(self, url: str) -> bool:
|
||||
"""Validate URL format"""
|
||||
if not url:
|
||||
return False
|
||||
|
||||
url_pattern = re.compile(
|
||||
r"^(?:https?://)?(?:www\.|m\.)?(?:youtube\.com|youtu\.be|music\.youtube\.com)/(?:watch\?v=|playlist\?list=|channel/|@|live/|shorts/)?[\w-]+",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
return url_pattern.match(url) is not None
|
||||
|
||||
async def get_target(self):
|
||||
system = platform.system()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "Windows":
|
||||
return "Windows"
|
||||
|
||||
if system == "Darwin":
|
||||
return (
|
||||
"aarch64-apple-darwin" if machine == "arm64" else "x86_64-apple-darwin"
|
||||
)
|
||||
|
||||
if system == "Linux":
|
||||
return (
|
||||
"aarch64-unknown-linux-gnu"
|
||||
if machine in ("aarch64", "arm64")
|
||||
else "x86_64-unknown-linux-gnu"
|
||||
)
|
||||
|
||||
return "x86_64-unknown-linux-gnu"
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"youtube_cookie",
|
||||
None,
|
||||
"Cookie вашего Ютуб-аккаунта (повышает стабильность и помогает скачивать видео с жесткими возрастными ограничениями) | Cookie of your YouTube-account (increases stability and helps downloading video with strict age rating restricrions)",
|
||||
validator=loader.validators.Hidden(),
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
deno_path = Path("deno")
|
||||
deno_which = shutil.which("deno")
|
||||
|
||||
# Trying to fix previous shitcode...
|
||||
if self.get("deno_source") == "file":
|
||||
self.set("deno_source", str(deno_path.resolve()))
|
||||
|
||||
if not deno_which and not deno_path.is_file():
|
||||
logger.warning("Deno is not installed, attempting installation...")
|
||||
target = await self.get_target()
|
||||
if target == "Windows":
|
||||
logger.critical(
|
||||
"Windows platform is unsupported by this module. All future commands will fail. Please, unload the module."
|
||||
)
|
||||
return
|
||||
async with aiohttp.ClientSession() as session:
|
||||
download_link = f"https://github.com/denoland/deno/releases/latest/download/deno-{target}.zip"
|
||||
async with session.get(download_link) as resp:
|
||||
if resp.status == 200:
|
||||
async with aiofiles.open("deno.zip", mode="wb") as f:
|
||||
async for chunk in resp.content.iter_chunked(8192):
|
||||
await f.write(chunk)
|
||||
else:
|
||||
logger.critical(f"Failed to download Deno: HTTP {resp.status}")
|
||||
self.set("deno_source", "install_failed")
|
||||
return
|
||||
if Path("deno.zip").is_file():
|
||||
with zipfile.ZipFile("deno.zip", "r") as zip_ref:
|
||||
zip_ref.extractall()
|
||||
os.remove("deno.zip")
|
||||
os.chmod(deno_path, 0o755)
|
||||
self.set("deno_source", str(deno_path.resolve()))
|
||||
elif deno_which:
|
||||
self.set("deno_source", deno_which)
|
||||
|
||||
@loader.command(en_doc="Download video", ru_doc="Скачать видео")
|
||||
async def ytdlvcmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
if not args or not self._validate_url(args[0]) or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
|
||||
source = self.get("deno_source")
|
||||
if source == "install_failed" or not Path(source).is_file():
|
||||
logger.critical(
|
||||
"Deno wasn't installed in auto-mode. Please, install it manually or resolve the issue and reboot userbot."
|
||||
)
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings["downloading"])
|
||||
|
||||
filename_prefix = f"video_{message.id}"
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"outtmpl": f"{filename_prefix}.%(ext)s",
|
||||
"js_runtimes": {"deno": {"path": source}},
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegVideoConvertor",
|
||||
"preferedformat": "mp4",
|
||||
}
|
||||
],
|
||||
"postprocessor_args": {
|
||||
"video_convertor": [
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-preset",
|
||||
"veryfast",
|
||||
"-crf",
|
||||
"23",
|
||||
"-c:a",
|
||||
"aac",
|
||||
],
|
||||
"merger": ["-movflags", "faststart"],
|
||||
},
|
||||
}
|
||||
if self.get("youtube_cookie"):
|
||||
ydl_opts["cookiefile"] = self.get("youtube_cookie")
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(args[0], download=True)
|
||||
filename = ydl.prepare_filename(info).split(".")[0] + ".mp4"
|
||||
await utils.answer(message, self.strings['done'], file=filename, invert_media=True)
|
||||
os.remove(filename)
|
||||
|
||||
@loader.command(en_doc="Download audio", ru_doc="Скачать аудио")
|
||||
async def ytdlacmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
if not args or not self._validate_url(args[0]) or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
|
||||
source = self.get("deno_source")
|
||||
if source == "install_failed" or not Path(source).is_file():
|
||||
logger.critical(
|
||||
"Deno wasn't installed in auto-mode. Please, install it manually or resolve the issue and reboot userbot."
|
||||
)
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings["downloading"])
|
||||
|
||||
filename_prefix = f"audio_{message.id}"
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"outtmpl": f"{filename_prefix}.%(ext)s",
|
||||
"js_runtimes": {"deno": {"path": source}},
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": "0",
|
||||
},
|
||||
{
|
||||
"key": "FFmpegMetadata",
|
||||
"add_metadata": True,
|
||||
},
|
||||
{
|
||||
"key": "EmbedThumbnail",
|
||||
},
|
||||
],
|
||||
"writethumbnail": True,
|
||||
}
|
||||
if self.get("youtube_cookie"):
|
||||
ydl_opts["cookiefile"] = self.get("youtube_cookie")
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(args[0], download=True)
|
||||
filename = ydl.prepare_filename(info).split(".")[0] + ".mp3"
|
||||
await utils.answer(message, self.strings['done'], file=filename)
|
||||
os.remove(filename)
|
||||
4
archquise/q.mods/.gitignore
vendored
4
archquise/q.mods/.gitignore
vendored
@@ -4,4 +4,6 @@
|
||||
.ruff_cache
|
||||
ruff.log
|
||||
ruff.log.2
|
||||
ruff.toml
|
||||
ruff.toml
|
||||
# Heroku files
|
||||
heroku/
|
||||
|
||||
438
archquise/q.mods/QNotes.py
Normal file
438
archquise/q.mods/QNotes.py
Normal file
@@ -0,0 +1,438 @@
|
||||
__version__ = (1, 1, 6)
|
||||
|
||||
# █▀▀▄ █▀▄▀█ █▀█ █▀▄ █▀
|
||||
# ▀▀▀█ ▄ █ ▀ █ █▄█ █▄▀ ▄█
|
||||
|
||||
# #### Copyright (c) 2026 Archquise #####
|
||||
|
||||
# 💬 Contact: https://t.me/archquise
|
||||
# 🔒 Licensed under the GNU AGPLv3.
|
||||
# 📄 LICENSE: https://raw.githubusercontent.com/archquise/Q.Mods/main/LICENSE
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: QNotes
|
||||
# Description: A notes module that just works
|
||||
# Author: @quise_m
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @quise_m
|
||||
# meta banner: https://raw.githubusercontent.com/archquise/qmods_meta/main/qnotes.png
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from datetime import date
|
||||
from typing import cast
|
||||
|
||||
from herokutl.tl.functions.users import GetUsersRequest
|
||||
from herokutl.tl.types import InputUserSelf
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class QNotes(loader.Module):
|
||||
"""A notes module that just works\nUsage: #notetag in any chat"""
|
||||
|
||||
strings = {
|
||||
"name": "QNotes",
|
||||
"topic_desc": "Stores your notes content\nUsage: #notetag in any chat",
|
||||
"wrongargs": "<emoji document_id=5980953710157632545>❌</emoji> <b>Wrong arguments. Check command usage.</b>",
|
||||
"not_exist": "There is no such note!",
|
||||
"no_reply": "No reply! Reply to the message, which text will become a note.",
|
||||
"already_exists": "Seems like note with the same tag already exists. Overwrite?",
|
||||
"show_note_inline": "<blockquote>#{}</blockquote>\n\n<blockquote>{}</blockquote>",
|
||||
"notelist": "Note list:",
|
||||
"msg_not_found_inline": "Message with this note wasn't found. Probably, it was been removed. Note has been removed from the database.",
|
||||
"remnote_inline": "🗑 Remove",
|
||||
"close_inline": "❌ Close",
|
||||
"yes": "✔️ Yes",
|
||||
"no": "❌ No",
|
||||
"true": "yes",
|
||||
"false": "no",
|
||||
"saved": "Note saved!",
|
||||
"removed": "Note removed!",
|
||||
"nonotes": "You don't have any notes!",
|
||||
"privacy_switch": "Determines whose data will be used by the my_* placeholders\n\nTrue - the account that is issuing the note\nFalse - the account on which the userbot is running",
|
||||
"note_prefix": "The prefix used to call up notes",
|
||||
"placeholders": """
|
||||
<b>Available placeholders</b>:
|
||||
|
||||
about the account on which userbot is installed:
|
||||
{my_id} - ID
|
||||
@{my_username} - username, tag
|
||||
{my_phone} - phone number
|
||||
{my_premium} - premium status (yes/no)
|
||||
|
||||
about reply author:
|
||||
{reply_id} - ID
|
||||
{reply_name} - name
|
||||
{reply_surname} - surname
|
||||
{reply_fullname} - full name (name + surname (if specified))
|
||||
@{reply_username} - username, tag
|
||||
{reply_phone} - phone number (if not hidden)
|
||||
{reply_premium} - premium status (yes/no)
|
||||
|
||||
general:
|
||||
{today} - current date
|
||||
""",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль для заметок, который просто работает\nИспользование: #тегзаметки в любом чате",
|
||||
"topic_desc": "Хранит содержимое ваших заметок\nИспользование: #тегзаметки в любом чате",
|
||||
"wrongargs": "<emoji document_id=5980953710157632545>❌</emoji> <b>Неверные аргументы. Проверьте использование команды.</b>",
|
||||
"no_reply": "Нет реплая! Ответьте на сообщение, текст которого станет заметкой.",
|
||||
"not_exist": "Такой заметки не найдено!",
|
||||
"already_exists": "Кажется, заметка с таким тегом уже существует. Перезаписать?",
|
||||
"show_note_inline": "<blockquote>#{}</blockquote>\n\n<blockquote>{}</blockquote>",
|
||||
"notelist": "Список заметок:",
|
||||
"msg_not_found_inline": "Сообщение с этой заметкой не было найдено. Вероятно, оно было удалено. Заметка очищена из базы данных.",
|
||||
"remnote_inline": "🗑 Удалить",
|
||||
"close_inline": "❌ Закрыть",
|
||||
"yes": "✔️ Да",
|
||||
"no": "❌ Нет",
|
||||
"saved": "Заметка сохранена!",
|
||||
"removed": "Заметка удалена!",
|
||||
"true": "да",
|
||||
"false": "нет",
|
||||
"nonotes": "Нет заметок!",
|
||||
"privacy_switch": "Влияет на то, чьи данные будут использовать my_* плейсхолдеры\n\nTrue - аккаунта, который вызывает заметку\nFalse - аккаунта на котором стоит юзербот",
|
||||
"note_prefix": "Префикс, с которым вызываются заметки",
|
||||
"placeholders": """
|
||||
<b>Доступные плейсхолдеры</b>:
|
||||
|
||||
об аккаунте, на котором стоит юзербот:
|
||||
{my_id} - айди
|
||||
@{my_username} - юзернейм, тег
|
||||
{my_phone} - номер телефона
|
||||
{my_premium} - статус премиум (да/нет)
|
||||
|
||||
об авторе реплая:
|
||||
{reply_id} - айди
|
||||
{reply_name} - имя
|
||||
{reply_surname} - фамилия
|
||||
{reply_fullname} - полное имя (имя + фамилия (если указана))
|
||||
@{reply_username} - юзернейм, тег
|
||||
{reply_phone} - номер телефона (если не скрыт)
|
||||
{reply_premium} - статус премиум (да/нет)
|
||||
|
||||
общее:
|
||||
{today} - текущая дата
|
||||
""",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"privacy_switch",
|
||||
True,
|
||||
lambda: self.strings["privacy_switch"],
|
||||
validator=loader.validators.Boolean(), # type: ignore
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"note_prefix",
|
||||
"#",
|
||||
lambda: self.strings["note_prefix"],
|
||||
validator=loader.validators.RegExp(r"^\S+$"), # type: ignore
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db): # type: ignore
|
||||
self._content_channel_id = await utils.wait_for_content_channel(self._db)
|
||||
self._notes_topic = await utils.asset_forum_topic(
|
||||
client=self._client,
|
||||
db=self._db,
|
||||
peer=self._content_channel_id, # type: ignore
|
||||
title="QNotes | Storage",
|
||||
description=self.strings["topic_desc"],
|
||||
icon_emoji_id=5272001961326049733,
|
||||
)
|
||||
|
||||
self.my_phone = (await self._client(GetUsersRequest(id=[InputUserSelf()])))[
|
||||
0
|
||||
].phone
|
||||
|
||||
self.placeholders = {
|
||||
"my_phone": self.my_phone,
|
||||
"my_username": self._client.heroku_me.username,
|
||||
"my_id": self.tg_id,
|
||||
"my_premium": self.strings["true"]
|
||||
if self._client.heroku_me.premium
|
||||
else self.strings["false"],
|
||||
}
|
||||
|
||||
self._notemap = cast(dict, self.pointer("notemap", default={}))
|
||||
|
||||
async def _ask_overwrite(self, message):
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
future = loop.create_future()
|
||||
|
||||
form = await self.inline.form(
|
||||
self.strings["already_exists"],
|
||||
message=message,
|
||||
reply_markup=[
|
||||
[
|
||||
{
|
||||
"text": self.strings["yes"],
|
||||
"callback": (
|
||||
lambda call, flag: (
|
||||
future.set_result(flag) if not future.done() else None
|
||||
)
|
||||
),
|
||||
"args": (True,),
|
||||
},
|
||||
{
|
||||
"text": self.strings["no"],
|
||||
"callback": (
|
||||
lambda call, flag: (
|
||||
future.set_result(flag) if not future.done() else None
|
||||
)
|
||||
),
|
||||
"args": (False,),
|
||||
},
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(15):
|
||||
overwrite_answer = await future
|
||||
except TimeoutError:
|
||||
await form.delete() # type: ignore
|
||||
return False, message
|
||||
|
||||
if not overwrite_answer:
|
||||
await form.delete() # type: ignore
|
||||
return False, form
|
||||
|
||||
return True, form
|
||||
|
||||
async def _show_note_inline(self, call, note, page=0):
|
||||
async def _remnote(call, notetag, note_msg):
|
||||
await note_msg.delete()
|
||||
self._notemap.pop(notetag, None)
|
||||
|
||||
await call.edit(self.strings["removed"])
|
||||
|
||||
note_msg = await self._client.get_messages(
|
||||
self._content_channel_id, ids=note[1]
|
||||
)
|
||||
|
||||
if not note_msg:
|
||||
self._notemap.pop(note[0], None)
|
||||
|
||||
await call.edit(
|
||||
self.strings["msg_not_found_inline"],
|
||||
reply_markup=[
|
||||
{"text": "⬅️ Назад", "callback": self._list_page, "args": (page,)},
|
||||
{"text": self.strings["close_inline"], "action": "close"},
|
||||
],
|
||||
)
|
||||
return
|
||||
|
||||
await call.edit(
|
||||
self.strings["show_note_inline"].format(note[0], note_msg.text), # type: ignore
|
||||
reply_markup=[
|
||||
[
|
||||
{"text": "⬅️ Назад", "callback": self._list_page, "args": (page,)},
|
||||
{
|
||||
"text": self.strings["remnote_inline"],
|
||||
"callback": _remnote,
|
||||
"args": (note[0], note_msg),
|
||||
},
|
||||
],
|
||||
[{"text": self.strings["close_inline"], "action": "close"}],
|
||||
],
|
||||
)
|
||||
|
||||
def _build_list_markup(self, page: int):
|
||||
items = list(self._notemap.items())
|
||||
total = -(-len(items) // 3)
|
||||
page = max(0, min(page, total - 1))
|
||||
rows = [
|
||||
[
|
||||
{
|
||||
"text": notetag,
|
||||
"callback": self._show_note_inline,
|
||||
"args": ([notetag, msg_id], page),
|
||||
}
|
||||
]
|
||||
for notetag, msg_id in items[page * 3 : (page + 1) * 3]
|
||||
]
|
||||
return (
|
||||
rows
|
||||
+ self.inline.build_pagination(
|
||||
callback=self._list_page, # type: ignore
|
||||
total_pages=total,
|
||||
current_page=page + 1,
|
||||
)
|
||||
+ [[{"text": self.strings["close_inline"], "action": "close"}]]
|
||||
)
|
||||
|
||||
async def _list_page(self, call, page):
|
||||
await call.edit(
|
||||
text=self.strings["notelist"], reply_markup=self._build_list_markup(page)
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Сохраняет заметку под тегом | Пример: .qnsave заметка",
|
||||
en_doc="Saves note by tag | Example: .qnsave note",
|
||||
)
|
||||
async def qnsave(self, message) -> None:
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings["wrongargs"])
|
||||
return
|
||||
|
||||
current_message = message
|
||||
|
||||
if not (reply := await message.get_reply_message()):
|
||||
await utils.answer(message, self.strings["no_reply"])
|
||||
return
|
||||
try:
|
||||
if args[0].strip() in self._notemap:
|
||||
need_overwrite, msg = await self._ask_overwrite(message)
|
||||
if not need_overwrite:
|
||||
return
|
||||
old_note_message = await self._client.get_messages(
|
||||
self._content_channel_id,
|
||||
ids=self._notemap[args[0].strip()],
|
||||
)
|
||||
old_note_message and await old_note_message.delete() # type: ignore
|
||||
current_message = msg
|
||||
|
||||
note_message = await self._client.send_message(
|
||||
self._content_channel_id,
|
||||
reply.text,
|
||||
reply_to=self._notes_topic.id,
|
||||
file=reply.media,
|
||||
)
|
||||
self._notemap[args[0].strip()] = note_message.id
|
||||
|
||||
except Exception as e:
|
||||
await utils.answer(current_message, f"Произошла ошибка: {e}")
|
||||
logger.exception("Произошла ошибка при сохранении заметки!")
|
||||
return
|
||||
await utils.answer(current_message, self.strings["saved"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Удаляет заметку по тегу | Пример: .qnrem заметка",
|
||||
en_doc="Removes note by tag | Example: .qnrem note",
|
||||
)
|
||||
async def qnrem(self, message) -> None:
|
||||
args = utils.get_args(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings["wrongargs"])
|
||||
return
|
||||
|
||||
if args[0] not in self._notemap or not (
|
||||
note_message := await self._client.get_messages(
|
||||
self._content_channel_id,
|
||||
ids=self._notemap[args[0]],
|
||||
)
|
||||
):
|
||||
await utils.answer(message, self.strings["not_exist"])
|
||||
return
|
||||
|
||||
await note_message.delete() # type: ignore
|
||||
self._notemap.pop(args[0], None)
|
||||
|
||||
await utils.answer(message, self.strings["removed"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выводит список всех заметок и позволяет управлять ими",
|
||||
en_doc="Shows note list and allows managing them",
|
||||
)
|
||||
async def qnlist(self, message) -> None:
|
||||
if self._notemap:
|
||||
await self.inline.form(
|
||||
text=self.strings["notelist"],
|
||||
reply_markup=self._build_list_markup(0),
|
||||
message=message,
|
||||
)
|
||||
return
|
||||
await utils.answer(message, self.strings["nonotes"])
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выводит список доступных плейсхолдеров",
|
||||
en_doc="Displays a list of available placeholders",
|
||||
)
|
||||
async def qnp(self, message) -> None:
|
||||
await utils.answer(message, self.strings["placeholders"])
|
||||
|
||||
@loader.watcher()
|
||||
async def _note_watcher(self, message):
|
||||
if not message.text.startswith(prefix := self.config["note_prefix"]) or not (
|
||||
await self._client.dispatcher.security.check(message, self._note_watcher)
|
||||
):
|
||||
return
|
||||
|
||||
notetag = message.text.split(prefix, maxsplit=1)[1]
|
||||
|
||||
if notetag in self._notemap:
|
||||
if not (
|
||||
note_message := await self._client.get_messages(
|
||||
self._content_channel_id,
|
||||
ids=self._notemap[notetag],
|
||||
)
|
||||
):
|
||||
self._notemap.pop(notetag, None)
|
||||
return
|
||||
notetext = note_message.text or "" # type: ignore
|
||||
if re.search(r"\{\w+\}", notetext):
|
||||
if (
|
||||
not self.config["privacy_switch"]
|
||||
or message.sender_id == self._client.heroku_me.id
|
||||
):
|
||||
placeholders = {**self.placeholders}
|
||||
else:
|
||||
message_author_entity = await self._client.get_entity(
|
||||
message.sender_id
|
||||
)
|
||||
placeholders = {
|
||||
"my_phone": (
|
||||
await self._client(GetUsersRequest(id=[message.sender_id]))
|
||||
)[0].phone,
|
||||
"my_username": message_author_entity.username,
|
||||
"my_id": message.sender_id,
|
||||
"my_premium": self.strings["true"]
|
||||
if message_author_entity.premium
|
||||
else self.strings["false"],
|
||||
}
|
||||
|
||||
if reply_msg := await message.get_reply_message():
|
||||
reply_user = await self._client.get_entity(reply_msg.sender_id)
|
||||
placeholders = {
|
||||
**placeholders,
|
||||
"reply_id": reply_user.id,
|
||||
"reply_fullname": " ".join(
|
||||
filter(None, [reply_user.first_name, reply_user.last_name])
|
||||
),
|
||||
"reply_name": reply_user.first_name,
|
||||
"reply_surname": reply_user.last_name,
|
||||
"reply_phone": (
|
||||
await self._client(GetUsersRequest(id=[reply_user.id]))
|
||||
)[0].phone,
|
||||
"reply_username": reply_user.username,
|
||||
"reply_premium": self.strings["true"]
|
||||
if reply_user.premium
|
||||
else self.strings["false"],
|
||||
}
|
||||
|
||||
placeholders = placeholders | {"today": date.today()}
|
||||
|
||||
def replacer(match):
|
||||
key = match.group(1)
|
||||
if key not in placeholders or not placeholders[key]:
|
||||
return match.group(0)
|
||||
return utils.escape_html(str(placeholders[key]))
|
||||
|
||||
notetext = re.sub(r"\{(\w+)\}", replacer, notetext)
|
||||
if media := note_message.media: # type: ignore
|
||||
await utils.answer_file(message, media, notetext) # type: ignore
|
||||
else:
|
||||
await utils.answer(message, notetext)
|
||||
return
|
||||
222
archquise/q.mods/UniversalDownloader.py
Normal file
222
archquise/q.mods/UniversalDownloader.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# █▀▀▄ █▀▄▀█ █▀█ █▀▄ █▀
|
||||
# ▀▀▀█ ▄ █ ▀ █ █▄█ █▄▀ ▄█
|
||||
|
||||
# #### Copyright (c) 2026 Archquise #####
|
||||
|
||||
# 💬 Contact: https://t.me/archquise
|
||||
# 🔒 Licensed under the GNU AGPLv3.
|
||||
# 📄 LICENSE: https://raw.githubusercontent.com/archquise/Q.Mods/main/LICENSE
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: UniversalDownloader # noqa: ERA001
|
||||
# Description: Downloads media from YouTube, VK, TikTok, and all yt-dlp supported sites
|
||||
# Author: @quise_m
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @quise_m
|
||||
# meta banner: https://raw.githubusercontent.com/archquise/qmods_meta/main/UniversalDownloader.png
|
||||
# requires: yt_dlp ffmpeg
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from http import HTTPStatus
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
from yt_dlp import YoutubeDL
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class UniversalDownloaderMod(loader.Module):
|
||||
"""Downloads media from YouTube, VK, TikTok, and all yt-dlp supported sites""" # noqa: D400, D415
|
||||
|
||||
strings = { # noqa: RUF012
|
||||
"name": "UniversalDownloader",
|
||||
"_cls_doc": "Downloads media from YouTube, VK, TikTok, and all yt-dlp supported sites", # noqa: E501
|
||||
"select_download_type": "<tg-emoji emoji-id=5879883461711367869>⬇️</tg-emoji> <b>Select download type:</b>", # noqa: E501
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or they are invalid", # noqa: E501
|
||||
"downloading": "<emoji document_id=5215484787325676090>🕐</emoji> Downloading...", # noqa: E501
|
||||
"cookie_desc": "Cookie account (helps downloading video with strict age rating restricrions)", # noqa: E501
|
||||
"deno_err": '<tg-emoji emoji-id=5879813604068298387>❗️</tg-emoji> <b>Error!</b> The <a href="http://deno.land/">Deno</a> JavaScript engine was not install automatically.\nThis is a required dependency for <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> (a library for downloading video/audio) to work correctly.\n\n<b>To continue, you need to install the engine manually, or resolve any issues preventing automatic installation and restart the userbot.</b>', # noqa: E501
|
||||
"err": "<tg-emoji emoji-id=5879813604068298387>❗️</tg-emoji> <b>Error!</b>\n\nAdditional info: {}", # noqa: E501
|
||||
"video": "video",
|
||||
"audio": "audio",
|
||||
}
|
||||
|
||||
strings_ru = { # noqa: RUF012
|
||||
"_cls_doc": "Скачивает медиа из YouTube, VK, TikTok и всех поддерживаемых yt-dlp сайтов", # noqa: E501
|
||||
"select_download_type": "<tg-emoji emoji-id=5879883461711367869>⬇️</tg-emoji> <b>Выберите тип загрузки:</b>", # noqa: E501
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или они неверны", # noqa: E501
|
||||
"downloading": "<emoji document_id=5215484787325676090>🕐</emoji> Скачиваю...",
|
||||
"cookie_desc": "Куки аккаунта (помогает скачивать видео с жесткими возрастными ограничениями)", # noqa: E501, RUF001
|
||||
"deno_err": '<tg-emoji emoji-id=5879813604068298387>❗️</tg-emoji> <b>Ошибка!</b> JS-движок <a href="http://deno.land/">Deno</a> не установился автоматически.\nЭто необходимая зависимость для корректной работы <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> (библиотека для скачивания видео/аудио).\n\n<b>Для продолжения вам необходимо установить движок вручную, или устранить препятствия для автоматической установки и перезагрузить юзербота.</b>', # noqa: E501
|
||||
"err": "<tg-emoji emoji-id=5879813604068298387>❗️</tg-emoji> <b>Ошибка!</b>\n\nДоп.информация: {}", # noqa: E501, RUF001
|
||||
"video": "видео",
|
||||
"audio": "аудио",
|
||||
}
|
||||
|
||||
deno_error = (
|
||||
"Deno wasn't installed in auto-mode.",
|
||||
"Please, install it manually or resolve the issue and reboot userbot.",
|
||||
)
|
||||
|
||||
def _validate_url(self, url: str) -> bool:
|
||||
"""Validate URL format."""
|
||||
if not url:
|
||||
return False
|
||||
|
||||
url_pattern = re.compile(
|
||||
r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
return url_pattern.match(url) is not None
|
||||
|
||||
async def get_target(self) -> str:
|
||||
"""Check OS and processor architecture and return right postfix."""
|
||||
system = platform.system()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "Windows":
|
||||
return "Windows"
|
||||
|
||||
if system == "Darwin":
|
||||
return (
|
||||
"aarch64-apple-darwin" if machine == "arm64" else "x86_64-apple-darwin"
|
||||
)
|
||||
|
||||
if system == "Linux":
|
||||
return (
|
||||
"aarch64-unknown-linux-gnu"
|
||||
if machine in ("aarch64", "arm64")
|
||||
else "x86_64-unknown-linux-gnu"
|
||||
)
|
||||
|
||||
return "x86_64-unknown-linux-gnu"
|
||||
|
||||
def _get_deno(self) -> str | None:
|
||||
if not (source := self.get("deno_source")) or source == "install_failed" or not os.path.exists(source):
|
||||
logger.critical("%s %s", *self.deno_error)
|
||||
return None
|
||||
return source
|
||||
|
||||
|
||||
def __init__(self): # noqa: ANN204, D107
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"youtube_cookie",
|
||||
None,
|
||||
lambda: self.strings["cookie_desc"],
|
||||
validator=loader.validators.Hidden(),
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db): # noqa: ANN001, ANN201, D102, ARG002
|
||||
|
||||
deno_which = shutil.which("deno", path=os.environ.get("PATH", "") + os.pathsep + os.getcwd()) # noqa: E501
|
||||
|
||||
if deno_which:
|
||||
self.set("deno_source", deno_which)
|
||||
return
|
||||
|
||||
logger.warning("Deno is not installed, attempting installation...")
|
||||
target = await self.get_target()
|
||||
if target == "Windows":
|
||||
logger.critical(
|
||||
"Windows platform is unsupported, please, unload the module.",
|
||||
)
|
||||
return
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(60)) as session:
|
||||
download_link = f"https://github.com/denoland/deno/releases/latest/download/deno-{target}.zip"
|
||||
async with session.get(download_link) as resp:
|
||||
if resp.status == HTTPStatus.OK:
|
||||
async with aiofiles.open("deno.zip", mode="wb") as f:
|
||||
async for chunk in resp.content.iter_chunked(8192):
|
||||
await f.write(chunk)
|
||||
else:
|
||||
logger.critical("Failed to download Deno: HTTP %s", resp.status)
|
||||
self.set("deno_source", "install_failed")
|
||||
return
|
||||
if os.path.exists('deno.zip'):
|
||||
with zipfile.ZipFile("deno.zip", "r") as zip_ref:
|
||||
zip_ref.extractall()
|
||||
os.remove('deno.zip')
|
||||
os.chmod(path=os.path.join(os.getcwd(), "deno"), mode=0o755)
|
||||
self.set("deno_source", os.path.join(os.getcwd(), "deno"))
|
||||
return
|
||||
|
||||
@loader.command(en_doc="Download media", ru_doc="Скачать медиа")
|
||||
async def unidlcmd(self, message) -> None: # noqa: ANN001, D102
|
||||
args = utils.get_args(message)
|
||||
if not args or not self._validate_url(args[0]) or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
|
||||
async def _download_media(call, download_type: str) -> None:
|
||||
|
||||
if not (source := self._get_deno()):
|
||||
await call.edit(self.strings["deno_err"])
|
||||
return
|
||||
|
||||
await call.answer()
|
||||
await call.delete()
|
||||
|
||||
downloading_msg = await self._client.send_message(message.chat_id, self.strings["downloading"], reply_to=message.reply_to_msg_id) # noqa: E501
|
||||
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"js_runtimes": {"deno": {"path": source}},
|
||||
}
|
||||
|
||||
if cookie := self.get("youtube_cookie"):
|
||||
ydl_opts["cookiefile"] = cookie
|
||||
|
||||
if download_type == "audio":
|
||||
ydl_opts["outtmpl"] = f"audio_{message.id}.%(ext)s"
|
||||
ydl_opts["format"] = "bestaudio/best"
|
||||
ydl_opts["postprocessors"] = [
|
||||
{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": "0",
|
||||
},
|
||||
{
|
||||
"key": "FFmpegMetadata",
|
||||
"add_metadata": True,
|
||||
},
|
||||
{
|
||||
"key": "EmbedThumbnail",
|
||||
},
|
||||
]
|
||||
ydl_opts["writethumbnail"] = True
|
||||
|
||||
if download_type == "video":
|
||||
ydl_opts["outtmpl"] = f"video_{message.id}.%(ext)s"
|
||||
ydl_opts["format"] = "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" # noqa: E501
|
||||
ydl_opts["merge_output_format"] = "mp4"
|
||||
|
||||
try:
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = await utils.run_sync(lambda: ydl.extract_info(args[0], download=True)) # noqa: E501
|
||||
filename = ydl.prepare_filename(info).split(".")[0] + (".mp3" if download_type == "audio" else ".mp4") # noqa: E501
|
||||
await self._client.send_file(message.chat_id, filename, reply_to=message.reply_to_msg_id) # noqa: E501
|
||||
await downloading_msg.delete()
|
||||
except Exception as e:
|
||||
logger.exception("Catched error during download!")
|
||||
await call.answer()
|
||||
await downloading_msg.edit(self.strings["err"].format(e))
|
||||
finally:
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
|
||||
|
||||
call = await self.inline.form("🪐", message)
|
||||
await message.delete()
|
||||
await call.edit(self.strings["select_download_type"], reply_markup=[[{"text": self.strings["video"], "callback": _download_media, "args": ("video",)}, {"text": self.strings["audio"], "callback": _download_media, "args": ("audio",)}]]) # noqa: E501
|
||||
@@ -59,7 +59,7 @@ class FaceMod(loader.Module):
|
||||
en_doc="Random kaomoji",
|
||||
)
|
||||
async def rfacecmd(self, message) -> None: # noqa: D102, ANN001
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
await utils.answer(message, self.strings["loading"])
|
||||
|
||||
url = "https://files.archquise.ru/kaomoji.txt"
|
||||
|
||||
@@ -72,7 +72,7 @@ class FaceMod(loader.Module):
|
||||
kaomoji = random.choice(kaomoji_list) # noqa: S311
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("random_face").format(kaomoji),
|
||||
self.strings["random_face"].format(kaomoji),
|
||||
)
|
||||
else:
|
||||
await utils.answer(message, self.strings("error"))
|
||||
await utils.answer(message, self.strings["error"])
|
||||
|
||||
@@ -113,24 +113,24 @@ class ShortenerMod(loader.Module):
|
||||
async def shortencmd(self, message): # noqa: ANN001, ANN201
|
||||
"""Shorten URL using bit.ly API."""
|
||||
if self.config["token"] is None:
|
||||
await utils.answer(message, self.strings("no_api"))
|
||||
await utils.answer(message, self.strings["no_api"])
|
||||
return
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("no_args"))
|
||||
await utils.answer(message, self.strings["no_args"])
|
||||
return
|
||||
|
||||
if not self._validate_url(args):
|
||||
await utils.answer(message, self.strings("invalid_url"))
|
||||
await utils.answer(message, self.strings["invalid_url"])
|
||||
return
|
||||
|
||||
try:
|
||||
short_url = await self.shorten_url(url=args, token=self.config["token"])
|
||||
await utils.answer(message, self.strings("shortencmd").format(c=short_url))
|
||||
await utils.answer(message, self.strings["shortencmd"].format(c=short_url))
|
||||
except Exception as e:
|
||||
logger.exception("Error shortening URL!")
|
||||
await utils.answer(message, self.strings("api_error").format(error=str(e)))
|
||||
await utils.answer(message, self.strings["api_error"].format(error=str(e)))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Посмотреть статистику ссылки через bit.ly (ссылка без https:// | Доступно только на платных аккаунтах)",
|
||||
@@ -139,22 +139,22 @@ class ShortenerMod(loader.Module):
|
||||
async def statclcmd(self, message): # noqa: ANN001, ANN201
|
||||
"""Get click statistics for shortened URL."""
|
||||
if self.config["token"] is None:
|
||||
await utils.answer(message, self.strings("no_api"))
|
||||
await utils.answer(message, self.strings["no_api"])
|
||||
return
|
||||
|
||||
args = utils.get_args_raw(message)
|
||||
if not args:
|
||||
await utils.answer(message, self.strings("no_args"))
|
||||
await utils.answer(message, self.strings["no_args"])
|
||||
return
|
||||
|
||||
try:
|
||||
if not args.startswith("bit.ly/"):
|
||||
await utils.answer(message, self.strings("invalid_url"))
|
||||
await utils.answer(message, self.strings["invalid_url"])
|
||||
return
|
||||
clicks = await self.get_bitlink_stats(
|
||||
bitlink=args, token=self.config["token"]
|
||||
)
|
||||
await utils.answer(message, self.strings("statclcmd").format(c=clicks))
|
||||
await utils.answer(message, self.strings["statclcmd"].format(c=clicks))
|
||||
except Exception as e:
|
||||
logger.exception("Error getting statistics!")
|
||||
await utils.answer(message, self.strings("api_error").format(error=str(e)))
|
||||
await utils.answer(message, self.strings["api_error"].format(error=str(e)))
|
||||
|
||||
@@ -26,7 +26,6 @@ from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Banners:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -324,6 +323,16 @@ class YaMusicMod(loader.Module):
|
||||
"name": "YaMusic"
|
||||
}
|
||||
|
||||
duration_placeholder = {
|
||||
"start_duration": "<tg-emoji emoji-id=5262663495538742892>☀️</tg-emoji><tg-emoji emoji-id=5260381609479153468>☀️</tg-emoji>",
|
||||
"start_full_duration": "<tg-emoji emoji-id=5262663495538742892>☀️</tg-emoji><tg-emoji emoji-id=5260609582048254485>☀️</tg-emoji>",
|
||||
"closed_duration": "<tg-emoji emoji-id=5260467667738859177>☀️</tg-emoji>",
|
||||
"empty_mid": "<tg-emoji emoji-id=5260415715814448198>☀️</tg-emoji>",
|
||||
"empty_closed_duration_duration": "<tg-emoji emoji-id=5260239235608255208>☀️</tg-emoji>",
|
||||
"end_duration_full": "<tg-emoji emoji-id=5260467667738859177>☀️</tg-emoji>",
|
||||
"empty_closed_duration": "<tg-emoji emoji-id=5260239235608255208>☀️</tg-emoji>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
@@ -553,67 +562,19 @@ class YaMusicMod(loader.Module):
|
||||
return "0%"
|
||||
|
||||
percent = (progress / duration) * 100
|
||||
fill_logic = int(percent // 16.66)
|
||||
|
||||
s_less_10 = (
|
||||
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||
"<emoji document_id=6158923355173949539>⭐</emoji>"
|
||||
"<emoji document_id=6159012102083188132>⭐</emoji>"
|
||||
"<emoji document_id=6159012102083188132>⭐</emoji>"
|
||||
"<emoji document_id=6158753257289158944>⭐</emoji>"
|
||||
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||
)
|
||||
|
||||
s_10_to_20 = (
|
||||
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||
"<emoji document_id=6159095673556840262>⭐</emoji>"
|
||||
"<emoji document_id=6159012102083188132>⭐</emoji>"
|
||||
"<emoji document_id=6156933677214341691>⭐</emoji>"
|
||||
"<emoji document_id=6158753257289158944>⭐</emoji>"
|
||||
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||
)
|
||||
|
||||
s_30_to_40 = (
|
||||
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=6158923355173949539>⭐</emoji>"
|
||||
"<emoji document_id=6159012102083188132>⭐</emoji>"
|
||||
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||
)
|
||||
|
||||
s_over_50 = (
|
||||
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=6156933677214341691>⭐</emoji>"
|
||||
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||
)
|
||||
|
||||
s_over_80 = (
|
||||
"<emoji document_id=5454137780454067986>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=5454397458471750662>➖</emoji>"
|
||||
"<emoji document_id=6156700344526049665>⭐</emoji>"
|
||||
)
|
||||
|
||||
if percent < 10:
|
||||
return s_less_10
|
||||
elif percent < 20:
|
||||
return s_10_to_20
|
||||
elif percent < 30:
|
||||
return s_10_to_20
|
||||
elif percent < 40:
|
||||
return s_30_to_40
|
||||
elif percent < 50:
|
||||
return s_30_to_40
|
||||
elif percent < 80:
|
||||
return s_over_50
|
||||
bar = self.duration_placeholder["start_full_duration"] if fill_logic >= 1 else self.duration_placeholder["start_duration"]
|
||||
for i in range(2, 6):
|
||||
if fill_logic >= i:
|
||||
bar += self.duration_placeholder["closed_duration"]
|
||||
else:
|
||||
bar += self.duration_placeholder["empty_mid"]
|
||||
if fill_logic >= 6:
|
||||
bar += self.duration_placeholder["end_duration_full"]
|
||||
else:
|
||||
return s_over_80
|
||||
|
||||
bar += self.duration_placeholder["empty_closed_duration"]
|
||||
return bar
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user