Compare commits

...

41 Commits

Author SHA1 Message Date
6b6afb7493 fix: Ported fix from stable, in python version below 3.12 you can't use " with f-strings 2026-06-08 09:46:37 +03:00
Macsim
2ed246b9ad Merge pull request #301 from MuRuLOSE/update-submodules_d279789b37a939b3d9ececce6b4d0e1992293c23
Update of repositories 2026-05-31 02:48:09
2026-06-01 00:23:40 +03:00
github-actions[bot]
837784206f Updated modules.json after parse 2026-05-31 02:47:46 2026-05-31 02:47:46 +00:00
github-actions[bot]
811beb2b74 Added and updated repositories 2026-05-31 02:47:15 2026-05-31 02:47:16 +00:00
Zahar Vanilovv
d279789b37 Merge pull request #273 from MuRuLOSE/update-submodules_63944822139e8b6869fe2250ad8c650e9db06765
Update of repositories 2026-05-03 02:11:45
2026-05-03 05:14:12 +03:00
github-actions[bot]
74dfe4caf8 Updated modules.json after parse 2026-05-03 02:11:26 2026-05-03 02:11:26 +00:00
github-actions[bot]
18b8247e21 Added and updated repositories 2026-05-03 02:10:53 2026-05-03 02:10:53 +00:00
6394482213 fix: banners now will be visible no matter what 2026-04-25 09:23:54 +03:00
43a0e578c1 fix: 1
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 21:58:27 +03:00
3112e6d4bf python-magic is shit so its replaced with filetype due to magic requires its own binary in order to work 2026-04-24 21:43:29 +03:00
18bb817239 fix: if server does not return Content-Type in header we will try determine MIME-type offline
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 21:37:39 +03:00
24f1983e2a fix: not awaited update function 2026-04-24 21:16:36 +03:00
74638c14d0 feat: now you can see version if module updated
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 20:56:58 +03:00
Macsim
db62a5aee1 Merge pull request #264 from MuRuLOSE/update-submodules_49956ba865b533dca03b344d048f96465a6be62e
Update of repositories 2026-04-24 17:42:48
2026-04-24 20:43:54 +03:00
github-actions[bot]
4d2a2899f4 Updated modules.json after parse 2026-04-24 17:42:27 2026-04-24 17:42:27 +00:00
github-actions[bot]
2a27c56d83 Added and updated repositories 2026-04-24 17:41:57 2026-04-24 17:41:57 +00:00
49956ba865 version bump 2026-04-24 20:33:45 +03:00
f0d2a28105 removed H.modules
feat: added force index update

Co-authored-by: Copilot <copilot@github.com>
2026-04-24 20:10:52 +03:00
13d091c56c removed H.Modules by request 2026-04-19 16:10:58 +03:00
d7cf406b78 feat: Added 'by developer' if meta developer in module and event if module new
fix: if some event happened with module, changes message will not be sended
2026-04-19 14:40:13 +03:00
Macsim
0f30a78990 Merge pull request #257 from MuRuLOSE/update-submodules_e50c7c1688b89901690bc01609544e5c3e238097
Update of repositories 2026-04-19 02:02:27
2026-04-19 14:16:50 +03:00
github-actions[bot]
16adfac8b5 Updated modules.json after parse 2026-04-19 02:02:03 2026-04-19 02:02:03 +00:00
github-actions[bot]
7ddb190b35 Added and updated repositories 2026-04-19 02:01:35 2026-04-19 02:01:35 +00:00
e50c7c1688 drop: debug log 2026-04-18 14:08:04 +03:00
f3682ed87a fix: version displaying wrong and some shit idk 2026-04-18 13:41:49 +03:00
be47e59d97 fix: some broken strings
feat: install button and new limoka version update notification
drop: watcher that installs module  from bot (temporary)
2026-04-18 13:19:24 +03:00
eb71e39fcf Merge branch 'main' of https://github.com/MuRuLOSE/limoka 2026-04-18 11:01:48 +03:00
Macsim
7d713e36c0 Merge pull request #256 from MuRuLOSE/update-submodules_a424d6bac4fb0d52521fca595af24968162bcc87
Update of repositories 2026-04-18 01:50:07
2026-04-18 10:58:20 +03:00
github-actions[bot]
a8fde5e498 Updated modules.json after parse 2026-04-18 01:49:43 2026-04-18 01:49:43 +00:00
github-actions[bot]
4a03c6cb1a Added and updated repositories 2026-04-18 01:49:10 2026-04-18 01:49:10 +00:00
Macsim
a424d6bac4 Merge pull request #254 from MuRuLOSE/update-submodules_59564f07b5b57f12eec78ff6e8f0574c70e37f8b
Update of repositories 2026-04-16 02:02:29
2026-04-16 07:39:02 +03:00
github-actions[bot]
fc8344ca05 Updated modules.json after parse 2026-04-16 02:02:11 2026-04-16 02:02:11 +00:00
github-actions[bot]
3e62dc0b69 Added and updated repositories 2026-04-16 02:01:42 2026-04-16 02:01:42 +00:00
41f253b471 feat: deleted files now got dedicated notification too 2026-04-15 18:02:16 +03:00
Macsim
59564f07b5 Merge pull request #253 from MuRuLOSE/update-submodules_3a193cfb2f731c403bbc542d2408c6144d0bcda7
Update of repositories 2026-04-15 01:54:22
2026-04-15 17:35:17 +03:00
github-actions[bot]
dfe2ae1103 Updated modules.json after parse 2026-04-15 01:53:44 2026-04-15 01:53:44 +00:00
github-actions[bot]
4ca7279309 Added and updated repositories 2026-04-15 01:53:05 2026-04-15 01:53:05 +00:00
3a193cfb2f fix: blockquote not needed here 2026-04-14 18:29:41 +03:00
637f9d82ae fix: another try to fix notifications 2026-04-13 20:24:29 +03:00
316e623c64 Merge branch 'main' of https://github.com/MuRuLOSE/limoka 2026-04-13 19:30:23 +03:00
57044428fd fix: added some tags 2026-04-13 19:30:20 +03:00
118 changed files with 70324 additions and 95502 deletions

View File

@@ -210,7 +210,6 @@ jobs:
notify_diffs:
runs-on: ubuntu-latest
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true)
steps:
- name: Checkout repository

View File

@@ -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
}
]

View File

@@ -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}">&#8204;</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()

View 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()

View 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

View File

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

View File

@@ -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=[]
)
)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View 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 "".

View 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 "".

View 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 "".

View File

@@ -2,3 +2,4 @@ akinator
FHeta
BSR
SCD
LFSecurity

596
Limoka.py
View File

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

View File

@@ -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,
},
{

View File

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

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

View File

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

View File

@@ -0,0 +1,5 @@
ChatCopy.py
Gemini.py
GiftFinder.py
MaillingChatGT99.py
NekoEditorMod.py

Binary file not shown.

View File

@@ -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: .

View File

@@ -1,7 +0,0 @@
full.py
autocleaner.py
silent.py
# Ruff Format
.ruff_cache/
.idea

View File

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

View File

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

View File

@@ -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 &lt;3</b>:", # &lt; == <
}
strings_ru = {
"announce": "<b>Анонс</b>:",
"ongoing": "<b>Онгоинг</b>:",
"type": "<b>Тип</b>:",
"genres": "<b>Жанры</b>:",
"favorite": "<b>Избранное &lt;3</b>:", # &lt; == <
}
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,
)

View File

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

View File

@@ -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 {}

View File

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

View File

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

View File

@@ -1 +0,0 @@
mods.archquise.ru

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"],
}

View File

@@ -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",
},
],
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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&center=true&vCenter=true&width=500&height=80&color=00D9FF&center=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>

View File

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

View File

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

View File

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

View File

@@ -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()]
),
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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сс:8м16сс",
"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)))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (35)",
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)

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More