First commit
This commit is contained in:
37
.dockerignore
Normal file
37
.dockerignore
Normal 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
92
.env_example
Normal 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
8
.idea/.gitignore
generated
vendored
Normal 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
19
.idea/app.iml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
26
Dockerfile
Normal 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
21
LICENSE
Normal 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.
|
||||
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
96
api/server.py
Normal file
96
api/server.py
Normal 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
BIN
assets/default.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 554 KiB |
BIN
assets/start.jpg
Normal file
BIN
assets/start.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 430 KiB |
3
bot/__init__.py
Normal file
3
bot/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .core import *
|
||||
from .handlers import *
|
||||
from .middlewares import *
|
||||
2
bot/core/__init__.py
Normal file
2
bot/core/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .bots import *
|
||||
from .webhook import *
|
||||
260
bot/core/bots.py
Normal file
260
bot/core/bots.py
Normal 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
53
bot/core/webhook.py
Normal 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
1
bot/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .topic_map import *
|
||||
4
bot/data/topic_map.py
Normal file
4
bot/data/topic_map.py
Normal 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
5
bot/filters/__init__.py
Normal 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
35
bot/filters/callback.py
Normal 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
152
bot/filters/chat_rights.py
Normal 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
33
bot/filters/chat_type.py
Normal 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"}
|
||||
71
bot/filters/message_content.py
Normal file
71
bot/filters/message_content.py
Normal 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
41
bot/filters/subscrided.py
Normal 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
21
bot/handlers/__init__.py
Normal 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,
|
||||
)
|
||||
21
bot/handlers/commands/__init__.py
Normal file
21
bot/handlers/commands/__init__.py
Normal 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,
|
||||
|
||||
)
|
||||
18
bot/handlers/commands/admins/__init__.py
Normal file
18
bot/handlers/commands/admins/__init__.py
Normal 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,
|
||||
|
||||
)
|
||||
80
bot/handlers/commands/admins/all_cmd.py
Normal file
80
bot/handlers/commands/admins/all_cmd.py
Normal 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}")
|
||||
258
bot/handlers/commands/admins/ban_cmd.py
Normal file
258
bot/handlers/commands/admins/ban_cmd.py
Normal 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("❌ Не удалось получить список забаненных пользователей")
|
||||
278
bot/handlers/commands/admins/kick_cmd.py
Normal file
278
bot/handlers/commands/admins/kick_cmd.py
Normal 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)
|
||||
0
bot/handlers/commands/admins/mute_cmd.py
Normal file
0
bot/handlers/commands/admins/mute_cmd.py
Normal file
55
bot/handlers/commands/admins/pin_cmd.py
Normal file
55
bot/handlers/commands/admins/pin_cmd.py
Normal 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("✅ Сообщение закреплено")
|
||||
51
bot/handlers/commands/admins/settings_cmd.py
Normal file
51
bot/handlers/commands/admins/settings_cmd.py
Normal 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)
|
||||
0
bot/handlers/commands/admins/varn_cmd.py
Normal file
0
bot/handlers/commands/admins/varn_cmd.py
Normal file
19
bot/handlers/commands/settings/__init__.py
Normal file
19
bot/handlers/commands/settings/__init__.py
Normal 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,
|
||||
)
|
||||
167
bot/handlers/commands/settings/set_description_cmd.py
Normal file
167
bot/handlers/commands/settings/set_description_cmd.py
Normal 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)
|
||||
151
bot/handlers/commands/settings/set_name_cmd.py
Normal file
151
bot/handlers/commands/settings/set_name_cmd.py
Normal 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)
|
||||
168
bot/handlers/commands/settings/set_widget_cmd.py
Normal file
168
bot/handlers/commands/settings/set_widget_cmd.py
Normal 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)
|
||||
48
bot/handlers/commands/settings/settings_cmd.py
Normal file
48
bot/handlers/commands/settings/settings_cmd.py
Normal 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)
|
||||
9
bot/handlers/commands/special/__init__.py
Normal file
9
bot/handlers/commands/special/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from aiogram import Router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
# router.include_routers(
|
||||
# )
|
||||
22
bot/handlers/commands/users/__init__.py
Normal file
22
bot/handlers/commands/users/__init__.py
Normal 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,
|
||||
)
|
||||
42
bot/handlers/commands/users/active.py
Normal file
42
bot/handlers/commands/users/active.py
Normal 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, )
|
||||
117
bot/handlers/commands/users/anon.py
Normal file
117
bot/handlers/commands/users/anon.py
Normal 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()
|
||||
27
bot/handlers/commands/users/cancel_cmd.py
Normal file
27
bot/handlers/commands/users/cancel_cmd.py
Normal 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)
|
||||
49
bot/handlers/commands/users/create_cmd.py
Normal file
49
bot/handlers/commands/users/create_cmd.py
Normal 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)
|
||||
|
||||
368
bot/handlers/commands/users/new_cmd.py
Normal file
368
bot/handlers/commands/users/new_cmd.py
Normal 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}")
|
||||
68
bot/handlers/commands/users/start_cmd.py
Normal file
68
bot/handlers/commands/users/start_cmd.py
Normal 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()
|
||||
58
bot/handlers/commands/users/union_cmd.py
Normal file
58
bot/handlers/commands/users/union_cmd.py
Normal 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)
|
||||
12
bot/handlers/custom/__init__.py
Normal file
12
bot/handlers/custom/__init__.py
Normal 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,
|
||||
)
|
||||
286
bot/handlers/custom/econom.py
Normal file
286
bot/handlers/custom/econom.py
Normal 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"]
|
||||
16
bot/handlers/form_utils/__init__.py
Normal file
16
bot/handlers/form_utils/__init__.py
Normal 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,
|
||||
)
|
||||
45
bot/handlers/form_utils/form_answer.py
Normal file
45
bot/handlers/form_utils/form_answer.py
Normal 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) # <--- Передаём оба аргумента
|
||||
)
|
||||
|
||||
|
||||
44
bot/handlers/form_utils/form_callback.py
Normal file
44
bot/handlers/form_utils/form_callback.py
Normal 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("Ответ отправлен пользователю.")
|
||||
29
bot/handlers/form_utils/topic_replies.py
Normal file
29
bot/handlers/form_utils/topic_replies.py
Normal 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}")
|
||||
|
||||
15
bot/handlers/messages/__init__.py
Normal file
15
bot/handlers/messages/__init__.py
Normal 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)
|
||||
14
bot/handlers/messages/default.py
Normal file
14
bot/handlers/messages/default.py
Normal 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)
|
||||
11
bot/handlers/messages/default_msg.py
Normal file
11
bot/handlers/messages/default_msg.py
Normal 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
|
||||
32
bot/handlers/messages/ping_test.py
Normal file
32
bot/handlers/messages/ping_test.py
Normal 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
|
||||
11
bot/handlers/union_utills/__init__.py
Normal file
11
bot/handlers/union_utills/__init__.py
Normal 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,
|
||||
)
|
||||
44
bot/handlers/union_utills/union_handlers.py
Normal file
44
bot/handlers/union_utills/union_handlers.py
Normal 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)
|
||||
|
||||
2
bot/keyboards/__init__.py
Normal file
2
bot/keyboards/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .inline import *
|
||||
from .reply import *
|
||||
1
bot/keyboards/inline/__init__.py
Normal file
1
bot/keyboards/inline/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .decision import *
|
||||
18
bot/keyboards/inline/decision.py
Normal file
18
bot/keyboards/inline/decision.py
Normal 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()
|
||||
0
bot/keyboards/reply/__init__.py
Normal file
0
bot/keyboards/reply/__init__.py
Normal file
55
bot/middlewares/__init__.py
Normal file
55
bot/middlewares/__init__.py
Normal 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)
|
||||
108
bot/middlewares/ban_user_mdw.py
Normal file
108
bot/middlewares/ban_user_mdw.py
Normal 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
|
||||
201
bot/middlewares/error_mdw.py
Normal file
201
bot/middlewares/error_mdw.py
Normal 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
|
||||
)
|
||||
272
bot/middlewares/logging_mdw.py
Normal file
272
bot/middlewares/logging_mdw.py
Normal 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
|
||||
57
bot/middlewares/msg_mdw.py
Normal file
57
bot/middlewares/msg_mdw.py
Normal 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,
|
||||
)
|
||||
59
bot/middlewares/referal_mdw.py
Normal file
59
bot/middlewares/referal_mdw.py
Normal 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)
|
||||
98
bot/middlewares/spam_mdw.py
Normal file
98
bot/middlewares/spam_mdw.py
Normal 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)
|
||||
110
bot/middlewares/subscription_mdw.py
Normal file
110
bot/middlewares/subscription_mdw.py
Normal 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)
|
||||
83
bot/middlewares/time_mdw.py
Normal file
83
bot/middlewares/time_mdw.py
Normal 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
2
bot/states/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .anketa_states import *
|
||||
from .new_states import *
|
||||
5
bot/states/anketa_states.py
Normal file
5
bot/states/anketa_states.py
Normal 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
10
bot/states/new_states.py
Normal 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()
|
||||
5
bot/states/union_states.py
Normal file
5
bot/states/union_states.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# bot/states/union_states.py
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
class UnionStates(StatesGroup):
|
||||
waiting_for_union = State()
|
||||
1
bot/templates/__init__.py
Normal file
1
bot/templates/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .message_callback import *
|
||||
88
bot/templates/message_callback.py
Normal file
88
bot/templates/message_callback.py
Normal 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
9
bot/utils/__init__.py
Normal 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
61
bot/utils/argument.py
Normal 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
19
bot/utils/auto_delete.py
Normal 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
17
bot/utils/clear_status.py
Normal 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
23
bot/utils/format_time.py
Normal 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} секунд"
|
||||
21
bot/utils/hidden_username.py
Normal file
21
bot/utils/hidden_username.py
Normal 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}"
|
||||
29
bot/utils/interesting_facts.py
Normal file
29
bot/utils/interesting_facts.py
Normal 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
29
bot/utils/pagination.py
Normal 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
85
bot/utils/type_message.py
Normal 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
23
bot/utils/usernames.py
Normal 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
3
configs/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .cmd_list import *
|
||||
from .config import *
|
||||
from .roles import *
|
||||
104
configs/cmd_list.py
Normal file
104
configs/cmd_list.py
Normal 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
399
configs/config.py
Normal 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
328
configs/roles.py
Normal 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
12
data/economy.json
Normal 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
1
database/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .database import *
|
||||
1182
database/database.py
Normal file
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
Reference in New Issue
Block a user