Merge pull request #149 from MuRuLOSE:update-submodules_1c99e02dd0d6aea77015f2b572b71ea751c066c0

Update of repositories 2026-01-10 01:11:19
This commit is contained in:
Macsim
2026-01-10 16:42:19 +03:00
committed by GitHub
68 changed files with 5908 additions and 1937 deletions

BIN
archquise/H.Modules/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -3,4 +3,5 @@ autocleaner.py
silent.py silent.py
# Ruff Format # Ruff Format
.ruff_cache/ .ruff_cache/
.idea

View File

@@ -27,12 +27,15 @@
# requires: pillow # requires: pillow
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import os import os
import tempfile import tempfile
from PIL import Image from PIL import Image
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class ASCIIArtMod(loader.Module): class ASCIIArtMod(loader.Module):
@@ -99,7 +102,7 @@ class ASCIIArtMod(loader.Module):
) )
except Exception as e: except Exception as e:
print(f"Error generating ASCII art: {e}") logger.error(f"Error generating ASCII art: {e}")
return None return None
finally: finally:
if image_path and os.path.exists(image_path): if image_path and os.path.exists(image_path):

View File

@@ -26,15 +26,29 @@
# scope: Api AccountData 0.0.1 # scope: Api AccountData 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
from datetime import datetime
import aiohttp import aiohttp
from datetime import datetime
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class AccountData(loader.Module): class AccountData(loader.Module):
"""Find out the approximate date of registration of the telegram account""" """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 = { strings = {
"name": "AccountData", "name": "AccountData",
"_cls_doc": "Find out the approximate date of registration of the telegram account", "_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: 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" url = "https://api.datereg.pro/api/v1/users/getCreationDateFast"
params = {"token": api_token, "user_id": user_id} params = {"token": api_token, "user_id": user_id}
@@ -76,16 +93,22 @@ class AccountData(loader.Module):
async def accdata(self, message): async def accdata(self, message):
if reply := await message.get_reply_message(): if reply := await message.get_reply_message():
result = await self.get_creation_date(user_id=reply.sender.id) 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) if "error" in result or not result.get("creation_date"):
formatted = date_object.strftime('%B %Y') error_msg = result.get("error", "Unknown error occurred")
await utils.answer(message, f"Ошибка: {error_msg}")
if "error" in result: return
await utils.answer(message, f"Ошибка: {result['error']}")
else: try:
month, year = map(int, result['creation_date'].split('.'))
date_object = datetime(year, month, 1)
formatted = date_object.strftime('%B %Y')
await utils.answer( await utils.answer(
message, message,
f"{self.strings('date_text').format(data=formatted, accuracy=result['accuracy_percent'])}\n\n{self.strings('date_text_ps')}", 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: else:
await utils.answer(message, self.strings("no_reply")) await utils.answer(message, self.strings("no_reply"))

View File

@@ -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": "<b>The announcement</b>:",
"ongoing": "<b>Ongoing</b>:",
"type": "<b>Type</b>:",
"genres": "<b>Genres</b>:",
"favorite": "<b>Favourites &lt;3</b>:", # &lt; == <
}
strings_ru = {
"announce": "<b>Анонс</b>:",
"ongoing": "<b>Онгоинг</b>:",
"type": "<b>Тип</b>:",
"genres": "<b>Жанры</b>:",
"favorite": "<b>Избранное &lt;3</b>:", # &lt; == <
}
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"<code>{anime_release.description}</code>\n\n"
text += f"{self.strings['favorite']} {str(anime_release.added_in_users_favorites)}"
kb = [
[
{
"text": "Ссылка",
"url": f"https://aniliberty.top/anime/releases/release/{anime_release.alias}/episodes",
}
]
]
kb.append([{"text": "🔃 Обновить", "callback": self.inline__update}])
kb.append([{"text": "🚫 Закрыть", "callback": self.inline__close}])
await self.inline.form(
text=text,
photo=f"https://aniliberty.top{anime_release.poster.preview}",
message=message,
reply_markup=kb,
silent=True,
)
@loader.inline_handler(
ru_doc="Возвращает список найденных по названию тайтлов",
en_doc="Returns a list of titles found by name",
)
async def asearch_inline_handler(self, query: InlineQuery) -> None:
text = query.args
if not text:
return
anime_releases = await self.search_title(text)
inline_query = []
for anime_release in anime_releases:
"""
Приходится запрашивать по второму кругу, т.к. API в поиске не отдает жанры, даже если попросить через include
"""
release_genres = await self.get_title(anime_release.id)
genres_str = ""
for genre in release_genres.genres[:-1]:
genres_str += f'{genre.name}, '
genres_str += release_genres.genres[-1].name
release_text = (
f"{anime_release.name.main}\n"
f"{self.strings['ongoing']} {"Да" if anime_release.is_ongoing else "Нет"}\n\n"
f"{self.strings['type']} {anime_release.type.description}\n"
f"{self.strings['genres']} {genres_str}\n\n"
f"<code>{anime_release.description}</code>\n\n"
f"{self.strings['favorite']} {anime_release.added_in_users_favorites}"
)
inline_query.append(
InlineQueryResultPhoto(
id=str(anime_release.id),
title=anime_release.name.main,
description=anime_release.type.description,
caption=release_text,
thumbnail_url=f"https://aniliberty.top{anime_release.poster.thumbnail}",
photo_url=f"https://aniliberty.top{anime_release.poster.preview}",
parse_mode="html",
)
)
method = query.answer(inline_query, cache_time=0)
await method.as_(self.inline.bot)
async def inline__close(self, call: CallbackQuery) -> None:
await call.delete()
async def inline__update(self, call: CallbackQuery) -> None:
anime_release = await self.get_random_title()
genres_str = ""
for genre in anime_release.genres[:-1]:
genres_str += f'{genre.name}, '
genres_str += anime_release.genres[-1].name
text = f"{anime_release.name.main} \n"
text += f"{self.strings['ongoing']} {"Да" if anime_release.is_ongoing else "Нет"}\n\n"
text += f"{self.strings['type']} {anime_release.type.description}\n"
text += f"{self.strings['genres']} {genres_str}\n\n"
text += f"<code>{anime_release.description}</code>\n\n"
text += f"{self.strings['favorite']} {str(anime_release.added_in_users_favorites)}"
kb = [
[
{
"text": "Ссылка",
"url": f"https://aniliberty.top/anime/releases/release/{anime_release.alias}/episodes",
}
]
]
kb.append([{"text": "🔃 Обновить", "callback": self.inline__update}])
kb.append([{"text": "🚫 Закрыть", "callback": self.inline__close}])
await call.edit(
text=text,
photo=f"https://aniliberty.top{anime_release.poster.preview}",
reply_markup=kb,
)

View File

@@ -1,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": "<b>The announcement</b>:",
"status": "<b>Status</b>:",
"type": "<b>Type</b>:",
"genres": "<b>Genres</b>:",
"favorite": "<b>Favourites &lt;3</b>:", # &lt; == <
"season": "<b>Season</b>:",
}
strings_ru = {
"announce": "<b>Анонс</b>:",
"status": "<b>Статус</b>:",
"type": "<b>Тип</b>:",
"genres": "<b>Жанры</b>:",
"favorite": "<b>Избранное &lt;3</b>:", # &lt; == <
"season": "<b>Сезон</b>:",
}
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"<code>{anime_title.description}</code>\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"<code>{anime_title.description}</code>\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"<code>{anime_title.description}</code>\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,
)

View File

@@ -27,10 +27,13 @@
# requires: requests # requires: requests
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import aiohttp import aiohttp
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class AnimeQuotesMod(loader.Module): class AnimeQuotesMod(loader.Module):

View File

@@ -27,13 +27,16 @@
# requires: requests # requires: requests
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import requests
import json import json
import logging
import random import random
from typing import Dict from typing import Dict
import requests
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class ArticleMod(loader.Module): class ArticleMod(loader.Module):

View File

@@ -26,15 +26,17 @@
# scope: AutofarmCookies 0.0.1 # scope: AutofarmCookies 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import random import random
from datetime import timedelta from datetime import timedelta
from telethon import functions from telethon import functions
from telethon.tl.custom import Message
from .. import loader, utils from .. import loader, utils
__version__ = (1, 0, 0) __version__ = (1, 0, 0)
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class AutofarmCookiesMod(loader.Module): class AutofarmCookiesMod(loader.Module):
@@ -197,8 +199,7 @@ class AutofarmCookiesMod(loader.Module):
async def ckies(self, message): async def ckies(self, message):
chelp = """ chelp = """
🍀| <b>Помощь по командам:</b> 🍀| <b>Помощь по командам:</b>
.cookon - Включает авто фарм. .cookon - Включает авто-фарм.
.cookoff - Выключает авто фарм. .cookoff - Выключает авто-фарм.
.farm - Показывает сколько вы нафармили. .me - Показывает ваш мешок"""
.me - Показывает ваш ммешок"""
await utils.answer(message, chelp) await utils.answer(message, chelp)

View File

@@ -26,17 +26,20 @@
# scope: Api BirthdayTime 0.0.1 # scope: Api BirthdayTime 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import random
import asyncio import asyncio
import calendar import calendar
import logging
import random
from datetime import datetime 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.errors.rpcerrorlist import UserPrivacyRestrictedError
from telethon.tl.functions.account import UpdateProfileRequest
from telethon.tl.functions.users import GetFullUserRequest
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
D_MSG = [ D_MSG = [
"Ждешь его?", "Ждешь его?",
"Осталось немного)", "Осталось немного)",
@@ -158,9 +161,9 @@ class DaysToMyBirthday(loader.Module):
self.db.set(__name__, "last_name", name) self.db.set(__name__, "last_name", name)
except UserPrivacyRestrictedError: except UserPrivacyRestrictedError:
self.db.set(__name__, "change_name", False) 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: except Exception as e:
print(f"Error in checker: {e}") logger.error(f"Error in checker: {e}")
finally: finally:
await asyncio.sleep(60) await asyncio.sleep(60)
@@ -173,7 +176,7 @@ class DaysToMyBirthday(loader.Module):
user = await self.client(GetFullUserRequest(self.client.hikka_me.id)) user = await self.client(GetFullUserRequest(self.client.hikka_me.id))
name = user.users[0].last_name or "" name = user.users[0].last_name or ""
except Exception as e: 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")) await utils.answer(message, self.strings("error"))
return return
@@ -191,7 +194,7 @@ class DaysToMyBirthday(loader.Module):
except UserPrivacyRestrictedError: except UserPrivacyRestrictedError:
await utils.answer(message, self.strings("name_privacy_error")) await utils.answer(message, self.strings("name_privacy_error"))
except Exception as e: except Exception as e:
print(f"Error removing name: {e}") logger.error(f"Error removing name: {e}")
await utils.answer(message, self.strings("error")) await utils.answer(message, self.strings("error"))
else: else:
@@ -238,5 +241,5 @@ class DaysToMyBirthday(loader.Module):
) )
except Exception as e: 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")) await utils.answer(message, self.strings("error"))

View File

@@ -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": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or reply with a file, or they are invalid",
"_cls_doc": "Uploads your code at the kmi.aeza.net (Pastebin and GitHub Gist alternative)",
"link_ready": "<emoji document_id=5854762571659218443>✅</emoji> <b>Code uploaded! Link:</b> <code>{}</code>",
}
strings_ru = {
"_cls_doc": "Загружает ваш код на kmi.aeza.net (альтернатива Pastebin и GitHub Gist)",
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или реплая с файлом, или они неверны",
"link_ready": "<emoji document_id=5854762571659218443>✅</emoji> <b>Код загружен! Ссылка:</b> <code>{}</code>",
}
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'])

View File

@@ -26,10 +26,13 @@
# scope: Api CryptoCurrency 0.0.1 # scope: Api CryptoCurrency 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import aiohttp import aiohttp
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class CryptoCurrencyMod(loader.Module): class CryptoCurrencyMod(loader.Module):

View File

@@ -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": "<emoji document_id=5854929766146118183>❌</emoji> Need delete messages permission",
"pack_blocked": "<emoji document_id=5854762571659218443>✅</emoji> Pack blocked",
"pack_not_found": "<emoji document_id=5854929766146118183>❌</emoji> Pack not found",
"sticker_blocked": "<emoji document_id=5854929766146118183>❌</emoji> Sticker blocked",
"emoji_blocked": "<emoji document_id=5854929766146118183>❌</emoji> Emoji blocked",
"pack_unblocked": "<emoji document_id=5854762571659218443>✅</emoji> Pack unblocked",
"item_unblocked": "<emoji document_id=5854929766146118183>❌</emoji> Item unblocked",
"not_found": "<emoji document_id=5854929766146118183>❌</emoji> Not in blocklist",
"no_reply": "<emoji document_id=5854929766146118183>❌</emoji> Reply to a sticker or emoji",
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Specify pack link or name",
"list_packs": "📦 Blocked packs: {}",
"list_stickers": "🖼 Blocked stickers: {}",
"list_emojis": "😀 Blocked emojis: {}",
"all_cleared": "✅ All blocks cleared",
}
strings_ru = {
"no_permission": "<emoji document_id=5854929766146118183>❌</emoji> Нужны права на удаление сообщений",
"pack_blocked": "<emoji document_id=5188311512791393083>✅</emoji> Пак заблокирован",
"pack_not_found": "<emoji document_id=5854929766146118183>❌</emoji> Пак не найден",
"sticker_blocked": "<emoji document_id=5854929766146118183>❌</emoji> Стикер заблокирован",
"emoji_blocked": "<emoji document_id=5854929766146118183>❌</emoji> Эмодзи заблокирован",
"pack_unblocked": "<emoji document_id=5854762571659218443>✅</emoji> Пак разблокирован",
"item_unblocked": "<emoji document_id=5854929766146118183>❌</emoji> Элемент разблокирован",
"not_found": "<emoji document_id=5854929766146118183>❌</emoji> Не найден в блоклисте",
"no_reply": "<emoji document_id=5854929766146118183>❌</emoji> Ответьте на стикер или эмодзи",
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Укажите ссылку или название пака",
"list_packs": "📦 Заблокированные паки: {}",
"list_stickers": "🖼 Заблокированные стикеры: {}",
"list_emojis": "😀 Заблокированные эмодзи: {}",
"all_cleared": "Все блоки очищены",
}
def __init__(self):
self.blocked_packs: Set[str] = set()
self.blocked_stickers: Set[str] = set()
self.blocked_emojis: Set[str] = set()
self._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)

View File

@@ -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": "⚠️ <b>You must reply to a message with media</b>",
"success": "✅ URL for <code>{}</code>:\n\n<code>{}</code>",
"error": "❌ An error occurred:\n<code>{}</code>",
"uploading": "⏳ <b>Uploading {} ({}{})...</b>",
}
strings_ru = {
"connection_error": "🚫 Хост в настоящее время недоступен, попробуйте позже.",
"no_reply": "⚠️ <b>Вы должны ответить на сообщение с медиа</b>",
"success": "✅ URL для <code>{}</code>:\n\n<code>{}</code>",
"error": "❌ Произошла ошибка:\n<code>{}</code>",
"uploading": "⏳ <b>Загрузка {} ({}{})...</b>",
}
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)
)

View File

@@ -27,9 +27,11 @@
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import asyncio import asyncio
import logging
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class FakeActionsMod(loader.Module): class FakeActionsMod(loader.Module):

View File

@@ -27,8 +27,11 @@
# scope: hikka_min 1.4.2 # scope: hikka_min 1.4.2
# ----------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------
import logging
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class FakeWallet(loader.Module): class FakeWallet(loader.Module):

View File

@@ -23,8 +23,8 @@
# meta developer: @hikka_mods # meta developer: @hikka_mods
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import os
import logging import logging
from telethon import functions from telethon import functions
from telethon.tl.types import DialogFilter, InputPeerChannel from telethon.tl.types import DialogFilter, InputPeerChannel

View File

@@ -26,8 +26,11 @@
# scope: Api GigaChat 0.0.1 # scope: Api GigaChat 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class GigaChatMod(loader.Module): class GigaChatMod(loader.Module):

View File

@@ -26,10 +26,10 @@
# scope: HAFK 0.0.1 # scope: HAFK 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import asyncio
import datetime import datetime
import logging import logging
import time import time
import asyncio
from telethon import types from telethon import types
from telethon.utils import get_peer_id from telethon.utils import get_peer_id
@@ -135,7 +135,6 @@ class HAFK(loader.Module):
async def _afk_toggle(self, message, global_afk: bool): async def _afk_toggle(self, message, global_afk: bool):
chat_id = utils.get_chat_id(message) 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" 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_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" afk_on_reason_string = "afk_on_reason" if global_afk else "afk_here_on_reason"

View File

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

View File

@@ -1,6 +1,6 @@
# Proprietary License Agreement # 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: # 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 # meta developer: @hikka_mods
from .. import loader, utils
import logging import logging
import random import random
import asyncio
from .. import loader
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -39,8 +38,13 @@ class InfoBannersManagerMod(loader.Module):
strings = {"name": "InfoBannersManager"} strings = {"name": "InfoBannersManager"}
def __init__(self): def __init__(self):
self.changer_instance = None
self.config = loader.ModuleConfig( self.config = loader.ModuleConfig(
loader.ConfigValue(
"enabled",
False,
"Включить автоматическую смену баннеров",
validator=loader.validators.Boolean(),
),
loader.ConfigValue( loader.ConfigValue(
"delay", "delay",
60, 60,
@@ -56,49 +60,49 @@ class InfoBannersManagerMod(loader.Module):
) )
async def banner_changer(self): async def banner_changer(self):
while True: """Change banner periodically"""
try: try:
if not self.config["bannerslist"]: if not self.config["bannerslist"]:
logger.warning("Banners list is empty!") logger.warning("Banners list is empty!")
await asyncio.sleep(10) return
return
banner = random.choice(self.config["bannerslist"]) banner = random.choice(self.config["bannerslist"])
instance = self.lookup("HerokuInfo") instance = self.lookup("HerokuInfo")
if not instance: if not instance:
instance = self.lookup("HikkaInfo") instance = self.lookup("HikkaInfo")
if instance:
instance.config["banner_url"] = banner instance.config["banner_url"] = banner
logger.info(f"Banner changed to: {banner}")
else:
logger.warning("Info module not found!")
except Exception as e: except Exception as e:
logger.exception(f"Caught exception: {e}") logger.exception(f"Error changing banner: {e}")
await asyncio.sleep(10)
await asyncio.sleep(self.config["delay"])
async def on_unload(self): @loader.loop(interval=60, autostart=False)
if self.changer_instance: async def banner_loop(self):
self.changer_instance.cancel() """Main banner changing loop"""
self.changer_instance = None if not self.config["enabled"]:
return
await self.banner_changer()
# Update interval from config
self.banner_loop.set_interval(self.config["delay"])
@loader.command( async def client_ready(self):
ru_doc="Включить или выключить модуль", """Initialize the banner changer loop"""
) if self.config["enabled"]:
async def autobannertoggle(self, message): self.banner_loop.start()
if not self.db.get(__name__, "enabled", False):
try:
if self.changer_instance:
self.changer_instance.cancel()
self.db.set(__name__, "enabled", True) def on_config_update(self, config_key, new_value):
self.changer_instance = asyncio.create_task(self.banner_changer()) """Handle config updates"""
await utils.answer(message, "Модуль запущен!") if config_key == "enabled":
except Exception as e: if new_value:
logger.exception(f"Caught exception: {e}") self.banner_loop.start()
else: else:
try: self.banner_loop.stop()
self.db.set(__name__, "enabled", False) elif config_key == "delay":
await utils.answer(message, "Модуль остановлен!") # Update interval immediately
if self.changer_instance: self.banner_loop.set_interval(new_value)
self.changer_instance.cancel()
self.changer_instance = None
except Exception as e:
logger.exception(f"Caught exception: {e}")

View File

@@ -26,28 +26,35 @@
# scope: InlineButton 0.0.1 # scope: InlineButton 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
from ..inline.types import InlineQuery import logging
from .. import loader, utils from .. import loader, utils
from ..inline.types import InlineQuery
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class InlineButtonMod(loader.Module): class InlineButtonMod(loader.Module):
"""Create inline button""" """Create inline buttons with enhanced functionality"""
strings = { strings = {
"name": "InlineButton", "name": "InlineButton",
"titles": "Create a message with the Inline Button", "titles": "🔘 Create message with Inline Button",
"error_title": "Error", "error_title": "<emoji document_id=5854929766146118183>❌</emoji> Error",
"error_description": "Invalid input format. Please provide exactly three comma-separated values.", "error_description": "<emoji document_id=5854929766146118183>❌</emoji> 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.", "error_message": "<emoji document_id=5854929766146118183>❌</emoji> Make sure your input is formatted as: message, name, url.",
"button_created": "<emoji document_id=5854762571659218443>✅</emoji> Button created successfully!",
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Please provide arguments: message, name, url.",
} }
strings_ru = { strings_ru = {
"titles": "Создай сообщение с Inline Кнопкой", "titles": "🔘 Создать сообщение с Inline Кнопкой",
"error_title": "Ошибка", "error_title": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка",
"error_description": "Неверный формат ввода. Пожалуйста, укажите ровно три значения, разделенных запятыми.", "error_description": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат ввода. Пожалуйста, укажите ровно три значения, разделенных запятыми: сообщение, имя, url.",
"error_message": "Убедитесь, что ваш ввод имеет следующий формат: сообщение, имя, url.", "error_message": "<emoji document_id=5854929766146118183>❌</emoji> Убедитесь, что ваш ввод имеет следующий формат: сообщение, имя, url.",
"button_created": "<emoji document_id=5854762571659218443>✅</emoji> Кнопка успешно создана!",
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Укажите аргументы: сообщение, имя, url.",
} }
@loader.command( @loader.command(
@@ -57,21 +64,25 @@ class InlineButtonMod(loader.Module):
async def crinl_inline_handler(self, query: InlineQuery): async def crinl_inline_handler(self, query: InlineQuery):
args = utils.get_args_raw(query.query) args = utils.get_args_raw(query.query)
if args: if not args:
args_list = [arg.strip() for arg in args.split(",")] return {
"title": self.strings("error_title"),
"description": self.strings("error_description"),
"message": self.strings("no_args"),
}
if len(args_list) == 3: args_list = [arg.strip() for arg in args.split(",")]
message, name, url = args_list
return { if len(args_list) != 3:
"title": self.strings("titles"), return {
"description": f"{message}, {name}, {url}", "title": self.strings("error_title"),
"message": message, "description": self.strings("error_description"),
"reply_markup": [{"text": name, "url": url}], "message": self.strings("error_message"),
} }
return { message, name, url = args_list
"title": self.strings("error_title"), return True, {
"description": self.strings("error_description"), "message": message,
"message": self.strings("error_message"), "reply_markup": [{"text": name, "url": url}],
"description": self.strings("button_created"),
} }

View File

@@ -26,49 +26,67 @@
# scope: InlineCoin 0.0.1 # scope: InlineCoin 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import random import random
from typing import Dict
from ..inline.types import InlineQuery
from .. import loader from .. import loader
from ..inline.types import InlineQuery
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class CoinSexMod(loader.Module): class CoinFlipMod(loader.Module):
"""Mini game heads or tails""" """Mini coin flip game"""
strings = { strings = {
"name": "InlineCoin", "name": "InlineCoin",
"titles": "Heads or tails?", "titles": "🪙 Heads or Tails?",
"description": "Let's find out!", "description": "🎲 Let's find out!",
"heads": "🌚 An eagle fell out!", "heads": "🦅 An eagle fell out!",
"tails": "🌝 Tails fell out!", "tails": "🪙 Tails fell out!",
"edge": "🙀 Miraculously, the coin remained on the edge!", "edge": "🙀 Miraculously, the coin remained on its edge!",
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Please provide a command to flip.",
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> An error occurred: {error}",
} }
strings_ru = { strings_ru = {
"titles": "Орёл или решка?", "titles": "🪙 Орёл или решка?",
"description": "Давай узнаем!", "description": "🎲 Давай узнаем!",
"heads": "🌚 Выпал орёл!", "heads": "🦅 Выпал орёл!",
"tails": "🌝 Выпала решка!", "tails": "🪙 Выпала решка!",
"edge": "🙀 Чудо, монетка осталась на ребре!", "edge": "🙀 Чудо, монетка осталась на ребре!",
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Укажите команду для подбрасывания монетки.",
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка: {error}",
} }
def get_coin_flip_result(self) -> dict: def get_coin_flip_result(self) -> Dict[str, str]:
results = [self.strings("heads"), self.strings("tails")] """Get coin flip result with better formatting"""
if random.random() < 0.1: return {
return self.strings("edge") "title": self.strings["titles"],
else: "description": self.strings["description"],
return random.choice(results) "message": f"<b>{random.choice([self.strings['heads'], self.strings['tails']])}</b>",
"thumb": "https://github.com/Codwizer/ReModules/blob/main/assets/images.png",
}
@loader.command( @loader.command(
ru_doc="Подбросит монетку ", ru_doc="Подбросить монетку",
en_doc="Flip a coin", en_doc="Flip a coin",
) )
async def coin_inline_handler(self, query: InlineQuery): 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() result = self.get_coin_flip_result()
return { return {
"title": self.strings("titles"), "title": self.strings["titles"],
"description": self.strings("description"), "description": self.strings["description"],
"message": f"<b>{result}</b>", "message": result["message"],
"thumb": "https://github.com/Codwizer/ReModules/blob/main/assets/images.png", "thumb": result["thumb"],
} }

View File

@@ -26,14 +26,15 @@
# scope: InlineHelper 0.0.1 # scope: InlineHelper 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import sys
import os
import asyncio import asyncio
import logging import logging
import shlex
import sys
from .. import loader, main, utils
from ..inline.types import InlineQuery from ..inline.types import InlineQuery
from .. import loader, utils, main logger = logging.getLogger(__name__)
@loader.tds @loader.tds
@@ -42,77 +43,118 @@ class InlineHelperMod(loader.Module):
strings = { strings = {
"name": "InlineHelper", "name": "InlineHelper",
"call_restart": "Restarting...", "call_restart": "<emoji document_id=5188311512791393083>🔄</emoji> Restarting...",
"call_update": "Updating...", "call_update": "<emoji document_id=5188311512791393083>🔄</emoji> Updating...",
"res_prefix": "Successfully reset prefix to default", "res_prefix": "<emoji document_id=5854762571659218443>✅</emoji> Prefix successfully reset to default",
"restart_inline_handler_title": "Restart Userbot", "restart_inline_handler_title": "🔄 Restart Userbot",
"restart_inline_handler_description": "Restart your userbot via inline", "restart_inline_handler_description": "Restart your userbot via inline",
"restart_inline_handler_message": "Press the button below to restart your userbot", "restart_inline_handler_message": "🔄 Restart",
"restart_inline_handler_reply_text": "Restart", "update_inline_handler_title": "🔄 Update Userbot",
"update_inline_handler_title": "Update Userbot",
"update_inline_handler_description": "Update your userbot via inline", "update_inline_handler_description": "Update your userbot via inline",
"update_inline_handler_message": "Press the button below to update your userbot", "update_inline_handler_message": "🔄 Update",
"update_inline_handler_reply_text": "Update", "terminal_inline_handler_title": "💻 Command Executed",
"terminal_inline_handler_title": "Command Executed!",
"terminal_inline_handler_description": "Command executed successfully", "terminal_inline_handler_description": "Command executed successfully",
"terminal_inline_handler_message": "Command {text} executed successfully in terminal", "terminal_inline_handler_message": "Command <code>{text}</code> executed successfully in terminal",
"modules_inline_handler_title": "Modules", "modules_inline_handler_title": "📦 Modules",
"modules_inline_handler_description": "List all installed modules", "modules_inline_handler_description": "List all installed modules",
"modules_inline_handler_result": "☘️ Installed modules:\n", "modules_inline_handler_result": "📦 All installed modules:\n\n",
"resetprefix_inline_handler_title": "Reset Prefix", "resetprefix_inline_handler_title": "⚠️ Reset Prefix",
"resetprefix_inline_handler_description": "Reset your prefix back to default", "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_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_yes": "Yes, reset it",
"resetprefix_inline_handler_reply_text_no": "No", "resetprefix_inline_handler_reply_text_no": "No, cancel",
"error_no_module": "<emoji document_id=5854929766146118183>❌</emoji> Module not found: {module}",
"error_command_failed": "<emoji document_id=5854929766146118183>❌</emoji> Command execution failed: {error}",
"error_git_failed": "<emoji document_id=5854929766146118183>❌</emoji> Git operation failed: {error}",
} }
strings_ru = { strings_ru = {
"call_restart": "Перезагружаю...", "call_restart": "<emoji document_id=5188311512791393083>🔄</emoji> Перезагружаю...",
"call_update": "Обновляю...", "call_update": "<emoji document_id=5188311512791393083>🔄</emoji> Обновляю...",
"res_prefix": "Префикс успешно сброшен по умолчанию", "res_prefix": "<emoji document_id=5854762571659218443>✅</emoji> Префикс успешно сброшен по умолчанию",
"restart_inline_handler_title": "Перезагрузить юзербота", "restart_inline_handler_title": "<emoji document_id=5188311512791393083>🔄</emoji> Перезагрузить юзербота",
"restart_inline_handler_description": "Перезагрузить юзербота через инлайн", "restart_inline_handler_description": "Перезагрузить юзербота через инлайн",
"restart_inline_handler_message": "<b>Нажмите на кнопку ниже для рестарта юзербота</b>", "restart_inline_handler_message": "<emoji document_id=5188311512791393083>🔄</emoji> Перезагрузка",
"restart_inline_handler_reply_text": "Перезапуск", "update_inline_handler_title": "<emoji document_id=5188311512791393083>🔄</emoji> Обновить юзербота",
"update_inline_handler_title": "Обновить юзербота",
"update_inline_handler_description": "Обновить юзербота через инлайн", "update_inline_handler_description": "Обновить юзербота через инлайн",
"update_inline_handler_message": "<b>Нажмите на кнопку ниже для обновления юзербота</b>", "update_inline_handler_message": "<emoji document_id=5188311512791393083>🔄</emoji> Обновить",
"update_inline_handler_reply_text": "Обновить", "terminal_inline_handler_title": "<emoji document_id=5854762571659218443>💻</emoji> Команда выполнена!",
"terminal_inline_handler_title": "Команда выполнена!", "terminal_inline_handler_description": "Команда успешно выполнена.",
"terminal_inline_handler_description": "Команда завершена.",
"terminal_inline_handler_message": "Команда <code>{text}</code> была успешно выполнена в терминале", "terminal_inline_handler_message": "Команда <code>{text}</code> была успешно выполнена в терминале",
"modules_inline_handler_title": "Модули", "modules_inline_handler_title": "<emoji document_id=5854762571659218443>📦</emoji> Модули",
"modules_inline_handler_description": "Вывести список установленных моудей", "modules_inline_handler_description": "Вывести список установленных модулей",
"modules_inline_handler_result": "☘️ Все установленные модули:\n", "modules_inline_handler_result": "<emoji document_id=5854762571659218443>📦</emoji> Все установленные модули:\n\n",
"resetprefix_inline_handler_title": "Сбросить префикс", "resetprefix_inline_handler_title": "<emoji document_id=5854929766146118183>⚠️</emoji> Сбросить префикс",
"resetprefix_inline_handler_description": "Сбросить префикс по умолчанию", "resetprefix_inline_handler_description": "Сбросить префикс по умолчанию (осторожно!)",
"resetprefix_inline_handler_message": "Вы действительно хотите сбросить ваш префикс и установить стандартную точку?", "resetprefix_inline_handler_message": "Вы действительно хотите сбросить ваш префикс и установить стандартную точку?",
"resetprefix_inline_handler_reply_text_yes": "Да", "resetprefix_inline_handler_reply_text_yes": "Да, сбросить",
"resetprefix_inline_handler_reply_text_no": "Нет", "resetprefix_inline_handler_reply_text_no": "Нет, отменить",
"error_no_module": "<emoji document_id=5854929766146118183>❌</emoji> Модуль не найден: {module}",
"error_command_failed": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка выполнения команды: {error}",
"error_git_failed": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка git операции: {error}",
} }
def __init__(self):
self.client = None
self.db = None
self._base_dir = utils.get_base_dir()
async def client_ready(self, client, db): async def client_ready(self, client, db):
self.client = client self.client = client
self.db = db self.db = db
async def restart(self, call): async def restart(self, call):
"""Restart callback""" """Restart callback"""
logging.error("InlineHelper: restarting userbot...") logger.info("InlineHelper: Restarting userbot...")
await call.edit(self.strings("call_restart")) try:
await sys.exit(0) 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): async def update(self, call):
"""Update callback""" """Update callback"""
logging.error("InlineHelper: updating userbot...") logger.info("InlineHelper: Updating userbot...")
os.system(f"cd {utils.get_base_dir()} && cd .. && git reset --hard HEAD") try:
os.system("git pull") await call.edit(self.strings["call_update"])
await call.edit(self.strings("call_update"))
await sys.exit(0) 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): async def reset_prefix(self, call):
"""Reset prefix""" """Reset prefix callback"""
self.db.set(main.__name__, "command_prefix", ".") try:
await call.edit(self.strings("res_prefix")) 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( @loader.inline_handler(
ru_doc="Перезагрузить юзербота", ru_doc="Перезагрузить юзербота",
@@ -152,42 +194,95 @@ class InlineHelperMod(loader.Module):
ru_doc="Выполнить команду в терминале (лучше сразу подготовить команду и просто вставить)", ru_doc="Выполнить команду в терминале (лучше сразу подготовить команду и просто вставить)",
en_doc="Execute the command in the terminal (it is better to prepare the command immediately and just paste it)", 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): async def terminal_inline_handler(self, query: InlineQuery):
text = _.args """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( command_text = query.args.strip()
f"{text}", if not command_text:
stdin=asyncio.subprocess.PIPE, return {
stdout=asyncio.subprocess.PIPE, "title": self.strings["terminal_inline_handler_title"],
stderr=asyncio.subprocess.PIPE, "description": self.strings["terminal_inline_handler_description"],
cwd=utils.get_base_dir(), "message": self.strings["terminal_inline_handler_message"].format(
) text="No command provided"
),
}
return { if any(char in command_text for char in ["&", "|", ";", "`", "$"]):
"title": self.strings("terminal_inline_handler_title"), return {
"description": self.strings("terminal_inline_handler_description"), "title": self.strings["terminal_inline_handler_title"],
"message": self.strings("terminal_inline_handler_message").format( "description": self.strings["terminal_inline_handler_description"],
text=text "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( @loader.inline_handler(
ru_doc="Вывести список установленных модулей через инлайн", ru_doc="Вывести список установленных модулей через инлайн",
en_doc="Display a list of installed modules via the inline", en_doc="Display a list of installed modules via the inline",
) )
async def modules_inline_handler(self, _: InlineQuery): async def modules_inline_handler(self, query: InlineQuery):
result = self.strings("modules_inline_handler_result") """List all installed modules"""
try:
result = self.strings["modules_inline_handler_result"]
for mod in self.allmodules.modules: for mod in self.allmodules.modules:
try: try:
name = mod.strings["name"] name = mod.strings["name"]
except KeyError: except KeyError:
name = mod.__clas__.__name__ name = mod.__class__.__name__
result += f"{name}\n" result += f"{name}\n"
except Exception as e:
logger.error(f"Error listing modules: {e}")
result = f"Error listing modules: {str(e)}"
return { return {
"title": self.strings("modules_inline_handler_title"), "title": self.strings["modules_inline_handler_title"],
"description": self.strings("modules_inline_handler_description"), "description": self.strings["modules_inline_handler_description"],
"message": result, "message": result,
} }

View File

@@ -26,53 +26,108 @@
# scope: IrisSimpleMod 1.0.1 # scope: IrisSimpleMod 1.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
from typing import Optional
from .. import loader, utils from .. import loader, utils
__version__ = (1, 0, 1) __version__ = (1, 0, 1)
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class IrisSimpleMod(loader.Module): class IrisSimpleMod(loader.Module):
"""Модуль для базового взаимодействия с Ирисом""" """Module for basic interaction with Iris bot"""
strings = {"name": "IrisSimpleMod"} strings = {
"name": "IrisSimpleMod",
"checking_bag": "<emoji document_id=5188311512791393083>🌎</emoji> Checking bag...",
"bag_result": "<emoji document_id=5854762571659218443>✅</emoji> Your bag: <code>{}</code>",
"farming": "<emoji document_id=5188311512791393083>🌎</emoji> Farming iris-coins...",
"farm_result": "<emoji document_id=5854762571659218443>✅</emoji> Farm result: <code>{}</code>",
"getting_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Getting user stats...",
"stats_result": "<emoji document_id=5854762571659218443>✅</emoji> User stats: <code>{}</code>",
"bot_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Getting bot stats...",
"bot_stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Bot stats: <code>{}</code>",
"error_no_response": "<emoji document_id=5854929766146118183>❌</emoji> No response from bot. Please try again.",
"error_timeout": "<emoji document_id=5854929766146118183>❌</emoji> Request timeout. Please try again.",
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> An error occurred: {error}",
}
@loader.command(ru_doc="Проверить мешок") strings_ru = {
"checking_bag": "<emoji document_id=5188311512791393083>🌎</emoji> Проверка мешка...",
"bag_result": "<emoji document_id=5854762571659218443>✅</emoji> Ваш мешок: <code>{}</code>",
"farming": "<emoji document_id=5188311512791393083>🌎</emoji> Фарм ирис-коинов...",
"farm_result": "<emoji document_id=5854762571659218443>✅</emoji> Результат фарма: <code>{}</code>",
"getting_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Получение статистики пользователя...",
"stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Статистика пользователя: <code>{}</code>",
"bot_stats": "<emoji document_id=5188311512791393083>🌎</emoji> Получение статистики ботов...",
"bot_stats_result": "<emoji document_id=5854762571659218443>✅</emoji> Статистика ботов: <code>{}</code>",
"error_no_response": "<emoji document_id=5854929766146118183>❌</emoji> Нет ответа от бота. Попробуйте еще раз.",
"error_timeout": "<emoji document_id=5854929766146118183>❌</emoji> Таймаут запроса. Попробуйте еще раз.",
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка: {error}",
}
async def _send_and_delete(
self, message, command_message: str, response_timeout: int = 15
) -> Optional[str]:
"""Send command to Iris and get response with timeout"""
try:
async with self.client.conversation(
self._iris_user_id, timeout=self._timeout
) as conv:
await conv.send_message(command_message)
await message.delete()
response_msg = await conv.get_response()
if response_msg:
await utils.answer(message, response_msg.text)
return response_msg.text
else:
return None
except Exception as e:
logger.error(f"Error in conversation: {e}")
await utils.answer(
message, self.strings["error_general"].format(error=str(e))
)
return None
@loader.command(
ru_doc="Проверить мешок",
en_doc="Check bag",
)
async def bag(self, message): async def bag(self, message):
"""Check bag""" """Check bag"""
async with self.client.conversation(5443619563) as conv: await utils.answer(message, self.strings["checking_bag"])
usermessage = await conv.send_message("мешок")
await usermessage.delete()
bagmessage = await conv.get_response()
await utils.answer(message, "Ваш мешок:\n" + bagmessage.text)
await bagmessage.delete()
@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): async def farm(self, message):
"""Farm iris-coins""" """Farm iris-coins"""
async with self.client.conversation(5443619563) as conv: await utils.answer(message, self.strings["farming"])
usermessage = await conv.send_message("ферма")
await usermessage.delete()
farmmessage = await conv.get_response()
await utils.answer(message, farmmessage.text)
await farmmessage.delete()
@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): async def irisstats(self, message):
"""Display user stats""" """Display user stats"""
async with self.client.conversation(5443619563) as conv: await utils.answer(message, self.strings["getting_stats"])
usermessage = await conv.send_message("анкета")
await usermessage.delete()
statsmessage = await conv.get_response()
await utils.answer(message, statsmessage.text)
await statsmessage.delete()
@loader.command(ru_doc="Вывести статистику ботов") result = await self._send_and_delete(message, "анкета", response_timeout=20)
async def irisping(self, message):
"""Display bot stats""" if result:
async with self.client.conversation(5443619563) as conv: await utils.answer(message, self.strings["stats_result"].format(result))
usermessage = await conv.send_message("🌺 Семейство ирисовых")
await usermessage.delete()
pingmessage = await conv.get_response()
await utils.answer(message, pingmessage.text)
await pingmessage.delete()

View File

@@ -26,11 +26,12 @@
# scope: KBSwapper 0.0.1 # scope: KBSwapper 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import string import string
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
EN_TO_RU = str.maketrans( EN_TO_RU = str.maketrans(
"qwertyuiop[]asdfghjkl;'zxcvbnm,./`" + 'QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?~', "qwertyuiop[]asdfghjkl;'zxcvbnm,./`" + 'QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>?~',
@@ -74,18 +75,55 @@ class KBSwapperMod(loader.Module):
return return
original_text = reply.text original_text = reply.text
if not original_text: if not original_text or original_text.isspace():
await utils.answer(message, self.strings("no_text")) await utils.answer(message, self.strings("no_text"))
return return
try: try:
first_char = original_text[0].lower() trimmed_text = original_text.strip()
if first_char in string.ascii_lowercase:
fixed_text = original_text.translate(EN_TO_RU) has_russian = any(
elif first_char in "йцукенгшщзхъфывапролджэячсмитьбю.ё": 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) 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: 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: if message.sender_id == reply.sender_id:
await reply.edit(fixed_text) await reply.edit(fixed_text)
@@ -96,5 +134,5 @@ class KBSwapperMod(loader.Module):
f"{self.strings('fixed_message').format(fixed=fixed_text)}", f"{self.strings('fixed_message').format(fixed=fixed_text)}",
) )
except Exception as e: except Exception as e:
print(f"Error during swap: {e}") logger.error(f"Error during swap: {e}")
await utils.answer(message, self.strings("error")) await utils.answer(message, self.strings("error"))

View File

@@ -27,12 +27,15 @@
# scope: Meme 0.0.1 # scope: Meme 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
from bs4 import BeautifulSoup import logging
import aiohttp import random # noqa: F401
import random
import aiohttp # noqa: F401
from bs4 import BeautifulSoup # noqa: F401
from .. import loader from .. import loader
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class MemesMod(loader.Module): class MemesMod(loader.Module):

View File

@@ -28,8 +28,10 @@
import logging import logging
import re import re
from typing import List, Optional
from telethon.types import Message
from herokutl.types import Message
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -43,50 +45,50 @@ class MessageMonitor(loader.Module):
strings = { strings = {
"name": "MessageMonitor", "name": "MessageMonitor",
"triggers_set": "Trigger words have been set: {}", "triggers_set": "<emoji document_id=5854762571659218443>✅</emoji> Trigger words have been set: <code>{}</code>",
"triggers_not_set": "Trigger words have not been set", "triggers_not_set": "<emoji document_id=5854929766146118183>❌</emoji> Trigger words have not been set",
"target_set": "Target chat for notifications has been set", "target_set": "<emoji document_id=5854762571659218443>✅</emoji> Target chat for notifications has been set",
"target_not_set": "Target chat for notifications has not been set", "target_not_set": "<emoji document_id=5854929766146118183>❌</emoji> Target chat for notifications has not been set",
"monitoring_started": "Monitoring has started", "monitoring_started": "<emoji document_id=5188311512791393083>🌎</emoji> Monitoring has started",
"monitoring_stopped": "Monitoring has stopped", "monitoring_stopped": "<emoji document_id=5854929766146118183>❌</emoji> Monitoring has stopped",
"monitoring_status": "Monitoring {}", "monitoring_status": "<emoji document_id=5188311512791393083>🌎</emoji> Monitoring <b>{}</b>",
"triggers_example": "Example: <code>.triggers word1 word2</code>", "triggers_example": "<emoji document_id=5854929766146118183>❌</emoji> Example: <code>.triggers word1 word2</code>",
"monitoring_status_on": "enabled", "monitoring_status_on": "<emoji document_id=5854762571659218443>✅</emoji> enabled",
"monitoring_status_off": "disabled", "monitoring_status_off": "<emoji document_id=5854929766146118183>❌</emoji> disabled",
"ignore_set": "Ignored chats have been set: {}", "ignore_set": "<emoji document_id=5854762571659218443>✅</emoji> Ignored chats have been set: <code>{}</code>",
"ignore_none": "Ignored chats have not been set", "ignore_none": "<emoji document_id=5854929766146118183>❌</emoji> Ignored chats have not been set",
"ignore_example": "Example: <code>.ignore 123456789 -987654321</code> (chat IDs)", "ignore_example": "<emoji document_id=5854929766146118183>❌</emoji> Example: <code>.ignore 123456789 -987654321</code> (chat IDs)",
"no_reply": "Reply to a message in the desired chat or specify its ID", "no_reply": "<emoji document_id=5854929766146118183>❌</emoji> Reply to a message in the desired chat or specify its ID",
"monitoring_msg": ( "monitoring_msg": (
"🚨 **Trigger word detected!** 🚨\n\n" "<emoji document_id=5854929766146118183>🚨</emoji> <b>Trigger word detected!</b> <emoji document_id=5854929766146118183>🚨</emoji>\n\n"
"**Chat:** {} (`{}`)\n" "<b>Chat:</b> <code>{}</code>\n"
"**User:** {}\n" "<b>User:</b> {}\n"
"**Link:** {}\n\n" "<b>Link:</b> <a href='{}'>{}</a>\n\n"
"**Messages:**\n{}" "<b>Message:</b>\n{}"
), ),
} }
strings_ru = { strings_ru = {
"triggers_set": "Триггерные слова установлены: {}", "triggers_set": "<emoji document_id=5854762571659218443>✅</emoji> Триггерные слова установлены: <code>{}</code>",
"triggers_not_set": "Триггерные слова не установлены", "triggers_not_set": "<emoji document_id=5854929766146118183>❌</emoji> Триггерные слова не установлены",
"target_set": "Целевой чат для уведомлений установлен", "target_set": "<emoji document_id=5854762571659218443>✅</emoji> Целевой чат для уведомлений установлен",
"target_not_set": "Целевой чат для уведомлений не установлен", "target_not_set": "<emoji document_id=5854929766146118183>❌</emoji> Целевой чат для уведомлений не установлен",
"monitoring_started": "Мониторинг запущен", "monitoring_started": "<emoji document_id=5188311512791393083>🌎</emoji> Мониторинг запущен",
"monitoring_stopped": "Мониторинг остановлен", "monitoring_stopped": "<emoji document_id=5854929766146118183>❌</emoji> Мониторинг остановлен",
"monitoring_status": "Мониторинг {}", "monitoring_status": "<emoji document_id=5188311512791393083>🌎</emoji> Мониторинг <b>{}</b>",
"triggers_example": "Пример: <code>.triggers слово1 слово2</code>", "triggers_example": "<emoji document_id=5854929766146118183>❌</emoji> Пример: <code>.triggers слово1 слово2</code>",
"monitoring_status_on": "включен", "monitoring_status_on": "<emoji document_id=5854762571659218443>✅</emoji> включен",
"monitoring_status_off": "выключен", "monitoring_status_off": "<emoji document_id=5854929766146118183>❌</emoji> выключен",
"ignore_set": "Игнорируемые чаты установлены: {}", "ignore_set": "<emoji document_id=5854762571659218443>✅</emoji> Игнорируемые чаты установлены: <code>{}</code>",
"ignore_none": "Игнорируемые чаты не установлены", "ignore_none": "<emoji document_id=5854929766146118183>❌</emoji> Игнорируемые чаты не установлены",
"ignore_example": "Пример: <code>.ignore 123456789 -987654321</code> (ID чатов)", "ignore_example": "<emoji document_id=5854929766146118183>❌</emoji> Пример: <code>.ignore 123456789 -987654321</code> (ID чатов)",
"no_reply": "Ответьте на сообщение в нужном чате или укажите его ID", "no_reply": "<emoji document_id=5854929766146118183>❌</emoji> Ответьте на сообщение в нужном чате или укажите его ID",
"monitoring_msg": ( "monitoring_msg": (
"🚨 **Обнаружено триггерное слово!** 🚨\n\n" "<emoji document_id=5854929766146118183>🚨</emoji> <b>Обнаружено триггерное слово!</b> <emoji document_id=5854929766146118183>🚨</emoji>\n\n"
"**Чат:** {} (`{}`)\n" "<b>Чат:</b> <code>{}</code>\n"
"**Пользователь:** {}\n" "<b>Пользователь:</b> {}\n"
"**Ссылка:** {}\n\n" "<b>Ссылка:</b> <a href='{}'>{}</a>\n\n"
"**Сообщение:**\n{}" "<b>Сообщение:</b>\n{}"
), ),
} }
@@ -95,35 +97,61 @@ class MessageMonitor(loader.Module):
loader.ConfigValue( loader.ConfigValue(
"triggers", "triggers",
[], [],
"Список триггерных слов", "List of trigger words to monitor",
validator=loader.validators.Series(), validator=loader.validators.Series(),
), ),
loader.ConfigValue( loader.ConfigValue(
"target_chat", "target_chat",
None, None,
"ID целевого чата для отправки уведомлений", "Target chat ID for notifications",
validator=loader.validators.Integer(), validator=loader.validators.Integer(),
), ),
loader.ConfigValue( loader.ConfigValue(
"ignore_chats", "ignore_chats",
[], [],
"Список ID чатов, которые будут игнорироваться", "List of chat IDs to ignore",
validator=loader.validators.Series(), validator=loader.validators.Series(),
), ),
) )
self._triggers = [] self._triggers: List[str] = []
self._target_chat = None self._target_chat: Optional[int] = None
self._ignore_chats = [] self._ignore_chats: List[int] = []
self._compiled_patterns: List[re.Pattern] = []
async def client_ready(self, client, db): 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._target_chat = self.config["target_chat"]
self._ignore_chats = [ self._ignore_chats = [
int(i) int(chat_id)
for i in self.config["ignore_chats"] for chat_id in self.config["ignore_chats"]
if str(i).isdigit() or str(i).startswith("-") 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( @loader.command(
ru_doc="Установить триггерные слова. Пример: .triggers слово1 слово2", ru_doc="Установить триггерные слова. Пример: .triggers слово1 слово2",
@@ -138,6 +166,7 @@ class MessageMonitor(loader.Module):
self._triggers = [arg.lower() for arg in args] self._triggers = [arg.lower() for arg in args]
self.config["triggers"] = self._triggers self.config["triggers"] = self._triggers
await self._update_config()
await utils.answer( await utils.answer(
message, self.strings["triggers_set"].format(", ".join(self._triggers)) message, self.strings["triggers_set"].format(", ".join(self._triggers))
) )
@@ -152,12 +181,10 @@ class MessageMonitor(loader.Module):
chat_id = None chat_id = None
if getattr(message, "is_reply", False): if getattr(message, "is_reply", False):
get_reply_method = getattr(message, "get_reply_message", None) reply_message = await message.get_reply_message()
if get_reply_method: if reply_message and hasattr(reply_message, "chat_id"):
reply_message = await get_reply_method() chat_id = reply_message.chat_id
if reply_message and getattr(reply_message, "chat_id", None): elif args and (args.isdigit() or (args.startswith("-") and args[1:].isdigit())):
chat_id = reply_message.chat_id
elif args and (args.isdigit() or args.startswith("-") and args[1:].isdigit()):
chat_id = int(args) chat_id = int(args)
if chat_id: if chat_id:
@@ -184,7 +211,7 @@ class MessageMonitor(loader.Module):
valid_ids.append(int(arg)) valid_ids.append(int(arg))
self.config["ignore_chats"] = valid_ids self.config["ignore_chats"] = valid_ids
self._ignore_chats = valid_ids await self._update_config()
if valid_ids: if valid_ids:
await utils.answer( await utils.answer(
@@ -192,19 +219,15 @@ class MessageMonitor(loader.Module):
self.strings["ignore_set"].format(", ".join(map(str, valid_ids))), self.strings["ignore_set"].format(", ".join(map(str, valid_ids))),
) )
else: else:
await utils.answer( await utils.answer(message, self.strings["ignore_none"])
message, "Не удалось распознать ID чатов. Используйте только числа."
)
@loader.watcher(out=False, only_messages=True) @loader.watcher(out=False, only_messages=True)
async def message_watcher(self, message: Message): async def message_watcher(self, message: Message):
"""Watch for messages containing trigger words""" """Watch for messages containing trigger words"""
if not self._target_chat or not self._triggers: if not self._target_chat or not self._triggers:
return return
chat_id = getattr(message, "chat_id", None) chat_id = getattr(message, "chat_id", None)
if chat_id and chat_id in self._ignore_chats: if chat_id and chat_id in self._ignore_chats:
logger.debug(f"Message in ignored chat: {chat_id}. Skipping monitoring.") logger.debug(f"Message in ignored chat: {chat_id}. Skipping monitoring.")
return return
@@ -213,34 +236,27 @@ class MessageMonitor(loader.Module):
if not text: if not text:
return return
text_lower = text.lower()
found_triggers = [ found_triggers = [
trigger trigger
for trigger in self._triggers for pattern, trigger in zip(self._compiled_patterns, self._triggers)
if re.search(r"\b" + re.escape(trigger) + r"\b", text_lower) if pattern.search(text)
] ]
if not found_triggers: if not found_triggers:
return return
try: try:
get_chat_method = getattr(message, "get_chat", None) chat = await message.get_chat()
if get_chat_method: chat_title = getattr(
chat = await get_chat_method() chat,
chat_title = getattr( "title",
chat, "Личные сообщения"
"title", if getattr(message, "is_private", False)
"Личные сообщения" else f"Чат с ID {chat_id}",
if getattr(message, "is_private", False) )
else "Чат с ID " + str(chat_id),
)
else:
chat_title = "Неизвестный чат"
get_sender_method = getattr(message, "get_sender", None) sender = await message.get_sender()
if get_sender_method: if sender:
sender = await get_sender_method()
sender_name = sender.first_name sender_name = sender.first_name
if getattr(sender, "last_name", None): if getattr(sender, "last_name", None):
sender_name += f" {sender.last_name}" sender_name += f" {sender.last_name}"
@@ -251,13 +267,7 @@ class MessageMonitor(loader.Module):
else: else:
sender_name = "Неизвестный пользователь" sender_name = "Неизвестный пользователь"
to_id_obj = getattr(message, "to_id", None) link = await self._get_message_link(message, sender)
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}"
await self.client.send_message( await self.client.send_message(
self._target_chat, self._target_chat,
@@ -268,10 +278,28 @@ class MessageMonitor(loader.Module):
link, link,
text, text,
), ),
parse_mode="Markdown", parse_mode="HTML",
) )
logger.debug( logger.debug(
f"Sent notification about trigger word(s) {found_triggers} to chat {self._target_chat}" f"Sent notification about trigger word(s) {found_triggers} to chat {self._target_chat}"
) )
except Exception as e: except Exception as e:
logger.error(f"Error processing message: {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}"

View File

@@ -27,22 +27,19 @@
# requires: aioredis # requires: aioredis
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
__version__ = (0, 1, 4, 10)
import os
import re
import typing
import asyncio import asyncio
import base64 import base64
import json
import re
import aioredis 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 import events
from telethon.tl.types import InputDocument, Message
from .. import loader, utils from .. import loader, utils
from ..inline.types import InlineCall from ..inline.types import InlineCall
import json
__version__ = (0, 1, 4, 10)
class DebugLogger: class DebugLogger:
@@ -127,7 +124,7 @@ class AutoFarmbotMod(loader.Module):
""" """
# Todo: Автокрафт и Автолес готовы на 95%, автохавка на 45% # NOTE: Автокрафт и Автолес готовы на 95%, автохавка на 45%
strings = { strings = {
"name": "AutoFarmbot", "name": "AutoFarmbot",
# Inline keys # Inline keys
@@ -1112,7 +1109,7 @@ class AutoFarmbotMod(loader.Module):
try: try:
self.config[config_key] = current self.config[config_key] = current
except Exception as e: except Exception:
await call.answer("❌ Ошибка валидации") await call.answer("❌ Ошибка валидации")
return return
@@ -1122,7 +1119,7 @@ class AutoFarmbotMod(loader.Module):
await call.answer("🔄 Синхронизация началась...") await call.answer("🔄 Синхронизация началась...")
chat_id = self.get_chat_id 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") msg = await self.client.send_message(chat_id, "/cow")
start_id = msg.id start_id = msg.id
@@ -1198,7 +1195,6 @@ class AutoFarmbotMod(loader.Module):
async def eating_handler(self, event): async def eating_handler(self, event):
chat_id = self.get_chat_id chat_id = self.get_chat_id
user_id = self.tg_id
food = self.config["config_bot_eat_lvl"] food = self.config["config_bot_eat_lvl"]
if event.chat_id != chat_id: if event.chat_id != chat_id:
return return
@@ -1662,7 +1658,7 @@ class AutoFarmbotMod(loader.Module):
:param action: :param action:
:return: :return:
""" """
chat_id = self.config["config_bot_used_chat_id"] self.config["config_bot_used_chat_id"]
user_id = self.tg_id user_id = self.tg_id
key = f"forest_task:{user_id}:{action}" key = f"forest_task:{user_id}:{action}"
await self.redis.set(key, "pending", ex=wait_time) await self.redis.set(key, "pending", ex=wait_time)

View File

@@ -18,7 +18,7 @@
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
# Name: Music # Name: Music
# Description: Searches for music using Telegram music bots. # Description: Searches for music using Telegram music bots
# Author: @hikka_mods # Author: @hikka_mods
# meta developer: @hikka_mods # meta developer: @hikka_mods
# scope: Music # scope: Music
@@ -45,49 +45,46 @@ logger = logging.getLogger(__name__)
class MusicMod(loader.Module): class MusicMod(loader.Module):
strings = { strings = {
"name": "Music", "name": "Music",
"no_query": "<emoji document_id=5337117114392127164>🤷‍♂</emoji> <b>Provide a search query.</b>", "no_query": "<emoji document_id=5337117114392127164>🤷‍♂</emoji> <b>Provide a search query!</b>",
"searching": "<emoji document_id=4918235297679934237>⌨️</emoji> <b>Searching...</b>", "searching": "<emoji document_id=4918235297679934237>⌨️</emoji> <b>Searching...</b>",
"found": "<emoji document_id=5336965905773504919>🗣</emoji> <b>Possible match:</b>", "found": "<emoji document_id=5336965905773504919>🗣</emoji> <b>Possible match:</b>",
"not_found": "<emoji document_id=5228947933545635555>😫</emoji> <b>Track not found: <code>{}</code>.</b>", "not_found": "<emoji document_id=5228947933545635555>😫</emoji> <b>Track not found: <code>{}</code></b>",
"invalid_service": "<emoji document_id=5462295343642956603>🚫</emoji> <b>Invalid service. (yandex, vk)</b>", "usage": "<b>Usage:</b> <code>.music [track name]</code>",
"usage": "<b>Usage:</b> <code>.music [yandex|vk] [track name]</code>",
"error": "<emoji document_id=5228947933545635555>⚠️</emoji> <b>Error:</b> <code>{}</code>", "error": "<emoji document_id=5228947933545635555>⚠️</emoji> <b>Error:</b> <code>{}</code>",
"no_results": "<emoji document_id=5228947933545635555>😫</emoji> <b>No results: <code>{}</code>.</b>", "no_results": "<emoji document_id=5228947933545635555>😫</emoji> <b>No results: <code>{}</code></b>",
"flood_wait": "<emoji document_id=5462295343642956603>⏳</emoji> <b>Wait {}s (Telegram limits).</b>", "flood_wait": "<emoji document_id=5462295343642956603>⏳</emoji> <b>Wait {}s (Telegram limits)</b>",
"bot_error": "<emoji document_id=5228947933545635555>🤖</emoji> <b>Bot error: <code>{}</code></b>", "bot_error": "<emoji document_id=5228947933545635555>🤖</emoji> <b>Bot error: <code>{}</code></b>",
"no_audio": "<emoji document_id=5228947933545635555>🎵</emoji> <b>No audio.</b>", "no_audio": "<emoji document_id=5228947933545635555>🎵</emoji> <b>No audio</b>",
"generic_result": "<emoji document_id=5336965905773504919></emoji> <b>Non-media result. Check the bot's chat.</b>", "generic_result": "<emoji document_id=5336965905773504919></emoji> <b>Non-media result. Check the bot's chat</b>",
"yafind_searching": "<emoji document_id=5258396243666681152>🔎</emoji> <b>Searching Yandex.Music...</b>", "yafind_searching": "<emoji document_id=5258396243666681152>🔎</emoji> <b>Searching Yandex.Music...</b>",
"yafind_not_found": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Track not found on Yandex.Music.</b>", "yafind_not_found": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Track not found on Yandex.Music</b>",
"yafind_error": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Error (Yandex): {}</b>", "yafind_error": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Error (Yandex): {}</b>",
} }
strings_ru = { strings_ru = {
"name": "Music", "name": "Music",
"no_query": "<emoji document_id=5337117114392127164>🤷‍♂</emoji> <b>Укажите запрос.</b>", "no_query": "<emoji document_id=5337117114392127164>🤷‍♂</emoji> <b>Укажите запрос!</b>",
"searching": "<emoji document_id=4918235297679934237>⌨️</emoji> <b>Поиск...</b>", "searching": "<emoji document_id=4918235297679934237>⌨️</emoji> <b>Поиск...</b>",
"found": "<emoji document_id=5336965905773504919>🗣</emoji> <b>Возможно, это оно:</b>", "found": "<emoji document_id=5336965905773504919>🗣</emoji> <b>Возможно, это оно:</b>",
"not_found": "<emoji document_id=5228947933545635555>😫</emoji> <b>Трек не найден: <code>{}</code>.</b>", "not_found": "<emoji document_id=5228947933545635555>😫</emoji> <b>Трек не найден: <code>{}</code></b>",
"invalid_service": "<emoji document_id=5462295343642956603>🚫</emoji> <b>Неверный сервис. (yandex, vk)</b>", "usage": "<b>Использование:</b> <code>.music [название трека]</code>",
"usage": "<b>Использование:</b> <code>.music [yandex|vk] [название трека]</code>",
"error": "<emoji document_id=5228947933545635555>⚠️</emoji> <b>Ошибка:</b> <code>{}</code>", "error": "<emoji document_id=5228947933545635555>⚠️</emoji> <b>Ошибка:</b> <code>{}</code>",
"no_results": "<emoji document_id=5228947933545635555>😫</emoji> <b>Нет результатов: <code>{}</code>.</b>", "no_results": "<emoji document_id=5228947933545635555>😫</emoji> <b>Нет результатов: <code>{}</code></b>",
"flood_wait": "<emoji document_id=5462295343642956603>⏳</emoji> <b>Подождите {}с (лимиты Telegram).</b>", "flood_wait": "<emoji document_id=5462295343642956603>⏳</emoji> <b>Подождите {}с (лимиты Telegram)</b>",
"bot_error": "<emoji document_id=5228947933545635555>🤖</emoji> <b>Ошибка бота: <code>{}</code></b>", "bot_error": "<emoji document_id=5228947933545635555>🤖</emoji> <b>Ошибка бота: <code>{}</code></b>",
"no_audio": "<emoji document_id=5228947933545635555>🎵</emoji> <b>Нет аудио.</b>", "no_audio": "<emoji document_id=5228947933545635555>🎵</emoji> <b>Нет аудио</b>",
"generic_result": "<emoji document_id=5336965905773504919></emoji> <b>Немедийный результат. Проверьте чат с ботом.</b>", "generic_result": "<emoji document_id=5336965905773504919></emoji> <b>Немедийный результат. Проверьте чат с ботом</b>",
"yafind_searching": "<emoji document_id=5258396243666681152>🔎</emoji> <b>Поиск в Яндекс.Музыке...</b>", "yafind_searching": "<emoji document_id=5258396243666681152>🔎</emoji> <b>Поиск в Яндекс.Музыке...</b>",
"yafind_not_found": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Трек не найден в Яндекс.Музыке.</b>", "yafind_not_found": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Трек не найден в Яндекс.Музыке</b>",
"yafind_error": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Ошибка (Яндекс): {}</b>", "yafind_error": "<emoji document_id=5843952899184398024>🚫</emoji> <b>Ошибка (Яндекс): {}</b>",
} }
def __init__(self): def __init__(self):
self.murglar_bot = "@murglar_bot" self.murglar_bot = "@murglar_bot"
self.vk_bot = "@vkmusic_bot"
@loader.command( @loader.command(
ru_doc="Найти трек в Yandex Music или VK: `.music yandex {название}` или `.music vk {название}`", ru_doc="Найти трек в Yandex.Music: `.music {название}`",
en_doc="Find a track in Yandex Music or VK: `.music yandex {name}` or `.music vk {name}`", en_doc="Find a track in Yandex.Music: `.music yandex {name}`",
) )
async def music(self, message): async def music(self, message):
args = utils.get_args(message) args = utils.get_args(message)
@@ -98,15 +95,8 @@ class MusicMod(loader.Module):
else: else:
await utils.answer(message, self.strings("usage", message)) await utils.answer(message, self.strings("usage", message))
return return
service, query = args[0].lower(), " ".join(args[1:]) await self._yafind(message, query=args)
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))
async def _yafind(self, message: Message, query: str): async def _yafind(self, message: Message, query: str):
if not query: if not query:
@@ -134,69 +124,3 @@ class MusicMod(loader.Module):
except Exception as e: except Exception as e:
logger.exception("Yandex search error:") logger.exception("Yandex search error:")
await utils.answer(message, self.strings("yafind_error", message).format(e)) 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))

View File

@@ -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": (
"<emoji document_id=5462882007451185227>🚫</emoji> You didn't specify the text"
),
"no_key": "<emoji document_id=5843952899184398024>🚫</emoji> The key was not found",
"done": "Your link with the code\n<emoji document_id=5985571061993837069>➡️</emoji> <code>{response_text}</code>",
}
strings_ru = {
"no_reply": (
"<emoji document_id=5462882007451185227>🚫</emoji> Вы не указали текст"
),
"no_key": "<emoji document_id=5843952899184398024>🚫</emoji> Ключ не найден",
"done": "Ваша ссылка с кодом\n<emoji document_id=5985571061993837069>➡️</emoji> <code>{response_text}</code>",
}
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)
)

View File

@@ -26,10 +26,13 @@
# scope: VowelReplacer 0.0.1 # scope: VowelReplacer 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
from telethon.tl.types import Message from telethon.tl.types import Message
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class VowelReplacer(loader.Module): class VowelReplacer(loader.Module):

View File

@@ -27,11 +27,15 @@
# requires: zipfile # requires: zipfile
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import zipfile import logging
import os import os
import zipfile
from datetime import datetime from datetime import datetime
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class SMArchiver(loader.Module): class SMArchiver(loader.Module):
@@ -64,7 +68,7 @@ class SMArchiver(loader.Module):
await utils.answer(message, self.strings["no_messages"]) await utils.answer(message, self.strings["no_messages"])
return return
archive_path = self.create_archive(saved_messages) archive_path = await self.create_archive(saved_messages)
try: try:
await message.client.send_file( await message.client.send_file(
@@ -79,7 +83,7 @@ class SMArchiver(loader.Module):
finally: finally:
self.cleanup(archive_path) self.cleanup(archive_path)
def create_archive(self, saved_messages): async def create_archive(self, saved_messages):
current_month = datetime.now().strftime("%B %Y") current_month = datetime.now().strftime("%B %Y")
archive_path = "saved_messages.zip" archive_path = "saved_messages.zip"

View File

@@ -26,11 +26,12 @@
# scope: TaskManager 0.0.1 # scope: TaskManager 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import asyncio
import datetime import datetime
import json import json
import logging import logging
import os
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from .. import loader, utils from .. import loader, utils
@@ -46,103 +47,138 @@ class Task:
due_date: Optional[datetime.datetime] = None due_date: Optional[datetime.datetime] = None
completed: bool = False completed: bool = False
created_at: datetime.datetime = field(default_factory=datetime.datetime.now) 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: class TaskManager:
"""Manages tasks, storing them in a JSON file.""" """Manages tasks, storing them in a JSON file."""
def __init__(self, data_file: str): def __init__(self, data_file: str):
self.data_file = data_file self.data_file = Path(data_file)
self.tasks: Dict[int, List[Task]] = {} self.tasks: Dict[int, List[Task]] = {}
self._lock = asyncio.Lock()
self.load_data() self.load_data()
def load_data(self): def load_data(self):
"""Loads task data from the JSON file.""" """Loads task data from the JSON file."""
if os.path.exists(self.data_file): if not self.data_file.exists():
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:
self.tasks = {} self.tasks = {}
logger.info("Task data file not found. Starting empty.") logger.info("Task data file not found. Starting empty.")
return
def save_data(self):
"""Saves task data to the JSON file."""
try: 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 = { data = {
user_id: [ str(user_id): [task.to_dict() for task in task_list]
{
"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
]
for user_id, task_list in self.tasks.items() for user_id, task_list in self.tasks.items()
} }
json.dump(data, f, indent=4, default=str) with open(self.data_file, "w", encoding="utf-8") as f:
except IOError as e: json.dump(data, f, indent=2, ensure_ascii=False)
logger.error(f"Failed to save task data: {e}") 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.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]): if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
del self.tasks[user_id][index] del self.tasks[user_id][index]
self.save_data() await self.save_data()
else: return True
logger.warning(f"Invalid index for removal: {index}, user: {user_id}") 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]): if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
self.tasks[user_id][index].completed = True self.tasks[user_id][index].completed = True
self.save_data() await self.save_data()
else: return True
logger.warning(f"Invalid index for completion: {index}, user: {user_id}") logger.warning(f"Invalid index for completion: {index}, user: {user_id}")
return False
def get_tasks(self, user_id: int) -> List[Task]: def get_tasks(self, user_id: int, include_completed: bool = True) -> List[Task]:
return self.tasks.get(user_id, []) 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: if user_id in self.tasks:
self.tasks[user_id] = [] self.tasks[user_id] = []
self.save_data() await self.save_data()
else: return True
logger.info(f"No tasks to clear for user: {user_id}") logger.info(f"No tasks to clear for user: {user_id}")
return False
def get_task(self, user_id: int, index: int) -> Optional[Task]: def get_task(self, user_id: int, index: int) -> Optional[Task]:
return ( if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]):
self.tasks[user_id][index] return self.tasks[user_id][index]
if user_id in self.tasks and 0 <= index < len(self.tasks[user_id]) return None
else 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 @loader.tds
@@ -198,7 +234,9 @@ class TaskManagerModule(loader.Module):
self.task_manager: Optional[TaskManager] = None self.task_manager: Optional[TaskManager] = None
async def client_ready(self, client, db): 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( @loader.command(
ru_doc="Добавить задачу:\n.taskadd <описание> | <дата (необязательно)>", ru_doc="Добавить задачу:\n.taskadd <описание> | <дата (необязательно)>",
@@ -230,7 +268,7 @@ class TaskManagerModule(loader.Module):
return return
task = Task(description=description, due_date=due_date) 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")) await utils.answer(message, self.strings("task_added"))
@loader.command(ru_doc="[index] - удалить задачу", en_doc="[index] - remove task") @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")) await utils.answer(message, self.strings("invalid_index"))
return 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")) 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( @loader.command(
ru_doc="[index] - Завершите задачу", en_doc="[index] - Complete task" ru_doc="[index] - Завершите задачу", en_doc="[index] - Complete task"
@@ -268,12 +304,10 @@ class TaskManagerModule(loader.Module):
await utils.answer(message, self.strings("invalid_index")) await utils.answer(message, self.strings("invalid_index"))
return 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")) 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") @loader.command(ru_doc="Список задач", en_doc="List tasks")
async def tasklist(self, message): async def tasklist(self, message):
@@ -342,8 +376,10 @@ class TaskManagerModule(loader.Module):
async def clear_confirm(self, call): async def clear_confirm(self, call):
"""Callback for confirming task clearing.""" """Callback for confirming task clearing."""
self.task_manager.clear_tasks(call.from_user.id) if await self.task_manager.clear_tasks(call.from_user.id):
await call.edit(self.strings("tasks_cleared")) await call.edit(self.strings("tasks_cleared"))
else:
await call.edit(self.strings("no_tasks"))
async def clear_cancel(self, call): async def clear_cancel(self, call):
"""Callback for canceling task clearing.""" """Callback for canceling task clearing."""

View File

@@ -26,8 +26,12 @@
# scope: Api TelegramStatusCodes 0.0.1 # scope: Api TelegramStatusCodes 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
responses = { responses = {
300: ( 300: (
"⛔ SEE_OTHER", "⛔ 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",
"""
Аналогично <b>400 BAD_REQUESTS</b>, но приложение должно отображать ошибку пользователю немного иначе.
Не показывайте пользователю видимую ошибку при получении конструктора <b>rpc_error</b>: вместо этого дождитесь обновления <a href="https://core.telegram.org/constructor/updateServiceNotification">updateServiceNotification</a> и обработайте его как обычно.
По сути, обновление-всплывающее окно <b>updateServiceNotification</b> будет отправлено независимо (т.е. НЕ как конструктор <b>Updates</b> внутри <b>rpc_result</b>, а как обычное обновление) сразу после выдачи 406 <b>rpc_error</b>: обновление будет содержать актуальное локализованное сообщение об ошибке для показа пользователю в интерфейсе.
Исключением является ошибка <b>AUTH_KEY_DUPLICATED</b>, которая возникает только в том случае, если любой из не-медиа DC обнаруживает, что авторизованная сессия отправляет запросы параллельно из двух отдельных TCP-соединений с одного или разных IP-адресов.
Обратите внимание, что параллельные соединения по-прежнему разрешены и фактически рекомендуются для медиа-DC.
Также обратите внимание, что под сессией понимается авторизованная сессия, идентифицируемая конструктором <a href="https://core.telegram.org/constructor/authorization">authorization</a>, которую можно получить с помощью <a href="https://core.telegram.org/method/account.getAuthorizations">account.getAuthorizations</a>, а не сессия MTProto.
Если клиент получает ошибку <b>AUTH_KEY_DUPLICATED</b>, сессия уже была аннулирована сервером, и пользователю необходимо сгенерировать новый ключ авторизации и войти снова.""",
),
420: (
"⛔ FLOOD",
"Превышено максимально допустимое количество попыток вызова данного метода с указанными входными параметрами. Например, при попытке запросить большое количество текстовых сообщений (SMS) для одного и того же номера телефона.",
),
500: (
"⛔ INTERNAL",
"""Произошла внутренняя ошибка сервера во время обработки запроса; например, произошел сбой при доступе к базе данных или файловому хранилищу.
Если клиент получает ошибку 500 или вы считаете, что эта ошибка не должна была возникнуть, пожалуйста, соберите как можно больше информации о запросе и ошибке и отправьте ее разработчикам.""",
),
}
@loader.tds @loader.tds
class TelegramStatusCodes(loader.Module): class TelegramStatusCodes(loader.Module):
@@ -91,20 +141,28 @@ class TelegramStatusCodes(loader.Module):
"args_incorrect": "<b>Неверные аргументы</b>", "args_incorrect": "<b>Неверные аргументы</b>",
"not_found": "<b>Код не найден</b>", "not_found": "<b>Код не найден</b>",
"syntax_error": "<b>Аргументы обязательны</b>", "syntax_error": "<b>Аргументы обязательны</b>",
"_cmd_doc_httpsc": "<код> - Получить информацию о Telegram error", "_cmd_doc_httpsc": "<код> - Получить информацию о статус-коде",
"_cmd_doc_httpscs": "Показать все доступные коды", "_cmd_doc_httpscs": "Показать все доступные коды",
"_cls_doc": "Словарь telegram error", "_cls_doc": "Словарь статус-кодов Telegram",
"scode": "<b>{} {}</b>\n⚜️ Описание статус-кода: <i>{}</i>",
} }
async def client_ready(self, client, db):
self.ub_lang = self._db.get("hikka.translations", "lang")
if not self.ub_lang:
self.ub_lang = self._db.get("heroku.translations", "lang")
@loader.unrestricted @loader.unrestricted
@loader.command( @loader.command(
ru_doc="<код состояния> - Получение информации о коде состояния", ru_doc="<код состояния> - Получение информации о статус-коде",
en_doc="<statuscode> - Get status code info", en_doc="<statuscode> - Get status code info",
) )
async def tgccmd(self, message): async def tgccmd(self, message):
args = utils.get_args(message) args = utils.get_args(message)
if not args: if not args:
await utils.answer(message, self.strings("syntax_error", message)) await utils.answer(message, self.strings("syntax_error", message))
return
try: try:
if int(args[0]) not in responses: if int(args[0]) not in responses:
@@ -112,17 +170,26 @@ class TelegramStatusCodes(loader.Module):
except ValueError: except ValueError:
await utils.answer(message, self.strings("args_incorrect", message)) await utils.answer(message, self.strings("args_incorrect", message))
await utils.answer( if self.ub_lang != "ru":
message, await utils.answer(
self.strings("scode", message).format( message,
responses[int(args[0])][0], args[0], responses[int(args[0])][1] 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.unrestricted
@loader.command( @loader.command(
ru_doc="Получите все коды статуса telegram", ru_doc="Получите все статус-коды Telegram",
en_doc="Get all telegram status codes", en_doc="Get all Telegram status codes",
) )
async def tgcscmd(self, message): async def tgcscmd(self, message):
await utils.answer( await utils.answer(

View File

@@ -27,7 +27,6 @@
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging import logging
import asyncio
from hikkatl import functions from hikkatl import functions
from datetime import datetime as dt from datetime import datetime as dt

View File

@@ -27,9 +27,12 @@
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import io import io
import logging
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class Text2File(loader.Module): class Text2File(loader.Module):
@@ -63,13 +66,15 @@ class Text2File(loader.Module):
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args: if not args:
await utils.answer(message, self.strings("no_args")) await utils.answer(message, self.strings("no_args"))
else: return
text = args
by = io.BytesIO(text.encode("utf-8"))
by.name = self.config["name"]
await utils.answer_file( text = args
message, by = io.BytesIO(text.encode("utf-8"))
by, by.name = self.config["name"]
reply_to=getattr(message, "reply_to_msg_id", None),
) await utils.send_file(
message.chat_id,
by,
caption=None,
reply_to=message.reply_to_msg_id,
)

View File

@@ -1,108 +0,0 @@
# Proprietary License Agreement
# Copyright (c) 2024-29 CodWiz
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
# ---------------------------------------------------------------------------------
# Name: Text in sticker
# Description: Text in sticker
# Author: @hikka_mods
# Commands:
# .st <hex color> [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 <color name> [text]",
}
strings_ru = {
"error": "Укажите .st <color name> [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="<color name> [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)

View File

@@ -26,20 +26,21 @@
# scope: Api TikTokDownloader 0.0.1 # scope: Api TikTokDownloader 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import aiohttp
import asyncio import asyncio
import re
import os
import warnings
import functools
import logging
from dataclasses import dataclass from dataclasses import dataclass
import logging
import os
import re
from typing import List, Optional, Union
from urllib.parse import urljoin from urllib.parse import urljoin
from typing import Union, Optional, List
import aiohttp
from tqdm import tqdm from tqdm import tqdm
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@dataclass @dataclass
class data: class data:
@@ -51,8 +52,11 @@ class data:
class TikTok: class TikTok:
def __init__(self, host: Optional[str] = None): def __init__(self, host: Optional[str] = None):
self.headers = { 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) " "User-Agent": (
"Version/4.0.4 Mobile/7B334b Safari/531.21.10" "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.host = host or "https://www.tikwm.com/"
self.session = aiohttp.ClientSession() self.session = aiohttp.ClientSession()
@@ -73,42 +77,25 @@ class TikTok:
self.logger.addHandler(handler) self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO) 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): async def close_session(self):
await self.session.close() await self.session.close()
async def _ensure_data(self, link: str): async def __ensure_data(self, link: str):
try: if self.link != link:
if self.result is None or self.link != link: self.link = link
self.link = link self.result = await self._fetch_data(link)
self.result = await self.fetch(link) self.logger.info("Successfully ensured data from the 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 __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"] download_dir = download_dir or self.result["id"]
os.makedirs(download_dir, exist_ok=True) os.makedirs(download_dir, exist_ok=True)
tasks = [ tasks = [
self._download_file(url, os.path.join(download_dir, f"image_{i + 1}.jpg")) self._download_file(url, os.path.join(download_dir, f"image_{i + 1}.jpg"))
for i, url in enumerate(self.result["images"]) for i, url in enumerate(self.result["images"])
] ]
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
self.logger.info(f"Images - Downloaded and saved photos to {download_dir}") self.logger.info(f"Images - Downloaded and saved photos to {download_dir}")
return data( return data(
@@ -120,7 +107,7 @@ class TikTok:
type="images", 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_url = self.result["hdplay"] if hd else self.result["play"]
video_filename = video_filename or f"{self.result['id']}.mp4" 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" 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( async with self.session.get(
urljoin(self.host, endpoint), params=params, headers=self.headers urljoin(self.host, endpoint), params=params, headers=self.headers
) as response: ) as response:
@@ -154,87 +184,12 @@ class TikTok:
urls = re.findall(r"http[s]?://[^\s]+", text) urls = re.findall(r"http[s]?://[^\s]+", text)
return urls[0] if urls else None return urls[0] if urls else None
@_warn() @staticmethod
async def convert_share_urls(self, url: str) -> Optional[str]: def _get_video_link(unique_id: str, aweme_id: str) -> 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:
return f"https://www.tiktok.com/@{unique_id}/video/{aweme_id}" 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}" return f"https://www.tiktok.com/@{unique_id}"

View File

@@ -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": "<emoji document_id=5337117114392127164>❌</emoji> <b>Specify emoji or emoji document_id</b>",
"no_time": "<emoji document_id=5337117114392127164>❌</emoji> <b>Specify time (ex: 1h, 30m, 2d)</b>",
"invalid_time": "<emoji document_id=5337117114392127164>❌</emoji> <b>Invalid time format (ex: 30m, 2h, 1d, 1w)</b>",
"status_set": "<emoji document_id=5336965905773504919>✅</emoji> <b>Status set:</b>\n<b>Current:</b> {}\n<b>Final:</b> {}\n<b>For:</b> {} ({})",
"status_updated": "<emoji document_id=5336965905773504919>✅</emoji> <b>Status updated: {}</b>",
"no_status": "<emoji document_id=5337117114392127164>❌</emoji> <b>No active status</b>",
"status_removed": "<emoji document_id=5336965905773504919>✅</emoji> <b>Status removed</b>",
"current_status": "<emoji document_id=5348186233610711303>📊</emoji> <b>Active status:</b>\n<b>Current:</b> {}\n<b>Final:</b> {}\n<b>Until:</b> {} ({})",
"no_premium": "<emoji document_id=5337117114392127164>❌</emoji> <b>Premium required for emoji status</b>",
"error": "<emoji document_id=5337117114392127164>❌</emoji> <b>Error: {}</b>",
}
strings_ru = {
"no_emoji": "<emoji document_id=5337117114392127164>❌</emoji> <b>Укажите эмодзи или document_id</b>",
"no_time": "<emoji document_id=5337117114392127164>❌</emoji> <b>Укажите время (напр: 1h, 30m, 2d)</b>",
"invalid_time": "<emoji document_id=5337117114392127164>❌</emoji> <b>Неверный формат времени (напр: 30m, 2h, 1d, 1w)</b>",
"status_set": "<emoji document_id=5336965905773504919>✅</emoji> <b>Статус установлен:</b>\n<b>Текущий:</b> {}\n<b>Финальный:</b> {}\n<b>На:</b> {} ({})",
"status_updated": "<emoji document_id=5336965905773504919>✅</emoji> <b>Статус обновлён: {}</b>",
"no_status": "<emoji document_id=5337117114392127164>❌</emoji> <b>Нет активного статуса</b>",
"status_removed": "<emoji document_id=5336965905773504919>✅</emoji> <b>Статус удалён</b>",
"current_status": "<emoji document_id=5348186233610711303>📊</emoji> <b>Активный статус:</b>\n<b>Текущий:</b> {}\n<b>Финальный:</b> {}\n<b>До:</b> {} ({})",
"no_premium": "<emoji document_id=5337117114392127164>❌</emoji> <b>Требуется Premium для эмодзи статуса</b>",
"error": "<emoji document_id=5337117114392127164>❌</emoji> <b>Ошибка: {}</b>",
}
def __init__(self):
self.status_data: Dict[int, Dict] = {}
self.scheduler_tasks: Dict[int, asyncio.Task] = {}
async def client_ready(self, client, db):
self._client = client
self._db = db
if not self._client.hikka_me.premium:
logger.warning("Premium required for emoji status functionality")
await self._restore_active_statuses()
async def _restore_active_statuses(self):
"""Restore and reschedule active statuses after restart"""
saved = self._db.get(__name__, "statuses", {})
current_time = time.time()
for user_id, data in saved.items():
end_time = data.get("end_time", 0)
if end_time > current_time:
remaining_time = end_time - current_time
logger.info(
f"Restoring status for user {user_id}, remaining: {remaining_time}s"
)
task = asyncio.create_task(
self._schedule_revert_sleep(user_id, remaining_time)
)
self.scheduler_tasks[user_id] = task
self.status_data[user_id] = data
else:
logger.info(f"Removing expired status for user {user_id}")
del saved[user_id]
if saved != self._db.get(__name__, "statuses", {}):
self._db.set(__name__, "statuses", saved)
def _parse_time(self, time_str: str) -> Optional[timedelta]:
"""Parse time string like 1h30m, 2d, 1w, 1mth"""
pattern = r"(\d+)([smhdwmth]+)"
matches = re.findall(pattern, time_str.lower())
if not matches:
return None
total_seconds = 0
for value, unit in matches:
value = int(value)
if unit == "s":
total_seconds += value
elif unit == "m":
total_seconds += value * 60
elif unit == "h":
total_seconds += value * 3600
elif unit == "d":
total_seconds += value * 86400
elif unit == "w":
total_seconds += value * 604800
elif unit in ["mth", "month"]:
total_seconds += value * 2592000 # 30 days
return timedelta(seconds=total_seconds)
def _format_time(self, td: timedelta) -> str:
"""Format timedelta to human readable string"""
total_days = td.days
months = total_days // 30
remaining_days = total_days % 30
if months > 0:
if remaining_days > 0:
return f"{months}mth {remaining_days}d"
return f"{months}mth"
elif total_days > 0:
return f"{total_days}d {td.seconds // 3600}h"
elif td.seconds >= 3600:
return f"{td.seconds // 3600}h {(td.seconds % 3600) // 60}m"
else:
return f"{td.seconds // 60}m"
def _extract_document_id(self, emoji_input: str) -> Optional[int]:
"""Extract document_id from emoji string"""
pattern = r"<emoji\s+document_id=(\d+)>.*?</emoji>"
match = re.search(pattern, emoji_input)
if match:
return int(match.group(1))
if emoji_input.isdigit():
return int(emoji_input)
return None
def _extract_document_id_from_entities(self, message: Message) -> Optional[int]:
"""Extract document_id from message entities"""
if not message.entities:
return None
for entity in message.entities:
if isinstance(entity, MessageEntityCustomEmoji):
return entity.document_id
return None
def _safe_emoji_display(
self, emoji_str: str, document_id: Optional[int] = None
) -> str:
"""Safely display emoji without causing errors"""
if not emoji_str:
return ""
if document_id:
return f"<emoji document_id={document_id}>📋</emoji>"
if emoji_str.isdigit():
return f"<emoji document_id={emoji_str}>📋</emoji>"
if "<emoji document_id=" in emoji_str:
return emoji_str
if len(emoji_str) == 1 or (
len(emoji_str) <= 4 and all(ord(c) >= 0x1F000 for c in emoji_str)
):
return emoji_str
return emoji_str
async def _set_emoji_status(
self, emoji_input: str, until: datetime | None = None, message: Message = None
) -> tuple[bool, Optional[int]]:
"""Set emoji status (requires Premium). Returns (success, document_id)"""
try:
logger.info(f"Setting emoji status for: {emoji_input}")
if not self._client.hikka_me.premium:
logger.warning("Premium required for emoji status")
return False, None
if not emoji_input:
logger.info("Removing emoji status")
await self._client(UpdateEmojiStatusRequest(emoji_status=None))
return True, None
document_id = None
if message:
document_id = self._extract_document_id_from_entities(message)
if document_id:
logger.info(
f"Found document_id from message entities: {document_id}"
)
if not document_id:
document_id = self._extract_document_id(emoji_input)
if document_id:
logger.info(f"Extracted document_id from text: {document_id}")
if not document_id:
try:
logger.info("Trying to get document_id from test message")
test_msg = await self._client.send_message("me", emoji_input)
document_id = self._extract_document_id_from_entities(test_msg)
await self._client.delete_messages("me", [test_msg.id])
if document_id:
logger.info(
f"Found document_id from test message: {document_id}"
)
else:
logger.warning("No document_id found in test message")
except Exception as e:
logger.error(f"Error getting document_id from test message: {e}")
if document_id:
try:
emoji_status = EmojiStatus(document_id=document_id, until=until)
await self._client(
UpdateEmojiStatusRequest(emoji_status=emoji_status)
)
logger.info(
f"Status set successfully with document_id: {document_id}"
)
return True, document_id
except Exception as e:
logger.error(f"Error setting status: {e}")
if "PREMIUM" in str(e).upper():
return False, None
return False, None
logger.warning("No document_id found, all methods failed")
return False, None
except Exception as e:
logger.error(f"General error setting emoji status: {e}")
return False, None
async def _revert_status(self, user_id: int):
"""Revert status to final emoji or remove"""
logger.info(f"Starting revert status for user {user_id}")
if user_id in self.scheduler_tasks:
del self.scheduler_tasks[user_id]
if user_id in self.status_data:
data = self.status_data[user_id]
final_emoji = data.get("final_emoji", "")
final_doc_id = data.get("final_doc_id")
logger.info(
f"Reverting status for user {user_id} to: '{final_emoji}' (saved doc_id: {final_doc_id})"
)
try:
if final_emoji and final_doc_id:
logger.info(
f"Setting final emoji using saved document_id: {final_doc_id}"
)
try:
emoji_status = EmojiStatus(document_id=final_doc_id)
await self._client(
UpdateEmojiStatusRequest(emoji_status=emoji_status)
)
logger.info(
f"Successfully set final emoji with document_id: {final_doc_id}"
)
except Exception as e:
logger.error(f"Error setting final emoji with document_id: {e}")
success, _ = await self._set_emoji_status(final_emoji)
if not success:
await self._set_emoji_status("")
elif final_emoji:
logger.info(f"Attempting to set final emoji: '{final_emoji}'")
success, final_doc_id = await self._set_emoji_status(final_emoji)
if success:
logger.info(
f"Successfully reverted to final emoji: '{final_emoji}' (doc_id: {final_doc_id})"
)
else:
logger.warning(
f"Failed to set final emoji '{final_emoji}', removing status instead"
)
await self._set_emoji_status("")
else:
logger.info("No final emoji specified, removing status")
await self._set_emoji_status("")
except Exception as e:
logger.error(f"Error reverting status: {e}")
try:
await self._set_emoji_status("")
except Exception as e2:
logger.error(f"Error removing status: {e2}")
logger.info(f"Removing status data for user {user_id}")
del self.status_data[user_id]
saved = self._db.get(__name__, "statuses", {})
if user_id in saved:
logger.info(f"Removing saved status for user {user_id}")
del saved[user_id]
self._db.set(__name__, "statuses", saved)
logger.info(f"Revert status completed for user {user_id}")
async def _schedule_revert_sleep(self, user_id: int, delay: float):
"""Schedule status revert using asyncio.sleep"""
try:
logger.info(f"Scheduling revert for user {user_id} in {delay} seconds")
await asyncio.sleep(delay)
await self._revert_status(user_id)
except asyncio.CancelledError:
logger.info(f"Revert task cancelled for user {user_id}")
except Exception as e:
logger.error(f"Error in scheduled revert for user {user_id}: {e}")
async def _schedule_revert(self, user_id: int, data: Dict):
"""Schedule status revert"""
end_time = data.get("end_time", 0)
delay = max(0, end_time - time.time())
self.status_data[user_id] = data
await self._schedule_revert_sleep(user_id, delay)
@loader.command(
ru_doc="<время> <эмодзи/document_id> [финальный_эмодзи/document_id] - установить временный статус",
en_doc="<time> <emoji/document_id> [final_emoji/document_id] - set temporary status",
)
async def setmoji(self, message: Message):
"""Set timed emoji status"""
args = utils.get_args_raw(message)
if not args:
return await utils.answer(message, self.strings["no_time"])
parts = args.split(maxsplit=2)
if len(parts) < 2:
return await utils.answer(message, self.strings["no_emoji"])
time_str, initial_emoji = parts[0], parts[1]
final_emoji = parts[2] if len(parts) > 2 else ""
td = self._parse_time(time_str)
if not td:
return await utils.answer(message, self.strings["invalid_time"])
if message.sender_id in self.scheduler_tasks:
self.scheduler_tasks[message.sender_id].cancel()
del self.scheduler_tasks[message.sender_id]
try:
success, initial_doc_id = await self._set_emoji_status(
initial_emoji, message=message
)
if not success:
return await utils.answer(message, self.strings["no_premium"])
except Exception as e:
return await utils.answer(message, self.strings["error"].format(str(e)))
final_doc_id = None
if final_emoji:
try:
final_doc_id = self._extract_document_id(final_emoji)
if not final_doc_id:
if message and len(parts) > 2:
emoji_entities = [
e
for e in message.entities
if isinstance(e, MessageEntityCustomEmoji)
]
if len(emoji_entities) >= 2:
final_doc_id = emoji_entities[1].document_id
if not final_doc_id:
try:
test_msg = await self._client.send_message("me", final_emoji)
final_doc_id = self._extract_document_id_from_entities(test_msg)
await self._client.delete_messages("me", [test_msg.id])
except Exception as e:
logger.warning(
f"Could not get document_id for final emoji: {e}"
)
if final_doc_id:
logger.info(f"Final emoji document_id: {final_doc_id}")
else:
logger.warning(
f"Could not resolve document_id for final emoji: {final_emoji}"
)
except Exception as e:
logger.warning(f"Error getting final emoji document_id: {e}")
end_time = time.time() + td.total_seconds()
user_id = message.sender_id
data = {
"initial_emoji": initial_emoji,
"final_emoji": final_emoji,
"initial_doc_id": initial_doc_id,
"final_doc_id": final_doc_id,
"end_time": end_time,
"set_time": time.time(),
}
self.status_data[user_id] = data
saved = self._db.get(__name__, "statuses", {})
saved[user_id] = data
self._db.set(__name__, "statuses", saved)
task = asyncio.create_task(
self._schedule_revert_sleep(user_id, td.total_seconds())
)
self.scheduler_tasks[user_id] = task
end_dt = datetime.fromtimestamp(end_time)
time_str = self._format_time(td)
logger.info(
f"Display formatting - initial: '{initial_emoji}' (doc_id: {initial_doc_id}), final: '{final_emoji}' (doc_id: {final_doc_id})"
)
current_display = self._safe_emoji_display(initial_emoji, initial_doc_id)
final_display = (
self._safe_emoji_display(final_emoji, final_doc_id)
if final_emoji
else "❌ (удалить)"
)
logger.info(
f"Display results - current: '{current_display}', final: '{final_display}'"
)
await utils.answer(
message,
self.strings["status_set"].format(
current_display, final_display, time_str, f"{end_dt:%H:%M:%S}"
),
)
@loader.command(ru_doc="Показать текущий статус", en_doc="Show current status")
async def showmoji(self, message: Message):
"""Show current emoji status"""
user_id = message.sender_id
if user_id not in self.status_data:
return await utils.answer(message, self.strings["no_status"])
data = self.status_data[user_id]
end_time = data.get("end_time", 0)
initial_emoji = data.get("initial_emoji", "")
final_emoji = data.get("final_emoji", "")
initial_doc_id = data.get("initial_doc_id")
final_doc_id = data.get("final_doc_id")
if end_time <= time.time():
return await utils.answer(message, self.strings["no_status"])
end_dt = datetime.fromtimestamp(end_time)
remaining = timedelta(seconds=end_time - time.time())
remaining_str = self._format_time(remaining)
current_display = self._safe_emoji_display(initial_emoji, initial_doc_id)
final_display = (
self._safe_emoji_display(final_emoji, final_doc_id)
if final_emoji
else "❌ (удалить)"
)
await utils.answer(
message,
self.strings["current_status"].format(
current_display, final_display, f"{end_dt:%H:%M:%S}", remaining_str
),
)
@loader.command(ru_doc="Удалить статус", en_doc="Remove status")
async def removemoji(self, message: Message):
"""Remove emoji status"""
user_id = message.sender_id
if user_id not in self.status_data:
return await utils.answer(message, self.strings["no_status"])
if user_id in self.scheduler_tasks:
self.scheduler_tasks[user_id].cancel()
del self.scheduler_tasks[user_id]
await self._revert_status(user_id)
await utils.answer(message, self.strings["status_removed"])
async def on_unload(self):
"""Cancel all scheduled tasks on unload"""
for task in self.scheduler_tasks.values():
task.cancel()
self.scheduler_tasks.clear()

View File

@@ -26,14 +26,13 @@
# scope: UserbotAvast 0.0.1 # scope: UserbotAvast 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import ast import ast
import astor
import requests
import base64 import base64
import zlib import logging
import re import re
import urllib.parse import zlib
import requests
from .. import loader, utils from .. import loader, utils
@@ -697,7 +696,7 @@ class SecurityAnalyzer:
for node in ast.walk(tree): for node in ast.walk(tree):
if isinstance(node, ast.For): if isinstance(node, ast.For):
for send_method in send_methods: for send_method in send_methods:
if send_method in astor.to_source(node): if send_method in ast.unparse(node):
issue_key = ( issue_key = (
f"Mass {send_method}", f"Mass {send_method}",
node.lineno, node.lineno,
@@ -715,8 +714,8 @@ class SecurityAnalyzer:
) )
self.reported_issues.add(issue_key) self.reported_issues.add(issue_key)
if "time.sleep(" in astor.to_source(tree): if "time.sleep(" in ast.unparse(tree):
sleep_calls = re.findall(r"time\.sleep\((.*?)\)", astor.to_source(tree)) sleep_calls = re.findall(r"time\.sleep\((.*?)\)", ast.unparse(tree))
for sleep_time in sleep_calls: for sleep_time in sleep_calls:
try: try:
sleep_value = float(sleep_time) sleep_value = float(sleep_time)

View File

@@ -24,82 +24,109 @@
# meta developer: @hikka_mods # meta developer: @hikka_mods
# scope: Video2GIF # scope: Video2GIF
# scope: Video2GIF 0.0.1 # scope: Video2GIF 0.0.1
# requires: moviepy
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import asyncio
import logging
import os import os
import subprocess import shutil
import tempfile
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class Video2GIF(loader.Module): class Video2GIFMod(loader.Module):
"""Converts video to GIF""" """Convert video to high quality GIF"""
strings = { strings = {
"name": "Video2GIF", "name": "Video2GIF",
"conversion_success": "🎉 The conversion is completed!", "success": "✅ GIF created",
"conversion_error": "An error occurred when converting video to GIF.", "error": "Conversion failed",
"not_video": "⚠️ Please reply to the message with the video or send the video in one message.", "no_video": "❌ Reply to a video",
"loading": "⏳ Conversion is underway", "no_ffmpeg": "❌ FFmpeg not installed. Install: apt install ffmpeg",
"processing": "🔄 Processing video...",
"compressing": "📦 Optimizing GIF...",
} }
strings_ru = { strings_ru = {
"conversion_success": "🎉 Преобразование завершено!", "success": "✅ GIF создан",
"conversion_error": "Произошла ошибка при преобразовании видео в GIF.", "error": "Ошибка конвертации",
"not_video": "⚠️ Пожалуйста, ответьте на сообщение с видео или отправьте видео одним сообщением.", "no_video": "❌ Ответьте на видео",
"loading": "⏳ Идет преобразование", "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( @loader.command(
ru_doc="[reply | в одном сообщении с видео] конвертирует видео в GIF.", ru_doc="[ответ] [fps] [ширина] - конвертировать видео в GIF",
en_doc="[reply | in one message with video] — Converts video to GIF.", en_doc="[reply] [fps] [width] - convert video to GIF",
) )
async def gifc(self, message): 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: reply = await message.get_reply_message()
await utils.answer(message, self.strings["not_video"]) if not reply or not reply.video:
return return await utils.answer(message, self.strings["no_video"])
await utils.answer(message, self.strings["loading"]) args = utils.get_args_raw(message).split()
video_path = await self.client.download_media(video) fps = 15 if len(args) < 1 else min(int(args[0]), 30)
gif_path = f"{os.path.splitext(video_path)[0]}.gif" width = 480 if len(args) < 2 else min(int(args[1]), 1024)
msg = await utils.answer(message, self.strings["processing"])
try: try:
self.convert_video_to_gif(video_path, gif_path) gif_path = await self._convert_to_gif(reply, fps, width)
await message.client.send_file(
message.chat_id, gif_path, caption=self.strings["conversion_success"] 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): os.remove(gif_path)
"""Получает видео из сообщения.""" await msg.delete()
if reply := await message.get_reply_message():
return reply.video
return message.video
def convert_video_to_gif(self, video_path: str, gif_path: str) -> None: except Exception:
"""Конвертирует видео в GIF с улучшенными параметрами.""" await utils.answer(message, self.strings["error"])
command = [
"ffmpeg",
"-i",
video_path,
"-vf",
"fps=30,scale=640:-1:flags=lanczos",
"-c:v",
"gif",
gif_path,
]
subprocess.run(command, check=True)
def cleanup_temp_files(self, video_path: str, gif_path: str) -> None: async def _convert_to_gif(self, reply, fps: int, width: int) -> str:
"""Удаляет временные файлы.""" """Convert video to optimized GIF"""
for temp_file in [video_path, gif_path]: with tempfile.TemporaryDirectory() as tmpdir:
if os.path.exists(temp_file): video_path = os.path.join(tmpdir, "video.mp4")
os.remove(temp_file) gif_path = os.path.join(tmpdir, "output.gif")
await reply.download_media(video_path)
cmd = [
"ffmpeg",
"-i",
video_path,
"-vf",
f"fps={fps},scale={width}:-1:flags=lanczos",
"-lavfi",
"[0:v]split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
"-y",
gif_path,
]
proc = await asyncio.create_subprocess_exec(*cmd)
await proc.communicate()
return gif_path

View File

@@ -26,136 +26,214 @@
# requires: json aiohttp tempfile # requires: json aiohttp tempfile
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import asyncio
import logging
import os import os
import tempfile import tempfile
import logging from typing import Any, Dict, Optional
import aiohttp
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class VirusTotalMod(loader.Module): class VirusTotalMod(loader.Module):
"""Checks files for viruses using VirusTotal.""" """Professional file scanning with VirusTotal"""
strings = { strings = {
"name": "VirusTotal", "name": "VirusTotal",
"no_file": "<emoji document_id=5210952531676504517>🚫</emoji> <b>You haven't selected a file.</b>", "no_file": "🚫 Reply to a file",
"download": "<emoji document_id=5334677912270415274>😑</emoji> <b>Downloading...</b>", "downloading": "📥 Downloading file...",
"scan": "<emoji document_id=5325792861885570739>🫥</emoji> <b>Scanning...</b>", "uploading": "📤 Uploading to VirusTotal...",
"link": "🦠 VirusTotal Link", "scanning": "🔍 Scanning in progress...",
"no_virus": "✅ File is clean.", "waiting": "⏳ Waiting for analysis...",
"error": "<emoji document_id=5463193238393274687>⚠️</emoji> Scan error.", "no_key": "🚫 Set VirusTotal API key in config",
"no_format": "This format is not supported.", "error": "❌ Error during scan",
"no_apikey": "<emoji document_id=5260342697075416641>🚫</emoji> You have not specified an API Key", "size_limit": "📁 File exceeds 32MB limit",
"config": "Need a token with www.virustotal.com/gui/my-apikey", "timeout": "⏰ Scan timeout",
"scanning": "<emoji document_id=5325792861885570739>🫥</emoji> <b>Waiting for scan results...</b>", "clean": "✅ File is clean",
"getting_upload_url": "<emoji document_id=5325792861885570739>🫥</emoji> <b>Getting upload URL...</b>", "suspicious": "⚠️ Suspicious file",
"analysis_failed": "<emoji document_id=5463193238393274687>⚠️</emoji> Analysis failed after multiple retries.", "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 = { strings_ru = {
"no_file": "<emoji document_id=5210952531676504517>🚫</emoji> </b>Вы не выбрали файл.</b>", "no_file": "🚫 Ответьте на файл",
"download": "<emoji document_id=5334677912270415274>😑</emoji> </b>Скачивание...</b>", "downloading": "📥 Скачиваю файл...",
"scan": "<emoji document_id=5325792861885570739>🫥</emoji> <b>Сканирую...</b>", "uploading": "📤 Загружаю на VirusTotal...",
"link": "🦠 Ссылка на VirusTotal", "scanning": "🔍 Сканирую...",
"no_virus": "✅ Файл чист.", "waiting": "⏳ Жду анализа...",
"error": "<emoji document_id=5463193238393274687>⚠️</emoji> Ошибка сканирования.", "no_key": "🚫 Укажите API ключ в конфиге",
"no_format": "Этот формат не поддерживается.", "error": "❌ Ошибка при сканировании",
"no_apikey": "<emoji document_id=5260342697075416641>🚫</emoji> Вы не указали Api Key", "size_limit": "📁 Файл больше 32МБ",
"config": "Need a token with www.virustotal.com/gui/my-apikey", "timeout": "⏰ Таймаут сканирования",
"scanning": "<emoji document_id=5325792861885570739>🫥</emoji> <b>Ожидание результатов сканирования...</b>", "clean": "✅ Файл чистый",
"getting_upload_url": "<emoji document_id=5325792861885570739>🫥</emoji> <b>Получение URL для загрузки...</b>", "suspicious": "⚠️ Подозрительный файл",
"analysis_failed": "<emoji document_id=5463193238393274687>⚠️</emoji> Анализ не удался после нескольких попыток.", "malicious": "⛔ Вредоносный файл",
"view_report": "📊 Полный отчёт",
"close": "❌ Закрыть",
"engines": "Антивирусов",
"detections": "Обнаружено",
"status": "Статус",
"completed": "Завершён",
"queued": "В очереди",
"scan_date": "Дата сканирования",
} }
def __init__(self): def __init__(self):
self.config = loader.ModuleConfig( self.config = loader.ModuleConfig(
loader.ConfigValue( loader.ConfigValue(
"token-vt", "api_key",
None, None,
lambda: "Need a token with www.virustotal.com/gui/my-apikey", "VirusTotal API key from https://virustotal.com",
validator=loader.validators.Hidden(), 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): async def client_ready(self, client, db):
self.hmodslib = await self.import_lib( self._client = client
"https://files.archquise.ru/HModsLibrary.py" 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( @loader.command(
ru_doc="<ответ на файл> - Проверяет файлы на наличие вирусов с использованием VirusTotal", ru_doc="[ответ] - просканировать файл через VirusTotal",
en_doc="<file response> - Checks files for viruses using VirusTotal", en_doc="[reply] - scan file with VirusTotal",
) )
async def vt(self, message): async def vt(self, message):
if not message.is_reply: """Scan file with VirusTotal"""
await utils.answer(message, self.strings("no_file")) api_key = self.config["api_key"]
return if not api_key:
return await utils.answer(message, self.strings["no_key"])
reply = await message.get_reply_message() reply = await message.get_reply_message()
if not reply.document: if not reply or not reply.document:
await utils.answer(message, self.strings("no_file")) return await utils.answer(message, self.strings["no_file"])
return
api_key = self.config.get("token-vt") async with self._get_session() as session:
if not api_key: try:
await utils.answer(message, self.strings("no_apikey")) msg = await utils.answer(message, self.strings["downloading"])
return
file_extension = os.path.splitext(reply.file.name)[1].lower() with tempfile.TemporaryDirectory() as tmpdir:
allowed_extensions = (".jpg", ".png", ".ico", ".mp3", ".mp4", ".gif", ".txt") file_path = os.path.join(tmpdir, reply.file.name)
if file_extension in allowed_extensions: await reply.download_media(file_path)
await utils.answer(message, self.strings("no_format"))
return
try: file_size = os.path.getsize(file_path)
await utils.answer(message, self.strings("download")) if file_size > self.MAX_SIZE:
with tempfile.TemporaryDirectory() as temp_dir: return await msg.edit(self.strings["size_limit"])
file_path = os.path.join(temp_dir, reply.file.name)
await reply.download_media(file_path)
file_size = os.path.getsize(file_path) await msg.edit(self.strings["uploading"])
is_large_file = file_size > 32 * 1024 * 1024 analysis_id = await self._upload_file(session, file_path)
if is_large_file: await msg.edit(self.strings["waiting"])
await utils.answer(message, self.strings("getting_upload_url")) result = await self._wait_for_analysis(session, analysis_id)
await utils.answer(message, self.strings("scan"))
analysis_results = await self.hmodslib.scan_file_virustotal( await self._show_results(msg, analysis_id, result)
file_path, api_key, is_large_file
)
if analysis_results: except asyncio.TimeoutError:
formatted_results = self.hmodslib.format_analysis_results( await utils.answer(message, self.strings["timeout"])
analysis_results except Exception as e:
) error_text = f"{self.strings['error']}: {str(e)[:100]}"
try: await utils.answer(message, error_text)
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"]
),
)
else: async def _upload_file(self, session: aiohttp.ClientSession, path: str) -> str:
await utils.answer(message, self.strings("analysis_failed")) """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: async with session.post(
logger.exception("An error occurred during the VT scan process.") "https://www.virustotal.com/api/v3/files", data=form
await utils.answer( ) as response:
message, self.strings("error") + f"\n\n{type(e).__name__}: {str(e)}" response.raise_for_status()
) data = await response.json()
return data["data"]["id"]
async def _wait_for_analysis(
self, session: aiohttp.ClientSession, analysis_id: str
) -> Dict[str, Any]:
"""Poll analysis results until completion"""
url = f"https://www.virustotal.com/api/v3/analyses/{analysis_id}"
for _ in range(20):
async with session.get(url) as response:
response.raise_for_status()
data = await response.json()
status = data["data"]["attributes"]["status"]
if status == "completed":
return data
await asyncio.sleep(3)
raise asyncio.TimeoutError()
async def _show_results(self, message, analysis_id: str, result: Dict[str, Any]):
"""Display scan results in inline form"""
stats = result["data"]["attributes"]["stats"]
date = result["data"]["attributes"]["date"]
malicious = stats.get("malicious", 0)
suspicious = stats.get("suspicious", 0)
undetected = stats.get("undetected", 0)
harmless = stats.get("harmless", 0)
total = malicious + suspicious + undetected + harmless
if malicious > 0:
verdict = self.strings["malicious"]
emoji = ""
elif suspicious > 0:
verdict = self.strings["suspicious"]
emoji = "⚠️"
else:
verdict = self.strings["clean"]
emoji = ""
from datetime import datetime
scan_date = datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:%S")
text = (
f"{emoji} <b>VirusTotal Scan Results</b>\n\n"
f"<b>{self.strings['status']}:</b> {verdict}\n"
f"<b>{self.strings['detections']}:</b> {malicious}\n"
f"<b>{self.strings['engines']}:</b> {total}\n"
f"<b>{self.strings['scan_date']}:</b> {scan_date}\n\n"
f"<code>Malicious: {malicious}/{total}</code>\n"
f"<code>Suspicious: {suspicious}/{total}</code>\n"
f"<code>Harmless: {harmless}/{total}</code>\n"
f"<code>Undetected: {undetected}/{total}</code>"
)
vt_url = f"https://www.virustotal.com/gui/file-analysis/{analysis_id}"
await self.inline.form(
text=text,
message=message,
reply_markup=[
[{"text": f"🔗 {self.strings['view_report']}", "url": vt_url}],
[{"text": self.strings["close"], "action": "close"}],
],
ttl=300, # 5 minutes timeout
)

View File

@@ -27,99 +27,92 @@
# requires: tempfile # requires: tempfile
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import asyncio
import logging
import os import os
import subprocess import shutil
import tempfile import tempfile
import time
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class VoiceDL(loader.Module): class VoiceDLMod(loader.Module):
"""Voice Downloader module""" """Download voice messages as MP3"""
strings = { strings = {
"name": "VoiceDL", "name": "VoiceDL",
"download_success": "Voice message downloaded in MP3 format.", "success": "Voice downloaded as MP3",
"download_error": "Error downloading voice message.", "error": "Error downloading voice",
"no_voice_message": "Please reply to a voice message.", "no_voice": "❌ Reply to a voice message",
"conversion_error": "Error converting to MP3.", "no_ffmpeg": "❌ FFmpeg not found. Install: apt install ffmpeg",
"file_not_found": "File not found.",
"unsupported_format": "The file format is not supported.",
} }
strings_ru = { strings_ru = {
"download_success": "Голосовое сообщение загружено в формате MP3.", "success": "Голосовое скачано как MP3",
"download_error": "Ошибка при загрузке голосового сообщения.", "error": "Ошибка скачивания",
"no_voice_message": "Пожалуйста, ответьте на голосовое сообщение.", "no_voice": "Ответьте на голосовое",
"conversion_error": "Ошибка при конвертации в MP3.", "no_ffmpeg": "❌ FFmpeg не установлен. Установите: apt install ffmpeg",
"file_not_found": "Файл не найден.",
"unsupported_format": "Формат файла не поддерживается.",
} }
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( @loader.command(
ru_doc=" [reply] — загружает выбранное голосовое сообщение в виде файла mp3 и кидает его в чат.", ru_doc="[ответ] - скачать голосовое как MP3",
en_doc=" [reply] downloads the selected voice message as an MP3 file and sends it in the chat.", en_doc="[reply] - download voice as MP3",
) )
async def voicedl(self, message): 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() reply = await message.get_reply_message()
if not reply or not reply.voice:
return await utils.answer(message, self.strings["no_voice"])
if reply: await self._process_voice(message, 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)
timestamp = int(time.time()) async def _process_voice(self, message, reply):
mp3_file_path = f"voice_message_{timestamp}.mp3" 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( proc = await asyncio.create_subprocess_exec(
message.chat.id, "ffmpeg",
mp3_file_path, "-i",
caption=self.strings("download_success"), 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) if proc.returncode != 0:
os.remove(mp3_file_path) raise Exception("FFmpeg error")
except FileNotFoundError: await message.client.send_file(
await utils.answer( message.chat.id,
message, mp3_path,
self.strings("download_error") caption=self.strings["success"],
+ " " reply_to=reply.id,
+ 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"))
async def convert_to_mp3(self, input_file: str, output_file: str): except Exception:
"""Convert audio file to MP3 format using FFmpeg.""" await utils.answer(message, self.strings["error"])
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,
)

View File

@@ -27,11 +27,11 @@
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging import logging
import requests
from datetime import datetime
from typing import Union, Dict, List
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from typing import Dict, List, Union
import requests
from .. import loader, utils from .. import loader, utils

View File

@@ -28,109 +28,108 @@
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging import logging
import json import time
import requests
from .. import loader, utils import aiohttp
from .. import loader
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class WindowsKeys(loader.Module): class WindowsKeysMod(loader.Module):
"""Provides you Windows activation keys""" """Windows activation keys"""
strings = { strings = {
"name": "WindowsKeys", "name": "WindowsKeys",
"winkey": "Your key: <code>{}</code>\n\nWarning! This key is not a pirate key. It is taken from the official Microsoft site and is intended for further activation via KMS-server", "winkey": "Key: <code>{}</code>\n\nFor KMS activation only",
"error": "An error occurred while retrieving the key. Please try again later.", "error": "Failed to get key",
"select": "🔓 Select version:",
"close": "🎈 Close",
"loading": "⌛ Loading...",
} }
strings_ru = { strings_ru = {
"winkey": "Ваш ключ: <code>{}</code>\n\nВнимание! Указанный ключ не является пиратским. Он взят с официального сайта Microsoft и предназначен для дальнейшей активации посредством KMS-сервера", "winkey": "Ключ: <code>{}</code>\n\nТолько для KMS активации",
"error": "Произошла ошибка при получении ключа. Попробуйте позже.", "error": "Ошибка получения",
"select": "🔓 Выберите версию:",
"close": "🎈 Закрыть",
"loading": "⌛ Загрузка...",
} }
@loader.command( def __init__(self):
ru_doc="Открывает выбор ключа для активации Windows", self.cache = None
en_doc="Opens the Windows activation key selection", 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): async def winkey(self, message):
await self.inline.form( await self.inline.form(
text="🔓 Выберите версию и издание Windows, для которой вам необходим ключ", self.strings["select"],
message=message, message=message,
reply_markup=[ reply_markup=[
[ [
{ {
"text": "Windows 10/11 Pro", "text": "Win 10/11 Pro",
"callback": self._inline__give_key, "callback": self._key,
"args": ["win10_11pro"], "args": ("win10_11pro",),
} }
], ],
[ [
{ {
"text": "Windows 10/11 Enterprise LTSC", "text": "Win 10/11 LTSC",
"callback": self._inline__give_key, "callback": self._key,
"args": ["win10_11enterpriseLTSC"], "args": ("win10_11enterpriseLTSC",),
} }
], ],
[ [
{ {
"text": "Windows 8.1 Pro", "text": "Win 8.1 Pro",
"callback": self._inline__give_key, "callback": self._key,
"args": ["win8.1pro"], "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", "text": "Vista Business",
"callback": self._inline__give_key, "callback": self._key,
"args": ["win8pro"], "args": ("winvistabusiness",),
}
],
[
{
"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": self.strings["close"], "action": "close"}],
], ],
force_me=False,
silent=True,
) )
async def _inline__give_key(self, call, winver): async def _key(self, call, version):
url = "https://files.archquise.ru/winkeys.json" 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: try:
response = requests.get(url) async with aiohttp.ClientSession(
response.raise_for_status() timeout=aiohttp.ClientTimeout(10)
data = response.json() ) as session:
await call.edit(self.strings["winkey"].format(data[winver])) async with session.get("https://files.archquise.ru/winkeys.json") as r:
self.cache = await r.json()
except requests.exceptions.RequestException as e: self.cache_time = time.time()
logger.error("Request error: %e", e) return self.cache
await call.answer(self.strings("error"), show_alert=True) except Exception: # noqa: E722
except json.JSONDecodeError as e: return None
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)

View File

@@ -27,10 +27,13 @@
# requires: requests # requires: requests
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import requests import requests
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class animals(loader.Module): class animals(loader.Module):
@@ -38,13 +41,13 @@ class animals(loader.Module):
strings = { strings = {
"name": "animals", "name": "animals",
"loading": "<b>Generation is underway</b>", "loading": "<b>Generation is underway</b> <emoji document_id=5215484787325676090>🕐</emoji>",
"done": "<b>Here is your salute</b>", "done": "<b>Here is your salute</b> <emoji document_id=5436246187944460315>❤️</emoji>",
} }
strings_ru = { strings_ru = {
"loading": "<b>Генерация идет полным ходом</b>", "loading": "<b>Генерация идет полным ходом</b> <emoji document_id=5215484787325676090>🕐</emoji>",
"done": "<b>Вот ваш результат</b>", "done": "<b>Вот ваш результат</b> <emoji document_id=5436246187944460315>❤️</emoji>",
} }
# thanks https://github.com/C0dwiz/H.Modules/pull/1 # thanks https://github.com/C0dwiz/H.Modules/pull/1
@@ -58,7 +61,7 @@ class animals(loader.Module):
) )
async def fcatcmd(self, message): async def fcatcmd(self, message):
await utils.answer(message, self.strings("loading")) 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( await utils.answer_file(
message, cat_url, self.strings("done"), force_document=True message, cat_url, self.strings("done"), force_document=True
) )
@@ -80,7 +83,7 @@ class animals(loader.Module):
) )
async def catcmd(self, message): async def catcmd(self, message):
await utils.answer(message, self.strings("loading")) 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( await utils.answer_file(
message, cat_url, self.strings("done"), force_document=False message, cat_url, self.strings("done"), force_document=False
) )

View File

@@ -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):
""" <keyword> [-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):
""" <start_time> <end_time> [-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):
""" <length> [-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

View File

@@ -27,10 +27,15 @@
# requires: aiohttp # requires: aiohttp
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import aiohttp import aiohttp
import re
import random
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class face(loader.Module): class face(loader.Module):
@@ -64,15 +69,16 @@ class face(loader.Module):
async def rfacecmd(self, message): async def rfacecmd(self, message):
await utils.answer(message, self.strings("loading")) 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 aiohttp.ClientSession() as session:
async with session.get(url) as response: async with session.get(url) as response:
if response.status == 200: if response.status == 200:
data = await response.json() data = await response.text()
random_face = data["data"] 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( await utils.answer(
message, self.strings("random_face").format(random_face) message, self.strings("random_face").format(kaomoji)
) )
else: else:
await utils.answer(message, self.strings("error")) await utils.answer(message, self.strings("error"))

View File

@@ -1,47 +1,54 @@
ASCIIArt
AccountData AccountData
AniLibria AniLiberty
animals
AnimeQuotes AnimeQuotes
Article Article
ASCIIArt
AutofarmCookies AutofarmCookies
BirthdayTime BirthdayTime
CheckSpamBan CheckSpamBan
CodeShare
CryptoCurrency CryptoCurrency
EnvsSH
face
FakeActions FakeActions
FakeWallet FakeWallet
full FolderAutoRead
GigaChat GigaChat
globalrestrict H
hikkahost HAFK
HInstall
InfoBannersManager
InlineButton InlineButton
InlineCoin InlineCoin
InlineHelper InlineHelper
IrisSimpleMod IrisSimpleMod
jacques
KBSwapper KBSwapper
Memes Memes
MessageMonitor
MooFarmRC1
Music Music
novoice
nsfwart
numbersapi
PastebinAPI
profile
ReplaceVowels ReplaceVowels
SafetyMod
search
shortener
SMAcrhiver SMAcrhiver
TaskManager
TelegramStatusCodes TelegramStatusCodes
TempChat TempChat
Text2File Text2File
Text_Sticker
TikTokDownloader TikTokDownloader
TimedEmojiStatus
UserbotAvast
Video2GIF Video2GIF
VirusTotal VirusTotal
VoiceDL VoiceDL
HAFK
Weather Weather
InfoBannersManager WindowsKeys
animals
face
globalrestrict
hikkahost
mediatools
novoice
nsfwart
passgen
profile
search
shortener
timezone
ytdl

View File

@@ -38,6 +38,7 @@
# scope: Api GlobalRestrict 0.0.1 # scope: Api GlobalRestrict 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import re import re
import time import time
import typing import typing
@@ -51,6 +52,8 @@ from telethon.tl.types import (
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
BANNED_RIGHTS = { BANNED_RIGHTS = {
"view_messages": False, "view_messages": False,
"send_messages": False, "send_messages": False,

View File

@@ -26,12 +26,15 @@
# scope: api HikkaHost 0.0.1 # scope: api HikkaHost 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import aiohttp
import json import json
import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import aiohttp
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
class HostApi: class HostApi:
""" """
@@ -55,7 +58,7 @@ class HostApi:
Returns: Returns:
dict: The API response as a dictionary. 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 aiohttp.ClientSession(trust_env=True) as session:
async with session.request( async with session.request(
method, method,
@@ -311,4 +314,4 @@ class HikkahostMod(loader.Module):
user_id = token.split(":")[0] user_id = token.split(":")[0]
api = HostApi(token) api = HostApi(token)
data = await api.action(user_id, token) await api.action(user_id, token)

View File

@@ -27,13 +27,16 @@
# scope: Жаконизатор 0.0.1 # scope: Жаконизатор 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import aiohttp
import io import io
from PIL import Image, ImageDraw, ImageFont import logging
from textwrap import wrap from textwrap import wrap
import aiohttp
from PIL import Image, ImageDraw, ImageFont
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class JacquesMod(loader.Module): class JacquesMod(loader.Module):

View File

@@ -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сс:8м16сс",
"compress_usage": "Доступные качества: 144p, 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p",
"split_time_usage": "Пример: .split 10m (10 минут)",
"split_size_usage": "Используйте: .split 10m или .split 5MB",
"merge_usage": "Ответьте на первое видео/аудио",
"min_files": "Нужно как минимум 2 медиафайла в цепочке",
"downloading": "Скачиваю {} файлов...",
"part": "Часть {}/{}",
}
async def client_ready(self, client, db):
self._client = client
self._db = db
if not check_ffmpeg():
self.logger.warning(self.strings["no_ffmpeg"])
@loader.command(
ru_doc="<формат> - конвертировать медиа в указанный формат",
en_doc="<format> - convert media to specified format",
)
async def convert(self, message: Message):
reply = await message.get_reply_message()
if not reply or not reply.media:
return await utils.answer(message, self.strings["no_reply"])
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
args = utils.get_args_raw(message).lower()
formats = {
"mp3": "audio",
"flac": "audio",
"wav": "audio",
"aac": "audio",
"ogg": "audio",
"m4a": "audio",
"opus": "audio",
"mp4": "video",
"avi": "video",
"mkv": "video",
"mov": "video",
"wmv": "video",
"flv": "video",
"webm": "video",
"3gp": "video",
"hevc": "video",
"h264": "video",
}
if not args or args not in formats:
return await utils.answer(message, self.strings["available_formats"])
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/")
output = f"{file.rsplit('.', 1)[0]}_converted.{args}"
cmd = ["ffmpeg", "-i", file, "-y"]
if formats[args] == "audio":
if args == "mp3":
cmd.extend(["-codec:a", "libmp3lame", "-q:a", "2"])
elif args == "flac":
cmd.extend(["-codec:a", "flac", "-compression_level", "12"])
elif args == "opus":
cmd.extend(["-codec:a", "libopus", "-b:a", "128k"])
elif args == "aac":
cmd.extend(["-codec:a", "aac", "-b:a", "192k"])
elif formats[args] == "video":
if args in ["hevc", "h264"]:
codec = "libx265" if args == "hevc" else "libx264"
cmd.extend(["-codec:v", codec, "-preset", "medium", "-crf", "23"])
if args == "webm":
cmd.extend(["-codec:v", "libvpx-vp9", "-b:v", "1M"])
cmd.append(output)
process = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["converted"].format(args),
reply_to=reply.id,
)
os.remove(file)
if os.path.exists(output):
os.remove(output)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
@loader.command(
ru_doc="Скачать голосовое сообщение как файл",
en_doc="Download voice message as file",
)
async def voicedl(self, message: Message):
reply = await message.get_reply_message()
if not reply or not reply.voice:
return await utils.answer(message, self.strings["no_reply"])
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/voice.ogg")
new_file = file.replace(".ogg", ".opus")
os.rename(file, new_file)
await message.client.send_file(
message.peer_id,
new_file,
caption=self.strings["downloaded"],
reply_to=reply.id,
voice_note=False,
)
os.remove(new_file)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
@loader.command(ru_doc="Преобразовать видео в GIF", en_doc="Convert video to GIF")
async def gif(self, message: Message):
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
reply = await message.get_reply_message()
if not reply or not reply.video:
return await utils.answer(message, self.strings["no_reply"])
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/")
output = f"{file.rsplit('.', 1)[0]}.gif"
cmd = [
"ffmpeg",
"-i",
file,
"-vf",
"fps=10,scale=480:-1:flags=lanczos",
"-gifflags",
"+transdiff",
"-y",
output,
]
process = await asyncio.create_subprocess_exec(*cmd)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["gif_created"],
reply_to=reply.id,
)
os.remove(file)
os.remove(output)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
def parse_time(self, time_str: str) -> Optional[float]:
time_str = time_str.lower()
total = 0
pattern = r"(\d+\.?\d*)([мm]?[сc]|[мm][сc]?)"
matches = re.findall(pattern, time_str)
for value, unit in matches:
value = float(value)
if "м" in unit or "m" in unit:
total += value * 60
elif "с" in unit or "c" in unit:
total += value
return total if total > 0 else None
@loader.command(
ru_doc="<начало:конец> - обрезать медиа по времени",
en_doc="<start:end> - trim media by time",
)
async def cut(self, message: Message):
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
reply = await message.get_reply_message()
if not reply or not reply.media:
return await utils.answer(message, self.strings["no_reply"])
args = utils.get_args_raw(message)
if not args or ":" not in args:
return await utils.answer(message, self.strings["cut_usage"])
start_str, end_str = args.split(":", 1)
start = self.parse_time(start_str)
end = self.parse_time(end_str)
if start is None or end is None or start >= end:
return await utils.answer(message, self.strings["invalid_args"])
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/")
output = f"{file.rsplit('.', 1)[0]}_cut.{file.rsplit('.', 1)[1]}"
cmd = [
"ffmpeg",
"-i",
file,
"-ss",
str(start),
"-to",
str(end),
"-c",
"copy",
"-avoid_negative_ts",
"make_zero",
"-y",
output,
]
process = await asyncio.create_subprocess_exec(*cmd)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["cut_done"],
reply_to=reply.id,
)
os.remove(file)
os.remove(output)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
@loader.command(
ru_doc="[начало:конец] - Видео в кружок",
en_doc="[start:end] - Convert video to circle",
)
async def vircle(self, message: Message):
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
reply = await message.get_reply_message()
if not reply or not (reply.video or reply.gif):
return await utils.answer(message, self.strings["no_reply"])
args = utils.get_args_raw(message)
filter_args = ""
if args and ":" in args:
start_str, end_str = args.split(":", 1)
start = self.parse_time(start_str)
end = self.parse_time(end_str)
if start is not None and end is not None and start < end:
filter_args = f",trim=start={start}:end={end},setpts=PTS-STARTPTS"
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/")
output = f"{file.rsplit('.', 1)[0]}_circle.mp4"
cmd = [
"ffmpeg",
"-i",
file,
"-vf",
f"scale=720:720:force_original_aspect_ratio=increase,crop=720:720{filter_args},format=rgba,geq='if(gt(X,360),if(gt(Y,360),if(lt(sqrt((X-360)^2+(Y-360)^2),360),p(X,Y),0),0),0)'",
"-c:v",
"libx264",
"-preset",
"fast",
"-crf",
"23",
"-pix_fmt",
"yuv420p",
"-y",
output,
]
process = await asyncio.create_subprocess_exec(*cmd)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["circle_done"],
reply_to=reply.id,
video_note=True,
)
os.remove(file)
os.remove(output)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
@loader.command(
ru_doc="[начало:конец] - Извлечь аудио из видео",
en_doc="[start:end] - Extract audio from video",
)
async def vsound(self, message: Message):
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
reply = await message.get_reply_message()
if not reply or not reply.video:
return await utils.answer(message, self.strings["no_reply"])
args = utils.get_args_raw(message)
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/")
output = f"{file.rsplit('.', 1)[0]}_audio.mp3"
cmd = ["ffmpeg", "-i", file]
if args and ":" in args:
start_str, end_str = args.split(":", 1)
start = self.parse_time(start_str)
end = self.parse_time(end_str)
if start is not None and end is not None and start < end:
cmd.extend(["-ss", str(start), "-to", str(end)])
cmd.extend(["-q:a", "2", "-map", "a", "-y", output])
process = await asyncio.create_subprocess_exec(*cmd)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["audio_extracted"],
reply_to=reply.id,
)
os.remove(file)
os.remove(output)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
@loader.command(
ru_doc="<качество> - Сжать видео", en_doc="<quality> - Compress video"
)
async def compress(self, message: Message):
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
reply = await message.get_reply_message()
if not reply or not reply.video:
return await utils.answer(message, self.strings["no_reply"])
args = utils.get_args_raw(message).lower()
resolutions = {
"144p": "256x144",
"240p": "426x240",
"360p": "640x360",
"480p": "854x480",
"720p": "1280x720",
"1080p": "1920x1080",
"1440p": "2560x1440",
"2160p": "3840x2160",
}
if not args or args not in resolutions:
return await utils.answer(message, self.strings["compress_usage"])
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/")
output = f"{file.rsplit('.', 1)[0]}_compressed.mp4"
probe_cmd = [
"ffprobe",
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=bit_rate",
"-of",
"default=noprint_wrappers=1:nokey=1",
file,
]
process = await asyncio.create_subprocess_exec(
*probe_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await process.communicate()
original_bitrate = stdout.decode().strip()
scale_factor = {
"144p": 0.1,
"240p": 0.2,
"360p": 0.3,
"480p": 0.4,
"720p": 0.6,
"1080p": 0.8,
"1440p": 0.9,
"2160p": 1.0,
}
target_bitrate = "500k"
if original_bitrate and original_bitrate.isdigit():
original_br = int(original_bitrate)
target_br = int(original_br * scale_factor[args] / 1000)
target_bitrate = f"{max(200, target_br)}k"
cmd = [
"ffmpeg",
"-i",
file,
"-vf",
f"scale={resolutions[args]}:force_original_aspect_ratio=decrease",
"-c:v",
"libx264",
"-preset",
"medium",
"-b:v",
target_bitrate,
"-maxrate",
target_bitrate,
"-bufsize",
f"{int(target_bitrate[:-1]) * 2}k",
"-c:a",
"aac",
"-b:a",
"128k",
"-y",
output,
]
process = await asyncio.create_subprocess_exec(*cmd)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["compressed"].format(args),
reply_to=reply.id,
)
os.remove(file)
os.remove(output)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
@loader.command(
ru_doc="<время/размер> - Разделить медиа на части",
en_doc="<time/size> - Split media into parts",
)
async def split(self, message: Message):
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
reply = await message.get_reply_message()
if not reply or not reply.media:
return await utils.answer(message, self.strings["no_reply"])
args = utils.get_args_raw(message).lower()
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/")
file_ext = file.rsplit(".", 1)[1]
if "m" in args or "м" in args:
duration = self.parse_time(args)
if not duration:
await msg.edit(self.strings["split_time_usage"])
os.remove(file)
return
probe_cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
file,
]
process = await asyncio.create_subprocess_exec(
*probe_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await process.communicate()
total_duration = float(stdout.decode().strip())
parts = math.ceil(total_duration / duration)
for i in range(parts):
start = i * duration
end = min((i + 1) * duration, total_duration)
output = f"{file.rsplit('.', 1)[0]}_part{i + 1}.{file_ext}"
split_cmd = [
"ffmpeg",
"-i",
file,
"-ss",
str(start),
"-to",
str(end),
"-c",
"copy",
"-avoid_negative_ts",
"make_zero",
"-y",
output,
]
process = await asyncio.create_subprocess_exec(*split_cmd)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["part"].format(i + 1, parts),
reply_to=reply.id if i == 0 else None,
)
os.remove(output)
await msg.edit(self.strings["split_done"].format(parts))
elif "mb" in args or "мб" in args:
await utils.answer(
message, "Size splitting requires additional implementation"
)
else:
await msg.edit(self.strings["split_size_usage"])
os.remove(file)
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
@loader.command(
ru_doc="Объединить несколько медиафайлов", en_doc="Merge multiple media files"
)
async def merge(self, message: Message):
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
reply = await message.get_reply_message()
if not reply:
return await utils.answer(message, self.strings["merge_usage"])
messages = []
current = reply
while current and current.media:
messages.append(current)
current = await current.get_reply_message()
if len(messages) < 2:
return await utils.answer(message, self.strings["min_files"])
msg = await utils.answer(
message, self.strings["downloading"].format(len(messages))
)
try:
files = []
file_list = "temp/filelist.txt"
with open(file_list, "w") as f:
for i, msg_file in enumerate(messages):
filename = f"temp/merge_{i}.{msg_file.file.ext if msg_file.file else 'mp4'}"
await msg_file.download_media(file=filename)
files.append(filename)
f.write(f"file '{os.path.abspath(filename)}'\n")
output = "temp/merged.mp4"
cmd = [
"ffmpeg",
"-f",
"concat",
"-safe",
"0",
"-i",
file_list,
"-c",
"copy",
"-y",
output,
]
process = await asyncio.create_subprocess_exec(*cmd)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["merged"],
reply_to=reply.id,
)
for file in files:
if os.path.exists(file):
os.remove(file)
os.remove(file_list)
os.remove(output)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))
@loader.command(
ru_doc="Удалить метаданные из медиа", en_doc="Remove metadata from media"
)
async def removemetadata(self, message: Message):
if not check_ffmpeg():
return await utils.answer(message, self.strings["no_ffmpeg"])
reply = await message.get_reply_message()
if not reply or not reply.media:
return await utils.answer(message, self.strings["no_reply"])
msg = await utils.answer(message, self.strings["processing"])
try:
file = await reply.download_media(file="temp/")
file_ext = file.rsplit(".", 1)[1]
output = f"{file.rsplit('.', 1)[0]}_nometa.{file_ext}"
cmd = ["ffmpeg", "-i", file, "-map_metadata", "-1", "-y", output]
process = await asyncio.create_subprocess_exec(*cmd)
await process.communicate()
await message.client.send_file(
message.peer_id,
output,
caption=self.strings["metadata_removed"],
reply_to=reply.id,
)
os.remove(file)
os.remove(output)
await msg.delete()
except Exception as e:
await utils.answer(message, self.strings["error"].format(str(e)))

View File

@@ -32,7 +32,7 @@ from telethon.tl.custom import Message
from .. import loader, utils from .. import loader, utils
logger = logging.INFO(__name__) logger = logging.getLogger(__name__)
@loader.tds @loader.tds

View File

@@ -26,33 +26,15 @@
# scope: Api NSFWArt 0.0.1 # scope: Api NSFWArt 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import functools import asyncio
import requests import logging
from typing import List from typing import List, Optional
import aiohttp
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
async def photos(tags: str, subreddit: str, quantity: int) -> List[str]:
ans = (
await utils.run_sync(
requests.get,
f"https://api.lolicon.app/setu/v2?tag={tags}",
json={
"query": (
" query SubredditQuery( $url: String! $filter: SubredditPostFilter"
" $iterator: String ) { getSubreddit(url: $url) { children("
f" limit: {quantity} iterator: $iterator filter: $filter"
" disabledHosts: null ) { iterator items {url subredditTitle"
" isNsfw mediaSources { url } } } } } "
),
"variables": {"url": subreddit, "filter": None, "hostsDown": None},
"authorization": None,
},
)
).json()
return [ans["data"][0]["urls"]["original"]]
@loader.tds @loader.tds
@@ -61,7 +43,17 @@ class NSFWArtMod(loader.Module):
strings = { strings = {
"name": "NSFWArt", "name": "NSFWArt",
"sreddit404": "🚫 <b>Subreddit not found</b>", "fetching": "<emoji document_id=5188311512791393083>🌎</emoji> Fetching NSFW art...",
"no_results": "<emoji document_id=5854929766146118183>❌</emoji> No results found for this tag.",
"api_error": "<emoji document_id=5854929766146118183>❌</emoji> API error: {error}",
"network_error": "<emoji document_id=5854929766146118183>❌</emoji> Network error. Please try again later.",
}
strings_ru = {
"fetching": "<emoji document_id=5188311512791393083>🌎</emoji> Получение NSFW арта...",
"no_results": "<emoji document_id=5854929766146118183>❌</emoji> Ничего не найдено для этого тега.",
"api_error": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка API: {error}",
"network_error": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка сети. Попробуйте позже.",
} }
def __init__(self): def __init__(self):
@@ -69,29 +61,75 @@ class NSFWArtMod(loader.Module):
loader.ConfigValue( loader.ConfigValue(
"tags", "tags",
"drool", "drool",
lambda: "tag: masturbation, drool, completely, sleeping, yuri", lambda: "Tag for NSFW art (e.g., drool, masturbation, yuri, etc.)",
) )
) )
self._session: Optional[aiohttp.ClientSession] = None
async def _get_session(self) -> aiohttp.ClientSession:
"""Get or create aiohttp session"""
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30)
)
return self._session
async def _fetch_photos(self, tags: str, quantity: int = 15) -> Optional[List[str]]:
"""Fetch photos from API"""
session = await self._get_session()
try:
url = f"https://api.lolicon.app/setu/v2?tag={tags}"
async with session.get(url) as response:
if response.status == 200:
data = await response.json()
if data.get("data") and len(data["data"]) > 0:
return data["data"][0].get("urls", {}).get("original", [])
return None
else:
logger.error(f"API error: {response.status}")
return None
except asyncio.TimeoutError:
logger.error("API timeout")
return None
except Exception as e:
logger.error(f"Fetch error: {e}")
return None
async def _handle_error(self, message, error: Exception):
"""Handle different types of errors"""
if isinstance(error, asyncio.TimeoutError):
await utils.answer(message, self.strings("network_error"))
else:
await utils.answer(
message, self.strings("api_error").format(error=str(error))
)
@loader.command( @loader.command(
ru_doc="Отправьте симпатичный nsfw-арт", ru_doc="Отправить симпатичный NSFW-арт",
en_doc="Send cute nsfw-art", en_doc="Send cute NSFW-art",
) )
async def nsfwartcmd(self, message): async def nsfwartcmd(self, message):
"""Send NSFW art based on configured tags"""
tags = self.config["tags"] tags = self.config["tags"]
subreddit = f"/v2?tag={tags}"
ans = await utils.run_sync( if not tags:
requests.get, f"https://api.lolicon.app/setu{subreddit}" await utils.answer(message, self.strings("no_results"))
)
if ans.status_code != 200:
await utils.answer(message, self.strings("sreddit404", message))
return return
await self.inline.gallery( await utils.answer(message, self.strings("fetching"))
message=message,
next_handler=functools.partial( try:
photos, tags, subreddit=subreddit, quantity=15 photos = await self._fetch_photos(tags)
), if not photos:
caption=f"<i>{utils.ascii_face()}</i>", await utils.answer(message, self.strings("no_results"))
) return
await self.inline.gallery(
message=message,
media=photos[:15],
caption=f"<i>{utils.ascii_face()}</i>",
)
except Exception as e:
await self._handle_error(message, e)

View File

@@ -26,70 +26,142 @@
# scope: NumbersAPI 0.0.1 # scope: NumbersAPI 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import asyncio
from typing import Optional
import aiohttp import aiohttp
from datetime import datetime
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
async def get_fact_about_number(number, fact_type):
url = f"http://numbersapi.com/{number}/{fact_type}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
return await response.text()
else:
return "Извините, не удалось получить факт."
async def get_fact_about_date(month, day):
date_str = datetime.now().replace(month=month, day=day).strftime("%m/%d")
url = f"http://numbersapi.com/{date_str}/date"
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
return await response.text()
else:
return "Извините, не удалось получить факт."
@loader.tds @loader.tds
class NumbersAPI(loader.Module): class NumbersAPI(loader.Module):
"""Many interesting facts about numbers.""" """Many interesting facts about numbers."""
strings = {"name": "NumbersAPI"} strings = {
"name": "NumbersAPI",
"usage": "<emoji document_id=5854929766146118183>❌</emoji> Usage: .num <number or date> <type>\nExamples: .num 42 math, .num 01.15 date",
"error_date_format": "<emoji document_id=5854929766146118183>❌</emoji> Invalid date format. Use: month.day (e.g., 01.15)",
"error_number_format": "<emoji document_id=5854929766146118183>❌</emoji> Invalid number format.",
"error_invalid_type": "<emoji document_id=5854929766146118183>❌</emoji> Invalid fact type. Available: math, trivia, date",
"error_api": "<emoji document_id=5854929766146118183>❌</emoji> Failed to get fact. Please try again later.",
"fetching": "<emoji document_id=5188311512791393083>🌎</emoji> Fetching fact...",
}
strings_ru = {
"usage": "<emoji document_id=5854929766146118183>❌</emoji> Использование: .num <число или дата> <тип>\nПримеры: .num 42 math, .num 01.15 date",
"error_date_format": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат даты. Используйте: месяц.день (например, 01.15)",
"error_number_format": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат числа.",
"error_invalid_type": "<emoji document_id=5854929766146118183>❌</emoji> Неверный тип факта. Доступны: math, trivia, date",
"error_api": "<emoji document_id=5854929766146118183>❌</emoji> Не удалось получить факт. Попробуйте позже.",
"fetching": "<emoji document_id=5188311512791393083>🌎</emoji> Получение факта...",
}
def __init__(self):
self._session: Optional[aiohttp.ClientSession] = None
self.valid_fact_types = ["math", "trivia", "date"]
async def _get_session(self) -> aiohttp.ClientSession:
"""Get or create aiohttp session"""
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=15)
)
return self._session
def _parse_date(self, date_str: str) -> Optional[tuple[int, int]]:
"""Parse date string in format MM.DD"""
try:
parts = date_str.split(".")
if len(parts) != 2:
return None
month, day = map(int, parts)
if not (1 <= month <= 12 and 1 <= day <= 31):
return None
return month, day
except ValueError:
return None
def _parse_number(self, num_str: str) -> Optional[int]:
"""Parse number string"""
try:
return int(num_str)
except ValueError:
return None
async def _fetch_fact(self, url: str) -> str:
"""Fetch fact from Numbers API"""
session = await self._get_session()
try:
async with session.get(url) as response:
if response.status == 200:
return await response.text()
else:
logger.error(f"Numbers API error: {response.status}")
return self.strings("error_api")
except asyncio.TimeoutError:
logger.error("Numbers API timeout")
return self.strings("error_api")
except Exception as e:
logger.error(f"Numbers API error: {e}")
return self.strings("error_api")
async def _get_number_fact(self, number: int, fact_type: str) -> str:
"""Get fact about number"""
url = f"http://numbersapi.com/{number}/{fact_type}"
return await self._fetch_fact(url)
async def _get_date_fact(self, month: int, day: int) -> str:
"""Get fact about date"""
date_str = f"{month:02d}/{day:02d}"
url = f"http://numbersapi.com/{date_str}/date"
return await self._fetch_fact(url)
@loader.command( @loader.command(
ru_doc="Дает интересный факт про число или дату\nНапример: .num 10 math или .num 01.01 date", ru_doc="Дает интересный факт про число или дату\nНапример: .num 10 math или .num 01.01 date",
en_doc="Gives an interesting fact about a number or date\nexample: .num 10 math or .num 01.01 date", en_doc="Gives an interesting fact about a number or date\nexample: .num 10 math or .num 01.01 date",
) )
async def num(self, message): async def num(self, message):
args = utils.get_args_raw(message).split() """Get interesting fact about number or date"""
args = utils.get_args_raw(message)
if len(args) < 2: if not args:
await utils.answer(message, "Использование: .num <число или дата> <тип>") await utils.answer(message, self.strings("usage"))
return return
num_or_date = args[0] parts = args.split(maxsplit=1)
fact_type = args[1] if len(parts) < 2:
await utils.answer(message, self.strings("usage"))
return
if "." in num_or_date: input_value = parts[0].strip()
try: fact_type = parts[1].strip().lower()
month, day = map(int, num_or_date.split("."))
result = await get_fact_about_date(month, day) if fact_type not in self.valid_fact_types:
except ValueError: await utils.answer(message, self.strings("error_invalid_type"))
await utils.answer( return
message, "Ошибка: некорректный формат даты. Используйте: месяц.день"
) await utils.answer(message, self.strings("fetching"))
if "." in input_value:
date_parts = self._parse_date(input_value)
if date_parts is None:
await utils.answer(message, self.strings("error_date_format"))
return return
month, day = date_parts
result = await self._get_date_fact(month, day)
else: else:
try: number = self._parse_number(input_value)
number = int(num_or_date) if number is None:
result = await get_fact_about_number(number, fact_type) await utils.answer(message, self.strings("error_number_format"))
except ValueError: return
await utils.answer(message, "Ошибка: некорректный ввод числа.")
return result = await self._get_number_fact(number, fact_type)
await utils.answer(message, result) await utils.answer(message, result)

View File

@@ -17,8 +17,8 @@
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru. # For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
# Name: SafetyMod # Name: PassgenMod
# Description: generate random password # Description: Generates random password
# Author: @hikka_mods # Author: @hikka_mods
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
# meta developer: @hikka_mods # meta developer: @hikka_mods
@@ -26,11 +26,13 @@
# scope: Api SafetyMod 0.0.1 # scope: Api SafetyMod 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import random import secrets
import string import string
import logging
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
def generate_password( def generate_password(
length: int, letters: bool = True, numbers: bool = True, symbols: bool = True length: int, letters: bool = True, numbers: bool = True, symbols: bool = True
@@ -61,16 +63,16 @@ def generate_password(
raise ValueError("At least one of letters, numbers, or symbols must be True") raise ValueError("At least one of letters, numbers, or symbols must be True")
combined_characters = "".join(character_sets) combined_characters = "".join(character_sets)
password = "".join(random.choice(combined_characters) for _ in range(length)) password = "".join(secrets.choice(combined_characters) for _ in range(length))
return password return password
@loader.tds @loader.tds
class SafetyMod(loader.Module): class PassgenMod(loader.Module):
"""generate random password""" """generate random password"""
strings = { strings = {
"name": "Safety", "name": "Passgen",
"pass": "<emoji document_id=5472287483318245416>*⃣</emoji> <b>Here is your secure password:</b> <code>{}</code>", "pass": "<emoji document_id=5472287483318245416>*⃣</emoji> <b>Here is your secure password:</b> <code>{}</code>",
} }
strings_ru = { strings_ru = {

View File

@@ -26,10 +26,16 @@
# scope: Profile 0.0.1 # scope: Profile 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
from telethon.errors.rpcerrorlist import UsernameOccupiedError import logging
import re
from telethon.errors.rpcerrorlist import UsernameOccupiedError, FloodWaitError
from telethon.tl.functions.account import UpdateProfileRequest, UpdateUsernameRequest from telethon.tl.functions.account import UpdateProfileRequest, UpdateUsernameRequest
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class ProfileEditorMod(loader.Module): class ProfileEditorMod(loader.Module):
@@ -37,61 +43,138 @@ class ProfileEditorMod(loader.Module):
strings = { strings = {
"name": "Profile", "name": "Profile",
"error_format": "Incorrect format of args. Try again.", "error_format": "<emoji document_id=5854929766146118183>❌</emoji> Incorrect format. Try again.",
"done_name": "The new name was successfully unstalled!", "done_name": "<emoji document_id=5854762571659218443>✅</emoji> Name successfully updated!",
"done_bio": "The new bio was successfully unstaled!", "done_bio": "<emoji document_id=5854762571659218443>✅</emoji> Bio successfully updated!",
"done_username": "The new username was succesfully installed!", "done_username": "<emoji document_id=5854762571659218443>✅</emoji> Username successfully updated!",
"error_occupied": "The new username is already occupied!", "error_occupied": "<emoji document_id=5854929766146118183>❌</emoji> Username is already occupied!",
"error_invalid_username": "<emoji document_id=5854929766146118183>❌</emoji> Invalid username format!",
"error_flood": "<emoji document_id=5854929766146118183>❌</emoji> Too many requests. Try again later.",
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> An error occurred: {error}",
} }
strings_ru = { strings_ru = {
"error_format": "Неправильный формат аргумента. Попробуйте еще раз.", "error_format": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат. Попробуйте еще раз.",
"done_name": "Новое имя успешно настроено!", "done_name": "<emoji document_id=5854762571659218443>✅</emoji> Имя успешно обновлено!",
"done_bio": "Новое био успешно настроено!", "done_bio": "<emoji document_id=5854762571659218443>✅</emoji> Био успешно обновлено!",
"done_username": "Новое имя пользователя успешно установлено!", "done_username": "<emoji document_id=5854762571659218443>✅</emoji> Имя пользователя успешно обновлено!",
"error_occupied": "Новое имя пользователя уже занято!", "error_occupied": "<emoji document_id=5854929766146118183>❌</emoji> Имя пользователя уже занято!",
"error_invalid_username": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат имени пользователя!",
"error_flood": "<emoji document_id=5854929766146118183>❌</emoji> Слишком много запросов. Попробуйте позже.",
"error_general": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка: {error}",
} }
def __init__(self):
pass
def _validate_username(self, username: str) -> bool:
"""Validate username format"""
if not username:
return False
username = username.strip("@")
if len(username) < 5 or len(username) > 32:
return False
return re.match(r"^[a-zA-Z0-9_]+$", username) is not None
def _sanitize_name(self, name: str) -> str:
"""Sanitize name input"""
if not name:
return ""
return " ".join(name.split())[:64]
def _sanitize_bio(self, bio: str) -> str:
"""Sanitize bio input"""
if not bio:
return ""
bio = bio.strip()
limit = 70 if not self._client.hikka_me.premium else 140
if len(bio) < limit:
return bio[:limit]
else:
return bio[: limit - 3] + "..."
async def _handle_error(self, message, error: Exception):
"""Handle common errors"""
if isinstance(error, UsernameOccupiedError):
await utils.answer(message, self.strings("error_occupied"))
elif isinstance(error, FloodWaitError):
await utils.answer(message, self.strings("error_flood"))
else:
await utils.answer(
message, self.strings("error_general").format(error=str(error))
)
@loader.command( @loader.command(
ru_doc="для того, чтобы сменить свое имя/отчество", ru_doc="для того, чтобы сменить свое имя/отчество",
en_doc="for change your first/second name", en_doc="for change your first/second name",
) )
async def namecmd(self, message): async def namecmd(self, message):
args = utils.get_args_raw(message).split("/") """Change first name and last name"""
if len(args) < 1 or len(args) > 2:
return await utils.answer(message, self.strings("error_format"))
firstname = args[0]
lastname = args[1] if len(args) == 2 else ""
await message.client(
UpdateProfileRequest(first_name=firstname, last_name=lastname)
)
await utils.answer(message, self.strings("done_name"))
@loader.command(
ru_doc="чтобы изменить свою биографию",
en_doc="for change your bio",
)
async def aboutcmd(self, message):
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args: if not args:
return await utils.answer(message, self.strings("error_format")) return await utils.answer(message, self.strings("error_format"))
await message.client(UpdateProfileRequest(about=args))
await utils.answer(message, self.strings("done_bio")) if "/" in args:
parts = args.split("/", 1)
else:
parts = args.split(" ", 1)
if len(parts) < 1:
return await utils.answer(message, self.strings("error_format"))
firstname = self._sanitize_name(parts[0])
lastname = self._sanitize_name(parts[1]) if len(parts) > 1 else ""
if not firstname:
return await utils.answer(message, self.strings("error_format"))
try:
await message.client(
UpdateProfileRequest(first_name=firstname, last_name=lastname)
)
await utils.answer(message, self.strings("done_name"))
except Exception as e:
await self._handle_error(message, e)
@loader.command(
ru_doc="для изменения вашего имени пользователя. Введите значение без '@'",
en_doc="for change your username. Enter value without '@'",
)
async def aboutcmd(self, message):
"""Change profile bio"""
args = utils.get_args_raw(message)
if not args:
return await utils.answer(message, self.strings("error_format"))
bio = self._sanitize_bio(args)
try:
await message.client(UpdateProfileRequest(about=bio))
await utils.answer(message, self.strings("done_bio"))
except Exception as e:
await self._handle_error(message, e)
@loader.command( @loader.command(
ru_doc="для изменения вашего имени пользователя. Введите значение без '@'", ru_doc="для изменения вашего имени пользователя. Введите значение без '@'",
en_doc="for change your username. Enter value without '@'", en_doc="for change your username. Enter value without '@'",
) )
async def usercmd(self, message): async def usercmd(self, message):
"""- for change your username. Enter value without "@".""" """Change username"""
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
if not args: if not args:
return await utils.answer(message, self.strings("error_format")) return await utils.answer(message, self.strings("error_format"))
username = args.strip("@")
if not self._validate_username(username):
return await utils.answer(message, self.strings("error_invalid_username"))
try: try:
await message.client(UpdateUsernameRequest(args)) await message.client(UpdateUsernameRequest(username))
await utils.answer(message, self.strings("done_username")) await utils.answer(message, self.strings("done_username"))
except UsernameOccupiedError: except Exception as e:
await utils.answer(message, self.strings("error_occupied")) await self._handle_error(message, e)

View File

@@ -26,8 +26,13 @@
# scope: Api Search 0.0.1 # scope: Api Search 0.0.1
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import logging
import urllib.parse
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class Search(loader.Module): class Search(loader.Module):
@@ -39,6 +44,7 @@ class Search(loader.Module):
"isearch": "🔎<b> I searched for information for you</b> ", "isearch": "🔎<b> I searched for information for you</b> ",
"link": "🗂️ Link to your request", "link": "🗂️ Link to your request",
"close": "❌ Close", "close": "❌ Close",
"no_query": "<emoji document_id=5854929766146118183>❌</emoji> Please provide a search query.",
} }
strings_ru = { strings_ru = {
@@ -46,160 +52,136 @@ class Search(loader.Module):
"isearch": "🔎<b> Я поискал информацию за тебя</b> ", "isearch": "🔎<b> Я поискал информацию за тебя</b> ",
"link": "🗂️ Ссылка на ваш запрос", "link": "🗂️ Ссылка на ваш запрос",
"close": "❌ Закрыть", "close": "❌ Закрыть",
"no_query": "<emoji document_id=5854929766146118183>❌</emoji> Пожалуйста, укажите поисковый запрос.",
} }
def __init__(self):
self.search_engines = {
"google": "https://google.com/search?q=",
"yandex": "https://yandex.ru/?q=",
"duckduckgo": "https://duckduckgo.com/?q=",
"bing": "https://bing.com/?q=",
"you": "https://you.com/?q=",
}
def _create_search_url(self, engine: str, query: str) -> str:
"""Create search URL with proper encoding"""
if not query.strip():
return None
base_url = self.search_engines.get(engine)
if not base_url:
return None
encoded_query = urllib.parse.quote_plus(query.strip())
return f"{base_url}{encoded_query}"
def _create_inline_markup(self, search_url: str):
"""Create inline keyboard markup"""
return [
[
{
"text": self.strings("link"),
"url": search_url,
}
],
[{"text": self.strings("close"), "action": "close"}],
]
async def _search_command(self, message, engine: str, inline: bool = False):
"""Universal search command handler"""
query = utils.get_args_raw(message)
if not query.strip():
await utils.answer(message, self.strings("no_query"))
return
search_url = self._create_search_url(engine, query)
if not search_url:
await utils.answer(message, self.strings("no_query"))
return
if inline:
await self.inline.form(
text=self.strings("isearch"),
message=message,
reply_markup=self._create_inline_markup(search_url),
silent=True,
)
else:
await utils.answer(
message, self.strings("search") + f": <a href={search_url}>link</a>"
)
@loader.command( @loader.command(
ru_doc="Поискать в Google", ru_doc="Поискать в Google",
en_doc="Search on Google", en_doc="Search on Google",
) )
async def google(self, message): async def google(self, message):
await self.search_engine(message, "https://google.com/search?q=") await self._search_command(message, "google")
@loader.command( @loader.command(
ru_doc="Поискать в Yandex", ru_doc="Поискать в Yandex",
en_doc="Search on Yandex", en_doc="Search on Yandex",
) )
async def yandex(self, message): async def yandex(self, message):
await self.search_engine(message, "https://yandex.ru/?q=") await self._search_command(message, "yandex")
@loader.command( @loader.command(
ru_doc="Поискать в Duckduckgo", ru_doc="Поискать в Duckduckgo",
en_doc="Search on Duckduckgo", en_doc="Search on Duckduckgo",
) )
async def duckduckgo(self, message): async def duckduckgo(self, message):
await self.search_engine(message, "https://duckduckgo.com/?q=") await self._search_command(message, "duckduckgo")
@loader.command( @loader.command(
ru_doc="Поискать в Bing", ru_doc="Поискать в Bing",
en_doc="Search on Bing", en_doc="Search on Bing",
) )
async def bing(self, message): async def bing(self, message):
await self.search_engine(message, "https://bing.com/?q=") await self._search_command(message, "bing")
@loader.command( @loader.command(
ru_doc="Поискать в You", ru_doc="Поискать в You",
en_doc="Search on You", en_doc="Search on You",
) )
async def you(self, message): async def you(self, message):
await self.search_engine(message, "https://you.com/?q=") await self._search_command(message, "you")
async def search_engine(self, message, base_url: str) -> None:
"""Searches on a given search engine."""
query = utils.get_args_raw(message)
search_url = f"{base_url}{query}"
await utils.answer(
message, self.strings("search") + f": <a href={search_url}>link</a>"
)
@loader.command( @loader.command(
ru_doc="Поискать в Google инлайн", ru_doc="Поискать в Google инлайн",
en_doc="Search on Google inline", en_doc="Search on Google inline",
) )
async def igoogle(self, message): async def igoogle(self, message):
g = utils.get_args_raw(message) await self._search_command(message, "google", inline=True)
google = f"https://google.com/search?q={g}"
await self.inline.form(
text=self.strings("isearch"),
message=message,
reply_markup=[
[
{
"text": self.strings("link"),
"url": google,
}
],
[{"text": self.strings("close"), "action": "close"}],
],
silent=True,
)
@loader.command( @loader.command(
ru_doc="Поискать в Yandex инлайн", ru_doc="Поискать в Yandex инлайн",
en_doc="Search on Yandex inline", en_doc="Search on Yandex inline",
) )
async def iyandex(self, message): async def iyandex(self, message):
y = utils.get_args_raw(message) await self._search_command(message, "yandex", inline=True)
yandex = f"https://yandex.ru/?q={y}"
await self.inline.form(
text=self.strings("isearch"),
message=message,
reply_markup=[
[
{
"text": self.strings("link"),
"url": yandex,
}
],
[{"text": self.strings("close"), "action": "close"}],
],
silent=True,
)
@loader.command( @loader.command(
ru_doc="Поискать в Duckduckgo инлайн", ru_doc="Поискать в Duckduckgo инлайн",
en_doc="Search on Duckduckgo inline", en_doc="Search on Duckduckgo inline",
) )
async def iduckduckgo(self, message): async def iduckduckgo(self, message):
d = utils.get_args_raw(message) await self._search_command(message, "duckduckgo", inline=True)
duckduckgo = f"https://duckduckgo.com/?q={d}"
await self.inline.form(
text=self.strings("isearch"),
message=message,
reply_markup=[
[
{
"text": self.strings("link"),
"url": duckduckgo,
}
],
[{"text": self.strings("close"), "action": "close"}],
],
silent=True,
)
@loader.command( @loader.command(
ru_doc="Поискать в Bing инлайн", ru_doc="Поискать в Bing инлайн",
en_doc="Search on Bing inline", en_doc="Search on Bing inline",
) )
async def ibing(self, message): async def ibing(self, message):
b = utils.get_args_raw(message) await self._search_command(message, "bing", inline=True)
bing = f"https://bing.com/?q={b}"
await self.inline.form(
text=self.strings("isearch"),
message=message,
reply_markup=[
[
{
"text": self.strings("link"),
"url": bing,
}
],
[{"text": self.strings("close"), "action": "close"}],
],
silent=True,
)
@loader.command( @loader.command(
ru_doc="Поискать в You инлайн", ru_doc="Поискать в You инлайн",
en_doc="Search on You inline", en_doc="Search on You inline",
) )
async def iyou(self, message): async def iyou(self, message):
y = utils.get_args_raw(message) await self._search_command(message, "you", inline=True)
you = f"https://you.com/?q={y}"
await self.inline.form(
text=self.strings("isearch"),
message=message,
reply_markup=[
[
{
"text": self.strings("link"),
"url": you,
}
],
[{"text": self.strings("close"), "action": "close"}],
],
silent=True,
)
async def close(self, call): async def close(self, call):
"""Callback button""" """Callback button"""

View File

@@ -1,6 +1,6 @@
# Proprietary License Agreement # Proprietary License Agreement
# Copyright (c) 2024-29 CodWiz # 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: # 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:
@@ -14,39 +14,50 @@
# 5. By using the Software, you agree to be bound by the terms and conditions of this license. # 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. # For any inquiries or requests for permissions, please contact archquise@gmail.com
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
# Name: Shortener # Name: Shortener
# Description: shortening the link # Description: Module for using bit.ly API
# Author: @hikka_mods # Author: @hikka_mods
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
# meta developer: @hikka_mods # meta developer: @hikka_mods
# scope: Shortener # scope: Shortener
# scope: Shortener 0.0.1 # scope: Shortener 0.0.1
# requires: pyshorteners
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import pyshorteners import logging
import re
import aiohttp
from .. import loader, utils from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds @loader.tds
class Shortener(loader.Module): class Shortener(loader.Module):
"""Module for working with the api bit.ly""" """Module for using bit.ly API"""
strings = { strings = {
"name": "Shortener", "name": "Shortener",
"no_api": "<emoji document_id=5854929766146118183>❌</emoji> You have not specified an API token from the site <a href='https://app.bitly.com/settings/api/'>bit.ly</a>", "no_api": "<emoji document_id=5854929766146118183>❌</emoji> You have not specified an API token from the site <a href='https://app.bitly.com/settings/api/'>bit.ly</a>",
"statclcmd": "<emoji document_id=5787384838411522455>📊</emoji> <b>Statistics on clicks for this link:</b> {c}", "statclcmd": "<emoji document_id=5787384838411522455>📊</emoji> <b>Statistics on clicks for this link:</b> {c}",
"shortencmd": "<emoji document_id=5854762571659218443>✅</emoji> <b>Your shortened link is ready:</b> <code>{c}</code>", "shortencmd": "<emoji document_id=5854762571659218443>✅</emoji> <b>Your shortened link is ready:</b> <code>{c}</code>",
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Please provide a URL to shorten.",
"invalid_url": "<emoji document_id=5854929766146118183>❌</emoji> Invalid URL format.",
"api_error": "<emoji document_id=5854929766146118183>❌</emoji> API error: {error}",
"_cls_doc": "Module for using bit.ly API",
} }
strings_ru = { strings_ru = {
"no_api": "<emoji document_id=5854929766146118183>❌</emoji> Вы не указали api токен с сайта <a href='https://app.bitly.com/settings/api/'>bit.ly</a>", "no_api": "<emoji document_id=5854929766146118183>❌</emoji> Вы не указали api токен с сайта <a href='https://app.bitly.com/settings/api/'>bit.ly</a>",
"statclcmd": "<emoji document_id=5787384838411522455>📊</emoji> <b>Статистика о переходе по этой ссылке:</b> {c}", "statclcmd": "<emoji document_id=5787384838411522455>📊</emoji> <b>Статистика о переходе по этой ссылке:</b> {c}",
"shortencmd": "<emoji document_id=5854762571659218443>✅</emoji> <b>Ваша сокращённая ссылка готова:</b> <code>{c}</code>", "shortencmd": "<emoji document_id=5854762571659218443>✅</emoji> <b>Ваша сокращённая ссылка готова:</b> <code>{c}</code>",
"no_args": "<emoji document_id=5854929766146118183>❌</emoji> Пожалуйста, укажите URL для сокращения.",
"invalid_url": "<emoji document_id=5854929766146118183>❌</emoji> Неверный формат URL.",
"api_error": "<emoji document_id=5854929766146118183>❌</emoji> Ошибка API: {error}",
"_cls_doc": "Модуль для использования API bit.ly",
} }
def __init__(self): def __init__(self):
@@ -59,32 +70,93 @@ class Shortener(loader.Module):
) )
) )
def _validate_url(self, url: str) -> bool:
"""Validate URL format"""
if not url:
return False
url_pattern = re.compile(
r"^https?://"
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|"
r"localhost|"
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
r"(?::\d+)?"
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
return url_pattern.match(url) is not None
async def shorten_url(self, url: str, token: str) -> str:
async with aiohttp.ClientSession() as session:
async with session.post("https://api-ssl.bitly.com/v4/shorten", json={'long_url': url}, headers={"Authorization": f"Bearer {token}"}) as resp:
if resp.status == 201:
json_response = await resp.json()
return json_response['link']
else:
logger.error(f"Error occurred! Status code: {resp.status}")
return
async def get_bitlink_stats(self, bitlink: str, token: str) -> str:
async with aiohttp.ClientSession() as session:
async with session.get(f"https://api-ssl.bitly.com/v4/bitlinks/{bitlink}/clicks/summary", headers={"Authorization": f"Bearer {token}"}) as resp:
if resp.status == 200:
json_response = await resp.json()
return json_response['total_clicks']
else:
logger.error(f"Error occurred! Status code: {resp.status}")
return
@loader.command( @loader.command(
ru_doc="Сократить ссылку через bit.ly", ru_doc="Сократить ссылку через bit.ly (ссылка с https://)",
en_doc="Shorten the link via bit.ly", en_doc="Shorten the link via bit.ly (url with https://)",
) )
async def shortencmd(self, message): async def shortencmd(self, message):
"""Shorten URL using bit.ly API"""
if self.config["token"] is None: if self.config["token"] is None:
await utils.answer(message, self.strings("no_api")) await utils.answer(message, self.strings("no_api"))
return return
s = pyshorteners.Shortener(api_key=self.config["token"])
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
await utils.answer( if not args:
message, self.strings("shortencmd").format(c=s.bitly.short(args)) await utils.answer(message, self.strings("no_args"))
) return
if not self._validate_url(args):
await utils.answer(message, self.strings("invalid_url"))
return
try:
short_url = await self.shorten_url(url=args, token=self.config['token'])
await utils.answer(message, self.strings("shortencmd").format(c=short_url))
except Exception as e:
logger.error(f"Error shortening URL: {e}")
await utils.answer(message, self.strings("api_error").format(error=str(e)))
@loader.command( @loader.command(
ru_doc="Посмотреть статистику ссылки через bit.ly", ru_doc="Посмотреть статистику ссылки через bit.ly (ссылка без https:// | Доступно только на платных аккаунтах)",
en_doc="View link statistics via bit.ly", en_doc="View link statistics via bit.ly (link without https:// | Works only on paid accounts)",
) )
async def statclcmd(self, message): async def statclcmd(self, message):
"""Get click statistics for shortened URL"""
if self.config["token"] is None: if self.config["token"] is None:
await utils.answer(message, self.strings("no_api")) await utils.answer(message, self.strings("no_api"))
return return
s = pyshorteners.Shortener(api_key=self.config["token"])
args = utils.get_args_raw(message) args = utils.get_args_raw(message)
await utils.answer( if not args:
message, self.strings("statclcmd").format(c=s.bitly.total_clicks(args)) await utils.answer(message, self.strings("no_args"))
) return
try:
if not args.startswith("bit.ly/"):
await utils.answer(message, self.strings("invalid_url"))
return
else:
clicks = await self.get_bitlink_stats(bitlink=args, token=self.config['token'])
await utils.answer(message, self.strings("statclcmd").format(c=clicks))
except Exception as e:
logger.error(f"Error getting statistics: {e}")
await utils.answer(message, self.strings("api_error").format(error=str(e)))

View File

@@ -0,0 +1,480 @@
# Proprietary License Agreement
# Copyright (c) 2024-29 CodWiz
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
# For any inquiries or requests for permissions, please contact codwiz@yandex.ru.
# ---------------------------------------------------------------------------------
# Name: Silent T&R
# Description: Silent tags and reactions
# Author: @hikka_mods
# ---------------------------------------------------------------------------------
# meta developer: @hikka_mods
# scope: Silent T&R
# scope: Silent T&R 0.0.1
# ---------------------------------------------------------------------------------
from telethon.types import Message
from .. import loader, utils
@loader.tds
class SilentTRMod(loader.Module):
"""Silent tags and reactions"""
strings = {
"name": "Silent T&R",
"global_reactions_on": "✅ Global silent reactions enabled",
"global_reactions_off": "❌ Global silent reactions disabled",
"global_tags_on": "✅ Global silent tags enabled",
"global_tags_off": "❌ Global silent tags disabled",
"chat_reactions_on": "✅ Silent reactions enabled in this chat",
"chat_reactions_off": "❌ Silent reactions disabled in this chat",
"chat_tags_on": "✅ Silent tags enabled in this chat",
"chat_tags_off": "❌ Silent tags disabled in this chat",
"ignore_added": "✅ User added to global ignore list",
"ignore_removed": "❌ User removed from global ignore list",
"hignore_added": "✅ User added to ignore list in this chat",
"hignore_removed": "❌ User removed from ignore list in this chat",
"no_reply": "❌ Reply to a user or specify username",
"user_not_found": "❌ User not found",
"args_error": "❌ Use: .sreacts on/off or .sreacts",
"chat_args_error": "❌ Use: .hsreacts on/off or .hsreacts",
"status": "📊 Silent T&R status:\n\nGlobal:\n Reactions: {}\n Tags: {}\n\nThis chat:\n Reactions: {}\n Tags: {}\n\nGlobal ignore: {}\nChat ignore: {}",
}
strings_ru = {
"global_reactions_on": "✅ Глобальные тихие реакции включены",
"global_reactions_off": "❌ Глобальные тихие реакции выключены",
"global_tags_on": "✅ Глобальные тихие упоминания включены",
"global_tags_off": "❌ Глобальные тихие упоминания выключены",
"chat_reactions_on": "✅ Тихие реакции включены в этом чате",
"chat_reactions_off": "❌ Тихие реакции выключены в этом чате",
"chat_tags_on": "✅ Тихие упоминания включены в этом чате",
"chat_tags_off": "❌ Тихие упоминания выключены в этом чате",
"ignore_added": "✅ Пользователь добавлен в глобальный игнор-лист",
"ignore_removed": "❌ Пользователь удален из глобального игнор-листа",
"hignore_added": "✅ Пользователь добавлен в игнор-лист этого чата",
"hignore_removed": "❌ Пользователь удален из игнор-листа этого чата",
"no_reply": "❌ Ответьте на пользователя или укажите username",
"user_not_found": "❌ Пользователь не найден",
"args_error": "❌ Используйте: .sreacts on/off или .sreacts",
"chat_args_error": "❌ Используйте: .hsreacts on/off или .hsreacts",
"status": "📊 Статус Silent T&R:\n\nГлобально:\n Реакции: {}\n Упоминания: {}\n\nВ этом чате:\n Реакции: {}\n Упоминания: {}\n\nГлобальный игнор: {}\nИгнор в чате: {}",
}
async def client_ready(self, client, db):
self._client = client
self._db = db
self._global_settings = self._db.get(
__name__, "global_settings", {"reactions": False, "tags": False}
)
self._chat_settings = self._db.get(__name__, "chat_settings", {})
self._global_ignore = self._db.get(__name__, "global_ignore", [])
self._chat_ignore = self._db.get(__name__, "chat_ignore", {})
client.add_event_handler(self._on_message_reaction_updated)
client.add_event_handler(self._on_new_message)
async def on_unload(self):
self._client.remove_event_handler(self._on_message_reaction_updated)
self._client.remove_event_handler(self._on_new_message)
def _save_global_settings(self):
self._db.set(__name__, "global_settings", self._global_settings)
def _save_chat_settings(self):
self._db.set(__name__, "chat_settings", self._chat_settings)
def _save_global_ignore(self):
self._db.set(__name__, "global_ignore", self._global_ignore)
def _save_chat_ignore(self):
self._db.set(__name__, "chat_ignore", self._chat_ignore)
async def _on_message_reaction_updated(self, event):
"""Обработчик обновления реакций"""
try:
message = await self._client.get_messages(
event.chat_id, ids=event.message_id
)
except Exception:
return
if message.sender_id != (await self._client.get_me()).id:
return
chat_id = str(event.chat_id)
user_id = event.user_id
if user_id in self._global_ignore:
return
if chat_id in self._chat_ignore and user_id in self._chat_ignore[chat_id]:
return
chat_settings = self._chat_settings.get(
chat_id, {"reactions": None, "tags": None}
)
if chat_settings["reactions"] is None:
if not self._global_settings["reactions"]:
return
else:
if not chat_settings["reactions"]:
return
await self._client.read_messages(event.chat_id, event.message_id)
async def _on_new_message(self, event):
"""Обработчик новых сообщений для упоминаний"""
if event.out:
return
me = await self._client.get_me()
mentioned = False
if event.mentioned:
mentioned = True
else:
if event.entities:
for entity in event.entities:
if entity.type == "mentionName" and entity.user_id == me.id:
mentioned = True
break
elif entity.type == "textMention" and entity.user_id == me.id:
mentioned = True
break
if not mentioned:
return
chat_id = str(event.chat_id)
user_id = event.sender_id
if user_id in self._global_ignore:
return
if chat_id in self._chat_ignore and user_id in self._chat_ignore[chat_id]:
return
chat_settings = self._chat_settings.get(
chat_id, {"reactions": None, "tags": None}
)
if chat_settings["tags"] is None:
if not self._global_settings["tags"]:
return
else:
if not chat_settings["tags"]:
return
await event.mark_read()
@loader.command(
ru_doc="[on/off] - тихие реакции во всех чатах",
en_doc="[on/off] - silent reactions in all chats",
)
async def sreacts(self, message: Message):
"""Toggle global silent reactions"""
args = utils.get_args_raw(message).lower()
if args == "on":
self._global_settings["reactions"] = True
self._save_global_settings()
await utils.answer(message, self.strings["global_reactions_on"])
elif args == "off":
self._global_settings["reactions"] = False
self._save_global_settings()
await utils.answer(message, self.strings["global_reactions_off"])
elif args == "":
status = "on" if self._global_settings["reactions"] else "off"
await utils.answer(message, f"Global silent reactions: {status}")
else:
await utils.answer(message, self.strings["args_error"])
@loader.command(
ru_doc="[on/off] - тихие упоминания во всех чатах",
en_doc="[on/off] - silent tags in all chats",
)
async def stags(self, message: Message):
"""Toggle global silent tags"""
args = utils.get_args_raw(message).lower()
if args == "on":
self._global_settings["tags"] = True
self._save_global_settings()
await utils.answer(message, self.strings["global_tags_on"])
elif args == "off":
self._global_settings["tags"] = False
self._save_global_settings()
await utils.answer(message, self.strings["global_tags_off"])
elif args == "":
status = "on" if self._global_settings["tags"] else "off"
await utils.answer(message, f"Global silent tags: {status}")
else:
await utils.answer(message, self.strings["args_error"])
@loader.command(
ru_doc="[on/off] - тихие реакции и упоминания во всех чатах",
en_doc="[on/off] - silent reactions and tags in all chats",
)
async def sall(self, message: Message):
"""Toggle global silent reactions and tags"""
args = utils.get_args_raw(message).lower()
if args == "on":
self._global_settings["reactions"] = True
self._global_settings["tags"] = True
self._save_global_settings()
await utils.answer(message, "✅ Global silent reactions and tags enabled")
elif args == "off":
self._global_settings["reactions"] = False
self._global_settings["tags"] = False
self._save_global_settings()
await utils.answer(message, "❌ Global silent reactions and tags disabled")
elif args == "":
status_r = "on" if self._global_settings["reactions"] else "off"
status_t = "on" if self._global_settings["tags"] else "off"
await utils.answer(
message, f"Global silent reactions: {status_r}, tags: {status_t}"
)
else:
await utils.answer(message, self.strings["args_error"])
@loader.command(
ru_doc="[on/off] - тихие реакции в этом чате",
en_doc="[on/off] - silent reactions in this chat",
)
async def hsreacts(self, message: Message):
"""Toggle silent reactions in this chat"""
args = utils.get_args_raw(message).lower()
chat_id = str(message.chat_id)
# Получаем настройки чата или создаем новые
chat_settings = self._chat_settings.get(
chat_id, {"reactions": None, "tags": None}
)
if args == "on":
chat_settings["reactions"] = True
self._chat_settings[chat_id] = chat_settings
self._save_chat_settings()
await utils.answer(message, self.strings["chat_reactions_on"])
elif args == "off":
chat_settings["reactions"] = False
self._chat_settings[chat_id] = chat_settings
self._save_chat_settings()
await utils.answer(message, self.strings["chat_reactions_off"])
elif args == "":
status = chat_settings["reactions"]
if status is None:
status = (
"global ("
+ ("on" if self._global_settings["reactions"] else "off")
+ ")"
)
else:
status = "on" if status else "off"
await utils.answer(message, f"Silent reactions in this chat: {status}")
else:
await utils.answer(message, self.strings["chat_args_error"])
@loader.command(
ru_doc="[on/off] - тихие упоминания в этом чате",
en_doc="[on/off] - silent tags in this chat",
)
async def hstags(self, message: Message):
"""Toggle silent tags in this chat"""
args = utils.get_args_raw(message).lower()
chat_id = str(message.chat_id)
chat_settings = self._chat_settings.get(
chat_id, {"reactions": None, "tags": None}
)
if args == "on":
chat_settings["tags"] = True
self._chat_settings[chat_id] = chat_settings
self._save_chat_settings()
await utils.answer(message, self.strings["chat_tags_on"])
elif args == "off":
chat_settings["tags"] = False
self._chat_settings[chat_id] = chat_settings
self._save_chat_settings()
await utils.answer(message, self.strings["chat_tags_off"])
elif args == "":
status = chat_settings["tags"]
if status is None:
status = (
"global ("
+ ("on" if self._global_settings["tags"] else "off")
+ ")"
)
else:
status = "on" if status else "off"
await utils.answer(message, f"Silent tags in this chat: {status}")
else:
await utils.answer(message, self.strings["chat_args_error"])
@loader.command(
ru_doc="[on/off] - тихие реакции и упоминания в этом чате",
en_doc="[on/off] - silent reactions and tags in this chat",
)
async def hsall(self, message: Message):
"""Toggle silent reactions and tags in this chat"""
args = utils.get_args_raw(message).lower()
chat_id = str(message.chat_id)
chat_settings = self._chat_settings.get(
chat_id, {"reactions": None, "tags": None}
)
if args == "on":
chat_settings["reactions"] = True
chat_settings["tags"] = True
self._chat_settings[chat_id] = chat_settings
self._save_chat_settings()
await utils.answer(
message, "✅ Silent reactions and tags enabled in this chat"
)
elif args == "off":
chat_settings["reactions"] = False
chat_settings["tags"] = False
self._chat_settings[chat_id] = chat_settings
self._save_chat_settings()
await utils.answer(
message, "❌ Silent reactions and tags disabled in this chat"
)
elif args == "":
status_r = chat_settings["reactions"]
if status_r is None:
status_r = (
"global ("
+ ("on" if self._global_settings["reactions"] else "off")
+ ")"
)
else:
status_r = "on" if status_r else "off"
status_t = chat_settings["tags"]
if status_t is None:
status_t = (
"global ("
+ ("on" if self._global_settings["tags"] else "off")
+ ")"
)
else:
status_t = "on" if status_t else "off"
await utils.answer(
message, f"Silent reactions: {status_r}, tags: {status_t} in this chat"
)
else:
await utils.answer(message, self.strings["chat_args_error"])
async def _get_user_id(self, message: Message):
"""Получить ID пользователя из аргументов или ответа"""
reply = await message.get_reply_message()
args = utils.get_args_raw(message)
if reply:
return reply.sender_id
elif args:
try:
user = await self._client.get_entity(args)
return user.id
except Exception:
return None
else:
return None
@loader.command(
ru_doc="[ответ/username] - игнорировать пользователя глобально",
en_doc="[reply/username] - ignore user globally",
)
async def ignore(self, message: Message):
"""Toggle ignore user globally"""
user_id = await self._get_user_id(message)
if not user_id:
await utils.answer(message, self.strings["no_reply"])
return
if user_id in self._global_ignore:
self._global_ignore.remove(user_id)
await utils.answer(message, self.strings["ignore_removed"])
else:
self._global_ignore.append(user_id)
await utils.answer(message, self.strings["ignore_added"])
self._save_global_ignore()
@loader.command(
ru_doc="[ответ/username] - игнорировать пользователя в этом чате",
en_doc="[reply/username] - ignore user in this chat",
)
async def hignore(self, message: Message):
"""Toggle ignore user in this chat"""
user_id = await self._get_user_id(message)
if not user_id:
await utils.answer(message, self.strings["no_reply"])
return
chat_id = str(message.chat_id)
if chat_id not in self._chat_ignore:
self._chat_ignore[chat_id] = []
if user_id in self._chat_ignore[chat_id]:
self._chat_ignore[chat_id].remove(user_id)
await utils.answer(message, self.strings["hignore_removed"])
else:
self._chat_ignore[chat_id].append(user_id)
await utils.answer(message, self.strings["hignore_added"])
self._save_chat_ignore()
@loader.command(
ru_doc="Показать статус Silent T&R", en_doc="Show Silent T&R status"
)
async def strstatus(self, message: Message):
"""Show status"""
global_reactions = "on" if self._global_settings["reactions"] else "off"
global_tags = "on" if self._global_settings["tags"] else "off"
chat_id = str(message.chat_id)
chat_settings = self._chat_settings.get(
chat_id, {"reactions": None, "tags": None}
)
chat_reactions = chat_settings["reactions"]
if chat_reactions is None:
chat_reactions = "global"
else:
chat_reactions = "on" if chat_reactions else "off"
chat_tags = chat_settings["tags"]
if chat_tags is None:
chat_tags = "global"
else:
chat_tags = "on" if chat_tags else "off"
global_ignore_count = len(self._global_ignore)
chat_ignore_count = len(self._chat_ignore.get(chat_id, []))
await utils.answer(
message,
self.strings["status"].format(
global_reactions,
global_tags,
chat_reactions,
chat_tags,
global_ignore_count,
chat_ignore_count,
),
)

View File

@@ -0,0 +1,86 @@
# Proprietary License Agreement
# Copyright (c) 2026-2029 Archquise
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
# For any inquiries or requests for permissions, please contact archquise@gmail.com
# ---------------------------------------------------------------------------------
# Name: TimeZone
# Description: Prints current time in selected timezone (UTC+n and tzdata formats supported)
# Author: @hikka_mods
# ---------------------------------------------------------------------------------
# meta developer: @hikka_mods
# requires: tzdata
# ---------------------------------------------------------------------------------
import logging
import tzdata
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds
class TimeZoneMod(loader.Module):
"""Prints current time in selected timezone (UTC+n and tzdata formats supported)"""
strings = {
"name": "TimeZone",
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or they are invalid",
"_cls_doc": "Prints current time in selected timezone (UTC+n and tzdata formats supported)",
"time_utc": "<emoji document_id=5276412364458059956>🕓</emoji> Current time by UTC+{}: {}",
"time_tzdata": "<emoji document_id=5276412364458059956>🕓</emoji> Current time in {}: {}"
}
strings_ru = {
"_cls_doc": "Выводит текущее время в выбранном часовом поясе (поддерживаются форматы UTC+n и tzdata)",
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или они неверны",
"tzdata_error": "<emoji document_id=5854929766146118183>❌</emoji> Произошла ошибка при получении времени по tzdata: {}\n\nУбедитесь, что часовой пояс указан верно",
"time_utc": "<emoji document_id=5276412364458059956>🕓</emoji> Текущее время по UTC+{}: {}",
"time_tzdata": "<emoji document_id=5276412364458059956>🕓</emoji> Текущее время в {}: {}"
}
@loader.command(
ru_doc="Выводит время по UTC+n | Использование: .utc 4",
en_doc="Prints UTC+n time | Usage: .utc 4",
)
async def utccmd(self, message):
args = utils.get_args(message)
if not args or not args[0].isdigit() or len(args) > 1:
await utils.answer(message, self.strings['invalid_args'])
return
offset = timedelta(hours=int(args[0]))
tz = timezone(offset)
time = datetime.now(tz)
await utils.answer(message, self.strings['time_utc'].format(args[0], time.strftime('%H:%M:%S')))
@loader.command(
ru_doc="Выводит время по часовому поясу tzdata | Использование: .tzdata Europe/Moscow",
en_doc="Prints time by tzdata timezone | Usage: .tzdata Europe/Moscow",
)
async def tzdatacmd(self, message):
args = utils.get_args(message)
if args[0].isdigit() or not args or len(args) > 1:
await utils.answer(message, self.strings['invalid_args'])
return
try:
time = datetime.now(ZoneInfo(args[0]))
except Exception as e:
await utils.answer(message, self.strings['tzdata_error'].format(e))
logger.error(self.strings['tzdata_error'].format(e))
return
await utils.answer(message, self.strings['time_tzdata'].format(args[0], time.strftime('%H:%M:%S')))

243
archquise/H.Modules/ytdl.py Normal file
View File

@@ -0,0 +1,243 @@
# Proprietary License Agreement
# Copyright (c) 2026-2029
# Permission is hereby granted to any person obtaining a copy of this software and associated documentation files (the "Software"), to use the Software for personal and non-commercial purposes, subject to the following conditions:
# 1. The Software may not be modified, altered, or otherwise changed in any way without the explicit written permission of the author.
# 2. Redistribution of the Software, in original or modified form, is strictly prohibited without the explicit written permission of the author.
# 3. The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author or copyright holder be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software.
# 4. Any use of the Software must include the above copyright notice and this permission notice in all copies or substantial portions of the Software.
# 5. By using the Software, you agree to be bound by the terms and conditions of this license.
# For any inquiries or requests for permissions, please contact archquise@gmail.com
# ---------------------------------------------------------------------------------
# Name: YTDL
# Description: Downloads and sends audio/video from YouTube
# Author: @hikka_mods
# ---------------------------------------------------------------------------------
# meta developer: @hikka_mods
# requires: yt_dlp ffmpeg
# ---------------------------------------------------------------------------------
import shutil
import platform
import aiohttp
import aiofiles
import zipfile
import os
import re
import logging
from pathlib import Path
from yt_dlp import YoutubeDL
from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds
class YTDLMod(loader.Module):
"""Downloads and sends audio/video from YouTube"""
strings = {
"name": "YTDL",
"_cls_doc": "Downloads and sends audio/video from YouTube",
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> There is no arguments or they are invalid",
"downloading_video": "<emoji document_id=5215484787325676090>🕐</emoji> Video is downloading...",
"downloaded_video": "<emoji document_id=5854762571659218443>✅</emoji> Video is downloaded!",
"downloading_audio": "<emoji document_id=5215484787325676090>🕐</emoji> Audio is downloading...",
"downloaded_audio": "<emoji document_id=5854762571659218443>✅</emoji> Audio is downloaded!",
}
strings_ru = {
"_cls_doc": "Скачивает и отправляет аудио/видео с Ютуба",
"invalid_args": "<emoji document_id=5854929766146118183>❌</emoji> Нет аргументов или они неверны",
"downloading_video": "<emoji document_id=5215484787325676090>🕐</emoji> Видео скачивается...",
"downloaded_video": "<emoji document_id=5854762571659218443>✅</emoji> Видео загружено!",
"downloading_audio": "<emoji document_id=5215484787325676090>🕐</emoji> Аудио скачивается...",
"downloaded_audio": "<emoji document_id=5854762571659218443>✅</emoji> Аудио загружено!",
}
def _validate_url(self, url: str) -> bool:
"""Validate URL format"""
if not url:
return False
url_pattern = re.compile(
r'^(?:https?://)?(?:www\.|m\.)?(?:youtube\.com|youtu\.be|music\.youtube\.com)/(?:watch\?v=|playlist\?list=|channel/|@|live/|shorts/)?[\w-]+',
re.IGNORECASE
)
return url_pattern.match(url) is not None
async def get_target(self):
system = platform.system()
machine = platform.machine().lower()
if system == "Windows":
return "Windows"
if system == "Darwin":
return "aarch64-apple-darwin" if machine == "arm64" else "x86_64-apple-darwin"
if system == "Linux":
return "aarch64-unknown-linux-gnu" if machine in ("aarch64", "arm64") else "x86_64-unknown-linux-gnu"
return "x86_64-unknown-linux-gnu"
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"youtube_cookie",
None,
"Cookie вашего Ютуб-аккаунта (повышает стабильность и помогает скачивать видео с жесткими возрастными ограничениями) | Cookie of your YouTube-account (increases stability and helps downloading video with strict age rating restricrions)",
validator=loader.validators.Hidden(),
),
)
async def client_ready(self, client, db):
deno_path = Path("deno")
deno_which = shutil.which("deno")
if not deno_which and not deno_path.is_file():
logger.warning("Deno is not installed, attempting installation...")
target = await self.get_target()
if target == "Windows":
logger.critical("Windows platform is unsupported by this module. All future commands will fail. Please, unload the module.")
return
async with aiohttp.ClientSession() as session:
download_link = f"https://github.com/denoland/deno/releases/latest/download/deno-{target}.zip"
async with session.get(download_link) as resp:
if resp.status == 200:
async with aiofiles.open("deno.zip", mode='wb') as f:
async for chunk in resp.content.iter_chunked(8192):
await f.write(chunk)
else:
logger.critical(f"Failed to download Deno: HTTP {resp.status}")
self.set("deno_source", "install_failed")
return
if Path("deno.zip").is_file():
with zipfile.ZipFile("deno.zip","r") as zip_ref:
zip_ref.extractall()
os.remove("deno.zip")
os.chmod(deno_path, 0o755)
self.set("deno_source", "file")
elif deno_which:
self.set("deno_source", deno_which)
@loader.command(
en_doc="Download video",
ru_doc="Скачать видео"
)
async def ytdlvcmd(self, message):
args = utils.get_args(message)
if not args or not self._validate_url(args[0]) or len(args) > 1:
await utils.answer(message, self.strings['invalid_args'])
return
source = self.get('deno_source')
deno_path = Path('deno').resolve() if source == 'file' else source if source != 'install_failed' else None
if not deno_path:
logger.critical("Deno wasn't installed in auto-mode. Please, install it manually or resolve the issue and reboot userbot.")
return
await utils.answer(message, self.strings['downloading_video'])
filename_prefix = f"video_{message.id}"
ydl_opts = {
'quiet': True,
'outtmpl': f'{filename_prefix}.%(ext)s',
'js_runtimes': {'deno': {'path': str(deno_path)}},
'extractor_args': {
'youtube': {
'player_client': ['mweb', 'android'],
}
},
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
'postprocessors': [{
'key': 'FFmpegVideoConvertor',
'preferedformat': 'mp4',
}],
'postprocessor_args': {
'video_convertor': [
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-preset', 'veryfast',
'-crf', '23',
'-c:a', 'aac'
],
'merger': [
'-movflags', 'faststart'
]
},
}
if self.get('youtube_cookie'):
ydl_opts['cookiefile'] = self.get('youtube_cookie')
with YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(args[0], download=True)
filename = ydl.prepare_filename(info)
await self.client.send_file(message.chat_id, filename, caption=self.strings['downloaded_video'])
await message.delete()
@loader.command(
en_doc="Download audio",
ru_doc="Скачать аудио"
)
async def ytdlacmd(self, message):
args = utils.get_args(message)
if not args or not self._validate_url(args[0]) or len(args) > 1:
await utils.answer(message, self.strings['invalid_args'])
return
source = self.get('deno_source')
deno_path = Path('deno').resolve() if source == 'file' else source if source != 'install_failed' else None
if not deno_path:
logger.critical("Deno wasn't installed in auto-mode. Please, install it manually or resolve the issue and reboot userbot.")
return
await utils.answer(message, self.strings['downloading_audio'])
filename_prefix = f"audio_{message.id}"
ydl_opts = {
'quiet': True,
'outtmpl': f'{filename_prefix}.%(ext)s',
'js_runtimes': {'deno': {'path': str(deno_path)}},
'postprocessors': [
{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '0',
},
{
'key': 'FFmpegMetadata',
'add_metadata': True,
},
{
'key': 'EmbedThumbnail',
},
],
'writethumbnail': True,
}
if self.get('youtube_cookie'):
ydl_opts['cookiefile'] = self.get('youtube_cookie')
with YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(args[0], download=True)
filename = ydl.prepare_filename(info).split(".")[0] + ".mp3"
await self.client.send_file(message.chat_id, filename, caption=self.strings['downloaded_audio'])
await message.delete()

View File

@@ -0,0 +1,250 @@
# ______ ___ ___ _ _
# ____ | ___ \ | \/ | | | | |
# / __ \| |_/ / _| . . | ___ __| |_ _| | ___
# / / _` | __/ | | | |\/| |/ _ \ / _` | | | | |/ _ \
# | | (_| | | | |_| | | | | (_) | (_| | |_| | | __/
# \ \__,_\_| \__, \_| |_/\___/ \__,_|\__,_|_|\___|
# \____/ __/ |
# |___/
# На модуль распространяется лицензия "GNU General Public License v3.0"
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule
# requires: aiohttp
import asyncio
import aiohttp
import logging
import re
from telethon import functions
from .. import loader, utils
@loader.tds
class AiUsernameGen(loader.Module):
"""AI-powered username generation and automatic creation of public channels with available usernames. (Before you begin, set up the config: .config AiUsernameGen)"""
strings = {
"name": "AiUsernameGen",
"no_prompt": "🚫 <b>Specify a query to generate a username</b>",
"checking": "🤖 <b>Generating and checking username availability...</b>",
"created_many": "✅ <b>Public channels have been created:</b>\n{}",
"available_many": "✅ <b>Available usernames found (no auto-creation):</b>\n{}",
"no_free": "😔 <b>No available usernames found. Try a different search!</b>",
"error_ai": "❌ <b>Error requesting AI. Check your configuration.</b>",
"config_api_key": "Key API for AI (https://openrouter.ai/settings/keys)",
"config_model": "Model AI for generation",
"config_channel_title_prefix": "Prefix for channel title (use {username} to insert username)",
"config_channel_about": "Channel description",
"config_autocreate_channels": "Automatically create channels for available usernames (True/False)",
}
strings_ru = {
"_cls_doc": "Генерация username с помощью AI и автоматическое создание публичных каналов с доступными юзернеймами. (Перед началом настройте модуль: .config AiUsernameGen)",
"no_prompt": "🚫 <b>Укажите запрос для генерации username</b>",
"checking": "🤖 <b>Генерация и проверка доступности username...</b>",
"created_many": "✅ <b>Созданы публичные каналы:</b>\n{}",
"available_many": "✅ <b>Доступные username найдены (автосоздание выключено):</b>\n{}",
"no_free": "😔 <b>Свободных username не найдено. Попробуйте другой запрос!</b>",
"error_ai": "❌ <b>Ошибка при запросе к AI. Проверьте конфигурацию.</b>",
"config_api_key": "Ключ API для AI (https://openrouter.ai/settings/keys)",
"config_model": "Модель AI для генерации",
"config_channel_title_prefix": "Префикс для заголовка канала (используйте {username} для вставки username)",
"config_channel_about": "Описание канала",
"config_autocreate_channels": "Автоматически создавать каналы с доступными username (True/False)",
}
USERNAME_REGEX = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$')
SYSTEM_PROMPT = (
"Ты без дополнительных слов будешь придумывать ровно 10 уникальных username. "
"Не меньше, не больше! После каждого username делай перенос строки. "
"Не используй символ @. Условие: username должен состоять из 5 или более символов "
"и соответствовать паттерну [a-zA-Z][\\w\\d]{3,30}[a-zA-Z\\d]. "
"Сделай их креативными и релевантными запросу."
)
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"AI_KEY",
"your_token_here",
lambda: self.strings["config_api_key"],
validator=loader.validators.String()
),
loader.ConfigValue(
"MODEL",
"deepseek/deepseek-r1-0528:free",
lambda: self.strings["config_model"],
validator=loader.validators.String()
),
loader.ConfigValue(
"CHANNEL_TITLE_PREFIX",
"Юзернейм: {username}",
lambda: self.strings["config_channel_title_prefix"],
validator=loader.validators.String()
),
loader.ConfigValue(
"CHANNEL_ABOUT",
"@pymodule",
lambda: self.strings["config_channel_about"],
validator=loader.validators.String()
),
loader.ConfigValue(
"AUTOCREATE_CHANNELS",
True,
lambda: self.strings["config_autocreate_channels"],
validator=loader.validators.Boolean()
)
)
async def client_ready(self, client, db):
self.client = client
self.db = db
self.logger = logging.getLogger(__name__)
self.api_url = "https://openrouter.ai/api/v1/chat/completions"
async def _query_ai(self, prompt: str) -> str | None:
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
headers = {
"Authorization": f"Bearer {self.config['AI_KEY']}",
"Content-Type": "application/json"
}
payload = {
"model": self.config["MODEL"],
"messages": [
{"role": "system", "content": self.SYSTEM_PROMPT},
{"role": "user", "content": prompt}
],
"temperature": 0.7,
"max_tokens": 150
}
async with session.post(self.api_url, json=payload, headers=headers) as resp:
if resp.status != 200:
self.logger.error(f"AI response status: {resp.status} - {await resp.text()}")
return None
res = await resp.json()
if "choices" not in res or not res["choices"]:
self.logger.error("Invalid AI response structure")
return None
return res["choices"][0]["message"]["content"].strip()
except Exception as e:
self.logger.error(f"AI request error: {e}")
return None
async def _is_username_available(self, username: str) -> bool:
try:
return await self.client(functions.account.CheckUsernameRequest(username=username))
except Exception as e:
self.logger.error(f"Error checking username '{username}': {e}")
return False
async def get_available(self, usernames: list[str]) -> list[str]:
semaphore = asyncio.Semaphore(10)
async def check_one(u: str):
async with semaphore:
return u if await self._is_username_available(u) else None
tasks = [check_one(u) for u in usernames]
results = await asyncio.gather(*tasks)
return [r for r in results if r is not None]
async def create_channels(self, usernames: list[str]) -> list[str]:
semaphore = asyncio.Semaphore(3)
async def create_one(u: str):
async with semaphore:
return await self._create_channel_with_username(u)
tasks = [create_one(u) for u in usernames]
results = await asyncio.gather(*tasks)
return [r for r in results if r is not None]
async def _create_channel_with_username(self, username: str) -> str | None:
title = self.config["CHANNEL_TITLE_PREFIX"].format(username=username)
about = self.config["CHANNEL_ABOUT"]
try:
if not await self._is_username_available(username):
return None
result = await self.client(functions.channels.CreateChannelRequest(
title=title,
about=about,
broadcast=True
))
channel = result.chats[0]
await self.client(functions.channels.UpdateUsernameRequest(
channel=channel,
username=username
))
self.logger.info(f"Successfully created channel with username: {username}")
return username
except Exception as e:
self.logger.error(f"Error creating channel for '{username}': {e}")
try:
if 'channel' in locals():
await self.client(functions.channels.DeleteChannelRequest(channel=channel))
except:
pass
return None
def _filter_and_validate_usernames(self, ai_text: str) -> list[str]:
lines = [u.strip() for u in ai_text.splitlines() if u.strip()]
valid = []
for u in lines[:10]:
if len(u) >= 5 and self.USERNAME_REGEX.match(u):
valid.append(u)
return valid
@loader.command(ru_doc="— <запрос> Генерирует username по запросу и (опционально) создаёт каналы")
async def genusercmd(self, message):
"""— <request> Generates usernames and optionally creates channels"""
user_query = utils.get_args_raw(message)
if not user_query:
return await utils.answer(message, self.strings["no_prompt"])
msg = await utils.answer(message, self.strings["checking"])
ai_text = await self._query_ai(user_query)
if not ai_text:
return await msg.edit(self.strings["error_ai"])
usernames = self._filter_and_validate_usernames(ai_text)
available = await self.get_available(usernames)
autocreate = self.config["AUTOCREATE_CHANNELS"]
created = []
if autocreate and available:
created = await self.create_channels(available)
if (autocreate and not created) or (not autocreate and not available):
retry_prompt = f"{user_query}. Придумай ещё 10 других уникальных username, строго по тем же правилам."
ai_text_retry = await self._query_ai(retry_prompt)
if ai_text_retry:
usernames_retry = self._filter_and_validate_usernames(ai_text_retry)
available_retry = await self.get_available(usernames_retry)
if autocreate:
created = await self.create_channels(available_retry)
else:
available = available_retry or available
if autocreate:
if created:
channels_list = "\n".join(f"• <code>t.me/{u}</code>" for u in created)
await msg.edit(self.strings["created_many"].format(channels_list))
else:
await msg.edit(self.strings["no_free"])
else:
if available:
avail_list = "\n".join(f"• <code>t.me/{u}</code>" for u in available)
await msg.edit(self.strings["available_many"].format(avail_list))
else:
await msg.edit(self.strings["no_free"])

View File

@@ -21,4 +21,5 @@ multiunloadmodule
tagall2.0 tagall2.0
point point
deviceinfo deviceinfo
mpi mpi
aigenuser

View File

@@ -1,5 +1,5 @@
# -- version -- # -- version --
__version__ = (1, 2, 0) __version__ = (1, 2, 1)
# -- version -- # -- version --
@@ -21,6 +21,7 @@ from herokutl.tl.functions.payments import GetPaymentFormRequest, SendStarsFormR
from herokutl.tl.types import InputInvoiceStarGift, TextWithEntities from herokutl.tl.types import InputInvoiceStarGift, TextWithEntities
from herokutl.errors.rpcerrorlist import BadRequestError from herokutl.errors.rpcerrorlist import BadRequestError
import logging import logging
import herokutl
@loader.tds @loader.tds
class SenderGifts(loader.Module): class SenderGifts(loader.Module):
@@ -78,7 +79,7 @@ class SenderGifts(loader.Module):
@loader.command() @loader.command()
async def sendgift(self, message): async def sendgift(self, message):
"""- <username> <text*> - отправить подарок пользователю (* - необязательный параметр.) Поддерживается реплай режим.""" """- <username> <text*> - отправить подарок пользователю (* - необязательный параметр.) Поддерживается реплай режим."""
args = utils.get_args_raw(message) args = utils.get_args_html(message)
reply = await message.get_reply_message() reply = await message.get_reply_message()
if reply: if reply:
user = reply.sender user = reply.sender
@@ -221,12 +222,17 @@ class SenderGifts(loader.Module):
self.strings["sending_gift"], self.strings["sending_gift"],
reply_markup=None reply_markup=None
) )
parse_mode = herokutl.utils.sanitize_parse_mode(
self.client.parse_mode,
)
text, entities = parse_mode.parse(text)
user = await self.client.get_input_entity(user_id) user = await self.client.get_input_entity(user_id)
inv = InputInvoiceStarGift( inv = InputInvoiceStarGift(
user, user,
gift_id, gift_id,
message=TextWithEntities(text, []) if text else TextWithEntities("", []) message=TextWithEntities(text, entities) if text else TextWithEntities("", [])
) )
form = await self.client(GetPaymentFormRequest(inv)) form = await self.client(GetPaymentFormRequest(inv))
result = await self.client(SendStarsFormRequest(form.form_id, inv)) result = await self.client(SendStarsFormRequest(form.form_id, inv))

File diff suppressed because it is too large Load Diff