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

37
.dockerignore Normal file
View File

@@ -0,0 +1,37 @@
# Исключить скрытые системные каталоги, но не всё подряд
.git/
.gitattributes
.gitignore
# Виртуальные окружения и Python-кэш
.venv/
venv/
__pycache__/
*.py[cod]
*.pyo
# IDE-файлы
.idea/
.vscode/
# Тесты и документация
tests/
test/
docs/
examples/
# Логи и артефакты сборки
*.log
*.logs
*.log.*
*.logs.*
Logs/
Log/
dist/
build/
# Примеры и шаблоны
.env
env
*.session
*.sessions

92
.env_example Normal file
View File

@@ -0,0 +1,92 @@
# Токены бота
BOT_TOKEN=your_bot_token_here
BOT_DEBUG_TOKEN=your_debug_bot_token_here
# Режим отладки
DEBUG=False
# Владелец бота
OWNER=@verdise
# Основные настройки
PARSE_MODE=HTML
ENCOD=utf-8
TIME_FORMAT=%Y-%m-%d %H:%M:%S
PREFIX=/!.&?
BOT_LANGUAGE=Aiogram3
# Настройки сообщений
DISABLE_NOTIFICATION=False
PROTECT_CONTENT=False
ALLOW_SENDING_WITHOUT_REPLY=True
LINK_PREVIEW_IS_DISABLED=False
LINK_PREVIEW_PREFER_SMALL_MEDIA=False
LINK_PREVIEW_PREFER_LARGE_MEDIA=True
LINK_PREVIEW_SHOW_ABOVE_TEXT=False
SHOW_CAPTION_ABOVE_MEDIA=False
# Разрешения
BOT_EDIT=False
START_INFO_CONSOLE=True
START_INFO_TO_FILE=True
# Логирование
LOG_CONSOLE=True
LOG_FILE=True
LOG_DIR=Logs
LOG_FILE_INFO=bot_info.log
# Вебхук
WEBHOOK=False
# API ключи
API_KEY=your_api_key
WEB_API_KEY=your_web_api_key
WEATHER_API_KEY=your_weather_api_key
# Telegram API ID и HASH
TG_API_UID=123456
TG_API_HASH=your_tg_api_hash
# Важные ID
ADMIN_ID=123456789
MODERATOR_ID=987654321
IMPORTANT_ID=1122334455
IMPORTANT_GROUP_ID=-1001122334455
IMPORTANT_CHANNEL_ID=-1009988776655
# Настройки бота
PROJECT_NAME=PRIMO
BOT_NAME=Первозданная Жемчужина
BOT_DESCRIPTION=Ваш помощник в удивительные миры! Prod. by:『@verdise』
BOT_SHORT_DESCRIPTION=Тех.поддержка: @verdise
# Настройки ролевого проекта
RP_NAME: str = "𝘗𝘳𝘪𝘮𝘰 𝘞𝘰𝘳𝘭𝘥"
# Права администратора
ANONYMOUS=False
MANAGE_CHAT=True
CHANGE_INFO=True
PROMOTE_MEMBERS=True
RESTRICT_MEMBERS=True
POST_MESSAGE=True
MANAGE_TOPICS=True
INVITE_USER=True
DELETE_MESSAGES=True
MANAGE_VIDEO_CHATS=True
EDIT_MESSAGES=True
PIN_MESSAGE=True
POST_STORIES=True
EDIT_STORIES=True
DELETE_STORIES=True
# Поддержка
SUPPORT_CHAT_ID=0

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

19
.idea/app.iml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/bot/templates" />
</list>
</option>
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/app.iml" filepath="$PROJECT_DIR$/.idea/app.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# Используем официальный образ Python с подходящей версией
FROM python:3.12
# Устанавливаем Poetry
RUN pip install poetry
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем файлы Poetry
COPY pyproject.toml poetry.lock* ./
# Настраиваем Poetry (не создавать виртуальное окружение внутри контейнера)
RUN poetry config virtualenvs.create false
# Устанавливаем зависимости через Poetry
RUN poetry install --no-interaction --no-ansi --no-root
# Копируем все файлы проекта внутрь контейнера
COPY . .
# Устанавливаем переменную окружения для буферизации
ENV PYTHONUNBUFFERED=1
# Команда запуска — запуск скрипта main.py
CMD ["python", "main.py"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) [2025] [Verum]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
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 NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS 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.

BIN
README.md Normal file

Binary file not shown.

0
api/__init__.py Normal file
View File

96
api/server.py Normal file
View File

@@ -0,0 +1,96 @@
from typing import Any
from aiohttp import web
from userbot.client import userbot_send_message, userbot_edit_message
from middleware import logger
class APIServer:
"""
Асинхронное API для связи aiogram ↔ pyrogram.
Предоставляет эндпоинты для отправки и редактирования сообщений через Premium-аккаунт.
"""
def __init__(self, host: str = "0.0.0.0", port: int = 8081) -> None:
self.host = host
self.port = port
self.app = web.Application()
self.app.add_routes([
web.post("/api/send", self.send_message),
web.post("/api/edit", self.edit_message),
web.get("/api/health", self.health_check)
])
self.runner: web.AppRunner | None = None
self.site: web.TCPSite | None = None
@staticmethod
async def health_check(_: web.Request) -> web.Response:
"""Простейший эндпоинт для проверки состояния API."""
return web.json_response({"status": "ok"})
@staticmethod
async def send_message(request: web.Request) -> web.Response:
"""
Эндпоинт: /api/send
Ожидает JSON:
{
"chat_id": str | int,
"text": str,
"parse_mode": str | None
}
"""
try:
payload: dict[str, Any] = await request.json()
chat_id = payload.get("chat_id")
text = payload.get("text")
parse_mode = payload.get("parse_mode")
if not chat_id or not text:
return web.json_response({"error": "chat_id and text required"}, status=400)
message = await userbot_send_message(chat_id, text, parse_mode)
return web.json_response({"status": "ok", "message_id": message.id})
except Exception as e:
logger.error(f"Ошибка отправки: {e}")
return web.json_response({"status": "error", "detail": str(e)}, status=500)
@staticmethod
async def edit_message(request: web.Request) -> web.Response:
"""
Эндпоинт: /api/edit
Ожидает JSON:
{
"chat_id": str | int,
"message_id": int,
"text": str,
"parse_mode": str | None
}
"""
try:
payload: dict[str, Any] = await request.json()
chat_id = payload.get("chat_id")
message_id = payload.get("message_id")
text = payload.get("text")
parse_mode = payload.get("parse_mode")
if not all([chat_id, message_id, text]):
return web.json_response({"error": "chat_id, message_id, text required"}, status=400)
message = await userbot_edit_message(chat_id, message_id, text, parse_mode)
return web.json_response({"status": "ok", "message_id": message.id})
except Exception as e:
logger.error(f"Ошибка редактирования: {e}")
return web.json_response({"status": "error", "detail": str(e)}, status=500)
async def start(self) -> None:
"""Запуск aiohttp API-сервера."""
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.host, self.port)
await self.site.start()
log.info(f"🚀 API запущено на http://{self.host}:{self.port}")
async def stop(self) -> None:
"""Остановка сервера."""
if self.runner:
await self.runner.cleanup()

BIN
assets/default.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

BIN
assets/start.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

BIN
bot.db Normal file

Binary file not shown.

3
bot/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .core import *
from .handlers import *
from .middlewares import *

2
bot/core/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .bots import *
from .webhook import *

260
bot/core/bots.py Normal file
View File

@@ -0,0 +1,260 @@
from asyncio import sleep
from datetime import datetime
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.exceptions import TelegramRetryAfter
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import User, ChatAdministratorRights, BotDescription, BotShortDescription
from aiogram.utils.i18n import I18n, SimpleI18nMiddleware
from configs.config import BotSettings, BotEdit, Webhook, Permission
from middleware.loggers import log, logger
# Настройка экспорта в модули
__all__ = ("dp", "bot", "BotInfo", "i18n")
# FSM-хранилище и диспетчер
storage: MemoryStorage = MemoryStorage()
dp: Dispatcher = Dispatcher(storage=storage)
dp["is_active"]: bool = True
# Локализация
i18n: I18n = I18n(path="locales", default_locale="ru", domain="bot")
i18n_middleware: SimpleI18nMiddleware = SimpleI18nMiddleware(i18n=i18n)
i18n_middleware.setup(dp)
# Экземпляр бота
bot: Bot = Bot(
token=BotSettings.BOT_TOKEN,
default=DefaultBotProperties(
parse_mode=BotSettings.PARSE_MODE,
disable_notification=BotSettings.DISABLE_NOTIFICATION,
protect_content=BotSettings.PROTECT_CONTENT,
allow_sending_without_reply=BotSettings.ALLOW_SENDING_WITHOUT_REPLY,
link_preview_is_disabled=BotSettings.LINK_PREVIEW_IS_DISABLED,
link_preview_prefer_small_media=BotSettings.LINK_PREVIEW_PREFER_SMALL_MEDIA,
link_preview_prefer_large_media=BotSettings.LINK_PREVIEW_PREFER_LARGE_MEDIA,
link_preview_show_above_text=BotSettings.LINK_PREVIEW_SHOW_ABOVE_TEXT,
show_caption_above_media=BotSettings.SHOW_CAPTION_ABOVE_MEDIA,
),
)
class BotInfo:
"""
Класс для хранения и управления информацией о боте.
Все поля строго аннотированы, description заменено на widget.
"""
id: int | None = None
url: str | None = None
first_name: str | None = None
last_name: str | None = None
username: str | None = None
widget: str | None = None # вместо description
description: str | None = None # вместо short_description
language_code: str = BotSettings.BOT_LANGUAGE
prefix: str = BotSettings.PREFIX
bot_owner: str = BotSettings.OWNER
added_to_attachment_menu: bool = False
supports_inline_queries: bool = False
can_connect_to_business: bool = False
has_main_web_app: bool = False
can_join_groups: bool = False
can_read_all_group_messages: bool = False
rights: ChatAdministratorRights | None = None
@classmethod
@log(level="INFO", log_type="BOT", text="Настройка вебхука бота")
async def webhook(
cls, bots: Bot = bot, webhook_url: str = Webhook.WEBHOOK_URL, use_webhook: bool = Webhook.WEBHOOK
) -> None:
"""
Установка или удаление вебхука для бота.
"""
try:
await bots.delete_webhook(drop_pending_updates=True)
if use_webhook:
if webhook_url is None:
raise ValueError("Для установки вебхука необходимо указать webhook_url")
try:
await bots.set_webhook(webhook_url)
except TelegramRetryAfter as e:
logger.warning(f"Flood control при установке вебхука. Повтор через {e.retry_after} сек.")
await sleep(e.retry_after)
await bots.set_webhook(webhook_url)
except Exception as e:
logger.error(f"Ошибка при настройке вебхука: {e}")
@classmethod
@log(level="INFO", log_type="BOT", text="Получение информации о боте")
async def info(cls, bots: Bot = bot) -> dict[str, object] | None:
"""
Получает и сохраняет основные данные о боте.
"""
try:
bot_info: User = await bots.get_me()
bot_description: BotDescription = await bots.get_my_description()
bot_short_description: BotShortDescription = await bots.get_my_short_description()
bot_rights: ChatAdministratorRights = await bot.get_my_default_administrator_rights()
cls.id = bot_info.id
cls.url = f"tg://user?id={cls.id}"
cls.first_name = bot_info.first_name
cls.last_name = bot_info.last_name
cls.username = bot_info.username
cls.language_code = bot_info.language_code
# Описание (widget) и короткое описание (description)
cls.widget = bot_description.description or ""
cls.description = bot_short_description.short_description or ""
cls.added_to_attachment_menu = getattr(bot_info, "added_to_attachment_menu", False)
cls.supports_inline_queries = getattr(bot_info, "supports_inline_queries", False)
cls.can_connect_to_business = getattr(bot_info, "can_connect_to_business", False)
cls.has_main_web_app = getattr(bot_info, "has_main_web_app", False)
cls.can_join_groups = getattr(bot_info, "can_join_groups", False)
cls.can_read_all_group_messages = getattr(bot_info, "can_read_all_group_messages", False)
cls.rights = bot_rights or None
return {
"id": cls.id,
"url": cls.url,
"first_name": cls.first_name,
"last_name": cls.last_name,
"username": cls.username,
"language_code": cls.language_code,
"widget": cls.widget,
"description": cls.description,
"added_to_attachment_menu": cls.added_to_attachment_menu,
"supports_inline_queries": cls.supports_inline_queries,
"can_connect_to_business": cls.can_connect_to_business,
"has_main_web_app": cls.has_main_web_app,
"can_join_groups": cls.can_join_groups,
"can_read_all_group_messages": cls.can_read_all_group_messages,
"prefix": cls.prefix,
"bot_owner": cls.bot_owner,
"rights": cls.rights,
}
except Exception as e:
logger.error(f"Ошибка при получении информации о боте: {e}")
return None
@staticmethod
@log(level="INFO", log_type="BOT", text="Установка прав администратора")
async def set_administrator_rights(rights: ChatAdministratorRights = BotEdit.RIGHTS, bots: Bot = bot) -> None:
"""
Устанавливает дефолтные права администратора для бота.
"""
try:
current_rights: ChatAdministratorRights = await bots.get_my_default_administrator_rights()
if current_rights != rights:
await bots.set_my_default_administrator_rights(rights=rights)
await bots.set_my_default_administrator_rights(rights=rights, for_channels=True)
except Exception as e:
logger.error(f"Ошибка при установке прав администратора: {e}")
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление имени бота")
async def set_name(new_name: str = BotEdit.NAME, bots: Bot = bot) -> None:
"""
Обновляет имя бота (от 1 до 32 символов).
"""
try:
current_name: str = (await bots.get_me()).first_name
if not (1 <= len(new_name) <= 32):
raise ValueError("Имя бота должно быть от 1 до 32 символов.")
if current_name != new_name:
await bots.set_my_name(name=new_name)
except Exception as e:
logger.error(f"Ошибка при обновлении имени бота: {e}")
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление виджета бота")
async def set_widget(new_widget: str = BotEdit.DESCRIPTION, bots: Bot = bot) -> None:
"""
Обновляет описание бота (widget).
"""
try:
current_widget: BotDescription = await bots.get_my_description()
if not (0 < len(new_widget) <= 255):
raise ValueError("Виджет должен быть от 1 до 255 символов.")
if current_widget.description != new_widget:
await bots.set_my_description(description=new_widget)
except Exception as e:
logger.error(f"Ошибка при обновлении виджета бота: {e}")
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление короткого виджета бота")
async def set_short_widget(new_short: str = BotEdit.SHORT_DESCRIPTION, bots: Bot = bot) -> None:
"""
Обновляет короткое описание (short_widget).
"""
try:
current_short: BotShortDescription = await bots.get_my_short_description()
if not (0 < len(new_short) <= 120):
raise ValueError("Короткий виджет должен быть от 1 до 120 символов.")
if current_short.short_description != new_short:
await bots.set_my_short_description(short_description=new_short)
except Exception as e:
logger.error(f"Ошибка при обновлении короткого виджета бота: {e}")
@staticmethod
def start_info_out(out: bool = True) -> str | None:
"""
Формирует и выводит стартовую информацию о боте.
"""
try:
bot_time: str = f"Бот @{BotInfo.username} запущен в {datetime.now().strftime('%S:%M:%H %d-%m-%Y')}\n"
bot_name: str = f"Основное имя: {BotInfo.first_name}\n"
bot_postname: str = f" Доп. имя: {BotInfo.last_name}\n"
bot_description: str = f" Описание бота: {BotInfo.description}\n"
bot_widget: str = f" Виджет бота: {BotInfo.widget}\n"
bot_username: str = f" Юзернейм: @{BotInfo.username}\n"
bot_id: str = f" ID: {BotInfo.id}\n"
bot_can_join_groups: str = f" Может ли вступать в группы: {BotInfo.can_join_groups}\n"
bot_can_read_all_group_messages: str = f" Чтение всех сообщений: {BotInfo.can_read_all_group_messages}\n"
bot_added_to_attachment_menu: str = f" Добавлен в меню вложений: {BotInfo.added_to_attachment_menu}\n"
bot_supports_inline_queries: str = f" Поддерживает инлайн-запросы: {BotInfo.supports_inline_queries}\n"
bot_can_connect_to_business: str = f" Подключение к бизнес-аккаунтам: {BotInfo.can_connect_to_business}\n"
bot_has_main_web_app: str = f" Основное веб-приложение: {BotInfo.has_main_web_app}\n"
bot_prefixs: str = f" Префиксы команд бота: {BotInfo.prefix}\n"
bot_all_info: str = (
f"{bot_name} {bot_postname} {bot_description} {bot_widget} {bot_username} "
f"{bot_id} {bot_can_join_groups} {bot_can_read_all_group_messages} "
f"{bot_added_to_attachment_menu} {bot_supports_inline_queries} "
f"{bot_can_connect_to_business} {bot_has_main_web_app} {bot_prefixs}"
)
if out:
print(f"\033[34m{bot_all_info}\033[0m")
with open("Logs/info.log", "w", encoding="utf-8") as log_file:
log_file.write(f"{bot_time}{bot_all_info}")
with open("Logs/bot_start.log", "a", encoding="utf-8") as log_start_file:
log_start_file.write(f"{bot_time}\n")
return bot_all_info
except Exception as e:
logger.error(f"Ошибка при выводе стартовой информации: {e}")
return None
@classmethod
@log(level="INFO", log_type="START", text="Процесс запуска бота!")
async def setup(cls, perm: bool = Permission.BOT_EDIT, bots: Bot = bot) -> None:
"""
Настройка и инициализация всех параметров бота при старте.
"""
try:
await cls.webhook(bots=bots)
await cls.info(bots=bots)
if perm:
await cls.set_administrator_rights(bots=bots)
await cls.set_widget(bots=bots)
await cls.set_short_widget(bots=bots)
await cls.set_name(bots=bots)
except Exception as e:
logger.error(f"Ошибка при запуске настройки бота: {e}")

53
bot/core/webhook.py Normal file
View File

@@ -0,0 +1,53 @@
from typing import Any
from aiohttp import web
from aiogram.types import Update
from middleware.loggers import logger
from bot.core.bots import dp, bot
# Настройки экспорта в модули
__all__ = ("WebhookApp",)
class WebhookApp:
"""Приложение aiohttp для обработки webhook-запросов."""
def __init__(self, host: str = "0.0.0.0", port: int = 8080) -> None:
self.host = host
self.port = port
self.app: web.Application = web.Application()
self.app.router.add_post("/webhook", self.handle_update)
self.runner: web.AppRunner | None = None
self.site: web.TCPSite | None = None
@staticmethod
async def handle_update(request: web.Request) -> web.Response:
"""Обработчик входящих запросов от Telegram."""
try:
update_json: dict[str, Any] = await request.json()
update: Update = Update.model_validate(update_json)
await dp.feed_update(bot=bot, update=update)
except Exception as e:
logger.error(f"Ошибка обработки webhook-запроса: {e}")
return web.Response(status=500)
return web.Response(status=200)
async def start(self) -> None:
"""Асинхронный запуск aiohttp-приложения."""
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.host, self.port)
await self.site.start()
logger.info(f"🌍 Webhook сервер запущен на http://{self.host}:{self.port}")
async def stop(self) -> None:
"""Остановка aiohttp-приложения."""
if self.runner:
await self.runner.cleanup()

1
bot/data/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .topic_map import *

4
bot/data/topic_map.py Normal file
View File

@@ -0,0 +1,4 @@
# bot/data/topic_map.py
# ключ: (user_id, тип) → thread_id
user_topic_map: dict[tuple[int, str], int] = {}

5
bot/filters/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .callback import *
from .chat_rights import *
from .chat_type import *
from .message_content import *
from .subscrided import *

35
bot/filters/callback.py Normal file
View File

@@ -0,0 +1,35 @@
from aiogram.filters import BaseFilter
from aiogram.types import CallbackQuery
# Настройка экспорта в модули
__all__ = ("CallbackDataStartsWith", "CallbackStartsWith")
class CallbackDataStartsWith(BaseFilter):
"""
Фильтр для callback_data, начинающихся с префикса.
Example:
@router.callback_query(CallbackDataStartsWith("menu:"))
async def handler(cb: CallbackQuery):
await cb.answer("Это callback из меню ✅")
"""
def __init__(self, prefix: str) -> None:
self.prefix = prefix
async def __call__(self, callback: CallbackQuery) -> bool:
return bool(callback.data and callback.data.startswith(self.prefix))
class CallbackStartsWith(BaseFilter):
"""
Фильтр для callback_data, которое начинается с команды из списка.
Игнорирует регистр.
"""
def __init__(self, commands: list[str]):
self.commands = [cmd.casefold() for cmd in commands]
async def __call__(self, callback: CallbackQuery) -> bool:
data = callback.data.casefold() if callback.data else ""
return any(data.startswith(cmd) for cmd in self.commands)

152
bot/filters/chat_rights.py Normal file
View File

@@ -0,0 +1,152 @@
from typing import Any
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from aiogram.filters import BaseFilter
from aiogram.types import Message, ResultChatMemberUnion, CallbackQuery
from configs import ImportantID
# Настройка экспорта в модули
__all__ = ("IsChatCreator", "IsAdmin", "IsModerator", "IsOwner",)
class IsOwner(BaseFilter):
"""
Фильтр для проверки, является ли пользователь владельцем бота.
Args:
send_error_message (bool): Если True, при попытке не- владельца выполнить команду,
бот отправит сообщение об ошибке.
Returns:
bool | dict[str, Any]:
- False, если пользователь не владелец и send_error_message=False
- True, если пользователь является владельцем
- dict с информацией о пользователе, если send_error_message=True
Example:
@router.message(IsOwner())
async def cmd_handler(message: Message):
...
@router.message(IsOwner(send_error_message=True))
async def admin_only(message: Message):
...
"""
def __init__(self, send_error_message: bool = False) -> None:
"""
Инициализация фильтра.
Args:
send_error_message: Нужно ли отправлять сообщение при запрещенном доступе
"""
self.send_error_message: bool = send_error_message
async def __call__(self, update: Message | CallbackQuery, bot: Bot) -> bool | dict[str, Any]:
"""
Проверяет, является ли пользователь владельцем.
Args:
update: Объект Message или CallbackQuery
bot: Экземпляр бота (не используется, но требуется сигнатурой)
Returns:
bool | dict[str, Any]: Результат фильтра. Если пользователь владелец,
возвращается True или dict с info. Иначе False
"""
if not update.from_user:
# Без from_user невозможно определить владельца
return False
user_id: int = update.from_user.id
is_owner: bool = user_id in ImportantID.OWNERS_ID
if not is_owner and self.send_error_message:
# Отправляем предупреждение о доступе
if isinstance(update, Message):
await update.answer(text="⛔ Эта команда доступна только владельцу бота!")
elif isinstance(update, CallbackQuery):
await update.answer(text="⛔ Доступно только владельцу бота!", show_alert=True)
return False
# Если пользователь владелец — возвращаем словарь с дополнительной информацией
if is_owner:
return {
"is_owner": True,
"user_id": user_id,
"owner_ids": ImportantID.OWNERS_ID
}
# Если не владелец и send_error_message=False
return False
class IsChatCreator(BaseFilter):
"""
Пользователь является создателем чата.
Example:
@router.message(IsChatCreator())
async def handler(msg: Message):
await msg.answer("Ты создатель этого чата 👑")
"""
async def __call__(self, message: Message, bot: Bot) -> bool:
try:
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
return member.status == "creator"
except (TelegramBadRequest, TelegramForbiddenError):
return False
class IsAdmin(BaseFilter):
"""
Пользователь является администратором (или создателем).
Example:
@router.message(IsAdmin())
async def handler(msg: Message):
await msg.answer("Ты админ ✅")
"""
async def __call__(self, message: Message, bot: Bot) -> bool:
try:
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
return member.status in {"administrator", "creator"}
except (TelegramBadRequest, TelegramForbiddenError):
return False
class IsModerator(BaseFilter):
"""
Администратор с модераторскими правами:
- удаление сообщений
- ограничение пользователей
- закрепление сообщений
Example:
@router.message(IsModerator())
async def handler(msg: Message):
await msg.answer("Ты модератор ✅")
"""
async def __call__(self, message: Message, bot: Bot) -> bool:
try:
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
if member.status == "creator":
return True
if member.status != "administrator":
return False
required_rights: list[bool] = [
getattr(member, "can_delete_messages", False),
getattr(member, "can_restrict_members", False),
getattr(member, "can_pin_messages", False),
]
return all(required_rights)
except (TelegramBadRequest, TelegramForbiddenError):
return False

33
bot/filters/chat_type.py Normal file
View File

@@ -0,0 +1,33 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
# Настройка экспорта в модули
__all__ = ("IsPrivate", "IsGroup",)
class IsPrivate(BaseFilter):
"""
Сообщение в личке с ботом.
Example:
@router.message(IsPrivate())
async def handler(msg: Message):
await msg.answer("Это ЛС ✅")
"""
async def __call__(self, message: Message) -> bool:
return message.chat.type == "private"
class IsGroup(BaseFilter):
"""
Сообщение в группе или супергруппе.
Example:
@router.message(IsGroup())
async def handler(msg: Message):
await msg.answer("Это сообщение в группе ✅")
"""
async def __call__(self, message: Message) -> bool:
return message.chat.type in {"group", "supergroup"}

View File

@@ -0,0 +1,71 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
# Настройка экспорта в модули
__all__ = ("IsReply", "IsForwarded", "HasMedia", "ContainsURL",)
class IsReply(BaseFilter):
"""
Сообщение является ответом.
Example:
@router.message(IsReply())
async def handler(msg: Message):
await msg.answer("Это реплай ✅")
"""
async def __call__(self, message: Message) -> bool:
return message.reply_to_message is not None
class IsForwarded(BaseFilter):
"""
Сообщение переслано из другого чата/от пользователя.
Example:
@router.message(IsForwarded())
async def handler(msg: Message):
await msg.answer("Это пересланное сообщение 🔄")
"""
async def __call__(self, message: Message) -> bool:
return (message.forward_from is not None) or (message.forward_from_chat is not None)
class HasMedia(BaseFilter):
"""
Сообщение содержит медиа (фото, видео, документ и т.д.).
Example:
@router.message(HasMedia())
async def handler(msg: Message):
await msg.answer("Это медиа ✅")
"""
async def __call__(self, message: Message) -> bool:
return any([
message.photo,
message.video,
message.document,
message.audio,
message.voice,
message.video_note,
message.sticker,
])
class ContainsURL(BaseFilter):
"""
Сообщение содержит ссылку (http/https).
Example:
@router.message(ContainsURL())
async def handler(msg: Message):
await msg.answer("Это сообщение с ссылкой 🔗")
"""
async def __call__(self, message: Message) -> bool:
if not message.text:
return False
return "http://" in message.text or "https://" in message.text

41
bot/filters/subscrided.py Normal file
View File

@@ -0,0 +1,41 @@
from typing import Union
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from aiogram.filters import BaseFilter
from aiogram.types import Message, ResultChatMemberUnion
# Настройка экспорта в модули
__all__ = ("FilterSubscribed",)
class FilterSubscribed(BaseFilter):
"""
Фильтр для проверки подписки пользователя на один или несколько каналов.
Поддерживает как публичные каналы (username), так и приватные (ID).
Пример:
# Проверка сразу двух каналов: публичный по username и приватный по ID
@router.message(FilterSubscribed(["@public_channel", -1001234567890]))
async def only_subscribed(message: Message):
await message.answer("Ты подписан и на публичный, и на приватный канал ✅")
"""
def __init__(self, channels: list[Union[str, int]]) -> None:
self.channels = channels
async def __call__(self, message: Message, bot: Bot) -> bool:
for channel in self.channels:
try:
member: ResultChatMemberUnion = await bot.get_chat_member(
chat_id=channel,
user_id=message.from_user.id
)
if member.status in ("left", "kicked"):
return False
except (TelegramBadRequest, TelegramForbiddenError):
# Канал недоступен, либо у бота нет прав
return False
return True

21
bot/handlers/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
from aiogram import Router
from .commands import router as cmd_routers
from .messages import router as messages_routers
from .form_utils import router as form_routers
from .union_utills import router as union_routers
from .custom import router as custom_routers
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
custom_routers,
#cmd_routers,
#messages_routers,
#form_routers,
#union_routers,
)

View File

@@ -0,0 +1,21 @@
from aiogram import Router
from .admins import router as admin_cmd_router
from .special import router as special_cmd_router
from .users import router as users_cmd_router
from .users.cancel_cmd import router as cancel_cmd_router
from .settings import router as settings_cmd_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
cancel_cmd_router,
settings_cmd_router,
admin_cmd_router,
users_cmd_router,
special_cmd_router,
)

View File

@@ -0,0 +1,18 @@
from aiogram import Router
from .ban_cmd import router as ban_cmd_router
from .all_cmd import router as all_cmd_router
from .pin_cmd import router as pin_cmd_router
from .kick_cmd import router as kick_cmd_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
router.include_routers(
ban_cmd_router,
kick_cmd_router,
pin_cmd_router,
all_cmd_router,
)

View File

@@ -0,0 +1,80 @@
from asyncio import create_task
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from bot.core.bots import bot, BotInfo
from bot.filters import IsOwner
from bot.utils import status_clear, auto_delete_message, hidden_admins_message
from configs import COMMANDS
from middleware.loggers import logger
__all__ = ("router",)
# Ключ для команды
CMD: str = "all"
# Инициализация роутера
router: Router = Router(name=f"{CMD}_cmd_router")
@router.message(
F.text.lower().regexp(rf"^({'|'.join(COMMANDS[CMD])})\s?.*"), # ловим текст без префикса
F.chat.type.in_({"supergroup", "group"}),
IsOwner()
)
@router.message(
Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True),
IsOwner()
)
async def notify_all_text(message: Message, state: FSMContext) -> None:
"""
Обработчик команды /all, /call и текстовых эквивалентов типа "Калл Привет всем".
Функционал:
1. Считывает весь текст после команды.
2. Формирует скрытое сообщение для администраторов.
3. Отправляет сообщение в чат.
4. Автоматически удаляет сообщение через неделю.
5. Пытается закрепить сообщение в чате.
Args:
message (Message): Объект входящего сообщения.
state (FSMContext): Контекст FSM, используется для очистки состояния.
"""
# Очистка состояния FSM перед выполнением команды
await status_clear(message=message, state=state)
# Извлечение текста после команды
parts: list[str] = message.text.split(" ", 1)
custom_text: str = parts[1] if len(parts) > 1 else "⚡ Внимание всем!"
# Формирование скрытого текста для администраторов
hidden_text: str = await hidden_admins_message(message=message, text=custom_text)
# Отправка сообщения в чат
sent_message: Message = await message.answer(hidden_text)
# Запуск асинхронной задачи по удалению сообщения через 7 дней
create_task(
auto_delete_message(
chat_id=message.chat.id,
message_id=sent_message.message_id,
delay=604800 # 7 дней в секундах
)
)
# Попытка закрепить сообщение и удалить "системное" сообщение о закреплении
try:
await bot.pin_chat_message(
chat_id=message.chat.id,
message_id=sent_message.message_id,
disable_notification=False
)
# Иногда Telegram создает дополнительное уведомление при закреплении
await bot.delete_message(chat_id=message.chat.id, message_id=sent_message.message_id + 1)
logger.debug(f"[ALL] Сообщение закреплено: {custom_text}")
except TelegramBadRequest as e:
logger.error(f"[ALL] Ошибка закрепления сообщения: {e}")

View File

@@ -0,0 +1,258 @@
from aiogram import Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, User
from html import escape
from bot.filters import IsAdmin
from bot.utils import status_clear
from configs import COMMANDS
from database import db
# Настройки роутера
__all__ = ("router",)
from middleware import logger
CMD: str = "ban"
router: Router = Router(name=f"{CMD}_cmd_router")
@router.message(Command(*COMMANDS[CMD], ignore_case=True), IsAdmin())
async def ban_user_cmd(message: Message, state: FSMContext) -> None:
"""
Команда /ban для блокировки пользователей.
Использование: /ban <user_id> или ответ на сообщение пользователя + /ban
"""
await status_clear(message=message, state=state)
try:
# Проверяем есть ли ответ на сообщение
if message.reply_to_message:
# Бан по ответу на сообщение
target_user: User | None = message.reply_to_message.from_user
if not target_user:
await message.answer("Не удалось определить пользователя")
return
target_user_id: int = target_user.id
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
# Проверяем, не пытаемся ли забанить бота
if target_user_id == message.bot.id:
await message.answer("❌ Нельзя заблокировать бота!")
return
# Баним пользователя
success: bool = await _ban_user(target_user_id, target_username, message)
if success:
safe_username: str = escape(target_username)
response_text = f"✅ Пользователь {safe_username} (ID: {target_user_id}) заблокирован!"
# Пытаемся забанить в чате (если команда вызвана в группе/чате)
if message.chat.type in ["group", "supergroup"]:
try:
await message.bot.ban_chat_member(
chat_id=message.chat.id,
user_id=target_user_id
)
response_text += "\n🚫 Пользователь исключен из чата."
except Exception as e:
logger.warning(f"Не удалось исключить пользователя из чата: {e}")
response_text += "\n⚠️ Не удалось исключить пользователя из чата."
await message.answer(
text=response_text,
parse_mode=None # Отключаем разметку
)
else:
await message.answer("Не удалось заблокировать пользователя")
else:
# Бан по ID пользователя
command_parts: list[str] = message.text.split()
if len(command_parts) < 2:
await message.answer(
" Использование команды:\n"
"• Ответьте на сообщение пользователя командой /ban\n"
"• Или укажите ID: /ban <user_id>"
)
return
try:
target_user_id: int = int(command_parts[1])
# Проверяем, не пытаемся ли забанить бота
if target_user_id == message.bot.id:
await message.answer("❌ Нельзя заблокировать бота!")
return
success: bool = await _ban_user(target_user_id, f"ID{target_user_id}", message)
if success:
response_text = f"✅ Пользователь (ID: {target_user_id}) заблокирован!"
# Пытаемся забанить в чате
if message.chat.type in ["group", "supergroup"]:
try:
await message.bot.ban_chat_member(
chat_id=message.chat.id,
user_id=target_user_id
)
response_text += "\n🚫 Пользователь исключен из чата."
except Exception as e:
logger.warning(f"Не удалось исключить пользователя из чата: {e}")
response_text += "\n⚠️ Не удалось исключить пользователя из чата."
await message.answer(
text=response_text,
parse_mode=None
)
else:
await message.answer("❌ Пользователь не найден или уже заблокирован")
except ValueError:
await message.answer("❌ Неверный формат ID пользователя")
except Exception as e:
logger.error(f"Ошибка в команде /ban: {e}")
await message.answer(
"⚠️ Произошла непредвиденная ошибка при выполнении команды.\n"
"Попробуйте повторить действие позже или нажмите /start"
)
async def _ban_user(user_id: int, username: str, message: Message) -> bool:
"""
Внутренняя функция для блокировки пользователя.
"""
try:
# Сначала проверяем существует ли пользователь
user: User | None = await db.get_user(user_id)
if not user:
# Если пользователя нет - создаем его забаненным
await db.add_user(
user_id=user_id,
username=username,
full_name=username
)
# Баним пользователя
await db.ban_user(user_id)
# Логируем действие
admin_username = message.from_user.username or message.from_user.full_name or f"ID{message.from_user.id}"
logger.info(f"🛑 Админ @{admin_username} заблокировал пользователя @{username} (ID: {user_id})")
return True
except Exception as e:
logger.error(f"❌ Ошибка при блокировке пользователя {user_id}: {e}")
return False
@router.message(Command("unban", ignore_case=True), IsAdmin())
async def unban_user_cmd(message: Message, state: FSMContext) -> None:
"""
Команда /unban для разблокировки пользователей.
"""
await status_clear(message=message, state=state)
try:
if message.reply_to_message:
target_user: User | None = message.reply_to_message.from_user
if not target_user:
await message.answer("Не удалось определить пользователя")
return
target_user_id: int = target_user.id
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
else:
command_parts: list[str] = message.text.split()
if len(command_parts) < 2:
await message.answer(
" Использование команды:\n"
"• Ответьте на сообщение пользователя командой /unban\n"
"• Или укажите ID: /unban <user_id>"
)
return
try:
target_user_id: int = int(command_parts[1])
target_username: str = f"ID{target_user_id}"
except ValueError:
await message.answer("❌ Неверный формат ID пользователя")
return
# Разбаниваем пользователя
await db.unban_user(target_user_id)
# Логируем действие
admin_username: str = message.from_user.username or message.from_user.full_name or f"ID{message.from_user.id}"
logger.info(f"🔓 Админ @{admin_username} разблокировал пользователя @{target_username} (ID: {target_user_id})")
# Экранируем специальные символы
safe_username: str = escape(target_username)
response_text = f"✅ Пользователь {safe_username} (ID: {target_user_id}) разблокирован!"
# Пытаемся разбанить в чате
if message.chat.type in ["group", "supergroup"]:
try:
await message.bot.unban_chat_member(
chat_id=message.chat.id,
user_id=target_user_id
)
response_text += "\n👥 Пользователь может вернуться в чат."
except Exception as e:
logger.warning(f"Не удалось разблокировать пользователя в чате: {e}")
await message.answer(
text=response_text,
parse_mode=None
)
except Exception as e:
logger.error(f"❌ Ошибка при разблокировке пользователя: {e}")
await message.answer("Не удалось разблокировать пользователя")
@router.message(Command("banned_list", ignore_case=True), IsAdmin())
async def banned_list_cmd(message: Message, state: FSMContext) -> None:
"""
Команда /banned_list для просмотра списка забаненных пользователей.
"""
await status_clear(message=message, state=state)
try:
# Получаем всех пользователей включая забаненных
all_users: list[User] = await db.get_all_users(include_banned=True)
# Фильтруем только забаненных
banned_users: list[User] = [user for user in all_users if getattr(user, 'status', None) == "banned"]
if not banned_users:
await message.answer("📭 Список забаненных пользователей пуст")
return
# Формируем сообщение со списком
banned_list: str = "🚫 Заблокированные пользователи:\n\n"
for user in banned_users[:50]: # Ограничиваем вывод
username: str = f"@{user.username}" if getattr(user, 'username', None) else getattr(user, 'full_name',
'Неизвестно')
# Экранируем специальные символы
safe_username = escape(username)
user_id = getattr(user, 'id', 'N/A')
banned_list += f"{safe_username} (ID: {user_id})\n"
if len(banned_users) > 50:
banned_list += f"\n... и еще {len(banned_users) - 50} пользователей"
await message.answer(banned_list, parse_mode=None)
except Exception as e:
logger.error(f"❌ Ошибка при получении списка забаненных: {e}")
await message.answer("Не удалось получить список забаненных пользователей")

View File

@@ -0,0 +1,278 @@
from aiogram import Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, User
from html import escape
from bot import bot
from bot.filters import IsAdmin
from bot.utils import status_clear
from configs import COMMANDS
# Настройки роутера
__all__ = ("router",)
from middleware import logger
CMD: str = "kick"
router: Router = Router(name=f"{CMD}_cmd_router")
@router.message(Command(*COMMANDS[CMD], ignore_case=True), IsAdmin())
async def kick_user_cmd(message: Message, state: FSMContext) -> None:
"""
Команда /kick для кика пользователей из чата.
Использование: /kick <user_id> или ответ на сообщение пользователя + /kick
"""
await status_clear(message=message, state=state)
# Проверяем, что команда используется в группе/супергруппе
if message.chat.type not in ["group", "supergroup"]:
await message.answer("❌ Эта команда работает только в группах и супергруппах!")
return
# Проверяем есть ли ответ на сообщение
if message.reply_to_message:
# Кик по ответу на сообщение
target_user: User | None = message.reply_to_message.from_user
target_user_id: int = target_user.id
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
# Кикаем пользователя
success: bool = await _kick_user(target_user_id, target_username, message)
if success:
safe_username: str = escape(target_username)
await message.answer(
text=f"👢 Пользователь {safe_username} (ID: {target_user_id}) кикнут из чата!",
parse_mode=None # Отключаем разметку
)
else:
await message.answer("Не удалось кикнуть пользователя")
else:
# Кик по ID пользователя
command_parts: list[str] = message.text.split()
if len(command_parts) < 2:
await message.answer(
" Использование команды:\n"
"• Ответьте на сообщение пользователя командой /kick\n"
"• Или укажите ID: /kick <user_id>"
)
return
try:
target_user_id: int = int(command_parts[1])
success: bool = await _kick_user(target_user_id, f"ID{target_user_id}", message)
if success:
await message.answer(
text=f"👢 Пользователь (ID: {target_user_id}) кикнут из чата!",
parse_mode=None # Отключаем разметку
)
else:
await message.answer("❌ Пользователь не найден или не удалось кикнуть")
except ValueError:
await message.answer("❌ Неверный формат ID пользователя")
async def _kick_user(user_id: int, username: str, message: Message) -> bool:
"""
Внутренняя функция для кика пользователя из чата.
Args:
user_id: ID пользователя для кика
username: Имя пользователя для логов
message: Объект сообщения для контекста
Returns:
bool: Успешно ли кикнут пользователь
"""
try:
# Проверяем, что бот имеет права администратора в чате
bot_member = await bot.get_chat_member(message.chat.id, bot.id)
if not bot_member.can_restrict_members:
await message.answer("У меня нет прав для кика пользователей!")
return False
# Проверяем, что целевой пользователь не является администратором/владельцем
target_member = await bot.get_chat_member(message.chat.id, user_id)
if target_member.status in ["creator", "administrator"]:
await message.answer("❌ Нельзя кикнуть администратора или создателя чата!")
return False
# Проверяем, что отправитель команды имеет права администратора
admin_member = await bot.get_chat_member(message.chat.id, message.from_user.id)
if admin_member.status not in ["creator", "administrator"]:
await message.answer("У вас нет прав для кика пользователей!")
return False
# Кикаем пользователя из чата
await bot.ban_chat_member(
chat_id=message.chat.id,
user_id=user_id,
revoke_messages=False # Не удаляем сообщения пользователя
)
# Сразу разбаниваем, чтобы пользователь мог вернуться по приглашению
await bot.unban_chat_member(
chat_id=message.chat.id,
user_id=user_id
)
# Логируем действие
admin_username = message.from_user.username or message.from_user.full_name
logger.info(
f"👢 Админ @{admin_username} кикнул пользователя @{username} (ID: {user_id}) из чата {message.chat.title}")
return True
except Exception as e:
logger.error(f"❌ Ошибка при кике пользователя {user_id}: {e}")
await message.answer(f"❌ Ошибка при кике пользователя: {str(e)}")
return False
@router.message(Command("kick_ban", ignore_case=True), IsAdmin())
async def kick_ban_user_cmd(message: Message, state: FSMContext) -> None:
"""
Команда /kick_ban для кика пользователя с удалением сообщений.
Использование: /kick_ban <user_id> или ответ на сообщение пользователя + /kick_ban
"""
await status_clear(message=message, state=state)
# Проверяем, что команда используется в группе/супергруппе
if message.chat.type not in ["group", "supergroup"]:
await message.answer("❌ Эта команда работает только в группах и супергруппах!")
return
# Проверяем есть ли ответ на сообщение
if message.reply_to_message:
# Кик по ответу на сообщение
target_user: User | None = message.reply_to_message.from_user
target_user_id: int = target_user.id
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
# Кикаем пользователя с удалением сообщений
success: bool = await _kick_ban_user(target_user_id, target_username, message)
if success:
safe_username: str = escape(target_username)
await message.answer(
text=f"💥 Пользователь {safe_username} (ID: {target_user_id}) кикнут с удалением сообщений!",
parse_mode=None # Отключаем разметку
)
else:
await message.answer("Не удалось кикнуть пользователя")
else:
# Кик по ID пользователя
command_parts: list[str] = message.text.split()
if len(command_parts) < 2:
await message.answer(
" Использование команды:\n"
"• Ответьте на сообщение пользователя командой /kick_ban\n"
"• Или укажите ID: /kick_ban <user_id>"
)
return
try:
target_user_id: int = int(command_parts[1])
success: bool = await _kick_ban_user(target_user_id, f"ID{target_user_id}", message)
if success:
await message.answer(
text=f"💥 Пользователь (ID: {target_user_id}) кикнут с удалением сообщений!",
parse_mode=None # Отключаем разметку
)
else:
await message.answer("❌ Пользователь не найден или не удалось кикнуть")
except ValueError:
await message.answer("❌ Неверный формат ID пользователя")
async def _kick_ban_user(user_id: int, username: str, message: Message) -> bool:
"""
Внутренняя функция для кика пользователя с удалением сообщений.
Args:
user_id: ID пользователя для кика
username: Имя пользователя для логов
message: Объект сообщения для контекста
Returns:
bool: Успешно ли кикнут пользователь
"""
try:
# Проверяем, что бот имеет права администратора в чате
bot_member = await bot.get_chat_member(message.chat.id, bot.id)
if not bot_member.can_restrict_members:
await message.answer("У меня нет прав для кика пользователей!")
return False
# Проверяем, что целевой пользователь не является администратором/владельцем
target_member = await bot.get_chat_member(message.chat.id, user_id)
if target_member.status in ["creator", "administrator"]:
await message.answer("❌ Нельзя кикнуть администратора или создателя чата!")
return False
# Проверяем, что отправитель команды имеет права администратора
admin_member = await bot.get_chat_member(message.chat.id, message.from_user.id)
if admin_member.status not in ["creator", "administrator"]:
await message.answer("У вас нет прав для кика пользователей!")
return False
# Кикаем пользователя из чата с удалением сообщений
await bot.ban_chat_member(
chat_id=message.chat.id,
user_id=user_id,
revoke_messages=True # Удаляем сообщения пользователя
)
# Сразу разбаниваем, чтобы пользователь мог вернуться по приглашению
await bot.unban_chat_member(
chat_id=message.chat.id,
user_id=user_id
)
# Логируем действие
admin_username = message.from_user.username or message.from_user.full_name
logger.info(
f"💥 Админ @{admin_username} кикнул пользователя @{username} (ID: {user_id}) из чата {message.chat.title} с удалением сообщений")
return True
except Exception as e:
logger.error(f"❌ Ошибка при кике пользователя {user_id} с удалением сообщений: {e}")
await message.answer(f"❌ Ошибка при кике пользователя: {str(e)}")
return False
@router.message(Command("kick_list", ignore_case=True), IsAdmin())
async def kick_help_cmd(message: Message, state: FSMContext) -> None:
"""
Команда /kick_list для показа справки по командам кика.
"""
await status_clear(message=message, state=state)
help_text = """
🤖 **Команды модерации:**
**👢 /kick** - Кикнуть пользователя (может вернуться по приглашению)
• Ответьте на сообщение пользователя с командой /kick
• Или используйте: /kick <user_id>
**💥 /kick_ban** - Кикнуть пользователя с удалением сообщений
• Ответьте на сообщение пользователя с командой /kick_ban
• Или используйте: /kick_ban <user_id>
**🚫 /ban** - Полностью забанить пользователя
**🔓 /unban** - Разбанить пользователя
**📋 /banned_list** - Список забаненных
⚠️ *Команды работают только в группах и требуют прав администратора*
"""
await message.answer(help_text, parse_mode=None)

View File

View File

@@ -0,0 +1,55 @@
from asyncio import create_task
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from bot.core.bots import BotInfo, bot
from bot.filters import IsOwner
from bot.templates import msg
from bot.utils import status_clear
from bot.utils.auto_delete import auto_delete_message
from configs import COMMANDS
__all__ = ("router",)
CMD: str = "pin".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
async def pin_cmd(message: Message, state: FSMContext) -> None:
"""
Обработчик команды /pin для закрепления последнего сообщения или ответа.
"""
await status_clear(message=message, state=state)
# Если есть reply → закрепляем его, иначе закрепляем саму команду
target = message.reply_to_message or message
await bot.pin_chat_message(chat_id=message.chat.id,
message_id=target.message_id,
disable_notification=False)
# Автоудаление через 7 суток
create_task(auto_delete_message(chat_id=message.chat.id,
message_id=target.message_id,
delay=604800))
await msg(message=message, text="✅ Сообщение успешно закреплено")
@router.callback_query(F.data.casefold().isin(COMMANDS[CMD]), IsOwner())
async def pin_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик кнопки с callback_data="pin".
"""
await status_clear(message=callback.message, state=state)
await bot.pin_chat_message(chat_id=callback.message.chat.id,
message_id=callback.message.message_id,
disable_notification=False)
create_task(auto_delete_message(chat_id=callback.message.chat.id,
message_id=callback.message.message_id,
delay=604800))
await callback.answer("✅ Сообщение закреплено")

View File

@@ -0,0 +1,51 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.utils.i18n import gettext as _
from bot.templates import msg_photo
from bot.utils.interesting_facts import interesting_fact
from bot.core.bots import BotInfo
from configs import COMMANDS, RpValue
# Настройки экспорта и роутера
__all__ = ("router",)
CMD: str = "settings".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
@router.callback_query(F.data.lower() == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
"""Обработчик команды /start"""
await state.clear()
# Создание инлайн-клавиатуры
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Инфо-канал🗂", url=RpValue.INFO_URL))
ikb.row(InlineKeyboardButton(text="Вступление🚀", callback_data='new'),
InlineKeyboardButton(text="Анкета📖", callback_data='anketa'))
ikb.row(InlineKeyboardButton(text="Связь с администрацией🌐", callback_data='admin'))
# Формируем приветственное сообщение
text: str = _(
"""Добро пожаловать, <a href="{url}">{name}</a>!
Я ваш искусственный помощник по ролевой - <b>{rp_name}</b>!
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
Интересный факт:
<blockquote>{fact}</blockquote>
"""
).format(
url=message.from_user.url if message.from_user else "",
name=message.from_user.first_name if message.from_user else "пользователь",
rp_name=RpValue.RP_NAME,
fact=interesting_fact(),
)
# Отправляем сообщение
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)

View File

View File

@@ -0,0 +1,19 @@
from aiogram import Router
from .set_description_cmd import router as set_description_cmd_router
from .set_name_cmd import router as set_name_cmd_router
from .set_widget_cmd import router as set_widget_cmd_router
from .settings_cmd import router as settings_cmd_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
settings_cmd_router,
set_name_cmd_router,
set_description_cmd_router,
set_widget_cmd_router,
)

View File

@@ -0,0 +1,167 @@
from aiogram import Router, F, Bot
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
from aiogram.filters import Command, CommandObject
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery
from aiogram.utils.i18n import gettext as _
from bot.core.bots import BotInfo
from bot.filters import IsOwner
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
from bot.templates import msg
from bot.utils import format_retry_time, status_clear
from configs import COMMANDS
from middleware.loggers import logger
__all__ = ("router",)
# Название команды
CMD: str = "set_description".lower()
# Роутер для обработки команды /set_description
router: Router = Router(name=f"{CMD}_cmd_router")
class SetBotDescriptionForm(StatesGroup):
"""Состояния FSM для изменения короткого описания бота."""
new_description: State = State()
async def handle_set_bot_description(
description: str,
message: Message | CallbackQuery,
state: FSMContext,
bot: Bot
) -> None:
"""
Установка короткого описания (short description) бота с обработкой FSM и ошибок API.
Args:
description (str): Новый текст описания (до 120 символов).
message (Message | CallbackQuery): Сообщение или callback-запрос.
state (FSMContext): Контекст FSM.
bot (Bot): Экземпляр бота.
"""
# Проверка ограничения Telegram
if len(description) > 120:
await msg(
message=message,
text=_("❌ Короткое описание бота должно быть не более 120 символов. Текущая длина: {length}").format(
length=len(description)
),
markup=settings_keyboard(),
)
return
try:
# Установка нового короткого описания
await bot.set_my_short_description(short_description=description)
# Сохраняем текущее значение в BotInfo
BotInfo.short_description = description
# Сбрасываем состояние FSM
await state.clear()
# Отправляем сообщение об успехе
await msg(
message=message,
text=_("✅ Короткое описание бота успешно изменено на: <b>{description}</b>").format(
description=description
),
markup=settings_keyboard(),
)
logger.info(f"Короткое описание бота изменено на: {description}")
except TelegramRetryAfter as e:
retry_text: str = format_retry_time(e.retry_after)
logger.warning(f"Превышен лимит запросов при смене short description. Попробуйте через {retry_text}")
await msg(
message=message,
text=_("⚠️ Слишком частая смена короткого описания!\nПопробуйте снова через: <b>{retry_text}</b>").format(
retry_text=retry_text
),
markup=settings_keyboard(),
)
except TelegramAPIError as e:
logger.error(f"Ошибка Telegram API при изменении короткого описания: {e}")
await msg(
message=message,
text=_("❌ Ошибка Telegram API при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
)
except Exception as e:
logger.error(f"Непредвиденная ошибка при изменении короткого описания: {e}")
await msg(
message=message,
text=_("❌ Непредвиденная ошибка при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
)
@router.callback_query(F.data.lower() == CMD, IsOwner())
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
async def settings_cmd(
message: Message | CallbackQuery,
state: FSMContext,
bot: Bot,
command: CommandObject | None = None
) -> None:
"""
Обработчик команды /set_description для короткого описания.
Поддерживает:
1. Немедленное изменение через аргумент (/set_description TEXT).
2. Callback-запрос.
3. FSM-ввод.
"""
current_description: str = BotInfo.description
# Вариант 1: если пользователь передал аргумент к команде
if command and command.args:
description: str = command.args.strip()
if len(description) > 120:
await msg(
message=message,
text=_("❌ Короткое описание не должно превышать 120 символов. Текущая длина: {length}").format(
length=len(description)
),
markup=settings_keyboard(),
)
return
await handle_set_bot_description(description, message, state, bot)
return
# Вариант 2: без аргумента → включаем FSM
await status_clear(message=message, state=state)
text: str = _(
"📝 <b>Смена короткого описания бота</b>\n\n"
"Текущее короткое описание: <i>{current}</i>\n\n"
"Введите новое короткое описание (максимум 120 символов):"
).format(current=current_description)
await msg(message=message, text=text, markup=settings_keyboard())
await state.set_state(SetBotDescriptionForm.new_description)
@router.message(SetBotDescriptionForm.new_description, IsOwner())
async def process_new_bot_description(
message: Message,
state: FSMContext,
bot: Bot
) -> None:
"""
Обработка ввода нового короткого описания через FSM.
"""
description: str = message.text.strip()
if not description:
await message.answer(_("❌ Пожалуйста, введите корректное короткое описание."))
return
await handle_set_bot_description(description, message, state, bot)

View File

@@ -0,0 +1,151 @@
from aiogram import Router, F, Bot
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
from aiogram.filters import Command, CommandObject
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery
from aiogram.utils.i18n import gettext as _
from bot.core.bots import BotInfo
from bot.filters import IsOwner
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
from bot.templates import msg
from configs import COMMANDS
from middleware.loggers import logger
__all__ = ("router",)
CMD: str = "set_name".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
class SetNameForm(StatesGroup):
new_name: State = State()
def format_retry_time(retry_after: int) -> str:
"""Форматирование времени повторной попытки в читаемом виде"""
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} секунд"
async def handle_set_name(
new_name: str,
message: Message | CallbackQuery,
state: FSMContext,
bot: Bot
) -> None:
"""
Установка имени бота с проверкой длины, обработкой перегрузки и логированием
"""
if len(new_name) > 64:
await msg(
message=message,
text=_("❌ Имя бота должно быть не более 64 символов. Текущая длина: {length}").format(
length=len(new_name)
),
markup=settings_keyboard(),
)
return
try:
await bot.set_my_name(new_name)
BotInfo.first_name = new_name
await state.clear()
await msg(
message=message,
text=_("✅ Имя бота успешно изменено на: <b>{new_name}</b>").format(new_name=new_name),
markup=settings_keyboard(),
)
logger.info(f"Имя бота изменено на: {new_name}")
except TelegramRetryAfter as e:
retry_text: str = format_retry_time(e.retry_after)
logger.warning(f"Превышен контроль перегрузки при смене имени. Попробуйте через {retry_text}")
await msg(
message=message,
text=_("⚠️ Слишком частая смена имени!\nПопробуйте снова через: <b>{retry_text}</b>").format(
retry_text=retry_text
),
markup=settings_keyboard(),
)
except TelegramAPIError as e:
logger.error(f"Ошибка Telegram API при изменении имени: {e}")
await msg(
message=message,
text=_("❌ Ошибка Telegram API: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
)
except Exception as e:
logger.error(f"Непредвиденная ошибка при изменении имени: {e}")
await msg(
message=message,
text=_("❌ Непредвиденная ошибка: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
)
@router.callback_query(F.data.lower() == CMD, IsOwner())
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
async def settings_cmd(
message: Message | CallbackQuery,
state: FSMContext,
bot: Bot,
command: CommandObject | None = None
):
"""
Обработчик команды /set_name с поддержкой:
1. Immediate установки через аргумент команды
2. Callback query
3. FSM ввод
"""
current_name = getattr(BotInfo, "first_name", "") or _("Не установлено")
# Immediate установка через аргумент команды
if command and command.args:
new_name = command.args.strip()
if len(new_name) > 64:
await msg(
message=message,
text=_("❌ Имя не должно превышать 64 символа. Текущая длина: {length}").format(
length=len(new_name)
),
markup=settings_keyboard(),
)
return
await handle_set_name(new_name, message, state, bot)
return
# Для callback query или пустой команды — показываем текущее имя и запускаем FSM
await state.clear()
if isinstance(message, CallbackQuery):
await message.answer()
text: str = _(
"🤖 <b>Смена имени бота</b>\n\n"
"Текущее имя: <i>{current}</i>\n\n"
"Пожалуйста, введите новое имя для бота (максимум 64 символа):"
).format(current=current_name)
await msg(message=message, text=text, markup=settings_keyboard())
await state.set_state(SetNameForm.new_name)
@router.message(SetNameForm.new_name, IsOwner())
async def process_new_name(message: Message, state: FSMContext, bot: Bot):
"""
Обработка ввода нового имени через FSM
"""
new_name: str = message.text.strip()
if not new_name:
await message.answer(_("❌ Пожалуйста, введите корректное имя."))
return
await handle_set_name(new_name, message, state, bot)

View File

@@ -0,0 +1,168 @@
from aiogram import Router, F, Bot
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
from aiogram.filters import Command, CommandObject
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery
from aiogram.utils.i18n import gettext as _
from bot.core.bots import BotInfo
from bot.filters import IsOwner
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
from bot.templates import msg
from bot.utils import format_retry_time, status_clear
from configs import COMMANDS
from middleware.loggers import logger
__all__ = ("router",)
CMD: str = "set_widget".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
class SetWidgetForm(StatesGroup):
"""Состояния FSM для изменения виджета (описания бота)."""
new_widget: State = State()
async def handle_set_widget(
new_widget: str,
message: Message | CallbackQuery,
state: FSMContext,
bot: Bot
) -> None:
"""
Устанавливает новое значение виджета (описания бота).
Args:
new_widget (str): Новый текст виджета.
message (Message | CallbackQuery): Объект сообщения или callback-запроса.
state (FSMContext): Контекст состояния FSM.
bot (Bot): Экземпляр текущего бота.
"""
# Проверка длины текста (Telegram API ограничивает description до 512 символов)
if len(new_widget) > 512:
await msg(
message=message,
text=_("❌ Виджет бота должен быть не более 512 символов. Текущая длина: {length}").format(
length=len(new_widget)
),
markup=settings_keyboard(),
)
return
try:
# Устанавливаем описание через Telegram API
await bot.set_my_description(description=new_widget)
# Сохраняем в BotInfo для локального использования
BotInfo.widget = new_widget
# Очищаем состояние FSM
await state.clear()
# Отправляем уведомление пользователю
await msg(
message=message,
text=_("✅ Виджет бота успешно изменён на: <b>{new_widget}</b>").format(
new_widget=new_widget
),
markup=settings_keyboard(),
)
logger.info(f"Виджет бота изменён на: {new_widget}")
except TelegramRetryAfter as e:
# Если запрос слишком частый
retry_text: str = format_retry_time(e.retry_after)
logger.warning(f"Превышен лимит запросов при смене виджета. Попробуйте через {retry_text}")
await msg(
message=message,
text=_("⚠️ Слишком частая смена виджета!\nПопробуйте снова через: <b>{retry_text}</b>").format(
retry_text=retry_text
),
markup=settings_keyboard(),
)
except TelegramAPIError as e:
# Ошибка Telegram API
logger.error(f"Ошибка Telegram API при изменении виджета: {e}")
await msg(
message=message,
text=_("❌ Ошибка Telegram API при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
)
except Exception as e:
# Непредвиденная ошибка
logger.error(f"Непредвиденная ошибка при изменении виджета: {e}")
await msg(
message=message,
text=_("❌ Непредвиденная ошибка при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
)
@router.callback_query(F.data.lower() == CMD, IsOwner())
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
async def settings_cmd(
message: Message | CallbackQuery,
state: FSMContext,
bot: Bot,
command: CommandObject | None = None
) -> None:
"""
Обработчик команды /set_widget.
Поддерживает:
1. Немедленное изменение через аргумент команды (/set_widget TEXT).
2. Callback-запрос.
3. FSM ввод.
"""
# Получаем текущее значение виджета
current_widget: str = BotInfo.widget
# Вариант 1: пользователь ввёл аргумент сразу (/set_widget TEXT)
if command and command.args:
new_widget: str = command.args.strip()
if len(new_widget) > 512:
await msg(
message=message,
text=_("❌ Виджет не должен превышать 512 символов. Текущая длина: {length}").format(
length=len(new_widget)
),
markup=settings_keyboard(),
)
return
await handle_set_widget(new_widget, message, state, bot)
return
# Вариант 2: Callback query или пустая команда → запускаем FSM
await status_clear(message=message, state=state)
text: str = _(
"📝 <b>Смена виджета бота</b>\n\n"
"Текущий виджет: <i>{current}</i>\n\n"
"Пожалуйста, введите новый виджет для бота (максимум 512 символов):"
).format(current=current_widget)
await msg(message=message, text=text, markup=settings_keyboard())
await state.set_state(SetWidgetForm.new_widget)
@router.message(SetWidgetForm.new_widget, IsOwner())
async def process_new_widget(
message: Message,
state: FSMContext,
bot: Bot
) -> None:
"""
Обрабатывает ввод нового текста виджета через FSM.
"""
new_widget: str = message.text.strip()
# Проверяем, что пользователь что-то ввёл
if not new_widget:
await message.answer(_("❌ Пожалуйста, введите корректный виджет."))
return
await handle_set_widget(new_widget, message, state, bot)

View File

@@ -0,0 +1,48 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.i18n import gettext as _
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.core.bots import BotInfo
from bot.filters import IsOwner
from bot.templates import msg
from bot.utils import status_clear
from configs import COMMANDS
# Настройки экспорта и роутера
__all__ = ("router", "settings_keyboard",)
CMD: str = "settings".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
def settings_keyboard() -> InlineKeyboardBuilder:
"""Клавиатура настроек"""
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="🔙 Вернуться", callback_data="settings"))
return ikb
@router.callback_query(F.data.lower() == CMD, IsOwner())
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
async def settings_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
"""Обработчик команды /settings"""
await status_clear(message=message, state=state)
# Создание инлайн-клавиатуры
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Имя бота⚜️", callback_data='set_name'))
ikb.row(InlineKeyboardButton(text="Описание бота📝", callback_data='set_description'))
ikb.row(InlineKeyboardButton(text="Виджет🧩", callback_data='set_widget'))
ikb.row(InlineKeyboardButton(text="Назад◀️", callback_data='menu'))
# Формируем приветственное сообщение
text: str = _("""
⚙️ Настройки
"""
).format(
)
# Отправляем сообщение
await msg(message=message, text=text, markup=ikb)

View File

@@ -0,0 +1,9 @@
from aiogram import Router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
# router.include_routers(
# )

View File

@@ -0,0 +1,22 @@
from aiogram import Router
#from .active import router as active_cmd_router
from .start_cmd import router as start_cmd_router
#from .union_cmd import router as union_cmd_router
from .new_cmd import router as new_cmd_router
#from .create_cmd import router as create_cmd_router
#from .anon import router as anon_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
start_cmd_router,
#active_cmd_router,
#union_cmd_router,
new_cmd_router,
#create_cmd_router,
#anon_router,
)

View File

@@ -0,0 +1,42 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from bot.core.bots import BotInfo
from bot.templates import msg_photo
from bot.utils import status_clear
from configs import COMMANDS
from database import db
# Настройки экспорта и роутера
__all__ = ("router",)
CMD: str = "active".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
@router.callback_query(F.data.lower() == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
async def active_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
"""Обработчик команды /active"""
await status_clear(message=message, state=state)
# Получить статистику сообщений пользователя
day, week, month, total = await db.get_message_stats(message.from_user.id)
print(f"За день: {day} сообщений")
print(f"За неделю: {week} сообщений")
print(f"За месяц: {month} сообщений")
print(f"Всего: {total} сообщений")
# Формируем приветственное сообщение
text: str = f"""
За день: {day} сообщений
За неделю: {week} сообщений
За месяц: {month} сообщений
Всего: {total} сообщений
"""
# Отправляем сообщение
await msg_photo(message=message, text=text, )

View File

@@ -0,0 +1,117 @@
from typing import Dict, Tuple
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.fsm.context import FSMContext
from bot.utils import status_clear
# -------------------
# Router
# -------------------
router: Router = Router(name="anon_router")
# -------------------
# Конфигурация
# -------------------
# CHAT_ID в формате "-100000_29" -> chat_id + thread_id
CHAT_ID: str = "-1003098225669_724"
def parse_chat_id(chat_id_str: str) -> Tuple[int, int]:
chat_str, thread_str = chat_id_str.split("_")
return int(chat_str), int(thread_str)
ADMIN_CHAT_ID, ADMIN_THREAD_ID = parse_chat_id(CHAT_ID)
# -------------------
# FSM состояния
# -------------------
class AnonStates:
USER_WAITING_TEXT = "user_waiting_text"
ADMIN_WAITING_REPLY = "admin_waiting_reply"
# -------------------
# Словари для отслеживания сообщений
# -------------------
# user_id -> message_id в админском топике
user_to_admin_map: Dict[int, int] = {}
# admin_message_id -> user_id
admin_to_user_map: Dict[int, int] = {}
# -------------------
# Команда /anon или callback
# -------------------
@router.callback_query(F.data.casefold() == "anon")
@router.message(Command("anon"))
async def anon_start(message: Message | CallbackQuery, state: FSMContext) -> None:
"""Начало анонимного сообщения. Ждём текст пользователя."""
await status_clear(message=message, state=state)
await state.clear()
await state.set_state(AnonStates.USER_WAITING_TEXT)
text = "Напишите сообщение, которое вы хотите отправить анонимно администраторам."
if isinstance(message, Message):
await message.reply(text)
else:
await message.message.answer(text)
# -------------------
# Получение текста от пользователя
# -------------------
@router.message(F.text, F.state == AnonStates.USER_WAITING_TEXT)
async def anon_send_text(message: Message, state: FSMContext) -> None:
"""Пересылает текст пользователя в админский топик анонимно."""
anon_text = message.text.strip()
if not anon_text:
await message.reply("Сообщение не может быть пустым. Попробуйте снова.")
return
forwarded_text = f"Сообщение от [пользователя](tg://user?id={message.from_user.id}):\n{anon_text}"
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Ответить", callback_data=f"anon_reply:{message.from_user.id}")]
]
)
sent_msg = await message.bot.send_message(
chat_id=ADMIN_CHAT_ID,
message_thread_id=ADMIN_THREAD_ID,
text=forwarded_text,
parse_mode="Markdown",
reply_markup=keyboard
)
user_to_admin_map[message.from_user.id] = sent_msg.message_id
admin_to_user_map[sent_msg.message_id] = message.from_user.id
await message.reply("Ваше сообщение отправлено анонимно администраторам.")
await state.clear()
# -------------------
# Кнопка "Ответить" админа
# -------------------
@router.callback_query(F.data.startswith("anon_reply:"))
async def anon_admin_reply(callback: CallbackQuery, state: FSMContext) -> None:
"""Начинаем сессию ответа админа пользователю."""
user_id = int(callback.data.split(":")[1])
await state.set_state(AnonStates.ADMIN_WAITING_REPLY)
await state.update_data(reply_to_user=user_id)
await callback.message.answer(f"Введите ответ для пользователя [id={user_id}]:")
await callback.answer()
# -------------------
# Текст ответа админа
# -------------------
@router.message(F.text, F.state == AnonStates.ADMIN_WAITING_REPLY)
async def anon_send_admin_text(message: Message, state: FSMContext) -> None:
"""Пересылает текст админа пользователю."""
data = await state.get_data()
reply_to_user = data.get("reply_to_user")
if reply_to_user:
await message.bot.send_message(
chat_id=reply_to_user,
text=f"Ответ администратора:\n{message.text}"
)
await message.reply("Сообщение отправлено пользователю.")
await state.clear()

View File

@@ -0,0 +1,27 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from bot import BotInfo
from bot.utils import status_clear
from configs import COMMANDS
from middleware.loggers import logger
__all__ = ("router",)
CMD: str = "cancel".casefold()
router: Router = Router(name=f"{CMD}_cmd_router")
@router.callback_query(F.data.casefold() == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
@router.message(F.text.casefold().in_(COMMANDS[CMD]))
async def cancel_handler(message: Message, state: FSMContext, text: str = "❌ Отмена предыдущего действия!"):
"""
Позволяет пользователю отменить процесс смены описания
"""
await status_clear(message=message, state=state)
logger.info(text=text)
await message.answer(text)

View File

@@ -0,0 +1,49 @@
# bot/handlers/commands/create_cmd.py
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from bot.core import BotInfo
from bot.states.anketa_states import StartForm
from bot.templates import msg_photo
from middleware import log
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name="create_cmd_router")
@router.callback_query(F.data == "create")
@router.message(Command('create','скуфеу', 'анкета', prefix=BotInfo.prefix, ignore_case=True))
@log(level='INFO', log_type='Start', text="использовал(а) команду /create")
async def create_cmd(message: Message|CallbackQuery, state: FSMContext) -> None:
"""
Обработчик команды /create.
"""
# Сбросим все состояния (отменим создание поста, если оно было)
await state.clear()
# Создание инлайн-клавиатуры
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Правила❗️", url='https://teletype.in/@velli_arsaan/XxUiHcB4Puj'))
ikb.row(InlineKeyboardButton(text="Назад↪️", callback_data='start'))
# Создание базовых переменных сообщения
caption: str = f"""
Если вы хотели бы вступить в наш проект, то напоминаю, что вам сначала нужно ознакомиться с <b>инфо-каналом</b>! При продолжении диалога вы автоматически подтверждаете то, что прочитали все правила и в курсе, что мы ролевой проект, не флуд.
<blockquote>Чтобы вступить к вам мы просим вас заполнить небольшую анкету:
1. <i>Желаемая роль</i>;
2. <i>Кого бы вы хотели в соролы?</i>;
3. <i>Кодовая фраза из наших правил</i>;</blockquote>
[‼️] Оно состоит всего из 4 слов, которые разбросаны в верном порядке по статьям о правилах.
"""
# Установим состояние ожидания анкеты
await state.set_state(StartForm.waiting_for_application)
# Обработчик ответа на сообщение
await msg_photo(message=message, text=caption, file='assets/help.png', markup=ikb)

View File

@@ -0,0 +1,368 @@
from typing import Dict
from aiogram import Router, F
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, InlineKeyboardButton, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.core.bots import BotInfo
from bot.utils import status_clear
from configs import COMMANDS, ImportantID
from middleware.loggers import log
# user_id -> thread_id (топик пользователя)
user_topic_map: Dict[int, int] = {}
# message_id в топике -> user_id
topic_message_map: Dict[int, int] = {}
__all__ = ("router", "user_topic_map")
CMD: str = "new"
router: Router = Router(name=f"{CMD}_cmd_router")
STATE_WAITING_REQUEST = "waiting_request"
def has_active_topic(user_id: int) -> bool:
"""Проверяет, есть ли у пользователя активный топик"""
return user_id in user_topic_map
async def send_topic_message(user_id: int, text: str, reply_markup=None):
"""Отправляет сообщение в топик пользователя"""
thread_id = user_topic_map.get(user_id)
if not thread_id:
return False
try:
await BotInfo.bot.send_message(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
text=text,
parse_mode="HTML",
reply_markup=reply_markup
)
return True
except Exception as e:
log(level='ERROR', log_type='TOPIC_SEND', text=f"Ошибка отправки в топик: {e}")
return False
# ===================== Продолжение диалога =====================
@router.callback_query(F.data == "continue_dialog")
async def continue_dialog_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""Обработчик продолжения существующего диалога"""
user_id = callback.from_user.id
if not has_active_topic(user_id):
await callback.answer("❌ Активный диалог не найден", show_alert=True)
return
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
await callback.message.edit_text(
text="💬 У вас уже есть активный диалог с поддержкой. Просто отправьте ваше сообщение (не через reply) и оно будет переслано администратору.",
reply_markup=ikb.as_markup()
)
await callback.answer()
# ===================== Обработчик callback /new =====================
@router.callback_query(F.data.casefold() == CMD)
@log(level='INFO', log_type=f"{CMD.upper()}_CBD", text=f"использовал команду /{CMD} через кнопку")
async def new_cmd_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""Обработчик команды /new из callback кнопки"""
user_id = callback.from_user.id
# Проверяем, есть ли уже активный топик
if has_active_topic(user_id):
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
ikb.row(InlineKeyboardButton(text="Создать новый📝", callback_data='force_new'))
await callback.message.edit_text(
text="⚠️ У вас уже есть активный диалог с поддержкой.\n\n"
"• <b>Продолжить текущий</b> - чтобы писать в существующий диалог\n"
"• <b>Создать новый</b> - если хотите начать новый запрос (старый диалог будет архивирован)",
reply_markup=ikb.as_markup(),
parse_mode="HTML"
)
await callback.answer()
return
await status_clear(message=callback.message, state=state)
await state.set_state(STATE_WAITING_REQUEST)
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
try:
await callback.message.edit_text(
text="Отправьте свой запрос:",
reply_markup=ikb.as_markup()
)
except Exception:
await callback.message.answer(
text="Отправьте свой запрос:",
reply_markup=ikb.as_markup()
)
await callback.answer()
# ===================== Принудительное создание нового топика =====================
@router.callback_query(F.data == "force_new")
async def force_new_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""Принудительное создание нового топика (при наличии активного)"""
user_id = callback.from_user.id
# Уведомляем в старом топике о создании нового
if has_active_topic(user_id):
await send_topic_message(
user_id,
f"🔔 <b>Пользователь начал новый запрос</b>\n"
f"Старый топик будет архивирован."
)
# Не удаляем старый топик из мапы сразу - он перезапишется при создании нового
await status_clear(message=callback.message, state=state)
await state.set_state(STATE_WAITING_REQUEST)
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
await callback.message.edit_text(
text="📝 <b>Создание нового запроса</b>\n\nОтправьте ваш запрос:",
reply_markup=ikb.as_markup(),
parse_mode="HTML"
)
await callback.answer()
# ===================== Обработчик сообщения /new =====================
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
async def new_cmd_message(message: Message, state: FSMContext) -> None:
"""Обработчик команды /new из текстового сообщения"""
user_id = message.from_user.id
# Проверяем, есть ли уже активный топик
if has_active_topic(user_id):
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
await message.answer(
text="⚠️ У вас уже есть активный диалог с поддержкой.\n\n"
"Используйте кнопку ниже чтобы продолжить общение в существующем диалоге.",
reply_markup=ikb.as_markup(),
parse_mode="HTML"
)
return
await status_clear(message=message, state=state)
await state.set_state(STATE_WAITING_REQUEST)
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
await message.answer(
text="Отправьте свой запрос:",
reply_markup=ikb.as_markup()
)
# ===================== Создание топика и отправка запроса =====================
@router.message(StateFilter(STATE_WAITING_REQUEST))
async def process_request(message: Message, state: FSMContext) -> None:
"""Создание топика и отправка запроса пользователя"""
text = message.text.strip()
if not text:
await message.reply("⚠️ Пожалуйста, отправьте непустое сообщение.")
return
user = message.from_user
try:
# Создаем новый топик для пользователя
topic_name = f"👤 {user.full_name} (ID: {user.id})"
topic_result = await message.bot.create_forum_topic(
chat_id=ImportantID.SUPPORT_CHAT_ID,
name=topic_name
)
thread_id = topic_result.message_thread_id
# Отправляем сообщение пользователя в новый топик
formatted_text = f"<b>📩 Сообщение от <a href='tg://user?id={user.id}'>{user.full_name}</a>:</b>\n{text}"
sent_msg = await message.bot.send_message(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
text=formatted_text,
parse_mode="HTML"
)
# Отправляем сообщение с уведомлением (со звуком)
await message.bot.send_message(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
text="🔔 <b>Новый запрос создан</b>\nАдминистратор уведомлен.",
parse_mode="HTML"
)
# Сохраняем связь пользователя и топика
user_topic_map[user.id] = thread_id
topic_message_map[sent_msg.message_id] = user.id
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Перейти к диалогу💬", callback_data='continue_dialog'))
ikb.row(InlineKeyboardButton(text="В меню↩️", callback_data='start'))
await message.answer(
text="✅ <b>Запрос отправлен!</b>\n\n"
"Администратор ответит в этом боте. Вы можете продолжить общение через меню.",
reply_markup=ikb.as_markup(),
parse_mode="HTML"
)
await state.clear()
except Exception as e:
await message.reply(f"⚠️ Не удалось создать запрос: {e}")
# ===================== Пересылка сообщений пользователя в топик =====================
@router.message(F.chat.type == "private", ~F.reply_to_message)
async def forward_user_to_admin(message: Message) -> None:
"""Пересылает сообщения пользователя в топик (если есть активный диалог)"""
if message.from_user.is_bot:
return
user_id = message.from_user.id
# Проверяем, есть ли активный топик
if not has_active_topic(user_id):
return # Нет активного топика - игнорируем
# Получаем топик пользователя
thread_id = user_topic_map.get(user_id)
if not thread_id:
return
try:
# Отправляем сообщение пользователя в топик
if message.text:
formatted_text = f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}"
sent_msg = await message.bot.send_message(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
text=formatted_text,
parse_mode="HTML"
)
topic_message_map[sent_msg.message_id] = user_id
elif message.photo:
caption = f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}" if message.caption else f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>"
sent_msg = await message.bot.send_photo(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
photo=message.photo[-1].file_id,
caption=caption,
parse_mode="HTML"
)
topic_message_map[sent_msg.message_id] = user_id
await message.answer("✅ Сообщение отправлено администратору")
except Exception as e:
await message.answer(f"⚠️ Не удалось отправить сообщение: {e}")
# ===================== Пересылка ответов админа пользователю =====================
@router.message(F.chat.id == ImportantID.SUPPORT_CHAT_ID, F.message_thread_id)
async def forward_admin_to_user(message: Message) -> None:
"""Пересылает сообщения админа из топика пользователю"""
if message.from_user.is_bot:
return
thread_id = message.message_thread_id
# Ищем пользователя по thread_id топика
user_id = None
for uid, tid in user_topic_map.items():
if tid == thread_id:
user_id = uid
break
if not user_id:
return # Не наш топик
try:
# Пересылаем сообщение админа пользователю
if message.text:
text = f"<b>👨‍💼 Ответ администратора:</b>\n{message.html_text}"
sent_msg = await message.bot.send_message(
chat_id=user_id,
text=text,
parse_mode="HTML"
)
# Сохраняем связь для возможного ответа пользователя
topic_message_map[sent_msg.message_id] = user_id
elif message.photo:
caption = f"<b>👨‍💼 Ответ администратора:</b>\n{message.html_text}" if message.caption else "<b>👨‍💼 Ответ администратора:</b>"
await message.bot.send_photo(
chat_id=user_id,
photo=message.photo[-1].file_id,
caption=caption,
parse_mode="HTML"
)
except Exception as e:
log(level='ERROR', log_type='FORWARD', text=f"Ошибка пересылки админ->пользователь: {e}")
# ===================== Пересылка ответов пользователя в топик =====================
@router.message(F.chat.type == "private", F.reply_to_message)
async def forward_user_reply_to_admin(message: Message) -> None:
"""Пересылает ответы пользователя (reply) в топик"""
if message.from_user.is_bot:
return
user_id = message.from_user.id
reply_to_id = message.reply_to_message.message_id
# Проверяем, является ли это ответом на сообщение из топика
original_user_id = topic_message_map.get(reply_to_id)
if not original_user_id or original_user_id != user_id:
return
# Получаем топик пользователя
thread_id = user_topic_map.get(user_id)
if not thread_id:
await message.reply("⚠️ Не найден активный диалог. Используйте /new для нового запроса.")
return
try:
# Отправляем ответ пользователя в топик
if message.text:
formatted_text = f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}"
sent_msg = await message.bot.send_message(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
text=formatted_text,
parse_mode="HTML"
)
topic_message_map[sent_msg.message_id] = user_id
elif message.photo:
caption = f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}" if message.caption else f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>"
sent_msg = await message.bot.send_photo(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
photo=message.photo[-1].file_id,
caption=caption,
parse_mode="HTML"
)
topic_message_map[sent_msg.message_id] = user_id
await message.reply("✅ Ответ отправлен администратору.")
except Exception as e:
await message.reply(f"⚠️ Не удалось отправить ответ: {e}")

View File

@@ -0,0 +1,68 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.i18n import gettext as _
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.core.bots import BotInfo
from bot.templates import msg_photo
from configs import COMMANDS, RpValue
from .new_cmd import user_topic_map # Импортируем мапу топиков из модуля new
__all__ = ("router",)
CMD: str = "start".casefold()
router: Router = Router(name=f"{CMD}_cmd_router")
@router.callback_query(F.data.casefold() == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
async def start_cmd(update: Message | CallbackQuery, state: FSMContext) -> None:
"""Обработчик команды /start"""
# Определяем тип update
if isinstance(update, CallbackQuery):
message = update.message
callback = update
else:
message = update
callback = None
# Проверяем, есть ли у пользователя активный топик
user_id = update.from_user.id
has_active_topic = user_id in user_topic_map
# Создание инлайн-клавиатуры
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Википедия🌐", url="https://t.me/PrimoWiki"))
if has_active_topic:
# Если есть активный топик, показываем кнопку "Продолжить диалог"
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
else:
# Если нет активного топика, показываем кнопку "Связаться"
ikb.row(InlineKeyboardButton(text="Связаться👀", callback_data='new'))
# Формируем приветственное сообщение
text: str = _(
"""Добро пожаловать, <a href="{url}">{name}</a>!
Я ваш помощник по проекту — <b>PrimoWiki</b>!
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
"""
).format(
url=update.from_user.url,
name=update.from_user.first_name,
rp_name=RpValue.RP_NAME,
)
# Добавляем информацию об активном диалоге, если есть
if has_active_topic:
text += "\n\n💬 <b>У вас есть активный диалог с поддержкой!</b>"
# Отправляем сообщение
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
if callback:
await callback.answer()

View File

@@ -0,0 +1,58 @@
# bot/handlers/commands/union_cmd.py
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from bot.core import BotInfo
from bot.states.union_states import UnionStates
from bot.templates import msg
from middleware import log
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name="union_cmd_router")
@router.callback_query(F.data == "union")
@router.message(Command('union','гтшщт', 'союз', prefix=BotInfo.prefix, ignore_case=True))
@log(level='INFO', log_type='Start', text="использовал(а) команду /union")
async def create_cmd(message: Message|CallbackQuery, state: FSMContext) -> None:
"""
Обработчик команды /union.
"""
# Сбросим все состояния (отменим создание поста, если оно было)
#await state.clear()
# Создание инлайн-клавиатуры
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Правила❗️", url='https://teletype.in/@velli_arsaan/XxUiHcB4Puj'))
ikb.row(InlineKeyboardButton(text="Назад↪️", callback_data='start'))
# Создание базовых переменных сообщения
caption: str = f"""
Приветствуем! Это бот для связи по вопросам союзов проекта ˚₊· ‌‌‌‌➳ 𝑆𝑦𝑠𝑡𝑒𝑚 𝑅𝑒𝑠𝑒𝑡 ·₊˚.
Задайте свой вопрос, и мы постараемся ответить вам в ближайшее время — в некотором случае можем попроосить вас дать юз/ссылку на ваш проект.
Предложение о заключении союзов должно выглядеть вот так:
Название
Юз и ссылка на инфо
Юз и ссылка на лайф
Условия союзов
Юзер следящего с вашей стороны
Желаемый следящий с нашей стороны (мы будем в праве поставить вам другого, но тот, которого вы назовёте, будет в приоритете)
Кодовое предложение из условий союзов. Оно состоит из 4 слов, которые расположены в верном порядке в статье о наших условиях сотрудничества.
Имейте ввиду, что мы можем отказаться от союза без объяснения причин!
"""
# Обработчик ответа на сообщение
await msg(message=message, text=caption, markup=ikb)
# Установим состояние ожидания анкеты
await state.set_state(UnionStates.waiting_for_union)

View File

@@ -0,0 +1,12 @@
# bot/handlers/__init__.py
from aiogram import Router
from .econom import router as economy_router
# Настройка экспорта и роутера
__all__ = ('router',)
router: Router = Router(name=__name__)
# Подготовка мастер-роутера
router.include_routers(
economy_router,
)

View File

@@ -0,0 +1,286 @@
# modules/economy.py
import json
from pathlib import Path
from typing import Dict, Optional, Tuple, List
import aiofiles
from aiogram import Router
from aiogram.filters import Command, CommandObject
from aiogram.types import Message, User
from aiogram.utils.markdown import hbold
from bot.filters import IsOwner
# ==================== Конфигурация ====================
ECONOMY_FILE = Path("data/economy.json")
ECONOMY_FILE.parent.mkdir(parents=True, exist_ok=True)
CURRENCY_NAME = "коинов"
# ==================== Хранилище ====================
class Economy:
def __init__(self):
self.data: Dict[int, dict] = {} # user_id → {balance, username, full_name}
self.username_to_id: Dict[str, int] = {} # username.lower() → user_id
async def load(self):
if not ECONOMY_FILE.exists():
return
try:
async with aiofiles.open(ECONOMY_FILE, "r", encoding="utf-8") as f:
content = await f.read()
if not content.strip():
return
raw = json.loads(content)
self.data = {int(uid): info for uid, info in raw.items()}
self.username_to_id = {}
for uid, info in self.data.items():
username = info.get("username")
if username:
self.username_to_id[username.lower()] = uid
except Exception as e:
print(f"[Economy] Load error: {e}")
async def save(self):
try:
async with aiofiles.open(ECONOMY_FILE, "w", encoding="utf-8") as f:
await f.write(json.dumps(self.data, indent=2, ensure_ascii=False))
except Exception as e:
print(f"[Economy] Save error: {e}")
async def ensure_user(self, user_id: int, username: Optional[str] = None, full_name: Optional[str] = None):
"""Создаёт пользователя с 0 балансом, если его нет"""
if user_id not in self.data:
self.data[user_id] = {
"balance": 50,
"username": username,
"full_name": full_name or "Unknown User"
}
if username:
self.username_to_id[username.lower()] = user_id
await self.save()
# Обновляем данные, если изменились
updated = False
if username and self.data[user_id]["username"] != username:
old = self.data[user_id]["username"]
if old and old.lower() in self.username_to_id:
del self.username_to_id[old.lower()]
self.data[user_id]["username"] = username
self.username_to_id[username.lower()] = user_id
updated = True
if full_name and self.data[user_id]["full_name"] != full_name:
self.data[user_id]["full_name"] = full_name
updated = True
if updated:
await self.save()
async def get_balance(self, user: User) -> int:
await self.ensure_user(user.id, user.username, user.full_name)
return self.data[user.id]["balance"]
async def modify_balance(self, user_id: int, delta: int, username: Optional[str] = None, full_name: Optional[str] = None) -> int:
await self.ensure_user(user_id, username, full_name)
self.data[user_id]["balance"] += delta
await self.save()
return self.data[user_id]["balance"]
async def set_balance(self, user_id: int, amount: int, username: Optional[str] = None, full_name: Optional[str] = None):
await self.ensure_user(user_id, username, full_name)
self.data[user_id]["balance"] = amount
await self.save()
async def delete_user(self, user_id: int) -> bool:
if user_id in self.data:
username = self.data[user_id].get("username")
if username and username.lower() in self.username_to_id:
del self.username_to_id[username.lower()]
del self.data[user_id]
await self.save()
return True
return False
def resolve_id(self, username: str) -> Optional[int]:
return self.username_to_id.get(username.removeprefix("@").lower())
def get_top(self, limit: int = 20) -> List[Tuple[int, int, str, str]]:
"""Топ только с положительным балансом"""
items = []
for uid, info in self.data.items():
bal = info["balance"]
if bal <= -1000:
continue # ← НЕ ПОКАЗЫВАЕМ НУЛЕВЫЕ БАЛАНСЫ
items.append((
uid,
bal,
info.get("username") or "",
info.get("full_name") or f"User#{uid}"
))
return sorted(items, key=lambda x: x[1], reverse=True)[:limit]
economy = Economy()
router = Router(name="economy")
# ==================== Утилиты ====================
def fmt(num: int) -> str:
return f"{num:,}".replace(",", " ")
def user_mention(user: Optional[User] = None, username: str = "", full_name: str = "") -> str:
if user:
if user.username:
return f"@{user.username}"
return hbold(user.full_name or "Unknown")
if username:
return f"@{username}"
return hbold(full_name or "Unknown User")
# ==================== Функция для регистрации при любом сообщении ====================
async def register_user_on_message(message: Message):
"""Вызывай эту функцию в глобальном обработчике сообщений"""
if message.from_user:
await economy.ensure_user(
user_id=message.from_user.id,
username=message.from_user.username,
full_name=message.from_user.full_name
)
# ==================== Команды ====================
@router.message(Command("balance"))
async def cmd_balance(message: Message, command: CommandObject):
target = message.from_user
if message.reply_to_message and message.reply_to_message.from_user:
target = message.reply_to_message.from_user
elif command.args:
uid = economy.resolve_id(command.args.strip())
if uid:
info = economy.data[uid]
name = user_mention(username=info.get("username"), full_name=info.get("full_name"))
await message.answer(f"Баланс {name}: {hbold(fmt(info['balance']))} {CURRENCY_NAME}")
return
bal = await economy.get_balance(target)
await message.answer(f"Баланс {user_mention(target)}: {hbold(fmt(bal))} {CURRENCY_NAME}")
async def _get_target(message: Message, arg: Optional[str] = None):
if message.reply_to_message and message.reply_to_message.from_user:
return message.reply_to_message.from_user, None, None
if arg:
username_raw = arg.strip().removeprefix("@")
uid = economy.resolve_id("@" + username_raw) or economy.resolve_id(username_raw)
return None, uid, username_raw
return message.from_user, None, None
@router.message(Command("setbalance"), IsOwner(send_error_message=True))
async def cmd_setbalance(message: Message, command: CommandObject):
if not command.args:
return await message.answer("Использование: /setbalance <сумма> [@username | реплай]")
parts = command.args.strip().split(maxsplit=2)
try:
amount = int(parts[0])
except ValueError:
return await message.answer("Сумма должна быть числом.")
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
target_id = user_obj.id if user_obj else uid
if not target_id:
return await message.answer("Пользователь не найден.")
await economy.set_balance(target_id, amount, username, user_obj.full_name if user_obj else None)
await message.answer(f"Баланс {user_mention(user_obj, username)}{hbold(fmt(amount))} {CURRENCY_NAME}")
return None
@router.message(Command("plusbalance"), IsOwner(send_error_message=True))
async def cmd_plusbalance(message: Message, command: CommandObject):
if not command.args:
return await message.answer("Использование: /plusbalance <сумма> [@username | реплай]")
parts = command.args.strip().split(maxsplit=2)
try:
delta = int(parts[0])
except ValueError:
return await message.answer("Сумма должна быть числом.")
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
target_id = user_obj.id if user_obj else uid
if not target_id:
return await message.answer("Пользователь не найден.")
new_bal = await economy.modify_balance(target_id, delta, username, user_obj.full_name if user_obj else None)
await message.answer(f"{user_mention(user_obj, username)} +{fmt(delta)}{hbold(fmt(new_bal))} {CURRENCY_NAME}")
return None
@router.message(Command("minbalance"), IsOwner(send_error_message=True))
async def cmd_minbalance(message: Message, command: CommandObject):
if not command.args:
return await message.answer("Использование: /minbalance <сумма> [@username | реплай]")
parts = command.args.strip().split(maxsplit=2)
try:
delta = int(parts[0])
except ValueError:
return await message.answer("Сумма должна быть числом.")
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
target_id = user_obj.id if user_obj else uid
if not target_id:
return await message.answer("Пользователь не найден.")
new_bal = await economy.modify_balance(target_id, -delta, username, user_obj.full_name if user_obj else None)
await message.answer(f"{user_mention(user_obj, username)} -{fmt(delta)}{hbold(fmt(new_bal))} {CURRENCY_NAME}")
return None
@router.message(Command("top"))
async def cmd_top(message: Message):
top = economy.get_top(20)
if not top:
return await message.answer("Топ пустой — никто ещё не заработал коины!")
lines = ["Топ-20 богачей:"]
for i, (_, bal, username, full_name) in enumerate(top, 1):
medal = ["1st", "2nd", "3rd"][i-1] if i <= 3 else f"{i}."
name = f"@{username}" if username else hbold(full_name)
lines.append(f"{medal} {name}{hbold(fmt(bal))} {CURRENCY_NAME}")
await message.answer("\n".join(lines))
return None
@router.message(Command("deletebalance"), IsOwner(send_error_message=True))
async def cmd_deletebalance(message: Message):
user_obj, uid, username = await _get_target(message)
if not (user_obj or uid):
return await message.answer("Укажи пользователя реплаем или @username")
target_id = user_obj.id if user_obj else uid
deleted = await economy.delete_user(target_id)
name = user_mention(user_obj, username)
await message.answer(f"Запись {name} {'удалена' if deleted else 'не существовала'}")
return None
# ==================== Запуск ====================
async def on_startup(_):
await economy.load()
__all__ = ["router", "on_startup", "register_user_on_message"]

View File

@@ -0,0 +1,16 @@
# bot/handlers/__init__.py
from aiogram import Router
from .form_answer import router as form_answer_router
from .topic_replies import router as topic_replies_router
from .form_callback import router as form_callback_router
# Настройка экспорта и роутера
__all__ = ('router',)
router: Router = Router(name="handlers_router")
# Подготовка мастер-роутера
router.include_routers(
form_answer_router,
topic_replies_router,
form_callback_router,
)

View File

@@ -0,0 +1,45 @@
from aiogram import Router
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from bot.data.topic_map import user_topic_map
from bot.keyboards import decision_keyboard
from bot.states.anketa_states import StartForm
from configs import ImportantID
router = Router(name="form_handlers")
TOPIC_TYPE = "anketa"
@router.message(StartForm.waiting_for_application)
async def handle_application(message: Message, state: FSMContext):
await state.clear()
await message.answer("Спасибо! Ваша анкета отправлена на рассмотрение!")
user = message.from_user
user_id = user.id
if user.username:
users = f' от @{user.username}'
else:
users = ''
text = f'<b><a href="tg://user?id={user_id}">Анкета{users}</a></b>\n\n{message.html_text}'
key = (user_id, TOPIC_TYPE) # Ключ с типом топика
if key in user_topic_map:
thread_id = user_topic_map[key]
else:
topic = await message.bot.create_forum_topic(
chat_id=ImportantID.SUPPORT_CHAT_ID,
name=f"Анкета от {user.full_name}"
)
thread_id = topic.message_thread_id
user_topic_map[key] = thread_id
await message.bot.send_message(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
text=text,
reply_markup=decision_keyboard(thread_id, TOPIC_TYPE) # <--- Передаём оба аргумента
)

View File

@@ -0,0 +1,44 @@
from aiogram import Router, F
from aiogram.types import CallbackQuery
from bot.data.topic_map import user_topic_map
router = Router(name="form_callbacks")
TEXTS = {
"anketa": {
"accept": "<b>🎉 Ваша анкета принята!</b>\n\nДобро пожаловать в проект!",
"reject": "<b>❌ Ваша анкета отклонена.</b>\n\nВы можете попробовать позже."
},
"application": {
"accept": "<b>🎉 Ваша анкета принята!</b>\n\nДобро пожаловать в проект!",
"reject": "<b>❌ Ваша анкета отклонена.</b>\n\nВы можете попробовать позже."
},
"union": {
"accept": "<b>🤝 Союз одобрен!</b>\n\nТеперь вы в союзе.",
"reject": "<b>💔 Союз отклонён.</b>\n\nВы можете обсудить детали с администрацией."
}
}
@router.callback_query(F.data.regexp(r"^([a-z_]+):(accept|reject):(\d+)$"))
async def process_decision_callback(callback: CallbackQuery):
kind, action, thread_id_str = callback.data.split(":")
thread_id = int(thread_id_str)
user_id = None
for (uid, k), tid in user_topic_map.items():
if k == kind and tid == thread_id:
user_id = uid
break
if not user_id:
await callback.answer("Пользователь не найден.", show_alert=True)
return
text_to_send = TEXTS.get(kind, {}).get(action)
if not text_to_send:
await callback.answer("Некорректные данные.", show_alert=True)
return
await callback.message.bot.send_message(chat_id=user_id, text=text_to_send, parse_mode="HTML")
await callback.message.edit_reply_markup(reply_markup=None)
await callback.answer("Ответ отправлен пользователю.")

View File

@@ -0,0 +1,29 @@
from aiogram import Router, F
from aiogram.types import Message
from bot.data.topic_map import user_topic_map
router: Router = Router(name="topic_replies")
@router.message(F.is_topic_message, F.reply_to_message, ~F.from_user.is_bot)
async def forward_reply_to_user(message: Message):
thread_id = message.message_thread_id
if thread_id is None:
return # нет thread_id, выходим
# Найдем user_id по thread_id
user_id = None
for (uid, kind), tid in user_topic_map.items():
if tid == thread_id:
user_id = uid
break
if user_id is None:
return # Топик не зарегистрирован
reply_text = f"<b>Ответ администратора:</b>\n{message.html_text}"
try:
await message.bot.send_message(chat_id=user_id, text=reply_text, parse_mode="HTML")
except Exception as e:
await message.reply(f"⚠️ Не удалось отправить сообщение пользователю: {e}")

View File

@@ -0,0 +1,15 @@
from aiogram import Router
from .default_msg import router as default_message_router
from .ping_test import router as ping_test_message_router
# Настройка экспорта и роутера
router: Router = Router(name=__name__)
# Подготовка роутера команд
router.include_routers(
ping_test_message_router,
)
# Подключение стандартного роутера
router.include_router(default_message_router)

View File

@@ -0,0 +1,14 @@
from aiogram import Router
from aiogram.types import Message
from bot.handlers.custom.econom import register_user_on_message
# Настройки экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
@router.message()
async def default_messages(message: Message) -> None:
"""Обработчик всех необработанных сообщений."""
await register_user_on_message(message)

View File

@@ -0,0 +1,11 @@
from aiogram import Router
from aiogram.types import Message
# Настройки экспорта и роутера
router: Router = Router(name=__name__)
@router.message()
async def default_msg(message: Message) -> None:
"""Обработчик всех необработанных сообщений."""
return

View File

@@ -0,0 +1,32 @@
from aiogram import Router
from aiogram.types import Message
router: Router = Router(name=__name__)
# Словарь с ответами по ключам
RESPONSE_DICT: dict[str, str] = {
"пинг": "Понг! 🏓",
"понг": "Пинг!",
"бот": "На месте! 🤖",
}
@router.message()
async def auto_response_handler(message: Message) -> None:
"""Обработчик автоматических ответов по ключевым словам."""
if not message.text:
return
text_lower: str = message.text.casefold().strip()
# Поиск точного совпадения
if text_lower in RESPONSE_DICT:
response: str = RESPONSE_DICT[text_lower]
await message.answer(response)
return
# Поиск частичного совпадения (если хотите расширенную функциональность)
for key, response in RESPONSE_DICT.items():
if key in text_lower and len(key) > 3: # Только для ключей длиннее 3 символов
await message.answer(response)
return

View File

@@ -0,0 +1,11 @@
# bot/handlers/__init__.py
from aiogram import Router
from .union_handlers import router as union_router
# Настройка экспорта и роутера
router: Router = Router(name=__name__)
# Подготовка мастер-роутера
router.include_routers(
union_router,
)

View File

@@ -0,0 +1,44 @@
from aiogram import Router
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from bot.data.topic_map import user_topic_map
from bot.keyboards import decision_keyboard
from bot.states.union_states import UnionStates
from bot.utils import status_clear
from configs import ImportantID
router: Router = Router(name="union_handlers")
@router.message(UnionStates.waiting_for_union)
async def handle_union(message: Message, state: FSMContext) -> None:
await message.answer("Спасибо! Ваше сообщение отправлено.")
user = message.from_user
user_id = user.id
msg_type = "union"
text = f"<b>СОЮЗ</b>\n\n{message.html_text}"
key = (user_id, msg_type)
if key in user_topic_map:
thread_id = user_topic_map[key]
else:
topic = await message.bot.create_forum_topic(
chat_id=ImportantID.SUPPORT_CHAT_ID,
name=f"Сообщение от {user.full_name}"
)
thread_id = topic.message_thread_id
user_topic_map[key] = thread_id
await message.bot.send_message(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=724,
text=text,
parse_mode="HTML",
reply_markup=decision_keyboard(thread_id, "union")
)
await status_clear(message=message, state=state)

View File

@@ -0,0 +1,2 @@
from .inline import *
from .reply import *

View File

@@ -0,0 +1 @@
from .decision import *

View File

@@ -0,0 +1,18 @@
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
def decision_keyboard(thread_id: int, kind: str) -> InlineKeyboardMarkup:
"""
Получение клавиатуры Принятия\Отклонить.
:param thread_id: Айди действия.
:param kind: Вид для клавиатуры.
:return: Инлайн-клавиатуру (Принять, Отклонить).
"""
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(
InlineKeyboardButton(text="✅ Принять", callback_data=f"{kind}:accept:{thread_id}"),
InlineKeyboardButton(text="❌ Отклонить", callback_data=f"{kind}:reject:{thread_id}")
)
return ikb.as_markup()

View File

View File

@@ -0,0 +1,55 @@
from aiogram import Dispatcher, Bot
from configs import ImportantID
from .error_mdw import ErrorHandlingMiddleware
from .logging_mdw import LoggingMiddleware
from .msg_mdw import MessageCounterMiddleware
from .referal_mdw import ReferralMiddleware
from .spam_mdw import RateLimitMiddleware
from .subscription_mdw import SubscriptionMiddleware
from .time_mdw import TimingMiddleware
from .ban_user_mdw import BanCheckMiddleware
# Настройки экспорта
__all__ = (
"LoggingMiddleware",
"SubscriptionMiddleware",
"RateLimitMiddleware",
"ErrorHandlingMiddleware",
"TimingMiddleware",
"MessageCounterMiddleware",
"setup_middlewares",
"ReferralMiddleware",
"BanCheckMiddleware",
)
def setup_middlewares(dp: Dispatcher, bot: Bot, channel_ids: list[int | str] = None) -> None:
"""
Регистрирует все middleware в диспетчере.
"""
channel_ids: list = channel_ids or []
# Middleware для ВСЕХ событий (update level)
middlewares_updates: list = [
TimingMiddleware(), # Замер времени
LoggingMiddleware(), # Логирование
ErrorHandlingMiddleware(admin_ids=ImportantID.ADMIN_ID), # Обработка ошибок
]
# Middleware только для СООБЩЕНИЙ (message level)
middlewares_msg: list = [
BanCheckMiddleware(),
# RateLimitMiddleware(rate_limit=3, time_period=5.0), # Антифлуд
# SubscriptionMiddleware(bot=bot, channel_ids=channel_ids), # Проверка подписки
MessageCounterMiddleware(), # Подсчет сообщений
ReferralMiddleware(), # Проверка реф-ссылок
]
# Регистрируем middleware для всех событий
for middleware in middlewares_updates:
dp.update.middleware(middleware)
# Регистрируем middleware только для сообщений
for middleware in middlewares_msg:
dp.message.middleware(middleware)

View File

@@ -0,0 +1,108 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import Message, CallbackQuery, TelegramObject
from database import db
__all__ = ("BanCheckMiddleware",)
class BanCheckMiddleware(BaseMiddleware):
"""
Middleware для проверки забанен ли пользователь.
Если пользователь забанен в боте - блокирует все его действия.
"""
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Проверяет каждый входящий запрос на наличие пользователя в черном списке.
Args:
handler: Следующий обработчик
event: Событие (сообщение, callback и т.д.)
data: Данные контекста
Returns:
Результат обработчика или None если пользователь забанен
"""
# Извлекаем информацию о пользователе из события
user = await self._extract_user(event)
if user is None:
# Не смогли определить пользователя - пропускаем
return await handler(event, data)
# Проверяем в базе данных статус пользователя
user_db = await db.get_user(user.id)
if user_db and user_db.status == "banned":
# Пользователь забанен - блокируем запрос
await self._send_ban_message(event, data)
return None
# Пользователь не забанен - пропускаем запрос дальше
return await handler(event, data)
@staticmethod
async def _extract_user(event: TelegramObject) -> Any:
"""
Извлекает пользователя из разных типов событий.
Args:
event: Событие Telegram
Returns:
Объект пользователя или None
"""
if isinstance(event, Message):
return event.from_user
elif isinstance(event, CallbackQuery):
return event.from_user
# Можно добавить другие типы событий при необходимости
return None
@staticmethod
async def _send_ban_message(event: TelegramObject, data: Dict[str, Any]) -> None:
"""
Отправляет сообщение о бане пользователю.
Args:
event: Событие которое triggered проверку
data: Данные контекста с ботом
"""
bot = data.get('bot')
if not bot:
return
chat_id = None
message_id = None
# Определяем куда отправлять сообщение в зависимости от типа события
if isinstance(event, Message):
chat_id = event.chat.id
message_id = event.message_id
elif isinstance(event, CallbackQuery) and event.message:
chat_id = event.message.chat.id
message_id = event.message.message_id
if chat_id:
try:
if isinstance(event, CallbackQuery):
# Для callback запросов отвечаем уведомлением
await event.answer("🚫 Вы заблокированы в боте!", show_alert=True)
else:
# Для сообщений отправляем новое сообщение
await bot.send_message(
chat_id=chat_id,
text="🚫 Вы заблокированы в боте!",
reply_to_message_id=message_id
)
except Exception:
# Игнорируем ошибки отправки (пользователь мог заблокировать бота)
pass

View File

@@ -0,0 +1,201 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import Bot, BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
from middleware.loggers import logger
class ErrorHandlingMiddleware(BaseMiddleware):
"""
Middleware для глобальной обработки ошибок в хендлерах.
Зачем нужен:
- Централизованная обработка исключений
- Уведомление администраторов об ошибках
- Graceful degradation при сбоях
"""
def __init__(self, admin_ids: list[int]):
"""
Инициализация middleware обработки ошибок.
Args:
admin_ids: Список ID администраторов для уведомлений
"""
self.admin_ids = admin_ids
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Перехватывает и обрабатывает ошибки в хендлерах.
"""
try:
return await handler(event, data)
except Exception as e:
# Получаем информацию о пользователе безопасным способом
user_str: str = self._extract_user_info(event)
# Логируем ошибку
error_message: str = f"Ошибка в хендлере: {type(e).__name__}: {str(e)}"
logger.error(
text=error_message,
log_type="HANDLER_ERROR",
user=user_str
)
# Уведомляем администраторов
await self._notify_admins(error_message, event, user_str)
# Отправляем пользователю сообщение об ошибке
await self._send_error_message(event, user_str)
return None
@staticmethod
def _extract_user_info(event: TelegramObject) -> str:
"""
Безопасно извлекает информацию о пользователе из события.
Args:
event: Объект события
Returns:
Строка с идентификатором пользователя
"""
user_str: str = "@System"
# Для Message и CallbackQuery
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
user = event.from_user
user_str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update (который содержит message или callback_query)
elif isinstance(event, Update):
# Пытаемся найти пользователя в различных полях Update
user_object = None
if event.message and event.message.from_user:
user_object = event.message.from_user
elif event.edited_message and event.edited_message.from_user:
user_object = event.edited_message.from_user
elif event.callback_query and event.callback_query.from_user:
user_object = event.callback_query.from_user
elif event.channel_post and event.channel_post.from_user:
user_object = event.channel_post.from_user
elif event.edited_channel_post and event.edited_channel_post.from_user:
user_object = event.edited_channel_post.from_user
if user_object:
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
return user_str
@staticmethod
def _extract_event_text(event: TelegramObject) -> str:
"""
Безопасно извлекает текст из события.
Args:
event: Объект события
Returns:
Текст события или пустая строка
"""
event_text: str = ""
# Для Message
if isinstance(event, Message) and hasattr(event, 'text') and event.text:
event_text: str = event.text
# Для CallbackQuery
elif isinstance(event, CallbackQuery) and hasattr(event, 'data') and event.data:
event_text: str = f"callback: {event.data}"
# Для Update
elif isinstance(event, Update):
if event.message and event.message.text:
event_text: str = event.message.text
elif event.callback_query and event.callback_query.data:
event_text: str = f"callback: {event.callback_query.data}"
elif event.edited_message and event.edited_message.text:
event_text: str = event.edited_message.text
return event_text[:100] + "..." if len(event_text) > 100 else event_text
async def _notify_admins(
self,
error_message: str,
event: TelegramObject,
user_str: str
) -> None:
"""Уведомляет администраторов об ошибке."""
bot: Bot = event.bot if hasattr(event, 'bot') else None
if bot:
for admin_id in self.admin_ids:
try:
event_info: str = f"Событие: {type(event).__name__}"
event_text: str = self._extract_event_text(event)
if event_text:
event_info += f", текст: {event_text}"
full_message: str = (
f"🚨 Ошибка в боте:\n\n"
f"Пользователь: {user_str}\n"
f"Ошибка: {error_message}\n"
f"{event_info}"
)
await bot.send_message(admin_id, full_message)
logger.info(
text=f"Администратор {admin_id} уведомлен об ошибке",
log_type="ADMIN_NOTIFIED",
user=user_str
)
except Exception as e:
logger.error(
text=f"Не удалось уведомить админа {admin_id}: {e}",
log_type="ADMIN_NOTIFY_ERROR",
user=user_str
)
@staticmethod
async def _send_error_message(
event: TelegramObject,
user_str: str
) -> None:
"""Отправляет пользователю сообщение об ошибке."""
error_text: str = (
"⚠️ Произошла непредвиденная ошибка. "
"Разработчики уже уведомлены и работают над исправлением.\n\n"
"Попробуйте повторить действие позже или нажмите /start"
)
try:
if isinstance(event, Message):
await event.answer(error_text)
elif isinstance(event, CallbackQuery):
await event.message.answer(error_text)
await event.answer()
elif isinstance(event, Update) and event.message:
await event.message.answer(error_text)
logger.info(
text="Пользователю отправлено сообщение об ошибке",
log_type="ERROR_MESSAGE_SENT",
user=user_str
)
except Exception as e:
logger.error(
text=f"Не удалось отправить сообщение об ошибке: {e}",
log_type="ERROR_MESSAGE_FAILED",
user=user_str
)

View File

@@ -0,0 +1,272 @@
from typing import Callable, Awaitable, Any, Dict, Optional, Tuple, Set
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Update, Message, CallbackQuery, MaybeInaccessibleMessageUnion, User
from bot.utils import type_msg
from configs import BotSettings, COMMANDS # импортируем настройки и команды
from middleware.loggers import logger # ваш глобальный логгер
class LoggingMiddleware(BaseMiddleware):
"""
Middleware для логирования апдейтов с определением типа события,
пользователя и добавлением префикса проекта к типу лога.
Автоматически добавляет префикс проекта (например, 'PRIMO-') к типам логов:
- PRIMO-UPDATE: общий апдейт без определенного типа
- PRIMO-MSG: текстовое сообщение от пользователя
- PRIMO-CMD: команда (сообщение, начинающееся с любого префикса)
- PRIMO-CBD: callback query от инлайн-кнопок
"""
# Префикс проекта для логов
PROJECT_PREFIX: str = "PRIMO"
# Кэш для всех команд из COMMANDS
_all_commands: Optional[Set[str]] = None
def __init__(self):
super().__init__()
# Предварительно загружаем все команды
self._load_all_commands()
def _load_all_commands(self) -> None:
"""Загружает все команды из COMMANDS в множество для быстрого поиска."""
if self._all_commands is None:
self._all_commands = set()
for command_list in COMMANDS.values():
self._all_commands.update(command_list)
def _is_command(self, text: str) -> bool:
"""
Проверяет, является ли текст командой с любым префиксом.
Args:
text: Текст для проверки
Returns:
True если это команда, False если нет
"""
if not text:
return False
# Проверяем все префиксы из BotSettings
for prefix in BotSettings.PREFIX:
if text.startswith(prefix):
# Извлекаем команду без префикса
command_without_prefix = text[len(prefix):].strip()
# Проверяем, есть ли такая команда в нашем списке
if command_without_prefix in self._all_commands:
return True
# Также проверяем команды с префиксом / (стандартные)
if text.startswith('/'):
command_without_slash = text[1:].strip()
if command_without_slash in self._all_commands:
return True
return False
@staticmethod
def _extract_command_name(text: str) -> str:
"""
Извлекает название команды из текста.
Args:
text: Текст команды с префиксом
Returns:
Название команды без префикса
"""
for prefix in BotSettings.PREFIX:
if text.startswith(prefix):
return text[len(prefix):].strip()
if text.startswith('/'):
return text[1:].strip()
return text
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Обрабатывает входящее событие, определяет его тип, логирует с префиксом проекта
и передает следующему обработчику.
Args:
handler: Следующий обработчик в цепочке middleware
event: Входящее событие для обработки (Update, Message, CallbackQuery)
data: Словарь с контекстными данными FSM
Returns:
Результат выполнения следующего обработчика
Raises:
Exception: Любое исключение, возникшее при обработке хендлером
"""
# Определяем тип события и информацию для логирования
log_type: str
log_text: str
message_obj: Optional[Message]
log_type, log_text, message_obj = self._determine_event_type(event)
# Добавляем префикс проекта к типу лога
prefixed_log_type: str = f"{log_type}"
# Определяем информацию о пользователе
user_str: str = self._extract_user_info(event, message_obj)
# Логируем получение события с префиксом проекта
logger.info(
text=log_text,
log_type=prefixed_log_type,
user=user_str
)
try:
# Передаем событие следующему обработчику
result: Any = await handler(event, data)
# Логируем успешное выполнение для команд
if log_type == "CMD":
logger.info(
text=f"[SUCCESS] команда обработана",
log_type=prefixed_log_type,
user=user_str
)
return result
except Exception as e:
# Логируем ошибку при обработке с префиксом проекта
logger.error(
text=f"Ошибка обработки: {str(e)}",
log_type=prefixed_log_type,
user=user_str
)
raise
def _determine_event_type(
self,
event: TelegramObject
) -> Tuple[str, str, Optional[Message]]:
"""
Определяет тип события и извлекает информацию для логирования.
Args:
event: Объект события для анализа
Returns:
Кортеж из (тип_лога, текст_лога, объект_сообщения)
"""
log_type: str = "UPDATE"
log_text: str = f"Получен апдейт: {type(event).__name__}"
message_obj: Optional[Message] = None
# Обработка Update объектов (основной тип в middleware)
if isinstance(event, Update):
# Пытаемся найти сообщение в различных полях Update
message_obj: Message | None = (
event.message or
event.edited_message or
event.channel_post or
event.edited_channel_post
)
if message_obj and message_obj.text:
if self._is_command(message_obj.text):
log_type: str = "CMD"
log_text: str = f"использовал команду '{message_obj.text}'"
else:
log_type: str = "MSG"
log_text: str = f"получено сообщение: {message_obj.text!r}"
elif message_obj:
# Не текстовое сообщение (фото, видео и т.д.)
log_type: str = "MSG"
log_text: str = f"получено сообщение: '{type_msg(message_obj)}'"
elif event.callback_query:
# Обработка callback query
callback: CallbackQuery = event.callback_query
log_type: str = "CBD"
log_text: str = f"получен callback: {callback.data!r}"
if callback.message:
message_obj: Optional[MaybeInaccessibleMessageUnion] = callback.message
# Прямая обработка Message (если мидлварь зарегистрирован на messages)
elif isinstance(event, Message):
message_obj: Message | None = event
if event.text and self._is_command(event.text):
log_type: str = "CMD"
log_text: str = f"использовал команду '{event.text}'"
elif event.text:
log_type: str = "MSG"
log_text: str = f"получено сообщение: {event.text!r}"
else:
log_type: str = "MSG"
log_text: str = f"получено сообщение типа: {event.content_type}"
# Прямая обработка CallbackQuery (если мидлварь зарегистрирован на callbacks)
elif isinstance(event, CallbackQuery):
log_type: str = "CBD"
log_text: str = f"получен callback: {event.data!r}"
if event.message:
message_obj = event.message
return log_type, log_text, message_obj
@staticmethod
def _extract_user_info(
event: TelegramObject,
message: Optional[Message] = None
) -> str:
"""
Извлекает информацию о пользователе из события.
Args:
event: Объект события (Update, Message или CallbackQuery)
message: Объект Message (если уже определен)
Returns:
Строка с идентификатором пользователя в формате '@username' или 'id<user_id>'
"""
user_str: str = "@System"
# Для CallbackQuery извлекаем пользователя из самого callback'а
if isinstance(event, CallbackQuery) and hasattr(event, 'from_user') and event.from_user:
user: User | None = event.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Message извлекаем пользователя из сообщения
elif isinstance(event, Message) and hasattr(event, 'from_user') and event.from_user:
user: User | None = event.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update с callback_query
elif (isinstance(event, Update) and
event.callback_query and
hasattr(event.callback_query, 'from_user') and
event.callback_query.from_user):
user: User | None = event.callback_query.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update с сообщением
elif (isinstance(event, Update) and
(event.message or event.edited_message) and
hasattr(event.message or event.edited_message, 'from_user')):
msg: Message | None = event.message or event.edited_message
if msg and msg.from_user:
user: Optional[User] = msg.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Если передан message объект
elif message and hasattr(message, 'from_user') and message.from_user:
user: Optional[User] = message.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
return user_str

View File

@@ -0,0 +1,57 @@
import logging
from typing import Callable, Dict, Any, Awaitable
from aiogram import BaseMiddleware
from aiogram.enums import ChatType
from aiogram.types import Message
from database import db
logger = logging.getLogger(__name__)
class MessageCounterMiddleware(BaseMiddleware):
"""
Middleware для подсчёта сообщений в группах и супергруппах.
"""
async def __call__(
self,
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
event: Any,
data: Dict[str, Any]
) -> Any:
if not isinstance(event, Message):
return await handler(event, data)
# Проверяем, что сообщение пришло из группового чата и не от бота
if (event.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP) and
not event.from_user.is_bot):
try:
await self.process_group_message(event)
except Exception as e:
logger.error(msg=f"Ошибка при обработке сообщения: {e}", exc_info=True)
return await handler(event, data)
@staticmethod
async def process_group_message(message: Message) -> None:
"""
Обработка сообщения из группового чата.
"""
user_id: int = message.from_user.id
message_text: str = message.text or message.caption or ""
# Добавляем пользователя (если его ещё нет)
await db.add_user(
user_id=user_id,
username=message.from_user.username,
full_name=message.from_user.full_name,
)
# Сохраняем сообщение
await db.add_message(
user_id=user_id,
message_text=message_text,
created_at=message.date,
)

View File

@@ -0,0 +1,59 @@
from typing import Callable, Awaitable, Any, Dict, Optional
from aiogram import BaseMiddleware
from aiogram.filters.command import CommandObject
from aiogram.types import TelegramObject, Message
from middleware.loggers import logger
class ReferralMiddleware(BaseMiddleware):
"""
Middleware для перехвата и обработки реферальных ссылок (?start=...).
Основные задачи:
- Отслеживание перехода по deep-link (например, /start ref123)
- Централизованное логирование
- Возможность передачи кода дальше в хендлеры
- Подготовка к сохранению кода в базу данных
"""
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
"""
Перехватывает входящие сообщения и извлекает deep-link аргумент,
если пользователь зашёл по реферальной ссылке.
Args:
handler: Следующий обработчик в цепочке middleware
event: Входящее событие (Message, CallbackQuery и др.)
data: Контекстные данные, доступные хендлеру
Returns:
Результат работы следующего обработчика
"""
# Проверяем, что событие — это именно сообщение
if isinstance(event, Message):
# Извлекаем объект команды (если был установлен фильтр CommandStart)
command: Optional[CommandObject] = data.get("command")
# Проверяем, что это именно команда /start с аргументом
if command and command.command.casefold() == "start" and command.args:
ref_code: str = command.args
user_id: int = event.from_user.id
username: Optional[str] = event.from_user.username
# 👉 Здесь можно сохранить код в БД
logger.debug(
f"[Referral] user={user_id}, username={username}, ref={ref_code}"
)
# Пробрасываем реф-код в data, чтобы использовать в хендлере
data["ref_code"] = ref_code
# Передаём управление дальше
return await handler(event, data)

View File

@@ -0,0 +1,98 @@
import time
from collections import defaultdict
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery
from middleware.loggers import logger # ваш логгер
class RateLimitMiddleware(BaseMiddleware):
"""
Middleware для ограничения частоты запросов от пользователей (анти-спам).
Зачем нужен:
- Защита от DDoS и флуда
- Предотвращение злоупотребления ботом
- Контроль нагрузки на сервер
"""
def __init__(self, rate_limit: int = 10, time_period: float = 2.0):
"""
Инициализация rate limit middleware.
Args:
rate_limit: Максимальное количество запросов за период
time_period: Период времени в секундах
"""
self.rate_limit = rate_limit
self.time_period = time_period
self.user_calls: Dict[int, list[float]] = defaultdict(list)
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
log: bool = False,
) -> Any:
"""
Проверяет rate limit перед обработкой запроса.
"""
# Пропускаем не-сообщения и не-колбэки
if not isinstance(event, (Message, CallbackQuery)):
return await handler(event, data)
user_id: int = event.from_user.id
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
current_time: float = time.time()
# Очищаем старые запросы
self.user_calls[user_id]: dict[int, list[float]] = [
call_time for call_time in self.user_calls[user_id]
if current_time - call_time < self.time_period
]
# Логируем текущее состояние rate limit
if log:
logger.debug(
text=f"Rate limit: {len(self.user_calls[user_id])}/{self.rate_limit} за {self.time_period}сек",
log_type="RATE_LIMIT_STATUS",
user=user_str
)
# Проверяем текущий лимит
if len(self.user_calls[user_id]) >= self.rate_limit:
# Логируем попытку спама
if log:
logger.warning(
text=f"Превышен rate limit ({self.rate_limit}/{self.time_period}сек)",
log_type="RATE_LIMIT_EXCEEDED",
user=user_str
)
# Отправляем сообщение о превышении лимита
if isinstance(event, Message):
await event.answer(
text="⏳ Слишком много запросов! Пожалуйста, подождите немного.",
)
elif isinstance(event, CallbackQuery):
await event.answer(
text="⏳ Подождите немного перед следующим действием.",
show_alert=True
)
return None
# Добавляем текущий запрос и продолжаем обработку
self.user_calls[user_id].append(current_time)
logger.debug(
text=f"Запрос добавлен в rate limit",
log_type="RATE_LIMIT_ADDED",
user=user_str
)
return await handler(event, data)

View File

@@ -0,0 +1,110 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware, Bot
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import TelegramObject, Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from middleware.loggers import logger
class SubscriptionMiddleware(BaseMiddleware):
"""
Middleware для проверки подписки пользователя на необходимые каналы.
Блокирует обработку команд, если пользователь не подписан.
Зачем нужен:
- Автоматическая проверка подписки для всех входящих сообщений
- Единая точка управления подписками
- Предотвращение доступа к функционалу без подписки
"""
def __init__(self, bot: Bot, channel_ids: list[int | str]):
"""
Инициализация middleware проверки подписки.
Args:
bot: Экземпляр бота
channel_ids: Список ID каналов/чатов для проверки подписки
"""
self.bot = bot
self.channel_ids = channel_ids
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Проверяет подписку пользователя перед обработкой команды.
"""
# Пропускаем не-сообщения и не-колбэки
if not isinstance(event, (Message, CallbackQuery)):
return await handler(event, data)
user_id: int = event.from_user.id
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
# Логируем начало проверки подписки
logger.info(
text=f"Проверка подписки для пользователя",
log_type="SUBSCRIPTION_CHECK",
user=user_str
)
# Проверяем подписку на все required каналы
not_subscribed_channels: list[str] = []
for channel_id in self.channel_ids:
try:
member = await self.bot.get_chat_member(
chat_id=channel_id,
user_id=user_id
)
# Проверяем, что пользователь является участником
if member.status not in ['member', 'administrator', 'creator']:
not_subscribed_channels.append(str(channel_id))
except TelegramBadRequest as e:
logger.error(
text=f"Ошибка проверки подписки на канал {channel_id}: {e}",
log_type="SUBSCRIPTION_ERROR",
user=user_str
)
# Если пользователь не подписан на некоторые каналы
if not_subscribed_channels:
logger.warning(
text=f"Пользователь не подписан на каналы: {', '.join(not_subscribed_channels)}",
log_type="SUBSCRIPTION_FAILED",
user=user_str
)
warning_text: str = (
"📢 Для использования бота необходимо подписаться на наши каналы!\n\n"
"После подписки нажмите /start для продолжения."
)
# Создаем кнопку "Проверить подписку"
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="✅ Я подписался", callback_data="check_subscription"))
if isinstance(event, Message):
await event.answer(warning_text, reply_markup=ikb.as_markup())
elif isinstance(event, CallbackQuery):
await event.message.answer(warning_text, reply_markup=ikb.as_markup())
await event.answer()
return None
# Логируем успешную проверку подписки
logger.info(
text="Пользователь подписан на все required каналы",
log_type="SUBSCRIPTION_SUCCESS",
user=user_str
)
# Если подписка есть, продолжаем обработку
return await handler(event, data)

View File

@@ -0,0 +1,83 @@
from time import time
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
from middleware.loggers import logger
class TimingMiddleware(BaseMiddleware):
"""
Middleware для измерения времени выполнения хендлеров.
Зачем нужен:
- Мониторинг производительности хендлеров
- Выявление медленных запросов
- Оптимизация кода бота
"""
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
perm: str = None,
) -> Any:
"""
Измеряет время выполнения хендлера.
"""
start_time: float = time()
try:
result: Any = await handler(event, data)
return result
finally:
execution_time: float = time() - start_time
# Получаем информацию о пользователе безопасным способом
user_str: str = "@System"
# Для Message и CallbackQuery
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
user = event.from_user
user_str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update (который содержит message или callback_query)
elif isinstance(event, Update):
# Пытаемся найти пользователя в различных полях Update
user_object = None
if event.message and event.message.from_user:
user_object = event.message.from_user
elif event.edited_message and event.edited_message.from_user:
user_object = event.edited_message.from_user
elif event.callback_query and event.callback_query.from_user:
user_object = event.callback_query.from_user
elif event.channel_post and event.channel_post.from_user:
user_object = event.channel_post.from_user
elif event.edited_channel_post and event.edited_channel_post.from_user:
user_object = event.edited_channel_post.from_user
if user_object:
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
# Логируем время выполнения
if execution_time > 1.0 and perm: # Медленные запросы
logger.warning(
text=f"Медленный хендлер: {execution_time:.2f}сек",
log_type="SLOW_HANDLER",
user=user_str
)
elif execution_time > 0.5 and perm == "medium": # Средние запросы
logger.info(
text=f"Среднее время выполнения: {execution_time:.3f}сек",
log_type="HANDLER_TIMING",
user=user_str
)
elif perm == "fast": # Быстрые запросы
logger.debug(
text=f"Быстрое выполнение: {execution_time:.3f}сек",
log_type="HANDLER_TIMING_FAST",
user=user_str
)

2
bot/states/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .anketa_states import *
from .new_states import *

View File

@@ -0,0 +1,5 @@
# bot/states/form.py
from aiogram.fsm.state import State, StatesGroup
class StartForm(StatesGroup):
waiting_for_application = State()

10
bot/states/new_states.py Normal file
View File

@@ -0,0 +1,10 @@
# bot/states/new_states.py
from aiogram.fsm.state import State, StatesGroup
__all__ = ("NewStates",)
class NewStates(StatesGroup):
role: State = State()
sorol: State = State()
code_phrase: State = State()
rules: State = State()

View File

@@ -0,0 +1,5 @@
# bot/states/union_states.py
from aiogram.fsm.state import State, StatesGroup
class UnionStates(StatesGroup):
waiting_for_union = State()

View File

@@ -0,0 +1 @@
from .message_callback import *

View File

@@ -0,0 +1,88 @@
from typing import Union
from aiogram.types import (
FSInputFile,
CallbackQuery,
Message,
ReplyKeyboardMarkup,
InlineKeyboardMarkup
)
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
# Настройка экспорта в модули
__all__ = ('msg', 'msg_photo', 'markups',)
def markups(markup: Union[
InlineKeyboardBuilder,
ReplyKeyboardBuilder,
InlineKeyboardMarkup,
ReplyKeyboardMarkup,
None] = None, ) -> None:
"""Получение маркапа"""
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None
if markup:
if isinstance(markup, InlineKeyboardBuilder):
reply_markup = markup.as_markup()
elif isinstance(markup, ReplyKeyboardBuilder):
reply_markup = markup.as_markup(resize_keyboard=True)
elif isinstance(markup, (InlineKeyboardMarkup, ReplyKeyboardMarkup)):
reply_markup = markup
return reply_markup
async def msg(
message: Message | CallbackQuery,
text: str = "Сообщение отправлено!",
markup: Union[
InlineKeyboardBuilder,
ReplyKeyboardBuilder,
InlineKeyboardMarkup,
ReplyKeyboardMarkup,
None,
] = None,
) -> None:
"""
Шаблон для отправки текстового сообщения или ответа на callback-запрос.
Args:
message (Message | CallbackQuery): Сообщение или callback-запрос.
text (str): Текст сообщения.
markup (Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, InlineKeyboardMarkup, ReplyKeyboardMarkup, None]):
Клавиатура для сообщения. Может быть билдера или готовый объект.
"""
if isinstance(message, Message):
await message.reply(text=text, reply_markup=markups(markup))
else:
await message.message.reply(text=text, reply_markup=markups(markup))
async def msg_photo(
message: Message | CallbackQuery,
file: str = "assets/default.jpg",
text: str = "Сообщение отправлено!",
markup: Union[
InlineKeyboardBuilder,
ReplyKeyboardBuilder,
InlineKeyboardMarkup,
ReplyKeyboardMarkup,
None,
] = None,
) -> None:
"""
Шаблон для отправки фотографии с подписью или ответа на callback-запрос.
Args:
message (Message | CallbackQuery): Сообщение или callback-запрос.
file (str): Путь к файлу фотографии.
text (str): Подпись к фото.
markup (Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, InlineKeyboardMarkup, ReplyKeyboardMarkup, None]):
Клавиатура для сообщения. Может быть билдера или готовый объект.
"""
if isinstance(message, Message):
await message.reply_photo(photo=FSInputFile(file), caption=text, reply_markup=markups(markup))
else:
await message.message.reply_photo(photo=FSInputFile(file), caption=text, reply_markup=markups(markup))

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

3
configs/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .cmd_list import *
from .config import *
from .roles import *

104
configs/cmd_list.py Normal file
View File

@@ -0,0 +1,104 @@
from typing import Final
# Список команд по ключу
COMMANDS: Final[dict[str, list[str]]] = {
"start": [
"start", "старт", "почати",
"ыефке", "cnfhn", "on", "вкл", "щт", "drk",
],
"help": [
"help", "помощь", "допомога",
"рудзщь", "dopomoga", "?",
],
"menu": [
"menu", "меню", "менюшка",
"ьщкф", "menyu",
],
"create": [
"create", "создать", "створити",
"сщзду", "sozdat", "stvoriti",
],
"report": [
"report", "репорт", "скарга",
"кщзщтв", "repert",
],
"mute": [
"mute", "заглушить", "заглушити",
"угуыщцук", "zaglushit",
],
"kick": [
"kick", "кик", "викинути",
"куиф", "vikynuty",
],
"ban": [
"ban", "бан", "забанити",
"ьфд", "zabanyty",
],
"stats": [
"stats", "статистика", "статистика",
"ыпщз", "statystyka",
],
"settings": [
"settings", "настройки", "налаштування",
"гшеукефьз", "nastroyky",
],
"info": [
"info", "инфо", "інфо",
"шкещ", "info",
],
"feedback": [
"feedback", "обратная связь", "зворотній зв’язок",
"гуеекфьз", "obratnaia_svyaz",
],
"subscribe": [
"subscribe", "подписаться", "підписатися",
"подписатсь", "pidpysatysia",
],
"unsubscribe": [
"unsubscribe", "отписаться", "відписатися",
"отписаться", "vidpysatysia",
],
"language": [
"language", "язык", "мова",
"йцукефь", "mova",
],
"cancel": [
"cancel", "отмена", "скасувати",
"утпщге", "skasuvaty",
],
"list": [
"list", "список", "список",
"дшззщк", "spysok",
],
"forward": [
"forward", "переслать", "переслати",
"дшпекщву", "pereslaty",
],
"all": [
"all", "фдд", "norify", "тщкшан", "call", "сфдд", "калл", "rfkk",
],
"pin": [
"pin", "зшт", "закреп", "pfrhtg", "закрепить",
"pfrhtgbnm",
],
"set_name": [
"set_name", "setname", "ыуетфьу", "ыуе_тфьу",
],
"set_description": [
"set_description", "setdescription", "ыуе_вшыскшзещт", "ыуевшыскшзещт",
],
"set_widget": [
"set_widget", "setwidget", "ыуе_цшвпук", "ыуецшвпук",
],
"set_rights": [
"set_rights", "setrights", "ыуе_кшпреы", "ыуекшпреы",
],
"new": [
"new", "туц", "вступление",
"cnegktybt", "ym.", "нью",
],
"active": [
"active",
]
}

399
configs/config.py Normal file
View File

@@ -0,0 +1,399 @@
from pathlib import Path
from typing import ClassVar, Final, Optional, Any
from urllib.parse import urlparse, ParseResult
from aiogram.types import ChatAdministratorRights
from pydantic import field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Улучшенный класс настроек с комплексной валидацией"""
# Конфигурация загрузки переменных окружения
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
case_sensitive=False,
validate_default=True,
)
# Режимы и базовые параметры
PYTHONUNBUFFERED: str = "1"
LOCALE_PATH: str = "locales"
DEBUG: bool = False
OWNER: str = "@verdise"
# Токены бота
BOT_TOKEN: Optional[str] = None
BOT_DEBUG_TOKEN: Optional[str] = None
# Параметры сообщений
PARSE_MODE: str = "HTML"
ENCOD: str = "utf-8"
TIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
PREFIX: str = "/!.&?"
BOT_LANGUAGE: str = "Aiogram3"
# Настройки сообщений
DISABLE_NOTIFICATION: bool = False
PROTECT_CONTENT: bool = False
ALLOW_SENDING_WITHOUT_REPLY: bool = True
LINK_PREVIEW_IS_DISABLED: bool = False
LINK_PREVIEW_PREFER_SMALL_MEDIA: bool = False
LINK_PREVIEW_PREFER_LARGE_MEDIA: bool = True
LINK_PREVIEW_SHOW_ABOVE_TEXT: bool = True
SHOW_CAPTION_ABOVE_MEDIA: bool = False
# Разрешения и логирование
BOT_EDIT: bool = False
START_INFO_CONSOLE: bool = True
START_INFO_TO_FILE: bool = True
LOG_CONSOLE: bool = True
LOG_FILE: bool = True
LOG_DIR: Path = Path('Logs')
LOG_FILE_INFO: Path = Path('bot_info.log')
# Вебхук
WEBHOOK: bool = False
WEBHOOK_URL: str = "https://bot.primo.dpdns.org/webhook" # публичный HTTPS url
WEBAPP_HOST: str = "0.0.0.0" # адрес, на котором слушает uvicorn внутри контейнера
WEBAPP_PORT: int = 3131
LOG_LEVEL: str = "warning"
ACCES_LOG: bool = False
# API ключи
API_KEY: Optional[str] = None
WEB_API_KEY: Optional[str] = None
WEATHER_API_KEY: Optional[str] = None
# Пользовательские данные
TG_API_UID: int = 0
TG_API_HASH: Optional[str] = None
SESSION_STRING: Optional[str] = None
# Идентификаторы
OWNERS_ID: list[int] = [6751720805, ]
ADMIN_ID: list[int] = [6751720805, ]
MODERATOR_ID: int = 0
IMPORTANT_ID: int = 0
IMPORTANT_GROUP_ID: int = 0
IMPORTANT_CHANNEL_ID: int = 0
SUPPORT_CHAT_ID: int = 0
SUPPORT_CHAT_ID_TOPIC: int = 0
# Настройки бота
PROJECT_NAME: str = "PRIMO"
BOT_NAME: str = "Первозданная Жемчужина"
BOT_DESCRIPTION: Optional[str] = None
BOT_SHORT_DESCRIPTION: Optional[str] = None
# Ролевой проект
RP_NAME: Optional[str] = "𝘗𝘳𝘪𝘮𝘰 𝘞𝘰𝘳𝘭𝘥"
INFO_URL: Optional[str] = "https://t.me/PrimoWorldRP"
FLUD_URL: Optional[str] = "https://t.me/PrimoWorldRP"
RP_URL: Optional[str] = "https://t.me/PrimoWorldRP"
LIFE_URL: Optional[str] = "https://t.me/PrimoWorldRP"
RP_OWNER: Optional[str] = None
ROLES: list[str] = ["Альбедо", "Чжун Ли", "Кэйа"]
# Права администратора
ANONYMOUS: bool = False
MANAGE_CHAT: bool = True
CHANGE_INFO: bool = True
PROMOTE_MEMBERS: bool = True
RESTRICT_MEMBERS: bool = True
POST_MESSAGE: bool = True
MANAGE_TOPICS: bool = True
INVITE_USER: bool = True
DELETE_MESSAGES: bool = True
MANAGE_VIDEO_CHATS: bool = True
EDIT_MESSAGES: bool = True
PIN_MESSAGE: bool = True
POST_STORIES: bool = True
EDIT_STORIES: bool = True
DELETE_STORIES: bool = True
# ================= ВАЛИДАТОРЫ =================
@field_validator('PYTHONUNBUFFERED')
def validate_unbuffered(cls, v: str) -> str:
"""Проверка корректности значения буферизации"""
if v not in ('0', '1'):
raise ValueError("PYTHONUNBUFFERED должен быть '0' или '1'")
return v
@field_validator('PARSE_MODE')
def validate_parse_mode(cls, v: str) -> str:
"""Проверка допустимого режима разметки"""
allowed_modes: set[str] = {"HTML", "Markdown", "MarkdownV2"}
if v not in allowed_modes:
raise ValueError(f"Недопустимый PARSE_MODE. Допустимые значения: {', '.join(allowed_modes)}")
return v
@field_validator('PREFIX')
def validate_prefix(cls, v: str) -> str:
"""Очистка и проверка префиксов команд"""
cleaned: str = ''.join(sorted(set(v), key=v.index)) # Удаление дубликатов с сохранением порядка
if len(cleaned) < 1:
raise ValueError("PREFIX должен содержать хотя бы один символ")
return cleaned
@field_validator('LOG_DIR', 'LOG_FILE_INFO', mode='before')
def validate_paths(cls, v: Any) -> Path:
"""Преобразование путей в объекты Path"""
return Path(v) if isinstance(v, str) else v
@field_validator('TG_API_UID', 'MODERATOR_ID')
def validate_ids(cls, v: int) -> int:
"""Проверка корректности идентификаторов"""
if v < 0:
raise ValueError("ID не может быть отрицательным")
return v
@field_validator('WEBHOOK_URL')
def validate_webhook_url(cls, v: str) -> str:
"""Базовая проверка URL вебхука"""
parsed: ParseResult = urlparse(v)
if not all([parsed.scheme, parsed.netloc]):
raise ValueError("Некорректный URL вебхука")
return v
@field_validator('BOT_NAME', 'PROJECT_NAME', 'OWNER')
def validate_non_empty(cls, v: str) -> str:
"""Проверка непустых строк"""
if not v.strip():
raise ValueError("Поле не может быть пустым")
return v
@model_validator(mode='after')
def validate_bot_token(cls, setting: "Settings") -> "Settings":
"""Проверка наличия необходимых токенов"""
if setting.DEBUG and not setting.BOT_DEBUG_TOKEN:
raise ValueError("Требуется BOT_DEBUG_TOKEN в режиме DEBUG")
if not setting.DEBUG and not setting.BOT_TOKEN:
raise ValueError("Требуется BOT_TOKEN для рабочего режима")
return setting
@model_validator(mode='after')
def validate_webhook_config(cls, setting: "Settings") -> "Settings":
"""Проверка конфигурации вебхука"""
if setting.WEBHOOK and not setting.WEBHOOK_URL:
raise ValueError("WEBHOOK_URL обязателен при включенном WEBHOOK")
return setting
@model_validator(mode='after')
def validate_logging_paths(cls, setting: "Settings") -> "Settings":
"""Создание директорий для логов при необходимости"""
if setting.LOG_FILE and not setting.LOG_DIR.exists():
setting.LOG_DIR.mkdir(parents=True, exist_ok=True)
return setting
@model_validator(mode='after')
def set_dynamic_descriptions(cls, setting: "Settings") -> "Settings":
"""Динамическая установка описаний бота"""
if setting.BOT_DESCRIPTION is None:
setting.BOT_DESCRIPTION = f"Ваш помощник в удивительные миры! Prod. by:『{setting.OWNER}"
if setting.BOT_SHORT_DESCRIPTION is None:
setting.BOT_SHORT_DESCRIPTION = f"Тех.поддержка: {setting.OWNER}"
return setting
# ================= СВОЙСТВА =================
@property
def rights(self) -> ChatAdministratorRights:
"""Права администратора бота"""
return ChatAdministratorRights(
is_anonymous=self.ANONYMOUS,
can_manage_chat=self.MANAGE_CHAT,
can_delete_messages=self.DELETE_MESSAGES,
can_manage_video_chats=self.MANAGE_VIDEO_CHATS,
can_restrict_members=self.RESTRICT_MEMBERS,
can_promote_members=self.PROMOTE_MEMBERS,
can_change_info=self.CHANGE_INFO,
can_invite_users=self.INVITE_USER,
can_post_stories=self.POST_STORIES,
can_edit_stories=self.EDIT_STORIES,
can_delete_stories=self.DELETE_STORIES,
can_post_messages=self.POST_MESSAGE,
can_edit_messages=self.EDIT_MESSAGES,
can_pin_messages=self.PIN_MESSAGE,
can_manage_topics=self.MANAGE_TOPICS,
)
@property
def active_bot_token(self) -> str:
"""Активный токен бота в зависимости от режима"""
token = self.BOT_DEBUG_TOKEN if self.DEBUG else self.BOT_TOKEN
if not token:
raise ValueError("Активный токен бота отсутствует")
return token
@property
def log_dir_absolute(self) -> Path:
"""Абсолютный путь к директории логов"""
return self.LOG_DIR.absolute()
# Инициализация настроек
settings: Settings = Settings()
# Классы для обратной совместимости и удобства использования
class BotSettings:
"""Алиасы для настроек бота."""
DEBUG: Final[bool] = settings.DEBUG
OWNER: Final[str] = settings.OWNER
BOT_TOKEN: Final[str] = settings.active_bot_token
PARSE_MODE: Final[str] = settings.PARSE_MODE
ENCOD: Final[str] = settings.ENCOD
TIME_FORMAT: Final[str] = settings.TIME_FORMAT
PREFIX: Final[str] = settings.PREFIX
BOT_LANGUAGE: Final[str] = settings.BOT_LANGUAGE
DISABLE_NOTIFICATION: Final[bool] = settings.DISABLE_NOTIFICATION
PROTECT_CONTENT: Final[bool] = settings.PROTECT_CONTENT
ALLOW_SENDING_WITHOUT_REPLY: Final[bool] = settings.ALLOW_SENDING_WITHOUT_REPLY
LINK_PREVIEW_IS_DISABLED: Final[bool] = settings.LINK_PREVIEW_IS_DISABLED
LINK_PREVIEW_PREFER_SMALL_MEDIA: Final[bool] = settings.LINK_PREVIEW_PREFER_SMALL_MEDIA
LINK_PREVIEW_PREFER_LARGE_MEDIA: Final[bool] = settings.LINK_PREVIEW_PREFER_LARGE_MEDIA
LINK_PREVIEW_SHOW_ABOVE_TEXT: Final[bool] = settings.LINK_PREVIEW_SHOW_ABOVE_TEXT
SHOW_CAPTION_ABOVE_MEDIA: Final[bool] = settings.SHOW_CAPTION_ABOVE_MEDIA
class Permission:
"""Алиасы для разрешений."""
BOT_EDIT: Final[bool] = settings.BOT_EDIT
START_INFO_CONSOLE: Final[bool] = settings.START_INFO_CONSOLE
START_INFO_TO_FILE: Final[bool] = settings.START_INFO_TO_FILE
class LogConfig:
"""Алиасы для конфигурации логов."""
CONSOLE: Final[bool] = settings.LOG_CONSOLE
FILE: Final[bool] = settings.LOG_FILE
DIR: Final[Path] = settings.LOG_DIR
FILE_INFO: Final[Path] = settings.LOG_FILE_INFO
ROTATION: ClassVar[str] = '100 MB'
RETENTION: ClassVar[str] = '7 days'
class Webhook:
"""Алиасы для вебхука."""
WEBHOOK: Final[bool] = settings.WEBHOOK
WEBHOOK_URL: Final[str] = settings.WEBHOOK_URL
WEBHOOK_HOST: Final[str] = settings.WEBAPP_HOST
WEBHOOK_PORT: Final[int] = settings.WEBAPP_PORT
LOG_LEVEL: Final[str] = settings.LOG_LEVEL
ACCES_LOG: Final[bool] = settings.ACCES_LOG
class APISettings:
"""Алиасы для API."""
API_KEY: Final[Optional[str]] = settings.API_KEY
WEB_API_KEY: Final[Optional[str]] = settings.WEB_API_KEY
WEATHER_API_KEY: Final[Optional[str]] = settings.WEATHER_API_KEY
class UserIn:
"""Алиасы для пользовательских данных."""
TG_API_UID: Final[int] = settings.TG_API_UID
TG_API_HASH: Final[Optional[str]] = settings.TG_API_HASH
SESSION_STRING: Final[str] = settings.SESSION_STRING
class ImportantID:
"""Алиасы для важных ID."""
OWNERS_ID: Final[list[int]] = settings.OWNERS_ID
ADMIN_ID: Final[list[int]] = settings.ADMIN_ID
MODERATOR_ID: Final[int] = settings.MODERATOR_ID
IMPORTANT_ID: Final[int] = settings.IMPORTANT_ID
IMPORTANT_GROUP_ID: Final[int] = settings.IMPORTANT_GROUP_ID
IMPORTANT_CHANNEL_ID: Final[int] = settings.IMPORTANT_CHANNEL_ID
SUPPORT_CHAT_ID: Final[int] = settings.SUPPORT_CHAT_ID
SUPPORT_CHAT_ID_TOPIC: Final[int] = settings.SUPPORT_CHAT_ID_TOPIC
class BotEdit:
"""Алиасы для настроек редактирования бота."""
ALLOW: Final[bool] = settings.BOT_EDIT
PROJECT_NAME: Final[str] = settings.PROJECT_NAME
NAME: Final[str] = settings.BOT_NAME
DESCRIPTION: Final[str] = settings.BOT_DESCRIPTION
SHORT_DESCRIPTION: Final[str] = settings.BOT_SHORT_DESCRIPTION
ANONYMOUS: Final[bool] = settings.ANONYMOUS
MANAGE_CHAT: Final[bool] = settings.MANAGE_CHAT
CHANGE_INFO: Final[bool] = settings.CHANGE_INFO
PROMOTE_MEMBERS: Final[bool] = settings.PROMOTE_MEMBERS
RESTRICT_MEMBERS: Final[bool] = settings.RESTRICT_MEMBERS
POST_MESSAGE: Final[bool] = settings.POST_MESSAGE
MANAGE_TOPICS: Final[bool] = settings.MANAGE_TOPICS
INVITE_USER: Final[bool] = settings.INVITE_USER
DELETE_MESSAGES: Final[bool] = settings.DELETE_MESSAGES
MANAGE_VIDEO_CHATS: Final[bool] = settings.MANAGE_VIDEO_CHATS
EDIT_MESSAGES: Final[bool] = settings.EDIT_MESSAGES
PIN_MESSAGE: Final[bool] = settings.PIN_MESSAGE
POST_STORIES: Final[bool] = settings.POST_STORIES
EDIT_STORIES: Final[bool] = settings.EDIT_STORIES
DELETE_STORIES: Final[bool] = settings.DELETE_STORIES
RIGHTS: Final[ChatAdministratorRights] = settings.rights
class RpValue:
"""Переменные связанные с ролевым проектом."""
RP_NAME: Final[str] = settings.RP_NAME
INFO_URL: str = settings.INFO_URL
FLUD_URL: str = settings.FLUD_URL
RP_URL: str = settings.RP_URL
LIFE_URL: str = settings.LIFE_URL
RP_OWNER: str = settings.RP_OWNER
ROLES: list[str] = settings.ROLES
class Project:
POSTS_DIR: ClassVar[Path] = Path('posts')
class Lists:
"""Интересные списки фактов, цитат и анекдотов."""
facts: list[str] = [
"Python был создан Гвидо ван Россумом в 1991 году.",
"Имена Python и Monty Python связаны — язык назван в честь шоу.",
"Python — язык с динамической типизацией.",
"В Python всё является объектом, даже функции и типы данных.",
"Списки в Python — это изменяемые коллекции, в отличие от кортежей.",
"Python поддерживает парадигмы ООП, функционального и императивного программирования.",
"Zen of Python можно увидеть, набрав `import this` в интерпретаторе.",
]
jokes: list[str] = [
"1",
"2",
"3",
"4",
]
quotes: list[str] = [
"5",
"6",
"7",
"8",
]
# Экспорт совместимых компонентов
__all__ = (
"BotSettings",
"LogConfig",
"Webhook",
"APISettings",
"UserIn",
"ImportantID",
"Permission",
"BotEdit",
"Project",
"RpValue",
'settings',
'Lists',
)

328
configs/roles.py Normal file
View File

@@ -0,0 +1,328 @@
from typing import Dict
from database import RoleRegion
# Настройка экспорта
__all__ = ("genshin_roles", "hsr_roles", "ID_TO_ROLE", "all_roles",)
# Словарь с ID пользователей и их ролями
ID_TO_ROLE: Dict[int, str] = {
6639261502: "Рацио",
7435095514: "Панталоне",
6250345032: "Сандэй",
5683309573: "Хохо",
833230790: "Сампо",
6688236743: "Аглая",
459453807: "Флинс",
7831579419: "Анакса",
7749831743: "Венти",
1364984004: "Аргенти",
1369873051: "Альбедо",
1222399228: "Химеко",
8199185983: "Лоча",
7576341592: "Фуга",
5426987140: "Варка",
1316852704: "Аха",
1764269904: "Цзин Юань",
1992416693: "Бутхилл",
1314539668: "Кафка",
1207917053: "Топаз",
5025299829: "Вельт",
991994028: "Авантюрин",
1362425172: "Цифер",
2006013059: "Жуань Мэй",
7794291575: "Стивен Ллойд",
6751720805: "Дотторе",
5260895056: "Фэйсяо",
1438721683: "Бай Чжу",
}
genshin_roles: list = [
# Мондштадт
("Альбедо", RoleRegion.MONDSTADT),
("Барбара", RoleRegion.MONDSTADT),
("Беннет", RoleRegion.MONDSTADT),
("Венти", RoleRegion.MONDSTADT),
("Далия", RoleRegion.MONDSTADT),
("Джинн", RoleRegion.MONDSTADT),
("Дилюк", RoleRegion.MONDSTADT),
("Диона", RoleRegion.MONDSTADT),
("Кли", RoleRegion.MONDSTADT),
("Кэйа", RoleRegion.MONDSTADT),
("Лиза", RoleRegion.MONDSTADT),
("Мика", RoleRegion.MONDSTADT),
("Мона", RoleRegion.MONDSTADT),
("Ноэлль", RoleRegion.MONDSTADT),
("Розария", RoleRegion.MONDSTADT),
("Рэйзор", RoleRegion.MONDSTADT),
("Сахароза", RoleRegion.MONDSTADT),
("Фишль", RoleRegion.MONDSTADT),
("Эмбер", RoleRegion.MONDSTADT),
("Эола", RoleRegion.MONDSTADT),
# Ли Юэ
("Бай Чжу", RoleRegion.LIYUE),
("Бэй Доу", RoleRegion.LIYUE),
("Гань Юй", RoleRegion.LIYUE),
("Е Лань", RoleRegion.LIYUE),
("Ка Мин", RoleRegion.LIYUE),
("Кэ Цин", RoleRegion.LIYUE),
("Лань Янь", RoleRegion.LIYUE),
("Нин Гуан", RoleRegion.LIYUE),
("Син Цю", RoleRegion.LIYUE),
("Синь Янь", RoleRegion.LIYUE),
("Сян Лин", RoleRegion.LIYUE),
("Сянь Юнь", RoleRegion.LIYUE),
("Сяо", RoleRegion.LIYUE),
("Ху Тао", RoleRegion.LIYUE),
("Ци Ци", RoleRegion.LIYUE),
("Чжун Ли", RoleRegion.LIYUE),
("Чун Юнь", RoleRegion.LIYUE),
("Шэнь Хэ", RoleRegion.LIYUE),
("Юнь Цзинь", RoleRegion.LIYUE),
("Янь Фэй", RoleRegion.LIYUE),
("Яо Яо", RoleRegion.LIYUE),
# Инадзума
("Аяка", RoleRegion.INAZUMA),
("Аято", RoleRegion.INAZUMA),
("Горо", RoleRegion.INAZUMA),
("Ёимия", RoleRegion.INAZUMA),
("Итто", RoleRegion.INAZUMA),
("Кадзуха", RoleRegion.INAZUMA),
("Кирара", RoleRegion.INAZUMA),
("Кокоми", RoleRegion.INAZUMA),
("Мидзуки", RoleRegion.INAZUMA),
("Райдэн Макото", RoleRegion.INAZUMA),
("Райдэн Эи", RoleRegion.INAZUMA),
("Сара", RoleRegion.INAZUMA),
("Саю", RoleRegion.INAZUMA),
("Синобу", RoleRegion.INAZUMA),
("Тиори", RoleRegion.INAZUMA),
("Тома", RoleRegion.INAZUMA),
("Хэйдзо", RoleRegion.INAZUMA),
("Яэ Мико", RoleRegion.INAZUMA),
# Сумеру
("Аль-Хайтам", RoleRegion.SUMERU),
("Дори", RoleRegion.SUMERU),
("Дэхья", RoleRegion.SUMERU),
("Кавех", RoleRegion.SUMERU),
("Кандакия", RoleRegion.SUMERU),
("Коллеи", RoleRegion.SUMERU),
("Лайла", RoleRegion.SUMERU),
("Нахида", RoleRegion.SUMERU),
("Нилу", RoleRegion.SUMERU),
("Руккхадевата", RoleRegion.SUMERU),
("Сайно", RoleRegion.SUMERU),
("Сетос", RoleRegion.SUMERU),
("Странник", RoleRegion.SUMERU),
("Тигнари", RoleRegion.SUMERU),
("Фарузан", RoleRegion.SUMERU),
# Фонтейн
("Клоринда", RoleRegion.FONTAINE),
("Линетт", RoleRegion.FONTAINE),
("Лини", RoleRegion.FONTAINE),
("Навия", RoleRegion.FONTAINE),
("Нёвиллет", RoleRegion.FONTAINE),
("Ризли", RoleRegion.FONTAINE),
("Сиджвин", RoleRegion.FONTAINE),
("Фокалорс", RoleRegion.FONTAINE),
("Фремине", RoleRegion.FONTAINE),
("Фурина", RoleRegion.FONTAINE),
("Шарлотта", RoleRegion.FONTAINE),
("Шеврёз", RoleRegion.FONTAINE),
("Эмилия", RoleRegion.FONTAINE),
("Эскофье", RoleRegion.FONTAINE),
# Натлан
("Ахав", RoleRegion.NATLAN),
("Вареса", RoleRegion.NATLAN),
("Иансан", RoleRegion.NATLAN),
("Ифа", RoleRegion.NATLAN),
("Качина", RoleRegion.NATLAN),
("Кинич", RoleRegion.NATLAN),
("Мавуика", RoleRegion.NATLAN),
("Муалани", RoleRegion.NATLAN),
("Оророн", RoleRegion.NATLAN),
("Ситлали", RoleRegion.NATLAN),
("Часка", RoleRegion.NATLAN),
("Шилонен", RoleRegion.NATLAN),
# Снежная
("Арлекино", RoleRegion.SNEZHNAYA),
("Дотторе", RoleRegion.SNEZHNAYA),
("Капитано", RoleRegion.SNEZHNAYA),
("Коломбина", RoleRegion.SNEZHNAYA),
("Панталоне", RoleRegion.SNEZHNAYA),
("Пульчинелла", RoleRegion.SNEZHNAYA),
("Пьеро", RoleRegion.SNEZHNAYA),
("Сандроне", RoleRegion.SNEZHNAYA),
("Синьора", RoleRegion.SNEZHNAYA),
("Царица", RoleRegion.SNEZHNAYA),
("Тарталья", RoleRegion.SNEZHNAYA),
# Каэнри'ах
("Айно", RoleRegion.KHAENRIAH),
("Алиса", RoleRegion.KHAENRIAH),
("Варка", RoleRegion.KHAENRIAH),
("Дурин", RoleRegion.KHAENRIAH),
("Инеффа", RoleRegion.KHAENRIAH),
("Лаума", RoleRegion.KHAENRIAH),
("Нефер", RoleRegion.KHAENRIAH),
("Николь", RoleRegion.KHAENRIAH),
("Флинс", RoleRegion.KHAENRIAH),
("Ягода", RoleRegion.KHAENRIAH),
# Другие (Genshin Impact)
("Дайнслейф", RoleRegion.GENSHIN_OTHER),
("Итэр", RoleRegion.GENSHIN_OTHER),
("Люмин", RoleRegion.GENSHIN_OTHER),
("Паймон", RoleRegion.GENSHIN_OTHER),
("Рэйндоттир", RoleRegion.GENSHIN_OTHER),
("Скирк", RoleRegion.GENSHIN_OTHER),
("Элой", RoleRegion.GENSHIN_OTHER),
]
# Роли для Honkai: Star Rail
hsr_roles: list = [
# Звездный экспресс
("Вельт", RoleRegion.HSR_STAR),
("Дань Хэн", RoleRegion.HSR_STAR),
("Келус", RoleRegion.HSR_STAR),
("Март 7", RoleRegion.HSR_STAR),
("Стелла", RoleRegion.HSR_STAR),
("Химеко", RoleRegion.HSR_STAR),
# Космическая станция Герта
("Арлан", RoleRegion.HSR_GERTA),
("Аста", RoleRegion.HSR_GERTA),
("Великая Герта", RoleRegion.HSR_GERTA),
("Жуань Мэй", RoleRegion.HSR_GERTA),
("Полька Какамонд", RoleRegion.HSR_GERTA),
("Скрюллум", RoleRegion.HSR_GERTA),
("Стивен Ллойд", RoleRegion.HSR_GERTA),
# Ярило-VI
("Броня", RoleRegion.HSR_YARILO),
("Гепард", RoleRegion.HSR_YARILO),
("Зеле", RoleRegion.HSR_YARILO),
("Клара", RoleRegion.HSR_YARILO),
("Коколия", RoleRegion.HSR_YARILO),
("Лука", RoleRegion.HSR_YARILO),
("Наташа", RoleRegion.HSR_YARILO),
("Пела", RoleRegion.HSR_YARILO),
("Рысь", RoleRegion.HSR_YARILO),
("Сампо", RoleRegion.HSR_YARILO),
("Сервал", RoleRegion.HSR_YARILO),
("Хук", RoleRegion.HSR_YARILO),
# Лофу Сяньчжоу
("Байлу", RoleRegion.HSR_LOFU),
("Байхэн", RoleRegion.HSR_LOFU),
("Гуйнайфэнь", RoleRegion.HSR_LOFU),
("Линша", RoleRegion.HSR_LOFU),
("Лоча", RoleRegion.HSR_LOFU),
("Моцзэ", RoleRegion.HSR_LOFU),
("Сушан", RoleRegion.HSR_LOFU),
("Сюэи", RoleRegion.HSR_LOFU),
("Фу Сюань", RoleRegion.HSR_LOFU),
("Фуга", RoleRegion.HSR_LOFU),
("Фэйсяо", RoleRegion.HSR_LOFU),
("Ханья", RoleRegion.HSR_LOFU),
("Хохо", RoleRegion.HSR_LOFU),
("Цзинлю", RoleRegion.HSR_LOFU),
("Цзин Юань", RoleRegion.HSR_LOFU),
("Цзяоцю", RoleRegion.HSR_LOFU),
("Цинцюэ", RoleRegion.HSR_LOFU),
("Юйкун", RoleRegion.HSR_LOFU),
("Юньли", RoleRegion.HSR_LOFU),
("Яньцин", RoleRegion.HSR_LOFU),
# Пенакония
("Ахерон", RoleRegion.HSR_PENACONY),
("Воскресенье", RoleRegion.HSR_PENACONY),
("Галлахер", RoleRegion.HSR_PENACONY),
("Мистер Река", RoleRegion.HSR_PENACONY),
("Зарянка", RoleRegion.HSR_PENACONY),
("Искорка", RoleRegion.HSR_PENACONY),
("Миша", RoleRegion.HSR_PENACONY),
("Рацио", RoleRegion.HSR_PENACONY),
("Чёрный Лебедь", RoleRegion.HSR_PENACONY),
# Амфореус
("Аглая", RoleRegion.HSR_AMPHOREUS),
("Анаксагор", RoleRegion.HSR_AMPHOREUS),
("Гиацина", RoleRegion.HSR_AMPHOREUS),
("Гисиленса", RoleRegion.HSR_AMPHOREUS),
("Кастория", RoleRegion.HSR_AMPHOREUS),
("Керидра", RoleRegion.HSR_AMPHOREUS),
("Кирена", RoleRegion.HSR_AMPHOREUS),
("Ликург", RoleRegion.HSR_AMPHOREUS),
("Мидей", RoleRegion.HSR_AMPHOREUS),
("Трибби", RoleRegion.HSR_AMPHOREUS),
("Фаенон", RoleRegion.HSR_AMPHOREUS),
("Цифер", RoleRegion.HSR_AMPHOREUS),
# Охотники за Стеллар
("Блэйд", RoleRegion.HSR_HUNTER),
("Кафка", RoleRegion.HSR_HUNTER),
("Светлячок", RoleRegion.HSR_HUNTER),
("Серебряный Волк", RoleRegion.HSR_HUNTER),
("Элио", RoleRegion.HSR_HUNTER),
# КММ
("Авантюрин", RoleRegion.HSR_KMM),
("Агат", RoleRegion.HSR_KMM),
("Алмаз", RoleRegion.HSR_KMM),
("Обсидиан", RoleRegion.HSR_KMM),
("Опал", RoleRegion.HSR_KMM),
("Перламутр", RoleRegion.HSR_KMM),
("Сапфир", RoleRegion.HSR_KMM),
("Сугилит", RoleRegion.HSR_KMM),
("Топаз", RoleRegion.HSR_KMM),
("Янтарь", RoleRegion.HSR_KMM),
("Яшма", RoleRegion.HSR_KMM),
# Эоны
("Акивили", RoleRegion.HSR_EONS),
("Аха", RoleRegion.HSR_EONS),
("Клипот", RoleRegion.HSR_EONS),
("Лань", RoleRegion.HSR_EONS),
("Нанук", RoleRegion.HSR_EONS),
("Нус", RoleRegion.HSR_EONS),
("Ороборос", RoleRegion.HSR_EONS),
("Тайззиронт", RoleRegion.HSR_EONS),
("Фили", RoleRegion.HSR_EONS),
("Шипе", RoleRegion.HSR_EONS),
("Эна", RoleRegion.HSR_EONS),
("Яоши", RoleRegion.HSR_EONS),
("IX", RoleRegion.HSR_EONS),
# Вечногорящий особняк
("Акаш", RoleRegion.HSR_FIRE_MANSION),
("Герцог Инферно", RoleRegion.HSR_FIRE_MANSION),
("Дубра", RoleRegion.HSR_FIRE_MANSION),
("Катерина", RoleRegion.HSR_FIRE_MANSION),
("Констанция", RoleRegion.HSR_FIRE_MANSION),
# Лорды Опустошители
("Асат Прамад", RoleRegion.HSR_LORDS),
("Зефиро", RoleRegion.HSR_LORDS),
("Оростелла", RoleRegion.HSR_LORDS),
("Фантилия", RoleRegion.HSR_LORDS),
# Прочие (Honkai: Star Rail)
("Аргенти", RoleRegion.HSR_OTHER),
("Бутхилл", RoleRegion.HSR_OTHER),
("Раппа", RoleRegion.HSR_OTHER),
("Архив Пустоты", RoleRegion.HSR_OTHER),
# Фейт
("Арчер", RoleRegion.HSR_FATE),
("Сейбер", RoleRegion.HSR_FATE),
]
# Общий список ролей
all_roles: list = genshin_roles + hsr_roles

12
data/economy.json Normal file
View File

@@ -0,0 +1,12 @@
{
"6751720805": {
"balance": 0,
"username": "verdise",
"full_name": "Лейн"
},
"7051557370": {
"balance": 23,
"username": "exetreon",
"full_name": ""
}
}

1
database/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .database import *

1182
database/database.py Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More