diff --git a/archquise/H.Modules/.DS_Store b/archquise/H.Modules/.DS_Store
new file mode 100644
index 0000000..cf85313
Binary files /dev/null and b/archquise/H.Modules/.DS_Store differ
diff --git a/archquise/H.Modules/.gitignore b/archquise/H.Modules/.gitignore
index a4ed46c..4fb1dc3 100644
--- a/archquise/H.Modules/.gitignore
+++ b/archquise/H.Modules/.gitignore
@@ -3,4 +3,5 @@ autocleaner.py
silent.py
# Ruff Format
-.ruff_cache/
\ No newline at end of file
+.ruff_cache/
+.idea
diff --git a/archquise/H.Modules/ASCIIArt.py b/archquise/H.Modules/ASCIIArt.py
index b0b5551..96f992c 100644
--- a/archquise/H.Modules/ASCIIArt.py
+++ b/archquise/H.Modules/ASCIIArt.py
@@ -27,12 +27,15 @@
# requires: pillow
# ---------------------------------------------------------------------------------
+import logging
import os
import tempfile
from PIL import Image
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class ASCIIArtMod(loader.Module):
@@ -99,7 +102,7 @@ class ASCIIArtMod(loader.Module):
)
except Exception as e:
- print(f"Error generating ASCII art: {e}")
+ logger.error(f"Error generating ASCII art: {e}")
return None
finally:
if image_path and os.path.exists(image_path):
diff --git a/archquise/H.Modules/AccountData.py b/archquise/H.Modules/AccountData.py
index fc050c1..1aee3ca 100644
--- a/archquise/H.Modules/AccountData.py
+++ b/archquise/H.Modules/AccountData.py
@@ -26,15 +26,29 @@
# scope: Api AccountData 0.0.1
# ---------------------------------------------------------------------------------
+import logging
+from datetime import datetime
+
import aiohttp
-from datetime import datetime
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
@loader.tds
class AccountData(loader.Module):
"""Find out the approximate date of registration of the telegram account"""
+ def __init__(self):
+ self.config = loader.ModuleConfig(
+ loader.ConfigValue(
+ "api_token",
+ "7518491974:1ea2284eec9dc40a9838cfbcb48a2b36",
+ "API token for datereg.pro",
+ validator=loader.validators.String(),
+ )
+ )
+
strings = {
"name": "AccountData",
"_cls_doc": "Find out the approximate date of registration of the telegram account",
@@ -51,7 +65,10 @@ class AccountData(loader.Module):
}
async def get_creation_date(self, user_id: int) -> str:
- api_token = "7518491974:1ea2284eec9dc40a9838cfbcb48a2b36"
+ api_token = self.config.get("api_token", "")
+ if not api_token:
+ return {"error": "API token not configured"}
+
url = "https://api.datereg.pro/api/v1/users/getCreationDateFast"
params = {"token": api_token, "user_id": user_id}
@@ -76,16 +93,22 @@ class AccountData(loader.Module):
async def accdata(self, message):
if reply := await message.get_reply_message():
result = await self.get_creation_date(user_id=reply.sender.id)
- month, year = map(int, result['creation_date'].split('.'))
- date_object = datetime(year, month, 1)
- formatted = date_object.strftime('%B %Y')
-
- if "error" in result:
- await utils.answer(message, f"Ошибка: {result['error']}")
- else:
+
+ if "error" in result or not result.get("creation_date"):
+ error_msg = result.get("error", "Unknown error occurred")
+ await utils.answer(message, f"Ошибка: {error_msg}")
+ return
+
+ try:
+ month, year = map(int, result['creation_date'].split('.'))
+ date_object = datetime(year, month, 1)
+ formatted = date_object.strftime('%B %Y')
+
await utils.answer(
message,
f"{self.strings('date_text').format(data=formatted, accuracy=result['accuracy_percent'])}\n\n{self.strings('date_text_ps')}",
)
+ except (ValueError, KeyError) as e:
+ await utils.answer(message, f"Ошибка обработки данных: {str(e)}")
else:
await utils.answer(message, self.strings("no_reply"))
diff --git a/archquise/H.Modules/AniLiberty.py b/archquise/H.Modules/AniLiberty.py
new file mode 100644
index 0000000..801e58f
--- /dev/null
+++ b/archquise/H.Modules/AniLiberty.py
@@ -0,0 +1,243 @@
+# 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": "The announcement:",
+ "ongoing": "Ongoing:",
+ "type": "Type:",
+ "genres": "Genres:",
+ "favorite": "Favourites <3:", # < == <
+ }
+
+ strings_ru = {
+ "announce": "Анонс:",
+ "ongoing": "Онгоинг:",
+ "type": "Тип:",
+ "genres": "Жанры:",
+ "favorite": "Избранное <3:", # < == <
+ }
+
+ async def search_title(self, query):
+ async with aiohttp.ClientSession() as 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):
+ async with aiohttp.ClientSession() as 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):
+ async with aiohttp.ClientSession() as session:
+ async with session.get(f'{BASE_API_URL}/anime/releases/random?limit=1&include=id') as resp:
+ randid = await resp.json()
+ """
+ Приходится запрашивать по второму кругу, т.к. API в рандомных релизах не отдает жанры, даже если попросить через include
+ """
+ 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"{anime_release.description}\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"{anime_release.description}\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"{anime_release.description}\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,
+ )
diff --git a/archquise/H.Modules/AniLibria.py b/archquise/H.Modules/AniLibria.py
deleted file mode 100644
index 6d81630..0000000
--- a/archquise/H.Modules/AniLibria.py
+++ /dev/null
@@ -1,187 +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: AniLibria
-# Description: Searches and gives random agtme on the AniLibria database.
-# Author: @hikka_mods
-# ---------------------------------------------------------------------------------
-# meta developer: @hikka_mods
-# scope: AniLibria
-# scope: AniLibria 0.0.1
-# requires: git+https://github.com/C0dwiz/anilibria.py.git
-# ---------------------------------------------------------------------------------
-
-from ..inline.types import InlineQuery
-from aiogram.types import InlineQueryResultPhoto, CallbackQuery
-from anilibria import AniLibriaClient
-
-from .. import loader
-
-ani_client = AniLibriaClient()
-
-
-@loader.tds
-class AniLibriaMod(loader.Module):
- """Searches and gives random agtme on the AniLibria database."""
-
- strings = {
- "name": "AniLibria",
- "announce": "The announcement:",
- "status": "Status:",
- "type": "Type:",
- "genres": "Genres:",
- "favorite": "Favourites <3:", # < == <
- "season": "Season:",
- }
-
- strings_ru = {
- "announce": "Анонс:",
- "status": "Статус:",
- "type": "Тип:",
- "genres": "Жанры:",
- "favorite": "Избранное <3:", # < == <
- "season": "Сезон:",
- }
-
- link = "https://anilibria.tv"
-
- @loader.command(
- ru_doc="Возвращает случайный тайтл из базы",
- en_doc="Returns a random title from the database",
- )
- async def arandom(self, message) -> None:
- anime_title = await ani_client.get_random_title()
-
- text = f"{anime_title.names.ru} \n"
- text += f"{self.strings['status']} {anime_title.status.string}\n\n"
- text += f"{self.strings['type']} {anime_title.type.full_string}\n"
- text += f"{self.strings['season']} {anime_title.season.string}\n"
- text += f"{self.strings['genres']} {' '.join(anime_title.genres)}\n\n"
-
- text += f"{anime_title.description}\n\n"
- text += f"{self.strings['favorite']} {anime_title.in_favorites}"
-
- kb = [
- [
- {
- "text": "Ссылка",
- "url": f"https://anilibria.tv/release/{anime_title.code}.html",
- }
- ]
- ]
-
- kb.extend(
- [
- {
- "text": f"{torrent.quality.string}",
- "url": f"https://anilibria.tv/{torrent.url}",
- }
- ]
- for torrent in anime_title.torrents.list
- )
- kb.append([{"text": "🔃 Обновить", "callback": self.inline__update}])
- kb.append([{"text": "🚫 Закрыть", "callback": self.inline__close}])
-
- await self.inline.form(
- text=text,
- photo=self.link + anime_title.posters.original.url,
- 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_titles = await ani_client.search_titles(search=text)
-
- inline_query = []
- for anime_title in anime_titles:
- title_text = (
- f"{anime_title.names.ru} | {anime_title.names.en}\n"
- f"{self.strings['status']} {anime_title.status.string}\n\n"
- f"{self.strings['type']} {anime_title.type.full_string}\n"
- f"{self.strings['season']} {anime_title.season.string} {anime_title.season.year}\n"
- f"{self.strings['genres']} {' '.join(anime_title.genres)}\n\n"
- f"{anime_title.description}\n\n"
- f"{self.strings['favorite']} {anime_title.in_favorites}"
- )
-
- inline_query.append(
- InlineQueryResultPhoto(
- id=str(anime_title.code),
- title=anime_title.names.ru,
- description=anime_title.type.full_string,
- caption=title_text,
- thumb_url=self.link + anime_title.posters.small.url,
- photo_url=self.link + anime_title.posters.original.url,
- parse_mode="html",
- )
- )
- await query.answer(inline_query, cache_time=0)
-
- async def inline__close(self, call: CallbackQuery) -> None:
- await call.delete()
-
- async def inline__update(self, call: CallbackQuery) -> None:
- anime_title = await ani_client.get_random_title()
-
- text = (
- f"{anime_title.names.ru} \n"
- f"{self.strings['status']} {anime_title.status.string}\n\n"
- f"{self.strings['type']} {anime_title.type.full_string}\n"
- f"{self.strings['season']} {anime_title.season.string}\n"
- f"{self.strings['genres']} {' '.join(anime_title.genres)}\n\n"
- f"{anime_title.description}\n\n"
- f"{self.strings['favorite']} {anime_title.in_favorites}"
- )
-
- kb = [
- [
- {
- "text": "Ссылка",
- "url": f"https://anilibria.tv/release/{anime_title.code}.html",
- }
- ]
- ]
-
- kb.extend(
- [
- {
- "text": f"{torrent.quality.string}",
- "url": f"https://anilibria.tv/{torrent.url}",
- }
- ]
- for torrent in anime_title.torrents.list
- )
- kb.append([{"text": "🔃 Обновить", "callback": self.inline__update}])
- kb.append([{"text": "🚫 Закрыть", "callback": self.inline__close}])
-
- await call.edit(
- text=text,
- photo=self.link + anime_title.posters.original.url,
- reply_markup=kb,
- )
diff --git a/archquise/H.Modules/AnimeQuotes.py b/archquise/H.Modules/AnimeQuotes.py
index 0908122..c06edb0 100644
--- a/archquise/H.Modules/AnimeQuotes.py
+++ b/archquise/H.Modules/AnimeQuotes.py
@@ -27,10 +27,13 @@
# requires: requests
# ---------------------------------------------------------------------------------
+import logging
+
import aiohttp
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class AnimeQuotesMod(loader.Module):
diff --git a/archquise/H.Modules/Article.py b/archquise/H.Modules/Article.py
index 341d0c9..67a81d0 100644
--- a/archquise/H.Modules/Article.py
+++ b/archquise/H.Modules/Article.py
@@ -27,13 +27,16 @@
# requires: requests
# ---------------------------------------------------------------------------------
-import requests
import json
+import logging
import random
from typing import Dict
+import requests
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class ArticleMod(loader.Module):
diff --git a/archquise/H.Modules/AutofarmCookies.py b/archquise/H.Modules/AutofarmCookies.py
index 840d6cb..44735e2 100644
--- a/archquise/H.Modules/AutofarmCookies.py
+++ b/archquise/H.Modules/AutofarmCookies.py
@@ -26,15 +26,17 @@
# scope: AutofarmCookies 0.0.1
# ---------------------------------------------------------------------------------
+import logging
import random
-
from datetime import timedelta
+
from telethon import functions
+from telethon.tl.custom import Message
from .. import loader, utils
__version__ = (1, 0, 0)
-
+logger = logging.getLogger(__name__)
@loader.tds
class AutofarmCookiesMod(loader.Module):
@@ -197,8 +199,7 @@ class AutofarmCookiesMod(loader.Module):
async def ckies(self, message):
chelp = """
🍀| Помощь по командам:
- .cookon - Включает авто фарм.
- .cookoff - Выключает авто фарм.
- .farm - Показывает сколько вы нафармили.
- .me - Показывает ваш ммешок"""
+ .cookon - Включает авто-фарм.
+ .cookoff - Выключает авто-фарм.
+ .me - Показывает ваш мешок"""
await utils.answer(message, chelp)
diff --git a/archquise/H.Modules/BirthdayTime.py b/archquise/H.Modules/BirthdayTime.py
index 085c760..8328fcc 100644
--- a/archquise/H.Modules/BirthdayTime.py
+++ b/archquise/H.Modules/BirthdayTime.py
@@ -26,17 +26,20 @@
# scope: Api BirthdayTime 0.0.1
# ---------------------------------------------------------------------------------
-import random
import asyncio
import calendar
+import logging
+import random
from datetime import datetime
-from telethon.tl.functions.users import GetFullUserRequest
-from telethon.tl.functions.account import UpdateProfileRequest
from telethon.errors.rpcerrorlist import UserPrivacyRestrictedError
+from telethon.tl.functions.account import UpdateProfileRequest
+from telethon.tl.functions.users import GetFullUserRequest
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
D_MSG = [
"Ждешь его?",
"Осталось немного)",
@@ -158,9 +161,9 @@ class DaysToMyBirthday(loader.Module):
self.db.set(__name__, "last_name", name)
except UserPrivacyRestrictedError:
self.db.set(__name__, "change_name", False)
- print("Error: Can't change name due to privacy settings.")
+ logger.error("Error: Can't change name due to privacy settings.")
except Exception as e:
- print(f"Error in checker: {e}")
+ logger.error(f"Error in checker: {e}")
finally:
await asyncio.sleep(60)
@@ -173,7 +176,7 @@ class DaysToMyBirthday(loader.Module):
user = await self.client(GetFullUserRequest(self.client.hikka_me.id))
name = user.users[0].last_name or ""
except Exception as e:
- print(f"Error getting user info: {e}")
+ logger.error(f"Error getting user info: {e}")
await utils.answer(message, self.strings("error"))
return
@@ -191,7 +194,7 @@ class DaysToMyBirthday(loader.Module):
except UserPrivacyRestrictedError:
await utils.answer(message, self.strings("name_privacy_error"))
except Exception as e:
- print(f"Error removing name: {e}")
+ logger.error(f"Error removing name: {e}")
await utils.answer(message, self.strings("error"))
else:
@@ -238,5 +241,5 @@ class DaysToMyBirthday(loader.Module):
)
except Exception as e:
- print(f"Error in bt command: {e}")
+ logger.error(f"Error in bt command: {e}")
await utils.answer(message, self.strings("error"))
diff --git a/archquise/H.Modules/CodeShare.py b/archquise/H.Modules/CodeShare.py
new file mode 100644
index 0000000..ede6423
--- /dev/null
+++ b/archquise/H.Modules/CodeShare.py
@@ -0,0 +1,94 @@
+# 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 .. 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": "❌ 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": "✅ Code uploaded! Link: {}",
+ }
+
+ strings_ru = {
+ "_cls_doc": "Загружает ваш код на kmi.aeza.net (альтернатива Pastebin и GitHub Gist)",
+ "invalid_args": "❌ Нет аргументов или реплая с файлом, или они неверны",
+ "link_ready": "✅ Код загружен! Ссылка: {}",
+ }
+
+ async def upload_to_kmi(self, content: str) -> str:
+ url = "https://kmi.aeza.net"
+ data = aiohttp.FormData()
+ data.add_field('kmi', content)
+
+ async with aiohttp.ClientSession() as 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
+
+ @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)
+ os.remove(file_name)
+ await utils.answer(message, self.strings['link_ready'].format(link))
+ return
+ await utils.answer(message, self.strings['invalid_args'])
+
+
+
+
+
+
diff --git a/archquise/H.Modules/CryptoCurrency.py b/archquise/H.Modules/CryptoCurrency.py
index 3fbcf84..488347f 100644
--- a/archquise/H.Modules/CryptoCurrency.py
+++ b/archquise/H.Modules/CryptoCurrency.py
@@ -26,10 +26,13 @@
# scope: Api CryptoCurrency 0.0.1
# ---------------------------------------------------------------------------------
+import logging
+
import aiohttp
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class CryptoCurrencyMod(loader.Module):
diff --git a/archquise/H.Modules/EmojiStickerBlocker.py b/archquise/H.Modules/EmojiStickerBlocker.py
new file mode 100644
index 0000000..953d5bc
--- /dev/null
+++ b/archquise/H.Modules/EmojiStickerBlocker.py
@@ -0,0 +1,337 @@
+# Proprietary License Agreement
+
+# Copyright (c) 2024-29 CodWiz
+
+# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
+
+# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
+
+# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
+
+# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
+
+# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
+
+# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
+
+# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
+
+# ---------------------------------------------------------------------------------
+# Name: EmojiStickerBlocker
+# Description: Block emojis, stickers and sticker packs
+# Author: @hikka_mods
+# ---------------------------------------------------------------------------------
+# meta developer: @hikka_mods
+# scope: EmojiStickerBlocker
+# scope: EmojiStickerBlocker0.0.1
+# ---------------------------------------------------------------------------------
+
+import logging
+import re
+from typing import Optional, Set
+
+from telethon.errors import FloodWaitError, MessageDeleteForbiddenError
+from telethon.tl.functions.messages import DeleteMessagesRequest
+from telethon.tl.types import Message, MessageMediaDocument
+
+from .. import loader, utils
+
+logger = logging.getLogger(__name__)
+
+
+@loader.tds
+class EmojiStickerBlocker(loader.Module):
+ """Block emojis, stickers and sticker packs with enhanced functionality"""
+
+ strings = {
+ "name": "EmojiStickerBlocker",
+ "no_permission": "❌ Need delete messages permission",
+ "pack_blocked": "✅ Pack blocked",
+ "pack_not_found": "❌ Pack not found",
+ "sticker_blocked": "❌ Sticker blocked",
+ "emoji_blocked": "❌ Emoji blocked",
+ "pack_unblocked": "✅ Pack unblocked",
+ "item_unblocked": "❌ Item unblocked",
+ "not_found": "❌ Not in blocklist",
+ "no_reply": "❌ Reply to a sticker or emoji",
+ "no_args": "❌ Specify pack link or name",
+ "list_packs": "📦 Blocked packs: {}",
+ "list_stickers": "🖼 Blocked stickers: {}",
+ "list_emojis": "😀 Blocked emojis: {}",
+ "all_cleared": "✅ All blocks cleared",
+ }
+
+ strings_ru = {
+ "no_permission": "❌ Нужны права на удаление сообщений",
+ "pack_blocked": "✅ Пак заблокирован",
+ "pack_not_found": "❌ Пак не найден",
+ "sticker_blocked": "❌ Стикер заблокирован",
+ "emoji_blocked": "❌ Эмодзи заблокирован",
+ "pack_unblocked": "✅ Пак разблокирован",
+ "item_unblocked": "❌ Элемент разблокирован",
+ "not_found": "❌ Не найден в блоклисте",
+ "no_reply": "❌ Ответьте на стикер или эмодзи",
+ "no_args": "❌ Укажите ссылку или название пака",
+ "list_packs": "📦 Заблокированные паки: {}",
+ "list_stickers": "🖼 Заблокированные стикеры: {}",
+ "list_emojis": "😀 Заблокированные эмодзи: {}",
+ "all_cleared": "✅ Все блоки очищены",
+ }
+
+ def __init__(self):
+ self.blocked_packs: Set[str] = set()
+ self.blocked_stickers: Set[str] = set()
+ self.blocked_emojis: Set[str] = set()
+
+ self._load_blocklists()
+
+ def _load_blocklists(self):
+ """Load blocklists from database"""
+ self.blocked_packs = set(self._db.get(__name__, "blocked_packs", []))
+ self.blocked_stickers = set(self._db.get(__name__, "blocked_stickers", []))
+ self.blocked_emojis = set(self._db.get(__name__, "blocked_emojis", []))
+
+ def _save_blocklists(self):
+ """Save blocklists to database"""
+ self._db.set(__name__, "blocked_packs", list(self.blocked_packs))
+ self._db.set(__name__, "blocked_stickers", list(self.blocked_stickers))
+ self._db.set(__name__, "blocked_emojis", list(self.blocked_emojis))
+
+ def _extract_pack_name(self, message: Message) -> Optional[str]:
+ """Extract pack name from sticker or emoji"""
+ if not message.media:
+ return None
+
+ if message.sticker:
+ if hasattr(message.sticker, "set_name") and message.sticker.set_name:
+ return message.sticker.set_name.lower()
+
+ if isinstance(message.media, MessageMediaDocument):
+ if hasattr(message.media.document, "attributes"):
+ for attr in message.media.document.attributes:
+ if hasattr(attr, "stickerset") and attr.stickerset:
+ return attr.stickerset.title.lower()
+
+ return None
+
+ def _extract_emoji_text(self, message: Message) -> Optional[str]:
+ """Extract emoji text from message"""
+ if not message.message:
+ return None
+
+ emoji_pattern = re.compile(
+ r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F1E0-\U0001F1FF\U00002702-\U000027B0\U000024C2-\U0001F251]"
+ )
+ emojis = emoji_pattern.findall(message.message)
+
+ if emojis:
+ return emojis[0]
+ else:
+ return None
+
+ async def _delete_message(self, message: Message) -> bool:
+ """Delete message with error handling"""
+ try:
+ await self._client(DeleteMessagesRequest([message.id]))
+ return True
+ except MessageDeleteForbiddenError:
+ await utils.answer(message, self.strings["no_permission"])
+ return False
+ except FloodWaitError as e:
+ logger.warning(f"Flood wait when deleting message: {e}")
+ return False
+ except Exception as e:
+ logger.error(f"Error deleting message: {e}")
+ return False
+
+ async def _should_block_message(self, message: Message) -> tuple[bool, str]:
+ """Check if message should be blocked and return reason"""
+ pack_name = self._extract_pack_name(message)
+ emoji_text = self._extract_emoji_text(message)
+
+ if pack_name and pack_name in self.blocked_packs:
+ return True, f"pack: {pack_name}"
+
+ if message.sticker:
+ sticker_id = str(message.sticker.id)
+ if sticker_id in self.blocked_stickers:
+ return True, f"sticker: {sticker_id}"
+
+ if emoji_text and emoji_text in self.blocked_emojis:
+ return True, f"emoji: {emoji_text}"
+
+ return False, ""
+
+ @loader.command(
+ ru_doc="[link/название пака] — блокирует эмодзипак/стикерпак в личных сообщениях",
+ en_doc="[link/pack name] — block emoji pack/sticker pack in private messages",
+ )
+ async def packblock(self, message: Message):
+ """Block emoji pack/sticker pack"""
+ args = utils.get_args_raw(message)
+ if not args:
+ return await utils.answer(message, self.strings["no_args"])
+
+ pack_name = args.lower().strip()
+
+ # Add to blocked packs
+ self.blocked_packs.add(pack_name)
+ self._save_blocklists()
+
+ await utils.answer(message, self.strings["pack_blocked"])
+
+ @loader.command(
+ ru_doc="[reply] — блокирует определенный стикер",
+ en_doc="[reply] — block specific sticker",
+ )
+ async def stickblock(self, message: Message):
+ """Block sticker from reply"""
+ if not message.is_reply:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ reply_msg = await message.get_reply_message()
+ if not reply_msg or not reply_msg.sticker:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ sticker_id = str(reply_msg.sticker.id)
+ self.blocked_stickers.add(sticker_id)
+ self._save_blocklists()
+
+ await utils.answer(message, self.strings["sticker_blocked"])
+
+ @loader.command(
+ ru_doc="[reply/enter] — блокирует определенное эмодзи",
+ en_doc="[reply/enter] — block specific emoji",
+ )
+ async def emojiblock(self, message: Message):
+ """Block emoji from reply or input"""
+ args = utils.get_args_raw(message)
+
+ if args:
+ emoji_text = args.strip()
+ if not emoji_text:
+ return await utils.answer(message, self.strings["no_args"])
+ else:
+ if not message.is_reply:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ reply_msg = await message.get_reply_message()
+ if not reply_msg:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ emoji_text = self._extract_emoji_text(reply_msg)
+ if not emoji_text:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ self.blocked_emojis.add(emoji_text)
+ self._save_blocklists()
+
+ await utils.answer(message, self.strings["emoji_blocked"])
+
+ @loader.command(
+ ru_doc="— снимает блокировку с эмодзипака/стикерпака",
+ en_doc="— unblock emoji pack/sticker pack",
+ )
+ async def ublpack(self, message: Message):
+ """Unblock emoji pack/sticker pack"""
+ args = utils.get_args_raw(message)
+ if not args:
+ return await utils.answer(message, self.strings["no_args"])
+
+ pack_name = args.lower().strip()
+
+ if pack_name in self.blocked_packs:
+ self.blocked_packs.remove(pack_name)
+ self._save_blocklists()
+ await utils.answer(message, self.strings["pack_unblocked"])
+ else:
+ await utils.answer(message, self.strings["not_found"])
+
+ @loader.command(
+ ru_doc="[reply/enter] — снимает блокировку с определенного эмодзи/стикера",
+ en_doc="[reply/enter] — unblock specific emoji/sticker",
+ )
+ async def ublthis(self, message: Message):
+ """Unblock emoji/sticker from reply or input"""
+ args = utils.get_args_raw(message)
+
+ if args:
+ item = args.strip()
+ if not item:
+ return await utils.answer(message, self.strings["no_args"])
+ else:
+ if not message.is_reply:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ reply_msg = await message.get_reply_message()
+ if not reply_msg:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ if reply_msg.sticker:
+ item = str(reply_msg.sticker.id)
+ else:
+ item = self._extract_emoji_text(reply_msg)
+
+ if not item:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ unblocked = False
+ if item in self.blocked_stickers:
+ self.blocked_stickers.remove(item)
+ unblocked = True
+ if item in self.blocked_emojis:
+ self.blocked_emojis.remove(item)
+ unblocked = True
+
+ if unblocked:
+ self._save_blocklists()
+ await utils.answer(message, self.strings["item_unblocked"])
+ else:
+ await utils.answer(message, self.strings["not_found"])
+
+ @loader.command(
+ ru_doc="— показать список заблокированных паков/стикеров/эмодзи",
+ en_doc="— show list of blocked packs/stickers/emojis",
+ )
+ async def blocklist(self, message: Message):
+ """Show blocklist"""
+ packs_list = ", ".join(self.blocked_packs) if self.blocked_packs else "нет"
+ stickers_list = (
+ ", ".join(self.blocked_stickers) if self.blocked_stickers else "нет"
+ )
+ emojis_list = ", ".join(self.blocked_emojis) if self.blocked_emojis else "нет"
+
+ result = []
+ if packs_list:
+ result.append(self.strings["list_packs"].format(packs_list))
+ if stickers_list:
+ result.append(self.strings["list_stickers"].format(stickers_list))
+ if emojis_list:
+ result.append(self.strings["list_emojis"].format(emojis_list))
+
+ if result:
+ await utils.answer(message, "\n".join(result))
+ else:
+ await utils.answer(message, self.strings["all_cleared"])
+
+ @loader.command(ru_doc="— очистить все блокировки", en_doc="— clear all blocks")
+ async def clearblocks(self, message: Message):
+ """Clear all blocks"""
+ self.blocked_packs.clear()
+ self.blocked_stickers.clear()
+ self.blocked_emojis.clear()
+ self._save_blocklists()
+
+ await utils.answer(message, self.strings["all_cleared"])
+
+ async def watcher(self, message: Message):
+ """Monitor messages and block unwanted content"""
+
+ if message.is_group or message.is_channel:
+ return
+
+ should_block, reason = await self._should_block_message(message)
+
+ if should_block:
+ logger.info(f"Blocking message: {reason}")
+ await self._delete_message(message)
diff --git a/archquise/H.Modules/EnvsSH.py b/archquise/H.Modules/EnvsSH.py
deleted file mode 100644
index 174fc9b..0000000
--- a/archquise/H.Modules/EnvsSH.py
+++ /dev/null
@@ -1,83 +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: EnvsSH
-# Description: Module for reuploading files to envs.sh
-# Author: @hikka_mods
-# ---------------------------------------------------------------------------------
-# meta developer: @hikka_mods
-# scope: Api EnvsSH
-# scope: Api EnvsSH 0.0.1
-# requires: aiohttp
-# ---------------------------------------------------------------------------------
-
-import aiohttp
-
-from .. import loader, utils # pylint: disable=relative-beyond-top-level
-
-
-@loader.tds
-class EnvsMod(loader.Module):
- """Module for reuploading files to envs.sh"""
-
- strings = {
- "name": "EnvsSH",
- "connection_error": "🚫 Host is unreachable for now, try again later.",
- "no_reply": "⚠️ You must reply to a message with media",
- "success": "✅ URL for {}:\n\n{}",
- "error": "❌ An error occurred:\n{}",
- "uploading": "⏳ Uploading {} ({}{})...",
- }
-
- strings_ru = {
- "connection_error": "🚫 Хост в настоящее время недоступен, попробуйте позже.",
- "no_reply": "⚠️ Вы должны ответить на сообщение с медиа",
- "success": "✅ URL для {}:\n\n{}",
- "error": "❌ Произошла ошибка:\n{}",
- "uploading": "⏳ Загрузка {} ({}{})...",
- }
-
- async def client_ready(self, client, db):
- self.hmodslib = await self.import_lib(
- "https://files.archquise.ru/HModsLibrary.py"
- )
-
- async def envcmd(self, message):
- """Reupload to envs.sh."""
- reply = await message.get_reply_message()
- if not reply or not reply.media:
- return await utils.answer(message, self.strings["no_reply"])
-
- size_len, size_unit = self.hmodslib.convert_size(reply.file.size)
- await utils.answer(
- message,
- self.strings["uploading"].format(reply.file.name, size_len, size_unit),
- )
-
- path = await self.client.download_media(reply)
- try:
- uploaded_url = await self.hmodslib.upload_to_envs(path)
- except aiohttp.ClientConnectionError:
- await utils.answer(message, self.strings["connection_error"])
- except aiohttp.ClientResponseError as e:
- await utils.answer(message, self.strings["error"].format(str(e)))
- else:
- await utils.answer(
- message, self.strings["success"].format(path, uploaded_url)
- )
diff --git a/archquise/H.Modules/FakeActions.py b/archquise/H.Modules/FakeActions.py
index 99b687c..e2a24e0 100644
--- a/archquise/H.Modules/FakeActions.py
+++ b/archquise/H.Modules/FakeActions.py
@@ -27,9 +27,11 @@
# ---------------------------------------------------------------------------------
import asyncio
+import logging
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class FakeActionsMod(loader.Module):
diff --git a/archquise/H.Modules/FakeWallet.py b/archquise/H.Modules/FakeWallet.py
index 0920731..289811a 100644
--- a/archquise/H.Modules/FakeWallet.py
+++ b/archquise/H.Modules/FakeWallet.py
@@ -27,8 +27,11 @@
# scope: hikka_min 1.4.2
# -----------------------------------------------------------------------------------
+import logging
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class FakeWallet(loader.Module):
diff --git a/archquise/H.Modules/FolderAutoRead.py b/archquise/H.Modules/FolderAutoRead.py
index ea0fb9c..40810ba 100644
--- a/archquise/H.Modules/FolderAutoRead.py
+++ b/archquise/H.Modules/FolderAutoRead.py
@@ -23,8 +23,8 @@
# meta developer: @hikka_mods
# ---------------------------------------------------------------------------------
-import os
import logging
+
from telethon import functions
from telethon.tl.types import DialogFilter, InputPeerChannel
diff --git a/archquise/H.Modules/GigaChat.py b/archquise/H.Modules/GigaChat.py
index 521e0e4..2ba5d25 100644
--- a/archquise/H.Modules/GigaChat.py
+++ b/archquise/H.Modules/GigaChat.py
@@ -26,8 +26,11 @@
# scope: Api GigaChat 0.0.1
# ---------------------------------------------------------------------------------
+import logging
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class GigaChatMod(loader.Module):
diff --git a/archquise/H.Modules/HAFK.py b/archquise/H.Modules/HAFK.py
index 3f0f94c..fb5b020 100644
--- a/archquise/H.Modules/HAFK.py
+++ b/archquise/H.Modules/HAFK.py
@@ -26,10 +26,10 @@
# scope: HAFK 0.0.1
# ---------------------------------------------------------------------------------
+import asyncio
import datetime
import logging
import time
-import asyncio
from telethon import types
from telethon.utils import get_peer_id
@@ -135,7 +135,6 @@ class HAFK(loader.Module):
async def _afk_toggle(self, message, global_afk: bool):
chat_id = utils.get_chat_id(message)
- db_key = "afk" if global_afk else f"afk_here_{chat_id}"
already_afk_string = "already_afk" if global_afk else "already_afk_here"
afk_on_string = "afk_on" if global_afk else "afk_here_on"
afk_on_reason_string = "afk_on_reason" if global_afk else "afk_here_on_reason"
diff --git a/archquise/H.Modules/HInstall.py b/archquise/H.Modules/HInstall.py
new file mode 100644
index 0000000..58a8fc5
--- /dev/null
+++ b/archquise/H.Modules/HInstall.py
@@ -0,0 +1,107 @@
+# 🔐 Licensed under the GNU AGPLv3.
+# ---------------------------------------------------------------------------------
+# Name: HInstall
+# Description: Provides H:Mods modules installation trough buttons
+# Author: @hikka_mods
+# ---------------------------------------------------------------------------------
+# meta developer: @hikka_mods
+# requires: PyCryptodome
+# ---------------------------------------------------------------------------------
+# #################################################################################
+# ########## This module is based on @hikariatama 's hikkamods_socket!! ###########
+# #################################################################################
+
+
+__version__ = (1, 0, 0)
+
+import base64
+import logging
+
+from Crypto.PublicKey import RSA
+from Crypto.Hash import SHA256
+from Crypto.Signature import pkcs1_15
+
+from telethon.tl.types import Message
+from telethon import functions, types
+from typing import Optional
+
+from .. import loader, utils
+
+logger = logging.getLogger(__name__)
+
+pubkey_data = """
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvekpGqKiD2HZwY/J7jZv
+PwGRobAS2TaC9HU5LUNRDg90jA/r8xgoFhlCBJocq8+XvJIWpgmIEYWJCz0KpCXu
+Meu42bAXvLqniDOqnOt8FjXFapGZvEMLen1CLCRr1OQhVNpRlPjjWo7PM+YpUnbw
+giqEZ9nA5DQ5Gi0vsSHXAnBa+ZIsxaY3EwosHMvUUhnnijcbBpkyYRJ8atvsT9AX
+cNS+NjDE4Kj8jSnArQ1D1Ct1pcZEXD6DUk2k3HAD4OlZS5nY5IFchWEcpLT/Fjbt
+BzGBZCJZ+rp8qR1tCVvVTV3itACc8O0Pirmptkrxb3A4pC0S8oxYBFQcnZAlIiw3
+uX36O90AkRwbsdnsp2JVg5AAPUYvdsMoCGG+cSGZC73arqcrvn0VFo7EhsYq/1Ds
+CevorFI4TiLVbSlFSVnX5baqmTj+XNhgaWWmiY/+mhErzsWtpCOHYFitf1xqp3zD
+9O2Vs7lQIxMsHFISAEhn8BqQxvlwslfcjmbuJxkYriqAHXQGS3IZDXhEZXwouOUV
+HGN2YD5aLK0L8OuTNY5cf1TN8C5xgVZoEodAKqAva/i1v/F6IQk3iEo0ncgypeyg
+NM1TUudkQ+f1wXqLj2YaVKqRdKswl9vgYpUCHjGZfN+WYT4DbOMrJm1OFeen6geo
+xqON1/xeRBgkE3tna3RuhmUCAwEAAQ==
+-----END PUBLIC KEY-----
+"""
+
+pubkey = RSA.importKey(pubkey_data.strip())
+
+
+@loader.tds
+class HInstallMod(loader.Module):
+ """Provides H:Mods modules installation trough buttons"""
+
+ strings = {
+ "name": "HInstall",
+ "_cls_doc": "Provides H:Mods modules installation trough buttons",
+ "module_downloaded": "Module downloaded!"
+ }
+
+ strings_ru = {
+ "_cls_doc": "Позволяет устанавливать модули от H:Mods через кнопки",
+ "module_downloaded": "Модуль загружен!"
+ }
+
+ async def on_dlmod(self, client, db):
+ ent = await self.client(functions.users.GetFullUserRequest('@hinstall_bot'))
+ if ent.full_user.blocked:
+ await self.client(functions.contacts.UnblockRequest('@hinstall_bot'))
+ await self.client.send_message('@hinstall_bot', '/start')
+ await self.client.delete_dialog('@hinstall_bot')
+
+
+ async def _load_module(self, url: str, message: Optional[Message] = None):
+ loader_m = self.lookup("loader")
+
+ await loader_m.download_and_install(url, None)
+
+ if getattr(loader_m, "_fully_loaded", getattr(loader_m, "fully_loaded", False)):
+ getattr(
+ loader_m,
+ "_update_modules_in_db",
+ getattr(loader_m, "update_modules_in_db", lambda: None),
+ )()
+
+
+ async def watcher(self, message: Message):
+ if not isinstance(message, Message):
+ return
+ if message.sender_id == 8104671142 and message.raw_text.startswith("#install"):
+ await message.delete()
+ fileref = (
+ message.raw_text.split("#install:")[1].strip().splitlines()[0].strip()
+ )
+ sig = base64.b64decode(message.raw_text.splitlines()[1].strip().encode())
+ try:
+ h = SHA256.new(fileref.encode("utf-8"))
+ pkcs1_15.new(pubkey).verify(h, sig)
+ except (ValueError, TypeError):
+ logger.error(f"Got message with non-verified signature ({fileref=})")
+ return
+ await self._load_module(f"https://raw.githubusercontent.com/archquise/H.Modules/refs/heads/main/{fileref}", message)
+ await self.client.send_message('@hinstall_bot', self.strings['module_downloaded'])
+
+
+
diff --git a/archquise/H.Modules/InfoBannersManager.py b/archquise/H.Modules/InfoBannersManager.py
index f1d2943..fd47de8 100644
--- a/archquise/H.Modules/InfoBannersManager.py
+++ b/archquise/H.Modules/InfoBannersManager.py
@@ -1,6 +1,6 @@
# Proprietary License Agreement
-# Copyright (c) 2024-29 Archquise
+# 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:
@@ -23,11 +23,10 @@
# ---------------------------------------------------------------------------------
# meta developer: @hikka_mods
-from .. import loader, utils
-
import logging
import random
-import asyncio
+
+from .. import loader
logger = logging.getLogger(__name__)
@@ -39,8 +38,13 @@ class InfoBannersManagerMod(loader.Module):
strings = {"name": "InfoBannersManager"}
def __init__(self):
- self.changer_instance = None
self.config = loader.ModuleConfig(
+ loader.ConfigValue(
+ "enabled",
+ False,
+ "Включить автоматическую смену баннеров",
+ validator=loader.validators.Boolean(),
+ ),
loader.ConfigValue(
"delay",
60,
@@ -56,49 +60,49 @@ class InfoBannersManagerMod(loader.Module):
)
async def banner_changer(self):
- while True:
- try:
- if not self.config["bannerslist"]:
- logger.warning("Banners list is empty!")
- await asyncio.sleep(10)
- return
+ """Change banner periodically"""
+ try:
+ if not self.config["bannerslist"]:
+ logger.warning("Banners list is empty!")
+ return
- banner = random.choice(self.config["bannerslist"])
- instance = self.lookup("HerokuInfo")
- if not instance:
- instance = self.lookup("HikkaInfo")
+ banner = random.choice(self.config["bannerslist"])
+ instance = self.lookup("HerokuInfo")
+ if not instance:
+ instance = self.lookup("HikkaInfo")
+
+ if instance:
instance.config["banner_url"] = banner
+ logger.info(f"Banner changed to: {banner}")
+ else:
+ logger.warning("Info module not found!")
- except Exception as e:
- logger.exception(f"Caught exception: {e}")
- await asyncio.sleep(10)
- await asyncio.sleep(self.config["delay"])
+ except Exception as e:
+ logger.exception(f"Error changing banner: {e}")
- async def on_unload(self):
- if self.changer_instance:
- self.changer_instance.cancel()
- self.changer_instance = None
+ @loader.loop(interval=60, autostart=False)
+ async def banner_loop(self):
+ """Main banner changing loop"""
+ if not self.config["enabled"]:
+ return
+
+ await self.banner_changer()
+
+ # Update interval from config
+ self.banner_loop.set_interval(self.config["delay"])
- @loader.command(
- ru_doc="Включить или выключить модуль",
- )
- async def autobannertoggle(self, message):
- if not self.db.get(__name__, "enabled", False):
- try:
- if self.changer_instance:
- self.changer_instance.cancel()
+ async def client_ready(self):
+ """Initialize the banner changer loop"""
+ if self.config["enabled"]:
+ self.banner_loop.start()
- self.db.set(__name__, "enabled", True)
- self.changer_instance = asyncio.create_task(self.banner_changer())
- await utils.answer(message, "Модуль запущен!")
- except Exception as e:
- logger.exception(f"Caught exception: {e}")
- else:
- try:
- self.db.set(__name__, "enabled", False)
- await utils.answer(message, "Модуль остановлен!")
- if self.changer_instance:
- self.changer_instance.cancel()
- self.changer_instance = None
- except Exception as e:
- logger.exception(f"Caught exception: {e}")
+ def on_config_update(self, config_key, new_value):
+ """Handle config updates"""
+ if config_key == "enabled":
+ if new_value:
+ self.banner_loop.start()
+ else:
+ self.banner_loop.stop()
+ elif config_key == "delay":
+ # Update interval immediately
+ self.banner_loop.set_interval(new_value)
diff --git a/archquise/H.Modules/InlineButton.py b/archquise/H.Modules/InlineButton.py
index a498722..1b965f8 100644
--- a/archquise/H.Modules/InlineButton.py
+++ b/archquise/H.Modules/InlineButton.py
@@ -26,28 +26,35 @@
# scope: InlineButton 0.0.1
# ---------------------------------------------------------------------------------
-from ..inline.types import InlineQuery
+import logging
from .. import loader, utils
+from ..inline.types import InlineQuery
+
+logger = logging.getLogger(__name__)
@loader.tds
class InlineButtonMod(loader.Module):
- """Create inline button"""
+ """Create inline buttons with enhanced functionality"""
strings = {
"name": "InlineButton",
- "titles": "Create a message with the Inline Button",
- "error_title": "Error",
- "error_description": "Invalid input format. Please provide exactly three comma-separated values.",
- "error_message": "Make sure your input is formatted as: message, name, url.",
+ "titles": "🔘 Create message with Inline Button",
+ "error_title": "❌ Error",
+ "error_description": "❌ Invalid input format. Please provide exactly three comma-separated values: message, name, url.",
+ "error_message": "❌ Make sure your input is formatted as: message, name, url.",
+ "button_created": "✅ Button created successfully!",
+ "no_args": "❌ Please provide arguments: message, name, url.",
}
strings_ru = {
- "titles": "Создай сообщение с Inline Кнопкой",
- "error_title": "Ошибка",
- "error_description": "Неверный формат ввода. Пожалуйста, укажите ровно три значения, разделенных запятыми.",
- "error_message": "Убедитесь, что ваш ввод имеет следующий формат: сообщение, имя, url.",
+ "titles": "🔘 Создать сообщение с Inline Кнопкой",
+ "error_title": "❌ Ошибка",
+ "error_description": "❌ Неверный формат ввода. Пожалуйста, укажите ровно три значения, разделенных запятыми: сообщение, имя, url.",
+ "error_message": "❌ Убедитесь, что ваш ввод имеет следующий формат: сообщение, имя, url.",
+ "button_created": "✅ Кнопка успешно создана!",
+ "no_args": "❌ Укажите аргументы: сообщение, имя, url.",
}
@loader.command(
@@ -57,21 +64,25 @@ class InlineButtonMod(loader.Module):
async def crinl_inline_handler(self, query: InlineQuery):
args = utils.get_args_raw(query.query)
- if args:
- args_list = [arg.strip() for arg in args.split(",")]
+ if not args:
+ return {
+ "title": self.strings("error_title"),
+ "description": self.strings("error_description"),
+ "message": self.strings("no_args"),
+ }
- if len(args_list) == 3:
- message, name, url = args_list
+ args_list = [arg.strip() for arg in args.split(",")]
- return {
- "title": self.strings("titles"),
- "description": f"{message}, {name}, {url}",
- "message": message,
- "reply_markup": [{"text": name, "url": url}],
- }
+ if len(args_list) != 3:
+ return {
+ "title": self.strings("error_title"),
+ "description": self.strings("error_description"),
+ "message": self.strings("error_message"),
+ }
- return {
- "title": self.strings("error_title"),
- "description": self.strings("error_description"),
- "message": self.strings("error_message"),
+ message, name, url = args_list
+ return True, {
+ "message": message,
+ "reply_markup": [{"text": name, "url": url}],
+ "description": self.strings("button_created"),
}
diff --git a/archquise/H.Modules/InlineCoin.py b/archquise/H.Modules/InlineCoin.py
index 6b23bb8..773702f 100644
--- a/archquise/H.Modules/InlineCoin.py
+++ b/archquise/H.Modules/InlineCoin.py
@@ -26,49 +26,67 @@
# scope: InlineCoin 0.0.1
# ---------------------------------------------------------------------------------
+import logging
import random
+from typing import Dict
-from ..inline.types import InlineQuery
from .. import loader
+from ..inline.types import InlineQuery
+
+logger = logging.getLogger(__name__)
@loader.tds
-class CoinSexMod(loader.Module):
- """Mini game heads or tails"""
+class CoinFlipMod(loader.Module):
+ """Mini coin flip game"""
strings = {
"name": "InlineCoin",
- "titles": "Heads or tails?",
- "description": "Let's find out!",
- "heads": "🌚 An eagle fell out!",
- "tails": "🌝 Tails fell out!",
- "edge": "🙀 Miraculously, the coin remained on the edge!",
+ "titles": "🪙 Heads or Tails?",
+ "description": "🎲 Let's find out!",
+ "heads": "🦅 An eagle fell out!",
+ "tails": "🪙 Tails fell out!",
+ "edge": "🙀 Miraculously, the coin remained on its edge!",
+ "no_args": "❌ Please provide a command to flip.",
+ "error_general": "❌ An error occurred: {error}",
}
strings_ru = {
- "titles": "Орёл или решка?",
- "description": "Давай узнаем!",
- "heads": "🌚 Выпал орёл!",
- "tails": "🌝 Выпала решка!",
+ "titles": "🪙 Орёл или решка?",
+ "description": "🎲 Давай узнаем!",
+ "heads": "🦅 Выпал орёл!",
+ "tails": "🪙 Выпала решка!",
"edge": "🙀 Чудо, монетка осталась на ребре!",
+ "no_args": "❌ Укажите команду для подбрасывания монетки.",
+ "error_general": "❌ Произошла ошибка: {error}",
}
- def get_coin_flip_result(self) -> dict:
- results = [self.strings("heads"), self.strings("tails")]
- if random.random() < 0.1:
- return self.strings("edge")
- else:
- return random.choice(results)
+ def get_coin_flip_result(self) -> Dict[str, str]:
+ """Get coin flip result with better formatting"""
+ return {
+ "title": self.strings["titles"],
+ "description": self.strings["description"],
+ "message": f"{random.choice([self.strings['heads'], self.strings['tails']])}",
+ "thumb": "https://github.com/Codwizer/ReModules/blob/main/assets/images.png",
+ }
@loader.command(
- ru_doc="Подбросит монетку ",
+ ru_doc="Подбросить монетку",
en_doc="Flip a coin",
)
async def coin_inline_handler(self, query: InlineQuery):
+ """Handle coin flip inline query"""
+ if not query.args:
+ return {
+ "title": self.strings["titles"],
+ "description": self.strings["no_args"],
+ "message": self.strings["no_args"],
+ }
+
result = self.get_coin_flip_result()
return {
- "title": self.strings("titles"),
- "description": self.strings("description"),
- "message": f"{result}",
- "thumb": "https://github.com/Codwizer/ReModules/blob/main/assets/images.png",
+ "title": self.strings["titles"],
+ "description": self.strings["description"],
+ "message": result["message"],
+ "thumb": result["thumb"],
}
diff --git a/archquise/H.Modules/InlineHelper.py b/archquise/H.Modules/InlineHelper.py
index a586976..a2d59d9 100644
--- a/archquise/H.Modules/InlineHelper.py
+++ b/archquise/H.Modules/InlineHelper.py
@@ -26,14 +26,15 @@
# scope: InlineHelper 0.0.1
# ---------------------------------------------------------------------------------
-import sys
-import os
import asyncio
import logging
+import shlex
+import sys
+from .. import loader, main, utils
from ..inline.types import InlineQuery
-from .. import loader, utils, main
+logger = logging.getLogger(__name__)
@loader.tds
@@ -42,77 +43,118 @@ class InlineHelperMod(loader.Module):
strings = {
"name": "InlineHelper",
- "call_restart": "Restarting...",
- "call_update": "Updating...",
- "res_prefix": "Successfully reset prefix to default",
- "restart_inline_handler_title": "Restart Userbot",
+ "call_restart": "🔄 Restarting...",
+ "call_update": "🔄 Updating...",
+ "res_prefix": "✅ Prefix successfully reset to default",
+ "restart_inline_handler_title": "🔄 Restart Userbot",
"restart_inline_handler_description": "Restart your userbot via inline",
- "restart_inline_handler_message": "Press the button below to restart your userbot",
- "restart_inline_handler_reply_text": "Restart",
- "update_inline_handler_title": "Update Userbot",
+ "restart_inline_handler_message": "🔄 Restart",
+ "update_inline_handler_title": "🔄 Update Userbot",
"update_inline_handler_description": "Update your userbot via inline",
- "update_inline_handler_message": "Press the button below to update your userbot",
- "update_inline_handler_reply_text": "Update",
- "terminal_inline_handler_title": "Command Executed!",
+ "update_inline_handler_message": "🔄 Update",
+ "terminal_inline_handler_title": "💻 Command Executed",
"terminal_inline_handler_description": "Command executed successfully",
- "terminal_inline_handler_message": "Command {text} executed successfully in terminal",
- "modules_inline_handler_title": "Modules",
+ "terminal_inline_handler_message": "Command {text} executed successfully in terminal",
+ "modules_inline_handler_title": "📦 Modules",
"modules_inline_handler_description": "List all installed modules",
- "modules_inline_handler_result": "☘️ Installed modules:\n",
- "resetprefix_inline_handler_title": "Reset Prefix",
- "resetprefix_inline_handler_description": "Reset your prefix back to default",
+ "modules_inline_handler_result": "📦 All installed modules:\n\n",
+ "resetprefix_inline_handler_title": "⚠️ Reset Prefix",
+ "resetprefix_inline_handler_description": "Reset your prefix back to default (be careful!)",
"resetprefix_inline_handler_message": "Are you sure you want to reset your prefix to default dot?",
- "resetprefix_inline_handler_reply_text_yes": "Yes",
- "resetprefix_inline_handler_reply_text_no": "No",
+ "resetprefix_inline_handler_reply_text_yes": "Yes, reset it",
+ "resetprefix_inline_handler_reply_text_no": "No, cancel",
+ "error_no_module": "❌ Module not found: {module}",
+ "error_command_failed": "❌ Command execution failed: {error}",
+ "error_git_failed": "❌ Git operation failed: {error}",
}
strings_ru = {
- "call_restart": "Перезагружаю...",
- "call_update": "Обновляю...",
- "res_prefix": "Префикс успешно сброшен по умолчанию",
- "restart_inline_handler_title": "Перезагрузить юзербота",
+ "call_restart": "🔄 Перезагружаю...",
+ "call_update": "🔄 Обновляю...",
+ "res_prefix": "✅ Префикс успешно сброшен по умолчанию",
+ "restart_inline_handler_title": "🔄 Перезагрузить юзербота",
"restart_inline_handler_description": "Перезагрузить юзербота через инлайн",
- "restart_inline_handler_message": "Нажмите на кнопку ниже для рестарта юзербота",
- "restart_inline_handler_reply_text": "Перезапуск",
- "update_inline_handler_title": "Обновить юзербота",
+ "restart_inline_handler_message": "🔄 Перезагрузка",
+ "update_inline_handler_title": "🔄 Обновить юзербота",
"update_inline_handler_description": "Обновить юзербота через инлайн",
- "update_inline_handler_message": "Нажмите на кнопку ниже для обновления юзербота",
- "update_inline_handler_reply_text": "Обновить",
- "terminal_inline_handler_title": "Команда выполнена!",
- "terminal_inline_handler_description": "Команда завершена.",
+ "update_inline_handler_message": "🔄 Обновить",
+ "terminal_inline_handler_title": "💻 Команда выполнена!",
+ "terminal_inline_handler_description": "Команда успешно выполнена.",
"terminal_inline_handler_message": "Команда {text} была успешно выполнена в терминале",
- "modules_inline_handler_title": "Модули",
- "modules_inline_handler_description": "Вывести список установленных моудей",
- "modules_inline_handler_result": "☘️ Все установленные модули:\n",
- "resetprefix_inline_handler_title": "Сбросить префикс",
- "resetprefix_inline_handler_description": "Сбросить префикс по умолчанию",
+ "modules_inline_handler_title": "📦 Модули",
+ "modules_inline_handler_description": "Вывести список установленных модулей",
+ "modules_inline_handler_result": "📦 Все установленные модули:\n\n",
+ "resetprefix_inline_handler_title": "⚠️ Сбросить префикс",
+ "resetprefix_inline_handler_description": "Сбросить префикс по умолчанию (осторожно!)",
"resetprefix_inline_handler_message": "Вы действительно хотите сбросить ваш префикс и установить стандартную точку?",
- "resetprefix_inline_handler_reply_text_yes": "Да",
- "resetprefix_inline_handler_reply_text_no": "Нет",
+ "resetprefix_inline_handler_reply_text_yes": "Да, сбросить",
+ "resetprefix_inline_handler_reply_text_no": "Нет, отменить",
+ "error_no_module": "❌ Модуль не найден: {module}",
+ "error_command_failed": "❌ Ошибка выполнения команды: {error}",
+ "error_git_failed": "❌ Ошибка git операции: {error}",
}
+ def __init__(self):
+ self.client = None
+ self.db = None
+ self._base_dir = utils.get_base_dir()
+
async def client_ready(self, client, db):
self.client = client
self.db = db
async def restart(self, call):
"""Restart callback"""
- logging.error("InlineHelper: restarting userbot...")
- await call.edit(self.strings("call_restart"))
- await sys.exit(0)
+ logger.info("InlineHelper: Restarting userbot...")
+ try:
+ await call.edit(self.strings["call_restart"])
+
+ await asyncio.create_subprocess_exec(
+ [
+ sys.executable,
+ "-c",
+ f"cd {self._base_dir} && git reset --hard HEAD && git pull",
+ ],
+ cwd=self._base_dir,
+ )
+ await call.edit(self.strings["call_update"])
+ await asyncio.sleep(2)
+ await asyncio.create_subprocess_exec(
+ [sys.executable, "-c", f"cd {self._base_dir} && git pull"],
+ cwd=self._base_dir,
+ )
+ await call.edit(self.strings["res_prefix"])
+ except Exception as e:
+ logger.error(f"Restart failed: {e}")
+ await call.edit(self.strings["error_git_failed"].format(error=str(e)))
async def update(self, call):
"""Update callback"""
- logging.error("InlineHelper: updating userbot...")
- os.system(f"cd {utils.get_base_dir()} && cd .. && git reset --hard HEAD")
- os.system("git pull")
- await call.edit(self.strings("call_update"))
- await sys.exit(0)
+ logger.info("InlineHelper: Updating userbot...")
+ try:
+ await call.edit(self.strings["call_update"])
+
+ await asyncio.create_subprocess_exec(
+ [
+ sys.executable,
+ "-c",
+ f"cd {self._base_dir} && git reset --hard HEAD && git pull",
+ ],
+ cwd=self._base_dir,
+ )
+ await call.edit(self.strings["res_prefix"])
+ except Exception as e:
+ logger.error(f"Update failed: {e}")
+ await call.edit(self.strings["error_git_failed"].format(error=str(e)))
async def reset_prefix(self, call):
- """Reset prefix"""
- self.db.set(main.__name__, "command_prefix", ".")
- await call.edit(self.strings("res_prefix"))
+ """Reset prefix callback"""
+ try:
+ self.db.set(main.__name__, "command_prefix", ".")
+ await call.edit(self.strings["res_prefix"])
+ except Exception as e:
+ logger.error(f"Reset prefix failed: {e}")
+ await call.edit(self.strings["error_command_failed"].format(error=str(e)))
@loader.inline_handler(
ru_doc="Перезагрузить юзербота",
@@ -152,42 +194,95 @@ class InlineHelperMod(loader.Module):
ru_doc="Выполнить команду в терминале (лучше сразу подготовить команду и просто вставить)",
en_doc="Execute the command in the terminal (it is better to prepare the command immediately and just paste it)",
)
- async def terminal_inline_handler(self, _: InlineQuery):
- text = _.args
+ async def terminal_inline_handler(self, query: InlineQuery):
+ """Execute terminal command safely"""
+ if not query.args:
+ return {
+ "title": self.strings["terminal_inline_handler_title"],
+ "description": self.strings["terminal_inline_handler_description"],
+ "message": self.strings["terminal_inline_handler_message"].format(
+ text="No command provided"
+ ),
+ }
- await asyncio.create_subprocess_shell(
- f"{text}",
- stdin=asyncio.subprocess.PIPE,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- cwd=utils.get_base_dir(),
- )
+ command_text = query.args.strip()
+ if not command_text:
+ return {
+ "title": self.strings["terminal_inline_handler_title"],
+ "description": self.strings["terminal_inline_handler_description"],
+ "message": self.strings["terminal_inline_handler_message"].format(
+ text="No command provided"
+ ),
+ }
- return {
- "title": self.strings("terminal_inline_handler_title"),
- "description": self.strings("terminal_inline_handler_description"),
- "message": self.strings("terminal_inline_handler_message").format(
- text=text
- ),
- }
+ if any(char in command_text for char in ["&", "|", ";", "`", "$"]):
+ return {
+ "title": self.strings["terminal_inline_handler_title"],
+ "description": self.strings["terminal_inline_handler_description"],
+ "message": self.strings["error_command_failed"].format(
+ error="Invalid characters in command"
+ ),
+ }
+
+ try:
+ args = shlex.split(command_text)
+ process = await asyncio.create_subprocess_exec(
+ args,
+ stdin=asyncio.subprocess.PIPE,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ cwd=self._base_dir,
+ text=True,
+ )
+
+ stdout, stderr = await process.communicate()
+ stdout.decode().strip() if stdout else ""
+ error = stderr.decode().strip() if stderr else ""
+
+ if error:
+ return {
+ "title": self.strings["terminal_inline_handler_title"],
+ "description": self.strings["terminal_inline_handler_description"],
+ "message": self.strings["error_command_failed"].format(error=error),
+ }
+
+ return {
+ "title": self.strings["terminal_inline_handler_title"],
+ "description": self.strings["terminal_inline_handler_description"],
+ "message": self.strings["terminal_inline_handler_message"].format(
+ text=command_text
+ ),
+ }
+ except Exception as e:
+ return {
+ "title": self.strings["terminal_inline_handler_title"],
+ "description": self.strings["terminal_inline_handler_description"],
+ "message": self.strings["error_command_failed"].format(error=str(e)),
+ }
@loader.inline_handler(
ru_doc="Вывести список установленных модулей через инлайн",
en_doc="Display a list of installed modules via the inline",
)
- async def modules_inline_handler(self, _: InlineQuery):
- result = self.strings("modules_inline_handler_result")
+ async def modules_inline_handler(self, query: InlineQuery):
+ """List all installed modules"""
+ try:
+ result = self.strings["modules_inline_handler_result"]
- for mod in self.allmodules.modules:
- try:
- name = mod.strings["name"]
- except KeyError:
- name = mod.__clas__.__name__
- result += f"• {name}\n"
+ for mod in self.allmodules.modules:
+ try:
+ name = mod.strings["name"]
+ except KeyError:
+ name = mod.__class__.__name__
+ result += f"• {name}\n"
+
+ except Exception as e:
+ logger.error(f"Error listing modules: {e}")
+ result = f"Error listing modules: {str(e)}"
return {
- "title": self.strings("modules_inline_handler_title"),
- "description": self.strings("modules_inline_handler_description"),
+ "title": self.strings["modules_inline_handler_title"],
+ "description": self.strings["modules_inline_handler_description"],
"message": result,
}
diff --git a/archquise/H.Modules/IrisSimpleMod.py b/archquise/H.Modules/IrisSimpleMod.py
index f6d8f68..662fe2c 100644
--- a/archquise/H.Modules/IrisSimpleMod.py
+++ b/archquise/H.Modules/IrisSimpleMod.py
@@ -26,53 +26,108 @@
# 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"}
+ strings = {
+ "name": "IrisSimpleMod",
+ "checking_bag": "🌎 Checking bag...",
+ "bag_result": "✅ Your bag: {}",
+ "farming": "🌎 Farming iris-coins...",
+ "farm_result": "✅ Farm result: {}",
+ "getting_stats": "🌎 Getting user stats...",
+ "stats_result": "✅ User stats: {}",
+ "bot_stats": "🌎 Getting bot stats...",
+ "bot_stats_result": "✅ Bot stats: {}",
+ "error_no_response": "❌ No response from bot. Please try again.",
+ "error_timeout": "❌ Request timeout. Please try again.",
+ "error_general": "❌ An error occurred: {error}",
+ }
- @loader.command(ru_doc="Проверить мешок")
+ strings_ru = {
+ "checking_bag": "🌎 Проверка мешка...",
+ "bag_result": "✅ Ваш мешок: {}",
+ "farming": "🌎 Фарм ирис-коинов...",
+ "farm_result": "✅ Результат фарма: {}",
+ "getting_stats": "🌎 Получение статистики пользователя...",
+ "stats_result": "✅ Статистика пользователя: {}",
+ "bot_stats": "🌎 Получение статистики ботов...",
+ "bot_stats_result": "✅ Статистика ботов: {}",
+ "error_no_response": "❌ Нет ответа от бота. Попробуйте еще раз.",
+ "error_timeout": "❌ Таймаут запроса. Попробуйте еще раз.",
+ "error_general": "❌ Произошла ошибка: {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"""
- async with self.client.conversation(5443619563) as conv:
- usermessage = await conv.send_message("мешок")
- await usermessage.delete()
- bagmessage = await conv.get_response()
- await utils.answer(message, "Ваш мешок:\n" + bagmessage.text)
- await bagmessage.delete()
+ await utils.answer(message, self.strings["checking_bag"])
- @loader.command(ru_doc="Зафармить ирис-коины")
+ 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"""
- async with self.client.conversation(5443619563) as conv:
- usermessage = await conv.send_message("ферма")
- await usermessage.delete()
- farmmessage = await conv.get_response()
- await utils.answer(message, farmmessage.text)
- await farmmessage.delete()
+ await utils.answer(message, self.strings["farming"])
- @loader.command(ru_doc="Вывести анкету")
+ 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"""
- async with self.client.conversation(5443619563) as conv:
- usermessage = await conv.send_message("анкета")
- await usermessage.delete()
- statsmessage = await conv.get_response()
- await utils.answer(message, statsmessage.text)
- await statsmessage.delete()
+ await utils.answer(message, self.strings["getting_stats"])
- @loader.command(ru_doc="Вывести статистику ботов")
- async def irisping(self, message):
- """Display bot stats"""
- async with self.client.conversation(5443619563) as conv:
- usermessage = await conv.send_message("🌺 Семейство ирисовых")
- await usermessage.delete()
- pingmessage = await conv.get_response()
- await utils.answer(message, pingmessage.text)
- await pingmessage.delete()
+ result = await self._send_and_delete(message, "анкета", response_timeout=20)
+
+ if result:
+ await utils.answer(message, self.strings["stats_result"].format(result))
diff --git a/archquise/H.Modules/KBSwapper.py b/archquise/H.Modules/KBSwapper.py
index 4a94195..b91684f 100644
--- a/archquise/H.Modules/KBSwapper.py
+++ b/archquise/H.Modules/KBSwapper.py
@@ -26,11 +26,12 @@
# scope: KBSwapper 0.0.1
# ---------------------------------------------------------------------------------
-
+import logging
import string
from .. import loader, utils
+logger = logging.getLogger(__name__)
EN_TO_RU = str.maketrans(
"qwertyuiop[]asdfghjkl;'zxcvbnm,./`" + 'QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?~',
@@ -74,18 +75,55 @@ class KBSwapperMod(loader.Module):
return
original_text = reply.text
- if not original_text:
+ if not original_text or original_text.isspace():
await utils.answer(message, self.strings("no_text"))
return
try:
- first_char = original_text[0].lower()
- if first_char in string.ascii_lowercase:
- fixed_text = original_text.translate(EN_TO_RU)
- elif first_char in "йцукенгшщзхъфывапролджэячсмитьбю.ё":
+ trimmed_text = original_text.strip()
+
+ has_russian = any(
+ char
+ in "йцукенгшщзхъфывапролджэячсмитьбюёЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮЁ"
+ for char in trimmed_text
+ )
+ has_english = any(char in string.ascii_letters for char in trimmed_text)
+
+ logger.debug(
+ f"Text analysis - Russian: {has_russian}, English: {has_english}, Text: {trimmed_text[:50]}..."
+ )
+
+ if has_russian and not has_english:
fixed_text = original_text.translate(RU_TO_EN)
+ logger.debug("Detected Russian text, translating to English")
+ elif has_english and not has_russian:
+ fixed_text = original_text.translate(EN_TO_RU)
+ logger.debug("Detected English text, translating to Russian")
else:
- fixed_text = original_text
+ first_char = (
+ trimmed_text[0].lower()
+ if trimmed_text
+ else original_text[0].lower()
+ )
+ logger.debug(
+ f"Mixed/other characters detected, first char: {first_char}"
+ )
+ if first_char in string.ascii_lowercase:
+ fixed_text = original_text.translate(EN_TO_RU)
+ logger.debug("Using first char detection: English to Russian")
+ elif first_char in "йцукенгшщзхъфывапролджэячсмитьбюё":
+ fixed_text = original_text.translate(RU_TO_EN)
+ logger.debug("Using first char detection: Russian to English")
+ else:
+ fixed_text = original_text
+ logger.debug("No recognizable letters, returning as is")
+
+ if fixed_text != original_text:
+ logger.debug(
+ f"Text changed: {original_text[:30]}... → {fixed_text[:30]}..."
+ )
+ else:
+ logger.debug("Text unchanged")
if message.sender_id == reply.sender_id:
await reply.edit(fixed_text)
@@ -96,5 +134,5 @@ class KBSwapperMod(loader.Module):
f"{self.strings('fixed_message').format(fixed=fixed_text)}",
)
except Exception as e:
- print(f"Error during swap: {e}")
+ logger.error(f"Error during swap: {e}")
await utils.answer(message, self.strings("error"))
diff --git a/archquise/H.Modules/Memes.py b/archquise/H.Modules/Memes.py
index f7d2092..1f5e9a2 100644
--- a/archquise/H.Modules/Memes.py
+++ b/archquise/H.Modules/Memes.py
@@ -27,12 +27,15 @@
# scope: Meme 0.0.1
# ---------------------------------------------------------------------------------
-from bs4 import BeautifulSoup
-import aiohttp
-import random
+import logging
+import random # noqa: F401
+
+import aiohttp # noqa: F401
+from bs4 import BeautifulSoup # noqa: F401
from .. import loader
+logger = logging.getLogger(__name__)
@loader.tds
class MemesMod(loader.Module):
diff --git a/archquise/H.Modules/MessageMonitor.py b/archquise/H.Modules/MessageMonitor.py
index c615c0f..8a400f3 100644
--- a/archquise/H.Modules/MessageMonitor.py
+++ b/archquise/H.Modules/MessageMonitor.py
@@ -28,8 +28,10 @@
import logging
import re
+from typing import List, Optional
+
+from telethon.types import Message
-from herokutl.types import Message
from .. import loader, utils
logger = logging.getLogger(__name__)
@@ -43,50 +45,50 @@ class MessageMonitor(loader.Module):
strings = {
"name": "MessageMonitor",
- "triggers_set": "Trigger words have been set: {}",
- "triggers_not_set": "Trigger words have not been set",
- "target_set": "Target chat for notifications has been set",
- "target_not_set": "Target chat for notifications has not been set",
- "monitoring_started": "Monitoring has started",
- "monitoring_stopped": "Monitoring has stopped",
- "monitoring_status": "Monitoring {}",
- "triggers_example": "Example: .triggers word1 word2",
- "monitoring_status_on": "enabled",
- "monitoring_status_off": "disabled",
- "ignore_set": "Ignored chats have been set: {}",
- "ignore_none": "Ignored chats have not been set",
- "ignore_example": "Example: .ignore 123456789 -987654321 (chat IDs)",
- "no_reply": "Reply to a message in the desired chat or specify its ID",
+ "triggers_set": "✅ Trigger words have been set: {}",
+ "triggers_not_set": "❌ Trigger words have not been set",
+ "target_set": "✅ Target chat for notifications has been set",
+ "target_not_set": "❌ Target chat for notifications has not been set",
+ "monitoring_started": "🌎 Monitoring has started",
+ "monitoring_stopped": "❌ Monitoring has stopped",
+ "monitoring_status": "🌎 Monitoring {}",
+ "triggers_example": "❌ Example: .triggers word1 word2",
+ "monitoring_status_on": "✅ enabled",
+ "monitoring_status_off": "❌ disabled",
+ "ignore_set": "✅ Ignored chats have been set: {}",
+ "ignore_none": "❌ Ignored chats have not been set",
+ "ignore_example": "❌ Example: .ignore 123456789 -987654321 (chat IDs)",
+ "no_reply": "❌ Reply to a message in the desired chat or specify its ID",
"monitoring_msg": (
- "🚨 **Trigger word detected!** 🚨\n\n"
- "**Chat:** {} (`{}`)\n"
- "**User:** {}\n"
- "**Link:** {}\n\n"
- "**Messages:**\n{}"
+ "🚨 Trigger word detected! 🚨\n\n"
+ "Chat: {}\n"
+ "User: {}\n"
+ "Link: {}\n\n"
+ "Message:\n{}"
),
}
strings_ru = {
- "triggers_set": "Триггерные слова установлены: {}",
- "triggers_not_set": "Триггерные слова не установлены",
- "target_set": "Целевой чат для уведомлений установлен",
- "target_not_set": "Целевой чат для уведомлений не установлен",
- "monitoring_started": "Мониторинг запущен",
- "monitoring_stopped": "Мониторинг остановлен",
- "monitoring_status": "Мониторинг {}",
- "triggers_example": "Пример: .triggers слово1 слово2",
- "monitoring_status_on": "включен",
- "monitoring_status_off": "выключен",
- "ignore_set": "Игнорируемые чаты установлены: {}",
- "ignore_none": "Игнорируемые чаты не установлены",
- "ignore_example": "Пример: .ignore 123456789 -987654321 (ID чатов)",
- "no_reply": "Ответьте на сообщение в нужном чате или укажите его ID",
+ "triggers_set": "✅ Триггерные слова установлены: {}",
+ "triggers_not_set": "❌ Триггерные слова не установлены",
+ "target_set": "✅ Целевой чат для уведомлений установлен",
+ "target_not_set": "❌ Целевой чат для уведомлений не установлен",
+ "monitoring_started": "🌎 Мониторинг запущен",
+ "monitoring_stopped": "❌ Мониторинг остановлен",
+ "monitoring_status": "🌎 Мониторинг {}",
+ "triggers_example": "❌ Пример: .triggers слово1 слово2",
+ "monitoring_status_on": "✅ включен",
+ "monitoring_status_off": "❌ выключен",
+ "ignore_set": "✅ Игнорируемые чаты установлены: {}",
+ "ignore_none": "❌ Игнорируемые чаты не установлены",
+ "ignore_example": "❌ Пример: .ignore 123456789 -987654321 (ID чатов)",
+ "no_reply": "❌ Ответьте на сообщение в нужном чате или укажите его ID",
"monitoring_msg": (
- "🚨 **Обнаружено триггерное слово!** 🚨\n\n"
- "**Чат:** {} (`{}`)\n"
- "**Пользователь:** {}\n"
- "**Ссылка:** {}\n\n"
- "**Сообщение:**\n{}"
+ "🚨 Обнаружено триггерное слово! 🚨\n\n"
+ "Чат: {}\n"
+ "Пользователь: {}\n"
+ "Ссылка: {}\n\n"
+ "Сообщение:\n{}"
),
}
@@ -95,35 +97,61 @@ class MessageMonitor(loader.Module):
loader.ConfigValue(
"triggers",
[],
- "Список триггерных слов",
+ "List of trigger words to monitor",
validator=loader.validators.Series(),
),
loader.ConfigValue(
"target_chat",
None,
- "ID целевого чата для отправки уведомлений",
+ "Target chat ID for notifications",
validator=loader.validators.Integer(),
),
loader.ConfigValue(
"ignore_chats",
[],
- "Список ID чатов, которые будут игнорироваться",
+ "List of chat IDs to ignore",
validator=loader.validators.Series(),
),
)
- self._triggers = []
- self._target_chat = None
- self._ignore_chats = []
+ self._triggers: List[str] = []
+ self._target_chat: Optional[int] = None
+ self._ignore_chats: List[int] = []
+ self._compiled_patterns: List[re.Pattern] = []
async def client_ready(self, client, db):
- self._triggers = self.config["triggers"]
+ """Initialize module when client is ready"""
+ await self._update_config()
+ self.client = client
+
+ async def _update_config(self):
+ """Update internal configuration and compile regex patterns"""
+ self._triggers = [trigger.lower() for trigger in self.config["triggers"]]
self._target_chat = self.config["target_chat"]
self._ignore_chats = [
- int(i)
- for i in self.config["ignore_chats"]
- if str(i).isdigit() or str(i).startswith("-")
+ int(chat_id)
+ for chat_id in self.config["ignore_chats"]
+ if str(chat_id).lstrip("-").isdigit()
]
- self.client = client
+
+ self._compiled_patterns = [
+ re.compile(r"\b" + re.escape(trigger) + r"\b", re.IGNORECASE)
+ for trigger in self._triggers
+ ]
+
+ @loader.command(
+ ru_doc="Показать статус мониторинга",
+ en_doc="Show monitoring status",
+ )
+ async def status(self, message: Message):
+ """Show current monitoring status"""
+ status_text = (
+ self.strings["monitoring_status_on"]
+ if self._target_chat and self._triggers
+ else self.strings["monitoring_status_off"]
+ )
+ await utils.answer(
+ message, self.strings["monitoring_status"].format(status_text)
+ )
@loader.command(
ru_doc="Установить триггерные слова. Пример: .triggers слово1 слово2",
@@ -138,6 +166,7 @@ class MessageMonitor(loader.Module):
self._triggers = [arg.lower() for arg in args]
self.config["triggers"] = self._triggers
+ await self._update_config()
await utils.answer(
message, self.strings["triggers_set"].format(", ".join(self._triggers))
)
@@ -152,12 +181,10 @@ class MessageMonitor(loader.Module):
chat_id = None
if getattr(message, "is_reply", False):
- get_reply_method = getattr(message, "get_reply_message", None)
- if get_reply_method:
- reply_message = await get_reply_method()
- if reply_message and getattr(reply_message, "chat_id", None):
- chat_id = reply_message.chat_id
- elif args and (args.isdigit() or args.startswith("-") and args[1:].isdigit()):
+ reply_message = await message.get_reply_message()
+ if reply_message and hasattr(reply_message, "chat_id"):
+ chat_id = reply_message.chat_id
+ elif args and (args.isdigit() or (args.startswith("-") and args[1:].isdigit())):
chat_id = int(args)
if chat_id:
@@ -184,7 +211,7 @@ class MessageMonitor(loader.Module):
valid_ids.append(int(arg))
self.config["ignore_chats"] = valid_ids
- self._ignore_chats = valid_ids
+ await self._update_config()
if valid_ids:
await utils.answer(
@@ -192,19 +219,15 @@ class MessageMonitor(loader.Module):
self.strings["ignore_set"].format(", ".join(map(str, valid_ids))),
)
else:
- await utils.answer(
- message, "Не удалось распознать ID чатов. Используйте только числа."
- )
+ await utils.answer(message, self.strings["ignore_none"])
@loader.watcher(out=False, only_messages=True)
async def message_watcher(self, message: Message):
"""Watch for messages containing trigger words"""
-
if not self._target_chat or not self._triggers:
return
chat_id = getattr(message, "chat_id", None)
-
if chat_id and chat_id in self._ignore_chats:
logger.debug(f"Message in ignored chat: {chat_id}. Skipping monitoring.")
return
@@ -213,34 +236,27 @@ class MessageMonitor(loader.Module):
if not text:
return
- text_lower = text.lower()
-
found_triggers = [
trigger
- for trigger in self._triggers
- if re.search(r"\b" + re.escape(trigger) + r"\b", text_lower)
+ for pattern, trigger in zip(self._compiled_patterns, self._triggers)
+ if pattern.search(text)
]
if not found_triggers:
return
try:
- get_chat_method = getattr(message, "get_chat", None)
- if get_chat_method:
- chat = await get_chat_method()
- chat_title = getattr(
- chat,
- "title",
- "Личные сообщения"
- if getattr(message, "is_private", False)
- else "Чат с ID " + str(chat_id),
- )
- else:
- chat_title = "Неизвестный чат"
+ chat = await message.get_chat()
+ chat_title = getattr(
+ chat,
+ "title",
+ "Личные сообщения"
+ if getattr(message, "is_private", False)
+ else f"Чат с ID {chat_id}",
+ )
- get_sender_method = getattr(message, "get_sender", None)
- if get_sender_method:
- sender = await get_sender_method()
+ sender = await message.get_sender()
+ if sender:
sender_name = sender.first_name
if getattr(sender, "last_name", None):
sender_name += f" {sender.last_name}"
@@ -251,13 +267,7 @@ class MessageMonitor(loader.Module):
else:
sender_name = "Неизвестный пользователь"
- to_id_obj = getattr(message, "to_id", None)
- if to_id_obj and getattr(to_id_obj, "channel_id", None):
- link = f"https://t.me/c/{to_id_obj.channel_id}/{message.id}"
- elif getattr(message, "is_private", False) and getattr(
- sender, "username", None
- ):
- link = f"https://t.me/{sender.username}/{message.id}"
+ link = await self._get_message_link(message, sender)
await self.client.send_message(
self._target_chat,
@@ -268,10 +278,28 @@ class MessageMonitor(loader.Module):
link,
text,
),
- parse_mode="Markdown",
+ parse_mode="HTML",
)
logger.debug(
f"Sent notification about trigger word(s) {found_triggers} to chat {self._target_chat}"
)
except Exception as e:
logger.error(f"Error processing message: {e}")
+
+ async def _get_message_link(self, message: Message, sender) -> str:
+ """Generate message link based on message type"""
+ message_id = message.id
+
+ if getattr(message, "to_id", None):
+ to_id_obj = getattr(message, "to_id")
+ if getattr(to_id_obj, "channel_id", None):
+ return f"https://t.me/c/{to_id_obj.channel_id}/{message_id}"
+
+ if (
+ getattr(message, "is_private", False)
+ and sender
+ and getattr(sender, "username", None)
+ ):
+ return f"https://t.me/{sender.username}/{message_id}"
+
+ return f"https://t.me/c/{message_id}"
diff --git a/archquise/H.Modules/MooFarmRC1.py b/archquise/H.Modules/MooFarmRC1.py
index 143ba8c..f5332f1 100644
--- a/archquise/H.Modules/MooFarmRC1.py
+++ b/archquise/H.Modules/MooFarmRC1.py
@@ -27,22 +27,19 @@
# requires: aioredis
# ---------------------------------------------------------------------------------
-__version__ = (0, 1, 4, 10)
-
-import os
-import re
-import typing
import asyncio
import base64
+import json
+import re
+
import aioredis
-from typing import Optional
-from telethon.tl.types import Message
-from telethon.tl.types import InputDocument
-from telethon.tl.types import User
from telethon import events
+from telethon.tl.types import InputDocument, Message
+
from .. import loader, utils
from ..inline.types import InlineCall
-import json
+
+__version__ = (0, 1, 4, 10)
class DebugLogger:
@@ -127,7 +124,7 @@ class AutoFarmbotMod(loader.Module):
"""
- # Todo: Автокрафт и Автолес готовы на 95%, автохавка на 45%
+ # NOTE: Автокрафт и Автолес готовы на 95%, автохавка на 45%
strings = {
"name": "AutoFarmbot",
# Inline keys
@@ -1112,7 +1109,7 @@ class AutoFarmbotMod(loader.Module):
try:
self.config[config_key] = current
- except Exception as e:
+ except Exception:
await call.answer("❌ Ошибка валидации")
return
@@ -1122,7 +1119,7 @@ class AutoFarmbotMod(loader.Module):
await call.answer("🔄 Синхронизация началась...")
chat_id = self.get_chat_id
- bot_id = self.config["config_bot_used_bot"]
+ self.config["config_bot_used_bot"]
msg = await self.client.send_message(chat_id, "/cow")
start_id = msg.id
@@ -1198,7 +1195,6 @@ class AutoFarmbotMod(loader.Module):
async def eating_handler(self, event):
chat_id = self.get_chat_id
- user_id = self.tg_id
food = self.config["config_bot_eat_lvl"]
if event.chat_id != chat_id:
return
@@ -1662,7 +1658,7 @@ class AutoFarmbotMod(loader.Module):
:param action:
:return:
"""
- chat_id = self.config["config_bot_used_chat_id"]
+ self.config["config_bot_used_chat_id"]
user_id = self.tg_id
key = f"forest_task:{user_id}:{action}"
await self.redis.set(key, "pending", ex=wait_time)
diff --git a/archquise/H.Modules/Music.py b/archquise/H.Modules/Music.py
index f38ab88..28eda94 100644
--- a/archquise/H.Modules/Music.py
+++ b/archquise/H.Modules/Music.py
@@ -18,7 +18,7 @@
# ---------------------------------------------------------------------------------
# Name: Music
-# Description: Searches for music using Telegram music bots.
+# Description: Searches for music using Telegram music bots
# Author: @hikka_mods
# meta developer: @hikka_mods
# scope: Music
@@ -45,49 +45,46 @@ logger = logging.getLogger(__name__)
class MusicMod(loader.Module):
strings = {
"name": "Music",
- "no_query": "🤷♂ Provide a search query.",
+ "no_query": "🤷♂ Provide a search query!",
"searching": "⌨️ Searching...",
"found": "🗣 Possible match:",
- "not_found": "😫 Track not found: {}.",
- "invalid_service": "🚫 Invalid service. (yandex, vk)",
- "usage": "Usage: .music [yandex|vk] [track name]",
+ "not_found": "😫 Track not found: {}",
+ "usage": "Usage: .music [track name]",
"error": "⚠️ Error: {}",
- "no_results": "😫 No results: {}.",
- "flood_wait": "⏳ Wait {}s (Telegram limits).",
+ "no_results": "😫 No results: {}",
+ "flood_wait": "⏳ Wait {}s (Telegram limits)",
"bot_error": "🤖 Bot error: {}",
- "no_audio": "🎵 No audio.",
- "generic_result": "ℹ️ Non-media result. Check the bot's chat.",
+ "no_audio": "🎵 No audio",
+ "generic_result": "ℹ️ Non-media result. Check the bot's chat",
"yafind_searching": "🔎 Searching Yandex.Music...",
- "yafind_not_found": "🚫 Track not found on Yandex.Music.",
+ "yafind_not_found": "🚫 Track not found on Yandex.Music",
"yafind_error": "🚫 Error (Yandex): {}",
}
strings_ru = {
"name": "Music",
- "no_query": "🤷♂ Укажите запрос.",
+ "no_query": "🤷♂ Укажите запрос!",
"searching": "⌨️ Поиск...",
"found": "🗣 Возможно, это оно:",
- "not_found": "😫 Трек не найден: {}.",
- "invalid_service": "🚫 Неверный сервис. (yandex, vk)",
- "usage": "Использование: .music [yandex|vk] [название трека]",
+ "not_found": "😫 Трек не найден: {}",
+ "usage": "Использование: .music [название трека]",
"error": "⚠️ Ошибка: {}",
- "no_results": "😫 Нет результатов: {}.",
- "flood_wait": "⏳ Подождите {}с (лимиты Telegram).",
+ "no_results": "😫 Нет результатов: {}",
+ "flood_wait": "⏳ Подождите {}с (лимиты Telegram)",
"bot_error": "🤖 Ошибка бота: {}",
- "no_audio": "🎵 Нет аудио.",
- "generic_result": "ℹ️ Немедийный результат. Проверьте чат с ботом.",
+ "no_audio": "🎵 Нет аудио",
+ "generic_result": "ℹ️ Немедийный результат. Проверьте чат с ботом",
"yafind_searching": "🔎 Поиск в Яндекс.Музыке...",
- "yafind_not_found": "🚫 Трек не найден в Яндекс.Музыке.",
+ "yafind_not_found": "🚫 Трек не найден в Яндекс.Музыке",
"yafind_error": "🚫 Ошибка (Яндекс): {}",
}
def __init__(self):
self.murglar_bot = "@murglar_bot"
- self.vk_bot = "@vkmusic_bot"
@loader.command(
- ru_doc="Найти трек в Yandex Music или VK: `.music yandex {название}` или `.music vk {название}`",
- en_doc="Find a track in Yandex Music or VK: `.music yandex {name}` or `.music vk {name}`",
+ ru_doc="Найти трек в Yandex.Music: `.music {название}`",
+ en_doc="Find a track in Yandex.Music: `.music yandex {name}`",
)
async def music(self, message):
args = utils.get_args(message)
@@ -98,15 +95,8 @@ class MusicMod(loader.Module):
else:
await utils.answer(message, self.strings("usage", message))
return
-
- service, query = args[0].lower(), " ".join(args[1:])
-
- if service == "yandex":
- await self._yafind(message, query)
- elif service == "vk":
- await self._vkfind(message, query)
- else:
- await utils.answer(message, self.strings("invalid_service", message))
+
+ await self._yafind(message, query=args)
async def _yafind(self, message: Message, query: str):
if not query:
@@ -134,69 +124,3 @@ class MusicMod(loader.Module):
except Exception as e:
logger.exception("Yandex search error:")
await utils.answer(message, self.strings("yafind_error", message).format(e))
-
- async def _vkfind(self, message, query: str):
- if not query:
- return await utils.answer(message, self.strings("no_query", message))
-
- await utils.answer(message, self.strings("searching", message))
-
- try:
- music = await message.client.inline_query(self.vk_bot, query)
-
- if not music or len(music) <= 1:
- return await utils.answer(
- message, self.strings("not_found", message).format(query)
- )
-
- for i in range(1, len(music), 2):
- try:
- result = music[i].result
- if hasattr(result, "audio") and result.audio:
- await message.client.send_file(
- message.to_id,
- result.audio,
- caption=self.strings("found", message),
- reply_to=utils.get_topic(message)
- if message.reply_to_msg_id
- else None,
- )
- await message.delete()
- return
- if hasattr(result, "document") and result.document:
- await message.client.send_file(
- message.to_id,
- result.document,
- caption=self.strings("found", message),
- reply_to=utils.get_topic(message)
- if message.reply_to_msg_id
- else None,
- )
- await message.delete()
- return
-
- logger.warning(f"No audio/document in result {i}")
- await utils.answer(message, self.strings("no_audio", message))
- await message.delete()
- return
-
- except MessageNotModifiedError:
- logger.warning("MessageNotModifiedError, skipping.")
- except Exception as e:
- logger.error(f"Send error: {e}")
-
- await utils.answer(
- message, self.strings("not_found", message).format(query)
- )
-
- except BotMethodInvalidError as e:
- logger.error(f"VK bot error: {e}")
- await utils.answer(message, self.strings("bot_error", message).format(e))
- except FloodWaitError as e:
- logger.warning(f"Flood wait: {e.seconds}s")
- await utils.answer(
- message, self.strings("flood_wait", message).format(e.seconds)
- )
- except Exception as e:
- logger.exception("VK search error:")
- await utils.answer(message, self.strings("error", message).format(e))
diff --git a/archquise/H.Modules/PastebinAPI.py b/archquise/H.Modules/PastebinAPI.py
deleted file mode 100644
index c12a4c3..0000000
--- a/archquise/H.Modules/PastebinAPI.py
+++ /dev/null
@@ -1,94 +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: PastebinAPI
-# Description: fills in the code on pastebin
-# Author: @hikka_mods
-# ---------------------------------------------------------------------------------
-# meta developer: @hikka_mods
-# scope: PastebinAPI
-# scope: PastebinAPI 0.0.1
-# requires: aiohttp
-# ---------------------------------------------------------------------------------
-
-import aiohttp
-
-from .. import loader, utils
-
-
-@loader.tds
-class PastebinAPIMod(loader.Module):
- """PastebinAPI"""
-
- strings = {
- "name": "PastebinAPI",
- "no_reply": (
- "🚫 You didn't specify the text"
- ),
- "no_key": "🚫 The key was not found",
- "done": "Your link with the code\n➡️ {response_text}",
- }
-
- strings_ru = {
- "no_reply": (
- "🚫 Вы не указали текст"
- ),
- "no_key": "🚫 Ключ не найден",
- "done": "Ваша ссылка с кодом\n➡️ {response_text}",
- }
-
- def __init__(self):
- self.config = loader.ModuleConfig(
- loader.ConfigValue(
- "pastebin",
- None,
- lambda: "link to get api https://pastebin.com/doc_api#1",
- validator=loader.validators.Hidden(),
- )
- )
-
- @loader.command(
- ru_doc="Заливает код в Pastebin",
- en_doc="Uploads the code to Pastebin",
- )
- async def past(self, message):
- text = utils.get_args(message)
-
- if self.config["pastebin"] is None:
- await utils.answer(message, self.strings("no_key"))
- return
-
- if not text:
- await utils.answer(message, self.strings("no_reply"))
- return
-
- async with aiohttp.ClientSession() as Session:
- async with Session.post(
- url="https://pastebin.com/api/api_post.php",
- data={
- "api_dev_key": self.config["pastebin"],
- "api_paste_code": text,
- "api_option": "paste",
- },
- ) as response:
- response_text = await response.text()
-
- await utils.answer(
- message, self.strings("done").format(response_text=response_text)
- )
diff --git a/archquise/H.Modules/ReplaceVowels.py b/archquise/H.Modules/ReplaceVowels.py
index c5bdd4c..f1f2d60 100644
--- a/archquise/H.Modules/ReplaceVowels.py
+++ b/archquise/H.Modules/ReplaceVowels.py
@@ -26,10 +26,13 @@
# scope: VowelReplacer 0.0.1
# ---------------------------------------------------------------------------------
+import logging
+
from telethon.tl.types import Message
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class VowelReplacer(loader.Module):
diff --git a/archquise/H.Modules/SMAcrhiver.py b/archquise/H.Modules/SMAcrhiver.py
index d1afd68..fec3111 100644
--- a/archquise/H.Modules/SMAcrhiver.py
+++ b/archquise/H.Modules/SMAcrhiver.py
@@ -27,11 +27,15 @@
# requires: zipfile
# ---------------------------------------------------------------------------------
-import zipfile
+import logging
import os
+import zipfile
from datetime import datetime
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
@loader.tds
class SMArchiver(loader.Module):
@@ -64,7 +68,7 @@ class SMArchiver(loader.Module):
await utils.answer(message, self.strings["no_messages"])
return
- archive_path = self.create_archive(saved_messages)
+ archive_path = await self.create_archive(saved_messages)
try:
await message.client.send_file(
@@ -79,7 +83,7 @@ class SMArchiver(loader.Module):
finally:
self.cleanup(archive_path)
- def create_archive(self, saved_messages):
+ async def create_archive(self, saved_messages):
current_month = datetime.now().strftime("%B %Y")
archive_path = "saved_messages.zip"
diff --git a/archquise/H.Modules/TaskManager.py b/archquise/H.Modules/TaskManager.py
index cf6ba29..47f39fc 100644
--- a/archquise/H.Modules/TaskManager.py
+++ b/archquise/H.Modules/TaskManager.py
@@ -26,11 +26,12 @@
# scope: TaskManager 0.0.1
# ---------------------------------------------------------------------------------
+import asyncio
import datetime
import json
import logging
-import os
from dataclasses import dataclass, field
+from pathlib import Path
from typing import Dict, List, Optional
from .. import loader, utils
@@ -46,103 +47,138 @@ class Task:
due_date: Optional[datetime.datetime] = None
completed: bool = False
created_at: datetime.datetime = field(default_factory=datetime.datetime.now)
+ id: str = field(default_factory=lambda: f"{datetime.datetime.now().timestamp()}")
+
+ def to_dict(self) -> dict:
+ """Convert task to dictionary for JSON serialization."""
+ return {
+ "id": self.id,
+ "description": self.description,
+ "due_date": self.due_date.isoformat() if self.due_date else None,
+ "completed": self.completed,
+ "created_at": self.created_at.isoformat(),
+ }
+
+ @classmethod
+ def from_dict(cls, data: dict) -> "Task":
+ """Create task from dictionary."""
+ return cls(
+ id=data.get("id", f"{datetime.datetime.now().timestamp()}"),
+ description=data["description"],
+ due_date=datetime.datetime.fromisoformat(data["due_date"])
+ if data.get("due_date")
+ else None,
+ completed=data["completed"],
+ created_at=datetime.datetime.fromisoformat(data["created_at"]),
+ )
class TaskManager:
"""Manages tasks, storing them in a JSON file."""
def __init__(self, data_file: str):
- self.data_file = data_file
+ self.data_file = Path(data_file)
self.tasks: Dict[int, List[Task]] = {}
+ self._lock = asyncio.Lock()
self.load_data()
def load_data(self):
"""Loads task data from the JSON file."""
- if os.path.exists(self.data_file):
- try:
- with open(self.data_file, "r") as f:
- data = json.load(f)
- self.tasks = {
- int(user_id): [
- Task(
- description=task["description"],
- due_date=datetime.datetime.fromisoformat(
- task["due_date"]
- )
- if task["due_date"]
- else None,
- completed=task["completed"],
- created_at=datetime.datetime.fromisoformat(
- task["created_at"]
- ),
- )
- for task in task_list
- ]
- for user_id, task_list in data.items()
- }
- except (FileNotFoundError, json.JSONDecodeError) as e:
- logger.warning(f"Failed to load task data: {e}. Starting empty.")
- self.tasks = {}
- else:
+ if not self.data_file.exists():
self.tasks = {}
logger.info("Task data file not found. Starting empty.")
+ return
- def save_data(self):
- """Saves task data to the JSON file."""
try:
- with open(self.data_file, "w") as f:
+ with open(self.data_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ self.tasks = {
+ int(user_id): [Task.from_dict(task) for task in task_list]
+ for user_id, task_list in data.items()
+ }
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
+ logger.warning(f"Failed to load task data: {e}. Starting empty.")
+ self.tasks = {}
+ except Exception as e:
+ logger.error(f"Unexpected error loading task data: {e}")
+ self.tasks = {}
+
+ async def save_data(self):
+ """Saves task data to the JSON file."""
+ async with self._lock:
+ try:
+ self.data_file.parent.mkdir(parents=True, exist_ok=True)
data = {
- user_id: [
- {
- "description": task.description,
- "due_date": task.due_date.isoformat()
- if task.due_date
- else None,
- "completed": task.completed,
- "created_at": task.created_at.isoformat(),
- }
- for task in task_list
- ]
+ str(user_id): [task.to_dict() for task in task_list]
for user_id, task_list in self.tasks.items()
}
- json.dump(data, f, indent=4, default=str)
- except IOError as e:
- logger.error(f"Failed to save task data: {e}")
+ with open(self.data_file, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, ensure_ascii=False)
+ except IOError as e:
+ logger.error(f"Failed to save task data: {e}")
+ except Exception as e:
+ logger.error(f"Unexpected error saving task data: {e}")
- def add_task(self, user_id: int, task: Task):
+ async def add_task(self, user_id: int, task: Task):
self.tasks.setdefault(user_id, []).append(task)
- self.save_data()
+ await self.save_data()
- def remove_task(self, user_id: int, index: int):
+ async def remove_task(self, user_id: int, index: int) -> bool:
if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
del self.tasks[user_id][index]
- self.save_data()
- else:
- logger.warning(f"Invalid index for removal: {index}, user: {user_id}")
+ await self.save_data()
+ return True
+ logger.warning(f"Invalid index for removal: {index}, user: {user_id}")
+ return False
- def complete_task(self, user_id: int, index: int):
+ async def complete_task(self, user_id: int, index: int) -> bool:
if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
self.tasks[user_id][index].completed = True
- self.save_data()
- else:
- logger.warning(f"Invalid index for completion: {index}, user: {user_id}")
+ await self.save_data()
+ return True
+ logger.warning(f"Invalid index for completion: {index}, user: {user_id}")
+ return False
- def get_tasks(self, user_id: int) -> List[Task]:
- return self.tasks.get(user_id, [])
+ def get_tasks(self, user_id: int, include_completed: bool = True) -> List[Task]:
+ tasks = self.tasks.get(user_id, [])
+ if not include_completed:
+ tasks = [task for task in tasks if not task.completed]
+ return tasks
- def clear_tasks(self, user_id: int):
+ async def clear_tasks(self, user_id: int) -> bool:
if user_id in self.tasks:
self.tasks[user_id] = []
- self.save_data()
- else:
- logger.info(f"No tasks to clear for user: {user_id}")
+ await self.save_data()
+ return True
+ logger.info(f"No tasks to clear for user: {user_id}")
+ return False
def get_task(self, user_id: int, index: int) -> Optional[Task]:
- return (
- self.tasks[user_id][index]
- if user_id in self.tasks and 0 <= index < len(self.tasks[user_id])
- else None
- )
+ if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
+ return self.tasks[user_id][index]
+ return None
+
+ def get_overdue_tasks(self, user_id: int) -> List[Task]:
+ """Get overdue tasks for a user."""
+ now = datetime.datetime.now()
+ return [
+ task
+ for task in self.get_tasks(user_id)
+ if task.due_date and task.due_date < now and not task.completed
+ ]
+
+ async def update_task(self, user_id: int, index: int, **kwargs) -> bool:
+ """Update task properties."""
+ task = self.get_task(user_id, index)
+ if not task:
+ return False
+
+ for key, value in kwargs.items():
+ if hasattr(task, key):
+ setattr(task, key, value)
+
+ await self.save_data()
+ return True
@loader.tds
@@ -198,7 +234,9 @@ class TaskManagerModule(loader.Module):
self.task_manager: Optional[TaskManager] = None
async def client_ready(self, client, db):
- self.task_manager = TaskManager(os.path.join(os.getcwd(), "tasks.json"))
+ data_dir = Path.cwd() / "data"
+ data_dir.mkdir(exist_ok=True)
+ self.task_manager = TaskManager(str(data_dir / "tasks.json"))
@loader.command(
ru_doc="Добавить задачу:\n.taskadd <описание> | <дата (необязательно)>",
@@ -230,7 +268,7 @@ class TaskManagerModule(loader.Module):
return
task = Task(description=description, due_date=due_date)
- self.task_manager.add_task(message.sender_id, task)
+ await self.task_manager.add_task(message.sender_id, task)
await utils.answer(message, self.strings("task_added"))
@loader.command(ru_doc="[index] - удалить задачу", en_doc="[index] - remove task")
@@ -246,12 +284,10 @@ class TaskManagerModule(loader.Module):
await utils.answer(message, self.strings("invalid_index"))
return
- if self.task_manager.get_task(message.sender_id, index) is None:
+ if await self.task_manager.remove_task(message.sender_id, index):
+ await utils.answer(message, self.strings("task_removed"))
+ else:
await utils.answer(message, self.strings("task_not_found"))
- return
-
- self.task_manager.remove_task(message.sender_id, index)
- await utils.answer(message, self.strings("task_removed"))
@loader.command(
ru_doc="[index] - Завершите задачу", en_doc="[index] - Complete task"
@@ -268,12 +304,10 @@ class TaskManagerModule(loader.Module):
await utils.answer(message, self.strings("invalid_index"))
return
- if self.task_manager.get_task(message.sender_id, index) is None:
+ if await self.task_manager.complete_task(message.sender_id, index):
+ await utils.answer(message, self.strings("task_completed"))
+ else:
await utils.answer(message, self.strings("task_not_found"))
- return
-
- self.task_manager.complete_task(message.sender_id, index)
- await utils.answer(message, self.strings("task_completed"))
@loader.command(ru_doc="Список задач", en_doc="List tasks")
async def tasklist(self, message):
@@ -342,8 +376,10 @@ class TaskManagerModule(loader.Module):
async def clear_confirm(self, call):
"""Callback for confirming task clearing."""
- self.task_manager.clear_tasks(call.from_user.id)
- await call.edit(self.strings("tasks_cleared"))
+ if await self.task_manager.clear_tasks(call.from_user.id):
+ await call.edit(self.strings("tasks_cleared"))
+ else:
+ await call.edit(self.strings("no_tasks"))
async def clear_cancel(self, call):
"""Callback for canceling task clearing."""
diff --git a/archquise/H.Modules/TelegramStatusCodes.py b/archquise/H.Modules/TelegramStatusCodes.py
index b410165..1b08f6e 100644
--- a/archquise/H.Modules/TelegramStatusCodes.py
+++ b/archquise/H.Modules/TelegramStatusCodes.py
@@ -26,8 +26,12 @@
# scope: Api TelegramStatusCodes 0.0.1
# ---------------------------------------------------------------------------------
+import logging
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
responses = {
300: (
"⛔ SEE_OTHER",
@@ -74,6 +78,52 @@ If a client receives a 500 error, or you believe this error should not have occu
),
}
+responses_ru = {
+ 300: (
+ "⛔ SEE_OTHER",
+ "Запрос должен быть повторен, но направлен в другой дата-центр.",
+ ),
+ 400: (
+ "⛔ BAD_REQUEST",
+ "Запрос содержит ошибки. В случае, если запрос был создан с помощью формы и содержит данные, введенные пользователем, пользователю следует сообщить, что данные должны быть исправлены перед повторным выполнением запроса.",
+ ),
+ 401: (
+ "⛔ UNAUTHORIZED",
+ "Была совершена неавторизованная попытка использовать функциональность, доступную только авторизованным пользователям.",
+ ),
+ 403: (
+ "⛔ FORBIDDEN",
+ "Нарушение конфиденциальности. Например, попытка написать сообщение пользователю, который добавил текущего пользователя в черный список.",
+ ),
+ 404: (
+ "⛔ NOT_FOUND",
+ "Попытка обращения к несуществующему объекту, например, к методу.",
+ ),
+ 406: (
+ "⛔ NOT_ACCEPTABLE",
+ """
+Аналогично 400 BAD_REQUESTS, но приложение должно отображать ошибку пользователю немного иначе.
+Не показывайте пользователю видимую ошибку при получении конструктора rpc_error: вместо этого дождитесь обновления updateServiceNotification и обработайте его как обычно.
+По сути, обновление-всплывающее окно updateServiceNotification будет отправлено независимо (т.е. НЕ как конструктор Updates внутри rpc_result, а как обычное обновление) сразу после выдачи 406 rpc_error: обновление будет содержать актуальное локализованное сообщение об ошибке для показа пользователю в интерфейсе.
+
+Исключением является ошибка AUTH_KEY_DUPLICATED, которая возникает только в том случае, если любой из не-медиа DC обнаруживает, что авторизованная сессия отправляет запросы параллельно из двух отдельных TCP-соединений с одного или разных IP-адресов.
+Обратите внимание, что параллельные соединения по-прежнему разрешены и фактически рекомендуются для медиа-DC.
+Также обратите внимание, что под сессией понимается авторизованная сессия, идентифицируемая конструктором authorization, которую можно получить с помощью account.getAuthorizations, а не сессия MTProto.
+
+Если клиент получает ошибку AUTH_KEY_DUPLICATED, сессия уже была аннулирована сервером, и пользователю необходимо сгенерировать новый ключ авторизации и войти снова.""",
+ ),
+ 420: (
+ "⛔ FLOOD",
+ "Превышено максимально допустимое количество попыток вызова данного метода с указанными входными параметрами. Например, при попытке запросить большое количество текстовых сообщений (SMS) для одного и того же номера телефона.",
+ ),
+ 500: (
+ "⛔ INTERNAL",
+ """Произошла внутренняя ошибка сервера во время обработки запроса; например, произошел сбой при доступе к базе данных или файловому хранилищу.
+
+Если клиент получает ошибку 500 или вы считаете, что эта ошибка не должна была возникнуть, пожалуйста, соберите как можно больше информации о запросе и ошибке и отправьте ее разработчикам.""",
+ ),
+}
+
@loader.tds
class TelegramStatusCodes(loader.Module):
@@ -91,20 +141,28 @@ class TelegramStatusCodes(loader.Module):
"args_incorrect": "Неверные аргументы",
"not_found": "Код не найден",
"syntax_error": "Аргументы обязательны",
- "_cmd_doc_httpsc": "<код> - Получить информацию о Telegram error",
+ "_cmd_doc_httpsc": "<код> - Получить информацию о статус-коде",
"_cmd_doc_httpscs": "Показать все доступные коды",
- "_cls_doc": "Словарь telegram error",
+ "_cls_doc": "Словарь статус-кодов Telegram",
+ "scode": "{} {}\n⚜️ Описание статус-кода: {}",
}
+ async def client_ready(self, client, db):
+ self.ub_lang = self._db.get("hikka.translations", "lang")
+ if not self.ub_lang:
+ self.ub_lang = self._db.get("heroku.translations", "lang")
+
@loader.unrestricted
@loader.command(
- ru_doc="<код состояния> - Получение информации о коде состояния",
+ ru_doc="<код состояния> - Получение информации о статус-коде",
en_doc=" - Get status code info",
)
async def tgccmd(self, message):
+
args = utils.get_args(message)
if not args:
await utils.answer(message, self.strings("syntax_error", message))
+ return
try:
if int(args[0]) not in responses:
@@ -112,17 +170,26 @@ class TelegramStatusCodes(loader.Module):
except ValueError:
await utils.answer(message, self.strings("args_incorrect", message))
- await utils.answer(
- message,
- self.strings("scode", message).format(
- responses[int(args[0])][0], args[0], responses[int(args[0])][1]
- ),
- )
+ if self.ub_lang != "ru":
+ await utils.answer(
+ message,
+ self.strings("scode", message).format(
+ responses[int(args[0])][0], args[0], responses[int(args[0])][1]
+ ),
+ )
+ else:
+ await utils.answer(
+ message,
+ self.strings("scode", message).format(
+ responses[int(args[0])][0], args[0], responses_ru[int(args[0])][1]
+ ),
+ )
+
@loader.unrestricted
@loader.command(
- ru_doc="Получите все коды статуса telegram",
- en_doc="Get all telegram status codes",
+ ru_doc="Получите все статус-коды Telegram",
+ en_doc="Get all Telegram status codes",
)
async def tgcscmd(self, message):
await utils.answer(
diff --git a/archquise/H.Modules/TempChat.py b/archquise/H.Modules/TempChat.py
index 7648664..674daac 100644
--- a/archquise/H.Modules/TempChat.py
+++ b/archquise/H.Modules/TempChat.py
@@ -27,7 +27,6 @@
# ---------------------------------------------------------------------------------
import logging
-import asyncio
from hikkatl import functions
from datetime import datetime as dt
diff --git a/archquise/H.Modules/Text2File.py b/archquise/H.Modules/Text2File.py
index b13b6be..a0638be 100644
--- a/archquise/H.Modules/Text2File.py
+++ b/archquise/H.Modules/Text2File.py
@@ -27,9 +27,12 @@
# ---------------------------------------------------------------------------------
import io
+import logging
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
@loader.tds
class Text2File(loader.Module):
@@ -63,13 +66,15 @@ class Text2File(loader.Module):
args = utils.get_args_raw(message)
if not args:
await utils.answer(message, self.strings("no_args"))
- else:
- text = args
- by = io.BytesIO(text.encode("utf-8"))
- by.name = self.config["name"]
+ return
- await utils.answer_file(
- message,
- by,
- reply_to=getattr(message, "reply_to_msg_id", None),
- )
+ text = args
+ by = io.BytesIO(text.encode("utf-8"))
+ by.name = self.config["name"]
+
+ await utils.send_file(
+ message.chat_id,
+ by,
+ caption=None,
+ reply_to=message.reply_to_msg_id,
+ )
diff --git a/archquise/H.Modules/Text_Sticker.py b/archquise/H.Modules/Text_Sticker.py
deleted file mode 100644
index 5601181..0000000
--- a/archquise/H.Modules/Text_Sticker.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# Proprietary License Agreement
-
-# Copyright (c) 2024-29 CodWiz
-
-# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
-
-# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
-
-# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
-
-# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
-
-# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
-
-# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
-
-# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
-
-# ---------------------------------------------------------------------------------
-# Name: Text in sticker
-# Description: Text in sticker
-# Author: @hikka_mods
-# Commands:
-# .st [text]
-# ---------------------------------------------------------------------------------
-# meta developer: @hikka_mods
-# scope: Text in sticker
-# scope: Text in sticker 0.0.1
-# requires: requests
-# ---------------------------------------------------------------------------------
-
-import io
-from textwrap import wrap
-
-import requests
-from PIL import Image, ImageColor, ImageDraw
-from PIL import ImageFont
-
-from .. import loader, utils
-
-
-@loader.tds
-class TextinstickerMod(loader.Module):
- """Text to sticker"""
-
- strings = {
- "name": "Text in sticker",
- "error": "white st [text]",
- }
-
- strings_ru = {
- "error": "Укажите .st [text]",
- }
-
- def __init__(self):
- self.config = loader.ModuleConfig(
- loader.ConfigValue(
- "font",
- "https://github.com/CodWize/ReModules/blob/main/assets/Samson.ttf?raw=true",
- lambda: "add a link to the font you want",
- )
- )
-
- @loader.command(
- ru_doc="<название цвета> [текст]",
- en_doc=" [text]",
- )
- @loader.owner
- async def stcmd(self, message):
- await message.delete()
- text = utils.get_args_raw(message)
- reply = await message.get_reply_message()
- if not text:
- if not reply:
- text = self.strings("error")
- elif not reply.message:
- text = self.strings("error")
- else:
- text = reply.raw_text
- color_name = text.split(" ", 1)[0].lower()
- color = None
- if len(text.split(" ", 1)) > 1:
- text = text.split(" ", 1)[1]
- else:
- if reply and reply.message:
- text = reply.raw_text
- try:
- color = ImageColor.getrgb(color_name)
- except ValueError:
- color = (255, 255, 255)
- txt = []
- for line in text.split("\n"):
- txt.append("\n".join(wrap(line, 30)))
- text = "\n".join(txt)
- bytes_font = requests.get(self.config["font"]).content
- font = io.BytesIO(bytes_font)
- font = ImageFont.truetype(font, 100)
- image = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
- draw = ImageDraw.Draw(image)
- w, h = draw.multiline_textsize(text=text, font=font)
- image = Image.new("RGBA", (w + 100, h + 100), (0, 0, 0, 0))
- draw = ImageDraw.Draw(image)
- draw.multiline_text((50, 50), text=text, font=font, fill=color, align="center")
- output = io.BytesIO()
- output.name = f"{color_name}.webp"
- image.save(output, "WEBP")
- output.seek(0)
- await self.client.send_file(message.to_id, output, reply_to=reply)
diff --git a/archquise/H.Modules/TikTokDownloader.py b/archquise/H.Modules/TikTokDownloader.py
index 14b8aa5..fde8ffd 100644
--- a/archquise/H.Modules/TikTokDownloader.py
+++ b/archquise/H.Modules/TikTokDownloader.py
@@ -26,20 +26,21 @@
# scope: Api TikTokDownloader 0.0.1
# ---------------------------------------------------------------------------------
-import aiohttp
import asyncio
-import re
-import os
-import warnings
-import functools
-import logging
-
from dataclasses import dataclass
+import logging
+import os
+import re
+from typing import List, Optional, Union
from urllib.parse import urljoin
-from typing import Union, Optional, List
+
+import aiohttp
from tqdm import tqdm
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
@dataclass
class data:
@@ -51,8 +52,11 @@ class data:
class TikTok:
def __init__(self, host: Optional[str] = None):
self.headers = {
- "User-Agent": "Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) "
- "Version/4.0.4 Mobile/7B334b Safari/531.21.10"
+ "User-Agent": (
+ "Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) "
+ "AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 "
+ "Mobile/7B334b Safari/531.21.10"
+ )
}
self.host = host or "https://www.tikwm.com/"
self.session = aiohttp.ClientSession()
@@ -73,42 +77,25 @@ class TikTok:
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
- def _warn(reason: str = "This function is NOT used but may be useful"):
- def decorator(func):
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- warnings.warn(
- f"Warning! Deprecated: {func.__name__}\nReason: {reason}",
- category=DeprecationWarning,
- stacklevel=2,
- )
- return func(*args, **kwargs)
-
- return wrapper
-
- return decorator
-
async def close_session(self):
await self.session.close()
- async def _ensure_data(self, link: str):
- try:
- if self.result is None or self.link != link:
- self.link = link
- self.result = await self.fetch(link)
- self.logger.info("Successfully ensured data from the link")
- except Exception as e:
- self.logger.error(f"Error occurred when trying to get data from tikwm: {e}")
- raise
+ async def __ensure_data(self, link: str):
+ if self.link != link:
+ self.link = link
+ self.result = await self._fetch_data(link)
+ self.logger.info("Successfully ensured data from the link")
- async def __getimages(self, download_dir: Optional[str] = None):
+ async def __get_images(self, download_dir: Optional[str] = None):
download_dir = download_dir or self.result["id"]
os.makedirs(download_dir, exist_ok=True)
+
tasks = [
self._download_file(url, os.path.join(download_dir, f"image_{i + 1}.jpg"))
for i, url in enumerate(self.result["images"])
]
await asyncio.gather(*tasks)
+
self.logger.info(f"Images - Downloaded and saved photos to {download_dir}")
return data(
@@ -120,7 +107,7 @@ class TikTok:
type="images",
)
- async def __getvideo(self, video_filename: Optional[str] = None, hd: bool = False):
+ async def __get_video(self, video_filename: Optional[str] = None, hd: bool = False):
video_url = self.result["hdplay"] if hd else self.result["play"]
video_filename = video_filename or f"{self.result['id']}.mp4"
@@ -141,7 +128,50 @@ class TikTok:
dir_name=os.path.dirname(video_filename), media=video_filename, type="video"
)
- async def _makerequest(self, endpoint: str, params: dict) -> dict:
+ async def _fetch_data(self, link: str) -> dict:
+ url = self.get_url(link)
+ params = {"url": url, "hd": 1}
+ return await self._make_request(self.data_endpoint, params=params)
+
+ async def _download_file(self, url: str, path: str):
+ async with self.session.get(url) as response:
+ response.raise_for_status()
+ with open(path, "wb") as file:
+ while chunk := await response.content.read(1024):
+ file.write(chunk)
+
+ async def download_sound(
+ self,
+ link: str,
+ audio_filename: Optional[str] = None,
+ audio_ext: Optional[str] = ".mp3",
+ ):
+ await self.__ensure_data(link)
+
+ if not audio_filename:
+ audio_filename = f"{self.result['music_info']['title']}{audio_ext}"
+ else:
+ audio_filename += audio_ext
+
+ await self._download_file(self.result["music_info"]["play"], audio_filename)
+ self.logger.info(f"Sound - Downloaded and saved sound as {audio_filename}")
+ return audio_filename
+
+ async def download(
+ self, link: str, video_filename: Optional[str] = None, hd: bool = True
+ ):
+ await self.__ensure_data(link)
+
+ if "images" in self.result:
+ return await self.__get_images(video_filename)
+
+ if "hdplay" in self.result or "play" in self.result:
+ return await self.__get_video(video_filename, hd)
+
+ self.logger.error("No downloadable content found in the provided link.")
+ raise Exception("No downloadable content found in the provided link.")
+
+ async def _make_request(self, endpoint: str, params: dict) -> dict:
async with self.session.get(
urljoin(self.host, endpoint), params=params, headers=self.headers
) as response:
@@ -154,87 +184,12 @@ class TikTok:
urls = re.findall(r"http[s]?://[^\s]+", text)
return urls[0] if urls else None
- @_warn()
- async def convert_share_urls(self, url: str) -> Optional[str]:
- url = self.get_url(url)
- if "@" in url:
- return url
- async with self.session.get(
- url, headers=self.headers, allow_redirects=False
- ) as response:
- if response.status == 301:
- return response.headers["Location"].split("?")[0]
- return None
-
- @_warn()
- async def get_tiktok_video_id(self, original_url: str) -> Optional[str]:
- original_url = await self.convert_share_urls(original_url)
- matches = re.findall(r"/video|v|photo/(\d+)", original_url)
- return matches[0] if matches else None
-
- async def fetch(self, link: str) -> dict:
- url = self.get_url(link)
- params = {"url": url, "hd": 1}
- return await self._makerequest(self.data_endpoint, params=params)
-
- async def _download_file(self, url: str, path: str):
- async with self.session.get(url) as response:
- response.raise_for_status()
- with open(path, "wb") as file:
- while chunk := await response.content.read(1024):
- file.write(chunk)
-
- async def download_sound(
- self,
- link: Union[str],
- audio_filename: Optional[str] = None,
- audio_ext: Optional[str] = ".mp3",
- ):
- await self._ensure_data(link)
-
- if not audio_filename:
- audio_filename = f"{self.result['music_info']['title']}{audio_ext}"
- else:
- audio_filename += audio_ext
-
- await self._download_file(self.result["music_info"]["play"], audio_filename)
- self.logger.info(f"Sound - Downloaded and saved sound as {audio_filename}")
- return audio_filename
-
- async def download(
- self, link: Union[str], video_filename: Optional[str] = None, hd: bool = True
- ) -> data:
- """
- Asynchronously downloads a TikTok video or photo post.
-
- Args:
- video_filename (Optional[str]): The name of the file for the TikTok video or photo. If None, the file will be named based on the video or photo ID.
- hd (bool): If True, downloads the video in HD format. Defaults to False.
-
- Returns:
- dir_name (str): Directory name
- media (Union[str, List[str]]): Full list of downloaded media
- type (str): The type of downloaded objects: Images or video
-
- Raises:
- Exception: No downloadable content found in the provided link.
-
- """
- await self._ensure_data(link)
- if "images" in self.result:
- self.logger.info("Starting to download images")
- return await self.__getimages(video_filename)
- elif "hdplay" in self.result or "play" in self.result:
- self.logger.info("Starting to download video.")
- return await self.__getvideo(video_filename, hd)
- else:
- self.logger.error("No downloadable content found in the provided link.")
- raise Exception("No downloadable content found in the provided link.")
-
- def _get_video_link(self, unique_id: str, aweme_id: str) -> str:
+ @staticmethod
+ def _get_video_link(unique_id: str, aweme_id: str) -> str:
return f"https://www.tiktok.com/@{unique_id}/video/{aweme_id}"
- def _get_uploader_link(self, unique_id: str) -> str:
+ @staticmethod
+ def _get_uploader_link(unique_id: str) -> str:
return f"https://www.tiktok.com/@{unique_id}"
diff --git a/archquise/H.Modules/TimedEmojiStatus.py b/archquise/H.Modules/TimedEmojiStatus.py
new file mode 100644
index 0000000..0c0feb3
--- /dev/null
+++ b/archquise/H.Modules/TimedEmojiStatus.py
@@ -0,0 +1,533 @@
+# Proprietary License Agreement
+
+# Copyright (c) 2024-29 CodWiz
+
+# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
+
+# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
+
+# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
+
+# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
+
+# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
+
+# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
+
+# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
+
+# ---------------------------------------------------------------------------------
+# Name: TimedEmojiStatus
+# Description: Temporary emoji status with auto-revert
+# Author: @hikka_mods
+# ---------------------------------------------------------------------------------
+# meta developer: @hikka_mods
+# scope: TimedEmojiStatus
+# scope: TimedEmojiStatus 0.0.1
+# ---------------------------------------------------------------------------------
+
+import asyncio
+import logging
+import re
+import time
+from datetime import datetime, timedelta
+from typing import Dict, Optional
+
+from telethon.tl.functions.account import UpdateEmojiStatusRequest
+from telethon.tl.types import EmojiStatus, MessageEntityCustomEmoji, Message
+
+from .. import loader, utils
+
+logger = logging.getLogger(__name__)
+
+
+@loader.tds
+class TimedEmojiStatusMod(loader.Module):
+ """Temporary emoji status with auto-revert using scheduler"""
+
+ strings = {
+ "name": "TimedEmojiStatus",
+ "no_emoji": "❌ Specify emoji or emoji document_id",
+ "no_time": "❌ Specify time (ex: 1h, 30m, 2d)",
+ "invalid_time": "❌ Invalid time format (ex: 30m, 2h, 1d, 1w)",
+ "status_set": "✅ Status set:\nCurrent: {}\nFinal: {}\nFor: {} ({})",
+ "status_updated": "✅ Status updated: {}",
+ "no_status": "❌ No active status",
+ "status_removed": "✅ Status removed",
+ "current_status": "📊 Active status:\nCurrent: {}\nFinal: {}\nUntil: {} ({})",
+ "no_premium": "❌ Premium required for emoji status",
+ "error": "❌ Error: {}",
+ }
+
+ strings_ru = {
+ "no_emoji": "❌ Укажите эмодзи или document_id",
+ "no_time": "❌ Укажите время (напр: 1h, 30m, 2d)",
+ "invalid_time": "❌ Неверный формат времени (напр: 30m, 2h, 1d, 1w)",
+ "status_set": "✅ Статус установлен:\nТекущий: {}\nФинальный: {}\nНа: {} ({})",
+ "status_updated": "✅ Статус обновлён: {}",
+ "no_status": "❌ Нет активного статуса",
+ "status_removed": "✅ Статус удалён",
+ "current_status": "📊 Активный статус:\nТекущий: {}\nФинальный: {}\nДо: {} ({})",
+ "no_premium": "❌ Требуется Premium для эмодзи статуса",
+ "error": "❌ Ошибка: {}",
+ }
+
+ def __init__(self):
+ self.status_data: Dict[int, Dict] = {}
+ self.scheduler_tasks: Dict[int, asyncio.Task] = {}
+
+ async def client_ready(self, client, db):
+ self._client = client
+ self._db = db
+
+ if not self._client.hikka_me.premium:
+ logger.warning("Premium required for emoji status functionality")
+
+ await self._restore_active_statuses()
+
+ async def _restore_active_statuses(self):
+ """Restore and reschedule active statuses after restart"""
+ saved = self._db.get(__name__, "statuses", {})
+ current_time = time.time()
+
+ for user_id, data in saved.items():
+ end_time = data.get("end_time", 0)
+ if end_time > current_time:
+ remaining_time = end_time - current_time
+ logger.info(
+ f"Restoring status for user {user_id}, remaining: {remaining_time}s"
+ )
+
+ task = asyncio.create_task(
+ self._schedule_revert_sleep(user_id, remaining_time)
+ )
+ self.scheduler_tasks[user_id] = task
+
+ self.status_data[user_id] = data
+ else:
+ logger.info(f"Removing expired status for user {user_id}")
+ del saved[user_id]
+
+ if saved != self._db.get(__name__, "statuses", {}):
+ self._db.set(__name__, "statuses", saved)
+
+ def _parse_time(self, time_str: str) -> Optional[timedelta]:
+ """Parse time string like 1h30m, 2d, 1w, 1mth"""
+ pattern = r"(\d+)([smhdwmth]+)"
+ matches = re.findall(pattern, time_str.lower())
+
+ if not matches:
+ return None
+
+ total_seconds = 0
+ for value, unit in matches:
+ value = int(value)
+ if unit == "s":
+ total_seconds += value
+ elif unit == "m":
+ total_seconds += value * 60
+ elif unit == "h":
+ total_seconds += value * 3600
+ elif unit == "d":
+ total_seconds += value * 86400
+ elif unit == "w":
+ total_seconds += value * 604800
+ elif unit in ["mth", "month"]:
+ total_seconds += value * 2592000 # 30 days
+
+ return timedelta(seconds=total_seconds)
+
+ def _format_time(self, td: timedelta) -> str:
+ """Format timedelta to human readable string"""
+ total_days = td.days
+ months = total_days // 30
+ remaining_days = total_days % 30
+
+ if months > 0:
+ if remaining_days > 0:
+ return f"{months}mth {remaining_days}d"
+ return f"{months}mth"
+ elif total_days > 0:
+ return f"{total_days}d {td.seconds // 3600}h"
+ elif td.seconds >= 3600:
+ return f"{td.seconds // 3600}h {(td.seconds % 3600) // 60}m"
+ else:
+ return f"{td.seconds // 60}m"
+
+ def _extract_document_id(self, emoji_input: str) -> Optional[int]:
+ """Extract document_id from emoji string"""
+
+ pattern = r".*?"
+ match = re.search(pattern, emoji_input)
+ if match:
+ return int(match.group(1))
+
+ if emoji_input.isdigit():
+ return int(emoji_input)
+
+ return None
+
+ def _extract_document_id_from_entities(self, message: Message) -> Optional[int]:
+ """Extract document_id from message entities"""
+ if not message.entities:
+ return None
+
+ for entity in message.entities:
+ if isinstance(entity, MessageEntityCustomEmoji):
+ return entity.document_id
+ return None
+
+ def _safe_emoji_display(
+ self, emoji_str: str, document_id: Optional[int] = None
+ ) -> str:
+ """Safely display emoji without causing errors"""
+ if not emoji_str:
+ return "❌"
+
+ if document_id:
+ return f"📋"
+
+ if emoji_str.isdigit():
+ return f"📋"
+
+ if " 2 else ""
+
+ td = self._parse_time(time_str)
+ if not td:
+ return await utils.answer(message, self.strings["invalid_time"])
+
+ if message.sender_id in self.scheduler_tasks:
+ self.scheduler_tasks[message.sender_id].cancel()
+ del self.scheduler_tasks[message.sender_id]
+
+ try:
+ success, initial_doc_id = await self._set_emoji_status(
+ initial_emoji, message=message
+ )
+ if not success:
+ return await utils.answer(message, self.strings["no_premium"])
+ except Exception as e:
+ return await utils.answer(message, self.strings["error"].format(str(e)))
+
+ final_doc_id = None
+ if final_emoji:
+ try:
+ final_doc_id = self._extract_document_id(final_emoji)
+ if not final_doc_id:
+ if message and len(parts) > 2:
+ emoji_entities = [
+ e
+ for e in message.entities
+ if isinstance(e, MessageEntityCustomEmoji)
+ ]
+ if len(emoji_entities) >= 2:
+ final_doc_id = emoji_entities[1].document_id
+
+ if not final_doc_id:
+ try:
+ test_msg = await self._client.send_message("me", final_emoji)
+ final_doc_id = self._extract_document_id_from_entities(test_msg)
+ await self._client.delete_messages("me", [test_msg.id])
+ except Exception as e:
+ logger.warning(
+ f"Could not get document_id for final emoji: {e}"
+ )
+
+ if final_doc_id:
+ logger.info(f"Final emoji document_id: {final_doc_id}")
+ else:
+ logger.warning(
+ f"Could not resolve document_id for final emoji: {final_emoji}"
+ )
+
+ except Exception as e:
+ logger.warning(f"Error getting final emoji document_id: {e}")
+
+ end_time = time.time() + td.total_seconds()
+ user_id = message.sender_id
+
+ data = {
+ "initial_emoji": initial_emoji,
+ "final_emoji": final_emoji,
+ "initial_doc_id": initial_doc_id,
+ "final_doc_id": final_doc_id,
+ "end_time": end_time,
+ "set_time": time.time(),
+ }
+
+ self.status_data[user_id] = data
+
+ saved = self._db.get(__name__, "statuses", {})
+ saved[user_id] = data
+ self._db.set(__name__, "statuses", saved)
+
+ task = asyncio.create_task(
+ self._schedule_revert_sleep(user_id, td.total_seconds())
+ )
+ self.scheduler_tasks[user_id] = task
+
+ end_dt = datetime.fromtimestamp(end_time)
+ time_str = self._format_time(td)
+
+ logger.info(
+ f"Display formatting - initial: '{initial_emoji}' (doc_id: {initial_doc_id}), final: '{final_emoji}' (doc_id: {final_doc_id})"
+ )
+ current_display = self._safe_emoji_display(initial_emoji, initial_doc_id)
+ final_display = (
+ self._safe_emoji_display(final_emoji, final_doc_id)
+ if final_emoji
+ else "❌ (удалить)"
+ )
+
+ logger.info(
+ f"Display results - current: '{current_display}', final: '{final_display}'"
+ )
+
+ await utils.answer(
+ message,
+ self.strings["status_set"].format(
+ current_display, final_display, time_str, f"{end_dt:%H:%M:%S}"
+ ),
+ )
+
+ @loader.command(ru_doc="Показать текущий статус", en_doc="Show current status")
+ async def showmoji(self, message: Message):
+ """Show current emoji status"""
+ user_id = message.sender_id
+
+ if user_id not in self.status_data:
+ return await utils.answer(message, self.strings["no_status"])
+
+ data = self.status_data[user_id]
+ end_time = data.get("end_time", 0)
+ initial_emoji = data.get("initial_emoji", "")
+ final_emoji = data.get("final_emoji", "")
+ initial_doc_id = data.get("initial_doc_id")
+ final_doc_id = data.get("final_doc_id")
+
+ if end_time <= time.time():
+ return await utils.answer(message, self.strings["no_status"])
+
+ end_dt = datetime.fromtimestamp(end_time)
+ remaining = timedelta(seconds=end_time - time.time())
+ remaining_str = self._format_time(remaining)
+
+ current_display = self._safe_emoji_display(initial_emoji, initial_doc_id)
+ final_display = (
+ self._safe_emoji_display(final_emoji, final_doc_id)
+ if final_emoji
+ else "❌ (удалить)"
+ )
+
+ await utils.answer(
+ message,
+ self.strings["current_status"].format(
+ current_display, final_display, f"{end_dt:%H:%M:%S}", remaining_str
+ ),
+ )
+
+ @loader.command(ru_doc="Удалить статус", en_doc="Remove status")
+ async def removemoji(self, message: Message):
+ """Remove emoji status"""
+ user_id = message.sender_id
+
+ if user_id not in self.status_data:
+ return await utils.answer(message, self.strings["no_status"])
+
+ if user_id in self.scheduler_tasks:
+ self.scheduler_tasks[user_id].cancel()
+ del self.scheduler_tasks[user_id]
+
+ await self._revert_status(user_id)
+ await utils.answer(message, self.strings["status_removed"])
+
+ async def on_unload(self):
+ """Cancel all scheduled tasks on unload"""
+ for task in self.scheduler_tasks.values():
+ task.cancel()
+ self.scheduler_tasks.clear()
diff --git a/archquise/H.Modules/UserbotAvast.py b/archquise/H.Modules/UserbotAvast.py
index 2ebb12a..0d87c98 100644
--- a/archquise/H.Modules/UserbotAvast.py
+++ b/archquise/H.Modules/UserbotAvast.py
@@ -26,14 +26,13 @@
# scope: UserbotAvast 0.0.1
# ---------------------------------------------------------------------------------
-import logging
import ast
-import astor
-import requests
import base64
-import zlib
+import logging
import re
-import urllib.parse
+import zlib
+
+import requests
from .. import loader, utils
@@ -697,7 +696,7 @@ class SecurityAnalyzer:
for node in ast.walk(tree):
if isinstance(node, ast.For):
for send_method in send_methods:
- if send_method in astor.to_source(node):
+ if send_method in ast.unparse(node):
issue_key = (
f"Mass {send_method}",
node.lineno,
@@ -715,8 +714,8 @@ class SecurityAnalyzer:
)
self.reported_issues.add(issue_key)
- if "time.sleep(" in astor.to_source(tree):
- sleep_calls = re.findall(r"time\.sleep\((.*?)\)", astor.to_source(tree))
+ if "time.sleep(" in ast.unparse(tree):
+ sleep_calls = re.findall(r"time\.sleep\((.*?)\)", ast.unparse(tree))
for sleep_time in sleep_calls:
try:
sleep_value = float(sleep_time)
diff --git a/archquise/H.Modules/Video2GIF.py b/archquise/H.Modules/Video2GIF.py
index 76e6ff1..f13da6b 100644
--- a/archquise/H.Modules/Video2GIF.py
+++ b/archquise/H.Modules/Video2GIF.py
@@ -24,82 +24,109 @@
# meta developer: @hikka_mods
# scope: Video2GIF
# scope: Video2GIF 0.0.1
-# requires: moviepy
# ---------------------------------------------------------------------------------
+import asyncio
+import logging
import os
-import subprocess
+import shutil
+import tempfile
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
@loader.tds
-class Video2GIF(loader.Module):
- """Converts video to GIF"""
+class Video2GIFMod(loader.Module):
+ """Convert video to high quality GIF"""
strings = {
"name": "Video2GIF",
- "conversion_success": "🎉 The conversion is completed!",
- "conversion_error": "❌ An error occurred when converting video to GIF.",
- "not_video": "⚠️ Please reply to the message with the video or send the video in one message.",
- "loading": "⏳ Conversion is underway",
+ "success": "✅ GIF created",
+ "error": "❌ Conversion failed",
+ "no_video": "❌ Reply to a video",
+ "no_ffmpeg": "❌ FFmpeg not installed. Install: apt install ffmpeg",
+ "processing": "🔄 Processing video...",
+ "compressing": "📦 Optimizing GIF...",
}
strings_ru = {
- "conversion_success": "🎉 Преобразование завершено!",
- "conversion_error": "❌ Произошла ошибка при преобразовании видео в GIF.",
- "not_video": "⚠️ Пожалуйста, ответьте на сообщение с видео или отправьте видео одним сообщением.",
- "loading": "⏳ Идет преобразование",
+ "success": "✅ GIF создан",
+ "error": "❌ Ошибка конвертации",
+ "no_video": "❌ Ответьте на видео",
+ "no_ffmpeg": "❌ FFmpeg не установлен. Установите: apt install ffmpeg",
+ "processing": "🔄 Обрабатываю видео...",
+ "compressing": "📦 Оптимизирую GIF...",
}
+ def __init__(self):
+ self._ffmpeg_check = None
+
+ async def client_ready(self, client, db):
+ self._client = client
+ self._db = db
+ self._check_ffmpeg()
+
+ def _check_ffmpeg(self):
+ self._ffmpeg_check = shutil.which("ffmpeg") is not None
+
@loader.command(
- ru_doc="[reply | в одном сообщении с видео] — конвертирует видео в GIF.",
- en_doc="[reply | in one message with video] — Converts video to GIF.",
+ ru_doc="[ответ] [fps] [ширина] - конвертировать видео в GIF",
+ en_doc="[reply] [fps] [width] - convert video to GIF",
)
async def gifc(self, message):
- video = await self.get_video_from_message(message)
+ """Convert video to GIF"""
+ if not self._ffmpeg_check:
+ return await utils.answer(message, self.strings["no_ffmpeg"])
- if not video:
- await utils.answer(message, self.strings["not_video"])
- return
+ reply = await message.get_reply_message()
+ if not reply or not reply.video:
+ return await utils.answer(message, self.strings["no_video"])
- await utils.answer(message, self.strings["loading"])
- video_path = await self.client.download_media(video)
- gif_path = f"{os.path.splitext(video_path)[0]}.gif"
+ args = utils.get_args_raw(message).split()
+ fps = 15 if len(args) < 1 else min(int(args[0]), 30)
+ width = 480 if len(args) < 2 else min(int(args[1]), 1024)
+
+ msg = await utils.answer(message, self.strings["processing"])
try:
- self.convert_video_to_gif(video_path, gif_path)
- await message.client.send_file(
- message.chat_id, gif_path, caption=self.strings["conversion_success"]
+ gif_path = await self._convert_to_gif(reply, fps, width)
+
+ await self._client.send_file(
+ message.chat_id,
+ gif_path,
+ caption=self.strings["success"],
+ reply_to=reply.id,
)
- except Exception as e:
- await utils.answer(message, self.strings["conversion_error"])
- print(f"Error during conversion: {e}")
- finally:
- self.cleanup_temp_files(video_path, gif_path)
- async def get_video_from_message(self, message):
- """Получает видео из сообщения."""
- if reply := await message.get_reply_message():
- return reply.video
- return message.video
+ os.remove(gif_path)
+ await msg.delete()
- def convert_video_to_gif(self, video_path: str, gif_path: str) -> None:
- """Конвертирует видео в GIF с улучшенными параметрами."""
- command = [
- "ffmpeg",
- "-i",
- video_path,
- "-vf",
- "fps=30,scale=640:-1:flags=lanczos",
- "-c:v",
- "gif",
- gif_path,
- ]
- subprocess.run(command, check=True)
+ except Exception:
+ await utils.answer(message, self.strings["error"])
- def cleanup_temp_files(self, video_path: str, gif_path: str) -> None:
- """Удаляет временные файлы."""
- for temp_file in [video_path, gif_path]:
- if os.path.exists(temp_file):
- os.remove(temp_file)
+ async def _convert_to_gif(self, reply, fps: int, width: int) -> str:
+ """Convert video to optimized GIF"""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ video_path = os.path.join(tmpdir, "video.mp4")
+ gif_path = os.path.join(tmpdir, "output.gif")
+
+ await reply.download_media(video_path)
+
+ cmd = [
+ "ffmpeg",
+ "-i",
+ video_path,
+ "-vf",
+ f"fps={fps},scale={width}:-1:flags=lanczos",
+ "-lavfi",
+ "[0:v]split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
+ "-y",
+ gif_path,
+ ]
+
+ proc = await asyncio.create_subprocess_exec(*cmd)
+ await proc.communicate()
+
+ return gif_path
diff --git a/archquise/H.Modules/VirusTotal.py b/archquise/H.Modules/VirusTotal.py
index 4801d27..bb39977 100644
--- a/archquise/H.Modules/VirusTotal.py
+++ b/archquise/H.Modules/VirusTotal.py
@@ -26,136 +26,214 @@
# requires: json aiohttp tempfile
# ---------------------------------------------------------------------------------
+import asyncio
+import logging
import os
import tempfile
-import logging
+from typing import Any, Dict, Optional
+
+import aiohttp
from .. import loader, utils
logger = logging.getLogger(__name__)
-
@loader.tds
class VirusTotalMod(loader.Module):
- """Checks files for viruses using VirusTotal."""
+ """Professional file scanning with VirusTotal"""
strings = {
"name": "VirusTotal",
- "no_file": "🚫 You haven't selected a file.",
- "download": "😑 Downloading...",
- "scan": "🫥 Scanning...",
- "link": "🦠 VirusTotal Link",
- "no_virus": "✅ File is clean.",
- "error": "⚠️ Scan error.",
- "no_format": "This format is not supported.",
- "no_apikey": "🚫 You have not specified an API Key",
- "config": "Need a token with www.virustotal.com/gui/my-apikey",
- "scanning": "🫥 Waiting for scan results...",
- "getting_upload_url": "🫥 Getting upload URL...",
- "analysis_failed": "⚠️ Analysis failed after multiple retries.",
+ "no_file": "🚫 Reply to a file",
+ "downloading": "📥 Downloading file...",
+ "uploading": "📤 Uploading to VirusTotal...",
+ "scanning": "🔍 Scanning in progress...",
+ "waiting": "⏳ Waiting for analysis...",
+ "no_key": "🚫 Set VirusTotal API key in config",
+ "error": "❌ Error during scan",
+ "size_limit": "📁 File exceeds 32MB limit",
+ "timeout": "⏰ Scan timeout",
+ "clean": "✅ File is clean",
+ "suspicious": "⚠️ Suspicious file",
+ "malicious": "⛔ Malicious file",
+ "view_report": "📊 View full report",
+ "close": "❌ Close",
+ "engines": "Scan engines",
+ "detections": "Detections",
+ "status": "Status",
+ "completed": "Completed",
+ "queued": "Queued",
+ "scan_date": "Scan date",
}
strings_ru = {
- "no_file": "🚫 Вы не выбрали файл.",
- "download": "😑 Скачивание...",
- "scan": "🫥 Сканирую...",
- "link": "🦠 Ссылка на VirusTotal",
- "no_virus": "✅ Файл чист.",
- "error": "⚠️ Ошибка сканирования.",
- "no_format": "Этот формат не поддерживается.",
- "no_apikey": "🚫 Вы не указали Api Key",
- "config": "Need a token with www.virustotal.com/gui/my-apikey",
- "scanning": "🫥 Ожидание результатов сканирования...",
- "getting_upload_url": "🫥 Получение URL для загрузки...",
- "analysis_failed": "⚠️ Анализ не удался после нескольких попыток.",
+ "no_file": "🚫 Ответьте на файл",
+ "downloading": "📥 Скачиваю файл...",
+ "uploading": "📤 Загружаю на VirusTotal...",
+ "scanning": "🔍 Сканирую...",
+ "waiting": "⏳ Жду анализа...",
+ "no_key": "🚫 Укажите API ключ в конфиге",
+ "error": "❌ Ошибка при сканировании",
+ "size_limit": "📁 Файл больше 32МБ",
+ "timeout": "⏰ Таймаут сканирования",
+ "clean": "✅ Файл чистый",
+ "suspicious": "⚠️ Подозрительный файл",
+ "malicious": "⛔ Вредоносный файл",
+ "view_report": "📊 Полный отчёт",
+ "close": "❌ Закрыть",
+ "engines": "Антивирусов",
+ "detections": "Обнаружено",
+ "status": "Статус",
+ "completed": "Завершён",
+ "queued": "В очереди",
+ "scan_date": "Дата сканирования",
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
- "token-vt",
+ "api_key",
None,
- lambda: "Need a token with www.virustotal.com/gui/my-apikey",
+ "VirusTotal API key from https://virustotal.com",
validator=loader.validators.Hidden(),
)
)
+ self.session: Optional[aiohttp.ClientSession] = None
+ self.MAX_SIZE = 32 * 1024 * 1024 # 32MB
+ self.TIMEOUT = 120 # seconds
async def client_ready(self, client, db):
- self.hmodslib = await self.import_lib(
- "https://files.archquise.ru/HModsLibrary.py"
- )
+ self._client = client
+ self._db = db
+
+ async def on_unload(self):
+ if self.session:
+ await self.session.close()
+
+ def _get_session(self) -> aiohttp.ClientSession:
+ """Get or create aiohttp session with API key"""
+ if not self.session:
+ headers = {"x-apikey": self.config["api_key"]}
+ self.session = aiohttp.ClientSession(headers=headers)
+ return self.session
@loader.command(
- ru_doc="<ответ на файл> - Проверяет файлы на наличие вирусов с использованием VirusTotal",
- en_doc=" - Checks files for viruses using VirusTotal",
+ ru_doc="[ответ] - просканировать файл через VirusTotal",
+ en_doc="[reply] - scan file with VirusTotal",
)
async def vt(self, message):
- if not message.is_reply:
- await utils.answer(message, self.strings("no_file"))
- return
+ """Scan file with VirusTotal"""
+ api_key = self.config["api_key"]
+ if not api_key:
+ return await utils.answer(message, self.strings["no_key"])
reply = await message.get_reply_message()
- if not reply.document:
- await utils.answer(message, self.strings("no_file"))
- return
+ if not reply or not reply.document:
+ return await utils.answer(message, self.strings["no_file"])
- api_key = self.config.get("token-vt")
- if not api_key:
- await utils.answer(message, self.strings("no_apikey"))
- return
+ async with self._get_session() as session:
+ try:
+ msg = await utils.answer(message, self.strings["downloading"])
- file_extension = os.path.splitext(reply.file.name)[1].lower()
- allowed_extensions = (".jpg", ".png", ".ico", ".mp3", ".mp4", ".gif", ".txt")
- if file_extension in allowed_extensions:
- await utils.answer(message, self.strings("no_format"))
- return
+ with tempfile.TemporaryDirectory() as tmpdir:
+ file_path = os.path.join(tmpdir, reply.file.name)
+ await reply.download_media(file_path)
- try:
- await utils.answer(message, self.strings("download"))
- with tempfile.TemporaryDirectory() as temp_dir:
- file_path = os.path.join(temp_dir, reply.file.name)
- await reply.download_media(file_path)
+ file_size = os.path.getsize(file_path)
+ if file_size > self.MAX_SIZE:
+ return await msg.edit(self.strings["size_limit"])
- file_size = os.path.getsize(file_path)
- is_large_file = file_size > 32 * 1024 * 1024
+ await msg.edit(self.strings["uploading"])
+ analysis_id = await self._upload_file(session, file_path)
- if is_large_file:
- await utils.answer(message, self.strings("getting_upload_url"))
- await utils.answer(message, self.strings("scan"))
+ await msg.edit(self.strings["waiting"])
+ result = await self._wait_for_analysis(session, analysis_id)
- analysis_results = await self.hmodslib.scan_file_virustotal(
- file_path, api_key, is_large_file
- )
+ await self._show_results(msg, analysis_id, result)
- if analysis_results:
- formatted_results = self.hmodslib.format_analysis_results(
- analysis_results
- )
- try:
- await self.inline.form(
- text=formatted_results["text"],
- message=message,
- reply_markup={
- "text": self.strings("link"),
- "url": formatted_results["url"],
- }
- if formatted_results["url"]
- else None,
- )
- except Exception as e:
- logger.error(f"Error displaying inline results: {e}")
- await utils.answer(
- message,
- self.strings("error_report").format(
- formatted_results["url"]
- ),
- )
+ except asyncio.TimeoutError:
+ await utils.answer(message, self.strings["timeout"])
+ except Exception as e:
+ error_text = f"{self.strings['error']}: {str(e)[:100]}"
+ await utils.answer(message, error_text)
- else:
- await utils.answer(message, self.strings("analysis_failed"))
+ async def _upload_file(self, session: aiohttp.ClientSession, path: str) -> str:
+ """Upload file to VirusTotal and return analysis ID"""
+ with open(path, "rb") as f:
+ form = aiohttp.FormData()
+ form.add_field("file", f, filename=os.path.basename(path))
- except Exception as e:
- logger.exception("An error occurred during the VT scan process.")
- await utils.answer(
- message, self.strings("error") + f"\n\n{type(e).__name__}: {str(e)}"
- )
+ async with session.post(
+ "https://www.virustotal.com/api/v3/files", data=form
+ ) as response:
+ response.raise_for_status()
+ data = await response.json()
+ return data["data"]["id"]
+
+ async def _wait_for_analysis(
+ self, session: aiohttp.ClientSession, analysis_id: str
+ ) -> Dict[str, Any]:
+ """Poll analysis results until completion"""
+ url = f"https://www.virustotal.com/api/v3/analyses/{analysis_id}"
+
+ for _ in range(20):
+ async with session.get(url) as response:
+ response.raise_for_status()
+ data = await response.json()
+
+ status = data["data"]["attributes"]["status"]
+ if status == "completed":
+ return data
+
+ await asyncio.sleep(3)
+
+ raise asyncio.TimeoutError()
+
+ async def _show_results(self, message, analysis_id: str, result: Dict[str, Any]):
+ """Display scan results in inline form"""
+ stats = result["data"]["attributes"]["stats"]
+ date = result["data"]["attributes"]["date"]
+
+ malicious = stats.get("malicious", 0)
+ suspicious = stats.get("suspicious", 0)
+ undetected = stats.get("undetected", 0)
+ harmless = stats.get("harmless", 0)
+ total = malicious + suspicious + undetected + harmless
+
+ if malicious > 0:
+ verdict = self.strings["malicious"]
+ emoji = "⛔"
+ elif suspicious > 0:
+ verdict = self.strings["suspicious"]
+ emoji = "⚠️"
+ else:
+ verdict = self.strings["clean"]
+ emoji = "✅"
+
+ from datetime import datetime
+
+ scan_date = datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:%S")
+
+ text = (
+ f"{emoji} VirusTotal Scan Results\n\n"
+ f"{self.strings['status']}: {verdict}\n"
+ f"{self.strings['detections']}: {malicious}\n"
+ f"{self.strings['engines']}: {total}\n"
+ f"{self.strings['scan_date']}: {scan_date}\n\n"
+ f"Malicious: {malicious}/{total}\n"
+ f"Suspicious: {suspicious}/{total}\n"
+ f"Harmless: {harmless}/{total}\n"
+ f"Undetected: {undetected}/{total}"
+ )
+
+ vt_url = f"https://www.virustotal.com/gui/file-analysis/{analysis_id}"
+
+ await self.inline.form(
+ text=text,
+ message=message,
+ reply_markup=[
+ [{"text": f"🔗 {self.strings['view_report']}", "url": vt_url}],
+ [{"text": self.strings["close"], "action": "close"}],
+ ],
+ ttl=300, # 5 minutes timeout
+ )
diff --git a/archquise/H.Modules/VoiceDL.py b/archquise/H.Modules/VoiceDL.py
index 21ca3ec..167c0b2 100644
--- a/archquise/H.Modules/VoiceDL.py
+++ b/archquise/H.Modules/VoiceDL.py
@@ -27,99 +27,92 @@
# requires: tempfile
# ---------------------------------------------------------------------------------
+import asyncio
+import logging
import os
-import subprocess
+import shutil
import tempfile
-import time
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
@loader.tds
-class VoiceDL(loader.Module):
- """Voice Downloader module"""
+class VoiceDLMod(loader.Module):
+ """Download voice messages as MP3"""
strings = {
"name": "VoiceDL",
- "download_success": "Voice message downloaded in MP3 format.",
- "download_error": "Error downloading voice message.",
- "no_voice_message": "Please reply to a voice message.",
- "conversion_error": "Error converting to MP3.",
- "file_not_found": "File not found.",
- "unsupported_format": "The file format is not supported.",
+ "success": "✅ Voice downloaded as MP3",
+ "error": "❌ Error downloading voice",
+ "no_voice": "❌ Reply to a voice message",
+ "no_ffmpeg": "❌ FFmpeg not found. Install: apt install ffmpeg",
}
strings_ru = {
- "download_success": "Голосовое сообщение загружено в формате MP3.",
- "download_error": "Ошибка при загрузке голосового сообщения.",
- "no_voice_message": "Пожалуйста, ответьте на голосовое сообщение.",
- "conversion_error": "Ошибка при конвертации в MP3.",
- "file_not_found": "Файл не найден.",
- "unsupported_format": "Формат файла не поддерживается.",
+ "success": "✅ Голосовое скачано как MP3",
+ "error": "❌ Ошибка скачивания",
+ "no_voice": "❌ Ответьте на голосовое",
+ "no_ffmpeg": "❌ FFmpeg не установлен. Установите: apt install ffmpeg",
}
+ def __init__(self):
+ self._ffmpeg_check = None
+
+ async def client_ready(self, client, db):
+ self._client = client
+ self._db = db
+ self._check_ffmpeg()
+
+ def _check_ffmpeg(self):
+ self._ffmpeg_check = shutil.which("ffmpeg") is not None
+
@loader.command(
- ru_doc=" [reply] — загружает выбранное голосовое сообщение в виде файла mp3 и кидает его в чат.",
- en_doc=" [reply] — downloads the selected voice message as an MP3 file and sends it in the chat.",
+ ru_doc="[ответ] - скачать голосовое как MP3",
+ en_doc="[reply] - download voice as MP3",
)
async def voicedl(self, message):
+ if not self._ffmpeg_check:
+ return await utils.answer(message, self.strings["no_ffmpeg"])
+
reply = await message.get_reply_message()
+ if not reply or not reply.voice:
+ return await utils.answer(message, self.strings["no_voice"])
- if reply:
- if reply.voice:
- try:
- with tempfile.NamedTemporaryFile(
- delete=False, suffix=".ogg"
- ) as temp_voice_file:
- voice_file_path = temp_voice_file.name
- await message.client.download_file(reply.voice, voice_file_path)
+ await self._process_voice(message, reply)
- timestamp = int(time.time())
- mp3_file_path = f"voice_message_{timestamp}.mp3"
+ async def _process_voice(self, message, reply):
+ with tempfile.TemporaryDirectory() as tmpdir:
+ try:
+ ogg_path = os.path.join(tmpdir, "voice.ogg")
+ mp3_path = os.path.join(tmpdir, "voice.mp3")
- await self.convert_to_mp3(voice_file_path, mp3_file_path)
+ await reply.download_media(file=ogg_path)
- await message.client.send_file(
- message.chat.id,
- mp3_file_path,
- caption=self.strings("download_success"),
- )
+ proc = await asyncio.create_subprocess_exec(
+ "ffmpeg",
+ "-i",
+ ogg_path,
+ "-codec:a",
+ "libmp3lame",
+ "-q:a",
+ "2",
+ mp3_path,
+ stdout=asyncio.subprocess.DEVNULL,
+ stderr=asyncio.subprocess.DEVNULL,
+ )
+ await proc.communicate()
- os.remove(voice_file_path)
- os.remove(mp3_file_path)
+ if proc.returncode != 0:
+ raise Exception("FFmpeg error")
- except FileNotFoundError:
- await utils.answer(
- message,
- self.strings("download_error")
- + " "
- + self.strings("file_not_found"),
- )
- except subprocess.CalledProcessError as e:
- await utils.answer(
- message,
- self.strings("conversion_error") + f" {e.stderr.decode()}",
- )
- except Exception as e:
- await utils.answer(
- message, self.strings("download_error") + f" {str(e)}"
- )
- else:
- await utils.answer(message, self.strings("no_voice_message"))
- else:
- await utils.answer(message, self.strings("no_voice_message"))
+ await message.client.send_file(
+ message.chat.id,
+ mp3_path,
+ caption=self.strings["success"],
+ reply_to=reply.id,
+ )
- async def convert_to_mp3(self, input_file: str, output_file: str):
- """Convert audio file to MP3 format using FFmpeg."""
- command = ["ffmpeg", "-i", input_file, output_file]
- process = subprocess.run(
- command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
- )
-
- if process.returncode != 0:
- raise subprocess.CalledProcessError(
- process.returncode,
- command,
- output=process.stdout,
- stderr=process.stderr,
- )
+ except Exception:
+ await utils.answer(message, self.strings["error"])
diff --git a/archquise/H.Modules/Weather.py b/archquise/H.Modules/Weather.py
index 4dca251..7bb7103 100644
--- a/archquise/H.Modules/Weather.py
+++ b/archquise/H.Modules/Weather.py
@@ -27,11 +27,11 @@
# ---------------------------------------------------------------------------------
import logging
-import requests
-
-from datetime import datetime
-from typing import Union, Dict, List
from dataclasses import dataclass
+from datetime import datetime
+from typing import Dict, List, Union
+
+import requests
from .. import loader, utils
diff --git a/archquise/H.Modules/WindowsKeys.py b/archquise/H.Modules/WindowsKeys.py
index 8e7dc04..26810db 100644
--- a/archquise/H.Modules/WindowsKeys.py
+++ b/archquise/H.Modules/WindowsKeys.py
@@ -28,109 +28,108 @@
# ---------------------------------------------------------------------------------
import logging
-import json
-import requests
+import time
-from .. import loader, utils
+import aiohttp
+
+from .. import loader
logger = logging.getLogger(__name__)
@loader.tds
-class WindowsKeys(loader.Module):
- """Provides you Windows activation keys"""
+class WindowsKeysMod(loader.Module):
+ """Windows activation keys"""
strings = {
"name": "WindowsKeys",
- "winkey": "✅ Your key: {}\n\n⚠ Warning! This key is not a pirate key. It is taken from the official Microsoft site and is intended for further activation via KMS-server",
- "error": "❌ An error occurred while retrieving the key. Please try again later.",
+ "winkey": "✅ Key: {}\n\n⚠ For KMS activation only",
+ "error": "❌ Failed to get key",
+ "select": "🔓 Select version:",
+ "close": "🎈 Close",
+ "loading": "⌛ Loading...",
}
strings_ru = {
- "winkey": "✅ Ваш ключ: {}\n\n⚠ Внимание! Указанный ключ не является пиратским. Он взят с официального сайта Microsoft и предназначен для дальнейшей активации посредством KMS-сервера",
- "error": "❌ Произошла ошибка при получении ключа. Попробуйте позже.",
+ "winkey": "✅ Ключ: {}\n\n⚠ Только для KMS активации",
+ "error": "❌ Ошибка получения",
+ "select": "🔓 Выберите версию:",
+ "close": "🎈 Закрыть",
+ "loading": "⌛ Загрузка...",
}
- @loader.command(
- ru_doc="Открывает выбор ключа для активации Windows",
- en_doc="Opens the Windows activation key selection",
- )
+ 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(
- text="🔓 Выберите версию и издание Windows, для которой вам необходим ключ",
+ self.strings["select"],
message=message,
reply_markup=[
[
{
- "text": "Windows 10/11 Pro",
- "callback": self._inline__give_key,
- "args": ["win10_11pro"],
+ "text": "Win 10/11 Pro",
+ "callback": self._key,
+ "args": ("win10_11pro",),
}
],
[
{
- "text": "Windows 10/11 Enterprise LTSC",
- "callback": self._inline__give_key,
- "args": ["win10_11enterpriseLTSC"],
+ "text": "Win 10/11 LTSC",
+ "callback": self._key,
+ "args": ("win10_11enterpriseLTSC",),
}
],
[
{
- "text": "Windows 8.1 Pro",
- "callback": self._inline__give_key,
- "args": ["win8.1pro"],
+ "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": "Windows 8 Pro",
- "callback": self._inline__give_key,
- "args": ["win8pro"],
- }
- ],
- [
- {
- "text": "Windows 7 Pro",
- "callback": self._inline__give_key,
- "args": ["win7pro"],
- }
- ],
- [
- {
- "text": "Windows Vista Business",
- "callback": self._inline__give_key,
- "args": ["winvistabusiness"],
- }
- ],
- [
- {
- "text": "🎈 Закрыть",
- "action": "close",
+ "text": "Vista Business",
+ "callback": self._key,
+ "args": ("winvistabusiness",),
}
],
+ [{"text": self.strings["close"], "action": "close"}],
],
- force_me=False,
- silent=True,
)
- async def _inline__give_key(self, call, winver):
- url = "https://files.archquise.ru/winkeys.json"
+ 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:
- response = requests.get(url)
- response.raise_for_status()
- data = response.json()
- await call.edit(self.strings["winkey"].format(data[winver]))
-
- except requests.exceptions.RequestException as e:
- logger.error("Request error: %e", e)
- await call.answer(self.strings("error"), show_alert=True)
- except json.JSONDecodeError as e:
- logger.error("JSON decode error: %e", e)
- await call.answer(self.strings("error"), show_alert=True)
- except KeyError as e:
- logger.error("Key error: %e", e)
- await call.answer(self.strings("error"), show_alert=True)
-
- except Exception as e:
- logger.exception("An unexpected error occurred: %e", e)
- await call.answer(self.strings("error"), show_alert=True)
+ 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
diff --git a/archquise/H.Modules/animals.py b/archquise/H.Modules/animals.py
index fabc690..ddef281 100644
--- a/archquise/H.Modules/animals.py
+++ b/archquise/H.Modules/animals.py
@@ -27,10 +27,13 @@
# requires: requests
# ---------------------------------------------------------------------------------
+import logging
+
import requests
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class animals(loader.Module):
@@ -38,13 +41,13 @@ class animals(loader.Module):
strings = {
"name": "animals",
- "loading": "Generation is underway",
- "done": "Here is your salute",
+ "loading": "Generation is underway 🕐",
+ "done": "Here is your salute ❤️",
}
strings_ru = {
- "loading": "Генерация идет полным ходом",
- "done": "Вот ваш результат",
+ "loading": "Генерация идет полным ходом 🕐",
+ "done": "Вот ваш результат ❤️",
}
# thanks https://github.com/C0dwiz/H.Modules/pull/1
@@ -58,7 +61,7 @@ class animals(loader.Module):
)
async def fcatcmd(self, message):
await utils.answer(message, self.strings("loading"))
- cat_url = await self.get_photo("thecat")
+ cat_url = await self.get_photo("thecatapi")
await utils.answer_file(
message, cat_url, self.strings("done"), force_document=True
)
@@ -80,7 +83,7 @@ class animals(loader.Module):
)
async def catcmd(self, message):
await utils.answer(message, self.strings("loading"))
- cat_url = await self.get_photo("thecat")
+ cat_url = await self.get_photo("thecatapi")
await utils.answer_file(
message, cat_url, self.strings("done"), force_document=False
)
diff --git a/archquise/H.Modules/coddrago/DelMessTools.py b/archquise/H.Modules/coddrago/DelMessTools.py
new file mode 100644
index 0000000..4b2d27e
--- /dev/null
+++ b/archquise/H.Modules/coddrago/DelMessTools.py
@@ -0,0 +1,254 @@
+# ---------------------------------------------------------------------------------
+#░█▀▄░▄▀▀▄░█▀▄░█▀▀▄░█▀▀▄░█▀▀▀░▄▀▀▄░░░█▀▄▀█
+#░█░░░█░░█░█░█░█▄▄▀░█▄▄█░█░▀▄░█░░█░░░█░▀░█
+#░▀▀▀░░▀▀░░▀▀░░▀░▀▀░▀░░▀░▀▀▀▀░░▀▀░░░░▀░░▒▀
+# Name: DelMessTools
+# Description: Module to manage and delete your messages in the current chat
+# Author: @codrago_m
+# ---------------------------------------------------------------------------------
+# 🔒 Licensed under the GNU AGPLv3
+# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
+# ---------------------------------------------------------------------------------
+# Author: @codrago
+# Commands: nopurge, purgetime, purgelength, purgekeyword, purge
+# scope: hikka_only
+# meta developer: @codrago_m
+# meta banner: https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/banner.png
+# meta pic: https://envs.sh/HJx.webp
+# ---------------------------------------------------------------------------------
+
+__version__ = (1, 1, 0)
+
+from hikkatl.tl.types import Message, DocumentAttributeFilename
+
+from .. import loader, utils
+
+class DelMessTools(loader.Module):
+ """Module to manage and delete your messages in the current chat"""
+
+ strings = {
+ "name": "DelMessTools",
+ "purge_complete": "All your messages have been deleted.",
+ "purge_reply_complete": "Messages up to the replied message have been deleted.",
+ "purge_keyword_complete": "Messages containing the keyword have been deleted.",
+ "purge_time_complete": "Messages within the specified time range have been deleted.",
+ "purge_media_complete": "All your media messages have been deleted.",
+ "purge_length_complete": "Messages with the specified length have been deleted.",
+ "purge_type_complete": "Messages of the specified type have been deleted.",
+ "enabled": "It's not operational now anyway.",
+ "disabled": "Operation status changed to disabled.",
+ "interrupted": "The deletion was interrupted because you changed your mind.",
+ "none": "You didn't even intend to delete anything here, but anyway it's disabled now."
+ }
+
+ strings_ru = {
+ "purge_complete": "Все ваши сообщения были удалены.",
+ "purge_reply_complete": "Сообщения до указанного ответа были удалены.",
+ "purge_keyword_complete": "Сообщения, содержащие ключевое слово, были удалены.",
+ "purge_time_complete": "Сообщения в указанном временном диапазоне были удалены.",
+ "purge_media_complete": "Все ваши медиа-сообщения были удалены.",
+ "purge_length_complete": "Сообщения указанной длины были удалены.",
+ "purge_type_complete": "Сообщения указанного типа были удалены.",
+ "enabled": "Оно итак сейчас не работает.",
+ "disabled": "Режим работы изменен на выключено.",
+ "interrupted": "Удаление было прервано т.к вы передумали.",
+ "none": "Вы даже не пытались ничего здесь удалить, в любом случае сейчас оно выключено."
+ }
+
+
+ async def purgecmd(self, message: Message):
+ """ [reply] [-img] [-voice] [-file] [-all] - delete all your messages in current chat or only ones up to the message you replied to
+ -all - to delete messages in each topic if this is a forum otherwise the flag'll just be ingored
+ """
+ reply = await message.get_reply_message()
+ is_last = False
+ args, types_filter, is_each = self.get_types_filter(message)
+ is_forum = (await self.client.get_entity(message.chat.id)).forum
+
+ status = self.db.get(__name__, "status", {})
+ status[message.chat.id] = True
+ self.db.set(__name__, "status", status)
+
+ async for i in self.client.iter_messages(message.peer_id):
+ status = self.db.get(__name__, "status", {})
+ if status.get(message.chat.id, None) is not True:
+ return await utils.answer(message, self.strings["interrupted"])
+
+ if is_forum and not is_each and utils.get_topic(message) != utils.get_topic(i):
+ continue
+
+ if i.from_id == self.tg_id and self.is_valid_type(i, types_filter):
+ if reply:
+ if is_last:
+ break
+ if i.id == reply.id:
+ is_last = True
+ await message.client.delete_messages(message.peer_id, [i.id])
+
+ if reply:
+ await utils.answer(message, self.strings["purge_reply_complete"])
+ else:
+ await utils.answer(message, self.strings["purge_complete"])
+
+ async def purgekeywordcmd(self, message: Message):
+ """ [-img] [-voice] [-file] [-all] - delete all your messages containing the specified keyword in the current chat
+ -all - to delete messages in each topic if this is a forum otherwise the flag'll just be ingored
+ """
+ args = utils.get_args_raw(message)
+ if not args:
+ return await utils.answer(message, "Please specify anything because you didn't.")
+
+ args, types_filter, is_each = self.get_types_filter(message)
+ if not args:
+ return await utils.answer(message, "Please specify a keyword to delete messages.")
+
+ is_forum = (await self.client.get_entity(message.chat.id)).forum
+
+ status = self.db.get(__name__, "status", {})
+ status[message.chat.id] = True
+ self.db.set(__name__, "status", status)
+
+ async for i in self.client.iter_messages(message.peer_id):
+ status = self.db.get(__name__, "status", {})
+ if status.get(message.chat.id, None) is not True:
+ return await utils.answer(message, self.strings["interrupted"])
+
+ if is_forum and not is_each and utils.get_topic(message) != utils.get_topic(i):
+ continue
+
+ if i.from_id == self.tg_id and args.lower() in (i.text or '').lower() and self.is_valid_type(i, types_filter):
+ await message.client.delete_messages(message.chat.id, [i.id])
+
+ await utils.answer(message, self.strings["purge_keyword_complete"])
+
+ async def purgetimecmd(self, message: Message):
+ """ [-img] [-voice] [-file] [-all] - delete all your messages within the specified time range in the current chat
+ -all - to delete messages in each topic if this is a forum otherwise the flag'll just be ingored
+ Time format: YYYY-MM-DD HH:MM:SS
+ """
+ args = utils.get_args_raw(message)
+ if not args:
+ return await utils.answer(message, "Please specify anything because you didn't.")
+
+ args, types_filter, is_each = self.get_types_filter(message)
+ args = args.split()
+
+ if not args or len(args) < 2:
+ return await utils.answer(message, "Please specify the start and end time in the format: YYYY-MM-DD HH:MM:SS")
+
+ from datetime import datetime
+
+ try:
+ start_time = datetime.strptime(args[0], "%Y-%m-%d %H:%M:%S")
+ end_time = datetime.strptime(args[1], "%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ return await utils.answer(message, "Invalid time format. Please use the format: YYYY-MM-DD HH:MM:SS")
+
+ is_forum = (await self.client.get_entity(message.chat.id)).forum
+
+ status = self.db.get(__name__, "status", {})
+ status[message.chat.id] = True
+ self.db.set(__name__, "status", status)
+
+ async for i in self.client.iter_messages(message.peer_id):
+ status = self.db.get(__name__, "status", {})
+ if status.get(message.chat.id, None) is not True:
+ return await utils.answer(message, self.strings["interrupted"])
+
+ if is_forum and not is_each and utils.get_topic(message) != utils.get_topic(i):
+ continue
+
+ if i.from_id == self.tg_id and start_time <= i.date <= end_time and self.is_valid_type(i, types_filter):
+ await message.client.delete_messages(message.peer_id, [i.id])
+
+ await utils.answer(message, self.strings["purge_time_complete"])
+
+ async def purgelengthcmd(self, message: Message):
+ """ [-img] [-voice] [-file] [-all] - delete all your messages with the specified length in the current chat
+ -all - to delete messages in each topic if this is a forum otherwise the flag'll just be ingored
+ """
+ args = utils.get_args_raw(message)
+ if not args:
+ return await utils.answer(message, "Please specify anything because you didn't.")
+
+ args, types_filter, is_each = self.get_types_filter(message)
+ if not args:
+ return await utils.answer(message, "Please specify a valid length.")
+
+ length = int(args)
+ is_forum = (await self.client.get_entity(message.chat.id)).forum
+
+ status = self.db.get(__name__, "status", {})
+ status[message.chat.id] = True
+ self.db.set(__name__, "status", status)
+
+ async for i in self.client.iter_messages(message.peer_id):
+ status = self.db.get(__name__, "status", {})
+ if status.get(message.chat.id, None) is not True:
+ return await utils.answer(message, self.strings["interrupted"])
+
+ if is_forum and not is_each and utils.get_topic(message) != utils.get_topic(i):
+ continue
+
+ if i.from_id == self.tg_id and len(i.text or '') == length and self.is_valid_type(i, types_filter):
+ await message.client.delete_messages(message.peer_id, [i.id])
+
+ await utils.answer(message, self.strings["purge_length_complete"])
+
+ async def nopurgecmd(self, message: Message):
+ """
+ Interrupt the deletion process
+ Use in the chat where you've previously started deletion
+ """
+ chat_id = utils.get_chat_id(message)
+
+ status = self.db.get(__name__, "status", {})
+ _status = status.get(chat_id, None)
+ status[chat_id] = False
+ self.db.set(__name__, "status", status)
+
+ if _status is True:
+ await utils.answer(message, self.strings["disabled"])
+ elif _status is False:
+ await utils.answer(message, self.strings["enabled"])
+ else:
+ await utils.answer(message, self.strings["none"])
+
+
+ def get_types_filter(self, message: Message):
+ """ Get the types filter from the command arguments."""
+ args = utils.get_args_raw(message).split()
+ types_filter = []
+ valid_types = ["-img", "-voice", "-file", "-all"]
+ is_each = "-all" in args
+
+ for i, arg in enumerate(args):
+ if arg in valid_types:
+ _args = " ".join(args[:i])
+ args_ = " ".join(args[i:])
+ break
+
+ if "-img" in args_:
+ types_filter.append("img")
+ if "-voice" in args_:
+ types_filter.append("voice")
+ if "-file" in args_:
+ types_filter.append("file")
+ if "-all" in args_:
+ is_each = True
+
+ return _args, types_filter, is_each
+
+ def is_valid_type(self, message: Message, types_filter):
+ """ Check if the message matches the specified types filter. """
+ if not types_filter:
+ return True # No filtering means all types are valid
+
+ if "img" in types_filter and message.photo:
+ return True
+ if "voice" in types_filter and message.voice:
+ return True
+ if "file" in types_filter and isinstance(message.document, DocumentAttributeFilename):
+ return True
+
+ return False
diff --git a/archquise/H.Modules/face.py b/archquise/H.Modules/face.py
index 2cf7f27..99aaf23 100644
--- a/archquise/H.Modules/face.py
+++ b/archquise/H.Modules/face.py
@@ -27,10 +27,15 @@
# requires: aiohttp
# ---------------------------------------------------------------------------------
+import logging
+
import aiohttp
+import re
+import random
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class face(loader.Module):
@@ -64,15 +69,16 @@ class face(loader.Module):
async def rfacecmd(self, message):
await utils.answer(message, self.strings("loading"))
- url = "https://vsecoder.dev/api/faces"
+ url = "https://files.archquise.ru/kaomoji.txt"
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
- data = await response.json()
- random_face = data["data"]
+ 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(random_face)
+ message, self.strings("random_face").format(kaomoji)
)
else:
await utils.answer(message, self.strings("error"))
diff --git a/archquise/H.Modules/full.txt b/archquise/H.Modules/full.txt
index 685e9da..b54203a 100644
--- a/archquise/H.Modules/full.txt
+++ b/archquise/H.Modules/full.txt
@@ -1,47 +1,54 @@
+ASCIIArt
AccountData
-AniLibria
-animals
+AniLiberty
AnimeQuotes
Article
-ASCIIArt
AutofarmCookies
BirthdayTime
CheckSpamBan
+CodeShare
CryptoCurrency
-EnvsSH
-face
FakeActions
FakeWallet
-full
+FolderAutoRead
GigaChat
-globalrestrict
-hikkahost
+H
+HAFK
+HInstall
+InfoBannersManager
InlineButton
InlineCoin
InlineHelper
IrisSimpleMod
-jacques
KBSwapper
Memes
+MessageMonitor
+MooFarmRC1
Music
-novoice
-nsfwart
-numbersapi
-PastebinAPI
-profile
ReplaceVowels
-SafetyMod
-search
-shortener
SMAcrhiver
+TaskManager
TelegramStatusCodes
TempChat
Text2File
-Text_Sticker
TikTokDownloader
+TimedEmojiStatus
+UserbotAvast
Video2GIF
VirusTotal
VoiceDL
-HAFK
Weather
-InfoBannersManager
+WindowsKeys
+animals
+face
+globalrestrict
+hikkahost
+mediatools
+novoice
+nsfwart
+passgen
+profile
+search
+shortener
+timezone
+ytdl
diff --git a/archquise/H.Modules/globalrestrict.py b/archquise/H.Modules/globalrestrict.py
index 91fdaac..9f642a1 100644
--- a/archquise/H.Modules/globalrestrict.py
+++ b/archquise/H.Modules/globalrestrict.py
@@ -38,6 +38,7 @@
# scope: Api GlobalRestrict 0.0.1
# ---------------------------------------------------------------------------------
+import logging
import re
import time
import typing
@@ -51,6 +52,8 @@ from telethon.tl.types import (
from .. import loader, utils
+logger = logging.getLogger(__name__)
+
BANNED_RIGHTS = {
"view_messages": False,
"send_messages": False,
diff --git a/archquise/H.Modules/hikkahost.py b/archquise/H.Modules/hikkahost.py
index c9ccf6c..43de8aa 100644
--- a/archquise/H.Modules/hikkahost.py
+++ b/archquise/H.Modules/hikkahost.py
@@ -26,12 +26,15 @@
# scope: api HikkaHost 0.0.1
# ---------------------------------------------------------------------------------
-import aiohttp
import json
+import logging
from datetime import datetime, timedelta, timezone
+import aiohttp
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
class HostApi:
"""
@@ -55,7 +58,7 @@ class HostApi:
Returns:
dict: The API response as a dictionary.
"""
- url = "http://158.160.84.24:5000" + path
+ url = "http://api.hikka.host" + path
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.request(
method,
@@ -311,4 +314,4 @@ class HikkahostMod(loader.Module):
user_id = token.split(":")[0]
api = HostApi(token)
- data = await api.action(user_id, token)
+ await api.action(user_id, token)
diff --git a/archquise/H.Modules/jacques.py b/archquise/H.Modules/jacques.py
index 4c8275e..3486400 100644
--- a/archquise/H.Modules/jacques.py
+++ b/archquise/H.Modules/jacques.py
@@ -27,13 +27,16 @@
# scope: Жаконизатор 0.0.1
# ---------------------------------------------------------------------------------
-import aiohttp
import io
-from PIL import Image, ImageDraw, ImageFont
+import logging
from textwrap import wrap
+import aiohttp
+from PIL import Image, ImageDraw, ImageFont
+
from .. import loader, utils
+logger = logging.getLogger(__name__)
@loader.tds
class JacquesMod(loader.Module):
diff --git a/archquise/H.Modules/mediatools.py b/archquise/H.Modules/mediatools.py
new file mode 100644
index 0000000..e91fd9e
--- /dev/null
+++ b/archquise/H.Modules/mediatools.py
@@ -0,0 +1,770 @@
+# Proprietary License Agreement
+
+# Copyright (c) 2024-29 CodWiz
+
+# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
+
+# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
+
+# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
+
+# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
+
+# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
+
+# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
+
+# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
+
+# ---------------------------------------------------------------------------------
+# Name: MediaTools
+# Description: Powerful tools for working with media files
+# Author: @hikka_mods
+# ---------------------------------------------------------------------------------
+# meta developer: @hikka_mods
+# scope: MediaTools
+# scope: MediaTools 0.0.1
+# ---------------------------------------------------------------------------------
+
+import asyncio
+import logging
+import math
+import os
+import re
+import shutil
+from typing import Optional
+
+from telethon.types import Message
+
+from .. import loader, utils
+
+logger = logging.getLogger(__name__)
+
+def check_ffmpeg():
+ return shutil.which("ffmpeg") is not None
+
+
+@loader.tds
+class MediaToolsMod(loader.Module):
+ """Powerful tools for working with media files"""
+
+ strings = {
+ "name": "MediaTools",
+ "no_reply": "🚫 Reply to a media file!",
+ "no_ffmpeg": "❌ FFmpeg is not installed! Install: apt-get install ffmpeg",
+ "processing": "⚙️ Processing...",
+ "converted": "✅ Converted to {}",
+ "downloaded": "✅ Voice message saved",
+ "gif_created": "✅ GIF created",
+ "cut_done": "✅ Trimmed",
+ "circle_done": "✅ Video circle created",
+ "audio_extracted": "✅ Audio extracted",
+ "compressed": "✅ Compressed to {}",
+ "split_done": "✅ Split into {} parts",
+ "merged": "✅ Merged",
+ "metadata_removed": "✅ Metadata removed",
+ "invalid_args": "❌ Invalid arguments",
+ "error": "❌ Error: {}",
+ "available_formats": "Available formats:\n🎵 Audio: mp3, flac, wav, aac, ogg, m4a, opus\n🎬 Video: mp4, avi, mkv, mov, wmv, flv, webm, 3gp, hevc, h264",
+ "cut_usage": "Usage: .cut 20s6ms:8m16s3ms",
+ "compress_usage": "Available qualities: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p",
+ "split_time_usage": "Example: .split 10m (10 minutes)",
+ "split_size_usage": "Usage: .split 10m or .split 5MB",
+ "merge_usage": "Reply to first video/audio",
+ "min_files": "Need at least 2 media files in chain",
+ "downloading": "Downloading {} files...",
+ "part": "Part {}/{}",
+ }
+
+ strings_ru = {
+ "no_reply": "🚫 Ответьте на медиафайл!",
+ "no_ffmpeg": "❌ FFmpeg не установлен! Установите: apt-get install ffmpeg",
+ "processing": "⚙️ Обрабатываю...",
+ "converted": "✅ Конвертировано в {}",
+ "downloaded": "✅ Голосовое сохранено",
+ "gif_created": "✅ GIF создан",
+ "cut_done": "✅ Обрезано",
+ "circle_done": "✅ Видео в кружок",
+ "audio_extracted": "✅ Аудио извлечено",
+ "compressed": "✅ Сжато до {}",
+ "split_done": "✅ Разделено на {} частей",
+ "merged": "✅ Объединено",
+ "metadata_removed": "✅ Метаданные удалены",
+ "invalid_args": "❌ Неверные аргументы",
+ "error": "❌ Ошибка: {}",
+ "available_formats": "Доступные форматы:\n🎵 Аудио: mp3, flac, wav, aac, ogg, m4a, opus\n🎬 Видео: mp4, avi, mkv, mov, wmv, flv, webm, 3gp, hevc, h264",
+ "cut_usage": "Используйте: .cut 20с6мс:8м16с3мс",
+ "compress_usage": "Доступные качества: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p",
+ "split_time_usage": "Пример: .split 10m (10 минут)",
+ "split_size_usage": "Используйте: .split 10m или .split 5MB",
+ "merge_usage": "Ответьте на первое видео/аудио",
+ "min_files": "Нужно как минимум 2 медиафайла в цепочке",
+ "downloading": "Скачиваю {} файлов...",
+ "part": "Часть {}/{}",
+ }
+
+ async def client_ready(self, client, db):
+ self._client = client
+ self._db = db
+ if not check_ffmpeg():
+ self.logger.warning(self.strings["no_ffmpeg"])
+
+ @loader.command(
+ ru_doc="<формат> - конвертировать медиа в указанный формат",
+ en_doc=" - convert media to specified format",
+ )
+ async def convert(self, message: Message):
+ reply = await message.get_reply_message()
+ if not reply or not reply.media:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ if not check_ffmpeg():
+ return await utils.answer(message, self.strings["no_ffmpeg"])
+
+ args = utils.get_args_raw(message).lower()
+ formats = {
+ "mp3": "audio",
+ "flac": "audio",
+ "wav": "audio",
+ "aac": "audio",
+ "ogg": "audio",
+ "m4a": "audio",
+ "opus": "audio",
+ "mp4": "video",
+ "avi": "video",
+ "mkv": "video",
+ "mov": "video",
+ "wmv": "video",
+ "flv": "video",
+ "webm": "video",
+ "3gp": "video",
+ "hevc": "video",
+ "h264": "video",
+ }
+
+ if not args or args not in formats:
+ return await utils.answer(message, self.strings["available_formats"])
+
+ msg = await utils.answer(message, self.strings["processing"])
+
+ try:
+ file = await reply.download_media(file="temp/")
+ output = f"{file.rsplit('.', 1)[0]}_converted.{args}"
+
+ cmd = ["ffmpeg", "-i", file, "-y"]
+ if formats[args] == "audio":
+ if args == "mp3":
+ cmd.extend(["-codec:a", "libmp3lame", "-q:a", "2"])
+ elif args == "flac":
+ cmd.extend(["-codec:a", "flac", "-compression_level", "12"])
+ elif args == "opus":
+ cmd.extend(["-codec:a", "libopus", "-b:a", "128k"])
+ elif args == "aac":
+ cmd.extend(["-codec:a", "aac", "-b:a", "192k"])
+ elif formats[args] == "video":
+ if args in ["hevc", "h264"]:
+ codec = "libx265" if args == "hevc" else "libx264"
+ cmd.extend(["-codec:v", codec, "-preset", "medium", "-crf", "23"])
+ if args == "webm":
+ cmd.extend(["-codec:v", "libvpx-vp9", "-b:v", "1M"])
+
+ cmd.append(output)
+
+ process = await asyncio.create_subprocess_exec(
+ *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
+ )
+ await process.communicate()
+
+ await message.client.send_file(
+ message.peer_id,
+ output,
+ caption=self.strings["converted"].format(args),
+ reply_to=reply.id,
+ )
+
+ os.remove(file)
+ if os.path.exists(output):
+ os.remove(output)
+
+ await msg.delete()
+
+ except Exception as e:
+ await utils.answer(message, self.strings["error"].format(str(e)))
+
+ @loader.command(
+ ru_doc="Скачать голосовое сообщение как файл",
+ en_doc="Download voice message as file",
+ )
+ async def voicedl(self, message: Message):
+ reply = await message.get_reply_message()
+ if not reply or not reply.voice:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ msg = await utils.answer(message, self.strings["processing"])
+
+ try:
+ file = await reply.download_media(file="temp/voice.ogg")
+ new_file = file.replace(".ogg", ".opus")
+ os.rename(file, new_file)
+
+ await message.client.send_file(
+ message.peer_id,
+ new_file,
+ caption=self.strings["downloaded"],
+ reply_to=reply.id,
+ voice_note=False,
+ )
+
+ os.remove(new_file)
+ await msg.delete()
+
+ except Exception as e:
+ await utils.answer(message, self.strings["error"].format(str(e)))
+
+ @loader.command(ru_doc="Преобразовать видео в GIF", en_doc="Convert video to GIF")
+ async def gif(self, message: Message):
+ if not check_ffmpeg():
+ return await utils.answer(message, self.strings["no_ffmpeg"])
+
+ reply = await message.get_reply_message()
+ if not reply or not reply.video:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ msg = await utils.answer(message, self.strings["processing"])
+
+ try:
+ file = await reply.download_media(file="temp/")
+ output = f"{file.rsplit('.', 1)[0]}.gif"
+
+ cmd = [
+ "ffmpeg",
+ "-i",
+ file,
+ "-vf",
+ "fps=10,scale=480:-1:flags=lanczos",
+ "-gifflags",
+ "+transdiff",
+ "-y",
+ output,
+ ]
+
+ process = await asyncio.create_subprocess_exec(*cmd)
+ await process.communicate()
+
+ await message.client.send_file(
+ message.peer_id,
+ output,
+ caption=self.strings["gif_created"],
+ reply_to=reply.id,
+ )
+
+ os.remove(file)
+ os.remove(output)
+ await msg.delete()
+
+ except Exception as e:
+ await utils.answer(message, self.strings["error"].format(str(e)))
+
+ def parse_time(self, time_str: str) -> Optional[float]:
+ time_str = time_str.lower()
+ total = 0
+ pattern = r"(\d+\.?\d*)([мm]?[сc]|[мm][сc]?)"
+ matches = re.findall(pattern, time_str)
+
+ for value, unit in matches:
+ value = float(value)
+ if "м" in unit or "m" in unit:
+ total += value * 60
+ elif "с" in unit or "c" in unit:
+ total += value
+
+ return total if total > 0 else None
+
+ @loader.command(
+ ru_doc="<начало:конец> - обрезать медиа по времени",
+ en_doc=" - trim media by time",
+ )
+ async def cut(self, message: Message):
+ if not check_ffmpeg():
+ return await utils.answer(message, self.strings["no_ffmpeg"])
+
+ reply = await message.get_reply_message()
+ if not reply or not reply.media:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ args = utils.get_args_raw(message)
+ if not args or ":" not in args:
+ return await utils.answer(message, self.strings["cut_usage"])
+
+ start_str, end_str = args.split(":", 1)
+ start = self.parse_time(start_str)
+ end = self.parse_time(end_str)
+
+ if start is None or end is None or start >= end:
+ return await utils.answer(message, self.strings["invalid_args"])
+
+ msg = await utils.answer(message, self.strings["processing"])
+
+ try:
+ file = await reply.download_media(file="temp/")
+ output = f"{file.rsplit('.', 1)[0]}_cut.{file.rsplit('.', 1)[1]}"
+
+ cmd = [
+ "ffmpeg",
+ "-i",
+ file,
+ "-ss",
+ str(start),
+ "-to",
+ str(end),
+ "-c",
+ "copy",
+ "-avoid_negative_ts",
+ "make_zero",
+ "-y",
+ output,
+ ]
+
+ process = await asyncio.create_subprocess_exec(*cmd)
+ await process.communicate()
+
+ await message.client.send_file(
+ message.peer_id,
+ output,
+ caption=self.strings["cut_done"],
+ reply_to=reply.id,
+ )
+
+ os.remove(file)
+ os.remove(output)
+ await msg.delete()
+
+ except Exception as e:
+ await utils.answer(message, self.strings["error"].format(str(e)))
+
+ @loader.command(
+ ru_doc="[начало:конец] - Видео в кружок",
+ en_doc="[start:end] - Convert video to circle",
+ )
+ async def vircle(self, message: Message):
+ if not check_ffmpeg():
+ return await utils.answer(message, self.strings["no_ffmpeg"])
+
+ reply = await message.get_reply_message()
+ if not reply or not (reply.video or reply.gif):
+ return await utils.answer(message, self.strings["no_reply"])
+
+ args = utils.get_args_raw(message)
+ filter_args = ""
+
+ if args and ":" in args:
+ start_str, end_str = args.split(":", 1)
+ start = self.parse_time(start_str)
+ end = self.parse_time(end_str)
+
+ if start is not None and end is not None and start < end:
+ filter_args = f",trim=start={start}:end={end},setpts=PTS-STARTPTS"
+
+ msg = await utils.answer(message, self.strings["processing"])
+
+ try:
+ file = await reply.download_media(file="temp/")
+ output = f"{file.rsplit('.', 1)[0]}_circle.mp4"
+
+ cmd = [
+ "ffmpeg",
+ "-i",
+ file,
+ "-vf",
+ f"scale=720:720:force_original_aspect_ratio=increase,crop=720:720{filter_args},format=rgba,geq='if(gt(X,360),if(gt(Y,360),if(lt(sqrt((X-360)^2+(Y-360)^2),360),p(X,Y),0),0),0)'",
+ "-c:v",
+ "libx264",
+ "-preset",
+ "fast",
+ "-crf",
+ "23",
+ "-pix_fmt",
+ "yuv420p",
+ "-y",
+ output,
+ ]
+
+ process = await asyncio.create_subprocess_exec(*cmd)
+ await process.communicate()
+
+ await message.client.send_file(
+ message.peer_id,
+ output,
+ caption=self.strings["circle_done"],
+ reply_to=reply.id,
+ video_note=True,
+ )
+
+ os.remove(file)
+ os.remove(output)
+ await msg.delete()
+
+ except Exception as e:
+ await utils.answer(message, self.strings["error"].format(str(e)))
+
+ @loader.command(
+ ru_doc="[начало:конец] - Извлечь аудио из видео",
+ en_doc="[start:end] - Extract audio from video",
+ )
+ async def vsound(self, message: Message):
+ if not check_ffmpeg():
+ return await utils.answer(message, self.strings["no_ffmpeg"])
+
+ reply = await message.get_reply_message()
+ if not reply or not reply.video:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ args = utils.get_args_raw(message)
+ msg = await utils.answer(message, self.strings["processing"])
+
+ try:
+ file = await reply.download_media(file="temp/")
+ output = f"{file.rsplit('.', 1)[0]}_audio.mp3"
+
+ cmd = ["ffmpeg", "-i", file]
+ if args and ":" in args:
+ start_str, end_str = args.split(":", 1)
+ start = self.parse_time(start_str)
+ end = self.parse_time(end_str)
+
+ if start is not None and end is not None and start < end:
+ cmd.extend(["-ss", str(start), "-to", str(end)])
+
+ cmd.extend(["-q:a", "2", "-map", "a", "-y", output])
+
+ process = await asyncio.create_subprocess_exec(*cmd)
+ await process.communicate()
+
+ await message.client.send_file(
+ message.peer_id,
+ output,
+ caption=self.strings["audio_extracted"],
+ reply_to=reply.id,
+ )
+
+ os.remove(file)
+ os.remove(output)
+ await msg.delete()
+
+ except Exception as e:
+ await utils.answer(message, self.strings["error"].format(str(e)))
+
+ @loader.command(
+ ru_doc="<качество> - Сжать видео", en_doc=" - Compress video"
+ )
+ async def compress(self, message: Message):
+ if not check_ffmpeg():
+ return await utils.answer(message, self.strings["no_ffmpeg"])
+
+ reply = await message.get_reply_message()
+ if not reply or not reply.video:
+ return await utils.answer(message, self.strings["no_reply"])
+
+ args = utils.get_args_raw(message).lower()
+ resolutions = {
+ "144p": "256x144",
+ "240p": "426x240",
+ "360p": "640x360",
+ "480p": "854x480",
+ "720p": "1280x720",
+ "1080p": "1920x1080",
+ "1440p": "2560x1440",
+ "2160p": "3840x2160",
+ }
+
+ if not args or args not in resolutions:
+ return await utils.answer(message, self.strings["compress_usage"])
+
+ msg = await utils.answer(message, self.strings["processing"])
+
+ try:
+ file = await reply.download_media(file="temp/")
+ output = f"{file.rsplit('.', 1)[0]}_compressed.mp4"
+
+ probe_cmd = [
+ "ffprobe",
+ "-v",
+ "error",
+ "-select_streams",
+ "v:0",
+ "-show_entries",
+ "stream=bit_rate",
+ "-of",
+ "default=noprint_wrappers=1:nokey=1",
+ file,
+ ]
+
+ process = await asyncio.create_subprocess_exec(
+ *probe_cmd,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+ stdout, _ = await process.communicate()
+ original_bitrate = stdout.decode().strip()
+
+ scale_factor = {
+ "144p": 0.1,
+ "240p": 0.2,
+ "360p": 0.3,
+ "480p": 0.4,
+ "720p": 0.6,
+ "1080p": 0.8,
+ "1440p": 0.9,
+ "2160p": 1.0,
+ }
+
+ target_bitrate = "500k"
+ if original_bitrate and original_bitrate.isdigit():
+ original_br = int(original_bitrate)
+ target_br = int(original_br * scale_factor[args] / 1000)
+ target_bitrate = f"{max(200, target_br)}k"
+
+ cmd = [
+ "ffmpeg",
+ "-i",
+ file,
+ "-vf",
+ f"scale={resolutions[args]}:force_original_aspect_ratio=decrease",
+ "-c:v",
+ "libx264",
+ "-preset",
+ "medium",
+ "-b:v",
+ target_bitrate,
+ "-maxrate",
+ target_bitrate,
+ "-bufsize",
+ f"{int(target_bitrate[:-1]) * 2}k",
+ "-c:a",
+ "aac",
+ "-b:a",
+ "128k",
+ "-y",
+ output,
+ ]
+
+ process = await asyncio.create_subprocess_exec(*cmd)
+ await process.communicate()
+
+ await message.client.send_file(
+ message.peer_id,
+ output,
+ caption=self.strings["compressed"].format(args),
+ reply_to=reply.id,
+ )
+
+ os.remove(file)
+ os.remove(output)
+ await msg.delete()
+
+ except Exception as e:
+ await utils.answer(message, self.strings["error"].format(str(e)))
+
+ @loader.command(
+ ru_doc="<время/размер> - Разделить медиа на части",
+ en_doc="