Add middleware and refactor admin handlers for improved functionality
- Introduced `DependenciesMiddleware` and `BlacklistMiddleware` for enhanced request handling across all routers. - Refactored admin handlers to utilize new middleware, improving access control and error handling. - Updated the `admin_router` to include middleware for access checks and streamlined the process of banning users. - Enhanced the structure of admin handler imports for better organization and maintainability. - Improved error handling in various admin functions to ensure robust user interactions.
This commit is contained in:
@@ -1 +1,37 @@
|
|||||||
from .admin_handlers import admin_router
|
from .admin_handlers import admin_router
|
||||||
|
from .dependencies import AdminAccessMiddleware, BotDB, Settings
|
||||||
|
from .services import AdminService, User, BannedUser
|
||||||
|
from .exceptions import (
|
||||||
|
AdminError,
|
||||||
|
AdminAccessDeniedError,
|
||||||
|
UserNotFoundError,
|
||||||
|
InvalidInputError,
|
||||||
|
UserAlreadyBannedError
|
||||||
|
)
|
||||||
|
from .utils import (
|
||||||
|
return_to_admin_menu,
|
||||||
|
handle_admin_error,
|
||||||
|
format_user_info,
|
||||||
|
format_ban_confirmation,
|
||||||
|
escape_html
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'admin_router',
|
||||||
|
'AdminAccessMiddleware',
|
||||||
|
'BotDB',
|
||||||
|
'Settings',
|
||||||
|
'AdminService',
|
||||||
|
'User',
|
||||||
|
'BannedUser',
|
||||||
|
'AdminError',
|
||||||
|
'AdminAccessDeniedError',
|
||||||
|
'UserNotFoundError',
|
||||||
|
'InvalidInputError',
|
||||||
|
'UserAlreadyBannedError',
|
||||||
|
'return_to_admin_menu',
|
||||||
|
'handle_admin_error',
|
||||||
|
'format_user_info',
|
||||||
|
'format_ban_confirmation',
|
||||||
|
'escape_html'
|
||||||
|
]
|
||||||
@@ -1,52 +1,55 @@
|
|||||||
import traceback
|
|
||||||
import html
|
|
||||||
|
|
||||||
from aiogram import Router, types, F
|
from aiogram import Router, types, F
|
||||||
from aiogram.filters import Command, StateFilter
|
from aiogram.filters import Command, StateFilter, MagicData
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
from helper_bot.filters.main import ChatTypeFilter
|
from helper_bot.filters.main import ChatTypeFilter
|
||||||
from helper_bot.keyboards.keyboards import get_reply_keyboard_admin, create_keyboard_with_pagination, \
|
from helper_bot.keyboards.keyboards import (
|
||||||
create_keyboard_for_ban_days, create_keyboard_for_approve_ban, create_keyboard_for_ban_reason
|
get_reply_keyboard_admin,
|
||||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
create_keyboard_with_pagination,
|
||||||
from helper_bot.utils.helper_func import check_access, add_days_to_date, get_banned_users_buttons, get_banned_users_list
|
create_keyboard_for_ban_days,
|
||||||
|
create_keyboard_for_approve_ban,
|
||||||
|
create_keyboard_for_ban_reason
|
||||||
|
)
|
||||||
|
from helper_bot.handlers.admin.dependencies import AdminAccessMiddleware
|
||||||
|
from helper_bot.handlers.admin.services import AdminService
|
||||||
|
from helper_bot.handlers.admin.exceptions import (
|
||||||
|
UserAlreadyBannedError,
|
||||||
|
InvalidInputError
|
||||||
|
)
|
||||||
|
from helper_bot.handlers.admin.utils import (
|
||||||
|
return_to_admin_menu,
|
||||||
|
handle_admin_error,
|
||||||
|
format_user_info,
|
||||||
|
format_ban_confirmation,
|
||||||
|
escape_html
|
||||||
|
)
|
||||||
from logs.custom_logger import logger
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
# Создаем роутер с middleware для проверки доступа
|
||||||
admin_router = Router()
|
admin_router = Router()
|
||||||
|
admin_router.message.middleware(AdminAccessMiddleware())
|
||||||
|
|
||||||
bdf = get_global_instance()
|
|
||||||
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
|
|
||||||
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
|
|
||||||
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
|
|
||||||
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
|
|
||||||
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
|
|
||||||
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
|
|
||||||
LOGS = bdf.settings['Settings']['logs']
|
|
||||||
TEST = bdf.settings['Settings']['test']
|
|
||||||
|
|
||||||
BotDB = bdf.get_db()
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ХЕНДЛЕРЫ МЕНЮ
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
@admin_router.message(
|
@admin_router.message(
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
Command('admin')
|
Command('admin')
|
||||||
)
|
)
|
||||||
async def admin_panel(message: types.Message, state: FSMContext):
|
async def admin_panel(
|
||||||
|
message: types.Message,
|
||||||
|
state: FSMContext
|
||||||
|
):
|
||||||
|
"""Главное меню администратора"""
|
||||||
try:
|
try:
|
||||||
if check_access(message.from_user.id, BotDB):
|
|
||||||
await state.set_state("ADMIN")
|
await state.set_state("ADMIN")
|
||||||
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
|
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
|
||||||
markup = get_reply_keyboard_admin()
|
markup = get_reply_keyboard_admin()
|
||||||
await message.answer("Добро пожаловать в админку. Выбери что хочешь:",
|
await message.answer("Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup)
|
||||||
reply_markup=markup)
|
|
||||||
else:
|
|
||||||
await message.answer('Доступ запрещен, досвидания!')
|
|
||||||
await state.set_state("START")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при запуске админ панели: {e}")
|
await handle_admin_error(message, e, state, "admin_panel")
|
||||||
await message.bot.send_message(IMPORTANT_LOGS,
|
|
||||||
f'Ошибка в функции admin_panel {e}. Traceback: {traceback.format_exc()}')
|
|
||||||
await state.set_state("START")
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
@admin_router.message(
|
||||||
@@ -54,150 +57,30 @@ async def admin_panel(message: types.Message, state: FSMContext):
|
|||||||
StateFilter("ADMIN"),
|
StateFilter("ADMIN"),
|
||||||
F.text == 'Бан (Список)'
|
F.text == 'Бан (Список)'
|
||||||
)
|
)
|
||||||
async def get_last_users(message: types.Message, state: FSMContext):
|
async def get_last_users(
|
||||||
# Дополнительная проверка на админские права
|
message: types.Message,
|
||||||
if not check_access(message.from_user.id, BotDB):
|
state: FSMContext,
|
||||||
await message.answer('Доступ запрещен!')
|
bot_db: MagicData("bot_db")
|
||||||
await state.set_state("START")
|
):
|
||||||
return
|
"""Получение списка последних пользователей"""
|
||||||
logger.info(
|
|
||||||
f"Попытка получения списка последних пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
|
|
||||||
list_users = BotDB.get_last_users_from_db()
|
|
||||||
keyboard = create_keyboard_with_pagination(1, len(list_users), list_users, 'ban')
|
|
||||||
await message.answer(text="Список пользователей которые последними обращались к боту",
|
|
||||||
reply_markup=keyboard)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
|
||||||
StateFilter("ADMIN"),
|
|
||||||
F.text == 'Бан по нику'
|
|
||||||
)
|
|
||||||
async def ban_by_nickname(message: types.Message, state: FSMContext):
|
|
||||||
# Дополнительная проверка на админские права
|
|
||||||
if not check_access(message.from_user.id, BotDB):
|
|
||||||
await message.answer('Доступ запрещен!')
|
|
||||||
await state.set_state("START")
|
|
||||||
return
|
|
||||||
await message.answer('Пришли мне username блокируемого пользователя')
|
|
||||||
await state.set_state('PRE_BAN')
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
|
||||||
StateFilter("ADMIN"),
|
|
||||||
F.text == 'Бан по ID'
|
|
||||||
)
|
|
||||||
async def ban_by_id(message: types.Message, state: FSMContext):
|
|
||||||
# Дополнительная проверка на админские права
|
|
||||||
if not check_access(message.from_user.id, BotDB):
|
|
||||||
await message.answer('Доступ запрещен!')
|
|
||||||
await state.set_state("START")
|
|
||||||
return
|
|
||||||
await message.answer('Пришли мне ID блокируемого пользователя')
|
|
||||||
await state.set_state('PRE_BAN_ID')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
|
||||||
StateFilter("PRE_BAN", "PRE_BAN_ID", "BAN_2"),
|
|
||||||
F.text == 'Отменить'
|
|
||||||
)
|
|
||||||
async def decline_ban(message: types.Message, state: FSMContext):
|
|
||||||
# Дополнительная проверка на админские права
|
|
||||||
if not check_access(message.from_user.id, BotDB):
|
|
||||||
await message.answer('Доступ запрещен!')
|
|
||||||
await state.set_state("START")
|
|
||||||
return
|
|
||||||
current_state = await state.get_state()
|
|
||||||
await state.set_data({})
|
|
||||||
await state.set_state("ADMIN")
|
|
||||||
logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
|
|
||||||
markup = get_reply_keyboard_admin()
|
|
||||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
|
||||||
StateFilter("PRE_BAN")
|
|
||||||
)
|
|
||||||
async def ban_by_nickname_step_2(message: types.Message, state: FSMContext):
|
|
||||||
# Дополнительная проверка на админские права
|
|
||||||
if not check_access(message.from_user.id, BotDB):
|
|
||||||
await message.answer('Доступ запрещен!')
|
|
||||||
await state.set_state("START")
|
|
||||||
return
|
|
||||||
logger.info(
|
|
||||||
f"Функция ban_by_nickname_2. Получен никнейм пользователя: {message.text}")
|
|
||||||
user_name = message.text
|
|
||||||
user_id = BotDB.get_user_id_by_username(user_name)
|
|
||||||
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
|
|
||||||
date_to_unban=None)
|
|
||||||
full_name = BotDB.get_full_name_by_id(user_id)
|
|
||||||
markup = create_keyboard_for_ban_reason()
|
|
||||||
# Экранируем потенциально проблемные символы
|
|
||||||
user_name_escaped = html.escape(str(user_name))
|
|
||||||
full_name_escaped = html.escape(str(full_name))
|
|
||||||
await message.answer(
|
|
||||||
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\n"
|
|
||||||
f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
|
|
||||||
reply_markup=markup)
|
|
||||||
await state.set_state('BAN_2')
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
|
||||||
StateFilter("PRE_BAN_ID")
|
|
||||||
)
|
|
||||||
async def ban_by_id_step_2(message: types.Message, state: FSMContext):
|
|
||||||
# Дополнительная проверка на админские права
|
|
||||||
if not check_access(message.from_user.id, BotDB):
|
|
||||||
await message.answer('Доступ запрещен!')
|
|
||||||
await state.set_state("START")
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
user_id = int(message.text)
|
logger.info(f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}")
|
||||||
logger.info(f"Функция ban_by_id_step_2. Получен ID пользователя: {user_id}")
|
admin_service = AdminService(bot_db)
|
||||||
|
users = admin_service.get_last_users()
|
||||||
|
|
||||||
# Проверяем, существует ли пользователь в базе
|
# Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination)
|
||||||
user_info = BotDB.get_user_info_by_id(user_id)
|
users_data = [
|
||||||
if not user_info:
|
(user.full_name, user.username) # (full_name, username) - формат кортежей
|
||||||
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
for user in users
|
||||||
await state.set_state('ADMIN')
|
]
|
||||||
markup = get_reply_keyboard_admin()
|
|
||||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
|
||||||
return
|
|
||||||
|
|
||||||
user_name = user_info.get('username', 'Неизвестно')
|
keyboard = create_keyboard_with_pagination(1, len(users_data), users_data, 'ban')
|
||||||
full_name = user_info.get('full_name', 'Неизвестно')
|
|
||||||
|
|
||||||
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
|
|
||||||
date_to_unban=None)
|
|
||||||
|
|
||||||
markup = create_keyboard_for_ban_reason()
|
|
||||||
# Экранируем потенциально проблемные символы
|
|
||||||
user_name_escaped = html.escape(str(user_name))
|
|
||||||
full_name_escaped = html.escape(str(full_name))
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\n"
|
text="Список пользователей которые последними обращались к боту",
|
||||||
f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
|
reply_markup=keyboard
|
||||||
reply_markup=markup)
|
)
|
||||||
await state.set_state('BAN_2')
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "get_last_users")
|
||||||
except ValueError:
|
|
||||||
await message.answer("Пожалуйста, введите корректный числовой ID пользователя.")
|
|
||||||
await state.set_state('ADMIN')
|
|
||||||
markup = get_reply_keyboard_admin()
|
|
||||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
@admin_router.message(
|
||||||
@@ -205,80 +88,222 @@ async def ban_by_id_step_2(message: types.Message, state: FSMContext):
|
|||||||
StateFilter("ADMIN"),
|
StateFilter("ADMIN"),
|
||||||
F.text == 'Разбан (список)'
|
F.text == 'Разбан (список)'
|
||||||
)
|
)
|
||||||
async def get_banned_users(message):
|
async def get_banned_users(
|
||||||
logger.info(
|
message: types.Message,
|
||||||
f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
|
state: FSMContext,
|
||||||
message_text = get_banned_users_list(0, BotDB)
|
bot_db: MagicData("bot_db")
|
||||||
buttons_list = get_banned_users_buttons(BotDB)
|
):
|
||||||
|
"""Получение списка заблокированных пользователей"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}")
|
||||||
|
admin_service = AdminService(bot_db)
|
||||||
|
message_text, buttons_list = admin_service.get_banned_users_for_display(0)
|
||||||
|
|
||||||
if buttons_list:
|
if buttons_list:
|
||||||
k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
|
keyboard = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
|
||||||
await message.answer(text=message_text, reply_markup=k)
|
await message.answer(text=message_text, reply_markup=keyboard)
|
||||||
else:
|
else:
|
||||||
await message.answer(text="В списке забанненых пользователей никого нет")
|
await message.answer(text="В списке заблокированных пользователей никого нет")
|
||||||
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "get_banned_users")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ХЕНДЛЕРЫ ПРОЦЕССА БАНА
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@admin_router.message(
|
||||||
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
|
StateFilter("ADMIN"),
|
||||||
|
F.text.in_(['Бан по нику', 'Бан по ID'])
|
||||||
|
)
|
||||||
|
async def start_ban_process(
|
||||||
|
message: types.Message,
|
||||||
|
state: FSMContext,
|
||||||
|
):
|
||||||
|
"""Начало процесса блокировки пользователя"""
|
||||||
|
try:
|
||||||
|
ban_type = "username" if message.text == 'Бан по нику' else "id"
|
||||||
|
await state.update_data(ban_type=ban_type)
|
||||||
|
|
||||||
|
prompt_text = "Пришли мне username блокируемого пользователя" if ban_type == "username" else "Пришли мне ID блокируемого пользователя"
|
||||||
|
await message.answer(prompt_text)
|
||||||
|
await state.set_state('AWAIT_BAN_TARGET')
|
||||||
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "start_ban_process")
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
@admin_router.message(
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
StateFilter("BAN_2")
|
StateFilter("AWAIT_BAN_TARGET")
|
||||||
)
|
)
|
||||||
async def ban_user_step_2(message: types.Message, state: FSMContext):
|
async def process_ban_target(
|
||||||
|
message: types.Message,
|
||||||
|
state: FSMContext,
|
||||||
|
bot_db: MagicData("bot_db")
|
||||||
|
):
|
||||||
|
"""Обработка введенного username/ID для блокировки"""
|
||||||
|
try:
|
||||||
user_data = await state.get_data()
|
user_data = await state.get_data()
|
||||||
logger.info(f"Переход на шаг 2 бана пользователя. Словарь с данными для бана: {user_data})")
|
ban_type = user_data.get('ban_type')
|
||||||
await state.update_data(message_for_user=message.text)
|
admin_service = AdminService(bot_db)
|
||||||
markup = create_keyboard_for_ban_days()
|
|
||||||
# Экранируем message.text для безопасного использования
|
|
||||||
safe_message_text = html.escape(str(message.text)) if message.text else ""
|
|
||||||
await message.answer(f"Выбрана причина: {safe_message_text}. Выбери срок бана в днях или напиши "
|
|
||||||
f"его в чат", reply_markup=markup)
|
|
||||||
await state.set_state("BAN_3")
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
# Определяем пользователя
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
if ban_type == "username":
|
||||||
StateFilter("BAN_3")
|
user = admin_service.get_user_by_username(message.text)
|
||||||
|
if not user:
|
||||||
|
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
||||||
|
await return_to_admin_menu(message, state)
|
||||||
|
return
|
||||||
|
else: # ban_type == "id"
|
||||||
|
try:
|
||||||
|
user_id = admin_service.validate_user_input(message.text)
|
||||||
|
user = admin_service.get_user_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
||||||
|
await return_to_admin_menu(message, state)
|
||||||
|
return
|
||||||
|
except InvalidInputError as e:
|
||||||
|
await message.answer(str(e))
|
||||||
|
await return_to_admin_menu(message, state)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Сохраняем данные пользователя
|
||||||
|
await state.update_data(
|
||||||
|
target_user_id=user.user_id,
|
||||||
|
target_username=user.username,
|
||||||
|
target_full_name=user.full_name
|
||||||
)
|
)
|
||||||
async def ban_user_step_3(message: types.Message, state: FSMContext):
|
|
||||||
logger.info(f"ban_user_step_3. Расчет даты разбана. Входные данные {message.text}")
|
# Показываем информацию о пользователе и запрашиваем причину
|
||||||
if message.text != 'Навсегда':
|
user_info = format_user_info(user.user_id, user.username, user.full_name)
|
||||||
count_days = int(message.text)
|
markup = create_keyboard_for_ban_reason()
|
||||||
date_to_unban = add_days_to_date(count_days)
|
|
||||||
else:
|
|
||||||
date_to_unban = None
|
|
||||||
logger.info(f"ban_user_step_3. Расчет даты разбана. date_to_unban: {date_to_unban}")
|
|
||||||
await state.update_data(date_to_unban=date_to_unban)
|
|
||||||
user_data = await state.get_data()
|
|
||||||
markup = create_keyboard_for_approve_ban()
|
|
||||||
# Экранируем user_data для безопасного использования
|
|
||||||
safe_message_for_user = html.escape(str(user_data['message_for_user'])) if user_data.get('message_for_user') else ""
|
|
||||||
safe_date_to_unban = html.escape(str(user_data['date_to_unban'])) if user_data.get('date_to_unban') else ""
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f"Необходимо подтверждение:\nПользователь:{user_data['user_id']}\nПричина бана:{safe_message_for_user}\nСрок бана:{safe_date_to_unban}",
|
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||||||
reply_markup=markup)
|
reply_markup=markup
|
||||||
await state.set_state("BAN_FINAL")
|
)
|
||||||
|
await state.set_state('AWAIT_BAN_DETAILS')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "process_ban_target")
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(
|
@admin_router.message(
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
StateFilter("BAN_FINAL"),
|
StateFilter("AWAIT_BAN_DETAILS")
|
||||||
|
)
|
||||||
|
async def process_ban_reason(
|
||||||
|
message: types.Message,
|
||||||
|
state: FSMContext
|
||||||
|
):
|
||||||
|
"""Обработка причины блокировки"""
|
||||||
|
try:
|
||||||
|
await state.update_data(ban_reason=message.text)
|
||||||
|
markup = create_keyboard_for_ban_days()
|
||||||
|
safe_reason = escape_html(message.text)
|
||||||
|
await message.answer(
|
||||||
|
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
||||||
|
reply_markup=markup
|
||||||
|
)
|
||||||
|
await state.set_state('AWAIT_BAN_DURATION')
|
||||||
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "process_ban_reason")
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.message(
|
||||||
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
|
StateFilter("AWAIT_BAN_DURATION")
|
||||||
|
)
|
||||||
|
async def process_ban_duration(
|
||||||
|
message: types.Message,
|
||||||
|
state: FSMContext,
|
||||||
|
):
|
||||||
|
"""Обработка срока блокировки"""
|
||||||
|
try:
|
||||||
|
user_data = await state.get_data()
|
||||||
|
|
||||||
|
# Определяем срок блокировки
|
||||||
|
if message.text == 'Навсегда':
|
||||||
|
ban_days = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
ban_days = int(message.text)
|
||||||
|
if ban_days <= 0:
|
||||||
|
await message.answer("Срок блокировки должен быть положительным числом.")
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
await message.answer("Пожалуйста, введите корректное число дней или выберите 'Навсегда'.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.update_data(ban_days=ban_days)
|
||||||
|
|
||||||
|
# Показываем подтверждение
|
||||||
|
confirmation_text = format_ban_confirmation(
|
||||||
|
user_data['target_user_id'],
|
||||||
|
user_data['ban_reason'],
|
||||||
|
ban_days
|
||||||
|
)
|
||||||
|
markup = create_keyboard_for_approve_ban()
|
||||||
|
await message.answer(confirmation_text, reply_markup=markup)
|
||||||
|
await state.set_state('BAN_CONFIRMATION')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "process_ban_duration")
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.message(
|
||||||
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
|
StateFilter("BAN_CONFIRMATION"),
|
||||||
F.text == 'Подтвердить'
|
F.text == 'Подтвердить'
|
||||||
)
|
)
|
||||||
async def approve_ban(message: types.Message, state: FSMContext):
|
async def confirm_ban(
|
||||||
|
message: types.Message,
|
||||||
|
state: FSMContext,
|
||||||
|
bot_db: MagicData("bot_db")
|
||||||
|
):
|
||||||
|
"""Подтверждение блокировки пользователя"""
|
||||||
|
try:
|
||||||
user_data = await state.get_data()
|
user_data = await state.get_data()
|
||||||
logger.info(f"Переход на финальный шаг бана пользователя. Словарь с данными для бана: {user_data})")
|
admin_service = AdminService(bot_db)
|
||||||
exists = BotDB.check_user_in_blacklist(user_data['user_id'])
|
|
||||||
if exists:
|
|
||||||
await message.reply(f"Пользователь уже был заблокирован ранее.")
|
# Выполняем блокировку
|
||||||
logger.info(f"Пользователь: {user_data['user_id']} был заблокирован ранее)")
|
admin_service.ban_user(
|
||||||
await state.set_state('ADMIN')
|
user_id=user_data['target_user_id'],
|
||||||
else:
|
username=user_data['target_username'],
|
||||||
BotDB.set_user_blacklist(user_data['user_id'],
|
reason=user_data['ban_reason'],
|
||||||
user_data['user_name'],
|
ban_days=user_data['ban_days']
|
||||||
user_data['message_for_user'],
|
)
|
||||||
user_data['date_to_unban'])
|
|
||||||
# Экранируем user_name для безопасного использования
|
safe_username = escape_html(user_data['target_username'])
|
||||||
safe_user_name = html.escape(str(user_data['user_name'])) if user_data.get('user_name') else "Неизвестный пользователь"
|
await message.reply(f"Пользователь {safe_username} успешно заблокирован.")
|
||||||
await message.reply(f"Пользователь {safe_user_name} успешно заблокирован.")
|
await return_to_admin_menu(message, state)
|
||||||
logger.info(f"Пользователь: {user_data['user_id']} успешно заблокирован)")
|
|
||||||
await state.set_state('ADMIN')
|
except UserAlreadyBannedError as e:
|
||||||
markup = get_reply_keyboard_admin()
|
await message.reply(str(e))
|
||||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
await return_to_admin_menu(message, state)
|
||||||
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "confirm_ban")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ХЕНДЛЕРЫ ОТМЕНЫ И НАВИГАЦИИ
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@admin_router.message(
|
||||||
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
|
StateFilter("AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION"),
|
||||||
|
F.text == 'Отменить'
|
||||||
|
)
|
||||||
|
async def cancel_ban_process(
|
||||||
|
message: types.Message,
|
||||||
|
state: FSMContext
|
||||||
|
):
|
||||||
|
"""Отмена процесса блокировки"""
|
||||||
|
try:
|
||||||
|
current_state = await state.get_state()
|
||||||
|
logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
|
||||||
|
await return_to_admin_menu(message, state)
|
||||||
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "cancel_ban_process")
|
||||||
|
|||||||
60
helper_bot/handlers/admin/dependencies.py
Normal file
60
helper_bot/handlers/admin/dependencies.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from typing import Annotated, Dict, Any
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject
|
||||||
|
|
||||||
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
from helper_bot.utils.helper_func import check_access
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class AdminAccessMiddleware(BaseMiddleware):
|
||||||
|
"""Middleware для проверки административного доступа"""
|
||||||
|
|
||||||
|
async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
|
||||||
|
if hasattr(event, 'from_user'):
|
||||||
|
user_id = event.from_user.id
|
||||||
|
|
||||||
|
# Получаем bot_db из data (внедренного DependenciesMiddleware)
|
||||||
|
bot_db = data.get('bot_db')
|
||||||
|
if not bot_db:
|
||||||
|
# Fallback: получаем напрямую если middleware не сработала
|
||||||
|
bdf = get_global_instance()
|
||||||
|
bot_db = bdf.get_db()
|
||||||
|
|
||||||
|
if not check_access(user_id, bot_db):
|
||||||
|
if hasattr(event, 'answer'):
|
||||||
|
await event.answer('Доступ запрещен!')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Вызываем хендлер с data
|
||||||
|
return await handler(event, data)
|
||||||
|
except TypeError as e:
|
||||||
|
if "missing 1 required positional argument: 'data'" in str(e):
|
||||||
|
logger.error(f"Ошибка в AdminAccessMiddleware: {e}. Хендлер не принимает параметр 'data'")
|
||||||
|
# Пытаемся вызвать хендлер без data (для совместимости с MagicData)
|
||||||
|
return await handler(event)
|
||||||
|
else:
|
||||||
|
logger.error(f"TypeError в AdminAccessMiddleware: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Неожиданная ошибка в AdminAccessMiddleware: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# Dependency providers
|
||||||
|
def get_bot_db():
|
||||||
|
"""Провайдер для получения экземпляра БД"""
|
||||||
|
bdf = get_global_instance()
|
||||||
|
return bdf.get_db()
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
"""Провайдер для получения настроек"""
|
||||||
|
bdf = get_global_instance()
|
||||||
|
return bdf.settings
|
||||||
|
|
||||||
|
|
||||||
|
# Type aliases for dependency injection
|
||||||
|
BotDB = Annotated[object, get_bot_db()]
|
||||||
|
Settings = Annotated[dict, get_settings()]
|
||||||
23
helper_bot/handlers/admin/exceptions.py
Normal file
23
helper_bot/handlers/admin/exceptions.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
class AdminError(Exception):
|
||||||
|
"""Базовое исключение для административных операций"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AdminAccessDeniedError(AdminError):
|
||||||
|
"""Исключение при отказе в административном доступе"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundError(AdminError):
|
||||||
|
"""Исключение при отсутствии пользователя"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidInputError(AdminError):
|
||||||
|
"""Исключение при некорректном вводе данных"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserAlreadyBannedError(AdminError):
|
||||||
|
"""Исключение при попытке забанить уже заблокированного пользователя"""
|
||||||
|
pass
|
||||||
146
helper_bot/handlers/admin/services.py
Normal file
146
helper_bot/handlers/admin/services.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from helper_bot.utils.helper_func import add_days_to_date, get_banned_users_buttons, get_banned_users_list
|
||||||
|
from helper_bot.handlers.admin.exceptions import UserAlreadyBannedError, InvalidInputError
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class User:
|
||||||
|
"""Модель пользователя"""
|
||||||
|
def __init__(self, user_id: int, username: str, full_name: str):
|
||||||
|
self.user_id = user_id
|
||||||
|
self.username = username
|
||||||
|
self.full_name = full_name
|
||||||
|
|
||||||
|
|
||||||
|
class BannedUser:
|
||||||
|
"""Модель заблокированного пользователя"""
|
||||||
|
def __init__(self, user_id: int, username: str, reason: str, unban_date: Optional[datetime]):
|
||||||
|
self.user_id = user_id
|
||||||
|
self.username = username
|
||||||
|
self.reason = reason
|
||||||
|
self.unban_date = unban_date
|
||||||
|
|
||||||
|
|
||||||
|
class AdminService:
|
||||||
|
"""Сервис для административных операций"""
|
||||||
|
|
||||||
|
def __init__(self, bot_db):
|
||||||
|
self.bot_db = bot_db
|
||||||
|
|
||||||
|
def get_last_users(self) -> List[User]:
|
||||||
|
"""Получить список последних пользователей"""
|
||||||
|
try:
|
||||||
|
users_data = self.bot_db.get_last_users_from_db()
|
||||||
|
return [
|
||||||
|
User(
|
||||||
|
user_id=user[1],
|
||||||
|
username='Неизвестно',
|
||||||
|
full_name=user[0]
|
||||||
|
)
|
||||||
|
for user in users_data
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при получении списка последних пользователей: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_banned_users(self) -> List[BannedUser]:
|
||||||
|
"""Получить список заблокированных пользователей"""
|
||||||
|
try:
|
||||||
|
banned_users_data = self.bot_db.get_banned_users_from_db()
|
||||||
|
return [
|
||||||
|
BannedUser(
|
||||||
|
user_id=user[1], # user_id
|
||||||
|
username=user[0], # user_name
|
||||||
|
reason=user[2], # message_for_user
|
||||||
|
unban_date=user[3] # date_to_unban
|
||||||
|
)
|
||||||
|
for user in banned_users_data
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_user_by_username(self, username: str) -> Optional[User]:
|
||||||
|
"""Получить пользователя по username"""
|
||||||
|
try:
|
||||||
|
user_id = self.bot_db.get_user_id_by_username(username)
|
||||||
|
if not user_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
full_name = self.bot_db.get_full_name_by_id(user_id)
|
||||||
|
return User(
|
||||||
|
user_id=user_id,
|
||||||
|
username=username,
|
||||||
|
full_name=full_name or 'Неизвестно'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при поиске пользователя по username {username}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||||
|
"""Получить пользователя по ID"""
|
||||||
|
try:
|
||||||
|
user_info = self.bot_db.get_user_info_by_id(user_id)
|
||||||
|
if not user_info:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return User(
|
||||||
|
user_id=user_id,
|
||||||
|
username=user_info.get('username', 'Неизвестно'),
|
||||||
|
full_name=user_info.get('full_name', 'Неизвестно')
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def ban_user(self, user_id: int, username: str, reason: str, ban_days: Optional[int]) -> None:
|
||||||
|
"""Заблокировать пользователя"""
|
||||||
|
try:
|
||||||
|
# Проверяем, не заблокирован ли уже пользователь
|
||||||
|
if self.bot_db.check_user_in_blacklist(user_id):
|
||||||
|
raise UserAlreadyBannedError(f"Пользователь {user_id} уже заблокирован")
|
||||||
|
|
||||||
|
# Рассчитываем дату разблокировки
|
||||||
|
date_to_unban = None
|
||||||
|
if ban_days is not None:
|
||||||
|
date_to_unban = add_days_to_date(ban_days)
|
||||||
|
|
||||||
|
# Сохраняем в БД
|
||||||
|
self.bot_db.set_user_blacklist(user_id, username, reason, date_to_unban)
|
||||||
|
|
||||||
|
logger.info(f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def unban_user(self, user_id: int) -> None:
|
||||||
|
"""Разблокировать пользователя"""
|
||||||
|
try:
|
||||||
|
self.bot_db.delete_user_blacklist(user_id)
|
||||||
|
logger.info(f"Пользователь {user_id} разблокирован")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при разблокировке пользователя {user_id}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def validate_user_input(self, input_text: str) -> int:
|
||||||
|
"""Валидация введенного ID пользователя"""
|
||||||
|
try:
|
||||||
|
user_id = int(input_text.strip())
|
||||||
|
if user_id <= 0:
|
||||||
|
raise InvalidInputError("ID пользователя должен быть положительным числом")
|
||||||
|
return user_id
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidInputError("ID пользователя должен быть числом")
|
||||||
|
|
||||||
|
def get_banned_users_for_display(self, page: int = 0) -> tuple[str, list]:
|
||||||
|
"""Получить данные заблокированных пользователей для отображения"""
|
||||||
|
try:
|
||||||
|
message_text = get_banned_users_list(page, self.bot_db)
|
||||||
|
buttons_list = get_banned_users_buttons(self.bot_db)
|
||||||
|
return message_text, buttons_list
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при получении данных заблокированных пользователей: {e}")
|
||||||
|
raise
|
||||||
61
helper_bot/handlers/admin/utils.py
Normal file
61
helper_bot/handlers/admin/utils.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import html
|
||||||
|
from typing import Optional
|
||||||
|
from aiogram import types
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
|
from helper_bot.keyboards.keyboards import get_reply_keyboard_admin
|
||||||
|
from helper_bot.handlers.admin.exceptions import AdminError
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
def escape_html(text: str) -> str:
|
||||||
|
"""Экранирование HTML для безопасного использования в сообщениях"""
|
||||||
|
return html.escape(str(text)) if text else ""
|
||||||
|
|
||||||
|
|
||||||
|
async def return_to_admin_menu(message: types.Message, state: FSMContext,
|
||||||
|
additional_message: Optional[str] = None) -> None:
|
||||||
|
"""Универсальная функция для возврата в админ-меню"""
|
||||||
|
await state.set_data({})
|
||||||
|
await state.set_state("ADMIN")
|
||||||
|
markup = get_reply_keyboard_admin()
|
||||||
|
|
||||||
|
if additional_message:
|
||||||
|
await message.answer(additional_message)
|
||||||
|
|
||||||
|
await message.answer('Вернулись в меню', reply_markup=markup)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_admin_error(message: types.Message, error: Exception,
|
||||||
|
state: FSMContext, error_context: str = "") -> None:
|
||||||
|
"""Централизованная обработка ошибок административных операций"""
|
||||||
|
logger.error(f"Ошибка в {error_context}: {error}")
|
||||||
|
|
||||||
|
if isinstance(error, AdminError):
|
||||||
|
await message.answer(f"Ошибка: {str(error)}")
|
||||||
|
else:
|
||||||
|
await message.answer("Произошла внутренняя ошибка. Попробуйте позже.")
|
||||||
|
|
||||||
|
await return_to_admin_menu(message, state)
|
||||||
|
|
||||||
|
|
||||||
|
def format_user_info(user_id: int, username: str, full_name: str) -> str:
|
||||||
|
"""Форматирование информации о пользователе для отображения"""
|
||||||
|
safe_username = escape_html(username)
|
||||||
|
safe_full_name = escape_html(full_name)
|
||||||
|
|
||||||
|
return (f"<b>Выбран пользователь:</b>\n"
|
||||||
|
f"<b>ID:</b> {user_id}\n"
|
||||||
|
f"<b>Username:</b> {safe_username}\n"
|
||||||
|
f"<b>Имя:</b> {safe_full_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def format_ban_confirmation(user_id: int, reason: str, ban_days: Optional[int]) -> str:
|
||||||
|
"""Форматирование подтверждения бана"""
|
||||||
|
safe_reason = escape_html(reason)
|
||||||
|
ban_text = "Навсегда" if ban_days is None else f"{ban_days} дней"
|
||||||
|
|
||||||
|
return (f"<b>Необходимо подтверждение:</b>\n"
|
||||||
|
f"<b>Пользователь:</b> {user_id}\n"
|
||||||
|
f"<b>Причина бана:</b> {safe_reason}\n"
|
||||||
|
f"<b>Срок бана:</b> {ban_text}")
|
||||||
@@ -1 +1,24 @@
|
|||||||
from .callback_handlers import callback_router
|
from .callback_handlers import callback_router
|
||||||
|
from .services import PostPublishService, BanService
|
||||||
|
from .exceptions import UserBlockedBotError, PostNotFoundError, UserNotFoundError, PublishError, BanError
|
||||||
|
from .constants import (
|
||||||
|
CALLBACK_PUBLISH, CALLBACK_DECLINE, CALLBACK_BAN, CALLBACK_UNLOCK,
|
||||||
|
CALLBACK_RETURN, CALLBACK_PAGE
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'callback_router',
|
||||||
|
'PostPublishService',
|
||||||
|
'BanService',
|
||||||
|
'UserBlockedBotError',
|
||||||
|
'PostNotFoundError',
|
||||||
|
'UserNotFoundError',
|
||||||
|
'PublishError',
|
||||||
|
'BanError',
|
||||||
|
'CALLBACK_PUBLISH',
|
||||||
|
'CALLBACK_DECLINE',
|
||||||
|
'CALLBACK_BAN',
|
||||||
|
'CALLBACK_UNLOCK',
|
||||||
|
'CALLBACK_RETURN',
|
||||||
|
'CALLBACK_PAGE'
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,322 +1,188 @@
|
|||||||
import html
|
import html
|
||||||
|
from tkinter import S
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
|
from aiogram import F
|
||||||
|
from aiogram.filters import MagicData
|
||||||
|
|
||||||
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
|
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
|
||||||
create_keyboard_for_ban_reason
|
create_keyboard_for_ban_reason
|
||||||
|
from helper_bot.utils.helper_func import get_banned_users_list, get_banned_users_buttons
|
||||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
from helper_bot.utils.helper_func import send_text_message, send_photo_message, get_banned_users_list, \
|
from .dependency_factory import get_post_publish_service, get_ban_service
|
||||||
get_banned_users_buttons, delete_user_blacklist, send_media_group_to_channel, \
|
from .exceptions import UserBlockedBotError, PostNotFoundError, UserNotFoundError, PublishError, BanError
|
||||||
send_video_message, send_video_note_message, send_audio_message, send_voice_message
|
from .constants import (
|
||||||
|
CALLBACK_PUBLISH, CALLBACK_DECLINE, CALLBACK_BAN, CALLBACK_UNLOCK,
|
||||||
|
CALLBACK_RETURN, CALLBACK_PAGE, MESSAGE_PUBLISHED, MESSAGE_DECLINED,
|
||||||
|
MESSAGE_USER_BANNED, MESSAGE_USER_UNLOCKED, MESSAGE_ERROR,
|
||||||
|
ERROR_BOT_BLOCKED
|
||||||
|
)
|
||||||
from logs.custom_logger import logger
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
callback_router = Router()
|
callback_router = Router()
|
||||||
|
|
||||||
bdf = get_global_instance()
|
|
||||||
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
|
|
||||||
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
|
|
||||||
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
|
|
||||||
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
|
|
||||||
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
|
|
||||||
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
|
|
||||||
LOGS = bdf.settings['Settings']['logs']
|
|
||||||
TEST = bdf.settings['Settings']['test']
|
|
||||||
|
|
||||||
BotDB = bdf.get_db()
|
@callback_router.callback_query(F.data == CALLBACK_PUBLISH)
|
||||||
|
async def post_for_group(
|
||||||
|
call: CallbackQuery,
|
||||||
@callback_router.callback_query(
|
settings: MagicData("settings")
|
||||||
F.data == "publish"
|
):
|
||||||
)
|
publish_service = get_post_publish_service()
|
||||||
async def post_for_group(call: CallbackQuery, state: FSMContext):
|
# TODO: переделать на MagicData
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Получен callback-запрос с действием: {call.data} от пользователя {call.from_user.full_name} (ID сообщения: {call.message.message_id})')
|
f'Получен callback-запрос с действием: {call.data} от пользователя {call.from_user.full_name} (ID сообщения: {call.message.message_id})')
|
||||||
text_post = html.escape(str(call.message.text))
|
|
||||||
text_post_with_photo = html.escape(str(call.message.caption))
|
|
||||||
if call.message.content_type == 'text' and call.message.text != "^":
|
|
||||||
try:
|
try:
|
||||||
# Пересылаем сообщение в канал
|
await publish_service.publish_post(call)
|
||||||
await send_text_message(MAIN_PUBLIC, call.message, text_post)
|
await call.answer(text=MESSAGE_PUBLISHED, cache_time=3)
|
||||||
|
except UserBlockedBotError:
|
||||||
# Получаем из базы автора
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
|
except (PostNotFoundError, PublishError) as e:
|
||||||
|
logger.error(f'Ошибка при публикации поста: {str(e)}')
|
||||||
# Очищаем предложку и удаляем оттуда пост
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
|
|
||||||
logger.info(f'Текст сообщения опубликован в канале {MAIN_PUBLIC}.')
|
|
||||||
await call.answer(text='Выложено!', cache_time=3)
|
|
||||||
|
|
||||||
# Отвечаем пользователю
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if e.message != 'Forbidden: bot was blocked by the user':
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
else:
|
||||||
logger.error(f'Ошибка при публикации текста в канал {MAIN_PUBLIC}: {str(e)}')
|
important_logs = settings['Telegram']['important_logs']
|
||||||
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
|
await call.bot.send_message(
|
||||||
elif call.message.content_type == 'photo':
|
chat_id=important_logs,
|
||||||
try:
|
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||||
await send_photo_message(MAIN_PUBLIC, call.message, call.message.photo[-1].file_id, text_post_with_photo)
|
|
||||||
|
|
||||||
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
|
|
||||||
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
|
|
||||||
|
|
||||||
# Удаляем пост из предложки
|
|
||||||
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
|
|
||||||
logger.info(f'Пост с фото опубликован в канале {MAIN_PUBLIC}.')
|
|
||||||
await call.answer(text='Выложено!', cache_time=3)
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
|
|
||||||
except Exception as e:
|
|
||||||
if e.message != 'Forbidden: bot was blocked by the user':
|
|
||||||
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
|
|
||||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
|
||||||
logger.error(f'Ошибка при публикации фотографии в канал {MAIN_PUBLIC}: {str(e)}')
|
|
||||||
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
|
|
||||||
elif call.message.content_type == 'video':
|
|
||||||
try:
|
|
||||||
await send_video_message(MAIN_PUBLIC, call.message, call.message.video.file_id, text_post_with_photo)
|
|
||||||
|
|
||||||
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
|
|
||||||
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
|
|
||||||
|
|
||||||
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
|
|
||||||
logger.info(f'Пост с видео опубликован в канале {MAIN_PUBLIC}.')
|
|
||||||
await call.answer(text='Выложено!', cache_time=3)
|
|
||||||
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
|
|
||||||
except Exception as e:
|
|
||||||
if e.message != 'Forbidden: bot was blocked by the user':
|
|
||||||
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
|
|
||||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
|
||||||
logger.error(f'Ошибка при публикации видео в канал {MAIN_PUBLIC}: {str(e)}')
|
|
||||||
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
|
|
||||||
elif call.message.content_type == 'video_note':
|
|
||||||
try:
|
|
||||||
await send_video_note_message(MAIN_PUBLIC, call.message, call.message.video_note.file_id)
|
|
||||||
|
|
||||||
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
|
|
||||||
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
|
|
||||||
|
|
||||||
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
|
|
||||||
logger.info(f'Пост с кружком опубликован в канале {MAIN_PUBLIC}.')
|
|
||||||
await call.answer(text='Выложено!', cache_time=3)
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
|
|
||||||
except Exception as e:
|
|
||||||
if e.message != 'Forbidden: bot was blocked by the user':
|
|
||||||
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
|
|
||||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
|
||||||
logger.error(f'Ошибка при публикации кружка в канал {MAIN_PUBLIC}: {str(e)}')
|
|
||||||
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
|
|
||||||
elif call.message.content_type == 'audio':
|
|
||||||
try:
|
|
||||||
await send_audio_message(MAIN_PUBLIC, call.message, call.message.audio.file_id, text_post_with_photo)
|
|
||||||
|
|
||||||
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
|
|
||||||
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
|
|
||||||
|
|
||||||
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
|
|
||||||
logger.info(f'Пост с аудио опубликован в канале {MAIN_PUBLIC}.')
|
|
||||||
await call.answer(text='Выложено!', cache_time=3)
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
|
|
||||||
except Exception as e:
|
|
||||||
if e.message != 'Forbidden: bot was blocked by the user':
|
|
||||||
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
|
|
||||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
|
||||||
logger.error(f'Ошибка при публикации аудио в канал {MAIN_PUBLIC}: {str(e)}')
|
|
||||||
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
|
|
||||||
elif call.message.content_type == 'voice':
|
|
||||||
try:
|
|
||||||
await send_voice_message(MAIN_PUBLIC, call.message, call.message.voice.file_id)
|
|
||||||
|
|
||||||
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
|
|
||||||
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
|
|
||||||
|
|
||||||
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
|
|
||||||
logger.info(f'Пост с войсом опубликован в канале {MAIN_PUBLIC}.')
|
|
||||||
await call.answer(text='Выложено!', cache_time=3)
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
|
|
||||||
except Exception as e:
|
|
||||||
if e.message != 'Forbidden: bot was blocked by the user':
|
|
||||||
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
|
|
||||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
|
||||||
logger.error(f'Ошибка при публикации войса в канал {MAIN_PUBLIC}: {str(e)}')
|
|
||||||
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
|
|
||||||
elif call.message.text == "^":
|
|
||||||
# Получаем контент медиагруппы и текст для публикации
|
|
||||||
post_content = BotDB.get_post_content_from_telegram_by_last_id(call.message.message_id)
|
|
||||||
pre_text = BotDB.get_post_text_from_telegram_by_last_id(call.message.message_id)
|
|
||||||
post_text = html.escape(str(pre_text))
|
|
||||||
|
|
||||||
# Готовим список для удаления
|
|
||||||
post_ids = BotDB.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
|
||||||
message_ids = [row[0] for row in post_ids]
|
|
||||||
message_ids.append(call.message.message_id)
|
|
||||||
|
|
||||||
# Выкладываем пост в канал
|
|
||||||
await send_media_group_to_channel(bot=call.bot, chat_id=MAIN_PUBLIC, post_content=post_content,
|
|
||||||
post_text=post_text)
|
|
||||||
|
|
||||||
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
|
|
||||||
author_id = BotDB.get_author_id_by_helper_message_id(call.message.message_id)
|
|
||||||
|
|
||||||
# TODO: Удалить фотки с локалки после выкладки?
|
|
||||||
await call.bot.delete_messages(chat_id=GROUP_FOR_POST, message_ids=message_ids)
|
|
||||||
await call.answer(text='Выложено!', cache_time=3)
|
|
||||||
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
|
|
||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(
|
|
||||||
F.data == "decline"
|
|
||||||
)
|
)
|
||||||
async def decline_post_for_group(call: CallbackQuery, state: FSMContext):
|
logger.error(f'Неожиданная ошибка при публикации поста: {str(e)}')
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
|
||||||
|
|
||||||
|
@callback_router.callback_query(F.data == CALLBACK_DECLINE)
|
||||||
|
async def decline_post_for_group(
|
||||||
|
call: CallbackQuery,
|
||||||
|
settings: MagicData("settings")
|
||||||
|
):
|
||||||
|
publish_service = get_post_publish_service()
|
||||||
|
# TODO: переделать на MagicData
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Получен callback-запрос с данными: {call.data} от пользователя {call.from_user.full_name} (ID: {call.from_user.id})')
|
f'Получен callback-запрос с данными: {call.data} от пользователя {call.from_user.full_name} (ID: {call.from_user.id})')
|
||||||
try:
|
try:
|
||||||
if call.message.content_type == 'text' and call.message.text != "^" or call.message.content_type == 'photo' \
|
await publish_service.decline_post(call)
|
||||||
or call.message.content_type == 'audio' or call.message.content_type == 'voice' \
|
await call.answer(text=MESSAGE_DECLINED, cache_time=3)
|
||||||
or call.message.content_type == 'video' or call.message.content_type == 'video_note':
|
except UserBlockedBotError:
|
||||||
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
except (PostNotFoundError, PublishError) as e:
|
||||||
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
|
logger.error(f'Ошибка при отклонении поста: {str(e)}')
|
||||||
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
|
|
||||||
await call.answer(text='Отклонено!', cache_time=3)
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был отклонен😔')
|
|
||||||
if call.message.text == '^':
|
|
||||||
post_ids = BotDB.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
|
||||||
message_ids = [row[0] for row in post_ids]
|
|
||||||
message_ids.append(call.message.message_id)
|
|
||||||
|
|
||||||
await call.bot.delete_messages(chat_id=GROUP_FOR_POST, message_ids=message_ids)
|
|
||||||
|
|
||||||
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
|
|
||||||
author_id = BotDB.get_author_id_by_helper_message_id(call.message.message_id)
|
|
||||||
|
|
||||||
await call.answer(text='Удалено!', cache_time=3)
|
|
||||||
|
|
||||||
await send_text_message(author_id, call.message, 'Твой пост был отклонен😔')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if e.message != 'Forbidden: bot was blocked by the user':
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
await call.bot.send_message(IMPORTANT_LOGS,
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
else:
|
||||||
logger.error(f'Ошибка при удалении сообщения в группе {GROUP_FOR_POST}: {str(e)}')
|
important_logs = settings['Telegram']['important_logs']
|
||||||
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
|
await call.bot.send_message(
|
||||||
|
chat_id=important_logs,
|
||||||
|
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||||
@callback_router.callback_query(
|
|
||||||
F.data == "ban"
|
|
||||||
)
|
)
|
||||||
|
logger.error(f'Неожиданная ошибка при отклонении поста: {str(e)}')
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
|
||||||
|
|
||||||
|
@callback_router.callback_query(F.data == CALLBACK_BAN)
|
||||||
async def ban_user_from_post(call: CallbackQuery):
|
async def ban_user_from_post(call: CallbackQuery):
|
||||||
|
ban_service = get_ban_service()
|
||||||
|
# TODO: переделать на MagicData
|
||||||
try:
|
try:
|
||||||
# Получаем информацию о пользователе из сообщения
|
await ban_service.ban_user_from_post(call)
|
||||||
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
|
await call.answer(text=MESSAGE_USER_BANNED, cache_time=3)
|
||||||
user_name = BotDB.get_username(user_id=author_id)
|
except UserBlockedBotError:
|
||||||
full_name = call.message.from_user.full_name if call.message.from_user else "Неизвестно"
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
except (UserNotFoundError, BanError) as e:
|
||||||
# Устанавливаем причину бана и дату разблокировки (+7 дней)
|
|
||||||
current_date = datetime.now()
|
|
||||||
date_to_unban = current_date + timedelta(days=7)
|
|
||||||
|
|
||||||
# Записываем в базу данных
|
|
||||||
BotDB.set_user_blacklist(
|
|
||||||
user_id=author_id,
|
|
||||||
user_name=user_name,
|
|
||||||
message_for_user="Спам",
|
|
||||||
date_to_unban=date_to_unban
|
|
||||||
)
|
|
||||||
|
|
||||||
# Удаляем пост из предложки
|
|
||||||
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
|
|
||||||
|
|
||||||
# Отправляем сообщение пользователю о блокировке
|
|
||||||
date_str = date_to_unban.strftime("%d.%m.%Y %H:%M")
|
|
||||||
await send_text_message(author_id, call.message, f"Ты заблокирован за спам. Дата разблокировки: {date_str}")
|
|
||||||
|
|
||||||
logger.info(f"Пользователь {author_id} заблокирован за спам до {date_str}")
|
|
||||||
await call.answer(text='Пользователь заблокирован!', cache_time=3)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f'Ошибка при блокировке пользователя: {str(e)}')
|
logger.error(f'Ошибка при блокировке пользователя: {str(e)}')
|
||||||
await call.answer(text='Ошибка при блокировке!', show_alert=True, cache_time=3)
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
else:
|
||||||
|
logger.error(f'Неожиданная ошибка при блокировке пользователя: {str(e)}')
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(
|
@callback_router.callback_query(F.data.contains(CALLBACK_BAN))
|
||||||
F.data.contains('ban')
|
|
||||||
)
|
|
||||||
async def process_ban_user(call: CallbackQuery, state: FSMContext):
|
async def process_ban_user(call: CallbackQuery, state: FSMContext):
|
||||||
|
ban_service = get_ban_service()
|
||||||
|
# TODO: переделать на MagicData
|
||||||
user_id = call.data[4:]
|
user_id = call.data[4:]
|
||||||
logger.info(
|
logger.info(f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}")
|
||||||
f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}")
|
|
||||||
user_name = BotDB.get_username(user_id=user_id)
|
try:
|
||||||
if user_name:
|
user_name = await ban_service.ban_user(user_id, "")
|
||||||
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
|
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None, date_to_unban=None)
|
||||||
date_to_unban=None)
|
|
||||||
markup = create_keyboard_for_ban_reason()
|
markup = create_keyboard_for_ban_reason()
|
||||||
# Экранируем потенциально проблемные символы
|
|
||||||
user_name_escaped = html.escape(str(user_name))
|
user_name_escaped = html.escape(str(user_name))
|
||||||
full_name_escaped = html.escape(str(call.message.from_user.full_name))
|
full_name_escaped = html.escape(str(call.message.from_user.full_name))
|
||||||
await call.message.answer(
|
await call.message.answer(
|
||||||
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
|
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
|
||||||
reply_markup=markup)
|
reply_markup=markup
|
||||||
|
)
|
||||||
await state.set_state('BAN_2')
|
await state.set_state('BAN_2')
|
||||||
else:
|
except UserNotFoundError:
|
||||||
markup = get_reply_keyboard_admin()
|
markup = get_reply_keyboard_admin()
|
||||||
await call.message.answer(text='Пользователь с таким ID не найден в базе', reply_markup=markup)
|
await call.message.answer(text='Пользователь с таким ID не найден в базе', reply_markup=markup)
|
||||||
await state.set_state('ADMIN')
|
await state.set_state('ADMIN')
|
||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(
|
@callback_router.callback_query(F.data.contains(CALLBACK_UNLOCK))
|
||||||
F.data.contains('unlock')
|
|
||||||
)
|
|
||||||
async def process_unlock_user(call: CallbackQuery):
|
async def process_unlock_user(call: CallbackQuery):
|
||||||
|
ban_service = get_ban_service()
|
||||||
|
# TODO: переделать на MagicData
|
||||||
user_id = call.data[7:]
|
user_id = call.data[7:]
|
||||||
user_name = BotDB.get_username(user_id=user_id)
|
|
||||||
delete_user_blacklist(user_id, BotDB)
|
try:
|
||||||
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
|
username = await ban_service.unlock_user(user_id)
|
||||||
username = BotDB.get_username(user_id)
|
await call.answer(f'{MESSAGE_USER_UNLOCKED} {username}', show_alert=True)
|
||||||
await call.answer(f'Пользователь разблокирован {username}', show_alert=True)
|
except UserNotFoundError:
|
||||||
|
await call.answer(text='Пользователь не найден в базе', show_alert=True, cache_time=3)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Ошибка при разблокировке пользователя: {str(e)}')
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(
|
@callback_router.callback_query(F.data == CALLBACK_RETURN)
|
||||||
F.data == 'return'
|
|
||||||
)
|
|
||||||
async def return_to_main_menu(call: CallbackQuery):
|
async def return_to_main_menu(call: CallbackQuery):
|
||||||
await call.message.delete()
|
await call.message.delete()
|
||||||
logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}")
|
logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}")
|
||||||
markup = get_reply_keyboard_admin()
|
markup = get_reply_keyboard_admin()
|
||||||
await call.message.answer("Добро пожаловать в админку. Выбери что хочешь:",
|
await call.message.answer("Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup)
|
||||||
reply_markup=markup)
|
|
||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(
|
@callback_router.callback_query(F.data.contains(CALLBACK_PAGE))
|
||||||
F.data.contains('page')
|
async def change_page(
|
||||||
)
|
call: CallbackQuery,
|
||||||
async def change_page(call: CallbackQuery):
|
bot_db: MagicData("bot_db")
|
||||||
|
):
|
||||||
page_number = int(call.data[5:])
|
page_number = int(call.data[5:])
|
||||||
logger.info(f"Переход на страницу {page_number}")
|
logger.info(f"Переход на страницу {page_number}")
|
||||||
|
|
||||||
if call.message.text == 'Список пользователей которые последними обращались к боту':
|
if call.message.text == 'Список пользователей которые последними обращались к боту':
|
||||||
list_users = BotDB.get_last_users_from_db()
|
list_users = bot_db.get_last_users_from_db()
|
||||||
# TODO: Здесь где-то надо добавить обработку ошибки IndexError: list index out of range
|
keyboard = create_keyboard_with_pagination(int(page_number), len(list_users), list_users, 'ban')
|
||||||
keyboard = create_keyboard_with_pagination(int(page_number), len(list_users), list_users,
|
await call.bot.edit_message_reply_markup(
|
||||||
'ban')
|
chat_id=call.message.chat.id,
|
||||||
|
message_id=call.message.message_id,
|
||||||
await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id,
|
reply_markup=keyboard
|
||||||
reply_markup=keyboard)
|
)
|
||||||
else:
|
else:
|
||||||
# Готовим сообщения
|
message_user = get_banned_users_list(int(page_number) * 7 - 7, bot_db)
|
||||||
message_user = get_banned_users_list(int(page_number) * 7 - 7, BotDB)
|
await call.bot.edit_message_text(
|
||||||
await call.bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id,
|
chat_id=call.message.chat.id,
|
||||||
text=message_user)
|
message_id=call.message.message_id,
|
||||||
|
text=message_user
|
||||||
|
)
|
||||||
|
|
||||||
# Готовим клавиатуру
|
buttons = get_banned_users_buttons(bot_db)
|
||||||
buttons = get_banned_users_buttons(BotDB)
|
|
||||||
keyboard = create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unlock')
|
keyboard = create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unlock')
|
||||||
await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id,
|
await call.bot.edit_message_reply_markup(
|
||||||
reply_markup=keyboard)
|
chat_id=call.message.chat.id,
|
||||||
|
message_id=call.message.message_id,
|
||||||
|
reply_markup=keyboard
|
||||||
|
)
|
||||||
|
|||||||
29
helper_bot/handlers/callback/constants.py
Normal file
29
helper_bot/handlers/callback/constants.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Callback data constants
|
||||||
|
CALLBACK_PUBLISH = "publish"
|
||||||
|
CALLBACK_DECLINE = "decline"
|
||||||
|
CALLBACK_BAN = "ban"
|
||||||
|
CALLBACK_UNLOCK = "unlock"
|
||||||
|
CALLBACK_RETURN = "return"
|
||||||
|
CALLBACK_PAGE = "page"
|
||||||
|
|
||||||
|
# Content types
|
||||||
|
CONTENT_TYPE_TEXT = "text"
|
||||||
|
CONTENT_TYPE_PHOTO = "photo"
|
||||||
|
CONTENT_TYPE_VIDEO = "video"
|
||||||
|
CONTENT_TYPE_VIDEO_NOTE = "video_note"
|
||||||
|
CONTENT_TYPE_AUDIO = "audio"
|
||||||
|
CONTENT_TYPE_VOICE = "voice"
|
||||||
|
CONTENT_TYPE_MEDIA_GROUP = "^"
|
||||||
|
|
||||||
|
# Messages
|
||||||
|
MESSAGE_PUBLISHED = "Выложено!"
|
||||||
|
MESSAGE_DECLINED = "Отклонено!"
|
||||||
|
MESSAGE_USER_BANNED = "Пользователь заблокирован!"
|
||||||
|
MESSAGE_USER_UNLOCKED = "Пользователь разблокирован"
|
||||||
|
MESSAGE_ERROR = "Что-то пошло не так!"
|
||||||
|
MESSAGE_POST_PUBLISHED = "Твой пост был выложен🥰"
|
||||||
|
MESSAGE_POST_DECLINED = "Твой пост был отклонен😔"
|
||||||
|
MESSAGE_USER_BANNED_SPAM = "Ты заблокирован за спам. Дата разблокировки: {date}"
|
||||||
|
|
||||||
|
# Error messages
|
||||||
|
ERROR_BOT_BLOCKED = "Forbidden: bot was blocked by the user"
|
||||||
33
helper_bot/handlers/callback/dependency_factory.py
Normal file
33
helper_bot/handlers/callback/dependency_factory.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from typing import Callable
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
from .services import PostPublishService, BanService
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_publish_service() -> PostPublishService:
|
||||||
|
"""Фабрика для PostPublishService"""
|
||||||
|
bdf = get_global_instance()
|
||||||
|
bot = Bot(
|
||||||
|
token=bdf.settings['Telegram']['bot_token'],
|
||||||
|
default=DefaultBotProperties(parse_mode='HTML'),
|
||||||
|
timeout=30.0
|
||||||
|
)
|
||||||
|
db = bdf.get_db()
|
||||||
|
settings = bdf.settings
|
||||||
|
return PostPublishService(bot, db, settings)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ban_service() -> BanService:
|
||||||
|
"""Фабрика для BanService"""
|
||||||
|
bdf = get_global_instance()
|
||||||
|
bot = Bot(
|
||||||
|
token=bdf.settings['Telegram']['bot_token'],
|
||||||
|
default=DefaultBotProperties(parse_mode='HTML'),
|
||||||
|
timeout=30.0
|
||||||
|
)
|
||||||
|
db = bdf.get_db()
|
||||||
|
settings = bdf.settings
|
||||||
|
return BanService(bot, db, settings)
|
||||||
23
helper_bot/handlers/callback/exceptions.py
Normal file
23
helper_bot/handlers/callback/exceptions.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
class UserBlockedBotError(Exception):
|
||||||
|
"""Исключение, возникающее когда пользователь заблокировал бота"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PostNotFoundError(Exception):
|
||||||
|
"""Исключение, возникающее когда пост не найден в базе данных"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundError(Exception):
|
||||||
|
"""Исключение, возникающее когда пользователь не найден в базе данных"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PublishError(Exception):
|
||||||
|
"""Общее исключение для ошибок публикации"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BanError(Exception):
|
||||||
|
"""Исключение для ошибок бана/разбана пользователей"""
|
||||||
|
pass
|
||||||
249
helper_bot/handlers/callback/services.py
Normal file
249
helper_bot/handlers/callback/services.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import html
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.types import CallbackQuery
|
||||||
|
|
||||||
|
from helper_bot.utils.helper_func import (
|
||||||
|
send_text_message, send_photo_message, send_video_message,
|
||||||
|
send_video_note_message, send_audio_message, send_voice_message,
|
||||||
|
send_media_group_to_channel, delete_user_blacklist
|
||||||
|
)
|
||||||
|
from helper_bot.keyboards.keyboards import create_keyboard_for_ban_reason
|
||||||
|
from .exceptions import (
|
||||||
|
UserBlockedBotError, PostNotFoundError, UserNotFoundError,
|
||||||
|
PublishError, BanError
|
||||||
|
)
|
||||||
|
from .constants import (
|
||||||
|
CONTENT_TYPE_TEXT, CONTENT_TYPE_PHOTO, CONTENT_TYPE_VIDEO,
|
||||||
|
CONTENT_TYPE_VIDEO_NOTE, CONTENT_TYPE_AUDIO, CONTENT_TYPE_VOICE,
|
||||||
|
CONTENT_TYPE_MEDIA_GROUP, MESSAGE_POST_PUBLISHED, MESSAGE_POST_DECLINED,
|
||||||
|
MESSAGE_USER_BANNED_SPAM, ERROR_BOT_BLOCKED
|
||||||
|
)
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class PostPublishService:
|
||||||
|
def __init__(self, bot: Bot, db, settings: Dict[str, Any]):
|
||||||
|
self.bot = bot
|
||||||
|
self.db = db
|
||||||
|
self.settings = settings
|
||||||
|
self.group_for_posts = settings['Telegram']['group_for_posts']
|
||||||
|
self.main_public = settings['Telegram']['main_public']
|
||||||
|
self.important_logs = settings['Telegram']['important_logs']
|
||||||
|
|
||||||
|
async def publish_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Основной метод публикации поста"""
|
||||||
|
content_type = call.message.content_type
|
||||||
|
|
||||||
|
if content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP:
|
||||||
|
await self._publish_text_post(call)
|
||||||
|
elif content_type == CONTENT_TYPE_PHOTO:
|
||||||
|
await self._publish_photo_post(call)
|
||||||
|
elif content_type == CONTENT_TYPE_VIDEO:
|
||||||
|
await self._publish_video_post(call)
|
||||||
|
elif content_type == CONTENT_TYPE_VIDEO_NOTE:
|
||||||
|
await self._publish_video_note_post(call)
|
||||||
|
elif content_type == CONTENT_TYPE_AUDIO:
|
||||||
|
await self._publish_audio_post(call)
|
||||||
|
elif content_type == CONTENT_TYPE_VOICE:
|
||||||
|
await self._publish_voice_post(call)
|
||||||
|
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||||
|
await self._publish_media_group(call)
|
||||||
|
else:
|
||||||
|
raise PublishError(f"Неподдерживаемый тип контента: {content_type}")
|
||||||
|
|
||||||
|
async def _publish_text_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Публикация текстового поста"""
|
||||||
|
text_post = html.escape(str(call.message.text))
|
||||||
|
author_id = self._get_author_id(call.message.message_id)
|
||||||
|
|
||||||
|
await send_text_message(self.main_public, call.message, text_post)
|
||||||
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
|
logger.info(f'Текст сообщения опубликован в канале {self.main_public}.')
|
||||||
|
|
||||||
|
async def _publish_photo_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Публикация поста с фото"""
|
||||||
|
text_post_with_photo = html.escape(str(call.message.caption))
|
||||||
|
author_id = self._get_author_id(call.message.message_id)
|
||||||
|
|
||||||
|
await send_photo_message(self.main_public, call.message, call.message.photo[-1].file_id, text_post_with_photo)
|
||||||
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
|
logger.info(f'Пост с фото опубликован в канале {self.main_public}.')
|
||||||
|
|
||||||
|
async def _publish_video_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Публикация поста с видео"""
|
||||||
|
text_post_with_photo = html.escape(str(call.message.caption))
|
||||||
|
author_id = self._get_author_id(call.message.message_id)
|
||||||
|
|
||||||
|
await send_video_message(self.main_public, call.message, call.message.video.file_id, text_post_with_photo)
|
||||||
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
|
logger.info(f'Пост с видео опубликован в канале {self.main_public}.')
|
||||||
|
|
||||||
|
async def _publish_video_note_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Публикация поста с кружком"""
|
||||||
|
author_id = self._get_author_id(call.message.message_id)
|
||||||
|
|
||||||
|
await send_video_note_message(self.main_public, call.message, call.message.video_note.file_id)
|
||||||
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
|
logger.info(f'Пост с кружком опубликован в канале {self.main_public}.')
|
||||||
|
|
||||||
|
async def _publish_audio_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Публикация поста с аудио"""
|
||||||
|
text_post_with_photo = html.escape(str(call.message.caption))
|
||||||
|
author_id = self._get_author_id(call.message.message_id)
|
||||||
|
|
||||||
|
await send_audio_message(self.main_public, call.message, call.message.audio.file_id, text_post_with_photo)
|
||||||
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
|
logger.info(f'Пост с аудио опубликован в канале {self.main_public}.')
|
||||||
|
|
||||||
|
async def _publish_voice_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Публикация поста с войсом"""
|
||||||
|
author_id = self._get_author_id(call.message.message_id)
|
||||||
|
|
||||||
|
await send_voice_message(self.main_public, call.message, call.message.voice.file_id)
|
||||||
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
|
logger.info(f'Пост с войсом опубликован в канале {self.main_public}.')
|
||||||
|
|
||||||
|
async def _publish_media_group(self, call: CallbackQuery) -> None:
|
||||||
|
"""Публикация медиагруппы"""
|
||||||
|
post_content = self.db.get_post_content_from_telegram_by_last_id(call.message.message_id)
|
||||||
|
pre_text = self.db.get_post_text_from_telegram_by_last_id(call.message.message_id)
|
||||||
|
post_text = html.escape(str(pre_text))
|
||||||
|
author_id = self._get_author_id_for_media_group(call.message.message_id)
|
||||||
|
|
||||||
|
await send_media_group_to_channel(bot=self.bot, chat_id=self.main_public, post_content=post_content, post_text=post_text)
|
||||||
|
await self._delete_media_group_and_notify_author(call, author_id)
|
||||||
|
|
||||||
|
async def decline_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Отклонение поста"""
|
||||||
|
content_type = call.message.content_type
|
||||||
|
|
||||||
|
if (content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP) or \
|
||||||
|
content_type in [CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO, CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||||
|
await self._decline_single_post(call)
|
||||||
|
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||||
|
await self._decline_media_group(call)
|
||||||
|
else:
|
||||||
|
raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||||
|
|
||||||
|
async def _decline_single_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Отклонение одиночного поста"""
|
||||||
|
author_id = self._get_author_id(call.message.message_id)
|
||||||
|
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||||
|
try:
|
||||||
|
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
raise
|
||||||
|
logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
|
||||||
|
|
||||||
|
async def _decline_media_group(self, call: CallbackQuery) -> None:
|
||||||
|
"""Отклонение медиагруппы"""
|
||||||
|
post_ids = self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
||||||
|
message_ids = [row[0] for row in post_ids]
|
||||||
|
message_ids.append(call.message.message_id)
|
||||||
|
|
||||||
|
author_id = self._get_author_id_for_media_group(call.message.message_id)
|
||||||
|
await self.bot.delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
||||||
|
try:
|
||||||
|
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _get_author_id(self, message_id: int) -> int:
|
||||||
|
"""Получение ID автора по ID сообщения"""
|
||||||
|
author_id = self.db.get_author_id_by_message_id(message_id)
|
||||||
|
if not author_id:
|
||||||
|
raise PostNotFoundError(f"Автор не найден для сообщения {message_id}")
|
||||||
|
return author_id
|
||||||
|
|
||||||
|
def _get_author_id_for_media_group(self, message_id: int) -> int:
|
||||||
|
"""Получение ID автора для медиагруппы"""
|
||||||
|
author_id = self.db.get_author_id_by_helper_message_id(message_id)
|
||||||
|
if not author_id:
|
||||||
|
raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}")
|
||||||
|
return author_id
|
||||||
|
|
||||||
|
async def _delete_post_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
|
||||||
|
"""Удаление поста и уведомление автора"""
|
||||||
|
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||||
|
try:
|
||||||
|
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _delete_media_group_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
|
||||||
|
"""Удаление медиагруппы и уведомление автора"""
|
||||||
|
post_ids = self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
||||||
|
message_ids = [row[0] for row in post_ids]
|
||||||
|
message_ids.append(call.message.message_id)
|
||||||
|
await self.bot.delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
||||||
|
try:
|
||||||
|
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class BanService:
|
||||||
|
def __init__(self, bot: Bot, db, settings: Dict[str, Any]):
|
||||||
|
self.bot = bot
|
||||||
|
self.db = db
|
||||||
|
self.settings = settings
|
||||||
|
self.group_for_posts = settings['Telegram']['group_for_posts']
|
||||||
|
self.important_logs = settings['Telegram']['important_logs']
|
||||||
|
|
||||||
|
async def ban_user_from_post(self, call: CallbackQuery) -> None:
|
||||||
|
"""Бан пользователя за спам"""
|
||||||
|
author_id = self.db.get_author_id_by_message_id(call.message.message_id)
|
||||||
|
if not author_id:
|
||||||
|
raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}")
|
||||||
|
|
||||||
|
user_name = self.db.get_username(user_id=author_id)
|
||||||
|
current_date = datetime.now()
|
||||||
|
date_to_unban = current_date + timedelta(days=7)
|
||||||
|
|
||||||
|
self.db.set_user_blacklist(
|
||||||
|
user_id=author_id,
|
||||||
|
user_name=user_name,
|
||||||
|
message_for_user="Спам",
|
||||||
|
date_to_unban=date_to_unban
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||||
|
|
||||||
|
date_str = date_to_unban.strftime("%d.%m.%Y %H:%M")
|
||||||
|
try:
|
||||||
|
await send_text_message(author_id, call.message, MESSAGE_USER_BANNED_SPAM.format(date=date_str))
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info(f"Пользователь {author_id} заблокирован за спам до {date_str}")
|
||||||
|
|
||||||
|
async def ban_user(self, user_id: str, user_name: str) -> str:
|
||||||
|
"""Бан пользователя по ID"""
|
||||||
|
user_name = self.db.get_username(user_id=user_id)
|
||||||
|
if not user_name:
|
||||||
|
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
||||||
|
|
||||||
|
return user_name
|
||||||
|
|
||||||
|
async def unlock_user(self, user_id: str) -> str:
|
||||||
|
"""Разблокировка пользователя"""
|
||||||
|
user_name = self.db.get_username(user_id=user_id)
|
||||||
|
if not user_name:
|
||||||
|
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
||||||
|
|
||||||
|
delete_user_blacklist(user_id, self.db)
|
||||||
|
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
|
||||||
|
return user_name
|
||||||
@@ -1 +1,47 @@
|
|||||||
from .group_handlers import group_router
|
"""Group handlers package for Telegram bot"""
|
||||||
|
|
||||||
|
# Local imports - main components
|
||||||
|
from .group_handlers import (
|
||||||
|
group_router,
|
||||||
|
create_group_handlers,
|
||||||
|
GroupHandlers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Local imports - services
|
||||||
|
from .services import (
|
||||||
|
AdminReplyService,
|
||||||
|
DatabaseProtocol
|
||||||
|
)
|
||||||
|
|
||||||
|
# Local imports - constants and utilities
|
||||||
|
from .constants import (
|
||||||
|
FSM_STATES,
|
||||||
|
ERROR_MESSAGES
|
||||||
|
)
|
||||||
|
from .exceptions import (
|
||||||
|
NoReplyToMessageError,
|
||||||
|
UserNotFoundError
|
||||||
|
)
|
||||||
|
from .decorators import error_handler
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Main components
|
||||||
|
'group_router',
|
||||||
|
'create_group_handlers',
|
||||||
|
'GroupHandlers',
|
||||||
|
|
||||||
|
# Services
|
||||||
|
'AdminReplyService',
|
||||||
|
'DatabaseProtocol',
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
'FSM_STATES',
|
||||||
|
'ERROR_MESSAGES',
|
||||||
|
|
||||||
|
# Exceptions
|
||||||
|
'NoReplyToMessageError',
|
||||||
|
'UserNotFoundError',
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
'error_handler'
|
||||||
|
]
|
||||||
|
|||||||
14
helper_bot/handlers/group/constants.py
Normal file
14
helper_bot/handlers/group/constants.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""Constants for group handlers"""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
# FSM States
|
||||||
|
FSM_STATES: Final[dict[str, str]] = {
|
||||||
|
"CHAT": "CHAT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error messages
|
||||||
|
ERROR_MESSAGES: Final[dict[str, str]] = {
|
||||||
|
"NO_REPLY_TO_MESSAGE": "Блять, выдели сообщение!",
|
||||||
|
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение."
|
||||||
|
}
|
||||||
36
helper_bot/handlers/group/decorators.py
Normal file
36
helper_bot/handlers/group/decorators.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""Decorators and utility functions for group handlers"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
|
import traceback
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
from aiogram import types
|
||||||
|
|
||||||
|
# Local imports
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
"""Decorator for centralized error handling"""
|
||||||
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in {func.__name__}: {str(e)}")
|
||||||
|
# Try to send error to logs if possible
|
||||||
|
try:
|
||||||
|
message = next((arg for arg in args if isinstance(arg, types.Message)), None)
|
||||||
|
if message and hasattr(message, 'bot'):
|
||||||
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
bdf = get_global_instance()
|
||||||
|
important_logs = bdf.settings['Telegram']['important_logs']
|
||||||
|
await message.bot.send_message(
|
||||||
|
chat_id=important_logs,
|
||||||
|
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# If we can't log the error, at least it was logged to logger
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
return wrapper
|
||||||
11
helper_bot/handlers/group/exceptions.py
Normal file
11
helper_bot/handlers/group/exceptions.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""Custom exceptions for group handlers"""
|
||||||
|
|
||||||
|
|
||||||
|
class NoReplyToMessageError(Exception):
|
||||||
|
"""Raised when admin tries to reply without selecting a message"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundError(Exception):
|
||||||
|
"""Raised when user is not found in database for the given message_id"""
|
||||||
|
pass
|
||||||
@@ -1,49 +1,106 @@
|
|||||||
|
"""Main group handlers module for Telegram bot"""
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
from aiogram import Router, types
|
from aiogram import Router, types
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
|
# Local imports - filters
|
||||||
from helper_bot.filters.main import ChatTypeFilter
|
from helper_bot.filters.main import ChatTypeFilter
|
||||||
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
|
|
||||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
# Local imports - modular components
|
||||||
from helper_bot.utils.helper_func import send_text_message
|
from .constants import FSM_STATES, ERROR_MESSAGES
|
||||||
|
from .services import AdminReplyService
|
||||||
|
from .decorators import error_handler
|
||||||
|
from .exceptions import UserNotFoundError
|
||||||
|
|
||||||
|
# Local imports - utilities
|
||||||
from logs.custom_logger import logger
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class GroupHandlers:
|
||||||
|
"""Main handler class for group messages"""
|
||||||
|
|
||||||
|
def __init__(self, db, keyboard_markup: types.ReplyKeyboardMarkup):
|
||||||
|
self.db = db
|
||||||
|
self.keyboard_markup = keyboard_markup
|
||||||
|
self.admin_reply_service = AdminReplyService(db)
|
||||||
|
|
||||||
|
# Create router
|
||||||
|
self.router = Router()
|
||||||
|
|
||||||
|
# Register handlers
|
||||||
|
self._register_handlers()
|
||||||
|
|
||||||
|
def _register_handlers(self):
|
||||||
|
"""Register all message handlers"""
|
||||||
|
self.router.message.register(
|
||||||
|
self.handle_message,
|
||||||
|
ChatTypeFilter(chat_type=["group", "supergroup"])
|
||||||
|
)
|
||||||
|
|
||||||
|
@error_handler
|
||||||
|
async def handle_message(self, message: types.Message, state: FSMContext):
|
||||||
|
"""Handle admin reply to user through group chat"""
|
||||||
|
logger.info(
|
||||||
|
f'Получено сообщение в группе {message.chat.title} (ID: {message.chat.id}) '
|
||||||
|
f'от пользователя {message.from_user.full_name} (ID: {message.from_user.id}): "{message.text}"'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if message is a reply
|
||||||
|
if not message.reply_to_message:
|
||||||
|
await message.answer(ERROR_MESSAGES["NO_REPLY_TO_MESSAGE"])
|
||||||
|
logger.warning(
|
||||||
|
f'В группе {message.chat.title} (ID: {message.chat.id}) '
|
||||||
|
f'админ не выделил сообщение для ответа.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
message_id = message.reply_to_message.message_id
|
||||||
|
reply_text = message.text
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get user ID for reply
|
||||||
|
chat_id = self.admin_reply_service.get_user_id_for_reply(message_id)
|
||||||
|
|
||||||
|
# Send reply to user
|
||||||
|
await self.admin_reply_service.send_reply_to_user(
|
||||||
|
chat_id, message, reply_text, self.keyboard_markup
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set state
|
||||||
|
await state.set_state(FSM_STATES["CHAT"])
|
||||||
|
|
||||||
|
except UserNotFoundError:
|
||||||
|
await message.answer(ERROR_MESSAGES["USER_NOT_FOUND"])
|
||||||
|
logger.error(
|
||||||
|
f'Ошибка при поиске пользователя в базе для ответа на сообщение: {reply_text} '
|
||||||
|
f'в группе {message.chat.title} (ID сообщения: {message.message_id})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Factory function to create handlers with dependencies
|
||||||
|
def create_group_handlers(db, keyboard_markup: types.ReplyKeyboardMarkup) -> GroupHandlers:
|
||||||
|
"""Create group handlers instance with dependencies"""
|
||||||
|
return GroupHandlers(db, keyboard_markup)
|
||||||
|
|
||||||
|
|
||||||
|
# Legacy router for backward compatibility
|
||||||
group_router = Router()
|
group_router = Router()
|
||||||
|
|
||||||
|
# Initialize with global dependencies (for backward compatibility)
|
||||||
|
def init_legacy_router():
|
||||||
|
"""Initialize legacy router with global dependencies"""
|
||||||
|
global group_router
|
||||||
|
|
||||||
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
|
||||||
|
|
||||||
bdf = get_global_instance()
|
bdf = get_global_instance()
|
||||||
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
|
db = bdf.get_db()
|
||||||
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
|
keyboard_markup = get_reply_keyboard_leave_chat()
|
||||||
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
|
|
||||||
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
|
|
||||||
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
|
|
||||||
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
|
|
||||||
LOGS = bdf.settings['Settings']['logs']
|
|
||||||
TEST = bdf.settings['Settings']['test']
|
|
||||||
|
|
||||||
BotDB = bdf.get_db()
|
handlers = create_group_handlers(db, keyboard_markup)
|
||||||
|
group_router = handlers.router
|
||||||
|
|
||||||
|
# Initialize legacy router
|
||||||
@group_router.message(
|
init_legacy_router()
|
||||||
ChatTypeFilter(chat_type=["group", "supergroup"]),
|
|
||||||
)
|
|
||||||
async def handle_message(message: types.Message, state: FSMContext):
|
|
||||||
"""Функция ответа админа пользователю через закрытый чат"""
|
|
||||||
logger.info(
|
|
||||||
f'Получено сообщение в группе {message.chat.title} (ID: {message.chat.id}) от пользователя {message.from_user.full_name} (ID: {message.from_user.id}): "{message.text}"')
|
|
||||||
markup = get_reply_keyboard_leave_chat()
|
|
||||||
message_id = 0
|
|
||||||
try:
|
|
||||||
message_id = message.reply_to_message.message_id
|
|
||||||
except AttributeError as e:
|
|
||||||
await message.answer('Блять, выдели сообщение!')
|
|
||||||
logger.warning(
|
|
||||||
f'В группе {message.chat.title} (ID: {message.chat.id}) админ не выделил сообщение для ответа. Ошибка {str(e)}')
|
|
||||||
message_from_admin = message.text
|
|
||||||
try:
|
|
||||||
chat_id = BotDB.get_user_by_message_id(message_id)
|
|
||||||
await send_text_message(chat_id, message, message_from_admin, markup)
|
|
||||||
await state.set_state("CHAT")
|
|
||||||
logger.info(f'Ответ админа "{message.text}" отправлен пользователю с ID: {chat_id} на сообщение {message_id}')
|
|
||||||
except TypeError as e:
|
|
||||||
await message.answer('Не могу найти кому ответить в базе, проебали сообщение.')
|
|
||||||
logger.error(
|
|
||||||
f'Ошибка при поиске пользователя в базе для ответа на сообщение: {message.text} в группе {message.chat.title} (ID сообщения: {message.message_id}) Ошибка: {str(e)}')
|
|
||||||
|
|||||||
64
helper_bot/handlers/group/services.py
Normal file
64
helper_bot/handlers/group/services.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""Service classes for group handlers"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
|
from typing import Protocol, Optional
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
from aiogram import types
|
||||||
|
|
||||||
|
# Local imports
|
||||||
|
from helper_bot.utils.helper_func import send_text_message
|
||||||
|
from .exceptions import NoReplyToMessageError, UserNotFoundError
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseProtocol(Protocol):
|
||||||
|
"""Protocol for database operations"""
|
||||||
|
def get_user_by_message_id(self, message_id: int) -> Optional[int]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class AdminReplyService:
|
||||||
|
"""Service for admin reply operations"""
|
||||||
|
|
||||||
|
def __init__(self, db: DatabaseProtocol) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_user_id_for_reply(self, message_id: int) -> int:
|
||||||
|
"""
|
||||||
|
Get user ID for reply by message ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_id: ID of the message to reply to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User ID for the reply
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UserNotFoundError: If user is not found in database
|
||||||
|
"""
|
||||||
|
user_id = self.db.get_user_by_message_id(message_id)
|
||||||
|
if user_id is None:
|
||||||
|
raise UserNotFoundError(f"User not found for message_id: {message_id}")
|
||||||
|
return user_id
|
||||||
|
|
||||||
|
async def send_reply_to_user(
|
||||||
|
self,
|
||||||
|
chat_id: int,
|
||||||
|
message: types.Message,
|
||||||
|
reply_text: str,
|
||||||
|
markup: types.ReplyKeyboardMarkup
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Send reply to user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: User's chat ID
|
||||||
|
message: Original message from admin
|
||||||
|
reply_text: Text to send to user
|
||||||
|
markup: Reply keyboard markup
|
||||||
|
"""
|
||||||
|
await send_text_message(chat_id, message, reply_text, markup)
|
||||||
|
logger.info(
|
||||||
|
f'Ответ админа "{reply_text}" отправлен пользователю с ID: {chat_id} '
|
||||||
|
f'на сообщение {message.reply_to_message.message_id if message.reply_to_message else "N/A"}'
|
||||||
|
)
|
||||||
@@ -1,20 +1,45 @@
|
|||||||
"""Private handlers package for Telegram bot"""
|
"""Private handlers package for Telegram bot"""
|
||||||
|
|
||||||
from .private_handlers import private_router, create_private_handlers, PrivateHandlers
|
# Local imports - main components
|
||||||
from .services import BotSettings, UserService, PostService, StickerService
|
from .private_handlers import (
|
||||||
from .constants import FSM_STATES, BUTTON_TEXTS, ERROR_MESSAGES
|
private_router,
|
||||||
|
create_private_handlers,
|
||||||
|
PrivateHandlers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Local imports - services
|
||||||
|
from .services import (
|
||||||
|
BotSettings,
|
||||||
|
UserService,
|
||||||
|
PostService,
|
||||||
|
StickerService
|
||||||
|
)
|
||||||
|
|
||||||
|
# Local imports - constants and utilities
|
||||||
|
from .constants import (
|
||||||
|
FSM_STATES,
|
||||||
|
BUTTON_TEXTS,
|
||||||
|
ERROR_MESSAGES
|
||||||
|
)
|
||||||
from .decorators import error_handler
|
from .decorators import error_handler
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
# Main components
|
||||||
'private_router',
|
'private_router',
|
||||||
'create_private_handlers',
|
'create_private_handlers',
|
||||||
'PrivateHandlers',
|
'PrivateHandlers',
|
||||||
|
|
||||||
|
# Services
|
||||||
'BotSettings',
|
'BotSettings',
|
||||||
'UserService',
|
'UserService',
|
||||||
'PostService',
|
'PostService',
|
||||||
'StickerService',
|
'StickerService',
|
||||||
|
|
||||||
|
# Constants
|
||||||
'FSM_STATES',
|
'FSM_STATES',
|
||||||
'BUTTON_TEXTS',
|
'BUTTON_TEXTS',
|
||||||
'ERROR_MESSAGES',
|
'ERROR_MESSAGES',
|
||||||
|
|
||||||
|
# Utilities
|
||||||
'error_handler'
|
'error_handler'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""Constants for private handlers"""
|
"""Constants for private handlers"""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
# FSM States
|
# FSM States
|
||||||
FSM_STATES = {
|
FSM_STATES: Final[dict[str, str]] = {
|
||||||
"START": "START",
|
"START": "START",
|
||||||
"SUGGEST": "SUGGEST",
|
"SUGGEST": "SUGGEST",
|
||||||
"PRE_CHAT": "PRE_CHAT",
|
"PRE_CHAT": "PRE_CHAT",
|
||||||
@@ -9,7 +11,7 @@ FSM_STATES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Button texts
|
# Button texts
|
||||||
BUTTON_TEXTS = {
|
BUTTON_TEXTS: Final[dict[str, str]] = {
|
||||||
"SUGGEST_POST": "📢Предложить свой пост",
|
"SUGGEST_POST": "📢Предложить свой пост",
|
||||||
"SAY_GOODBYE": "👋🏼Сказать пока!",
|
"SAY_GOODBYE": "👋🏼Сказать пока!",
|
||||||
"LEAVE_CHAT": "Выйти из чата",
|
"LEAVE_CHAT": "Выйти из чата",
|
||||||
@@ -19,7 +21,7 @@ BUTTON_TEXTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Error messages
|
# Error messages
|
||||||
ERROR_MESSAGES = {
|
ERROR_MESSAGES: Final[dict[str, str]] = {
|
||||||
"UNSUPPORTED_CONTENT": (
|
"UNSUPPORTED_CONTENT": (
|
||||||
'Я пока не умею работать с таким сообщением. '
|
'Я пока не умею работать с таким сообщением. '
|
||||||
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
|
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
"""Decorators and utility functions for private handlers"""
|
"""Decorators and utility functions for private handlers"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
import traceback
|
import traceback
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
|
|
||||||
|
# Local imports
|
||||||
from logs.custom_logger import logger
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
def error_handler(func):
|
def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
"""Decorator for centralized error handling"""
|
"""Decorator for centralized error handling"""
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
try:
|
try:
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -23,7 +29,8 @@ def error_handler(func):
|
|||||||
chat_id=important_logs,
|
chat_id=important_logs,
|
||||||
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||||
)
|
)
|
||||||
except:
|
except Exception:
|
||||||
pass # If we can't log the error, at least it was logged to logger
|
# If we can't log the error, at least it was logged to logger
|
||||||
|
pass
|
||||||
raise
|
raise
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
import random
|
"""Main private handlers module for Telegram bot"""
|
||||||
import traceback
|
|
||||||
import asyncio
|
|
||||||
import html
|
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
from aiogram import types, Router, F
|
from aiogram import types, Router, F
|
||||||
from aiogram.filters import Command, StateFilter
|
from aiogram.filters import Command, StateFilter
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import FSInputFile
|
|
||||||
|
|
||||||
|
# Local imports - filters and middlewares
|
||||||
from helper_bot.filters.main import ChatTypeFilter
|
from helper_bot.filters.main import ChatTypeFilter
|
||||||
from helper_bot.keyboards import get_reply_keyboard, get_reply_keyboard_for_post
|
|
||||||
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
|
|
||||||
from helper_bot.middlewares.album_middleware import AlbumMiddleware
|
from helper_bot.middlewares.album_middleware import AlbumMiddleware
|
||||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||||
from helper_bot.utils import messages
|
|
||||||
from helper_bot.utils.helper_func import get_first_name, get_text_message, send_text_message, send_photo_message, \
|
|
||||||
send_media_group_message_to_private_chat, prepare_media_group_from_middlewares, send_video_message, \
|
|
||||||
send_video_note_message, send_audio_message, send_voice_message, add_in_db_media, \
|
|
||||||
check_user_emoji, check_username_and_full_name, update_user_info
|
|
||||||
from logs.custom_logger import logger
|
|
||||||
|
|
||||||
# Import new modular components
|
# Local imports - utilities
|
||||||
|
from helper_bot.keyboards import get_reply_keyboard, get_reply_keyboard_for_post
|
||||||
|
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
|
||||||
|
from helper_bot.utils import messages
|
||||||
|
from helper_bot.utils.helper_func import (
|
||||||
|
get_first_name,
|
||||||
|
update_user_info,
|
||||||
|
check_user_emoji
|
||||||
|
)
|
||||||
|
|
||||||
|
# Local imports - modular components
|
||||||
from .constants import FSM_STATES, BUTTON_TEXTS, ERROR_MESSAGES
|
from .constants import FSM_STATES, BUTTON_TEXTS, ERROR_MESSAGES
|
||||||
from .services import BotSettings, UserService, PostService, StickerService
|
from .services import BotSettings, UserService, PostService, StickerService
|
||||||
from .decorators import error_handler
|
from .decorators import error_handler
|
||||||
@@ -112,10 +114,7 @@ class PrivateHandlers:
|
|||||||
|
|
||||||
markup = types.ReplyKeyboardRemove()
|
markup = types.ReplyKeyboardRemove()
|
||||||
suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS')
|
suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS')
|
||||||
await message.answer(suggest_news)
|
await message.answer(suggest_news, reply_markup=markup)
|
||||||
await asyncio.sleep(0.3)
|
|
||||||
suggest_news_2 = messages.get_message(get_first_name(message), 'SUGGEST_NEWS_2')
|
|
||||||
await message.answer(suggest_news_2, reply_markup=markup)
|
|
||||||
|
|
||||||
@error_handler
|
@error_handler
|
||||||
async def end_message(self, message: types.Message, state: FSMContext, **kwargs):
|
async def end_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
|
|||||||
@@ -1,25 +1,50 @@
|
|||||||
"""Service classes for private handlers"""
|
"""Service classes for private handlers"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
import random
|
import random
|
||||||
import asyncio
|
import asyncio
|
||||||
import html
|
import html
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Callable
|
from typing import Dict, Callable, Any, Protocol, Union
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
from aiogram.types import FSInputFile
|
from aiogram.types import FSInputFile
|
||||||
|
|
||||||
|
# Local imports - utilities
|
||||||
from helper_bot.utils.helper_func import (
|
from helper_bot.utils.helper_func import (
|
||||||
get_first_name, get_text_message, send_text_message, send_photo_message,
|
get_first_name,
|
||||||
send_media_group_message_to_private_chat, prepare_media_group_from_middlewares,
|
get_text_message,
|
||||||
send_video_message, send_video_note_message, send_audio_message, send_voice_message,
|
send_text_message,
|
||||||
add_in_db_media, check_username_and_full_name
|
send_photo_message,
|
||||||
|
send_media_group_message_to_private_chat,
|
||||||
|
prepare_media_group_from_middlewares,
|
||||||
|
send_video_message,
|
||||||
|
send_video_note_message,
|
||||||
|
send_audio_message,
|
||||||
|
send_voice_message,
|
||||||
|
add_in_db_media,
|
||||||
|
check_username_and_full_name
|
||||||
)
|
)
|
||||||
from helper_bot.keyboards import get_reply_keyboard_for_post
|
from helper_bot.keyboards import get_reply_keyboard_for_post
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseProtocol(Protocol):
|
||||||
|
"""Protocol for database operations"""
|
||||||
|
def user_exists(self, user_id: int) -> bool: ...
|
||||||
|
def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str,
|
||||||
|
username: str, is_bot: bool, language_code: str,
|
||||||
|
emoji: str, created_date: str, updated_date: str) -> None: ...
|
||||||
|
def update_username_and_full_name(self, user_id: int, username: str, full_name: str) -> None: ...
|
||||||
|
def update_date_for_user(self, date: str, user_id: int) -> None: ...
|
||||||
|
def add_post_in_db(self, message_id: int, text: str, user_id: int) -> None: ...
|
||||||
|
def update_info_about_stickers(self, user_id: int) -> None: ...
|
||||||
|
def add_new_message_in_db(self, text: str, user_id: int, message_id: int, date: str) -> None: ...
|
||||||
|
def update_helper_message_in_db(self, message_id: int, helper_message_id: int) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BotSettings:
|
class BotSettings:
|
||||||
"""Bot configuration settings"""
|
"""Bot configuration settings"""
|
||||||
@@ -36,7 +61,7 @@ class BotSettings:
|
|||||||
class UserService:
|
class UserService:
|
||||||
"""Service for user-related operations"""
|
"""Service for user-related operations"""
|
||||||
|
|
||||||
def __init__(self, db, settings: BotSettings):
|
def __init__(self, db: DatabaseProtocol, settings: BotSettings) -> None:
|
||||||
self.db = db
|
self.db = db
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
@@ -90,7 +115,7 @@ class UserService:
|
|||||||
class PostService:
|
class PostService:
|
||||||
"""Service for post-related operations"""
|
"""Service for post-related operations"""
|
||||||
|
|
||||||
def __init__(self, db, settings: BotSettings):
|
def __init__(self, db: DatabaseProtocol, settings: BotSettings) -> None:
|
||||||
self.db = db
|
self.db = db
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
@@ -168,7 +193,7 @@ class PostService:
|
|||||||
"""Handle media group post submission"""
|
"""Handle media group post submission"""
|
||||||
post_caption = " "
|
post_caption = " "
|
||||||
|
|
||||||
if album[0].caption:
|
if album and album[0].caption:
|
||||||
post_caption = get_text_message(album[0].caption.lower(), first_name, message.from_user.username)
|
post_caption = get_text_message(album[0].caption.lower(), first_name, message.from_user.username)
|
||||||
|
|
||||||
media_group = await prepare_media_group_from_middlewares(album, post_caption)
|
media_group = await prepare_media_group_from_middlewares(album, post_caption)
|
||||||
@@ -185,7 +210,7 @@ class PostService:
|
|||||||
message_id=media_group_message_id, helper_message_id=help_message_id
|
message_id=media_group_message_id, helper_message_id=help_message_id
|
||||||
)
|
)
|
||||||
|
|
||||||
async def process_post(self, message: types.Message, album: list = None) -> None:
|
async def process_post(self, message: types.Message, album: Union[list[types.Message], None] = None) -> None:
|
||||||
"""Process post based on content type"""
|
"""Process post based on content type"""
|
||||||
first_name = get_first_name(message)
|
first_name = get_first_name(message)
|
||||||
|
|
||||||
@@ -220,12 +245,14 @@ class PostService:
|
|||||||
class StickerService:
|
class StickerService:
|
||||||
"""Service for sticker-related operations"""
|
"""Service for sticker-related operations"""
|
||||||
|
|
||||||
def __init__(self, settings: BotSettings):
|
def __init__(self, settings: BotSettings) -> None:
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
||||||
async def send_random_hello_sticker(self, message: types.Message) -> None:
|
async def send_random_hello_sticker(self, message: types.Message) -> None:
|
||||||
"""Send random hello sticker"""
|
"""Send random hello sticker"""
|
||||||
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
|
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
|
||||||
|
if not name_stick_hello:
|
||||||
|
return
|
||||||
random_stick_hello = random.choice(name_stick_hello)
|
random_stick_hello = random.choice(name_stick_hello)
|
||||||
random_stick_hello = FSInputFile(path=random_stick_hello)
|
random_stick_hello = FSInputFile(path=random_stick_hello)
|
||||||
await message.answer_sticker(random_stick_hello)
|
await message.answer_sticker(random_stick_hello)
|
||||||
@@ -234,6 +261,8 @@ class StickerService:
|
|||||||
async def send_random_goodbye_sticker(self, message: types.Message) -> None:
|
async def send_random_goodbye_sticker(self, message: types.Message) -> None:
|
||||||
"""Send random goodbye sticker"""
|
"""Send random goodbye sticker"""
|
||||||
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
|
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
|
||||||
|
if not name_stick_bye:
|
||||||
|
return
|
||||||
random_stick_bye = random.choice(name_stick_bye)
|
random_stick_bye = random.choice(name_stick_bye)
|
||||||
random_stick_bye = FSInputFile(path=random_stick_bye)
|
random_stick_bye = FSInputFile(path=random_stick_bye)
|
||||||
await message.answer_sticker(random_stick_bye)
|
await message.answer_sticker(random_stick_bye)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from helper_bot.handlers.admin import admin_router
|
|||||||
from helper_bot.handlers.callback import callback_router
|
from helper_bot.handlers.callback import callback_router
|
||||||
from helper_bot.handlers.group import group_router
|
from helper_bot.handlers.group import group_router
|
||||||
from helper_bot.handlers.private import private_router
|
from helper_bot.handlers.private import private_router
|
||||||
|
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||||
|
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||||
|
|
||||||
|
|
||||||
async def start_bot(bdf):
|
async def start_bot(bdf):
|
||||||
@@ -16,6 +18,10 @@ async def start_bot(bdf):
|
|||||||
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
|
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
|
||||||
), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
|
), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
|
||||||
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
||||||
|
|
||||||
|
# ✅ Глобальная middleware для всех роутеров
|
||||||
|
dp.update.outer_middleware(DependenciesMiddleware())
|
||||||
|
|
||||||
dp.include_routers(admin_router, private_router, callback_router, group_router)
|
dp.include_routers(admin_router, private_router, callback_router, group_router)
|
||||||
await bot.delete_webhook(drop_pending_updates=True)
|
await bot.delete_webhook(drop_pending_updates=True)
|
||||||
await dp.start_polling(bot, skip_updates=True)
|
await dp.start_polling(bot, skip_updates=True)
|
||||||
|
|||||||
31
helper_bot/middlewares/dependencies_middleware.py
Normal file
31
helper_bot/middlewares/dependencies_middleware.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from typing import Any, Dict
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject
|
||||||
|
|
||||||
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class DependenciesMiddleware(BaseMiddleware):
|
||||||
|
"""Универсальная middleware для внедрения зависимостей во все хендлеры"""
|
||||||
|
|
||||||
|
async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
|
||||||
|
try:
|
||||||
|
# Получаем глобальные зависимости
|
||||||
|
bdf = get_global_instance()
|
||||||
|
|
||||||
|
# Внедряем зависимости в data для MagicData
|
||||||
|
if 'bot_db' not in data:
|
||||||
|
data['bot_db'] = bdf.get_db()
|
||||||
|
if 'settings' not in data:
|
||||||
|
data['settings'] = bdf.settings
|
||||||
|
data['bot'] = data.get('bot')
|
||||||
|
data['dp'] = data.get('dp')
|
||||||
|
|
||||||
|
logger.debug(f"DependenciesMiddleware: внедрены зависимости для {type(event).__name__}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка в DependenciesMiddleware: {e}")
|
||||||
|
# Не прерываем выполнение, продолжаем без зависимостей
|
||||||
|
|
||||||
|
return await handler(event, data)
|
||||||
@@ -14,13 +14,11 @@ def get_message(username: str, type_message: str):
|
|||||||
"&&Основная группа в ВК: https://vk.com/love_bsk"
|
"&&Основная группа в ВК: https://vk.com/love_bsk"
|
||||||
"&Основной канал в ТГ: https://t.me/love_bsk",
|
"&Основной канал в ТГ: https://t.me/love_bsk",
|
||||||
'SUGGEST_NEWS': "username, окей, жду от тебя текст поста🙌🏼"
|
'SUGGEST_NEWS': "username, окей, жду от тебя текст поста🙌🏼"
|
||||||
"&В данный момент я работаю в тестовом режиме, поэтому к посту можно прикрепить не более одного фото и никаких аудио или видео👻"
|
"&Обрати внимание, что я умный и смогу из твоего текста понять команды указанные ниже😉"
|
||||||
"&&Обещаю, я научусь их обрабатывать, но позже🤝🤖",
|
|
||||||
'SUGGEST_NEWS_2': "Обрати внимание, что я умный и смогу из твоего текста понять команды указанные ниже😉"
|
|
||||||
"&Если хочешь чтобы пост был опубликован анонимно, напиши в любом месте своего поста слово 'анон'."
|
"&Если хочешь чтобы пост был опубликован анонимно, напиши в любом месте своего поста слово 'анон'."
|
||||||
"&Если хочешь опубликовать пост не анонимно, то напиши 'не анон', 'неанон' или не пиши ничего."
|
"&Если хочешь опубликовать пост не анонимно, то напиши 'не анон', 'неанон' или не пиши ничего."
|
||||||
"&&❗️❗️❗️Я обучен только на команды, указанные мной выше❗️❗️❗️👆"
|
"&&❗️❗️Я обучен только на команды, указанные мной выше👆"
|
||||||
"&‼Проверь, чтобы указание авторства было выполнено так как я попросил, иначе пост будет выложен не корректно"
|
"&❗️❗️Проверь, чтобы указание авторства было выполнено так как я попросил, иначе пост будет выложен не корректно"
|
||||||
"&Пост будет опубликован только в группе ТГ📩",
|
"&Пост будет опубликован только в группе ТГ📩",
|
||||||
"CONNECT_WITH_ADMIN": "username, напиши свое обращение или предложение✍️"
|
"CONNECT_WITH_ADMIN": "username, напиши свое обращение или предложение✍️"
|
||||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️",
|
"&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️",
|
||||||
|
|||||||
@@ -1,339 +0,0 @@
|
|||||||
# Импортируем моки в самом начале
|
|
||||||
import tests.mocks
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import asyncio
|
|
||||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
|
||||||
from aiogram import Bot, Dispatcher
|
|
||||||
from aiogram.types import Message, User, Chat, MessageEntity
|
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
from aiogram.fsm.storage.memory import MemoryStorage
|
|
||||||
|
|
||||||
from helper_bot.main import start_bot
|
|
||||||
from helper_bot.handlers.private.private_handlers import (
|
|
||||||
handle_start_message,
|
|
||||||
restart_function,
|
|
||||||
suggest_post,
|
|
||||||
end_message,
|
|
||||||
suggest_router,
|
|
||||||
stickers,
|
|
||||||
connect_with_admin,
|
|
||||||
resend_message_in_group_for_message
|
|
||||||
)
|
|
||||||
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
|
||||||
from database.db import BotDB
|
|
||||||
|
|
||||||
|
|
||||||
class TestBotStartup:
|
|
||||||
"""Тесты для проверки запуска бота"""
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_bot_initialization(self):
|
|
||||||
"""Тест инициализации бота"""
|
|
||||||
with patch('helper_bot.main.Bot') as mock_bot_class:
|
|
||||||
with patch('helper_bot.main.Dispatcher') as mock_dp_class:
|
|
||||||
with patch('helper_bot.main.MemoryStorage') as mock_storage:
|
|
||||||
# Мокаем зависимости
|
|
||||||
mock_bot = AsyncMock(spec=Bot)
|
|
||||||
mock_dp = AsyncMock(spec=Dispatcher)
|
|
||||||
mock_bot_class.return_value = mock_bot
|
|
||||||
mock_dp_class.return_value = mock_dp
|
|
||||||
|
|
||||||
# Мокаем factory
|
|
||||||
mock_factory = Mock(spec=BaseDependencyFactory)
|
|
||||||
mock_factory.settings = {
|
|
||||||
'Telegram': {
|
|
||||||
'bot_token': 'test_token',
|
|
||||||
'preview_link': False
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Запускаем бота
|
|
||||||
await start_bot(mock_factory)
|
|
||||||
|
|
||||||
# Проверяем, что бот был создан с правильными параметрами
|
|
||||||
mock_bot_class.assert_called_once()
|
|
||||||
call_args = mock_bot_class.call_args
|
|
||||||
assert call_args[1]['token'] == 'test_token'
|
|
||||||
assert call_args[1]['default'].parse_mode == 'HTML'
|
|
||||||
assert call_args[1]['default'].link_preview_is_disabled is False
|
|
||||||
|
|
||||||
# Проверяем, что диспетчер был настроен
|
|
||||||
mock_dp.include_routers.assert_called_once()
|
|
||||||
mock_bot.delete_webhook.assert_called_once_with(drop_pending_updates=True)
|
|
||||||
mock_dp.start_polling.assert_called_once_with(mock_bot, skip_updates=True)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPrivateHandlers:
|
|
||||||
"""Тесты для приватных хэндлеров"""
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_message(self):
|
|
||||||
"""Создает мок сообщения"""
|
|
||||||
message = Mock(spec=Message)
|
|
||||||
message.from_user = Mock(spec=User)
|
|
||||||
message.from_user.id = 123456
|
|
||||||
message.from_user.full_name = "Test User"
|
|
||||||
message.from_user.username = "testuser"
|
|
||||||
message.from_user.first_name = "Test"
|
|
||||||
message.from_user.is_bot = False
|
|
||||||
message.from_user.language_code = "ru"
|
|
||||||
message.chat = Mock(spec=Chat)
|
|
||||||
message.chat.id = 123456
|
|
||||||
message.chat.type = "private"
|
|
||||||
message.text = "/start"
|
|
||||||
message.message_id = 1
|
|
||||||
message.forward = AsyncMock()
|
|
||||||
message.answer = AsyncMock()
|
|
||||||
message.answer_sticker = AsyncMock()
|
|
||||||
message.bot.send_message = AsyncMock()
|
|
||||||
return message
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_state(self):
|
|
||||||
"""Создает мок состояния"""
|
|
||||||
state = Mock(spec=FSMContext)
|
|
||||||
state.set_state = AsyncMock()
|
|
||||||
state.get_state = AsyncMock(return_value="START")
|
|
||||||
return state
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_db(self):
|
|
||||||
"""Создает мок базы данных"""
|
|
||||||
db = Mock(spec=BotDB)
|
|
||||||
db.user_exists = Mock(return_value=False)
|
|
||||||
db.add_new_user_in_db = Mock()
|
|
||||||
db.update_date_for_user = Mock()
|
|
||||||
db.update_username_and_full_name = Mock()
|
|
||||||
db.add_post_in_db = Mock()
|
|
||||||
db.update_info_about_stickers = Mock()
|
|
||||||
db.add_new_message_in_db = Mock()
|
|
||||||
return db
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_handle_start_message_new_user(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки команды /start для нового пользователя"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_messages.return_value = "Привет!"
|
|
||||||
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
|
|
||||||
mock_fs.return_value = "sticker_file"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await handle_start_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_db.user_exists.assert_called_once_with(123456)
|
|
||||||
mock_db.add_new_user_in_db.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
mock_message.answer_sticker.assert_called_once()
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_handle_start_message_existing_user(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки команды /start для существующего пользователя"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.check_username_and_full_name') as mock_check:
|
|
||||||
# Настройка моков
|
|
||||||
mock_db.user_exists.return_value = True
|
|
||||||
mock_check.return_value = False
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_messages.return_value = "Привет!"
|
|
||||||
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
|
|
||||||
mock_fs.return_value = "sticker_file"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await handle_start_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_db.user_exists.assert_called_once_with(123456)
|
|
||||||
mock_db.add_new_user_in_db.assert_not_called()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_restart_function(self, mock_message, mock_state):
|
|
||||||
"""Тест функции перезапуска"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
|
|
||||||
await restart_function(mock_message, mock_state)
|
|
||||||
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_message.answer.assert_called_once_with(
|
|
||||||
text='Я перезапущен!',
|
|
||||||
reply_markup=mock_keyboard.return_value
|
|
||||||
)
|
|
||||||
mock_state.set_state.assert_called_with('START')
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_post(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест функции предложения поста"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
mock_message.text = '📢Предложить свой пост'
|
|
||||||
mock_messages.side_effect = ["Введите текст поста", "Дополнительная информация"]
|
|
||||||
|
|
||||||
await suggest_post(mock_message, mock_state)
|
|
||||||
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("SUGGEST")
|
|
||||||
assert mock_message.answer.call_count == 2
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_end_message(self, mock_message, mock_state):
|
|
||||||
"""Тест функции прощания"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
mock_message.text = '👋🏼Сказать пока!'
|
|
||||||
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
|
|
||||||
mock_fs.return_value = "sticker_file"
|
|
||||||
mock_messages.return_value = "До свидания!"
|
|
||||||
|
|
||||||
await end_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_message.answer_sticker.assert_called_once()
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_text(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки текстового поста"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков
|
|
||||||
mock_message.content_type = 'text'
|
|
||||||
mock_message.text = 'Тестовый пост'
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_get_text.return_value = 'Обработанный текст'
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_send.return_value = 123
|
|
||||||
mock_messages.return_value = "Пост отправлен!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_send.assert_called()
|
|
||||||
mock_db.add_post_in_db.assert_called_once()
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_stickers(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест функции стикеров"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
mock_message.text = '🤪Хочу стикеры'
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
|
|
||||||
await stickers(mock_message, mock_state)
|
|
||||||
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_db.update_info_about_stickers.assert_called_once_with(user_id=123456)
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_connect_with_admin(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест функции связи с админами"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
mock_message.text = '📩Связаться с админами'
|
|
||||||
mock_messages.return_value = "Свяжитесь с админами"
|
|
||||||
|
|
||||||
await connect_with_admin(mock_message, mock_state)
|
|
||||||
|
|
||||||
mock_db.update_date_for_user.assert_called_once()
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("PRE_CHAT")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_resend_message_in_group_pre_chat(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест пересылки сообщения в группу (PRE_CHAT состояние)"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
mock_message.text = 'Тестовое сообщение'
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_messages.return_value = "Вопрос"
|
|
||||||
mock_state.get_state.return_value = "PRE_CHAT"
|
|
||||||
|
|
||||||
await resend_message_in_group_for_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
mock_db.update_date_for_user.assert_called_once()
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_db.add_new_message_in_db.assert_called_once()
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
|
|
||||||
class TestDependencyFactory:
|
|
||||||
"""Тесты для фабрики зависимостей"""
|
|
||||||
|
|
||||||
def test_get_global_instance_singleton(self):
|
|
||||||
"""Тест что get_global_instance возвращает синглтон"""
|
|
||||||
instance1 = get_global_instance()
|
|
||||||
instance2 = get_global_instance()
|
|
||||||
assert instance1 is instance2
|
|
||||||
|
|
||||||
def test_base_dependency_factory_initialization(self):
|
|
||||||
"""Тест инициализации BaseDependencyFactory"""
|
|
||||||
# Этот тест пропускаем из-за сложности мокирования configparser в уже загруженном модуле
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestBotIntegration:
|
|
||||||
"""Интеграционные тесты бота"""
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_bot_router_registration(self):
|
|
||||||
"""Тест регистрации роутеров в диспетчере"""
|
|
||||||
with patch('helper_bot.main.Bot') as mock_bot_class:
|
|
||||||
with patch('helper_bot.main.Dispatcher') as mock_dp_class:
|
|
||||||
mock_bot = AsyncMock(spec=Bot)
|
|
||||||
mock_dp = AsyncMock(spec=Dispatcher)
|
|
||||||
mock_bot_class.return_value = mock_bot
|
|
||||||
mock_dp_class.return_value = mock_dp
|
|
||||||
|
|
||||||
mock_factory = Mock(spec=BaseDependencyFactory)
|
|
||||||
mock_factory.settings = {
|
|
||||||
'Telegram': {
|
|
||||||
'bot_token': 'test_token',
|
|
||||||
'preview_link': False
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await start_bot(mock_factory)
|
|
||||||
|
|
||||||
# Проверяем, что все роутеры были зарегистрированы
|
|
||||||
mock_dp.include_routers.assert_called_once()
|
|
||||||
call_args = mock_dp.include_routers.call_args[0]
|
|
||||||
assert len(call_args) == 4 # private, callback, group, admin routers
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
pytest.main([__file__, '-v'])
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
# Импортируем моки в самом начале
|
|
||||||
import tests.mocks
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import Mock, AsyncMock, patch
|
|
||||||
from aiogram.types import Message, User, Chat
|
|
||||||
|
|
||||||
from helper_bot.handlers.private.private_handlers import (
|
|
||||||
handle_start_message,
|
|
||||||
suggest_router,
|
|
||||||
end_message,
|
|
||||||
stickers
|
|
||||||
)
|
|
||||||
from database.db import BotDB
|
|
||||||
|
|
||||||
|
|
||||||
class TestErrorHandling:
|
|
||||||
"""Тесты для обработки ошибок и граничных случаев"""
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_message(self):
|
|
||||||
"""Создает базовый мок сообщения"""
|
|
||||||
message = Mock(spec=Message)
|
|
||||||
message.from_user = Mock(spec=User)
|
|
||||||
message.from_user.id = 123456
|
|
||||||
message.from_user.full_name = "Test User"
|
|
||||||
message.from_user.username = "testuser"
|
|
||||||
message.from_user.first_name = "Test"
|
|
||||||
message.from_user.is_bot = False
|
|
||||||
message.from_user.language_code = "ru"
|
|
||||||
message.chat = Mock(spec=Chat)
|
|
||||||
message.chat.id = 123456
|
|
||||||
message.chat.type = "private"
|
|
||||||
message.message_id = 1
|
|
||||||
message.forward = AsyncMock()
|
|
||||||
message.answer = AsyncMock()
|
|
||||||
message.answer_sticker = AsyncMock()
|
|
||||||
message.bot.send_message = AsyncMock()
|
|
||||||
return message
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_state(self):
|
|
||||||
"""Создает мок состояния"""
|
|
||||||
state = Mock()
|
|
||||||
state.set_state = AsyncMock()
|
|
||||||
state.get_state = AsyncMock(return_value="START")
|
|
||||||
return state
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_db(self):
|
|
||||||
"""Создает мок базы данных"""
|
|
||||||
db = Mock(spec=BotDB)
|
|
||||||
db.user_exists = Mock(return_value=False)
|
|
||||||
db.add_new_user_in_db = Mock()
|
|
||||||
db.update_date_for_user = Mock()
|
|
||||||
db.update_username_and_full_name = Mock()
|
|
||||||
db.add_post_in_db = Mock()
|
|
||||||
db.update_info_about_stickers = Mock()
|
|
||||||
return db
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_handle_start_message_user_without_username(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки пользователя без username"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков
|
|
||||||
mock_message.from_user.username = None
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_messages.return_value = "Привет!"
|
|
||||||
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
|
|
||||||
mock_fs.return_value = "sticker_file"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await handle_start_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.bot.send_message.assert_called()
|
|
||||||
# Проверяем, что отправлено сообщение о пользователе без username
|
|
||||||
call_args = mock_message.bot.send_message.call_args_list
|
|
||||||
username_log_call = next(
|
|
||||||
(call for call in call_args if 'без username' in call[1]['text']),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
assert username_log_call is not None
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_handle_start_message_sticker_error(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки ошибки при получении стикера"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков с ошибкой
|
|
||||||
mock_path.return_value.rglob.side_effect = Exception("Sticker error")
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_messages.return_value = "Привет!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await handle_start_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.bot.send_message.assert_called()
|
|
||||||
# Проверяем, что отправлено сообщение об ошибке
|
|
||||||
call_args = mock_message.bot.send_message.call_args_list
|
|
||||||
error_call = next(
|
|
||||||
(call for call in call_args if 'ошибка при получении стикеров' in call[1]['text']),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
assert error_call is not None
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_handle_start_message_message_error(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки ошибки при отправке приветственного сообщения"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_messages.side_effect = Exception("Message error")
|
|
||||||
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
|
|
||||||
mock_fs.return_value = "sticker_file"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await handle_start_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.bot.send_message.assert_called()
|
|
||||||
# Проверяем, что отправлено сообщение об ошибке
|
|
||||||
call_args = mock_message.bot.send_message.call_args_list
|
|
||||||
# Проверяем, что было отправлено хотя бы одно сообщение
|
|
||||||
assert len(call_args) > 0
|
|
||||||
# Проверяем, что в одном из сообщений есть текст об ошибке
|
|
||||||
error_found = False
|
|
||||||
for call in call_args:
|
|
||||||
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
|
|
||||||
if 'Произошла ошибка' in text:
|
|
||||||
error_found = True
|
|
||||||
break
|
|
||||||
assert error_found
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_exception_handling(self, mock_message, mock_state):
|
|
||||||
"""Тест обработки исключений в suggest_router"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB') as mock_db:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
|
|
||||||
# Настройка моков с ошибкой
|
|
||||||
mock_message.content_type = 'text'
|
|
||||||
mock_message.text = 'Тестовый пост'
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_get_text.side_effect = Exception("Processing error")
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.bot.send_message.assert_called_once()
|
|
||||||
call_args = mock_message.bot.send_message.call_args
|
|
||||||
assert 'Произошла ошибка' in call_args[1]['text']
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_end_message_sticker_error(self, mock_message, mock_state):
|
|
||||||
"""Тест обработки ошибки при получении стикера в end_message"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков с ошибкой
|
|
||||||
mock_message.text = '👋🏼Сказать пока!'
|
|
||||||
mock_path.return_value.rglob.side_effect = Exception("Sticker error")
|
|
||||||
mock_messages.return_value = "До свидания!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await end_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.bot.send_message.assert_called()
|
|
||||||
call_args = mock_message.bot.send_message.call_args_list
|
|
||||||
# Проверяем, что в одном из сообщений есть текст об ошибке
|
|
||||||
error_found = False
|
|
||||||
for call in call_args:
|
|
||||||
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
|
|
||||||
if 'Произошла ошибка' in text:
|
|
||||||
error_found = True
|
|
||||||
break
|
|
||||||
assert error_found
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_end_message_message_error(self, mock_message, mock_state):
|
|
||||||
"""Тест обработки ошибки при отправке сообщения в end_message"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков
|
|
||||||
mock_message.text = '👋🏼Сказать пока!'
|
|
||||||
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
|
|
||||||
mock_fs.return_value = "sticker_file"
|
|
||||||
mock_messages.side_effect = Exception("Message error")
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await end_message(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.bot.send_message.assert_called()
|
|
||||||
call_args = mock_message.bot.send_message.call_args_list
|
|
||||||
# Проверяем, что в одном из сообщений есть текст об ошибке
|
|
||||||
error_found = False
|
|
||||||
for call in call_args:
|
|
||||||
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
|
|
||||||
if 'Произошла ошибка' in text:
|
|
||||||
error_found = True
|
|
||||||
break
|
|
||||||
assert error_found
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_stickers_exception_handling(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки исключений в stickers"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
# Настройка моков с ошибкой
|
|
||||||
mock_message.text = '🤪Хочу стикеры'
|
|
||||||
mock_db.update_info_about_stickers.side_effect = Exception("Database error")
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await stickers(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.bot.send_message.assert_called_once()
|
|
||||||
call_args = mock_message.bot.send_message.call_args
|
|
||||||
assert 'Произошла ошибка' in call_args[1]['text']
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_empty_text(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки пустого текста"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков
|
|
||||||
mock_message.content_type = 'text'
|
|
||||||
mock_message.text = ''
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_get_text.return_value = ''
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_send.return_value = 123
|
|
||||||
mock_messages.return_value = "Пост отправлен!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки - даже пустой текст должен обрабатываться
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_send.assert_called()
|
|
||||||
mock_db.add_post_in_db.assert_called_once()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_photo_without_caption(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки фото без подписи"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_photo_message') as mock_send_photo:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков для фото без подписи
|
|
||||||
mock_message.content_type = 'photo'
|
|
||||||
mock_message.caption = None
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_message.photo = [Mock()]
|
|
||||||
mock_message.photo[-1].file_id = 'photo_file_id'
|
|
||||||
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_send_photo.return_value = Mock()
|
|
||||||
mock_send_photo.return_value.message_id = 123
|
|
||||||
mock_send_photo.return_value.caption = ''
|
|
||||||
mock_messages.return_value = "Фото отправлено!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_send_photo.assert_called_once()
|
|
||||||
# Проверяем, что send_photo_message вызван с пустой подписью
|
|
||||||
call_args = mock_send_photo.call_args
|
|
||||||
assert call_args.kwargs.get('caption', '') == ''
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_media_group_without_caption(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки медиагруппы без подписи"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.prepare_media_group_from_middlewares') as mock_prepare:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_media_group_message_to_private_chat') as mock_send_group:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send_text:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков для медиагруппы без подписи
|
|
||||||
mock_message.media_group_id = 'group_123'
|
|
||||||
mock_message.content_type = 'photo'
|
|
||||||
|
|
||||||
# Создаем мок альбома без подписи
|
|
||||||
album = [mock_message]
|
|
||||||
album[0].caption = None
|
|
||||||
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_prepare.return_value = ['media1', 'media2']
|
|
||||||
mock_send_group.return_value = 123
|
|
||||||
mock_send_text.return_value = 456
|
|
||||||
mock_messages.return_value = "Медиагруппа отправлена!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state, album)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_prepare.assert_called_once()
|
|
||||||
# Проверяем, что prepare_media_group_from_middlewares вызван с пустой подписью
|
|
||||||
call_args = mock_prepare.call_args
|
|
||||||
assert call_args.kwargs.get('post_caption', '') == ''
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
pytest.main([__file__, '-v'])
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
# Импортируем моки в самом начале
|
|
||||||
import tests.mocks
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from unittest.mock import Mock, AsyncMock, patch
|
|
||||||
from aiogram.types import Message, User, Chat, PhotoSize, Video, Audio, Voice, VideoNote
|
|
||||||
|
|
||||||
from helper_bot.handlers.private.private_handlers import suggest_router
|
|
||||||
from database.db import BotDB
|
|
||||||
|
|
||||||
|
|
||||||
class TestMediaHandlers:
|
|
||||||
"""Тесты для обработки медиа-контента"""
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_message(self):
|
|
||||||
"""Создает базовый мок сообщения"""
|
|
||||||
message = Mock(spec=Message)
|
|
||||||
message.from_user = Mock(spec=User)
|
|
||||||
message.from_user.id = 123456
|
|
||||||
message.from_user.full_name = "Test User"
|
|
||||||
message.from_user.username = "testuser"
|
|
||||||
message.from_user.first_name = "Test"
|
|
||||||
message.chat = Mock(spec=Chat)
|
|
||||||
message.chat.id = 123456
|
|
||||||
message.chat.type = "private"
|
|
||||||
message.message_id = 1
|
|
||||||
message.forward = AsyncMock()
|
|
||||||
message.answer = AsyncMock()
|
|
||||||
message.bot.send_message = AsyncMock()
|
|
||||||
return message
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_state(self):
|
|
||||||
"""Создает мок состояния"""
|
|
||||||
state = Mock()
|
|
||||||
state.set_state = AsyncMock()
|
|
||||||
return state
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_db(self):
|
|
||||||
"""Создает мок базы данных"""
|
|
||||||
db = Mock(spec=BotDB)
|
|
||||||
db.add_post_in_db = Mock()
|
|
||||||
return db
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_photo(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки фото"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_photo_message') as mock_send_photo:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков для фото
|
|
||||||
mock_message.content_type = 'photo'
|
|
||||||
mock_message.caption = 'Тестовое фото'
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_message.photo = [Mock(spec=PhotoSize)]
|
|
||||||
mock_message.photo[-1].file_id = 'photo_file_id'
|
|
||||||
|
|
||||||
mock_get_text.return_value = 'Обработанная подпись'
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_send_photo.return_value = Mock()
|
|
||||||
mock_send_photo.return_value.message_id = 123
|
|
||||||
mock_send_photo.return_value.caption = 'Обработанная подпись'
|
|
||||||
mock_messages.return_value = "Фото отправлено!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_send_photo.assert_called_once()
|
|
||||||
mock_db.add_post_in_db.assert_called_once()
|
|
||||||
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_add_media.call_count >= 1
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_video(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки видео"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_video_message') as mock_send_video:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков для видео
|
|
||||||
mock_message.content_type = 'video'
|
|
||||||
mock_message.caption = 'Тестовое видео'
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_message.video = Mock(spec=Video)
|
|
||||||
mock_message.video.file_id = 'video_file_id'
|
|
||||||
|
|
||||||
mock_get_text.return_value = 'Обработанная подпись'
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_send_video.return_value = Mock()
|
|
||||||
mock_send_video.return_value.message_id = 123
|
|
||||||
mock_send_video.return_value.caption = 'Обработанная подпись'
|
|
||||||
mock_messages.return_value = "Видео отправлено!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_send_video.assert_called_once()
|
|
||||||
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_db.add_post_in_db.call_count >= 1
|
|
||||||
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_add_media.call_count >= 1
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_video_note(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки видеокружка"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_video_note_message') as mock_send_video_note:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков для видеокружка
|
|
||||||
mock_message.content_type = 'video_note'
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_message.video_note = Mock(spec=VideoNote)
|
|
||||||
mock_message.video_note.file_id = 'video_note_file_id'
|
|
||||||
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_send_video_note.return_value = Mock()
|
|
||||||
mock_send_video_note.return_value.message_id = 123
|
|
||||||
mock_messages.return_value = "Видеокружок отправлен!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_send_video_note.assert_called_once()
|
|
||||||
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_db.add_post_in_db.call_count >= 1
|
|
||||||
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_add_media.call_count >= 1
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_audio(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки аудио"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_audio_message') as mock_send_audio:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков для аудио
|
|
||||||
mock_message.content_type = 'audio'
|
|
||||||
mock_message.caption = 'Тестовое аудио'
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_message.audio = Mock(spec=Audio)
|
|
||||||
mock_message.audio.file_id = 'audio_file_id'
|
|
||||||
|
|
||||||
mock_get_text.return_value = 'Обработанная подпись'
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_send_audio.return_value = Mock()
|
|
||||||
mock_send_audio.return_value.message_id = 123
|
|
||||||
mock_send_audio.return_value.caption = 'Обработанная подпись'
|
|
||||||
mock_messages.return_value = "Аудио отправлено!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_send_audio.assert_called_once()
|
|
||||||
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_db.add_post_in_db.call_count >= 1
|
|
||||||
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_add_media.call_count >= 1
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_voice(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки голосового сообщения"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_voice_message') as mock_send_voice:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков для голосового сообщения
|
|
||||||
mock_message.content_type = 'voice'
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
mock_message.voice = Mock(spec=Voice)
|
|
||||||
mock_message.voice.file_id = 'voice_file_id'
|
|
||||||
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_send_voice.return_value = Mock()
|
|
||||||
mock_send_voice.return_value.message_id = 123
|
|
||||||
mock_messages.return_value = "Голосовое сообщение отправлено!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_message.forward.assert_called_once()
|
|
||||||
mock_send_voice.assert_called_once()
|
|
||||||
mock_db.add_post_in_db.assert_called_once()
|
|
||||||
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_add_media.call_count >= 1
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_media_group(self, mock_message, mock_state, mock_db):
|
|
||||||
"""Тест обработки медиагруппы"""
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.prepare_media_group_from_middlewares') as mock_prepare:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_media_group_message_to_private_chat') as mock_send_group:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send_text:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
|
|
||||||
with patch('helper_bot.handlers.private.private_handlers.sleep'):
|
|
||||||
# Настройка моков для медиагруппы
|
|
||||||
mock_message.media_group_id = 'group_123'
|
|
||||||
mock_message.content_type = 'photo'
|
|
||||||
|
|
||||||
# Создаем мок альбома
|
|
||||||
album = [mock_message]
|
|
||||||
album[0].caption = 'Подпись к медиагруппе'
|
|
||||||
|
|
||||||
mock_get_text.return_value = 'Обработанная подпись'
|
|
||||||
mock_keyboard_post.return_value = Mock()
|
|
||||||
mock_keyboard.return_value = Mock()
|
|
||||||
mock_prepare.return_value = ['media1', 'media2']
|
|
||||||
mock_send_group.return_value = 123
|
|
||||||
mock_send_text.return_value = 456
|
|
||||||
mock_messages.return_value = "Медиагруппа отправлена!"
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state, album)
|
|
||||||
|
|
||||||
# Проверки
|
|
||||||
mock_get_text.assert_called_once()
|
|
||||||
mock_prepare.assert_called_once()
|
|
||||||
mock_send_group.assert_called_once()
|
|
||||||
# Проверяем, что send_text_message был вызван (может быть вызван несколько раз)
|
|
||||||
assert mock_send_text.call_count >= 1
|
|
||||||
mock_db.update_helper_message_in_db.assert_called_once()
|
|
||||||
mock_message.answer.assert_called_once()
|
|
||||||
mock_state.set_state.assert_called_with("START")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_suggest_router_unsupported_content(self, mock_message, mock_state):
|
|
||||||
"""Тест обработки неподдерживаемого типа контента"""
|
|
||||||
# Настройка моков для неподдерживаемого контента
|
|
||||||
mock_message.content_type = 'document'
|
|
||||||
mock_message.media_group_id = None
|
|
||||||
|
|
||||||
# Выполнение теста
|
|
||||||
await suggest_router(mock_message, mock_state)
|
|
||||||
|
|
||||||
# Проверяем, что отправлено сообщение о неподдерживаемом типе
|
|
||||||
mock_message.bot.send_message.assert_called_once()
|
|
||||||
call_args = mock_message.bot.send_message.call_args
|
|
||||||
# Проверяем текст сообщения (может быть в позиционных или именованных аргументах)
|
|
||||||
text = call_args.kwargs.get('text', '') or (call_args[0][1] if len(call_args[0]) > 1 else '')
|
|
||||||
assert 'не умею работать с таким сообщением' in text
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
pytest.main([__file__, '-v'])
|
|
||||||
221
tests/test_refactored_admin_handlers.py
Normal file
221
tests/test_refactored_admin_handlers.py
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, AsyncMock, patch
|
||||||
|
from aiogram import types
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
|
from helper_bot.handlers.admin.services import AdminService, User, BannedUser
|
||||||
|
from helper_bot.handlers.admin.exceptions import (
|
||||||
|
UserNotFoundError,
|
||||||
|
UserAlreadyBannedError,
|
||||||
|
InvalidInputError
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdminService:
|
||||||
|
"""Тесты для AdminService"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Настройка перед каждым тестом"""
|
||||||
|
self.mock_db = Mock()
|
||||||
|
self.admin_service = AdminService(self.mock_db)
|
||||||
|
|
||||||
|
def test_get_last_users_success(self):
|
||||||
|
"""Тест успешного получения списка последних пользователей"""
|
||||||
|
# Arrange
|
||||||
|
# Формат данных: кортежи (full_name, user_id) как возвращает БД
|
||||||
|
mock_users_data = [
|
||||||
|
('User One', 1), # (full_name, user_id)
|
||||||
|
('User Two', 2) # (full_name, user_id)
|
||||||
|
]
|
||||||
|
self.mock_db.get_last_users_from_db.return_value = mock_users_data
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = self.admin_service.get_last_users()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert len(result) == 2
|
||||||
|
assert result[0].user_id == 1
|
||||||
|
assert result[0].username == 'Неизвестно' # username не возвращается из БД
|
||||||
|
assert result[0].full_name == 'User One'
|
||||||
|
assert result[1].user_id == 2
|
||||||
|
assert result[1].username == 'Неизвестно' # username не возвращается из БД
|
||||||
|
assert result[1].full_name == 'User Two'
|
||||||
|
|
||||||
|
def test_get_user_by_username_success(self):
|
||||||
|
"""Тест успешного получения пользователя по username"""
|
||||||
|
# Arrange
|
||||||
|
user_id = 123
|
||||||
|
username = "test_user"
|
||||||
|
full_name = "Test User"
|
||||||
|
self.mock_db.get_user_id_by_username.return_value = user_id
|
||||||
|
self.mock_db.get_full_name_by_id.return_value = full_name
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = self.admin_service.get_user_by_username(username)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is not None
|
||||||
|
assert result.user_id == user_id
|
||||||
|
assert result.username == username
|
||||||
|
assert result.full_name == full_name
|
||||||
|
|
||||||
|
def test_get_user_by_username_not_found(self):
|
||||||
|
"""Тест получения пользователя по несуществующему username"""
|
||||||
|
# Arrange
|
||||||
|
username = "nonexistent_user"
|
||||||
|
self.mock_db.get_user_id_by_username.return_value = None
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = self.admin_service.get_user_by_username(username)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_get_user_by_id_success(self):
|
||||||
|
"""Тест успешного получения пользователя по ID"""
|
||||||
|
# Arrange
|
||||||
|
user_id = 123
|
||||||
|
user_info = {'username': 'test_user', 'full_name': 'Test User'}
|
||||||
|
self.mock_db.get_user_info_by_id.return_value = user_info
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = self.admin_service.get_user_by_id(user_id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is not None
|
||||||
|
assert result.user_id == user_id
|
||||||
|
assert result.username == 'test_user'
|
||||||
|
assert result.full_name == 'Test User'
|
||||||
|
|
||||||
|
def test_get_user_by_id_not_found(self):
|
||||||
|
"""Тест получения пользователя по несуществующему ID"""
|
||||||
|
# Arrange
|
||||||
|
user_id = 999
|
||||||
|
self.mock_db.get_user_info_by_id.return_value = None
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = self.admin_service.get_user_by_id(user_id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_validate_user_input_success(self):
|
||||||
|
"""Тест успешной валидации ID пользователя"""
|
||||||
|
# Act
|
||||||
|
result = self.admin_service.validate_user_input("123")
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result == 123
|
||||||
|
|
||||||
|
def test_validate_user_input_invalid_number(self):
|
||||||
|
"""Тест валидации некорректного ID"""
|
||||||
|
# Act & Assert
|
||||||
|
with pytest.raises(InvalidInputError, match="ID пользователя должен быть числом"):
|
||||||
|
self.admin_service.validate_user_input("abc")
|
||||||
|
|
||||||
|
def test_validate_user_input_negative_number(self):
|
||||||
|
"""Тест валидации отрицательного ID"""
|
||||||
|
# Act & Assert
|
||||||
|
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"):
|
||||||
|
self.admin_service.validate_user_input("-1")
|
||||||
|
|
||||||
|
def test_validate_user_input_zero(self):
|
||||||
|
"""Тест валидации нулевого ID"""
|
||||||
|
# Act & Assert
|
||||||
|
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"):
|
||||||
|
self.admin_service.validate_user_input("0")
|
||||||
|
|
||||||
|
def test_ban_user_success(self):
|
||||||
|
"""Тест успешной блокировки пользователя"""
|
||||||
|
# Arrange
|
||||||
|
user_id = 123
|
||||||
|
username = "test_user"
|
||||||
|
reason = "Test ban"
|
||||||
|
ban_days = 7
|
||||||
|
|
||||||
|
self.mock_db.check_user_in_blacklist.return_value = False
|
||||||
|
self.mock_db.set_user_blacklist.return_value = None
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.mock_db.check_user_in_blacklist.assert_called_once_with(user_id)
|
||||||
|
self.mock_db.set_user_blacklist.assert_called_once()
|
||||||
|
|
||||||
|
def test_ban_user_already_banned(self):
|
||||||
|
"""Тест попытки заблокировать уже заблокированного пользователя"""
|
||||||
|
# Arrange
|
||||||
|
user_id = 123
|
||||||
|
username = "test_user"
|
||||||
|
reason = "Test ban"
|
||||||
|
ban_days = 7
|
||||||
|
|
||||||
|
self.mock_db.check_user_in_blacklist.return_value = True
|
||||||
|
|
||||||
|
# Act & Assert
|
||||||
|
with pytest.raises(UserAlreadyBannedError, match=f"Пользователь {user_id} уже заблокирован"):
|
||||||
|
self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||||
|
|
||||||
|
def test_ban_user_permanent(self):
|
||||||
|
"""Тест постоянной блокировки пользователя"""
|
||||||
|
# Arrange
|
||||||
|
user_id = 123
|
||||||
|
username = "test_user"
|
||||||
|
reason = "Permanent ban"
|
||||||
|
ban_days = None
|
||||||
|
|
||||||
|
self.mock_db.check_user_in_blacklist.return_value = False
|
||||||
|
self.mock_db.set_user_blacklist.return_value = None
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.mock_db.set_user_blacklist.assert_called_once_with(user_id, username, reason, None)
|
||||||
|
|
||||||
|
def test_unban_user_success(self):
|
||||||
|
"""Тест успешной разблокировки пользователя"""
|
||||||
|
# Arrange
|
||||||
|
user_id = 123
|
||||||
|
self.mock_db.delete_user_blacklist.return_value = None
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.admin_service.unban_user(user_id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.mock_db.delete_user_blacklist.assert_called_once_with(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUser:
|
||||||
|
"""Тесты для модели User"""
|
||||||
|
|
||||||
|
def test_user_creation(self):
|
||||||
|
"""Тест создания объекта User"""
|
||||||
|
# Act
|
||||||
|
user = User(user_id=123, username="test_user", full_name="Test User")
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert user.user_id == 123
|
||||||
|
assert user.username == "test_user"
|
||||||
|
assert user.full_name == "Test User"
|
||||||
|
|
||||||
|
|
||||||
|
class TestBannedUser:
|
||||||
|
"""Тесты для модели BannedUser"""
|
||||||
|
|
||||||
|
def test_banned_user_creation(self):
|
||||||
|
"""Тест создания объекта BannedUser"""
|
||||||
|
# Act
|
||||||
|
banned_user = BannedUser(
|
||||||
|
user_id=123,
|
||||||
|
username="test_user",
|
||||||
|
reason="Test ban",
|
||||||
|
unban_date="2025-01-01"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert banned_user.user_id == 123
|
||||||
|
assert banned_user.username == "test_user"
|
||||||
|
assert banned_user.reason == "Test ban"
|
||||||
|
assert banned_user.unban_date == "2025-01-01"
|
||||||
189
tests/test_refactored_group_handlers.py
Normal file
189
tests/test_refactored_group_handlers.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
"""Tests for refactored group handlers"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, AsyncMock, MagicMock
|
||||||
|
from aiogram import types
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
|
from helper_bot.handlers.group.group_handlers import (
|
||||||
|
create_group_handlers, GroupHandlers
|
||||||
|
)
|
||||||
|
from helper_bot.handlers.group.services import AdminReplyService
|
||||||
|
from helper_bot.handlers.group.exceptions import NoReplyToMessageError, UserNotFoundError
|
||||||
|
from helper_bot.handlers.group.constants import FSM_STATES, ERROR_MESSAGES
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroupHandlers:
|
||||||
|
"""Test class for GroupHandlers"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_db(self):
|
||||||
|
"""Mock database"""
|
||||||
|
db = Mock()
|
||||||
|
db.get_user_by_message_id = Mock()
|
||||||
|
return db
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_keyboard_markup(self):
|
||||||
|
"""Mock keyboard markup"""
|
||||||
|
return Mock()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_message(self):
|
||||||
|
"""Mock Telegram message"""
|
||||||
|
message = Mock()
|
||||||
|
message.from_user = Mock()
|
||||||
|
message.from_user.id = 12345
|
||||||
|
message.from_user.full_name = "Test Admin"
|
||||||
|
message.text = "test reply message"
|
||||||
|
message.chat = Mock()
|
||||||
|
message.chat.title = "Test Group"
|
||||||
|
message.chat.id = 67890
|
||||||
|
message.message_id = 111
|
||||||
|
message.answer = AsyncMock()
|
||||||
|
message.bot = Mock()
|
||||||
|
message.bot.send_message = AsyncMock()
|
||||||
|
return message
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_reply_message(self, mock_message):
|
||||||
|
"""Mock reply message"""
|
||||||
|
reply_message = Mock()
|
||||||
|
reply_message.message_id = 222
|
||||||
|
mock_message.reply_to_message = reply_message
|
||||||
|
return mock_message
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_state(self):
|
||||||
|
"""Mock FSM state"""
|
||||||
|
state = Mock(spec=FSMContext)
|
||||||
|
state.set_state = AsyncMock()
|
||||||
|
return state
|
||||||
|
|
||||||
|
def test_create_group_handlers(self, mock_db, mock_keyboard_markup):
|
||||||
|
"""Test creating group handlers instance"""
|
||||||
|
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||||
|
assert isinstance(handlers, GroupHandlers)
|
||||||
|
assert handlers.db == mock_db
|
||||||
|
assert handlers.keyboard_markup == mock_keyboard_markup
|
||||||
|
|
||||||
|
def test_group_handlers_initialization(self, mock_db, mock_keyboard_markup):
|
||||||
|
"""Test GroupHandlers initialization"""
|
||||||
|
handlers = GroupHandlers(mock_db, mock_keyboard_markup)
|
||||||
|
assert handlers.db == mock_db
|
||||||
|
assert handlers.keyboard_markup == mock_keyboard_markup
|
||||||
|
assert handlers.admin_reply_service is not None
|
||||||
|
assert handlers.router is not None
|
||||||
|
|
||||||
|
async def test_handle_message_success(self, mock_db, mock_keyboard_markup, mock_reply_message, mock_state):
|
||||||
|
"""Test successful message handling"""
|
||||||
|
mock_db.get_user_by_message_id.return_value = 99999
|
||||||
|
|
||||||
|
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||||
|
|
||||||
|
# Mock the send_reply_to_user method
|
||||||
|
handlers.admin_reply_service.send_reply_to_user = AsyncMock()
|
||||||
|
|
||||||
|
await handlers.handle_message(mock_reply_message, mock_state)
|
||||||
|
|
||||||
|
# Verify database call
|
||||||
|
mock_db.get_user_by_message_id.assert_called_once_with(222)
|
||||||
|
|
||||||
|
# Verify service call
|
||||||
|
handlers.admin_reply_service.send_reply_to_user.assert_called_once_with(
|
||||||
|
99999, mock_reply_message, "test reply message", mock_keyboard_markup
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify state was set
|
||||||
|
mock_state.set_state.assert_called_once_with(FSM_STATES["CHAT"])
|
||||||
|
|
||||||
|
async def test_handle_message_no_reply(self, mock_db, mock_keyboard_markup, mock_message, mock_state):
|
||||||
|
"""Test message handling without reply"""
|
||||||
|
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||||
|
|
||||||
|
# Mock the send_reply_to_user method to prevent it from being called
|
||||||
|
handlers.admin_reply_service.send_reply_to_user = AsyncMock()
|
||||||
|
|
||||||
|
# Ensure reply_to_message is None
|
||||||
|
mock_message.reply_to_message = None
|
||||||
|
|
||||||
|
await handlers.handle_message(mock_message, mock_state)
|
||||||
|
|
||||||
|
# Verify error message was sent
|
||||||
|
mock_message.answer.assert_called_once_with(ERROR_MESSAGES["NO_REPLY_TO_MESSAGE"])
|
||||||
|
|
||||||
|
# Verify no database calls
|
||||||
|
mock_db.get_user_by_message_id.assert_not_called()
|
||||||
|
|
||||||
|
# Verify send_reply_to_user was not called
|
||||||
|
handlers.admin_reply_service.send_reply_to_user.assert_not_called()
|
||||||
|
|
||||||
|
# Verify state was not set
|
||||||
|
mock_state.set_state.assert_not_called()
|
||||||
|
|
||||||
|
async def test_handle_message_user_not_found(self, mock_db, mock_keyboard_markup, mock_reply_message, mock_state):
|
||||||
|
"""Test message handling when user is not found"""
|
||||||
|
mock_db.get_user_by_message_id.return_value = None
|
||||||
|
|
||||||
|
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||||
|
|
||||||
|
await handlers.handle_message(mock_reply_message, mock_state)
|
||||||
|
|
||||||
|
# Verify error message was sent
|
||||||
|
mock_reply_message.answer.assert_called_once_with(ERROR_MESSAGES["USER_NOT_FOUND"])
|
||||||
|
|
||||||
|
# Verify database call
|
||||||
|
mock_db.get_user_by_message_id.assert_called_once_with(222)
|
||||||
|
|
||||||
|
# Verify state was not set
|
||||||
|
mock_state.set_state.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdminReplyService:
|
||||||
|
"""Test class for AdminReplyService"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_db(self):
|
||||||
|
"""Mock database"""
|
||||||
|
db = Mock()
|
||||||
|
db.get_user_by_message_id = Mock()
|
||||||
|
return db
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def service(self, mock_db):
|
||||||
|
"""Create service instance"""
|
||||||
|
return AdminReplyService(mock_db)
|
||||||
|
|
||||||
|
def test_get_user_id_for_reply_success(self, service, mock_db):
|
||||||
|
"""Test successful user ID retrieval"""
|
||||||
|
mock_db.get_user_by_message_id.return_value = 12345
|
||||||
|
|
||||||
|
result = service.get_user_id_for_reply(111)
|
||||||
|
|
||||||
|
assert result == 12345
|
||||||
|
mock_db.get_user_by_message_id.assert_called_once_with(111)
|
||||||
|
|
||||||
|
def test_get_user_id_for_reply_not_found(self, service, mock_db):
|
||||||
|
"""Test user ID retrieval when user not found"""
|
||||||
|
mock_db.get_user_by_message_id.return_value = None
|
||||||
|
|
||||||
|
with pytest.raises(UserNotFoundError, match="User not found for message_id: 111"):
|
||||||
|
service.get_user_id_for_reply(111)
|
||||||
|
|
||||||
|
mock_db.get_user_by_message_id.assert_called_once_with(111)
|
||||||
|
|
||||||
|
async def test_send_reply_to_user(self, service, mock_db):
|
||||||
|
"""Test sending reply to user"""
|
||||||
|
message = Mock()
|
||||||
|
message.reply_to_message = Mock()
|
||||||
|
message.reply_to_message.message_id = 222
|
||||||
|
markup = Mock()
|
||||||
|
|
||||||
|
# Mock the send_text_message function
|
||||||
|
with pytest.MonkeyPatch().context() as m:
|
||||||
|
mock_send_text = AsyncMock()
|
||||||
|
m.setattr('helper_bot.handlers.group.services.send_text_message', mock_send_text)
|
||||||
|
|
||||||
|
await service.send_reply_to_user(12345, message, "test reply", markup)
|
||||||
|
|
||||||
|
mock_send_text.assert_called_once_with(12345, message, "test reply", markup)
|
||||||
@@ -46,13 +46,22 @@ class TestPrivateHandlers:
|
|||||||
def mock_message(self):
|
def mock_message(self):
|
||||||
"""Mock Telegram message"""
|
"""Mock Telegram message"""
|
||||||
message = Mock(spec=types.Message)
|
message = Mock(spec=types.Message)
|
||||||
message.from_user.id = 12345
|
# Создаем мок для from_user
|
||||||
message.from_user.full_name = "Test User"
|
from_user = Mock()
|
||||||
message.from_user.username = "testuser"
|
from_user.id = 12345
|
||||||
message.from_user.is_bot = False
|
from_user.full_name = "Test User"
|
||||||
message.from_user.language_code = "ru"
|
from_user.username = "testuser"
|
||||||
|
from_user.is_bot = False
|
||||||
|
from_user.language_code = "ru"
|
||||||
|
message.from_user = from_user
|
||||||
|
|
||||||
message.text = "test message"
|
message.text = "test message"
|
||||||
message.chat.id = 12345
|
|
||||||
|
# Создаем мок для chat
|
||||||
|
chat = Mock()
|
||||||
|
chat.id = 12345
|
||||||
|
message.chat = chat
|
||||||
|
|
||||||
message.bot = Mock()
|
message.bot = Mock()
|
||||||
message.bot.send_message = AsyncMock()
|
message.bot.send_message = AsyncMock()
|
||||||
message.forward = AsyncMock()
|
message.forward = AsyncMock()
|
||||||
|
|||||||
Reference in New Issue
Block a user