First commit

This commit is contained in:
2026-01-23 04:45:55 +07:00
commit 0b251c5967
118 changed files with 9580 additions and 0 deletions

9
bot/utils/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
from .argument import *
from .clear_status import *
from .format_time import *
from .interesting_facts import *
from .pagination import *
from .type_message import *
from .usernames import *
from .auto_delete import *
from .hidden_username import *

61
bot/utils/argument.py Normal file
View File

@@ -0,0 +1,61 @@
from __future__ import annotations
from typing import Optional
from configs import BotSettings
# Настройка экспорта в модули
__all__ = ("is_command", "find_argument")
def is_command(message: Optional[str]) -> bool:
"""
Проверяет, является ли сообщение командой.
Сообщение считается командой, если:
1. Оно не пустое;
2. Начинается с префикса команды, указанного в настройках.
Args:
message (Optional[str]): Входное сообщение.
Returns:
bool: True, если сообщение является командой, иначе False.
Пример:
>>> is_command("/start")
True
>>> is_command("hello")
False
"""
if not message:
return False
return message.strip().startswith(BotSettings.PREFIX)
def find_argument(message: Optional[str]) -> Optional[str]:
"""
Извлекает аргумент команды из сообщения.
Аргументом считается текст после первой команды и пробела.
Если аргумента нет — возвращает None.
Args:
message (Optional[str]): Входное сообщение.
Returns:
Optional[str]: Аргумент команды или None, если его нет.
Пример:
>>> find_argument("/start referrer")
'referrer'
>>> find_argument("/start")
None
>>> find_argument("hello")
None
"""
if not is_command(message):
return None
parts = message.strip().split(maxsplit=1)
return parts[1] if len(parts) > 1 else None

19
bot/utils/auto_delete.py Normal file
View File

@@ -0,0 +1,19 @@
from asyncio import sleep
from aiogram.exceptions import TelegramBadRequest
from bot import bot
from middleware import logger
__all__ = ("auto_delete_message",)
async def auto_delete_message(chat_id: int, message_id: int, delay: int = 604800) -> None:
"""
Автоматически удаляет сообщение через указанный промежуток времени.
По умолчанию — 7 суток (604800 секунд).
"""
await sleep(delay=delay)
try:
await bot.delete_message(chat_id=chat_id, message_id=message_id)
logger.info("Закрепленное сообщение удалено")
except TelegramBadRequest as e:
logger.error(f"[ALL] Ошибка при автоудалении: {e}")

17
bot/utils/clear_status.py Normal file
View File

@@ -0,0 +1,17 @@
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
# Настройка экспорта в модули
__all__ = ("status_clear", "inline_clear")
async def inline_clear(message: Message | CallbackQuery) -> None:
"""Очищает все статусы инлайн сообщений"""
if isinstance(message, CallbackQuery):
await message.answer()
async def status_clear(message: Message | CallbackQuery, state: FSMContext) -> None:
"""Очищает все статусы, и отвечает на сообщения"""
await state.clear()
await inline_clear(message=message)

23
bot/utils/format_time.py Normal file
View File

@@ -0,0 +1,23 @@
# Настройка экспорта в модули
__all__ = ("format_retry_time",)
def format_retry_time(retry_after: int) -> str:
"""
Форматирование времени повторной попытки в читаемом виде.
Args:
retry_after (int): Время в секундах до следующей попытки.
Returns:
str: Строка в формате X часов, Y минут, Z секунд.
"""
hours, remainder = divmod(retry_after, 3600)
minutes, seconds = divmod(remainder, 60)
if hours > 0:
return f"{hours} часов, {minutes} минут, {seconds} секунд"
elif minutes > 0:
return f"{minutes} минут, {seconds} секунд"
else:
return f"{seconds} секунд"

View File

@@ -0,0 +1,21 @@
from aiogram.types import Message
from aiogram.utils.markdown import hide_link
from bot import bot
__all__ = ("hidden_admins_message",)
async def hidden_admins_message(message: Message,
text: str = "") -> str:
"""
Формирует текст с упоминанием всех админов через скрытые ссылки.
"""
admins = await bot.get_chat_administrators(message.chat.id)
hidden_links: str = "".join(
hide_link(f"tg://user?id={admin.user.id}")
for admin in admins if not admin.user.is_bot
)
return f"{hidden_links}{text}"

View File

@@ -0,0 +1,29 @@
from random import choice
from configs.config import Lists
# Настройка экспорта в модули
__all__ = ("interesting_fact",)
def interesting_fact(mode: str = "факт", lists: list[str] = None) -> str:
"""
Возвращает случайный факт, анекдот или цитату, в зависимости от режима.
:param mode: Строка, определяющая тип контента ("факт", "анекдот", "цитата").
:param lists: Необязательный список строк, из которого можно выбирать вручную.
:return: Случайный элемент из соответствующего списка.
"""
if lists is not None:
return choice(lists)
mode: str = mode.lower()
if mode == "анекдот":
source: list[str] = Lists.jokes
elif mode == "цитата":
source: list[str] = Lists.quotes
else:
source: list[str] = Lists.facts
return choice(source)

29
bot/utils/pagination.py Normal file
View File

@@ -0,0 +1,29 @@
from aiogram.types import InlineKeyboardButton
# Настройка экспорта в модули
__all__ = ('pagination_btn',)
def pagination_btn(action: str,
page: int = 0,
total_posts: int = 0,
bt_page: int = 5) -> list[InlineKeyboardButton]:
"""
Создает кнопки для пагинации.
:param action: Действие в котором нужна пангинация.
:param page: Номер начальной страницы, по умолчанию 0.
:param total_posts: Количество постов.
:param bt_page: Количество кнопок на одной странице.
:return: Готовый лист списка инлайн-кнопок.
"""
navigation_buttons: list[InlineKeyboardButton] = []
if page > 0:
navigation_buttons.append(InlineKeyboardButton(
text="", callback_data=f"{action}_page_{page - 1}"
))
if (page + 1) * bt_page < total_posts:
navigation_buttons.append(InlineKeyboardButton(
text="", callback_data=f"{action}_page_{page + 1}"
))
return navigation_buttons

85
bot/utils/type_message.py Normal file
View File

@@ -0,0 +1,85 @@
from typing import Final
from aiogram.types import Message
# Настройка экспорта в модули
__all__ = ("CHAT_TYPES", "CONTENT_TYPE_RU", "type_chat", "type_msg")
# Словарь сопоставлений "chat_type -> русское название"
CHAT_TYPES: Final[dict[str, str]] = {
"private": "Личный",
"group": "Группа",
"supergroup": "Группа",
"channel": "Канал",
}
# Словарь сопоставлений "content_type -> русское название"
CONTENT_TYPE_RU: Final[dict[str, str]] = {
"text": "Текст",
"animation": "Гиф",
"audio": "Аудио",
"document": "Файл",
"photo": "Фото",
"sticker": "Стикер",
"video": "Видео",
"video_note": "Видеосообщение",
"voice": "Голосовое сообщение",
"contact": "Контакт",
"dice": "Кубик",
"game": "Игра",
"poll": "Опрос",
"venue": "Место",
"location": "Локация",
"new_chat_members": "Новые участники чата",
"left_chat_member": "Участник вышел",
"new_chat_title": "Новое название чата",
"new_chat_photo": "Новая картинка чата",
"delete_chat_photo": "Удалена картинка чата",
"group_chat_created": "Создана группа",
"supergroup_chat_created": "Создана супергруппа",
"channel_chat_created": "Создан канал",
"message_auto_delete_timer_changed": "Изменён автоудалитель",
"migrate_to_chat_id": "Группа → супергруппа",
"migrate_from_chat_id": "Супергруппа → группа",
"pinned_message": "Закреплённое сообщение",
"invoice": "Счёт",
"successful_payment": "Успешный платёж",
"connected_website": "Подключённый сайт",
"passport_data": "Данные Telegram Passport",
"proximity_alert_triggered": "Алерт о приближении",
"video_chat_scheduled": "Запланированный видеочат",
"video_chat_started": "Видеочат начался",
"video_chat_ended": "Видеочат завершён",
"video_chat_participants_invited": "Приглашены участники видеочата",
"web_app_data": "Данные из веб-приложения",
"forum_topic_created": "Создана тема форума",
"forum_topic_edited": "Изменена тема форума",
"forum_topic_closed": "Тема форума закрыта",
"forum_topic_reopened": "Тема форума открыта",
"general_forum_topic_hidden": "Общая тема скрыта",
"general_forum_topic_unhidden": "Общая тема снова отображается",
"giveaway_created": "Создан розыгрыш",
"giveaway": "Розыгрыш",
"giveaway_completed": "Розыгрыш завершён",
"message_reaction": "Реакция на сообщение",
}
def type_msg(message: Message) -> str:
"""
Определяет и возвращает тип сообщения на русском языке.
:param message: объект Message от aiogram
:return: строка с типом сообщения
"""
return CONTENT_TYPE_RU.get(message.content_type, f"Неизвестный тип ({message.content_type})")
def type_chat(message: Message) -> str:
"""
Преобразует информацию о чате в его тип на русском языке.
:param message: Объект сообщения из aiogram, содержащий информацию о чате.
:return: Тип чата строкой.
"""
return CHAT_TYPES.get(message.chat.type, f"Неизвестный тип чата {message.chat.type}")

23
bot/utils/usernames.py Normal file
View File

@@ -0,0 +1,23 @@
from aiogram.types import Message, CallbackQuery
# Настройка экспорта в модули
__all__ = ('username',)
# Функция получения юзера или ID пользователя
def username(message: Message | CallbackQuery) -> str:
"""
Возвращает юзернейм пользователя из сообщения, или ID, если юзернейм не указан.
:param message: Объект сообщения из aiogram.
:return: Строка с юзернеймом пользователя или его ID.
:raises ValueError: Если в сообщении отсутствует информация о пользователе.
"""
try:
if message.from_user:
return f"@{message.from_user.username}" if message.from_user.username else f"@{message.from_user.id}"
raise ValueError("Информация о пользователе отсутствует в сообщении.")
except ValueError as e:
# Перебрасываем ошибку выше для дальнейшей обработки
raise e