Compare commits

..

25 Commits

Author SHA1 Message Date
github-actions[bot]
b42232dd0b Updated modules.json after parse 2026-04-24 02:01:18 2026-04-24 02:01:18 +00:00
github-actions[bot]
2cff934bf7 Added and updated repositories 2026-04-24 02:00:45 2026-04-24 02:00:45 +00: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
32 changed files with 2089 additions and 2554 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

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
@@ -302,7 +302,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",
@@ -333,11 +333,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": "Описание",
@@ -365,11 +366,12 @@ class FHeta(loader.Module):
"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": "Опис",
@@ -397,11 +399,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": "Сипаттама",
@@ -429,11 +432,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,11 +465,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 mavzu.",
"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",
@@ -493,11 +498,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",
@@ -525,11 +531,12 @@ class FHeta(loader.Module):
"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."
"doctheme": "Thema für Emojis.",
"channel": "Dies ist der Kanal mit allen Updates in FHeta!"
}
strings_jp = {
"_cls_doc": "モジュール検索用モジュール!@FHeta_UpdatesでFHetaのすべてのニュースをフォローしてください",
"_cls_doc": "モジュール検索用モジュール!@NFHeta_UpdatesでFHetaのすべてのニュースをフォローしてください",
"lang": "jp",
"author": "作成者",
"description": "説明",
@@ -557,7 +564,8 @@ class FHeta(loader.Module):
"overwrite": "✘ エラー、モジュールが組み込みモジュールを上書きしようとしました!",
"dependency": "✘ 依存関係のインストールエラー! {deps}",
"docdevs": "検索時に公式Heroku開発者のモジュールのみを使用しますか",
"doctheme": "絵文字のテーマ。"
"doctheme": "絵文字のテーマ。",
"channel": "これはFHetaのすべての更新を含むチャンネルです"
}
THEMES = {
@@ -569,7 +577,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": {
@@ -580,7 +588,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": {
@@ -591,7 +599,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": {
@@ -602,7 +610,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": {
@@ -613,7 +621,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>'
}
}
@@ -652,6 +660,11 @@ class FHeta(loader.Module):
self.installer = MInstaller()
self.ui = FHetaUI(self)
await self.request_join(
"NFHeta_Updates",
f"{self.ui.emoji('channel')} {self.strings('channel')}"
)
self.api.token = self.token
router = None
@@ -704,13 +717,20 @@ class FHeta(loader.Module):
self.api.token = self.token
except Exception:
pass
@loader.loop(interval=1, autostart=True)
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:
try:
cl = self.strings["lang"]
if cl != ll:
await self.api.send("dataset", user_id=self.identifier, lang=cl)
ll = cl
except Exception:
pass
await asyncio.sleep(1)
async def answer(self, callback: Union[CallbackQuery, ChosenInlineResult], text: Optional[str] = None, alert: bool = False) -> None:
try:
@@ -860,7 +880,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.bot_username} ')}</code></b>",
"thumb": "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FHeta/magnifying_glass.png"
}
@@ -878,7 +898,7 @@ 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"
}
@@ -929,18 +949,18 @@ 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)

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,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/SCD/banner.png
# meta developer: @FModules
# meta developer: @NFModules
# requires: curl_cffi

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

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

472
Limoka.py
View File

@@ -25,15 +25,37 @@ from telethon import TelegramClient
from telethon.errors.rpcerrorlist import YouBlockedUserError
from telethon import functions
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
import ast
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, 1)
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 +258,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 +284,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 +384,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 +412,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 +445,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 +482,15 @@ 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": "No updates available. You are using the latest version of Limoka.",
"module_update_available": "Notification about module update has been sent, check @{bot}.",
}
strings_ru = {
"name": "Limoka",
@@ -461,23 +501,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 +531,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 +568,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 +605,15 @@ class Limoka(loader.Module):
"Если ошибка сохраняется снова, сообщите разработчикам</blockquote>"
),
"body_page": "Команды",
"install_failed": "Установка не удалась. Проверьте логи для деталей.",
"install_succeeded": "Модуль успешно установлен!",
"update_available": (
"🔔 <b>Доступно новое обновление!</b>\n\n"
"Новая версия Limoka {version} уже доступна. Пожалуйста, обновитесь для лучшей производительности, исправления багов и новых функций.\n"
"Нажмите кнопку ниже, чтобы обновить модуль."
),
"no_updates_available": "Нет доступных обновлений. У вас установлена последняя версия Limoka.",
"module_update_available": "Уведомление об обновлении модуля было отправлено, проверьте @{bot}.",
"_cls_doc": "Модули теперь в одном месте с простым и удобным поиском!",
}
@@ -590,11 +638,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 +668,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 +767,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
@@ -805,13 +904,15 @@ 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:
@@ -951,6 +1052,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 +1134,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 +1181,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"]
@@ -1589,6 +1724,15 @@ class Limoka(loader.Module):
message, module_info, module_path, display_session, 0
)
@loader.command(ru_doc="Проверить наличие обновлений модуля")
async def limokaupdatecmd(self, message: Message):
"""Check for module 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 +1886,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

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

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

@@ -633,7 +633,7 @@ class SoundCloudMod(loader.Module):
"oauth_token",
"",
"SoundCloud OAuth token",
validator=loader.validators.String(),
validator=loader.validators.Hidden(),
),
loader.ConfigValue(
"history_count",

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

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

@@ -0,0 +1,204 @@
# ______ ___ ___ _ _
# ____ | ___ \ | \/ | | | | |
# / __ \| |_/ / _| . . | ___ __| |_ _| | ___
# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \
# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/
# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___|
# \____/ __/ |
# |___/
# На модуль распространяется лицензия "GNU General Public License v3.0"
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule
import asyncio
import os
import toml
from .. import loader, utils
from herokutl.tl.types import Message
@loader.tds
class IwaAnimation(loader.Module):
"""Frame-by-frame text animations loaded from .anim TOML files"""
strings = {
"name": "IwaAnimation",
"err_no_reply": "<b>{e} Reply to a .anim file.</b>",
"err_not_anim": "<b>{e} File must have .anim extension.</b>",
"err_bad_format": "<b>{e} Invalid file format (missing name or cmd).</b>",
"err_no_frames": "<b>{e} No frames found in the file.</b>",
"err_not_found": "<b>{e} Animation not found.</b>",
"err_no_cmd": "<b>{e} Specify a command name.</b>",
"err_generic": "<b>{e} Error:</b>\n\n{exc}",
"ok_loaded": "<b>{s} Loaded: {name}\nCommand: <code>.anim {cmd}</code></b>",
"ok_deleted": "<b>{s} Deleted.</b>",
"list_header": "<blockquote><b>Animations:</b></blockquote>\n\n<blockquote expandable><b>",
"list_row": "• <code>{cmd}</code> — {name} ({n} frames)\n",
"list_footer": "</b></blockquote>",
"list_empty": "<b>{e} No animations.</b>",
}
strings_ru = {
"name": "IwaAnimation",
"err_no_reply": "<b>{e} Ответьте на .anim файл.</b>",
"err_not_anim": "<b>{e} Файл должен быть формата .anim</b>",
"err_bad_format": "<b>{e} Неверный формат файла (нет name или cmd).</b>",
"err_no_frames": "<b>{e} В файле нет кадров.</b>",
"err_not_found": "<b>{e} Анимация не найдена.</b>",
"err_no_cmd": "<b>{e} Укажи команду.</b>",
"err_generic": "<b>{e} Ошибка:</b>\n\n{exc}",
"ok_loaded": "<b>{s} Загружено: {name}\nКоманда: <code>.anim {cmd}</code></b>",
"ok_deleted": "<b>{s} Удалено.</b>",
"list_header": "<blockquote><b>Анимации:</b></blockquote>\n\n<blockquote expandable><b>",
"list_row": "• <code>{cmd}</code> — {name} ({n} кадров)\n",
"list_footer": "</b></blockquote>",
"list_empty": "<b>{e} Нет анимаций.</b>",
}
_E = "<emoji document_id=5774077015388852135>❌</emoji>"
_S = "<emoji document_id=5774022692642492953>✅</emoji>"
async def client_ready(self):
if not self.db.get("IwaAnimations", "anims", False):
self.db.set("IwaAnimations", "anims", {})
@loader.command(ru_doc="- Загрузить анимацию из полученного .anim файла")
async def lanimcmd(self, message: Message):
"""- Load animation from a replied .anim file"""
reply = await message.get_reply_message()
if not reply or not reply.document:
return await utils.answer(
message, self.strings["err_no_reply"].format(e=self._E)
)
filename = reply.file.name or ""
if not filename.endswith(".anim"):
return await utils.answer(
message, self.strings["err_not_anim"].format(e=self._E)
)
tmp = "anim_load.anim"
await reply.download_media(tmp)
try:
data = toml.load(tmp)
name = data.get("name")
cmd = data.get("cmd")
delay = float(data.get("time", 0.5))
if not name or not cmd:
return await utils.answer(
message, self.strings["err_bad_format"].format(e=self._E)
)
frames = []
for key in sorted(
(k for k in data if str(k).isdigit()), key=lambda x: int(x)
):
frame = data[key]
frames.append("\n".join(frame) if isinstance(frame, list) else str(frame))
if not frames:
return await utils.answer(
message, self.strings["err_no_frames"].format(e=self._E)
)
anims = self.db.get("IwaAnimations", "anims", {})
anims[cmd] = {"name": name, "frames": frames, "delay": delay}
self.db.set("IwaAnimations", "anims", anims)
await utils.answer(
message,
self.strings["ok_loaded"].format(s=self._S, name=name, cmd=cmd),
)
except Exception as exc:
await utils.answer(
message, self.strings["err_generic"].format(e=self._E, exc=exc)
)
finally:
if os.path.exists(tmp):
os.remove(tmp)
@loader.command(ru_doc="<cmd> - Воспроизвести загруженную анимацию")
async def animcmd(self, message: Message):
"""<cmd> - Play a loaded animation"""
cmd = utils.get_args_raw(message)
if not cmd:
return await utils.answer(
message, self.strings["err_no_cmd"].format(e=self._E)
)
anims = self.db.get("IwaAnimations", "anims", {})
if cmd not in anims:
return await utils.answer(
message, self.strings["err_not_found"].format(e=self._E)
)
anim = anims[cmd]
msg = await utils.answer(message, anim["frames"][0])
try:
for frame in anim["frames"][1:]:
await asyncio.sleep(anim["delay"])
await msg.edit(frame)
except Exception:
pass
@loader.command(ru_doc="- Отобразить список всех загруженных анимаций")
async def animscmd(self, message: Message):
"""- List all loaded animations"""
anims = self.db.get("IwaAnimations", "anims", {})
if not anims:
return await utils.answer(
message, self.strings["list_empty"].format(e=self._E)
)
text = self.strings["list_header"]
for cmd, data in anims.items():
text += self.strings["list_row"].format(
cmd=cmd, name=data["name"], n=len(data["frames"])
)
text += self.strings["list_footer"]
await utils.answer(message, text)
@loader.command(ru_doc="<cmd> - Удалить анимацию")
async def delanimcmd(self, message: Message):
"""<cmd> - Delete an animation"""
cmd = utils.get_args_raw(message)
anims = self.db.get("IwaAnimations", "anims", {})
if cmd not in anims:
return await utils.answer(
message, self.strings["err_not_found"].format(e=self._E)
)
anims.pop(cmd)
self.db.set("IwaAnimations", "anims", anims)
await utils.answer(message, self.strings["ok_deleted"].format(s=self._S))
@loader.command(ru_doc="<cmd> - Экспорт анимации в файл .anim")
async def dumpanimcmd(self, message: Message):
"""<cmd> - Export an animation to a .anim file"""
cmd = utils.get_args_raw(message)
anims = self.db.get("IwaAnimations", "anims", {})
if cmd not in anims:
return await utils.answer(
message, self.strings["err_not_found"].format(e=self._E)
)
anim = anims[cmd]
data = {"name": anim["name"], "cmd": cmd, "time": str(anim["delay"])}
for i, frame in enumerate(anim["frames"], start=1):
data[str(i)] = frame.split("\n")
file = f"{cmd}.anim"
try:
with open(file, "w", encoding="utf-8") as f:
toml.dump(data, f)
await message.delete()
await self._client.send_file(message.to_id, file)
finally:
if os.path.exists(file):
os.remove(file)

View File

@@ -26,4 +26,5 @@ aigenuser
github
stream
placeholders+
PyInstall
PyInstall
IwaAnimation

View File

@@ -26,7 +26,7 @@ from typing import Optional, Dict, Any
from collections import OrderedDict
from .. import loader, utils, validators
from herokutl.tl.functions.users import GetFullUserRequest
from telethon.tl.functions.users import GetFullUserRequest
from herokutl.tl.functions.payments import GetStarsStatusRequest
logger = logging.getLogger(__name__)
@@ -118,21 +118,23 @@ class PlaceholdersMod(loader.Module):
)
self.cache = LRUCache(max_size=100, ttl=300)
async def client_ready(self):
async def client_ready(self, client, db):
self._client = client
self.session = aiohttp.ClientSession()
self.me = await self._client.get_me()
self.full_me = await self._client(GetFullUserRequest(self.me))
self.me = await client.get_me()
self.full_me = await client(GetFullUserRequest(self.me))
try:
stars_status = await self._client(GetStarsStatusRequest(entity="me"))
stars_status = await self._client(GetStarsStatusRequest(entity='me'))
self.stars_balance = stars_status.balance
except Exception:
except:
self.stars_balance = 0
self.tz = timezone(timedelta(hours=self.config["timezone"]))
self.weekdays_ru = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]
# Регистрация плейсхолдеров
self._register_placeholders()
def _register_placeholders(self):
@@ -199,30 +201,18 @@ class PlaceholdersMod(loader.Module):
utils.register_placeholder(name, func, desc)
async def get_premium_check(self):
if not getattr(self.me, "premium", False):
if not self.me.premium:
return "Нет Premium"
# premium_until отсутствует в публичном MTProto API herokutl/Telethon —
# пробуем достать его, но не падаем если поля нет
until = None
try:
until = getattr(self.full_me.full_user, "premium_until", None)
# Иногда это datetime, иногда unix timestamp (int)
if isinstance(until, datetime):
until = until.timestamp()
except Exception:
until = None
if not until:
return "✅ Premium активен"
if until < time.time():
return "⚠️ Премиум истёк"
until = self.full_me.full_user.premium_until
if not until or until < time.time():
return "Премиум закончился"
end_date = datetime.fromtimestamp(until, tz=self.tz)
days_left = (end_date.date() - datetime.now(self.tz).date()).days
formatted = end_date.strftime("%d.%m.%Y")
return f"✅ до {formatted} (ещё {days_left} дн.)"
return f"{formatted} (Осталось {days_left} дней)"
async def get_username(self):
return f"@{self.me.username}" if self.me.username else "Нет"
@@ -245,8 +235,10 @@ class PlaceholdersMod(loader.Module):
async def get_dc_id(self):
return str(self.me.dc_id if hasattr(self.me, "dc_id") else "Неизвестно")
async def get_stars(self):
return f"{self.stars_balance:,}".replace(",", " ") if self.stars_balance else "0"
async def get_stars(self):
result = await self.client(GetStarsStatusRequest("me"))
stars = result.balance.amount if result and result.balance else 0
return f"{stars:,}".replace(",", " ") if stars else "0"
async def get_usd_to_rub(self):
cache_key = "usd_rub"
@@ -261,7 +253,7 @@ class PlaceholdersMod(loader.Module):
result = f"1 USD ≈ {rate:.2f} RUB"
self.cache.set(cache_key, result)
return result
except Exception:
except:
try:
async with self.session.get("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json") as resp:
data = await resp.json()
@@ -269,7 +261,7 @@ class PlaceholdersMod(loader.Module):
result = f"1 USD ≈ {rate:.2f} RUB"
self.cache.set(cache_key, result)
return result
except Exception:
except:
return "Курс USD недоступен"
async def get_rub_to_usd(self):
@@ -278,7 +270,7 @@ class PlaceholdersMod(loader.Module):
try:
rate = float(usd_rub.split("")[1].strip().split()[0])
return f"1 RUB ≈ {1/rate:.4f} USD"
except Exception:
except:
pass
return "Курс RUB недоступен"
@@ -301,7 +293,7 @@ class PlaceholdersMod(loader.Module):
result = f"1 TON ≈ {rate:.2f} RUB"
self.cache.set(cache_key, result)
return result
except Exception:
except:
return "Курс TON недоступен"
async def get_rub_to_ton(self):
@@ -310,7 +302,7 @@ class PlaceholdersMod(loader.Module):
try:
rate = float(ton_rub.split("")[1].strip().split()[0])
return f"1 RUB ≈ {1/rate:.6f} TON"
except Exception:
except:
pass
return "Курс недоступен"
@@ -327,7 +319,7 @@ class PlaceholdersMod(loader.Module):
result = f"1 BTC ≈ {rate:,.0f} RUB"
self.cache.set(cache_key, result)
return result
except Exception:
except:
return "Курс BTC недоступен"
async def get_eth_to_rub(self):
@@ -343,7 +335,7 @@ class PlaceholdersMod(loader.Module):
result = f"1 ETH ≈ {rate:,.0f} RUB"
self.cache.set(cache_key, result)
return result
except Exception:
except:
return "Курс ETH недоступен"
async def get_stars_to_rub(self):
@@ -373,7 +365,7 @@ class PlaceholdersMod(loader.Module):
sent_gb = net.bytes_sent // (1024**3)
recv_gb = net.bytes_recv // (1024**3)
return f"{sent_gb} GB │ ↓ {recv_gb} GB"
except Exception:
except:
return "↑ 0 GB │ ↓ 0 GB"
async def get_speedtest(self):
@@ -405,7 +397,7 @@ class PlaceholdersMod(loader.Module):
result = f"{speed_mbps:.1f} Mbps"
self.cache.set(cache_key, result)
return result
except Exception:
except:
continue
return "Тест скорости недоступен"
@@ -426,7 +418,7 @@ class PlaceholdersMod(loader.Module):
used_gb = usage.used // (1024**3)
total_gb = usage.total // (1024**3)
return f"{used_gb} GB / {total_gb} GB ({percent:.1f}%)"
except Exception:
except:
return "Диск недоступен"
async def get_local_ip(self):
@@ -436,7 +428,7 @@ class PlaceholdersMod(loader.Module):
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
except:
return "Неизвестно"
async def get_user_hostname(self):
@@ -507,7 +499,7 @@ class PlaceholdersMod(loader.Module):
}
self.cache.set(cache_key, weather_data)
return weather_data
except Exception:
except:
pass
default = {
@@ -595,7 +587,7 @@ class PlaceholdersMod(loader.Module):
result = f"🎵 {stats['playcount']} скробблов"
self.cache.set(cache_key, result)
return result
except Exception:
except:
pass
return "Статистика недоступна"
@@ -637,14 +629,10 @@ class PlaceholdersMod(loader.Module):
}
self.cache.set(cache_key, result)
return result
except Exception:
except:
pass
return None
async def on_unload(self):
utils.unregister_placeholders(self.__class__.__name__)
try:
await self.session.close()
except Exception:
pass
await self.session.close()

File diff suppressed because it is too large Load Diff

View File

@@ -145,11 +145,6 @@
"tags": ["hikkatrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/archquise/H.Modules",
"tags": ["hikkatrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/GD-alt/mm-hikka-mods",
"tags": ["hikkatrusted", "herokutrusted", "nonactive"],
@@ -242,12 +237,12 @@
},
{
"url": "https://github.com/Fixyres/FModules",
"tags": [],
"blacklist": ["FHeta.py"]
"tags": ["herokutrusted"],
"blacklist": ["FHeta.py", "LFSecurity.py"]
},
{
"url": "https://github.com/Midga3/Heroku-modules",
"tags": ["newbie"],
"tags": ["newbie", "herokutrusted"],
"blacklist": ["deletelinux.py", "bleabratanspapibobolshoyevyrychil.py"]
},
{

View File

@@ -6,6 +6,7 @@ import os
import tempfile
from pathlib import Path
import parse
parser = argparse.ArgumentParser(description="Update Diffs Script")
parser.add_argument(
@@ -71,6 +72,37 @@ def get_changed_files(base_commit):
except subprocess.CalledProcessError:
return []
def get_deleted_files(base_commit):
"""Get list of deleted files between commits"""
try:
result = subprocess.check_output(
['git', 'diff', '--diff-filter=D', '--name-only', base_commit, 'HEAD'],
cwd=os.getcwd()
).decode().strip().split('\n')
return [f for f in result if f]
except subprocess.CalledProcessError:
return []
def get_diff_files(base_commit, diff_filter):
"""Get list of files for a specific git diff filter"""
try:
result = subprocess.check_output(
['git', 'diff', f'--diff-filter={diff_filter}', '--name-only', base_commit, 'HEAD'],
cwd=os.getcwd()
).decode().strip().splitlines()
return [f for f in result if f]
except subprocess.CalledProcessError:
return []
def get_added_files(base_commit):
return get_diff_files(base_commit, 'A')
def get_modified_files(base_commit):
return get_diff_files(base_commit, 'M')
def get_file_diff(file_path, base_commit):
"""Get diff for a specific file"""
try:
@@ -82,6 +114,24 @@ def get_file_diff(file_path, base_commit):
except subprocess.CalledProcessError:
return ""
def get_module_developer(file_path):
"""Read module metadata and return the developer handle"""
try:
module_info = parse.get_module_info(file_path)
except Exception:
return None
if not module_info:
return None
developer = module_info.get('meta', {}).get('developer')
if developer:
return developer.strip()
return None
def is_module_file(file_path):
"""Check if file is a Python module in a modules directory"""
# Check if it's a .py file and in a modules-like directory
@@ -95,25 +145,42 @@ def extract_module_name(file_path):
return Path(file_path).stem
async def main():
changed_files = get_changed_files(arguments.base_commit)
added_files = get_added_files(arguments.base_commit)
modified_files = get_modified_files(arguments.base_commit)
deleted_files = get_deleted_files(arguments.base_commit)
if not changed_files:
all_files = added_files + modified_files + deleted_files
if not all_files:
print("No changes detected")
return
# Filter for module files only
module_files = [f for f in changed_files if is_module_file(f)]
new_module_files = [f for f in added_files if is_module_file(f)]
modified_module_files = [f for f in modified_files if is_module_file(f)]
deleted_module_files = [f for f in deleted_files if is_module_file(f)]
if not module_files:
if not new_module_files and not modified_module_files and not deleted_module_files:
print("No module changes detected")
return
async with aiohttp.ClientSession() as session:
for file_path in module_files:
# Handle deleted files first
for file_path in deleted_module_files:
try:
module_name = extract_module_name(file_path)
# Create message with raw GitHub URL
message = f"🪼 <b>Module <code>{module_name}</code> has been deleted</b>"
result = await send_message(session, message)
print(f"Sent deletion notice for {module_name}: {result}")
except Exception as e:
print(f"Error processing deleted {file_path}: {e}")
# Handle newly added modules
for file_path in new_module_files:
try:
module_name = extract_module_name(file_path)
developer = get_module_developer(file_path)
github_url = f"https://raw.githubusercontent.com/MuRuLOSE/limoka/refs/heads/main/{file_path}"
try:
new_hash = subprocess.check_output(
@@ -132,21 +199,21 @@ async def main():
old_hash = arguments.base_commit
diff_url = f"https://github.com/MuRuLOSE/limoka/compare/{old_hash}...{new_hash}.diff"
title = f"🪼 <b>New module <code>{module_name}</code> approved</b>"
if developer:
title += f"\n<code>{developer}</code>"
message = (
f"🪼 <b>Module <code>{module_name}</code> changes approved</b>\n\n"
# f"🪼 <b>Module <code>{module_name}</code> by <code>ueban123</code> changes approved</b>\n\n"
f"{title}\n\n"
f"<b><a href=\"{github_url}\">File URL</a></b> | "
f"<b><a href=\"{diff_url}\">Diff URL</a></b>"
)
# Get diff
diff = get_file_diff(file_path, arguments.base_commit)
if not diff:
print(f"Skipping {file_path} - no diff content")
continue
# Create temporary file with diff using only module name
diff_filename = f"{module_name}.diff"
with tempfile.NamedTemporaryFile(
mode='w',
@@ -158,24 +225,15 @@ async def main():
) as tmp_file:
tmp_file.write(diff)
tmp_file_path = tmp_file.name
try:
# Rename temp file to have proper name
final_path = os.path.join(tempfile.gettempdir(), diff_filename)
os.rename(tmp_file_path, final_path)
# Send diff as document with full message as caption
doc_result = await send_document(
session,
final_path,
caption=message
)
print(f"Sent diff for {module_name}: {doc_result}")
doc_result = await send_document(session, final_path, caption=message)
print(f"Sent new module diff for {module_name}: {doc_result}")
except Exception as e:
print(f"Error sending {module_name}: {e}")
finally:
# Cleanup temp files
if os.path.exists(tmp_file_path):
try:
os.remove(tmp_file_path)
@@ -187,9 +245,81 @@ async def main():
os.remove(final_path)
except:
pass
except Exception as e:
print(f"Error processing {file_path}: {e}")
print(f"Error processing new file {file_path}: {e}")
# Handle modified files
for file_path in modified_module_files:
try:
module_name = extract_module_name(file_path)
developer = get_module_developer(file_path)
github_url = f"https://raw.githubusercontent.com/MuRuLOSE/limoka/refs/heads/main/{file_path}"
try:
new_hash = subprocess.check_output(
['git', 'rev-list', '-n', '1', 'HEAD', '--', file_path],
cwd=os.getcwd()
).decode().strip()
except Exception:
new_hash = 'HEAD'
try:
old_hash = subprocess.check_output(
['git', 'rev-list', '-n', '1', arguments.base_commit, '--', file_path],
cwd=os.getcwd()
).decode().strip()
except Exception:
old_hash = arguments.base_commit
diff_url = f"https://github.com/MuRuLOSE/limoka/compare/{old_hash}...{new_hash}.diff"
title = f"🪼 <b>Module <code>{module_name}</code> changes approved</b>"
if developer:
title += f"\nby <code>{developer}</code>"
message = (
f"{title}\n\n"
f"<b><a href=\"{github_url}\">File URL</a></b> | "
f"<b><a href=\"{diff_url}\">Diff URL</a></b>"
)
diff = get_file_diff(file_path, arguments.base_commit)
if not diff:
print(f"Skipping {file_path} - no diff content")
continue
diff_filename = f"{module_name}.diff"
with tempfile.NamedTemporaryFile(
mode='w',
suffix='',
prefix='',
delete=False,
encoding='utf-8',
dir=tempfile.gettempdir()
) as tmp_file:
tmp_file.write(diff)
tmp_file_path = tmp_file.name
try:
final_path = os.path.join(tempfile.gettempdir(), diff_filename)
os.rename(tmp_file_path, final_path)
doc_result = await send_document(session, final_path, caption=message)
print(f"Sent diff for {module_name}: {doc_result}")
except Exception as e:
print(f"Error sending {module_name}: {e}")
finally:
if os.path.exists(tmp_file_path):
try:
os.remove(tmp_file_path)
except:
pass
final_path = os.path.join(tempfile.gettempdir(), diff_filename)
if os.path.exists(final_path):
try:
os.remove(final_path)
except:
pass
except Exception as e:
print(f"Error processing modified {file_path}: {e}")
if __name__ == "__main__":
asyncio.run(main())