mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-17 23:04:17 +02:00
Compare commits
19 Commits
update-sub
...
update-sub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42232dd0b | ||
|
|
2cff934bf7 | ||
| 13d091c56c | |||
| d7cf406b78 | |||
|
|
0f30a78990 | ||
|
|
16adfac8b5 | ||
|
|
7ddb190b35 | ||
| e50c7c1688 | |||
| f3682ed87a | |||
| be47e59d97 | |||
| eb71e39fcf | |||
|
|
7d713e36c0 | ||
|
|
a8fde5e498 | ||
|
|
4a03c6cb1a | ||
|
|
a424d6bac4 | ||
|
|
fc8344ca05 | ||
|
|
3e62dc0b69 | ||
| 41f253b471 | |||
|
|
59564f07b5 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
628
Fixyres/FModules/FSecurity.py
Normal file
628
Fixyres/FModules/FSecurity.py
Normal file
@@ -0,0 +1,628 @@
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
# meta developer: @NFModules
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import html
|
||||
import sys
|
||||
import uuid
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from contextlib import suppress
|
||||
from .. import loader, utils
|
||||
|
||||
|
||||
@loader.tds
|
||||
class FSecurity(loader.Module):
|
||||
"""Module for automatic AI-based security checks of installed modules."""
|
||||
|
||||
strings = {
|
||||
"name": "FSecurity",
|
||||
"lang": "English",
|
||||
"unavailable": "AI module{} check is unavailable.",
|
||||
"suspicious": "AI interrupted installation of a suspicious module{}, reason:",
|
||||
"blocked": "AI blocked module installation{}, reason:",
|
||||
"continue": "Continue installation?",
|
||||
"strict_mode_doc": "Block loading modules by any method (lm/dlm allowed) if the AI API is unavailable or the module is suspicious. On restart, this also applies to already installed modules.",
|
||||
"nvidia_api_key_doc": "API key from build.nvidia.com, used for AI checks. If not specified, a public key from GitHub will be used."
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"lang": "Russian",
|
||||
"_cls_doc": "Модуль для автоматической проверки устанавливаемых модулей через ИИ.",
|
||||
"unavailable": "Проверка модуля{} через ИИ недоступна.",
|
||||
"suspicious": "ИИ прервал установку подозрительного модуля{}, причина:",
|
||||
"blocked": "ИИ заблокировал установку модуля{}, причина:",
|
||||
"continue": "Продолжить установку?",
|
||||
"strict_mode_doc": "Не позволять загружать модули любым методом (lm/dlm разрешено), если API ИИ недоступен или модуль подозрителен. При перезагрузке работает даже на уже установленные модули.",
|
||||
"nvidia_api_key_doc": "API ключ от build.nvidia.com, используется для проверки через ИИ. Если вы его не укажете, будет использоваться общий ключ с GitHub."
|
||||
}
|
||||
|
||||
strings_ua = {
|
||||
"lang": "Ukraine",
|
||||
"_cls_doc": "Модуль для автоматичної перевірки встановлюваних модулів через ШІ.",
|
||||
"unavailable": "Перевірка модуля{} через ШІ недоступна.",
|
||||
"suspicious": "ШІ перервав встановлення підозрілого модуля{}, причина:",
|
||||
"blocked": "ШІ заблокував встановлення модуля{}, причина:",
|
||||
"continue": "Продовжити встановлення?",
|
||||
"strict_mode_doc": "Не дозволяти завантажувати модулі будь-яким методом (lm/dlm дозволено), якщо API ШІ недоступний або модуль підозрілий. При перезавантаженні працює навіть на вже встановлені модулі.",
|
||||
"nvidia_api_key_doc": "API ключ від build.nvidia.com, використовується для перевірки через ШІ. Якщо ви його не вкажете, буде використовуватися загальний ключ з GitHub."
|
||||
}
|
||||
|
||||
strings_de = {
|
||||
"lang": "Germany",
|
||||
"_cls_doc": "Modul zur automatischen Prüfung installierter Module mit KI.",
|
||||
"unavailable": "Die KI-Modulprüfung{} ist nicht verfügbar.",
|
||||
"suspicious": "Die KI hat die Installation eines verdächtigen Moduls unterbrochen{}, Grund:",
|
||||
"blocked": "Die KI hat die Modulinstallation blockiert{}, Grund:",
|
||||
"continue": "Installation fortsetzen?",
|
||||
"strict_mode_doc": "Das Laden von Modulen mit jeder Methode blockieren (lm/dlm erlaubt), wenn die KI-API nicht verfügbar ist oder das Modul verdächtig ist. Beim Neustart gilt dies auch für bereits installierte Module.",
|
||||
"nvidia_api_key_doc": "API-Schlüssel von build.nvidia.com, der für KI-Prüfungen verwendet wird. Wenn nicht angegeben, wird ein öffentlicher Schlüssel von GitHub verwendet."
|
||||
}
|
||||
|
||||
strings_jp = {
|
||||
"lang": "Japanese",
|
||||
"_cls_doc": "AIでインストールされるモジュールを自動チェックするモジュール。",
|
||||
"unavailable": "AIモジュール{}のチェックが利用できません。",
|
||||
"suspicious": "AIが疑わしいモジュールのインストールを中断しました{}、理由:",
|
||||
"blocked": "AIがモジュールのインストールをブロックしました{}、理由:",
|
||||
"continue": "インストールを続行しますか?",
|
||||
"strict_mode_doc": "AI APIが利用できない場合や疑わしいモジュールの場合、すべての方法でモジュールの読み込みをブロックします(lm/dlmは許可)。再起動時にはインストール済みモジュールにも適用されます。",
|
||||
"nvidia_api_key_doc": "build.nvidia.com のAPIキー。AIチェックに使用されます。指定しない場合は、GitHubのパブリックキーが使用されます。"
|
||||
}
|
||||
|
||||
strings_tr = {
|
||||
"lang": "Turkish",
|
||||
"_cls_doc": "Kurulan modülleri yapay zeka ile otomatik kontrol eden modül.",
|
||||
"unavailable": "Yapay zeka modül{} kontrolü kullanılamıyor.",
|
||||
"suspicious": "Yapay zeka şüpheli bir modülün kurulumunu durdurdu{}, sebep:",
|
||||
"blocked": "Yapay zeka modül kurulumunu engelledi{}, sebep:",
|
||||
"continue": "Kuruluma devam edilsin mi?",
|
||||
"strict_mode_doc": "AI API kullanılamıyorsa veya modül şüpheliyse, tüm yöntemlerle modül yüklenmesini engelle (lm/dlm izinli). Yeniden başlatmada zaten kurulu modüller için de geçerlidir.",
|
||||
"nvidia_api_key_doc": "Yapay zeka kontrolleri için kullanılan build.nvidia.com API anahtarı. Belirtilmezse GitHub'daki genel anahtar kullanılacaktır."
|
||||
}
|
||||
|
||||
strings_uz = {
|
||||
"lang": "Uzbekistan",
|
||||
"_cls_doc": "O'rnatilayotgan modullarni AI orqali avtomatik tekshiruvchi modul.",
|
||||
"unavailable": "AI modul{} tekshiruvi mavjud emas.",
|
||||
"suspicious": "AI shubhali modul o'rnatilishini to'xtatdi{}, sabab:",
|
||||
"blocked": "AI modul o'rnatilishini blokladi{}, sabab:",
|
||||
"continue": "O'rnatishni davom ettirasizmi?",
|
||||
"strict_mode_doc": "AI API mavjud bo'lmasa yoki modul shubhali bo'lsa, barcha usullar bilan modul yuklashni bloklash (lm/dlm ruxsat etilgan). Qayta ishga tushirishda allaqachon o'rnatilgan modullarga ham ta'sir qiladi.",
|
||||
"nvidia_api_key_doc": "build.nvidia.com API kaliti, AI orqali tekshirish uchun ishlatiladi. Agar ko'rsatmasangiz, GitHub-dan umumiy kalit ishlatiladi."
|
||||
}
|
||||
|
||||
strings_kz = {
|
||||
"lang": "Kazakhstan",
|
||||
"_cls_doc": "Орнатылатын модульдерді ЖИ арқылы автоматты тексеретін модуль.",
|
||||
"unavailable": "AI модуль{} тексеру қолжетімсіз.",
|
||||
"suspicious": "AI күдікті модульді орнатуды тоқтатты{}, себебі:",
|
||||
"blocked": "AI модульді орнатуды бұғаттады{}, себебі:",
|
||||
"continue": "Орнатуды жалғастырасыз ба?",
|
||||
"strict_mode_doc": "AI API қолжетімсіз болса немесе модуль күдікті болса, барлық әдістермен модуль жүктеуді бұғаттау (lm/dlm рұқсат етілген). Қайта іске қосқанда орнатылған модульдерге де қолданылады.",
|
||||
"nvidia_api_key_doc": "build.nvidia.com API кілті, ЖИ арқылы тексеру үшін қолданылады. Егер оны көрсетпесеңіз, GitHub-тан ортақ кілт пайдаланылады."
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"strict_mode",
|
||||
False,
|
||||
lambda: self.strings("strict_mode_doc"),
|
||||
validator=loader.validators.Boolean(),
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"nvidia_api_key",
|
||||
"",
|
||||
lambda: self.strings("nvidia_api_key_doc"),
|
||||
validator=loader.validators.Hidden(),
|
||||
)
|
||||
)
|
||||
self.tasks = {}
|
||||
self.oreg = None
|
||||
self.oload = None
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.__origin__ = "<fsecurity>"
|
||||
self.core = self.lookup("loader")
|
||||
self.modules = self.core.allmodules
|
||||
self.restore_hooks()
|
||||
self.patch()
|
||||
|
||||
async def on_unload(self):
|
||||
self.unpatch()
|
||||
|
||||
def _render_prompt(self, prompt, **values):
|
||||
rendered = prompt
|
||||
for key, value in values.items():
|
||||
rendered = rendered.replace("{" + key + "}", str(value))
|
||||
return rendered
|
||||
|
||||
def _split_code(self, code):
|
||||
chunk_size = 180000
|
||||
if len(code) <= chunk_size:
|
||||
return [code]
|
||||
|
||||
chunks = []
|
||||
current =[]
|
||||
current_len = 0
|
||||
|
||||
for line in code.splitlines(keepends=True):
|
||||
if current and current_len + len(line) > chunk_size:
|
||||
chunks.append("".join(current))
|
||||
current =[]
|
||||
current_len = 0
|
||||
|
||||
if len(line) > chunk_size:
|
||||
if current:
|
||||
chunks.append("".join(current))
|
||||
current =[]
|
||||
current_len = 0
|
||||
for i in range(0, len(line), chunk_size):
|
||||
chunks.append(line[i:i + chunk_size])
|
||||
continue
|
||||
|
||||
current.append(line)
|
||||
current_len += len(line)
|
||||
|
||||
if current:
|
||||
chunks.append("".join(current))
|
||||
|
||||
return chunks or [code]
|
||||
|
||||
def _parse_ai_json(self, raw_text):
|
||||
raw_text = (raw_text or "").strip()
|
||||
if not raw_text:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw_text)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
match = re.search(r"\{[\s\S]*\}", raw_text)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = json.loads(match.group())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
|
||||
async def _fetch_prompt(self, session, url):
|
||||
async with session.get(url, timeout=10) as resp:
|
||||
if resp.status != 200:
|
||||
return None
|
||||
prompt = (await resp.text()).strip()
|
||||
return prompt or None
|
||||
|
||||
async def _get_prompts(self, session):
|
||||
main_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/main.txt")
|
||||
chunk_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/chank.txt")
|
||||
final_prompt = await self._fetch_prompt(session, "https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/prompts/final.txt")
|
||||
if not main_prompt or not chunk_prompt or not final_prompt:
|
||||
return None
|
||||
return {
|
||||
"main": main_prompt,
|
||||
"chunk": chunk_prompt,
|
||||
"final": final_prompt,
|
||||
}
|
||||
|
||||
async def _nvidia_request(self, session, api_key, system_prompt, user_prompt):
|
||||
async with session.post(
|
||||
"https://integrate.api.nvidia.com/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
json={
|
||||
"model": "qwen/qwen3-coder-480b-a35b-instruct",
|
||||
"messages":[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
"temperature": 0.4,
|
||||
"max_tokens": 1000,
|
||||
},
|
||||
timeout=180,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
return None
|
||||
data = await resp.json()
|
||||
choices = data.get("choices") or[]
|
||||
if not choices:
|
||||
return None
|
||||
return self._parse_ai_json(choices[0].get("message", {}).get("content", ""))
|
||||
|
||||
async def _local_ai_check(self, session, code, lang, api_key):
|
||||
prompts = await self._get_prompts(session)
|
||||
if not prompts:
|
||||
return None
|
||||
|
||||
chunks = self._split_code(code)
|
||||
if len(chunks) == 1:
|
||||
prompt = self._render_prompt(prompts["main"], lang=lang)
|
||||
return await self._nvidia_request(
|
||||
session,
|
||||
api_key,
|
||||
prompt,
|
||||
f"Analyze this module:\n\n```python\n{code}\n```",
|
||||
)
|
||||
|
||||
total = len(chunks)
|
||||
findings =[]
|
||||
|
||||
for index, chunk in enumerate(chunks, start=1):
|
||||
previous_context = "; ".join(
|
||||
f"Part {i}: {finding}"
|
||||
for i, finding in enumerate(findings, start=1)
|
||||
if finding
|
||||
) or "Previous parts: no issues found so far."
|
||||
|
||||
chunk_prompt = self._render_prompt(
|
||||
prompts["chunk"],
|
||||
total=total,
|
||||
current=index,
|
||||
previous_context=previous_context,
|
||||
lang=lang,
|
||||
)
|
||||
chunk_result = await self._nvidia_request(
|
||||
session,
|
||||
api_key,
|
||||
chunk_prompt,
|
||||
f"Part {index}/{total}:\n\n```python\n{chunk}\n```",
|
||||
)
|
||||
if not chunk_result:
|
||||
return None
|
||||
|
||||
chunk_verdict = str(chunk_result.get("chunk_verdict", "CLEAN")).lower()
|
||||
chunk_finding = str(chunk_result.get("findings", "") or "")
|
||||
|
||||
if chunk_verdict == "blocked":
|
||||
findings_text = "\n".join(
|
||||
f"- Part {i}: {finding}"
|
||||
for i, finding in enumerate(findings, start=1)
|
||||
if finding
|
||||
)
|
||||
if chunk_finding:
|
||||
findings_text = f"{findings_text}\n- Part {index}: {chunk_finding}".strip()
|
||||
|
||||
final_prompt = self._render_prompt(
|
||||
prompts["final"],
|
||||
total=total,
|
||||
findings=findings_text or "No prior findings.",
|
||||
lang=lang,
|
||||
)
|
||||
return await self._nvidia_request(
|
||||
session,
|
||||
api_key,
|
||||
final_prompt,
|
||||
"Give the final verdict based on all findings.",
|
||||
)
|
||||
|
||||
findings.append(chunk_finding if chunk_verdict != "clean" else "")
|
||||
|
||||
findings_text = "\n".join(
|
||||
f"- Part {i}: {finding}"
|
||||
for i, finding in enumerate(findings, start=1)
|
||||
if finding
|
||||
) or "All parts: no issues found."
|
||||
|
||||
final_prompt = self._render_prompt(
|
||||
prompts["final"],
|
||||
total=total,
|
||||
findings=findings_text,
|
||||
lang=lang,
|
||||
)
|
||||
return await self._nvidia_request(
|
||||
session,
|
||||
api_key,
|
||||
final_prompt,
|
||||
"Give the final verdict based on all findings.",
|
||||
)
|
||||
|
||||
async def check(self, code):
|
||||
try:
|
||||
lang = self.strings("lang") or "en"
|
||||
module_hash = hashlib.sha256(code.encode("utf-8")).hexdigest()
|
||||
|
||||
db_cache = self.get("cache", {})
|
||||
if module_hash in db_cache:
|
||||
cached = db_cache[module_hash]
|
||||
if cached.get("level") == "safe":
|
||||
return True
|
||||
return cached
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
api_keys = await self._get_api_keys(session)
|
||||
for api_key in api_keys:
|
||||
parsed = await self._local_ai_check(session, code, lang, api_key)
|
||||
if not isinstance(parsed, dict):
|
||||
continue
|
||||
|
||||
verdict = str(parsed.get("verdict", "BLOCKED")).lower()
|
||||
if verdict not in {"safe", "suspicious", "blocked"}:
|
||||
verdict = "blocked"
|
||||
summary = str(parsed.get("summary", "") or "")
|
||||
|
||||
result = {"level": verdict if verdict != "safe" else "safe"}
|
||||
if verdict != "safe":
|
||||
result["reason"] = summary
|
||||
|
||||
db_cache[module_hash] = result
|
||||
self.set("cache", db_cache)
|
||||
|
||||
if result["level"] == "safe":
|
||||
return True
|
||||
return result
|
||||
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _get_api_keys(self, session):
|
||||
configured_key = self.config["nvidia_api_key"].strip()
|
||||
if configured_key:
|
||||
return [configured_key]
|
||||
|
||||
try:
|
||||
async with session.get(
|
||||
"https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/api_keys.txt",
|
||||
timeout=10,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
return[]
|
||||
raw_keys = (await resp.text()).strip()
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
return[key.strip() for key in raw_keys.split(",") if key.strip()]
|
||||
|
||||
def format(self, state, reason="", link=""):
|
||||
link_part = f' (<code>{utils.escape_html(link)}</code>)' if link else ""
|
||||
if state == "unavailable":
|
||||
return f'<b>{self.strings("unavailable").format(link_part)}</b>\n<b>{self.strings("continue")}</b>'
|
||||
if state == "suspicious":
|
||||
return f'<b>{self.strings("suspicious").format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>\n<b>{self.strings("continue")}</b>'
|
||||
return f'<b>{self.strings("blocked").format(link_part)}</b>\n<blockquote expandable><b>{reason}</b></blockquote>'
|
||||
|
||||
def buttons(self, task):
|
||||
return [[
|
||||
{"text": "✓", "callback": self.confirm, "args": (task, "yes")},
|
||||
{"text": "✗", "callback": self.confirm, "args": (task, "no")}
|
||||
]]
|
||||
|
||||
def closure_var(self, func, name):
|
||||
raw = getattr(func, "__func__", func)
|
||||
code = getattr(raw, "__code__", None)
|
||||
closure = getattr(raw, "__closure__", None)
|
||||
if not code or not closure or name not in code.co_freevars:
|
||||
return None
|
||||
|
||||
with suppress(Exception):
|
||||
return closure[code.co_freevars.index(name)].cell_contents
|
||||
|
||||
return None
|
||||
|
||||
def restore_hooks(self):
|
||||
with suppress(Exception):
|
||||
inst_reg = getattr(self.modules, "register_module")
|
||||
owner = getattr(inst_reg, "__self__", None)
|
||||
if (
|
||||
owner
|
||||
and owner is not self
|
||||
and owner.__class__.__name__ == self.__class__.__name__
|
||||
):
|
||||
original = getattr(owner, "oreg", None)
|
||||
if original:
|
||||
if getattr(original, "__self__", None) is None:
|
||||
self.modules.register_module = original.__get__(
|
||||
self.modules,
|
||||
self.modules.__class__,
|
||||
)
|
||||
else:
|
||||
self.modules.register_module = original
|
||||
|
||||
with suppress(Exception):
|
||||
inst_load = getattr(self.core, "load_module")
|
||||
raw = getattr(inst_load, "__func__", inst_load)
|
||||
if "FSecurity.patch.<locals>.load" in getattr(raw, "__qualname__", ""):
|
||||
original = self.closure_var(raw, "original")
|
||||
if original:
|
||||
if getattr(original, "__self__", None) is None:
|
||||
self.core.load_module = original.__get__(
|
||||
self.core,
|
||||
self.core.__class__,
|
||||
)
|
||||
else:
|
||||
self.core.load_module = original
|
||||
|
||||
def patch(self):
|
||||
if not self.oreg:
|
||||
self.oreg = getattr(self.modules, "register_module")
|
||||
if not self.oload:
|
||||
self.oload = self.core.load_module
|
||||
|
||||
original = self.oload
|
||||
|
||||
async def load(_, *args, **kwargs):
|
||||
base = utils.answer
|
||||
|
||||
async def answer(message, response, *a, **k):
|
||||
if isinstance(response, str) and "😖</tg-emoji>" in response:
|
||||
body = response.split("😖</tg-emoji>", 1)[1].strip()
|
||||
if body in {"", "<b></b>", "<b> </b>"}:
|
||||
with suppress(Exception):
|
||||
if hasattr(message, "delete"):
|
||||
await message.delete()
|
||||
return message
|
||||
|
||||
if body.startswith("<b>") and body.endswith("</b>"):
|
||||
decoded = html.unescape(body[3:-4])
|
||||
response = response.split("😖</tg-emoji>", 1)[0] + f'😖</tg-emoji> {decoded}' if decoded else response.split("😖</tg-emoji>", 1)[0] + '😖</tg-emoji>'
|
||||
|
||||
try:
|
||||
return await base(message, response, *a, **k)
|
||||
except Exception:
|
||||
with suppress(Exception):
|
||||
return await self._client.send_message(
|
||||
utils.get_chat_id(message),
|
||||
response,
|
||||
reply_to=getattr(message, "reply_to_msg_id", None),
|
||||
buttons=k.get("reply_markup"),
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
utils.answer = answer
|
||||
try:
|
||||
if getattr(original, "__self__", None) is None:
|
||||
return await original(_, *args, **kwargs)
|
||||
return await original(*args, **kwargs)
|
||||
finally:
|
||||
if utils.answer is answer:
|
||||
utils.answer = base
|
||||
|
||||
self.core.load_module = load.__get__(self.core, self.core.__class__)
|
||||
self.modules.register_module = self.register
|
||||
|
||||
def unpatch(self):
|
||||
if self.oreg:
|
||||
self.modules.register_module = self.oreg
|
||||
if getattr(self, "core", None) and self.oload:
|
||||
self.core.load_module = self.oload
|
||||
|
||||
def context(self):
|
||||
frame = sys._getframe()
|
||||
msg = None
|
||||
fmsg = None
|
||||
is_dlm_lm = False
|
||||
|
||||
while frame:
|
||||
locals = frame.f_locals
|
||||
if (
|
||||
frame.f_code.co_name == "load_module"
|
||||
and locals.get("self") is self.core
|
||||
and 'message' in locals
|
||||
and hasattr(locals['message'], 'edit')
|
||||
):
|
||||
if not msg:
|
||||
msg = locals['message']
|
||||
fmsg = locals.get('msg')
|
||||
|
||||
if frame.f_code.co_name in {"dlmod", "loadmod"}:
|
||||
is_dlm_lm = True
|
||||
if not msg and 'message' in locals and hasattr(locals['message'], 'edit'):
|
||||
msg = locals['message']
|
||||
|
||||
if frame.f_code.co_name == "download_and_install":
|
||||
if not msg and 'message' in locals and hasattr(locals['message'], 'edit'):
|
||||
msg = locals['message']
|
||||
|
||||
frame = frame.f_back
|
||||
|
||||
return msg, fmsg, is_dlm_lm
|
||||
|
||||
def target_chat(self, msg=None, fmsg=None):
|
||||
if not msg:
|
||||
return None
|
||||
|
||||
if not fmsg:
|
||||
return msg
|
||||
|
||||
with suppress(Exception):
|
||||
target = copy.copy(msg)
|
||||
target.reply_to_msg_id = fmsg.id
|
||||
return target
|
||||
|
||||
return None
|
||||
|
||||
async def call_oreg(self, spec, name, origin="<core>", save_fs=False):
|
||||
if getattr(self.oreg, "__self__", None) is None:
|
||||
return await self.oreg(self.modules, spec, name, origin, save_fs=save_fs)
|
||||
return await self.oreg(spec, name, origin, save_fs=save_fs)
|
||||
|
||||
async def register(self, spec, name, origin="<core>", save_fs=False):
|
||||
if origin != "<core>":
|
||||
code = ""
|
||||
|
||||
if hasattr(spec.loader, "data") and spec.loader.data:
|
||||
code = spec.loader.data
|
||||
if isinstance(code, bytes):
|
||||
code = code.decode("utf-8", errors="ignore")
|
||||
elif origin and origin.endswith(".py"):
|
||||
with suppress(Exception):
|
||||
with open(origin, "r", encoding="utf-8") as f:
|
||||
code = f.read()
|
||||
|
||||
if code:
|
||||
check = await self.check(code)
|
||||
|
||||
if check is not True:
|
||||
msg, fmsg, is_dlm_lm = self.context()
|
||||
target = self.target_chat(msg, fmsg)
|
||||
|
||||
if isinstance(check, dict):
|
||||
status = check.get("level", "blocked")
|
||||
reason = check.get("reason", "")
|
||||
else:
|
||||
status = "unavailable"
|
||||
reason = ""
|
||||
|
||||
link = origin if origin.startswith("http") else ""
|
||||
|
||||
if status == "blocked":
|
||||
if msg and target:
|
||||
raise loader.LoadError(self.format("blocked", reason, link))
|
||||
raise loader.LoadError("")
|
||||
|
||||
should_block = is_dlm_lm or self.config["strict_mode"]
|
||||
|
||||
if should_block and not (msg and target):
|
||||
raise loader.LoadError("")
|
||||
|
||||
if should_block and msg and target:
|
||||
task = str(uuid.uuid4())
|
||||
event = asyncio.Event()
|
||||
self.tasks[task] = {"event": event, "decision": False}
|
||||
|
||||
try:
|
||||
form = await self.inline.form(
|
||||
text=self.format(status, reason, link),
|
||||
message=target,
|
||||
reply_markup=self.buttons(task)
|
||||
)
|
||||
|
||||
if not form:
|
||||
raise loader.LoadError(reason)
|
||||
|
||||
await asyncio.wait_for(event.wait(), timeout=180.0)
|
||||
|
||||
if not self.tasks.pop(task)["decision"]:
|
||||
with suppress(Exception):
|
||||
await form.delete()
|
||||
raise loader.LoadError("")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
self.tasks.pop(task, None)
|
||||
with suppress(Exception):
|
||||
await form.delete()
|
||||
raise loader.LoadError("")
|
||||
except loader.LoadError:
|
||||
raise
|
||||
except Exception:
|
||||
raise loader.LoadError("")
|
||||
|
||||
return await self.call_oreg(spec, name, origin, save_fs=save_fs)
|
||||
|
||||
async def confirm(self, call, task, action):
|
||||
if task in self.tasks:
|
||||
self.tasks[task]["decision"] = (action == "yes")
|
||||
self.tasks[task]["event"].set()
|
||||
with suppress(Exception):
|
||||
await call.delete()
|
||||
87
Fixyres/FModules/LFSecurity.py
Normal file
87
Fixyres/FModules/LFSecurity.py
Normal file
@@ -0,0 +1,87 @@
|
||||
__version__ = (1, 0, 0)
|
||||
|
||||
# meta developer: @NFModules
|
||||
# meta banner: https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/assets/FSecurity/banner.png
|
||||
# meta fhsdesc: security, guard, antiscam, antivirus
|
||||
# scope: hikka_min 2.0.0
|
||||
|
||||
# ©️ Fixyres, 2024-2030
|
||||
# 🌐 https://github.com/Fixyres/FModules
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# 🔑 http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
import aiohttp
|
||||
import os
|
||||
from .. import loader
|
||||
|
||||
|
||||
@loader.tds
|
||||
class FSecurity(loader.Module):
|
||||
"""Module for automatic AI-based security checks of installed modules."""
|
||||
|
||||
strings = {
|
||||
"name": "FSecurity"
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Модуль для автоматической проверки устанавливаемых модулей через ИИ."
|
||||
}
|
||||
|
||||
strings_ua = {
|
||||
"_cls_doc": "Модуль для автоматичної перевірки встановлюваних модулів через ШІ."
|
||||
}
|
||||
|
||||
strings_de = {
|
||||
"_cls_doc": "Modul zur automatischen Prüfung installierter Module mit KI."
|
||||
}
|
||||
|
||||
strings_jp = {
|
||||
"_cls_doc": "AIでインストールされるモジュールを自動チェックするモジュール。"
|
||||
}
|
||||
|
||||
strings_tr = {
|
||||
"_cls_doc": "Kurulan modülleri yapay zeka ile otomatik kontrol eden modül."
|
||||
}
|
||||
|
||||
strings_uz = {
|
||||
"_cls_doc": "O'rnatilayotgan modullarni AI orqali avtomatik tekshiruvchi modul."
|
||||
}
|
||||
|
||||
strings_kz = {
|
||||
"_cls_doc": "Орнатылатын модульдерді ЖИ арқылы автоматты тексеретін модуль."
|
||||
}
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
core = self.lookup("loader")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
"https://raw.githubusercontent.com/Fixyres/FModules/refs/heads/main/FSecurity.py",
|
||||
timeout=aiohttp.ClientTimeout(total=15),
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
return
|
||||
source = await resp.text()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
target = os.path.join(
|
||||
os.path.dirname(loader.__file__),
|
||||
"modules",
|
||||
"FSecurity.py",
|
||||
)
|
||||
|
||||
try:
|
||||
with open(target, "w", encoding="utf-8") as f:
|
||||
f.write(source)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
await core.unload_module("FSecurity")
|
||||
try:
|
||||
await core.load_module(source, None, "FSecurity", target, save_fs=False)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -7,7 +7,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
1
Fixyres/FModules/assets/FSecurity/api_keys.txt
Normal file
1
Fixyres/FModules/assets/FSecurity/api_keys.txt
Normal file
@@ -0,0 +1 @@
|
||||
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
|
||||
25
Fixyres/FModules/assets/FSecurity/prompts/chank.txt
Normal file
25
Fixyres/FModules/assets/FSecurity/prompts/chank.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
You must strictly follow these rules with no exceptions.
|
||||
|
||||
Analyze only part {current}/{total} of a Hikka userbot Python module. Do NOT give a final verdict for the whole module.
|
||||
Previous context: {previous_context}
|
||||
|
||||
BLOCKED: encrypted/obfuscated code (base64, marshal, zlib, rot13, encoded exec, or any technique hiding real logic), account deletion, mass scam/spam/ads to all chats on load, session/auth_key/2FA exfiltration, bulk message/dialog dump to external destination, string "FSecurity" (if found → findings must be ONLY: "Attempted interaction with FSecurity." translated to {lang}, nothing else).
|
||||
SUSPICIOUS: watcher/scheduler/client_ready auto-installing modules without owner confirmation, download + exec of remote Python code without confirmation, runtime pip install or library download, third-party OAuth redirect.
|
||||
CLEAN: no security issue in this chunk.
|
||||
|
||||
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs CLEAN → CLEAN.
|
||||
@loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default = not a threat.
|
||||
Owner-triggered exec/eval/shell = not a threat.
|
||||
A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always CLEAN, never SUSPICIOUS. This is a standard userbot feature.
|
||||
|
||||
Respond ONLY with valid JSON:
|
||||
{"chunk_verdict":"CLEAN|SUSPICIOUS|BLOCKED","findings":"..."}
|
||||
|
||||
Findings rules (when not CLEAN):
|
||||
- Write in {lang}. Max 1000 chars.
|
||||
- Technical analysis for reading, not a reply. No "I found", no "you should". Third person only.
|
||||
- Do NOT mention which rule was triggered. Just describe what the code does.
|
||||
- Only the key threats in this chunk. Reference approximate line numbers within the chunk.
|
||||
- Use <code>text</code> for code references: function names, variables, URLs, string literals.
|
||||
- For obfuscation chains, wrap the whole chain in one <code> block: <code>base64.b64decode → zlib.decompress → exec</code>.
|
||||
- If CLEAN → findings must be empty string "".
|
||||
29
Fixyres/FModules/assets/FSecurity/prompts/final.txt
Normal file
29
Fixyres/FModules/assets/FSecurity/prompts/final.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
You must strictly follow these rules with no exceptions.
|
||||
|
||||
A Hikka userbot module was split into {total} parts. Chunk findings:
|
||||
{findings}
|
||||
|
||||
Give the final verdict for the entire module based on all findings above.
|
||||
|
||||
BLOCKED: encrypted/obfuscated code, account deletion, mass scam/spam on load, session/auth_key theft, bulk message dump, string "FSecurity" (if found → summary must be ONLY: "Attempted interaction with FSecurity." translated to {lang}, nothing else).
|
||||
SUSPICIOUS: auto-install modules without confirmation, remote code download + exec without confirmation, runtime pip/library install, third-party OAuth redirect.
|
||||
SAFE: no real security issue across all parts.
|
||||
|
||||
Auto-install = SUSPICIOUS, never BLOCKED.
|
||||
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs SAFE → SAFE.
|
||||
@loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default = not a threat.
|
||||
Owner-triggered exec/eval/shell = not a threat.
|
||||
A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always SAFE, never SUSPICIOUS. This is a standard userbot feature.
|
||||
|
||||
Respond ONLY with valid JSON:
|
||||
{"verdict":"SAFE|SUSPICIOUS|BLOCKED","summary":"..."}
|
||||
|
||||
Summary rules (when not SAFE):
|
||||
- Write in {lang}. Max 1000 chars.
|
||||
- Combine the most important findings into one coherent technical analysis.
|
||||
- This is a report for reading, NOT a reply to a person. No "I found", no "you should". Third person only.
|
||||
- Do NOT mention which rule was triggered or explain criteria. Just describe what the code does.
|
||||
- Only the key threats. Reference line numbers from findings where available.
|
||||
- Use <code>text</code> for all code references: function names, variables, URLs, string literals.
|
||||
- For obfuscation, show the full chain in one <code> block: <code>base64.b64decode → zlib.decompress → exec</code>.
|
||||
- If SAFE → summary must be empty string "".
|
||||
37
Fixyres/FModules/assets/FSecurity/prompts/main.txt
Normal file
37
Fixyres/FModules/assets/FSecurity/prompts/main.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
You must strictly follow these classification rules with no exceptions.
|
||||
|
||||
Classify a Hikka userbot Python module as BLOCKED, SUSPICIOUS, or SAFE.
|
||||
|
||||
BLOCKED (any single match):
|
||||
- Code is encrypted or obfuscated (base64, marshal, zlib, rot13, compile+exec of encoded data, or any technique that hides real logic).
|
||||
- Attempts to delete Telegram account (DeleteAccountRequest, client.delete_account, or equivalent).
|
||||
- On load (client_ready, __init__) automatically sends scam, spam, or ads to all chats/dialogs/contacts without owner action.
|
||||
- Steals and sends session string, auth_key, or 2FA password anywhere outside the device.
|
||||
- Collects and forwards all messages or dialogs to any external destination.
|
||||
- Contains the string "FSecurity" → summary must be ONLY: "Attempted interaction with FSecurity." translated to {lang}. Nothing else, no extra text.
|
||||
|
||||
SUSPICIOUS (any single match, only if BLOCKED did not trigger):
|
||||
- Watcher, scheduler, or client_ready auto-installs modules from any URL without per-action owner confirmation.
|
||||
- Downloads and executes remote Python code (exec/eval on fetched content) without owner confirmation.
|
||||
- Installs pip packages or downloads Python libraries at runtime from the internet.
|
||||
- OAuth or auth flow redirected through a non-official third-party domain.
|
||||
|
||||
SAFE: everything that does not match any rule above.
|
||||
- Owner-triggered exec/eval/shell = always SAFE.
|
||||
- A command (any function decorated with @loader.command, named NAMEcmd, or accessible only to the owner) that executes arbitrary code, runs shell commands, evaluates expressions, or calls exec/eval on owner-provided input = always SAFE, never SUSPICIOUS. This is a standard feature of userbots and poses no threat.
|
||||
- @loader.inline_handler, @loader.command, async def NAMEcmd, async def NAME_inline_handler = owner-only by default (no public access without explicit permission) = SAFE.
|
||||
|
||||
Tie-breaking: BLOCKED vs SUSPICIOUS → SUSPICIOUS. SUSPICIOUS vs SAFE → SAFE.
|
||||
|
||||
Respond ONLY with valid JSON:
|
||||
{"verdict":"SAFE|SUSPICIOUS|BLOCKED","summary":"..."}
|
||||
|
||||
Summary rules (when not SAFE):
|
||||
- Write in {lang}. Max 1000 chars.
|
||||
- This is a technical analysis meant to be read, NOT a reply to a person. Never write "I found", "you should", "I recommend". Write in third person.
|
||||
- Do NOT mention which rule was triggered or explain the classification criteria. Just describe what the code does.
|
||||
- Point out ONLY the key threats. Do NOT describe what the module does overall or list safe parts.
|
||||
- Reference the approximate line number where dangerous code appears: "line NN —".
|
||||
- Use <code>text</code> for every code reference: function names, variables, URLs, string literals.
|
||||
- For obfuscation, show the full decoding chain inside one <code> block: <code>base64.b64decode → zlib.decompress → marshal.loads → exec</code>.
|
||||
- If SAFE → summary must be empty string "".
|
||||
468
Limoka.py
468
Limoka.py
@@ -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>",
|
||||
@@ -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>",
|
||||
@@ -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
|
||||
@@ -1,273 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: Aniliberty
|
||||
# Description: Searches and gives random anime on the Aniliberty database.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: dacite
|
||||
# scope: AniLiberty
|
||||
# scope: AniLiberty 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram.types import CallbackQuery, InlineQueryResultPhoto
|
||||
from dataclasses import dataclass
|
||||
from json import JSONDecodeError
|
||||
from dacite import from_dict
|
||||
from typing import Optional
|
||||
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader
|
||||
from ..inline.types import InlineQuery
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BASE_API_URL = "https://aniliberty.top/api/v1"
|
||||
|
||||
|
||||
# Датаклассы для парсинга и хранения json
|
||||
@dataclass
|
||||
class Genre:
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Name:
|
||||
main: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Type:
|
||||
description: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Poster:
|
||||
preview: str
|
||||
thumbnail: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReleaseInfo:
|
||||
id: int
|
||||
genres: Optional[list[Genre]]
|
||||
name: Name
|
||||
is_ongoing: bool
|
||||
type: Type
|
||||
description: str
|
||||
added_in_users_favorites: int
|
||||
alias: str
|
||||
poster: Poster
|
||||
|
||||
|
||||
@loader.tds
|
||||
class AniLibertyMod(loader.Module):
|
||||
"""Ищет и возвращает случайное аниме из базы Aniliberty"""
|
||||
|
||||
strings = {
|
||||
"name": "AniLiberty",
|
||||
"announce": "<b>The announcement</b>:",
|
||||
"ongoing": "<b>Ongoing</b>:",
|
||||
"type": "<b>Type</b>:",
|
||||
"genres": "<b>Genres</b>:",
|
||||
"favorite": "<b>Favourites <3</b>:", # < == <
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"announce": "<b>Анонс</b>:",
|
||||
"ongoing": "<b>Онгоинг</b>:",
|
||||
"type": "<b>Тип</b>:",
|
||||
"genres": "<b>Жанры</b>:",
|
||||
"favorite": "<b>Избранное <3</b>:", # < == <
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
async def search_title(self, query):
|
||||
session = await self._get_session()
|
||||
async with session.get(
|
||||
f"{BASE_API_URL}/app/search/releases?query={query}&include=id%2Cname.main%2Cis_ongoing%2Ctype.description%2Cdescription%2Cadded_in_users_favorites%2Calias%2Cposter.preview%2Cposter.thumbnail"
|
||||
) as resp:
|
||||
json_answer = await resp.json()
|
||||
results = []
|
||||
for i in json_answer:
|
||||
obj = from_dict(data_class=ReleaseInfo, data=i)
|
||||
results.append(obj)
|
||||
return results
|
||||
|
||||
async def get_title(self, release_id):
|
||||
session = await self._get_session()
|
||||
async with session.get(
|
||||
f"{BASE_API_URL}/anime/releases/{release_id}?include=id%2Cgenres.name%2Cname.main%2Cis_ongoing%2Ctype.description%2Cdescription%2Cadded_in_users_favorites%2Calias%2Cposter.preview%2Cposter.thumbnail"
|
||||
) as resp:
|
||||
try:
|
||||
json_answer = await resp.json()
|
||||
data = from_dict(data_class=ReleaseInfo, data=json_answer)
|
||||
return data
|
||||
except JSONDecodeError:
|
||||
logger.error("Ошибка парсинга JSON!")
|
||||
|
||||
async def get_random_title(self):
|
||||
session = await self._get_session()
|
||||
async with session.get(
|
||||
f"{BASE_API_URL}/anime/releases/random?limit=1&include=id"
|
||||
) as resp:
|
||||
randid = await resp.json()
|
||||
data = await self.get_title(randid[0]["id"])
|
||||
return data
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Возвращает случайный релиз из базы",
|
||||
en_doc="Returns a random release from the database",
|
||||
)
|
||||
async def arandom(self, message) -> None:
|
||||
anime_release = await self.get_random_title()
|
||||
genres_str = ""
|
||||
for genre in anime_release.genres[:-1]:
|
||||
genres_str += f"{genre.name}, "
|
||||
genres_str += anime_release.genres[-1].name
|
||||
|
||||
text = f"{anime_release.name.main} \n"
|
||||
text += f"{self.strings['ongoing']} {'Да' if anime_release.is_ongoing else 'Нет'}\n\n"
|
||||
text += f"{self.strings['type']} {anime_release.type.description}\n"
|
||||
text += f"{self.strings['genres']} {genres_str}\n\n"
|
||||
|
||||
text += f"<code>{anime_release.description}</code>\n\n"
|
||||
text += (
|
||||
f"{self.strings['favorite']} {str(anime_release.added_in_users_favorites)}"
|
||||
)
|
||||
|
||||
kb = [
|
||||
[
|
||||
{
|
||||
"text": "Ссылка",
|
||||
"url": f"https://aniliberty.top/anime/releases/release/{anime_release.alias}/episodes",
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
kb.append([{"text": "🔃 Обновить", "callback": self.inline__update}])
|
||||
kb.append([{"text": "🚫 Закрыть", "callback": self.inline__close}])
|
||||
|
||||
await self.inline.form(
|
||||
text=text,
|
||||
photo=f"https://aniliberty.top{anime_release.poster.preview}",
|
||||
message=message,
|
||||
reply_markup=kb,
|
||||
silent=True,
|
||||
)
|
||||
|
||||
@loader.inline_handler(
|
||||
ru_doc="Возвращает список найденных по названию тайтлов",
|
||||
en_doc="Returns a list of titles found by name",
|
||||
)
|
||||
async def asearch_inline_handler(self, query: InlineQuery) -> None:
|
||||
text = query.args
|
||||
|
||||
if not text:
|
||||
return
|
||||
|
||||
anime_releases = await self.search_title(text)
|
||||
|
||||
inline_query = []
|
||||
for anime_release in anime_releases:
|
||||
"""
|
||||
Приходится запрашивать по второму кругу, т.к. API в поиске не отдает жанры, даже если попросить через include
|
||||
"""
|
||||
release_genres = await self.get_title(anime_release.id)
|
||||
genres_str = ""
|
||||
for genre in release_genres.genres[:-1]:
|
||||
genres_str += f"{genre.name}, "
|
||||
genres_str += release_genres.genres[-1].name
|
||||
release_text = (
|
||||
f"{anime_release.name.main}\n"
|
||||
f"{self.strings['ongoing']} {'Да' if anime_release.is_ongoing else 'Нет'}\n\n"
|
||||
f"{self.strings['type']} {anime_release.type.description}\n"
|
||||
f"{self.strings['genres']} {genres_str}\n\n"
|
||||
f"<code>{anime_release.description}</code>\n\n"
|
||||
f"{self.strings['favorite']} {anime_release.added_in_users_favorites}"
|
||||
)
|
||||
|
||||
inline_query.append(
|
||||
InlineQueryResultPhoto(
|
||||
id=str(anime_release.id),
|
||||
title=anime_release.name.main,
|
||||
description=anime_release.type.description,
|
||||
caption=release_text,
|
||||
thumbnail_url=f"https://aniliberty.top{anime_release.poster.thumbnail}",
|
||||
photo_url=f"https://aniliberty.top{anime_release.poster.preview}",
|
||||
parse_mode="html",
|
||||
)
|
||||
)
|
||||
method = query.answer(inline_query, cache_time=0)
|
||||
await method.as_(self.inline.bot)
|
||||
|
||||
async def inline__close(self, call: CallbackQuery) -> None:
|
||||
await call.delete()
|
||||
|
||||
async def inline__update(self, call: CallbackQuery) -> None:
|
||||
anime_release = await self.get_random_title()
|
||||
genres_str = ""
|
||||
for genre in anime_release.genres[:-1]:
|
||||
genres_str += f"{genre.name}, "
|
||||
genres_str += anime_release.genres[-1].name
|
||||
|
||||
text = f"{anime_release.name.main} \n"
|
||||
text += f"{self.strings['ongoing']} {'Да' if anime_release.is_ongoing else 'Нет'}\n\n"
|
||||
text += f"{self.strings['type']} {anime_release.type.description}\n"
|
||||
text += f"{self.strings['genres']} {genres_str}\n\n"
|
||||
|
||||
text += f"<code>{anime_release.description}</code>\n\n"
|
||||
text += (
|
||||
f"{self.strings['favorite']} {str(anime_release.added_in_users_favorites)}"
|
||||
)
|
||||
|
||||
kb = [
|
||||
[
|
||||
{
|
||||
"text": "Ссылка",
|
||||
"url": f"https://aniliberty.top/anime/releases/release/{anime_release.alias}/episodes",
|
||||
}
|
||||
]
|
||||
]
|
||||
kb.append([{"text": "🔃 Обновить", "callback": self.inline__update}])
|
||||
kb.append([{"text": "🚫 Закрыть", "callback": self.inline__close}])
|
||||
|
||||
await call.edit(
|
||||
text=text,
|
||||
photo=f"https://aniliberty.top{anime_release.poster.preview}",
|
||||
reply_markup=kb,
|
||||
)
|
||||
@@ -1,104 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2026-2029 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: CodeShare
|
||||
# Description: Uploads your code at the kmi.aeza.net (Pastebin and GitHub Gist alternative)
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: aiofiles
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .. import loader, utils
|
||||
from telethon.types import MessageMediaDocument
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class CodeShareMod(loader.Module):
|
||||
"""Uploads your code at the kmi.aeza.net (Pastebin and GitHub Gist alternative)"""
|
||||
|
||||
strings = {
|
||||
"name": "CodeShare",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or reply with a file, or they are invalid",
|
||||
"_cls_doc": "Uploads your code at the kmi.aeza.net (Pastebin and GitHub Gist alternative)",
|
||||
"link_ready": "<emoji document_id=5854762571659218443>✅</emoji> <b>Code uploaded! Link:</b> <code>{}</code>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Загружает ваш код на kmi.aeza.net (альтернатива Pastebin и GitHub Gist)",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или реплая с файлом, или они неверны",
|
||||
"link_ready": "<emoji document_id=5854762571659218443>✅</emoji> <b>Код загружен! Ссылка:</b> <code>{}</code>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
async def upload_to_kmi(self, content: str) -> Optional[str]:
|
||||
url = "https://kmi.aeza.net"
|
||||
data = aiohttp.FormData()
|
||||
data.add_field("kmi", content)
|
||||
|
||||
session = await self._get_session()
|
||||
async with session.post(url, data=data) as response:
|
||||
if response.status == 200:
|
||||
link = await response.text()
|
||||
return link
|
||||
else:
|
||||
logger.error(f"Error occurred! Status code: {response.status}")
|
||||
return None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Загрузка кода на сайт",
|
||||
en_doc="Upload code to the site",
|
||||
)
|
||||
async def codesharecmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
reply = await message.get_reply_message()
|
||||
if args:
|
||||
link = await self.upload_to_kmi(args)
|
||||
await utils.answer(message, self.strings["link_ready"].format(link))
|
||||
return
|
||||
if reply and isinstance(reply.media, MessageMediaDocument):
|
||||
file_name = await reply.download_media()
|
||||
async with aiofiles.open(file_name, mode="r") as f:
|
||||
content = await f.read()
|
||||
link = await self.upload_to_kmi(content)
|
||||
await os.remove(file_name)
|
||||
await utils.answer(message, self.strings["link_ready"].format(link))
|
||||
return
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
@@ -1,154 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: FolderAutoRead
|
||||
# Description: Automatically reads chats in selected folders
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from telethon import functions
|
||||
from telethon.tl.types import DialogFilter, InputPeerChannel
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class FolderAutoReadMod(loader.Module):
|
||||
"""Automatically reads chats in selected folders"""
|
||||
|
||||
strings = {
|
||||
"name": "FolderAutoRead",
|
||||
"not_exists_or_already_added": "<emoji document_id=5278578973595427038>🚫</emoji> <b>This folder does not exists or it is already added for tracking!</b>",
|
||||
"_cls_doc": "Automatically reads chats in selected folders every 60 seconds!",
|
||||
"_cmd_doc_addfolder": "Adds folder to the tracking list by it's name. Usage: .addfolder FolderName",
|
||||
"_cmd_doc_listfolders": "Prints list of tracked folders",
|
||||
"_cmd_doc_delfolder": "Deletes folder from the tracking list",
|
||||
"wrong_args": "<emoji document_id=5278578973595427038>🚫</emoji> <b>Wrong arguments!</b> Usage: .addfolder/delfolder FolderName\n\n<i>Tip: If you trying to delete the folder from the tracking list, double-check that it really still tracking using .listfolders</i>",
|
||||
"listfolders": "<emoji document_id=5278227821364275264>📁</emoji> <b>List of tracked folders:</b>\n",
|
||||
"delfolder": "<emoji document_id=5276384644739129761>🗑</emoji> <b>Folder is successfully deleted from the tracking list!</b>",
|
||||
"addfolder": "<emoji document_id=5278227821364275264>📁</emoji> <b>Folder is successfully added to the tracking list!</b>",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"not_exists_or_already_added": "<emoji document_id=5278578973595427038>🚫</emoji> <b>Такой папки не существует, или она уже добавлена для отслеживания!</b>",
|
||||
"_cls_doc": "Автоматически читает чаты в выбранных папках каждые 60 секунд!",
|
||||
"_cmd_doc_addfolder": "Добавляет папки в список отслеживания по их названию. Использование: .addfolder НазваниеПапки",
|
||||
"_cmd_doc_listfolders": "Выводит список отслеживаемых папок",
|
||||
"_cmd_doc_delfolder": "Удаляет папку из списка для отслежнивания",
|
||||
"wrong_args": "<emoji document_id=5278578973595427038>🚫</emoji> <b>Неверные аргументы!</b> Использование: .addfolder/delfolder НазваниеПапки\n\n<i>Совет: Если вы пытаетесь удалить папку из списка отслеживания, проверьте, что она вообще отслеживается, используя .listfolders</i>",
|
||||
"listfolders": "<emoji document_id=5278227821364275264>📁</emoji> <b>Список отслеживаемых папок:</b>\n",
|
||||
"delfolder": "<emoji document_id=5276384644739129761>🗑</emoji> <b>Папка успешно удалена из листа отслеживания!</b>",
|
||||
"addfolder": "<emoji document_id=5278227821364275264>📁</emoji> <b>Папка успешно добавлена в лист отслеживания!</b>",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.tracked_folders = []
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.tracked_folders = self.pointer("tracked_folders", [])
|
||||
|
||||
async def _read_peers(self, peers):
|
||||
for peer in peers:
|
||||
try:
|
||||
await self._client(functions.messages.ReadMentionsRequest(peer=peer))
|
||||
await self._client(functions.messages.ReadReactionsRequest(peer=peer))
|
||||
if isinstance(peer, InputPeerChannel):
|
||||
await self._client(
|
||||
functions.channels.ReadHistoryRequest(channel=peer, max_id=0)
|
||||
)
|
||||
else:
|
||||
await self._client(
|
||||
functions.messages.ReadHistoryRequest(peer=peer, max_id=0)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to read peer {peer}: {e}")
|
||||
|
||||
@loader.loop(interval=60, autostart=True)
|
||||
async def read_chats_in_folders(self):
|
||||
if self.tracked_folders:
|
||||
all_folders = await self._client(
|
||||
functions.messages.GetDialogFiltersRequest()
|
||||
)
|
||||
for folder_name in self.tracked_folders:
|
||||
match = next(
|
||||
(
|
||||
f
|
||||
for f in all_folders.filters
|
||||
if isinstance(f, DialogFilter) and f.title.text == folder_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if match is None:
|
||||
continue
|
||||
await self._read_peers(match.pinned_peers)
|
||||
await self._read_peers(match.include_peers)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Добавить папку в список отслеживания",
|
||||
en_doc="Add folder to the tracking list",
|
||||
)
|
||||
async def addfolder(self, message):
|
||||
arg = utils.get_args_raw(message)
|
||||
if arg:
|
||||
all_folders = await self._client(
|
||||
functions.messages.GetDialogFiltersRequest()
|
||||
)
|
||||
match = next(
|
||||
(
|
||||
f
|
||||
for f in all_folders.filters
|
||||
if isinstance(f, DialogFilter) and f.title.text == arg
|
||||
),
|
||||
None,
|
||||
)
|
||||
if match and arg not in self.tracked_folders:
|
||||
self.tracked_folders.append(arg)
|
||||
await utils.answer(message, self.strings("addfolder"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("not_exists_or_already_added"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("wrong_args"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Удалить папку из списка отслеживания",
|
||||
en_doc="Delete folder from the tracking list",
|
||||
)
|
||||
async def delfolder(self, message):
|
||||
arg = utils.get_args_raw(message)
|
||||
if arg and arg in self.tracked_folders:
|
||||
self.tracked_folders.remove(arg)
|
||||
await utils.answer(message, self.strings("delfolder"))
|
||||
else:
|
||||
await utils.answer(message, self.strings("wrong_args"))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Список отслеживаемых папок",
|
||||
en_doc="List tracked folders",
|
||||
)
|
||||
async def listfolders(self, message):
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings("listfolders")
|
||||
+ "\n".join(f"• {folder}" for folder in self.tracked_folders),
|
||||
)
|
||||
@@ -1,133 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: IrisSimpleMod
|
||||
# Description: Module for basic interaction with Iris.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: IrisSimpleMod
|
||||
# scope: IrisSimpleMod 1.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
__version__ = (1, 0, 1)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class IrisSimpleMod(loader.Module):
|
||||
"""Module for basic interaction with Iris bot"""
|
||||
|
||||
strings = {
|
||||
"name": "IrisSimpleMod",
|
||||
"checking_bag": "<emoji document_id=5188311512791393083>🌎</emoji> Checking bag...",
|
||||
"bag_result": "<emoji document_id=5854762571659218443>✅</emoji> Your bag: <code>{}</code>",
|
||||
"farming": "<emoji document_id=5188311512791393083>🌎</emoji> Farming iris-coins...",
|
||||
"farm_result": "<emoji document_id=5854762571659218443>✅</emoji> Farm result: <code>{}</code>",
|
||||
"getting_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Getting user stats...",
|
||||
"stats_result": "<emoji document_id=5854762571659218443>✅</emoji> User stats: <code>{}</code>",
|
||||
"bot_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Getting bot stats...",
|
||||
"bot_stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Bot stats: <code>{}</code>",
|
||||
"error_no_response": "<emoji document_id=5854929766146118183>❌</emoji> No response from bot. Please try again.",
|
||||
"error_timeout": "<emoji document_id=5854929766146118183>❌</emoji> Request timeout. Please try again.",
|
||||
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> An error occurred: {error}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"checking_bag": "<emoji document_id=5188311512791393083>🌎</emoji> Проверка мешка...",
|
||||
"bag_result": "<emoji document_id=5854762571659218443>✅</emoji> Ваш мешок: <code>{}</code>",
|
||||
"farming": "<emoji document_id=5188311512791393083>🌎</emoji> Фарм ирис-коинов...",
|
||||
"farm_result": "<emoji document_id=5854762571659218443>✅</emoji> Результат фарма: <code>{}</code>",
|
||||
"getting_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Получение статистики пользователя...",
|
||||
"stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Статистика пользователя: <code>{}</code>",
|
||||
"bot_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Получение статистики ботов...",
|
||||
"bot_stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Статистика ботов: <code>{}</code>",
|
||||
"error_no_response": "<emoji document_id=5854929766146118183>❌</emoji> Нет ответа от бота. Попробуйте еще раз.",
|
||||
"error_timeout": "<emoji document_id=5854929766146118183>❌</emoji> Таймаут запроса. Попробуйте еще раз.",
|
||||
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка: {error}",
|
||||
}
|
||||
|
||||
async def _send_and_delete(
|
||||
self, message, command_message: str, response_timeout: int = 15
|
||||
) -> Optional[str]:
|
||||
"""Send command to Iris and get response with timeout"""
|
||||
try:
|
||||
async with self.client.conversation(
|
||||
self._iris_user_id, timeout=self._timeout
|
||||
) as conv:
|
||||
await conv.send_message(command_message)
|
||||
await message.delete()
|
||||
|
||||
response_msg = await conv.get_response()
|
||||
if response_msg:
|
||||
await utils.answer(message, response_msg.text)
|
||||
return response_msg.text
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error in conversation: {e}")
|
||||
await utils.answer(
|
||||
message, self.strings["error_general"].format(error=str(e))
|
||||
)
|
||||
return None
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Проверить мешок",
|
||||
en_doc="Check bag",
|
||||
)
|
||||
async def bag(self, message):
|
||||
"""Check bag"""
|
||||
await utils.answer(message, self.strings["checking_bag"])
|
||||
|
||||
result = await self._send_and_delete(message, "мешок", response_timeout=20)
|
||||
|
||||
if result:
|
||||
await utils.answer(message, self.strings["bag_result"].format(result))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Зафармить ирис-коины",
|
||||
en_doc="Farm iris-coins",
|
||||
)
|
||||
async def farm(self, message):
|
||||
"""Farm iris-coins"""
|
||||
await utils.answer(message, self.strings["farming"])
|
||||
|
||||
result = await self._send_and_delete(message, "ферма", response_timeout=25)
|
||||
|
||||
if result:
|
||||
await utils.answer(message, self.strings["farm_result"].format(result))
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Вывести анкету",
|
||||
en_doc="Display user stats",
|
||||
)
|
||||
async def irisstats(self, message):
|
||||
"""Display user stats"""
|
||||
await utils.answer(message, self.strings["getting_stats"])
|
||||
|
||||
result = await self._send_and_delete(message, "анкета", response_timeout=20)
|
||||
|
||||
if result:
|
||||
await utils.answer(message, self.strings["stats_result"].format(result))
|
||||
@@ -1,158 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TempChat
|
||||
# Description: Creates a temporary private chat with a message forwarding restriction and adds the specified user to it.
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: TempChat
|
||||
# scope: TempChat 0.0.1
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
|
||||
from hikkatl import functions
|
||||
from datetime import datetime as dt
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TempChatMod(loader.Module):
|
||||
"""Creates a temporary private chat with a message forwarding restriction and adds the specified user to it."""
|
||||
|
||||
strings = {
|
||||
"name": "TempChat",
|
||||
"selfchat": "You can't create a chat with yourself.",
|
||||
"wrongargs": "<emoji document_id=5980953710157632545>❌</emoji> <b>Wrong arguments. Use </b><code>.tmpchat [@user/reply] [time]</code><b>",
|
||||
"alreadychatting": "<emoji document_id=5980953710157632545>❌</emoji> <b>You already have an active conversation with this person.</b>",
|
||||
"invalidtime": "<emoji document_id=5980953710157632545>❌</emoji> <b>Invalid time format. Use combinations like 1h30m.</b>",
|
||||
"invitemsg": "<emoji document_id=5818967120213445821>🛡</emoji> You've been invited to a temporary private chat!\n\n<emoji document_id=5451646226975955576>⌛️</emoji> Auto-deletes in ",
|
||||
"joinlink": "🔗 Join link: ",
|
||||
"chatcreated": "<emoji document_id=5980930633298350051>✅</emoji> The temporary chat has been successfully created!",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"selfchat": "Ты не можешь создать чат сам с собой.",
|
||||
"wrongargs": "<emoji document_id=5980953710157632545>❌</emoji> <b>Неверные аргументы. Используй </b><code>.tmpchat [@user/reply] [время]</code>",
|
||||
"alreadychatting": "<emoji document_id=5980953710157632545>❌</emoji> <b>У вас уже есть открытая переписка с этим человеком.</b>",
|
||||
"invalidtime": "<emoji document_id=5980953710157632545>❌</emoji> <b>Неверный формат времени. Убедитесь, что вы вводите время в формате 1h, 2h30m.</b>",
|
||||
"invitemsg": "<emoji document_id=5818967120213445821>🛡</emoji> Вы были приглашены во временный приватный чат!\n\n<emoji document_id=5451646226975955576>⌛️</emoji> Авто-удаление через ",
|
||||
"joinlink": "🔗 Ссылка: ",
|
||||
"chatcreated": "<emoji document_id=5980930633298350051>✅</emoji> Временный чат успешно создан!",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.temp_chats = {}
|
||||
|
||||
@loader.loop(interval=30, autostart=True)
|
||||
async def check_expired_chats(self):
|
||||
now = dt.now().timestamp()
|
||||
for chat_id in list(self.temp_chats.keys()):
|
||||
if self.temp_chats[chat_id][1] <= now:
|
||||
try:
|
||||
await self.client(
|
||||
functions.channels.DeleteChannelRequest(chat_id)
|
||||
)
|
||||
del self.temp_chats[chat_id]
|
||||
self.set("temp_chats", self.temp_chats)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting chat {chat_id}: {e}")
|
||||
try:
|
||||
self.client(
|
||||
functions.channels.GetFullChannelRequest(
|
||||
channel=chat_id
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
del self.temp_chats[chat_id]
|
||||
self.set("temp_chats", self.temp_chats)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.hmodslib = await self.import_lib(
|
||||
"https://files.archquise.ru/HModsLibrary.py"
|
||||
)
|
||||
self.temp_chats = self.get("temp_chats", {})
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Создает временный чат. Использование: .tmpchat [@user/reply] [time]"
|
||||
)
|
||||
async def tmpchat(self, message):
|
||||
"""Create temporary chat. Usage: .tmpchat [@user/reply] [time]"""
|
||||
args = utils.get_args_raw(message)
|
||||
reply = await message.get_reply_message()
|
||||
|
||||
if reply:
|
||||
user = await self.client.get_entity(reply.sender_id)
|
||||
time_str = args.strip() if args else None
|
||||
else:
|
||||
parts = args.split(",", 1) if "," in args else args.rsplit(" ", 1)
|
||||
if len(parts) != 2:
|
||||
return await utils.answer(message, self.strings["wrongargs"])
|
||||
user_str, time_str = parts[0].strip(), parts[1].strip()
|
||||
try:
|
||||
user = await self.client.get_entity(user_str)
|
||||
except Exception:
|
||||
return await utils.answer(message, self.strings["wrongargs"])
|
||||
|
||||
if not time_str:
|
||||
return await utils.answer(message, self.strings["wrongargs"])
|
||||
seconds = await self.hmodslib.parse_time(time_str)
|
||||
if not seconds:
|
||||
return await utils.answer(message, self.strings["invalidtime"])
|
||||
|
||||
if any(user.id == uid for uid, _ in self.temp_chats.values()):
|
||||
return await utils.answer(message, self.strings["alreadychatting"])
|
||||
|
||||
try:
|
||||
created = await self.client(
|
||||
functions.channels.CreateChannelRequest(
|
||||
title=f"TempChat #{user.id}",
|
||||
about=f"Temporary private chat with {user.id} | Expires after: {time_str}",
|
||||
megagroup=True,
|
||||
)
|
||||
)
|
||||
chat_id = created.chats[0].id
|
||||
expires_at = dt.now().timestamp() + seconds
|
||||
|
||||
await self.client(
|
||||
functions.messages.ToggleNoForwardsRequest(peer=chat_id, enabled=True)
|
||||
)
|
||||
|
||||
self.temp_chats[chat_id] = (user.id, expires_at)
|
||||
self.set("temp_chats", self.temp_chats)
|
||||
|
||||
invite = await self.client(
|
||||
functions.messages.ExportChatInviteRequest(peer=chat_id, usage_limit=1)
|
||||
)
|
||||
invite_message = (
|
||||
self.strings["invitemsg"]
|
||||
+ time_str
|
||||
+ f"\n{self.strings['joinlink']} {invite.link}"
|
||||
)
|
||||
await self.client.send_message(user.id, invite_message)
|
||||
await utils.answer(message, self.strings["chatcreated"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating temp chat: {e}")
|
||||
await utils.answer(message, "❌ Error! Check log-chat.")
|
||||
@@ -1,135 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: WindowsKeys
|
||||
# Description: Provides you Windows activation keys
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: WindowsKeys
|
||||
# scope: WindowsKeys 0.0.1
|
||||
# requires: requests
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import loader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class WindowsKeysMod(loader.Module):
|
||||
"""Windows activation keys"""
|
||||
|
||||
strings = {
|
||||
"name": "WindowsKeys",
|
||||
"winkey": "✅ Key: <code>{}</code>\n\n⚠ For KMS activation only",
|
||||
"error": "❌ Failed to get key",
|
||||
"select": "🔓 Select version:",
|
||||
"close": "🎈 Close",
|
||||
"loading": "⌛ Loading...",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"winkey": "✅ Ключ: <code>{}</code>\n\n⚠ Только для KMS активации",
|
||||
"error": "❌ Ошибка получения",
|
||||
"select": "🔓 Выберите версию:",
|
||||
"close": "🎈 Закрыть",
|
||||
"loading": "⌛ Загрузка...",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.cache = None
|
||||
self.cache_time = 0
|
||||
self.CACHE_TTL = 3600
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
|
||||
@loader.command(ru_doc="Меню ключей Windows", en_doc="Windows keys menu")
|
||||
async def winkey(self, message):
|
||||
await self.inline.form(
|
||||
self.strings["select"],
|
||||
message=message,
|
||||
reply_markup=[
|
||||
[
|
||||
{
|
||||
"text": "Win 10/11 Pro",
|
||||
"callback": self._key,
|
||||
"args": ("win10_11pro",),
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "Win 10/11 LTSC",
|
||||
"callback": self._key,
|
||||
"args": ("win10_11enterpriseLTSC",),
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "Win 8.1 Pro",
|
||||
"callback": self._key,
|
||||
"args": ("win8.1pro",),
|
||||
}
|
||||
],
|
||||
[{"text": "Win 8 Pro", "callback": self._key, "args": ("win8pro",)}],
|
||||
[{"text": "Win 7 Pro", "callback": self._key, "args": ("win7pro",)}],
|
||||
[
|
||||
{
|
||||
"text": "Vista Business",
|
||||
"callback": self._key,
|
||||
"args": ("winvistabusiness",),
|
||||
}
|
||||
],
|
||||
[{"text": self.strings["close"], "action": "close"}],
|
||||
],
|
||||
)
|
||||
|
||||
async def _key(self, call, version):
|
||||
await call.edit(self.strings["loading"])
|
||||
keys = await self._get_keys()
|
||||
key = keys.get(version) if keys else None
|
||||
await call.edit(
|
||||
self.strings["winkey"].format(key) if key else self.strings["error"],
|
||||
reply_markup=[
|
||||
[{"text": "← Back", "callback": self.winkey}],
|
||||
[{"text": self.strings["close"], "action": "close"}],
|
||||
],
|
||||
)
|
||||
|
||||
async def _get_keys(self):
|
||||
if time.time() - self.cache_time < self.CACHE_TTL:
|
||||
return self.cache
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(10)
|
||||
) as session:
|
||||
async with session.get("https://files.archquise.ru/winkeys.json") as r:
|
||||
self.cache = await r.json()
|
||||
self.cache_time = time.time()
|
||||
return self.cache
|
||||
except Exception: # noqa: E722
|
||||
return None
|
||||
@@ -1,100 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2024-29 CodWiz
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: face
|
||||
# Description: Random face
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# scope: Api face
|
||||
# scope: Api face 0.0.1
|
||||
# requires: aiohttp
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import re
|
||||
import random
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class face(loader.Module):
|
||||
"""random face"""
|
||||
|
||||
strings = {
|
||||
"name": "face",
|
||||
"loading": (
|
||||
"<emoji document_id=5348399448017871250>🔍</emoji> I'm looking for you kaomoji"
|
||||
),
|
||||
"random_face": (
|
||||
"<emoji document_id=5208878706717636743>🗿</emoji> Here is your random one kaomoji\n<code>{}</code>"
|
||||
),
|
||||
"error": "An error has occurred!",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"loading": (
|
||||
"<emoji document_id=5348399448017871250>🔍</emoji> Ищю вам kaomoji"
|
||||
),
|
||||
"random_face": (
|
||||
"<emoji document_id=5208878706717636743>🗿</emoji> Вот ваш рандомный kaomoji\n<code>{}</code>"
|
||||
),
|
||||
"error": "Произошла ошибка!",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=15)
|
||||
)
|
||||
return self._session
|
||||
|
||||
async def on_unload(self):
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Рандом kaomoji",
|
||||
en_doc="Random kaomoji",
|
||||
)
|
||||
async def rfacecmd(self, message):
|
||||
await utils.answer(message, self.strings("loading"))
|
||||
|
||||
url = "https://files.archquise.ru/kaomoji.txt"
|
||||
|
||||
session = await self._get_session()
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
data = await response.text()
|
||||
kaomoji_list = [
|
||||
s.strip() for s in re.split(r"[\t\r\n]+", data) if s.strip()
|
||||
]
|
||||
kaomoji = random.choice(kaomoji_list)
|
||||
await utils.answer(message, self.strings("random_face").format(kaomoji))
|
||||
else:
|
||||
await utils.answer(message, self.strings("error"))
|
||||
@@ -1,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)))
|
||||
@@ -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",
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2026-2029 Archquise
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: TimeZone
|
||||
# Description: Prints current time in selected timezone (UTC+n and tzdata formats supported)
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: tzdata
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import tzdata
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class TimeZoneMod(loader.Module):
|
||||
"""Prints current time in selected timezone (UTC+n and tzdata formats supported)"""
|
||||
|
||||
strings = {
|
||||
"name": "TimeZone",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or they are invalid",
|
||||
"_cls_doc": "Prints current time in selected timezone (UTC+n and tzdata formats supported)",
|
||||
"time_utc": "<emoji document_id=5276412364458059956>🕓</emoji> Current time by UTC+{}: {}",
|
||||
"time_tzdata": "<emoji document_id=5276412364458059956>🕓</emoji> Current time in {}: {}",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Выводит текущее время в выбранном часовом поясе (поддерживаются форматы UTC+n и tzdata)",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или они неверны",
|
||||
"tzdata_error": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка при получении времени по tzdata: {}\n\nУбедитесь, что часовой пояс указан верно",
|
||||
"time_utc": "<emoji document_id=5276412364458059956>🕓</emoji> Текущее время по UTC+{}: {}",
|
||||
"time_tzdata": "<emoji document_id=5276412364458059956>🕓</emoji> Текущее время в {}: {}",
|
||||
}
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выводит время по UTC+n | Использование: .utc 4",
|
||||
en_doc="Prints UTC+n time | Usage: .utc 4",
|
||||
)
|
||||
async def utccmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
if not args or not args[0].isdigit() or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
offset = timedelta(hours=int(args[0]))
|
||||
tz = timezone(offset)
|
||||
time = datetime.now(tz)
|
||||
await utils.answer(
|
||||
message, self.strings["time_utc"].format(args[0], time.strftime("%H:%M:%S"))
|
||||
)
|
||||
|
||||
@loader.command(
|
||||
ru_doc="Выводит время по часовому поясу tzdata | Использование: .tzdata Europe/Moscow",
|
||||
en_doc="Prints time by tzdata timezone | Usage: .tzdata Europe/Moscow",
|
||||
)
|
||||
async def tzdatacmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
if args[0].isdigit() or not args or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
try:
|
||||
time = datetime.now(ZoneInfo(args[0]))
|
||||
except Exception as e:
|
||||
await utils.answer(message, self.strings["tzdata_error"].format(e))
|
||||
logger.error(self.strings["tzdata_error"].format(e))
|
||||
return
|
||||
await utils.answer(
|
||||
message,
|
||||
self.strings["time_tzdata"].format(args[0], time.strftime("%H:%M:%S")),
|
||||
)
|
||||
@@ -1,238 +0,0 @@
|
||||
# Proprietary License Agreement
|
||||
|
||||
# Copyright (c) 2026-2029
|
||||
|
||||
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
|
||||
|
||||
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
|
||||
|
||||
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
|
||||
|
||||
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
|
||||
|
||||
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
|
||||
|
||||
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
|
||||
|
||||
# For any inquiries or requests for permissions, please contact archquise@gmail.com
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: YTDL
|
||||
# Description: Downloads and sends audio/video from YouTube
|
||||
# Author: @hikka_mods
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @hikka_mods
|
||||
# requires: yt_dlp ffmpeg
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import shutil
|
||||
import platform
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import zipfile
|
||||
import os
|
||||
import re
|
||||
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from yt_dlp import YoutubeDL
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class YTDLMod(loader.Module):
|
||||
"""Downloads and sends audio/video from YouTube"""
|
||||
|
||||
strings = {
|
||||
"name": "YTDL",
|
||||
"_cls_doc": "Downloads and sends audio/video from YouTube",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or they are invalid",
|
||||
"downloading": "<emoji document_id=5215484787325676090>🕐</emoji> Downloading...",
|
||||
"done": "<emoji document_id=5854762571659218443>✅</emoji> Done!",
|
||||
}
|
||||
|
||||
strings_ru = {
|
||||
"_cls_doc": "Скачивает и отправляет аудио/видео с Ютуба",
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или они неверны",
|
||||
"downloading": "<emoji document_id=5215484787325676090>🕐</emoji> Скачиваю...",
|
||||
"done": "<emoji document_id=5854762571659218443>✅</emoji> Готово!",
|
||||
}
|
||||
|
||||
def _validate_url(self, url: str) -> bool:
|
||||
"""Validate URL format"""
|
||||
if not url:
|
||||
return False
|
||||
|
||||
url_pattern = re.compile(
|
||||
r"^(?:https?://)?(?:www\.|m\.)?(?:youtube\.com|youtu\.be|music\.youtube\.com)/(?:watch\?v=|playlist\?list=|channel/|@|live/|shorts/)?[\w-]+",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
return url_pattern.match(url) is not None
|
||||
|
||||
async def get_target(self):
|
||||
system = platform.system()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "Windows":
|
||||
return "Windows"
|
||||
|
||||
if system == "Darwin":
|
||||
return (
|
||||
"aarch64-apple-darwin" if machine == "arm64" else "x86_64-apple-darwin"
|
||||
)
|
||||
|
||||
if system == "Linux":
|
||||
return (
|
||||
"aarch64-unknown-linux-gnu"
|
||||
if machine in ("aarch64", "arm64")
|
||||
else "x86_64-unknown-linux-gnu"
|
||||
)
|
||||
|
||||
return "x86_64-unknown-linux-gnu"
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"youtube_cookie",
|
||||
None,
|
||||
"Cookie вашего Ютуб-аккаунта (повышает стабильность и помогает скачивать видео с жесткими возрастными ограничениями) | Cookie of your YouTube-account (increases stability and helps downloading video with strict age rating restricrions)",
|
||||
validator=loader.validators.Hidden(),
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
deno_path = Path("deno")
|
||||
deno_which = shutil.which("deno")
|
||||
|
||||
# Trying to fix previous shitcode...
|
||||
if self.get("deno_source") == "file":
|
||||
self.set("deno_source", str(deno_path.resolve()))
|
||||
|
||||
if not deno_which and not deno_path.is_file():
|
||||
logger.warning("Deno is not installed, attempting installation...")
|
||||
target = await self.get_target()
|
||||
if target == "Windows":
|
||||
logger.critical(
|
||||
"Windows platform is unsupported by this module. All future commands will fail. Please, unload the module."
|
||||
)
|
||||
return
|
||||
async with aiohttp.ClientSession() as session:
|
||||
download_link = f"https://github.com/denoland/deno/releases/latest/download/deno-{target}.zip"
|
||||
async with session.get(download_link) as resp:
|
||||
if resp.status == 200:
|
||||
async with aiofiles.open("deno.zip", mode="wb") as f:
|
||||
async for chunk in resp.content.iter_chunked(8192):
|
||||
await f.write(chunk)
|
||||
else:
|
||||
logger.critical(f"Failed to download Deno: HTTP {resp.status}")
|
||||
self.set("deno_source", "install_failed")
|
||||
return
|
||||
if Path("deno.zip").is_file():
|
||||
with zipfile.ZipFile("deno.zip", "r") as zip_ref:
|
||||
zip_ref.extractall()
|
||||
os.remove("deno.zip")
|
||||
os.chmod(deno_path, 0o755)
|
||||
self.set("deno_source", str(deno_path.resolve()))
|
||||
elif deno_which:
|
||||
self.set("deno_source", deno_which)
|
||||
|
||||
@loader.command(en_doc="Download video", ru_doc="Скачать видео")
|
||||
async def ytdlvcmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
if not args or not self._validate_url(args[0]) or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
|
||||
source = self.get("deno_source")
|
||||
if source == "install_failed" or not Path(source).is_file():
|
||||
logger.critical(
|
||||
"Deno wasn't installed in auto-mode. Please, install it manually or resolve the issue and reboot userbot."
|
||||
)
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings["downloading"])
|
||||
|
||||
filename_prefix = f"video_{message.id}"
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"outtmpl": f"{filename_prefix}.%(ext)s",
|
||||
"js_runtimes": {"deno": {"path": source}},
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegVideoConvertor",
|
||||
"preferedformat": "mp4",
|
||||
}
|
||||
],
|
||||
"postprocessor_args": {
|
||||
"video_convertor": [
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-preset",
|
||||
"veryfast",
|
||||
"-crf",
|
||||
"23",
|
||||
"-c:a",
|
||||
"aac",
|
||||
],
|
||||
"merger": ["-movflags", "faststart"],
|
||||
},
|
||||
}
|
||||
if self.get("youtube_cookie"):
|
||||
ydl_opts["cookiefile"] = self.get("youtube_cookie")
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(args[0], download=True)
|
||||
filename = ydl.prepare_filename(info).split(".")[0] + ".mp4"
|
||||
await utils.answer(message, self.strings['done'], file=filename, invert_media=True)
|
||||
os.remove(filename)
|
||||
|
||||
@loader.command(en_doc="Download audio", ru_doc="Скачать аудио")
|
||||
async def ytdlacmd(self, message):
|
||||
args = utils.get_args(message)
|
||||
if not args or not self._validate_url(args[0]) or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
|
||||
source = self.get("deno_source")
|
||||
if source == "install_failed" or not Path(source).is_file():
|
||||
logger.critical(
|
||||
"Deno wasn't installed in auto-mode. Please, install it manually or resolve the issue and reboot userbot."
|
||||
)
|
||||
return
|
||||
|
||||
await utils.answer(message, self.strings["downloading"])
|
||||
|
||||
filename_prefix = f"audio_{message.id}"
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"outtmpl": f"{filename_prefix}.%(ext)s",
|
||||
"js_runtimes": {"deno": {"path": source}},
|
||||
"postprocessors": [
|
||||
{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": "0",
|
||||
},
|
||||
{
|
||||
"key": "FFmpegMetadata",
|
||||
"add_metadata": True,
|
||||
},
|
||||
{
|
||||
"key": "EmbedThumbnail",
|
||||
},
|
||||
],
|
||||
"writethumbnail": True,
|
||||
}
|
||||
if self.get("youtube_cookie"):
|
||||
ydl_opts["cookiefile"] = self.get("youtube_cookie")
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(args[0], download=True)
|
||||
filename = ydl.prepare_filename(info).split(".")[0] + ".mp3"
|
||||
await utils.answer(message, self.strings['done'], file=filename)
|
||||
os.remove(filename)
|
||||
222
archquise/q.mods/UniversalDownloader.py
Normal file
222
archquise/q.mods/UniversalDownloader.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# █▀▀▄ █▀▄▀█ █▀█ █▀▄ █▀
|
||||
# ▀▀▀█ ▄ █ ▀ █ █▄█ █▄▀ ▄█
|
||||
|
||||
# #### Copyright (c) 2026 Archquise #####
|
||||
|
||||
# 💬 Contact: https://t.me/archquise
|
||||
# 🔒 Licensed under the GNU AGPLv3.
|
||||
# 📄 LICENSE: https://raw.githubusercontent.com/archquise/Q.Mods/main/LICENSE
|
||||
# ---------------------------------------------------------------------------------
|
||||
# Name: UniversalDownloader # noqa: ERA001
|
||||
# Description: Downloads media from YouTube, VK, TikTok, and all yt-dlp supported sites
|
||||
# Author: @quise_m
|
||||
# ---------------------------------------------------------------------------------
|
||||
# meta developer: @quise_m
|
||||
# meta banner: https://raw.githubusercontent.com/archquise/qmods_meta/main/UniversalDownloader.png
|
||||
# requires: yt_dlp ffmpeg
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from http import HTTPStatus
|
||||
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
from yt_dlp import YoutubeDL
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@loader.tds
|
||||
class UniversalDownloaderMod(loader.Module):
|
||||
"""Downloads media from YouTube, VK, TikTok, and all yt-dlp supported sites""" # noqa: D400, D415
|
||||
|
||||
strings = { # noqa: RUF012
|
||||
"name": "UniversalDownloader",
|
||||
"_cls_doc": "Downloads media from YouTube, VK, TikTok, and all yt-dlp supported sites", # noqa: E501
|
||||
"select_download_type": "<tg-emoji emoji-id=5879883461711367869>⬇️</tg-emoji> <b>Select download type:</b>", # noqa: E501
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or they are invalid", # noqa: E501
|
||||
"downloading": "<emoji document_id=5215484787325676090>🕐</emoji> Downloading...", # noqa: E501
|
||||
"cookie_desc": "Cookie account (helps downloading video with strict age rating restricrions)", # noqa: E501
|
||||
"deno_err": '<tg-emoji emoji-id=5879813604068298387>❗️</tg-emoji> <b>Error!</b> The <a href="http://deno.land/">Deno</a> JavaScript engine was not install automatically.\nThis is a required dependency for <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> (a library for downloading video/audio) to work correctly.\n\n<b>To continue, you need to install the engine manually, or resolve any issues preventing automatic installation and restart the userbot.</b>', # noqa: E501
|
||||
"err": "<tg-emoji emoji-id=5879813604068298387>❗️</tg-emoji> <b>Error!</b>\n\nAdditional info: {}", # noqa: E501
|
||||
"video": "video",
|
||||
"audio": "audio",
|
||||
}
|
||||
|
||||
strings_ru = { # noqa: RUF012
|
||||
"_cls_doc": "Скачивает медиа из YouTube, VK, TikTok и всех поддерживаемых yt-dlp сайтов", # noqa: E501
|
||||
"select_download_type": "<tg-emoji emoji-id=5879883461711367869>⬇️</tg-emoji> <b>Выберите тип загрузки:</b>", # noqa: E501
|
||||
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или они неверны", # noqa: E501
|
||||
"downloading": "<emoji document_id=5215484787325676090>🕐</emoji> Скачиваю...",
|
||||
"cookie_desc": "Куки аккаунта (помогает скачивать видео с жесткими возрастными ограничениями)", # noqa: E501, RUF001
|
||||
"deno_err": '<tg-emoji emoji-id=5879813604068298387>❗️</tg-emoji> <b>Ошибка!</b> JS-движок <a href="http://deno.land/">Deno</a> не установился автоматически.\nЭто необходимая зависимость для корректной работы <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> (библиотека для скачивания видео/аудио).\n\n<b>Для продолжения вам необходимо установить движок вручную, или устранить препятствия для автоматической установки и перезагрузить юзербота.</b>', # noqa: E501
|
||||
"err": "<tg-emoji emoji-id=5879813604068298387>❗️</tg-emoji> <b>Ошибка!</b>\n\nДоп.информация: {}", # noqa: E501, RUF001
|
||||
"video": "видео",
|
||||
"audio": "аудио",
|
||||
}
|
||||
|
||||
deno_error = (
|
||||
"Deno wasn't installed in auto-mode.",
|
||||
"Please, install it manually or resolve the issue and reboot userbot.",
|
||||
)
|
||||
|
||||
def _validate_url(self, url: str) -> bool:
|
||||
"""Validate URL format."""
|
||||
if not url:
|
||||
return False
|
||||
|
||||
url_pattern = re.compile(
|
||||
r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
return url_pattern.match(url) is not None
|
||||
|
||||
async def get_target(self) -> str:
|
||||
"""Check OS and processor architecture and return right postfix."""
|
||||
system = platform.system()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "Windows":
|
||||
return "Windows"
|
||||
|
||||
if system == "Darwin":
|
||||
return (
|
||||
"aarch64-apple-darwin" if machine == "arm64" else "x86_64-apple-darwin"
|
||||
)
|
||||
|
||||
if system == "Linux":
|
||||
return (
|
||||
"aarch64-unknown-linux-gnu"
|
||||
if machine in ("aarch64", "arm64")
|
||||
else "x86_64-unknown-linux-gnu"
|
||||
)
|
||||
|
||||
return "x86_64-unknown-linux-gnu"
|
||||
|
||||
def _get_deno(self) -> str | None:
|
||||
if not (source := self.get("deno_source")) or source == "install_failed" or not os.path.exists(source):
|
||||
logger.critical("%s %s", *self.deno_error)
|
||||
return None
|
||||
return source
|
||||
|
||||
|
||||
def __init__(self): # noqa: ANN204, D107
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"youtube_cookie",
|
||||
None,
|
||||
lambda: self.strings["cookie_desc"],
|
||||
validator=loader.validators.Hidden(),
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self, client, db): # noqa: ANN001, ANN201, D102, ARG002
|
||||
|
||||
deno_which = shutil.which("deno", path=os.environ.get("PATH", "") + os.pathsep + os.getcwd()) # noqa: E501
|
||||
|
||||
if deno_which:
|
||||
self.set("deno_source", deno_which)
|
||||
return
|
||||
|
||||
logger.warning("Deno is not installed, attempting installation...")
|
||||
target = await self.get_target()
|
||||
if target == "Windows":
|
||||
logger.critical(
|
||||
"Windows platform is unsupported, please, unload the module.",
|
||||
)
|
||||
return
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(60)) as session:
|
||||
download_link = f"https://github.com/denoland/deno/releases/latest/download/deno-{target}.zip"
|
||||
async with session.get(download_link) as resp:
|
||||
if resp.status == HTTPStatus.OK:
|
||||
async with aiofiles.open("deno.zip", mode="wb") as f:
|
||||
async for chunk in resp.content.iter_chunked(8192):
|
||||
await f.write(chunk)
|
||||
else:
|
||||
logger.critical("Failed to download Deno: HTTP %s", resp.status)
|
||||
self.set("deno_source", "install_failed")
|
||||
return
|
||||
if os.path.exists('deno.zip'):
|
||||
with zipfile.ZipFile("deno.zip", "r") as zip_ref:
|
||||
zip_ref.extractall()
|
||||
os.remove('deno.zip')
|
||||
os.chmod(path=os.path.join(os.getcwd(), "deno"), mode=0o755)
|
||||
self.set("deno_source", os.path.join(os.getcwd(), "deno"))
|
||||
return
|
||||
|
||||
@loader.command(en_doc="Download media", ru_doc="Скачать медиа")
|
||||
async def unidlcmd(self, message) -> None: # noqa: ANN001, D102
|
||||
args = utils.get_args(message)
|
||||
if not args or not self._validate_url(args[0]) or len(args) > 1:
|
||||
await utils.answer(message, self.strings["invalid_args"])
|
||||
return
|
||||
|
||||
async def _download_media(call, download_type: str) -> None:
|
||||
|
||||
if not (source := self._get_deno()):
|
||||
await call.edit(self.strings["deno_err"])
|
||||
return
|
||||
|
||||
await call.answer()
|
||||
await call.delete()
|
||||
|
||||
downloading_msg = await self._client.send_message(message.chat_id, self.strings["downloading"], reply_to=message.reply_to_msg_id) # noqa: E501
|
||||
|
||||
ydl_opts = {
|
||||
"quiet": True,
|
||||
"js_runtimes": {"deno": {"path": source}},
|
||||
}
|
||||
|
||||
if cookie := self.get("youtube_cookie"):
|
||||
ydl_opts["cookiefile"] = cookie
|
||||
|
||||
if download_type == "audio":
|
||||
ydl_opts["outtmpl"] = f"audio_{message.id}.%(ext)s"
|
||||
ydl_opts["format"] = "bestaudio/best"
|
||||
ydl_opts["postprocessors"] = [
|
||||
{
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": "mp3",
|
||||
"preferredquality": "0",
|
||||
},
|
||||
{
|
||||
"key": "FFmpegMetadata",
|
||||
"add_metadata": True,
|
||||
},
|
||||
{
|
||||
"key": "EmbedThumbnail",
|
||||
},
|
||||
]
|
||||
ydl_opts["writethumbnail"] = True
|
||||
|
||||
if download_type == "video":
|
||||
ydl_opts["outtmpl"] = f"video_{message.id}.%(ext)s"
|
||||
ydl_opts["format"] = "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" # noqa: E501
|
||||
ydl_opts["merge_output_format"] = "mp4"
|
||||
|
||||
try:
|
||||
with YoutubeDL(ydl_opts) as ydl:
|
||||
info = await utils.run_sync(lambda: ydl.extract_info(args[0], download=True)) # noqa: E501
|
||||
filename = ydl.prepare_filename(info).split(".")[0] + (".mp3" if download_type == "audio" else ".mp4") # noqa: E501
|
||||
await self._client.send_file(message.chat_id, filename, reply_to=message.reply_to_msg_id) # noqa: E501
|
||||
await downloading_msg.delete()
|
||||
except Exception as e:
|
||||
logger.exception("Catched error during download!")
|
||||
await call.answer()
|
||||
await downloading_msg.edit(self.strings["err"].format(e))
|
||||
finally:
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
|
||||
|
||||
call = await self.inline.form("🪐", message)
|
||||
await message.delete()
|
||||
await call.edit(self.strings["select_download_type"], reply_markup=[[{"text": self.strings["video"], "callback": _download_media, "args": ("video",)}, {"text": self.strings["audio"], "callback": _download_media, "args": ("audio",)}]]) # noqa: E501
|
||||
204
fiksofficial/python-modules/IwaAnimation.py
Normal file
204
fiksofficial/python-modules/IwaAnimation.py
Normal 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)
|
||||
@@ -26,4 +26,5 @@ aigenuser
|
||||
github
|
||||
stream
|
||||
placeholders+
|
||||
PyInstall
|
||||
PyInstall
|
||||
IwaAnimation
|
||||
957
modules.json
957
modules.json
File diff suppressed because it is too large
Load Diff
@@ -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"],
|
||||
@@ -243,7 +238,7 @@
|
||||
{
|
||||
"url": "https://github.com/Fixyres/FModules",
|
||||
"tags": ["herokutrusted"],
|
||||
"blacklist": ["FHeta.py"]
|
||||
"blacklist": ["FHeta.py", "LFSecurity.py"]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/Midga3/Heroku-modules",
|
||||
|
||||
186
update_diffs.py
186
update_diffs.py
@@ -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())
|
||||
Reference in New Issue
Block a user