fix quality code
This commit is contained in:
@@ -1,27 +1,37 @@
|
||||
from .admin_handlers import admin_router
|
||||
from .dependencies import AdminAccessMiddleware, BotDB, Settings
|
||||
from .exceptions import (AdminAccessDeniedError, AdminError, InvalidInputError,
|
||||
UserAlreadyBannedError, UserNotFoundError)
|
||||
from .exceptions import (
|
||||
AdminAccessDeniedError,
|
||||
AdminError,
|
||||
InvalidInputError,
|
||||
UserAlreadyBannedError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
from .services import AdminService, BannedUser, User
|
||||
from .utils import (escape_html, format_ban_confirmation, format_user_info,
|
||||
handle_admin_error, return_to_admin_menu)
|
||||
from .utils import (
|
||||
escape_html,
|
||||
format_ban_confirmation,
|
||||
format_user_info,
|
||||
handle_admin_error,
|
||||
return_to_admin_menu,
|
||||
)
|
||||
|
||||
__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'
|
||||
]
|
||||
"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,21 +1,29 @@
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, MagicData, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.handlers.admin.dependencies import AdminAccessMiddleware
|
||||
from helper_bot.handlers.admin.exceptions import (InvalidInputError,
|
||||
UserAlreadyBannedError)
|
||||
from helper_bot.handlers.admin.exceptions import (
|
||||
InvalidInputError,
|
||||
UserAlreadyBannedError,
|
||||
)
|
||||
from helper_bot.handlers.admin.services import AdminService
|
||||
from helper_bot.handlers.admin.utils import (escape_html,
|
||||
format_ban_confirmation,
|
||||
format_user_info,
|
||||
handle_admin_error,
|
||||
return_to_admin_menu)
|
||||
from helper_bot.keyboards.keyboards import (create_keyboard_for_approve_ban,
|
||||
create_keyboard_for_ban_days,
|
||||
create_keyboard_for_ban_reason,
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard_admin)
|
||||
from helper_bot.handlers.admin.utils import (
|
||||
escape_html,
|
||||
format_ban_confirmation,
|
||||
format_user_info,
|
||||
handle_admin_error,
|
||||
return_to_admin_menu,
|
||||
)
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
create_keyboard_for_approve_ban,
|
||||
create_keyboard_for_ban_days,
|
||||
create_keyboard_for_ban_reason,
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard_admin,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
@@ -29,23 +37,19 @@ admin_router.message.middleware(AdminAccessMiddleware())
|
||||
# ХЕНДЛЕРЫ МЕНЮ
|
||||
# ============================================================================
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command('admin')
|
||||
)
|
||||
|
||||
@admin_router.message(ChatTypeFilter(chat_type=["private"]), Command("admin"))
|
||||
@track_time("admin_panel", "admin_handlers")
|
||||
@track_errors("admin_handlers", "admin_panel")
|
||||
async def admin_panel(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def admin_panel(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Главное меню администратора"""
|
||||
try:
|
||||
await state.set_state("ADMIN")
|
||||
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
|
||||
markup = get_reply_keyboard_admin()
|
||||
await message.answer("Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup)
|
||||
await message.answer(
|
||||
"Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup
|
||||
)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "admin_panel")
|
||||
|
||||
@@ -54,18 +58,20 @@ async def admin_panel(
|
||||
# ХЕНДЛЕР ОТМЕНЫ
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION"),
|
||||
F.text == 'Отменить'
|
||||
StateFilter(
|
||||
"AWAIT_BAN_TARGET",
|
||||
"AWAIT_BAN_DETAILS",
|
||||
"AWAIT_BAN_DURATION",
|
||||
"BAN_CONFIRMATION",
|
||||
),
|
||||
F.text == "Отменить",
|
||||
)
|
||||
@track_time("cancel_ban_process", "admin_handlers")
|
||||
@track_errors("admin_handlers", "cancel_ban_process")
|
||||
async def cancel_ban_process(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def cancel_ban_process(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Отмена процесса блокировки"""
|
||||
try:
|
||||
current_state = await state.get_state()
|
||||
@@ -78,32 +84,31 @@ async def cancel_ban_process(
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("ADMIN"),
|
||||
F.text == 'Бан (Список)'
|
||||
F.text == "Бан (Список)",
|
||||
)
|
||||
@track_time("get_last_users", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_last_users")
|
||||
@db_query_time("get_last_users", "users", "select")
|
||||
async def get_last_users(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Получение списка последних пользователей"""
|
||||
try:
|
||||
logger.info(f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}")
|
||||
logger.info(
|
||||
f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}"
|
||||
)
|
||||
admin_service = AdminService(bot_db)
|
||||
users = await admin_service.get_last_users()
|
||||
|
||||
|
||||
# Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination)
|
||||
users_data = [
|
||||
(user.full_name, user.user_id)
|
||||
for user in users
|
||||
]
|
||||
|
||||
keyboard = create_keyboard_with_pagination(1, len(users_data), users_data, 'ban')
|
||||
users_data = [(user.full_name, user.user_id) for user in users]
|
||||
|
||||
keyboard = create_keyboard_with_pagination(
|
||||
1, len(users_data), users_data, "ban"
|
||||
)
|
||||
await message.answer(
|
||||
text="Список пользователей которые последними обращались к боту",
|
||||
reply_markup=keyboard
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "get_last_users")
|
||||
@@ -112,27 +117,31 @@ async def get_last_users(
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("ADMIN"),
|
||||
F.text == 'Разбан (список)'
|
||||
F.text == "Разбан (список)",
|
||||
)
|
||||
@track_time("get_banned_users", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_banned_users")
|
||||
@db_query_time("get_banned_users", "users", "select")
|
||||
async def get_banned_users(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Получение списка заблокированных пользователей"""
|
||||
try:
|
||||
logger.info(f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}")
|
||||
logger.info(
|
||||
f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}"
|
||||
)
|
||||
admin_service = AdminService(bot_db)
|
||||
message_text, buttons_list = await admin_service.get_banned_users_for_display(0)
|
||||
|
||||
|
||||
if buttons_list:
|
||||
keyboard = 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=keyboard)
|
||||
else:
|
||||
await message.answer(text="В списке заблокированных пользователей никого нет")
|
||||
await message.answer(
|
||||
text="В списке заблокированных пользователей никого нет"
|
||||
)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "get_banned_users")
|
||||
|
||||
@@ -141,68 +150,80 @@ async def get_banned_users(
|
||||
# ХЕНДЛЕРЫ ПРОЦЕССА БАНА
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("ADMIN"),
|
||||
F.text.in_(['Бан по нику', 'Бан по ID'])
|
||||
F.text.in_(["Бан по нику", "Бан по ID"]),
|
||||
)
|
||||
@track_time("start_ban_process", "admin_handlers")
|
||||
@track_errors("admin_handlers", "start_ban_process")
|
||||
async def start_ban_process(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def start_ban_process(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Начало процесса блокировки пользователя"""
|
||||
try:
|
||||
ban_type = "username" if message.text == 'Бан по нику' else "id"
|
||||
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 блокируемого пользователя"
|
||||
|
||||
prompt_text = (
|
||||
"Пришли мне username блокируемого пользователя"
|
||||
if ban_type == "username"
|
||||
else "Пришли мне ID блокируемого пользователя"
|
||||
)
|
||||
await message.answer(prompt_text)
|
||||
await state.set_state('AWAIT_BAN_TARGET')
|
||||
await state.set_state("AWAIT_BAN_TARGET")
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "start_ban_process")
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_TARGET")
|
||||
ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_TARGET")
|
||||
)
|
||||
@track_time("process_ban_target", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_target")
|
||||
async def process_ban_target(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Обработка введенного username/ID для блокировки"""
|
||||
logger.info(f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||
|
||||
logger.info(
|
||||
f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}"
|
||||
)
|
||||
|
||||
try:
|
||||
user_data = await state.get_data()
|
||||
ban_type = user_data.get('ban_type')
|
||||
ban_type = user_data.get("ban_type")
|
||||
admin_service = AdminService(bot_db)
|
||||
|
||||
|
||||
logger.info(f"process_ban_target: ban_type={ban_type}, user_data={user_data}")
|
||||
|
||||
# Определяем пользователя
|
||||
if ban_type == "username":
|
||||
logger.info(f"process_ban_target: Поиск пользователя по username: {message.text}")
|
||||
logger.info(
|
||||
f"process_ban_target: Поиск пользователя по username: {message.text}"
|
||||
)
|
||||
user = await admin_service.get_user_by_username(message.text)
|
||||
if not user:
|
||||
logger.warning(f"process_ban_target: Пользователь с username '{message.text}' не найден")
|
||||
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
||||
logger.warning(
|
||||
f"process_ban_target: Пользователь с username '{message.text}' не найден"
|
||||
)
|
||||
await message.answer(
|
||||
f"Пользователь с username '{escape_html(message.text)}' не найден."
|
||||
)
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
else: # ban_type == "id"
|
||||
try:
|
||||
logger.info(f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}")
|
||||
logger.info(
|
||||
f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}"
|
||||
)
|
||||
user_id = await admin_service.validate_user_input(message.text)
|
||||
user = await admin_service.get_user_by_id(user_id)
|
||||
if not user:
|
||||
logger.warning(f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных")
|
||||
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
||||
logger.warning(
|
||||
f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных"
|
||||
)
|
||||
await message.answer(
|
||||
f"Пользователь с ID {user_id} не найден в базе данных."
|
||||
)
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
except InvalidInputError as e:
|
||||
@@ -210,115 +231,117 @@ async def process_ban_target(
|
||||
await message.answer(str(e))
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
|
||||
logger.info(f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}"
|
||||
)
|
||||
|
||||
# Сохраняем данные пользователя
|
||||
await state.update_data(
|
||||
target_user_id=user.user_id,
|
||||
target_username=user.username,
|
||||
target_full_name=user.full_name
|
||||
target_full_name=user.full_name,
|
||||
)
|
||||
|
||||
|
||||
# Показываем информацию о пользователе и запрашиваем причину
|
||||
user_info = format_user_info(user.user_id, user.username, user.full_name)
|
||||
markup = create_keyboard_for_ban_reason()
|
||||
logger.info(f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}")
|
||||
|
||||
logger.info(
|
||||
f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
await state.set_state('AWAIT_BAN_DETAILS')
|
||||
await state.set_state("AWAIT_BAN_DETAILS")
|
||||
logger.info("process_ban_target: Состояние изменено на AWAIT_BAN_DETAILS")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"process_ban_target: Неожиданная ошибка: {e}", exc_info=True)
|
||||
await handle_admin_error(message, e, state, "process_ban_target")
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_DETAILS")
|
||||
ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DETAILS")
|
||||
)
|
||||
@track_time("process_ban_reason", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_reason")
|
||||
async def process_ban_reason(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def process_ban_reason(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Обработка причины блокировки"""
|
||||
logger.info(f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||
|
||||
logger.info(
|
||||
f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Проверяем текущее состояние
|
||||
current_state = await state.get_state()
|
||||
logger.info(f"process_ban_reason: Текущее состояние: {current_state}")
|
||||
|
||||
|
||||
# Проверяем данные состояния
|
||||
state_data = await state.get_data()
|
||||
logger.info(f"process_ban_reason: Данные состояния: {state_data}")
|
||||
|
||||
logger.info(f"process_ban_reason: Обновление данных состояния с причиной: {message.text}")
|
||||
|
||||
logger.info(
|
||||
f"process_ban_reason: Обновление данных состояния с причиной: {message.text}"
|
||||
)
|
||||
await state.update_data(ban_reason=message.text)
|
||||
|
||||
|
||||
markup = create_keyboard_for_ban_days()
|
||||
safe_reason = escape_html(message.text)
|
||||
logger.info(f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}")
|
||||
|
||||
logger.info(
|
||||
f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
await state.set_state('AWAIT_BAN_DURATION')
|
||||
await state.set_state("AWAIT_BAN_DURATION")
|
||||
logger.info("process_ban_reason: Состояние изменено на AWAIT_BAN_DURATION")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"process_ban_reason: Неожиданная ошибка: {e}", exc_info=True)
|
||||
await handle_admin_error(message, e, state, "process_ban_reason")
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_DURATION")
|
||||
ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DURATION")
|
||||
)
|
||||
@track_time("process_ban_duration", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_duration")
|
||||
async def process_ban_duration(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def process_ban_duration(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Обработка срока блокировки"""
|
||||
try:
|
||||
user_data = await state.get_data()
|
||||
|
||||
|
||||
# Определяем срок блокировки
|
||||
if message.text == 'Навсегда':
|
||||
if message.text == "Навсегда":
|
||||
ban_days = None
|
||||
else:
|
||||
try:
|
||||
ban_days = int(message.text)
|
||||
if ban_days <= 0:
|
||||
await message.answer("Срок блокировки должен быть положительным числом.")
|
||||
await message.answer(
|
||||
"Срок блокировки должен быть положительным числом."
|
||||
)
|
||||
return
|
||||
except ValueError:
|
||||
await message.answer("Пожалуйста, введите корректное число дней или выберите 'Навсегда'.")
|
||||
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
|
||||
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')
|
||||
|
||||
await state.set_state("BAN_CONFIRMATION")
|
||||
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "process_ban_duration")
|
||||
|
||||
@@ -326,35 +349,31 @@ async def process_ban_duration(
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("BAN_CONFIRMATION"),
|
||||
F.text == 'Подтвердить'
|
||||
F.text == "Подтвердить",
|
||||
)
|
||||
@track_time("confirm_ban", "admin_handlers")
|
||||
@track_errors("admin_handlers", "confirm_ban")
|
||||
async def confirm_ban(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
**kwargs
|
||||
):
|
||||
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), **kwargs
|
||||
):
|
||||
"""Подтверждение блокировки пользователя"""
|
||||
try:
|
||||
user_data = await state.get_data()
|
||||
admin_service = AdminService(bot_db)
|
||||
|
||||
|
||||
# Выполняем блокировку
|
||||
await admin_service.ban_user(
|
||||
user_id=user_data['target_user_id'],
|
||||
username=user_data['target_username'],
|
||||
reason=user_data['ban_reason'],
|
||||
ban_days=user_data['ban_days'],
|
||||
user_id=user_data["target_user_id"],
|
||||
username=user_data["target_username"],
|
||||
reason=user_data["ban_reason"],
|
||||
ban_days=user_data["ban_days"],
|
||||
ban_author_id=message.from_user.id,
|
||||
)
|
||||
|
||||
safe_username = escape_html(user_data['target_username'])
|
||||
|
||||
safe_username = escape_html(user_data["target_username"])
|
||||
await message.reply(f"Пользователь {safe_username} успешно заблокирован.")
|
||||
await return_to_admin_menu(message, state)
|
||||
|
||||
|
||||
except UserAlreadyBannedError as e:
|
||||
await message.reply(str(e))
|
||||
await return_to_admin_menu(message, state)
|
||||
|
||||
@@ -9,7 +9,7 @@ ADMIN_BUTTON_TEXTS: Final[Dict[str, str]] = {
|
||||
"BAN_BY_ID": "Бан по ID",
|
||||
"UNBAN_LIST": "Разбан (список)",
|
||||
"RETURN_TO_BOT": "Вернуться в бота",
|
||||
"CANCEL": "Отменить"
|
||||
"CANCEL": "Отменить",
|
||||
}
|
||||
|
||||
# Admin button to command mapping for metrics
|
||||
@@ -19,11 +19,11 @@ ADMIN_BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"Бан по ID": "admin_ban_by_id",
|
||||
"Разбан (список)": "admin_unban_list",
|
||||
"Вернуться в бота": "admin_return_to_bot",
|
||||
"Отменить": "admin_cancel"
|
||||
"Отменить": "admin_cancel",
|
||||
}
|
||||
|
||||
# Admin commands
|
||||
ADMIN_COMMANDS: Final[Dict[str, str]] = {
|
||||
"ADMIN": "admin",
|
||||
"TEST_METRICS": "test_metrics"
|
||||
"TEST_METRICS": "test_metrics",
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ except ImportError:
|
||||
|
||||
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
|
||||
@@ -14,36 +15,46 @@ 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'):
|
||||
|
||||
async def __call__(
|
||||
self, handler, event: TelegramObject, data: Dict[str, Any]
|
||||
) -> Any:
|
||||
if hasattr(event, "from_user"):
|
||||
user_id = event.from_user.id
|
||||
username = getattr(event.from_user, 'username', 'Unknown')
|
||||
|
||||
logger.info(f"AdminAccessMiddleware: проверка доступа для пользователя {username} (ID: {user_id})")
|
||||
|
||||
username = getattr(event.from_user, "username", "Unknown")
|
||||
|
||||
logger.info(
|
||||
f"AdminAccessMiddleware: проверка доступа для пользователя {username} (ID: {user_id})"
|
||||
)
|
||||
|
||||
# Получаем bot_db из data (внедренного DependenciesMiddleware)
|
||||
bot_db = data.get('bot_db')
|
||||
bot_db = data.get("bot_db")
|
||||
if not bot_db:
|
||||
# Fallback: получаем напрямую если middleware не сработала
|
||||
bdf = get_global_instance()
|
||||
bot_db = bdf.get_db()
|
||||
|
||||
|
||||
is_admin_result = await check_access(user_id, bot_db)
|
||||
logger.info(f"AdminAccessMiddleware: результат проверки для {username}: {is_admin_result}")
|
||||
|
||||
logger.info(
|
||||
f"AdminAccessMiddleware: результат проверки для {username}: {is_admin_result}"
|
||||
)
|
||||
|
||||
if not is_admin_result:
|
||||
logger.warning(f"AdminAccessMiddleware: доступ запрещен для пользователя {username} (ID: {user_id})")
|
||||
if hasattr(event, 'answer'):
|
||||
await event.answer('Доступ запрещен!')
|
||||
logger.warning(
|
||||
f"AdminAccessMiddleware: доступ запрещен для пользователя {username} (ID: {user_id})"
|
||||
)
|
||||
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'")
|
||||
logger.error(
|
||||
f"Ошибка в AdminAccessMiddleware: {e}. Хендлер не принимает параметр 'data'"
|
||||
)
|
||||
# Пытаемся вызвать хендлер без data (для совместимости с MagicData)
|
||||
return await handler(event)
|
||||
else:
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
class AdminError(Exception):
|
||||
"""Базовое исключение для административных операций"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AdminAccessDeniedError(AdminError):
|
||||
"""Исключение при отказе в административном доступе"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFoundError(AdminError):
|
||||
"""Исключение при отсутствии пользователя"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidInputError(AdminError):
|
||||
"""Исключение при некорректном вводе данных"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserAlreadyBannedError(AdminError):
|
||||
"""Исключение при попытке забанить уже заблокированного пользователя"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
"""
|
||||
Обработчики команд для мониторинга rate limiting
|
||||
"""
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, MagicData
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.middlewares.dependencies_middleware import \
|
||||
DependenciesMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import track_errors, track_time
|
||||
from helper_bot.utils.rate_limit_metrics import (
|
||||
get_rate_limit_metrics_summary, update_rate_limit_gauges)
|
||||
from helper_bot.utils.rate_limit_monitor import (get_rate_limit_summary,
|
||||
rate_limit_monitor)
|
||||
get_rate_limit_metrics_summary,
|
||||
update_rate_limit_gauges,
|
||||
)
|
||||
from helper_bot.utils.rate_limit_monitor import (
|
||||
get_rate_limit_summary,
|
||||
rate_limit_monitor,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class RateLimitHandlers:
|
||||
def __init__(self, db, settings):
|
||||
self.db = db.get_db() if hasattr(db, 'get_db') else db
|
||||
self.db = db.get_db() if hasattr(db, "get_db") else db
|
||||
self.settings = settings
|
||||
self.router = Router()
|
||||
self._setup_handlers()
|
||||
@@ -33,38 +39,38 @@ class RateLimitHandlers:
|
||||
self.router.message.register(
|
||||
self.rate_limit_stats_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_stats")
|
||||
Command("ratelimit_stats"),
|
||||
)
|
||||
|
||||
|
||||
# Команда для сброса статистики rate limiting
|
||||
self.router.message.register(
|
||||
self.reset_rate_limit_stats_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("reset_ratelimit_stats")
|
||||
Command("reset_ratelimit_stats"),
|
||||
)
|
||||
|
||||
|
||||
# Команда для просмотра ошибок rate limiting
|
||||
self.router.message.register(
|
||||
self.rate_limit_errors_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_errors")
|
||||
Command("ratelimit_errors"),
|
||||
)
|
||||
|
||||
|
||||
# Команда для просмотра Prometheus метрик
|
||||
self.router.message.register(
|
||||
self.rate_limit_prometheus_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_prometheus")
|
||||
Command("ratelimit_prometheus"),
|
||||
)
|
||||
|
||||
@track_time("rate_limit_stats_handler", "rate_limit_handlers")
|
||||
@track_errors("rate_limit_handlers", "rate_limit_stats_handler")
|
||||
async def rate_limit_stats_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Показывает статистику rate limiting"""
|
||||
try:
|
||||
@@ -72,11 +78,11 @@ class RateLimitHandlers:
|
||||
if not await bot_db.is_admin(message.from_user.id):
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
|
||||
# Получаем сводку
|
||||
summary = get_rate_limit_summary()
|
||||
global_stats = rate_limit_monitor.get_global_stats()
|
||||
|
||||
|
||||
# Формируем сообщение со статистикой
|
||||
stats_text = (
|
||||
f"📊 <b>Статистика Rate Limiting</b>\n\n"
|
||||
@@ -89,15 +95,17 @@ class RateLimitHandlers:
|
||||
f"• Активных чатов: {summary['active_chats']}\n"
|
||||
f"• Ошибок за час: {summary['recent_errors_count']}\n\n"
|
||||
)
|
||||
|
||||
|
||||
# Добавляем детальную статистику
|
||||
stats_text += f"🔍 <b>Детальная статистика:</b>\n"
|
||||
stats_text += f"• Успешных запросов: {global_stats.successful_requests}\n"
|
||||
stats_text += f"• Неудачных запросов: {global_stats.failed_requests}\n"
|
||||
stats_text += f"• RetryAfter ошибок: {global_stats.retry_after_errors}\n"
|
||||
stats_text += f"• Других ошибок: {global_stats.other_errors}\n"
|
||||
stats_text += f"• Общее время ожидания: {global_stats.total_wait_time:.2f}с\n\n"
|
||||
|
||||
stats_text += (
|
||||
f"• Общее время ожидания: {global_stats.total_wait_time:.2f}с\n\n"
|
||||
)
|
||||
|
||||
# Добавляем топ чатов по запросам
|
||||
top_chats = rate_limit_monitor.get_top_chats_by_requests(5)
|
||||
if top_chats:
|
||||
@@ -105,16 +113,16 @@ class RateLimitHandlers:
|
||||
for i, (chat_id, chat_stats) in enumerate(top_chats, 1):
|
||||
stats_text += f"{i}. Chat {chat_id}: {chat_stats.total_requests} запросов ({chat_stats.success_rate:.1%} успех)\n"
|
||||
stats_text += "\n"
|
||||
|
||||
|
||||
# Добавляем чаты с высоким процентом ошибок
|
||||
high_error_chats = rate_limit_monitor.get_chats_with_high_error_rate(0.1)
|
||||
if high_error_chats:
|
||||
stats_text += f"⚠️ <b>Чаты с высоким процентом ошибок (>10%):</b>\n"
|
||||
for chat_id, chat_stats in high_error_chats[:3]:
|
||||
stats_text += f"• Chat {chat_id}: {chat_stats.error_rate:.1%} ошибок ({chat_stats.failed_requests}/{chat_stats.total_requests})\n"
|
||||
|
||||
await message.answer(stats_text, parse_mode='HTML')
|
||||
|
||||
|
||||
await message.answer(stats_text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики rate limiting: {e}")
|
||||
await message.answer("Произошла ошибка при получении статистики.")
|
||||
@@ -123,10 +131,10 @@ class RateLimitHandlers:
|
||||
@track_errors("rate_limit_handlers", "reset_rate_limit_stats_handler")
|
||||
async def reset_rate_limit_stats_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Сбрасывает статистику rate limiting"""
|
||||
try:
|
||||
@@ -134,12 +142,12 @@ class RateLimitHandlers:
|
||||
if not await bot_db.is_admin(message.from_user.id):
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
|
||||
# Сбрасываем статистику
|
||||
rate_limit_monitor.reset_stats()
|
||||
|
||||
|
||||
await message.answer("✅ Статистика rate limiting сброшена.")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сбросе статистики rate limiting: {e}")
|
||||
await message.answer("Произошла ошибка при сбросе статистики.")
|
||||
@@ -148,10 +156,10 @@ class RateLimitHandlers:
|
||||
@track_errors("rate_limit_handlers", "rate_limit_errors_handler")
|
||||
async def rate_limit_errors_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Показывает недавние ошибки rate limiting"""
|
||||
try:
|
||||
@@ -159,29 +167,34 @@ class RateLimitHandlers:
|
||||
if not await bot_db.is_admin(message.from_user.id):
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
|
||||
# Получаем ошибки за последний час
|
||||
recent_errors = rate_limit_monitor.get_recent_errors(60)
|
||||
error_summary = rate_limit_monitor.get_error_summary(60)
|
||||
|
||||
|
||||
if not recent_errors:
|
||||
await message.answer("✅ Ошибок rate limiting за последний час не было.")
|
||||
await message.answer(
|
||||
"✅ Ошибок rate limiting за последний час не было."
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
# Формируем сообщение с ошибками
|
||||
errors_text = f"🚨 <b>Ошибки Rate Limiting (последний час)</b>\n\n"
|
||||
errors_text += f"📊 <b>Сводка ошибок:</b>\n"
|
||||
for error_type, count in error_summary.items():
|
||||
errors_text += f"• {error_type}: {count}\n"
|
||||
errors_text += f"\nВсего ошибок: {len(recent_errors)}\n\n"
|
||||
|
||||
|
||||
# Показываем последние 10 ошибок
|
||||
errors_text += f"🔍 <b>Последние ошибки:</b>\n"
|
||||
for i, error in enumerate(recent_errors[-10:], 1):
|
||||
from datetime import datetime
|
||||
timestamp = datetime.fromtimestamp(error['timestamp']).strftime("%H:%M:%S")
|
||||
|
||||
timestamp = datetime.fromtimestamp(error["timestamp"]).strftime(
|
||||
"%H:%M:%S"
|
||||
)
|
||||
errors_text += f"{i}. {timestamp} - Chat {error['chat_id']} - {error['error_type']}\n"
|
||||
|
||||
|
||||
# Если сообщение слишком длинное, разбиваем на части
|
||||
if len(errors_text) > 4000:
|
||||
# Отправляем сводку
|
||||
@@ -190,32 +203,37 @@ class RateLimitHandlers:
|
||||
for error_type, count in error_summary.items():
|
||||
summary_text += f"• {error_type}: {count}\n"
|
||||
summary_text += f"\nВсего ошибок: {len(recent_errors)}"
|
||||
|
||||
await message.answer(summary_text, parse_mode='HTML')
|
||||
|
||||
|
||||
await message.answer(summary_text, parse_mode="HTML")
|
||||
|
||||
# Отправляем детали отдельным сообщением
|
||||
details_text = f"🔍 <b>Последние ошибки:</b>\n"
|
||||
for i, error in enumerate(recent_errors[-10:], 1):
|
||||
from datetime import datetime
|
||||
timestamp = datetime.fromtimestamp(error['timestamp']).strftime("%H:%M:%S")
|
||||
|
||||
timestamp = datetime.fromtimestamp(error["timestamp"]).strftime(
|
||||
"%H:%M:%S"
|
||||
)
|
||||
details_text += f"{i}. {timestamp} - Chat {error['chat_id']} - {error['error_type']}\n"
|
||||
|
||||
await message.answer(details_text, parse_mode='HTML')
|
||||
|
||||
await message.answer(details_text, parse_mode="HTML")
|
||||
else:
|
||||
await message.answer(errors_text, parse_mode='HTML')
|
||||
|
||||
await message.answer(errors_text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении ошибок rate limiting: {e}")
|
||||
await message.answer("Произошла ошибка при получении информации об ошибках.")
|
||||
await message.answer(
|
||||
"Произошла ошибка при получении информации об ошибках."
|
||||
)
|
||||
|
||||
@track_time("rate_limit_prometheus_handler", "rate_limit_handlers")
|
||||
@track_errors("rate_limit_handlers", "rate_limit_prometheus_handler")
|
||||
async def rate_limit_prometheus_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Показывает Prometheus метрики rate limiting"""
|
||||
try:
|
||||
@@ -223,13 +241,13 @@ class RateLimitHandlers:
|
||||
if not await bot_db.is_admin(message.from_user.id):
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
|
||||
# Обновляем gauge метрики
|
||||
update_rate_limit_gauges()
|
||||
|
||||
|
||||
# Получаем сводку метрик
|
||||
metrics_summary = get_rate_limit_metrics_summary()
|
||||
|
||||
|
||||
# Формируем сообщение с метриками
|
||||
metrics_text = (
|
||||
f"📊 <b>Prometheus метрики Rate Limiting</b>\n\n"
|
||||
@@ -241,30 +259,40 @@ class RateLimitHandlers:
|
||||
f"• rate_limit_avg_wait_time: {metrics_summary['average_wait_time']:.3f}s\n"
|
||||
f"• rate_limit_active_chats: {metrics_summary['active_chats']}\n\n"
|
||||
)
|
||||
|
||||
|
||||
# Добавляем детальные метрики
|
||||
metrics_text += f"🔍 <b>Детальные метрики:</b>\n"
|
||||
metrics_text += f"• Успешных запросов: {metrics_summary['successful_requests']}\n"
|
||||
metrics_text += f"• Неудачных запросов: {metrics_summary['failed_requests']}\n"
|
||||
metrics_text += f"• RetryAfter ошибок: {metrics_summary['retry_after_errors']}\n"
|
||||
metrics_text += (
|
||||
f"• Успешных запросов: {metrics_summary['successful_requests']}\n"
|
||||
)
|
||||
metrics_text += (
|
||||
f"• Неудачных запросов: {metrics_summary['failed_requests']}\n"
|
||||
)
|
||||
metrics_text += (
|
||||
f"• RetryAfter ошибок: {metrics_summary['retry_after_errors']}\n"
|
||||
)
|
||||
metrics_text += f"• Других ошибок: {metrics_summary['other_errors']}\n"
|
||||
metrics_text += f"• Общее время ожидания: {metrics_summary['total_wait_time']:.2f}s\n\n"
|
||||
|
||||
metrics_text += (
|
||||
f"• Общее время ожидания: {metrics_summary['total_wait_time']:.2f}s\n\n"
|
||||
)
|
||||
|
||||
# Добавляем информацию о доступных метриках
|
||||
metrics_text += f"📈 <b>Доступные Prometheus метрики:</b>\n"
|
||||
metrics_text += f"• rate_limit_requests_total - общее количество запросов\n"
|
||||
metrics_text += f"• rate_limit_errors_total - количество ошибок по типам\n"
|
||||
metrics_text += f"• rate_limit_wait_duration_seconds - время ожидания\n"
|
||||
metrics_text += f"• rate_limit_request_interval_seconds - интервалы между запросами\n"
|
||||
metrics_text += (
|
||||
f"• rate_limit_request_interval_seconds - интервалы между запросами\n"
|
||||
)
|
||||
metrics_text += f"• rate_limit_active_chats - количество активных чатов\n"
|
||||
metrics_text += f"• rate_limit_success_rate - процент успеха по чатам\n"
|
||||
metrics_text += f"• rate_limit_requests_per_minute - запросов в минуту\n"
|
||||
metrics_text += f"• rate_limit_total_requests - общее количество запросов\n"
|
||||
metrics_text += f"• rate_limit_total_errors - количество ошибок\n"
|
||||
metrics_text += f"• rate_limit_avg_wait_time - среднее время ожидания\n"
|
||||
|
||||
await message.answer(metrics_text, parse_mode='HTML')
|
||||
|
||||
|
||||
await message.answer(metrics_text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении Prometheus метрик: {e}")
|
||||
await message.answer("Произошла ошибка при получении метрик.")
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from helper_bot.handlers.admin.exceptions import (InvalidInputError,
|
||||
UserAlreadyBannedError)
|
||||
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 (
|
||||
InvalidInputError,
|
||||
UserAlreadyBannedError,
|
||||
)
|
||||
from helper_bot.utils.helper_func import (
|
||||
add_days_to_date,
|
||||
get_banned_users_buttons,
|
||||
get_banned_users_list,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
@@ -13,6 +18,7 @@ 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
|
||||
@@ -21,7 +27,10 @@ class User:
|
||||
|
||||
class BannedUser:
|
||||
"""Модель заблокированного пользователя"""
|
||||
def __init__(self, user_id: int, username: str, reason: str, unban_date: Optional[datetime]):
|
||||
|
||||
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
|
||||
@@ -30,10 +39,10 @@ class BannedUser:
|
||||
|
||||
class AdminService:
|
||||
"""Сервис для административных операций"""
|
||||
|
||||
|
||||
def __init__(self, bot_db):
|
||||
self.bot_db = bot_db
|
||||
|
||||
|
||||
@track_time("get_last_users", "admin_service")
|
||||
@track_errors("admin_service", "get_last_users")
|
||||
async def get_last_users(self) -> List[User]:
|
||||
@@ -41,17 +50,13 @@ class AdminService:
|
||||
try:
|
||||
users_data = await self.bot_db.get_last_users(30)
|
||||
return [
|
||||
User(
|
||||
user_id=user[1],
|
||||
username='Неизвестно',
|
||||
full_name=user[0]
|
||||
)
|
||||
User(user_id=user[1], username="Неизвестно", full_name=user[0])
|
||||
for user in users_data
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении списка последних пользователей: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@track_time("get_banned_users", "admin_service")
|
||||
@track_errors("admin_service", "get_banned_users")
|
||||
async def get_banned_users(self) -> List[BannedUser]:
|
||||
@@ -65,18 +70,22 @@ class AdminService:
|
||||
username = await self.bot_db.get_username(user_id)
|
||||
full_name = await self.bot_db.get_full_name_by_id(user_id)
|
||||
user_name = username or full_name or f"User_{user_id}"
|
||||
|
||||
banned_users.append(BannedUser(
|
||||
user_id=user_id,
|
||||
username=user_name,
|
||||
reason=reason,
|
||||
unban_date=unban_date
|
||||
))
|
||||
|
||||
banned_users.append(
|
||||
BannedUser(
|
||||
user_id=user_id,
|
||||
username=user_name,
|
||||
reason=reason,
|
||||
unban_date=unban_date,
|
||||
)
|
||||
)
|
||||
return banned_users
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при получении списка заблокированных пользователей: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@track_time("get_user_by_username", "admin_service")
|
||||
@track_errors("admin_service", "get_user_by_username")
|
||||
async def get_user_by_username(self, username: str) -> Optional[User]:
|
||||
@@ -85,17 +94,15 @@ class AdminService:
|
||||
user_id = await self.bot_db.get_user_id_by_username(username)
|
||||
if not user_id:
|
||||
return None
|
||||
|
||||
|
||||
full_name = await self.bot_db.get_full_name_by_id(user_id)
|
||||
return User(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
full_name=full_name or 'Неизвестно'
|
||||
user_id=user_id, username=username, full_name=full_name or "Неизвестно"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске пользователя по username {username}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@track_time("get_user_by_id", "admin_service")
|
||||
@track_errors("admin_service", "get_user_by_id")
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
@@ -104,39 +111,50 @@ class AdminService:
|
||||
user_info = await self.bot_db.get_user_by_id(user_id)
|
||||
if not user_info:
|
||||
return None
|
||||
|
||||
|
||||
return User(
|
||||
user_id=user_id,
|
||||
username=user_info.username or 'Неизвестно',
|
||||
full_name=user_info.full_name or 'Неизвестно'
|
||||
username=user_info.username or "Неизвестно",
|
||||
full_name=user_info.full_name or "Неизвестно",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@track_time("ban_user", "admin_service")
|
||||
@track_errors("admin_service", "ban_user")
|
||||
async def ban_user(self, user_id: int, username: str, reason: str, ban_days: Optional[int], ban_author_id: int) -> None:
|
||||
async def ban_user(
|
||||
self,
|
||||
user_id: int,
|
||||
username: str,
|
||||
reason: str,
|
||||
ban_days: Optional[int],
|
||||
ban_author_id: int,
|
||||
) -> None:
|
||||
"""Заблокировать пользователя"""
|
||||
try:
|
||||
# Проверяем, не заблокирован ли уже пользователь
|
||||
if await 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)
|
||||
|
||||
|
||||
# Сохраняем в БД (username больше не передается, так как не используется в новой схеме)
|
||||
await self.bot_db.set_user_blacklist(user_id, None, reason, date_to_unban, ban_author=ban_author_id)
|
||||
|
||||
logger.info(f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней")
|
||||
|
||||
await self.bot_db.set_user_blacklist(
|
||||
user_id, None, reason, date_to_unban, ban_author=ban_author_id
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@track_time("unban_user", "admin_service")
|
||||
@track_errors("admin_service", "unban_user")
|
||||
async def unban_user(self, user_id: int) -> None:
|
||||
@@ -147,7 +165,7 @@ class AdminService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при разблокировке пользователя {user_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@track_time("validate_user_input", "admin_service")
|
||||
@track_errors("admin_service", "validate_user_input")
|
||||
async def validate_user_input(self, input_text: str) -> int:
|
||||
@@ -155,11 +173,13 @@ class AdminService:
|
||||
try:
|
||||
user_id = int(input_text.strip())
|
||||
if user_id <= 0:
|
||||
raise InvalidInputError("ID пользователя должен быть положительным числом")
|
||||
raise InvalidInputError(
|
||||
"ID пользователя должен быть положительным числом"
|
||||
)
|
||||
return user_id
|
||||
except ValueError:
|
||||
raise InvalidInputError("ID пользователя должен быть числом")
|
||||
|
||||
|
||||
@track_time("get_banned_users_for_display", "admin_service")
|
||||
@track_errors("admin_service", "get_banned_users_for_display")
|
||||
async def get_banned_users_for_display(self, page: int = 0) -> tuple[str, list]:
|
||||
@@ -170,5 +190,7 @@ class AdminService:
|
||||
buttons_list = await get_banned_users_buttons(self.bot_db)
|
||||
return message_text, buttons_list
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении данных заблокированных пользователей: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при получении данных заблокированных пользователей: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.handlers.admin.exceptions import AdminError
|
||||
from helper_bot.keyboards.keyboards import get_reply_keyboard_admin
|
||||
from logs.custom_logger import logger
|
||||
@@ -13,33 +14,41 @@ def escape_html(text: str) -> str:
|
||||
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:
|
||||
async def return_to_admin_menu(
|
||||
message: types.Message, state: FSMContext, additional_message: Optional[str] = None
|
||||
) -> None:
|
||||
"""Универсальная функция для возврата в админ-меню"""
|
||||
logger.info(f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}")
|
||||
|
||||
logger.info(
|
||||
f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}"
|
||||
)
|
||||
|
||||
await state.set_data({})
|
||||
await state.set_state("ADMIN")
|
||||
markup = get_reply_keyboard_admin()
|
||||
|
||||
|
||||
if additional_message:
|
||||
logger.info(f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}")
|
||||
logger.info(
|
||||
f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}"
|
||||
)
|
||||
await message.answer(additional_message)
|
||||
|
||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
||||
logger.info(f"return_to_admin_menu: Пользователь {message.from_user.id} успешно возвращен в админ-меню")
|
||||
|
||||
await message.answer("Вернулись в меню", reply_markup=markup)
|
||||
logger.info(
|
||||
f"return_to_admin_menu: Пользователь {message.from_user.id} успешно возвращен в админ-меню"
|
||||
)
|
||||
|
||||
|
||||
async def handle_admin_error(message: types.Message, error: Exception,
|
||||
state: FSMContext, error_context: str = "") -> None:
|
||||
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)
|
||||
|
||||
|
||||
@@ -47,19 +56,23 @@ 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}")
|
||||
|
||||
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}")
|
||||
|
||||
return (
|
||||
f"<b>Необходимо подтверждение:</b>\n"
|
||||
f"<b>Пользователь:</b> {user_id}\n"
|
||||
f"<b>Причина бана:</b> {safe_reason}\n"
|
||||
f"<b>Срок бана:</b> {ban_text}"
|
||||
)
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
from .callback_handlers import callback_router
|
||||
from .constants import (CALLBACK_BAN, CALLBACK_DECLINE, CALLBACK_PAGE,
|
||||
CALLBACK_PUBLISH, CALLBACK_RETURN, CALLBACK_UNLOCK)
|
||||
from .exceptions import (BanError, PostNotFoundError, PublishError,
|
||||
UserBlockedBotError, UserNotFoundError)
|
||||
from .constants import (
|
||||
CALLBACK_BAN,
|
||||
CALLBACK_DECLINE,
|
||||
CALLBACK_PAGE,
|
||||
CALLBACK_PUBLISH,
|
||||
CALLBACK_RETURN,
|
||||
CALLBACK_UNLOCK,
|
||||
)
|
||||
from .exceptions import (
|
||||
BanError,
|
||||
PostNotFoundError,
|
||||
PublishError,
|
||||
UserBlockedBotError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
from .services import BanService, PostPublishService
|
||||
|
||||
__all__ = [
|
||||
'callback_router',
|
||||
'PostPublishService',
|
||||
'BanService',
|
||||
'UserBlockedBotError',
|
||||
'PostNotFoundError',
|
||||
'UserNotFoundError',
|
||||
'PublishError',
|
||||
'BanError',
|
||||
'CALLBACK_PUBLISH',
|
||||
'CALLBACK_DECLINE',
|
||||
'CALLBACK_BAN',
|
||||
'CALLBACK_UNLOCK',
|
||||
'CALLBACK_RETURN',
|
||||
'CALLBACK_PAGE'
|
||||
"callback_router",
|
||||
"PostPublishService",
|
||||
"BanService",
|
||||
"UserBlockedBotError",
|
||||
"PostNotFoundError",
|
||||
"UserNotFoundError",
|
||||
"PublishError",
|
||||
"BanError",
|
||||
"CALLBACK_PUBLISH",
|
||||
"CALLBACK_DECLINE",
|
||||
"CALLBACK_BAN",
|
||||
"CALLBACK_UNLOCK",
|
||||
"CALLBACK_RETURN",
|
||||
"CALLBACK_PAGE",
|
||||
]
|
||||
|
||||
@@ -7,28 +7,49 @@ from aiogram import F, Router
|
||||
from aiogram.filters import MagicData
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from helper_bot.handlers.admin.utils import format_user_info
|
||||
from helper_bot.handlers.voice.constants import CALLBACK_DELETE, CALLBACK_SAVE
|
||||
from helper_bot.handlers.voice.services import AudioFileService
|
||||
from helper_bot.keyboards.keyboards import (create_keyboard_for_ban_reason,
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard_admin)
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
create_keyboard_for_ban_reason,
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard_admin,
|
||||
)
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from helper_bot.utils.helper_func import (get_banned_users_buttons,
|
||||
get_banned_users_list)
|
||||
from helper_bot.utils.helper_func import get_banned_users_buttons, get_banned_users_list
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (db_query_time, track_errors,
|
||||
track_file_operations, track_time)
|
||||
from helper_bot.utils.metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_file_operations,
|
||||
track_time,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
from .constants import (CALLBACK_BAN, CALLBACK_DECLINE, CALLBACK_PAGE,
|
||||
CALLBACK_PUBLISH, CALLBACK_RETURN, CALLBACK_UNLOCK,
|
||||
ERROR_BOT_BLOCKED, MESSAGE_DECLINED, MESSAGE_ERROR,
|
||||
MESSAGE_PUBLISHED, MESSAGE_USER_BANNED,
|
||||
MESSAGE_USER_UNLOCKED)
|
||||
from .constants import (
|
||||
CALLBACK_BAN,
|
||||
CALLBACK_DECLINE,
|
||||
CALLBACK_PAGE,
|
||||
CALLBACK_PUBLISH,
|
||||
CALLBACK_RETURN,
|
||||
CALLBACK_UNLOCK,
|
||||
ERROR_BOT_BLOCKED,
|
||||
MESSAGE_DECLINED,
|
||||
MESSAGE_ERROR,
|
||||
MESSAGE_PUBLISHED,
|
||||
MESSAGE_USER_BANNED,
|
||||
MESSAGE_USER_UNLOCKED,
|
||||
)
|
||||
from .dependency_factory import get_ban_service, get_post_publish_service
|
||||
from .exceptions import (BanError, PostNotFoundError, PublishError,
|
||||
UserBlockedBotError, UserNotFoundError)
|
||||
from .exceptions import (
|
||||
BanError,
|
||||
PostNotFoundError,
|
||||
PublishError,
|
||||
UserBlockedBotError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
|
||||
callback_router = Router()
|
||||
|
||||
@@ -36,65 +57,61 @@ callback_router = Router()
|
||||
@callback_router.callback_query(F.data == CALLBACK_PUBLISH)
|
||||
@track_time("post_for_group", "callback_handlers")
|
||||
@track_errors("callback_handlers", "post_for_group")
|
||||
async def post_for_group(
|
||||
call: CallbackQuery,
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
async def post_for_group(call: CallbackQuery, settings: MagicData("settings")):
|
||||
publish_service = get_post_publish_service()
|
||||
# TODO: переделать на MagicData
|
||||
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})"
|
||||
)
|
||||
|
||||
try:
|
||||
await publish_service.publish_post(call)
|
||||
await call.answer(text=MESSAGE_PUBLISHED, cache_time=3)
|
||||
except UserBlockedBotError:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except (PostNotFoundError, PublishError) as e:
|
||||
logger.error(f'Ошибка при публикации поста: {str(e)}')
|
||||
logger.error(f"Ошибка при публикации поста: {str(e)}")
|
||||
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:
|
||||
important_logs = settings['Telegram']['important_logs']
|
||||
important_logs = settings["Telegram"]["important_logs"]
|
||||
await call.bot.send_message(
|
||||
chat_id=important_logs,
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}",
|
||||
)
|
||||
logger.error(f'Неожиданная ошибка при публикации поста: {str(e)}')
|
||||
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)
|
||||
@track_time("decline_post_for_group", "callback_handlers")
|
||||
@track_errors("callback_handlers", "decline_post_for_group")
|
||||
async def decline_post_for_group(
|
||||
call: CallbackQuery,
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
async def decline_post_for_group(call: CallbackQuery, settings: MagicData("settings")):
|
||||
publish_service = get_post_publish_service()
|
||||
# TODO: переделать на MagicData
|
||||
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:
|
||||
await publish_service.decline_post(call)
|
||||
await call.answer(text=MESSAGE_DECLINED, cache_time=3)
|
||||
except UserBlockedBotError:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except (PostNotFoundError, PublishError) as e:
|
||||
logger.error(f'Ошибка при отклонении поста: {str(e)}')
|
||||
logger.error(f"Ошибка при отклонении поста: {str(e)}")
|
||||
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:
|
||||
important_logs = settings['Telegram']['important_logs']
|
||||
important_logs = settings["Telegram"]["important_logs"]
|
||||
await call.bot.send_message(
|
||||
chat_id=important_logs,
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}",
|
||||
)
|
||||
logger.error(f'Неожиданная ошибка при отклонении поста: {str(e)}')
|
||||
logger.error(f"Неожиданная ошибка при отклонении поста: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
|
||||
|
||||
@@ -110,65 +127,75 @@ async def ban_user_from_post(call: CallbackQuery, **kwargs):
|
||||
except UserBlockedBotError:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except (UserNotFoundError, BanError) as e:
|
||||
logger.error(f'Ошибка при блокировке пользователя: {str(e)}')
|
||||
logger.error(f"Ошибка при блокировке пользователя: {str(e)}")
|
||||
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)}')
|
||||
logger.error(f"Неожиданная ошибка при блокировке пользователя: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_BAN))
|
||||
@track_time("process_ban_user", "callback_handlers")
|
||||
@track_errors("callback_handlers", "process_ban_user")
|
||||
async def process_ban_user(call: CallbackQuery, state: FSMContext, bot_db: MagicData("bot_db"), **kwargs):
|
||||
async def process_ban_user(
|
||||
call: CallbackQuery, state: FSMContext, bot_db: MagicData("bot_db"), **kwargs
|
||||
):
|
||||
ban_service = get_ban_service()
|
||||
# TODO: переделать на MagicData
|
||||
user_id = call.data[4:]
|
||||
logger.info(f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}")
|
||||
|
||||
logger.info(
|
||||
f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}"
|
||||
)
|
||||
|
||||
# Проверяем, что user_id является валидным числом
|
||||
try:
|
||||
user_id_int = int(user_id)
|
||||
except ValueError:
|
||||
logger.error(f"Некорректный user_id в callback: {user_id}")
|
||||
await call.answer(text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3)
|
||||
await call.answer(
|
||||
text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
# Получаем username пользователя
|
||||
username = await ban_service.ban_user(str(user_id_int), "")
|
||||
if not username:
|
||||
raise UserNotFoundError(f"Пользователь с ID {user_id_int} не найден в базе")
|
||||
|
||||
|
||||
# Получаем full_name пользователя из базы данных
|
||||
full_name = await bot_db.get_full_name_by_id(user_id_int)
|
||||
if not full_name:
|
||||
full_name = 'Неизвестно'
|
||||
|
||||
full_name = "Неизвестно"
|
||||
|
||||
# Сохраняем данные в формате, совместимом с admin_handlers
|
||||
await state.update_data(
|
||||
target_user_id=user_id_int,
|
||||
target_username=username,
|
||||
target_full_name=full_name
|
||||
target_full_name=full_name,
|
||||
)
|
||||
|
||||
|
||||
# Используем единый формат отображения информации о пользователе
|
||||
user_info = format_user_info(user_id_int, username, full_name)
|
||||
markup = create_keyboard_for_ban_reason()
|
||||
|
||||
|
||||
await call.message.answer(
|
||||
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
await state.set_state("AWAIT_BAN_DETAILS")
|
||||
logger.info(
|
||||
f"process_ban_user: Состояние изменено на AWAIT_BAN_DETAILS для пользователя {user_id_int}"
|
||||
)
|
||||
await state.set_state('AWAIT_BAN_DETAILS')
|
||||
logger.info(f"process_ban_user: Состояние изменено на AWAIT_BAN_DETAILS для пользователя {user_id_int}")
|
||||
except UserNotFoundError:
|
||||
markup = get_reply_keyboard_admin()
|
||||
await call.message.answer(text='Пользователь с таким ID не найден в базе', reply_markup=markup)
|
||||
await state.set_state('ADMIN')
|
||||
await call.message.answer(
|
||||
text="Пользователь с таким ID не найден в базе", reply_markup=markup
|
||||
)
|
||||
await state.set_state("ADMIN")
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_UNLOCK))
|
||||
@@ -178,22 +205,26 @@ async def process_unlock_user(call: CallbackQuery, **kwargs):
|
||||
ban_service = get_ban_service()
|
||||
# TODO: переделать на MagicData
|
||||
user_id = call.data[7:]
|
||||
|
||||
|
||||
# Проверяем, что user_id является валидным числом
|
||||
try:
|
||||
user_id_int = int(user_id)
|
||||
except ValueError:
|
||||
logger.error(f"Некорректный user_id в callback: {user_id}")
|
||||
await call.answer(text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3)
|
||||
await call.answer(
|
||||
text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
username = await ban_service.unlock_user(str(user_id_int))
|
||||
await call.answer(f'{MESSAGE_USER_UNLOCKED} {username}', show_alert=True)
|
||||
await call.answer(f"{MESSAGE_USER_UNLOCKED} {username}", show_alert=True)
|
||||
except UserNotFoundError:
|
||||
await call.answer(text='Пользователь не найден в базе', show_alert=True, cache_time=3)
|
||||
await call.answer(
|
||||
text="Пользователь не найден в базе", show_alert=True, cache_time=3
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка при разблокировке пользователя: {str(e)}')
|
||||
logger.error(f"Ошибка при разблокировке пользователя: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
|
||||
|
||||
@@ -204,48 +235,52 @@ async def return_to_main_menu(call: CallbackQuery, **kwargs):
|
||||
await call.message.delete()
|
||||
logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}")
|
||||
markup = get_reply_keyboard_admin()
|
||||
await call.message.answer("Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup)
|
||||
await call.message.answer(
|
||||
"Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup
|
||||
)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_PAGE))
|
||||
@track_time("change_page", "callback_handlers")
|
||||
@track_errors("callback_handlers", "change_page")
|
||||
async def change_page(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
**kwargs
|
||||
):
|
||||
async def change_page(call: CallbackQuery, bot_db: MagicData("bot_db"), **kwargs):
|
||||
try:
|
||||
page_number = int(call.data[5:])
|
||||
except ValueError:
|
||||
logger.error(f"Некорректный номер страницы в callback: {call.data}")
|
||||
await call.answer(text="Ошибка: некорректный номер страницы", show_alert=True, cache_time=3)
|
||||
await call.answer(
|
||||
text="Ошибка: некорректный номер страницы", show_alert=True, cache_time=3
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
logger.info(f"Переход на страницу {page_number}")
|
||||
|
||||
if call.message.text == 'Список пользователей которые последними обращались к боту':
|
||||
|
||||
if call.message.text == "Список пользователей которые последними обращались к боту":
|
||||
list_users = await bot_db.get_last_users(30)
|
||||
keyboard = create_keyboard_with_pagination(page_number, len(list_users), list_users, 'ban')
|
||||
keyboard = create_keyboard_with_pagination(
|
||||
page_number, len(list_users), list_users, "ban"
|
||||
)
|
||||
await call.bot.edit_message_reply_markup(
|
||||
chat_id=call.message.chat.id,
|
||||
chat_id=call.message.chat.id,
|
||||
message_id=call.message.message_id,
|
||||
reply_markup=keyboard
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
else:
|
||||
message_user = await get_banned_users_list(int(page_number) * 7 - 7, bot_db)
|
||||
await call.bot.edit_message_text(
|
||||
chat_id=call.message.chat.id,
|
||||
chat_id=call.message.chat.id,
|
||||
message_id=call.message.message_id,
|
||||
text=message_user
|
||||
text=message_user,
|
||||
)
|
||||
|
||||
|
||||
buttons = await get_banned_users_buttons(bot_db)
|
||||
keyboard = create_keyboard_with_pagination(page_number, len(buttons), buttons, 'unlock')
|
||||
keyboard = create_keyboard_with_pagination(
|
||||
page_number, len(buttons), buttons, "unlock"
|
||||
)
|
||||
await call.bot.edit_message_reply_markup(
|
||||
chat_id=call.message.chat.id,
|
||||
chat_id=call.message.chat.id,
|
||||
message_id=call.message.message_id,
|
||||
reply_markup=keyboard
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
|
||||
|
||||
@@ -255,73 +290,81 @@ async def change_page(
|
||||
@track_file_operations("voice")
|
||||
@db_query_time("save_voice_message", "audio_moderate", "mixed")
|
||||
async def save_voice_message(
|
||||
call: CallbackQuery,
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
**kwargs
|
||||
):
|
||||
**kwargs,
|
||||
):
|
||||
try:
|
||||
logger.info(f"Начинаем сохранение голосового сообщения. Message ID: {call.message.message_id}")
|
||||
|
||||
logger.info(
|
||||
f"Начинаем сохранение голосового сообщения. Message ID: {call.message.message_id}"
|
||||
)
|
||||
|
||||
# Создаем сервис для работы с аудио файлами
|
||||
audio_service = AudioFileService(bot_db)
|
||||
|
||||
|
||||
# Получаем ID пользователя из базы
|
||||
user_id = await bot_db.get_user_id_by_message_id_for_voice_bot(call.message.message_id)
|
||||
user_id = await bot_db.get_user_id_by_message_id_for_voice_bot(
|
||||
call.message.message_id
|
||||
)
|
||||
logger.info(f"Получен user_id: {user_id}")
|
||||
|
||||
|
||||
# Генерируем имя файла
|
||||
file_name = await audio_service.generate_file_name(user_id)
|
||||
logger.info(f"Сгенерировано имя файла: {file_name}")
|
||||
|
||||
|
||||
# Собираем инфо о сообщении
|
||||
time_UTC = int(time.time())
|
||||
date_added = datetime.fromtimestamp(time_UTC)
|
||||
|
||||
|
||||
# Получаем file_id из voice сообщения
|
||||
file_id = call.message.voice.file_id if call.message.voice else ""
|
||||
logger.info(f"Получен file_id: {file_id}")
|
||||
|
||||
|
||||
# ВАЖНО: Сначала скачиваем и сохраняем файл на диск
|
||||
logger.info("Начинаем скачивание и сохранение файла на диск...")
|
||||
await audio_service.download_and_save_audio(call.bot, call.message, file_name)
|
||||
logger.info("Файл успешно скачан и сохранен на диск")
|
||||
|
||||
|
||||
# Только после успешного сохранения файла - сохраняем в базу данных
|
||||
logger.info("Начинаем сохранение информации в базу данных...")
|
||||
await audio_service.save_audio_file(file_name, user_id, date_added, file_id)
|
||||
logger.info("Информация успешно сохранена в базу данных")
|
||||
|
||||
|
||||
# Удаляем сообщение из предложки
|
||||
logger.info("Удаляем сообщение из предложки...")
|
||||
await call.bot.delete_message(
|
||||
chat_id=settings['Telegram']['group_for_posts'],
|
||||
message_id=call.message.message_id
|
||||
chat_id=settings["Telegram"]["group_for_posts"],
|
||||
message_id=call.message.message_id,
|
||||
)
|
||||
logger.info("Сообщение удалено из предложки")
|
||||
|
||||
|
||||
# Удаляем запись из таблицы audio_moderate
|
||||
logger.info("Удаляем запись из таблицы audio_moderate...")
|
||||
await bot_db.delete_audio_moderate_record(call.message.message_id)
|
||||
logger.info("Запись удалена из таблицы audio_moderate")
|
||||
|
||||
await call.answer(text='Сохранено!', cache_time=3)
|
||||
|
||||
await call.answer(text="Сохранено!", cache_time=3)
|
||||
logger.info(f"Голосовое сообщение успешно сохранено: {file_name}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении голосового сообщения: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
|
||||
|
||||
# Дополнительная информация для диагностики
|
||||
try:
|
||||
if 'call' in locals() and call.message:
|
||||
if "call" in locals() and call.message:
|
||||
logger.error(f"Message ID: {call.message.message_id}")
|
||||
logger.error(f"User ID: {user_id if 'user_id' in locals() else 'не определен'}")
|
||||
logger.error(f"File name: {file_name if 'file_name' in locals() else 'не определен'}")
|
||||
logger.error(
|
||||
f"User ID: {user_id if 'user_id' in locals() else 'не определен'}"
|
||||
)
|
||||
logger.error(
|
||||
f"File name: {file_name if 'file_name' in locals() else 'не определен'}"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
await call.answer(text='Ошибка при сохранении!', cache_time=3)
|
||||
|
||||
await call.answer(text="Ошибка при сохранении!", cache_time=3)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_DELETE)
|
||||
@@ -329,23 +372,23 @@ async def save_voice_message(
|
||||
@track_errors("callback_handlers", "delete_voice_message")
|
||||
@db_query_time("delete_voice_message", "audio_moderate", "delete")
|
||||
async def delete_voice_message(
|
||||
call: CallbackQuery,
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
**kwargs
|
||||
):
|
||||
**kwargs,
|
||||
):
|
||||
try:
|
||||
# Удаляем сообщение из предложки
|
||||
await call.bot.delete_message(
|
||||
chat_id=settings['Telegram']['group_for_posts'],
|
||||
message_id=call.message.message_id
|
||||
chat_id=settings["Telegram"]["group_for_posts"],
|
||||
message_id=call.message.message_id,
|
||||
)
|
||||
|
||||
|
||||
# Удаляем запись из таблицы audio_moderate
|
||||
await bot_db.delete_audio_moderate_record(call.message.message_id)
|
||||
|
||||
await call.answer(text='Удалено!', cache_time=3)
|
||||
|
||||
|
||||
await call.answer(text="Удалено!", cache_time=3)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении голосового сообщения: {e}")
|
||||
await call.answer(text='Ошибка при удалении!', cache_time=3)
|
||||
await call.answer(text="Ошибка при удалении!", cache_time=3)
|
||||
|
||||
@@ -33,9 +33,9 @@ ERROR_BOT_BLOCKED = "Forbidden: bot was blocked by the user"
|
||||
# Callback to command mapping for metrics
|
||||
CALLBACK_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"publish": "publish",
|
||||
"decline": "decline",
|
||||
"decline": "decline",
|
||||
"ban": "ban",
|
||||
"unlock": "unlock",
|
||||
"return": "return",
|
||||
"page": "page"
|
||||
"page": "page",
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 BanService, PostPublishService
|
||||
@@ -21,7 +22,7 @@ def get_post_publish_service() -> PostPublishService:
|
||||
def get_ban_service() -> BanService:
|
||||
"""Фабрика для BanService"""
|
||||
bdf = get_global_instance()
|
||||
|
||||
|
||||
db = bdf.get_db()
|
||||
settings = bdf.settings
|
||||
return BanService(None, db, settings)
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
class UserBlockedBotError(Exception):
|
||||
"""Исключение, возникающее когда пользователь заблокировал бота"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PostNotFoundError(Exception):
|
||||
"""Исключение, возникающее когда пост не найден в базе данных"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFoundError(Exception):
|
||||
"""Исключение, возникающее когда пользователь не найден в базе данных"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PublishError(Exception):
|
||||
"""Общее исключение для ошибок публикации"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BanError(Exception):
|
||||
"""Исключение для ошибок бана/разбана пользователей"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -4,28 +4,49 @@ from typing import Any, Dict
|
||||
|
||||
from aiogram import Bot, types
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from helper_bot.keyboards.keyboards import create_keyboard_for_ban_reason
|
||||
from helper_bot.utils.helper_func import (delete_user_blacklist,
|
||||
get_text_message, send_audio_message,
|
||||
send_media_group_to_channel,
|
||||
send_photo_message,
|
||||
send_text_message,
|
||||
send_video_message,
|
||||
send_video_note_message,
|
||||
send_voice_message)
|
||||
from helper_bot.utils.helper_func import (
|
||||
delete_user_blacklist,
|
||||
get_text_message,
|
||||
send_audio_message,
|
||||
send_media_group_to_channel,
|
||||
send_photo_message,
|
||||
send_text_message,
|
||||
send_video_message,
|
||||
send_video_note_message,
|
||||
send_voice_message,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (db_query_time, track_errors,
|
||||
track_media_processing, track_time)
|
||||
from helper_bot.utils.metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_media_processing,
|
||||
track_time,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
from .constants import (CONTENT_TYPE_AUDIO, CONTENT_TYPE_MEDIA_GROUP,
|
||||
CONTENT_TYPE_PHOTO, CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE,
|
||||
CONTENT_TYPE_VOICE, ERROR_BOT_BLOCKED,
|
||||
MESSAGE_POST_DECLINED, MESSAGE_POST_PUBLISHED,
|
||||
MESSAGE_USER_BANNED_SPAM)
|
||||
from .exceptions import (BanError, PostNotFoundError, PublishError,
|
||||
UserBlockedBotError, UserNotFoundError)
|
||||
from .constants import (
|
||||
CONTENT_TYPE_AUDIO,
|
||||
CONTENT_TYPE_MEDIA_GROUP,
|
||||
CONTENT_TYPE_PHOTO,
|
||||
CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_VIDEO,
|
||||
CONTENT_TYPE_VIDEO_NOTE,
|
||||
CONTENT_TYPE_VOICE,
|
||||
ERROR_BOT_BLOCKED,
|
||||
MESSAGE_POST_DECLINED,
|
||||
MESSAGE_POST_PUBLISHED,
|
||||
MESSAGE_USER_BANNED_SPAM,
|
||||
)
|
||||
from .exceptions import (
|
||||
BanError,
|
||||
PostNotFoundError,
|
||||
PublishError,
|
||||
UserBlockedBotError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
|
||||
|
||||
class PostPublishService:
|
||||
@@ -35,10 +56,10 @@ class PostPublishService:
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
self.s3_storage = s3_storage
|
||||
self.group_for_posts = settings['Telegram']['group_for_posts']
|
||||
self.main_public = settings['Telegram']['main_public']
|
||||
self.important_logs = settings['Telegram']['important_logs']
|
||||
|
||||
self.group_for_posts = settings["Telegram"]["group_for_posts"]
|
||||
self.main_public = settings["Telegram"]["main_public"]
|
||||
self.important_logs = settings["Telegram"]["important_logs"]
|
||||
|
||||
def _get_bot(self, message) -> Bot:
|
||||
"""Получает бота из контекста сообщения или использует переданного"""
|
||||
if self.bot:
|
||||
@@ -53,14 +74,14 @@ class PostPublishService:
|
||||
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
await self._publish_media_group(call)
|
||||
return
|
||||
|
||||
|
||||
# Проверяем, является ли сообщение частью медиагруппы (для обратной совместимости)
|
||||
if call.message.media_group_id:
|
||||
await self._publish_media_group(call)
|
||||
return
|
||||
|
||||
|
||||
content_type = call.message.content_type
|
||||
|
||||
|
||||
if content_type == CONTENT_TYPE_TEXT:
|
||||
await self._publish_text_post(call)
|
||||
elif content_type == CONTENT_TYPE_PHOTO:
|
||||
@@ -82,34 +103,50 @@ class PostPublishService:
|
||||
"""Публикация текстового поста"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
# Получаем сырой текст и is_anonymous из базы
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_message_id(call.message.message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
|
||||
# Получаем данные автора
|
||||
user = await self.db.get_user_by_id(author_id)
|
||||
if not user:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
|
||||
# Формируем финальный текст с учетом is_anonymous
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
|
||||
sent_message = await send_text_message(self.main_public, call.message, formatted_text)
|
||||
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
sent_message = await send_text_message(
|
||||
self.main_public, call.message, formatted_text
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Текст сообщение опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Текст сообщение опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_photo_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_photo_post")
|
||||
@@ -117,37 +154,58 @@ class PostPublishService:
|
||||
"""Публикация поста с фото"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
# Получаем сырой текст и is_anonymous из базы
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_message_id(call.message.message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
|
||||
# Получаем данные автора
|
||||
user = await self.db.get_user_by_id(author_id)
|
||||
if not user:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
|
||||
# Формируем финальный текст с учетом is_anonymous
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
|
||||
sent_message = await send_photo_message(self.main_public, call.message, call.message.photo[-1].file_id, formatted_text)
|
||||
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
sent_message = await send_photo_message(
|
||||
self.main_public,
|
||||
call.message,
|
||||
call.message.photo[-1].file_id,
|
||||
formatted_text,
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с фото опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с фото опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_video_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_video_post")
|
||||
@@ -155,37 +213,55 @@ class PostPublishService:
|
||||
"""Публикация поста с видео"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
# Получаем сырой текст и is_anonymous из базы
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_message_id(call.message.message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
|
||||
# Получаем данные автора
|
||||
user = await self.db.get_user_by_id(author_id)
|
||||
if not user:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
|
||||
# Формируем финальный текст с учетом is_anonymous
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
|
||||
sent_message = await send_video_message(self.main_public, call.message, call.message.video.file_id, formatted_text)
|
||||
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
sent_message = await send_video_message(
|
||||
self.main_public, call.message, call.message.video.file_id, formatted_text
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с видео опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с видео опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_video_note_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_video_note_post")
|
||||
@@ -193,24 +269,36 @@ class PostPublishService:
|
||||
"""Публикация поста с кружком"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
|
||||
sent_message = await send_video_note_message(self.main_public, call.message, call.message.video_note.file_id)
|
||||
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
sent_message = await send_video_note_message(
|
||||
self.main_public, call.message, call.message.video_note.file_id
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с кружком опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с кружком опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_audio_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_audio_post")
|
||||
@@ -218,37 +306,55 @@ class PostPublishService:
|
||||
"""Публикация поста с аудио"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
# Получаем сырой текст и is_anonymous из базы
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_message_id(call.message.message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
|
||||
# Получаем данные автора
|
||||
user = await self.db.get_user_by_id(author_id)
|
||||
if not user:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
|
||||
# Формируем финальный текст с учетом is_anonymous
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
|
||||
sent_message = await send_audio_message(self.main_public, call.message, call.message.audio.file_id, formatted_text)
|
||||
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
sent_message = await send_audio_message(
|
||||
self.main_public, call.message, call.message.audio.file_id, formatted_text
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с аудио опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с аудио опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_voice_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_voice_post")
|
||||
@@ -256,24 +362,36 @@ class PostPublishService:
|
||||
"""Публикация поста с войсом"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
|
||||
sent_message = await send_voice_message(self.main_public, call.message, call.message.voice.file_id)
|
||||
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
sent_message = await send_voice_message(
|
||||
self.main_public, call.message, call.message.voice.file_id
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с войсом опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с войсом опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_media_group", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_media_group")
|
||||
@@ -282,91 +400,130 @@ class PostPublishService:
|
||||
"""Публикация медиагруппы"""
|
||||
try:
|
||||
helper_message_id = call.message.message_id
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
if not media_group_message_ids:
|
||||
logger.error(f"_publish_media_group: Не найдены message_id медиагруппы для helper_message_id={helper_message_id}")
|
||||
logger.error(
|
||||
f"_publish_media_group: Не найдены message_id медиагруппы для helper_message_id={helper_message_id}"
|
||||
)
|
||||
raise PublishError("Не найдены message_id медиагруппы в базе данных")
|
||||
|
||||
post_content = await self.db.get_post_content_by_helper_id(helper_message_id)
|
||||
|
||||
post_content = await self.db.get_post_content_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
if not post_content:
|
||||
logger.error(f"_publish_media_group: Контент медиагруппы не найден в базе данных для helper_message_id={helper_message_id}")
|
||||
logger.error(
|
||||
f"_publish_media_group: Контент медиагруппы не найден в базе данных для helper_message_id={helper_message_id}"
|
||||
)
|
||||
raise PublishError("Контент медиагруппы не найден в базе данных")
|
||||
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_helper_id(helper_message_id)
|
||||
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(helper_message_id)
|
||||
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(
|
||||
helper_message_id
|
||||
)
|
||||
if not author_id:
|
||||
logger.error(f"_publish_media_group: Автор не найден для медиагруппы helper_message_id={helper_message_id}")
|
||||
raise PostNotFoundError(f"Автор не найден для медиагруппы {helper_message_id}")
|
||||
|
||||
logger.error(
|
||||
f"_publish_media_group: Автор не найден для медиагруппы helper_message_id={helper_message_id}"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Автор не найден для медиагруппы {helper_message_id}"
|
||||
)
|
||||
|
||||
user = await self.db.get_user_by_id(author_id)
|
||||
if not user:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
|
||||
raise PostNotFoundError(
|
||||
f"Пользователь {author_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
try:
|
||||
await self._get_bot(call.message).delete_messages(
|
||||
chat_id=self.group_for_posts,
|
||||
message_ids=media_group_message_ids
|
||||
chat_id=self.group_for_posts, message_ids=media_group_message_ids
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"_publish_media_group: Ошибка при удалении медиагруппы из чата модерации: {e}")
|
||||
|
||||
logger.warning(
|
||||
f"_publish_media_group: Ошибка при удалении медиагруппы из чата модерации: {e}"
|
||||
)
|
||||
|
||||
sent_messages = await send_media_group_to_channel(
|
||||
bot=self._get_bot(call.message),
|
||||
chat_id=self.main_public,
|
||||
post_content=post_content,
|
||||
bot=self._get_bot(call.message),
|
||||
chat_id=self.main_public,
|
||||
post_content=post_content,
|
||||
post_text=formatted_text,
|
||||
s3_storage=self.s3_storage
|
||||
s3_storage=self.s3_storage,
|
||||
)
|
||||
|
||||
|
||||
if len(sent_messages) == len(media_group_message_ids):
|
||||
for i, original_message_id in enumerate(media_group_message_ids):
|
||||
published_message_id = sent_messages[i].message_id
|
||||
try:
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=original_message_id,
|
||||
published_message_id=published_message_id
|
||||
published_message_id=published_message_id,
|
||||
)
|
||||
await self._save_published_post_content(
|
||||
sent_messages[i], published_message_id, original_message_id
|
||||
)
|
||||
await self._save_published_post_content(sent_messages[i], published_message_id, original_message_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"_publish_media_group: Ошибка при сохранении published_message_id для {original_message_id}: {e}")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Ошибка при сохранении published_message_id для {original_message_id}: {e}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"_publish_media_group: Количество опубликованных сообщений ({len(sent_messages)}) не совпадает с количеством оригинальных ({len(media_group_message_ids)})")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Количество опубликованных сообщений ({len(sent_messages)}) не совпадает с количеством оригинальных ({len(media_group_message_ids)})"
|
||||
)
|
||||
|
||||
await self.db.update_status_for_media_group_by_helper_id(
|
||||
helper_message_id, "approved"
|
||||
)
|
||||
|
||||
await self.db.update_status_for_media_group_by_helper_id(helper_message_id, "approved")
|
||||
|
||||
# Удаляем helper сообщение - это критично, делаем это всегда
|
||||
try:
|
||||
await self._get_bot(call.message).delete_message(
|
||||
chat_id=self.group_for_posts,
|
||||
message_id=helper_message_id
|
||||
chat_id=self.group_for_posts, message_id=helper_message_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"_publish_media_group: Ошибка при удалении helper сообщения: {e}")
|
||||
|
||||
logger.warning(
|
||||
f"_publish_media_group: Ошибка при удалении helper сообщения: {e}"
|
||||
)
|
||||
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
logger.warning(f"_publish_media_group: Пользователь {author_id} заблокировал бота")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Пользователь {author_id} заблокировал бота"
|
||||
)
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"_publish_media_group: Ошибка при отправке уведомления автору: {e}")
|
||||
|
||||
logger.error(
|
||||
f"_publish_media_group: Ошибка при отправке уведомления автору: {e}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"_publish_media_group: Ошибка при публикации медиагруппы: {e}")
|
||||
logger.error(
|
||||
f"_publish_media_group: Ошибка при публикации медиагруппы: {e}"
|
||||
)
|
||||
# Пытаемся удалить helper сообщение даже при ошибке
|
||||
try:
|
||||
await self._get_bot(call.message).delete_message(
|
||||
chat_id=self.group_for_posts,
|
||||
message_id=call.message.message_id
|
||||
chat_id=self.group_for_posts, message_id=call.message.message_id
|
||||
)
|
||||
except Exception as delete_error:
|
||||
logger.warning(f"_publish_media_group: Не удалось удалить helper сообщение при ошибке: {delete_error}")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Не удалось удалить helper сообщение при ошибке: {delete_error}"
|
||||
)
|
||||
raise PublishError(f"Не удалось опубликовать медиагруппу: {str(e)}")
|
||||
|
||||
@track_time("decline_post", "post_publish_service")
|
||||
@@ -377,15 +534,25 @@ class PostPublishService:
|
||||
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
await self._decline_media_group(call)
|
||||
return
|
||||
|
||||
|
||||
content_type = call.message.content_type
|
||||
|
||||
if content_type in [CONTENT_TYPE_TEXT, CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO,
|
||||
CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||
|
||||
if content_type in [
|
||||
CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_PHOTO,
|
||||
CONTENT_TYPE_AUDIO,
|
||||
CONTENT_TYPE_VOICE,
|
||||
CONTENT_TYPE_VIDEO,
|
||||
CONTENT_TYPE_VIDEO_NOTE,
|
||||
]:
|
||||
await self._decline_single_post(call)
|
||||
else:
|
||||
logger.error(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||
raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||
logger.error(
|
||||
f"Неподдерживаемый тип контента для отклонения: {content_type}"
|
||||
)
|
||||
raise PublishError(
|
||||
f"Неподдерживаемый тип контента для отклонения: {content_type}"
|
||||
)
|
||||
|
||||
@track_time("_decline_single_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_decline_single_post")
|
||||
@@ -393,13 +560,21 @@ class PostPublishService:
|
||||
"""Отклонение одиночного поста"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "declined")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "declined"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'declined'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'declined'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
await self._get_bot(call.message).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:
|
||||
@@ -408,7 +583,9 @@ class PostPublishService:
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"Ошибка при отправке уведомления автору {author_id}: {e}")
|
||||
raise
|
||||
logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
|
||||
logger.info(
|
||||
f"Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id})."
|
||||
)
|
||||
|
||||
@track_time("_decline_media_group", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_decline_media_group")
|
||||
@@ -416,10 +593,14 @@ class PostPublishService:
|
||||
async def _decline_media_group(self, call: CallbackQuery) -> None:
|
||||
"""Отклонение медиагруппы"""
|
||||
helper_message_id = call.message.message_id
|
||||
|
||||
await self.db.update_status_for_media_group_by_helper_id(helper_message_id, "declined")
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
|
||||
await self.db.update_status_for_media_group_by_helper_id(
|
||||
helper_message_id, "declined"
|
||||
)
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
|
||||
message_ids_to_delete = media_group_message_ids.copy()
|
||||
message_ids_to_delete.append(helper_message_id)
|
||||
@@ -428,19 +609,22 @@ class PostPublishService:
|
||||
|
||||
try:
|
||||
await self._get_bot(call.message).delete_messages(
|
||||
chat_id=self.group_for_posts,
|
||||
message_ids=message_ids_to_delete
|
||||
chat_id=self.group_for_posts, message_ids=message_ids_to_delete
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"_decline_media_group: Ошибка при удалении сообщений: {e}")
|
||||
|
||||
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
logger.warning(f"_decline_media_group: Пользователь {author_id} заблокировал бота")
|
||||
logger.warning(
|
||||
f"_decline_media_group: Пользователь {author_id} заблокировал бота"
|
||||
)
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"_decline_media_group: Ошибка при отправке уведомления автору {author_id}: {e}")
|
||||
logger.error(
|
||||
f"_decline_media_group: Ошибка при отправке уведомления автору {author_id}: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
@track_time("_get_author_id", "post_publish_service")
|
||||
@@ -460,7 +644,7 @@ class PostPublishService:
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(message_id)
|
||||
if author_id:
|
||||
return author_id
|
||||
|
||||
|
||||
# Если не найден, ищем по основному message_id медиагруппы
|
||||
# Для этого нужно найти связанные сообщения медиагруппы
|
||||
try:
|
||||
@@ -474,7 +658,7 @@ class PostPublishService:
|
||||
return author_id
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось найти автора через связанные сообщения: {e}")
|
||||
|
||||
|
||||
# Если все способы не сработали, ищем напрямую
|
||||
author_id = await self.db.get_author_id_by_message_id(message_id)
|
||||
if not author_id:
|
||||
@@ -483,10 +667,14 @@ class PostPublishService:
|
||||
|
||||
@track_time("_delete_post_and_notify_author", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_delete_post_and_notify_author")
|
||||
async def _delete_post_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
|
||||
async def _delete_post_and_notify_author(
|
||||
self, call: CallbackQuery, author_id: int
|
||||
) -> None:
|
||||
"""Удаление поста и уведомление автора"""
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
|
||||
await self._get_bot(call.message).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:
|
||||
@@ -497,16 +685,22 @@ class PostPublishService:
|
||||
@track_time("_delete_media_group_and_notify_author", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_delete_media_group_and_notify_author")
|
||||
@track_media_processing("media_group")
|
||||
async def _delete_media_group_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
|
||||
async def _delete_media_group_and_notify_author(
|
||||
self, call: CallbackQuery, author_id: int
|
||||
) -> None:
|
||||
"""Удаление медиагруппы и уведомление автора (legacy метод, используется для обратной совместимости)"""
|
||||
helper_message_id = call.message.message_id
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
|
||||
message_ids_to_delete = media_group_message_ids.copy()
|
||||
message_ids_to_delete.append(helper_message_id)
|
||||
|
||||
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids_to_delete)
|
||||
|
||||
await self._get_bot(call.message).delete_messages(
|
||||
chat_id=self.group_for_posts, message_ids=message_ids_to_delete
|
||||
)
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||
except Exception as e:
|
||||
@@ -516,30 +710,47 @@ class PostPublishService:
|
||||
|
||||
@track_time("_save_published_post_content", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_save_published_post_content")
|
||||
async def _save_published_post_content(self, published_message: types.Message, published_message_id: int, original_message_id: int) -> None:
|
||||
async def _save_published_post_content(
|
||||
self,
|
||||
published_message: types.Message,
|
||||
published_message_id: int,
|
||||
original_message_id: int,
|
||||
) -> None:
|
||||
"""Сохраняет ссылку на медиафайл из опубликованного поста (файл уже в S3 или на диске)."""
|
||||
try:
|
||||
# Получаем уже сохраненный путь/S3 ключ из оригинального поста
|
||||
saved_content = await self.db.get_post_content_by_message_id(original_message_id)
|
||||
|
||||
saved_content = await self.db.get_post_content_by_message_id(
|
||||
original_message_id
|
||||
)
|
||||
|
||||
if saved_content and len(saved_content) > 0:
|
||||
# Копируем тот же путь/S3 ключ
|
||||
file_path, content_type = saved_content[0]
|
||||
logger.debug(f"Копируем путь/S3 ключ для опубликованного поста: {file_path}")
|
||||
|
||||
logger.debug(
|
||||
f"Копируем путь/S3 ключ для опубликованного поста: {file_path}"
|
||||
)
|
||||
|
||||
success = await self.db.add_published_post_content(
|
||||
published_message_id=published_message_id,
|
||||
content_path=file_path, # Тот же путь/S3 ключ
|
||||
content_type=content_type
|
||||
content_type=content_type,
|
||||
)
|
||||
if success:
|
||||
logger.info(f"Ссылка на файл сохранена для опубликованного поста: published_message_id={published_message_id}, path={file_path}")
|
||||
logger.info(
|
||||
f"Ссылка на файл сохранена для опубликованного поста: published_message_id={published_message_id}, path={file_path}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Не удалось сохранить ссылку на файл: published_message_id={published_message_id}")
|
||||
logger.warning(
|
||||
f"Не удалось сохранить ссылку на файл: published_message_id={published_message_id}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Контент не найден для оригинального поста message_id={original_message_id}")
|
||||
logger.warning(
|
||||
f"Контент не найден для оригинального поста message_id={original_message_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении ссылки на контент опубликованного поста {published_message_id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при сохранении ссылки на контент опубликованного поста {published_message_id}: {e}"
|
||||
)
|
||||
# Не прерываем публикацию, если сохранение контента не удалось
|
||||
|
||||
|
||||
@@ -548,8 +759,8 @@ class BanService:
|
||||
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']
|
||||
self.group_for_posts = settings["Telegram"]["group_for_posts"]
|
||||
self.important_logs = settings["Telegram"]["important_logs"]
|
||||
|
||||
def _get_bot(self, message) -> Bot:
|
||||
"""Получает бота из контекста сообщения или использует переданного"""
|
||||
@@ -564,16 +775,22 @@ class BanService:
|
||||
"""Бан пользователя за спам"""
|
||||
# Если это helper-сообщение медиагруппы, используем специальный метод
|
||||
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(call.message.message_id)
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
else:
|
||||
author_id = await self.db.get_author_id_by_message_id(call.message.message_id)
|
||||
|
||||
author_id = await self.db.get_author_id_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
|
||||
if not author_id:
|
||||
raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}")
|
||||
|
||||
raise UserNotFoundError(
|
||||
f"Автор не найден для сообщения {call.message.message_id}"
|
||||
)
|
||||
|
||||
current_date = datetime.now()
|
||||
date_to_unban = int((current_date + timedelta(days=7)).timestamp())
|
||||
|
||||
|
||||
ban_author_id = call.from_user.id
|
||||
|
||||
await self.db.set_user_blacklist(
|
||||
@@ -583,17 +800,21 @@ class BanService:
|
||||
date_to_unban=date_to_unban,
|
||||
ban_author=ban_author_id,
|
||||
)
|
||||
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
|
||||
|
||||
await self._get_bot(call.message).delete_message(
|
||||
chat_id=self.group_for_posts, message_id=call.message.message_id
|
||||
)
|
||||
|
||||
date_str = (current_date + timedelta(days=7)).strftime("%d.%m.%Y %H:%M")
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_USER_BANNED_SPAM.format(date=date_str))
|
||||
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}")
|
||||
|
||||
@track_time("ban_user", "ban_service")
|
||||
@@ -603,7 +824,7 @@ class BanService:
|
||||
user_name = await self.db.get_username(int(user_id))
|
||||
if not user_name:
|
||||
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
||||
|
||||
|
||||
return user_name
|
||||
|
||||
@track_time("unlock_user", "ban_service")
|
||||
@@ -614,7 +835,7 @@ class BanService:
|
||||
user_name = await self.db.get_username(int(user_id))
|
||||
if not user_name:
|
||||
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
||||
|
||||
|
||||
await delete_user_blacklist(int(user_id), self.db)
|
||||
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
|
||||
return user_name
|
||||
|
||||
@@ -6,27 +6,24 @@ from .constants import ERROR_MESSAGES, FSM_STATES
|
||||
from .decorators import error_handler
|
||||
from .exceptions import NoReplyToMessageError, UserNotFoundError
|
||||
from .group_handlers import GroupHandlers, create_group_handlers, group_router
|
||||
|
||||
# Local imports - services
|
||||
from .services import AdminReplyService, DatabaseProtocol
|
||||
|
||||
__all__ = [
|
||||
# Main components
|
||||
'group_router',
|
||||
'create_group_handlers',
|
||||
'GroupHandlers',
|
||||
|
||||
"group_router",
|
||||
"create_group_handlers",
|
||||
"GroupHandlers",
|
||||
# Services
|
||||
'AdminReplyService',
|
||||
'DatabaseProtocol',
|
||||
|
||||
"AdminReplyService",
|
||||
"DatabaseProtocol",
|
||||
# Constants
|
||||
'FSM_STATES',
|
||||
'ERROR_MESSAGES',
|
||||
|
||||
"FSM_STATES",
|
||||
"ERROR_MESSAGES",
|
||||
# Exceptions
|
||||
'NoReplyToMessageError',
|
||||
'UserNotFoundError',
|
||||
|
||||
"NoReplyToMessageError",
|
||||
"UserNotFoundError",
|
||||
# Utilities
|
||||
'error_handler'
|
||||
"error_handler",
|
||||
]
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
from typing import Dict, Final
|
||||
|
||||
# FSM States
|
||||
FSM_STATES: Final[Dict[str, str]] = {
|
||||
"CHAT": "CHAT"
|
||||
}
|
||||
FSM_STATES: Final[Dict[str, str]] = {"CHAT": "CHAT"}
|
||||
|
||||
# Error messages
|
||||
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
||||
"NO_REPLY_TO_MESSAGE": "Блять, выдели сообщение!",
|
||||
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение."
|
||||
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение.",
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ 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)
|
||||
@@ -19,18 +21,23 @@ def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
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
|
||||
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']
|
||||
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()}"
|
||||
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
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
# Third-party imports
|
||||
from aiogram import Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
# Local imports - filters
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import metrics, track_errors, track_time
|
||||
|
||||
# Local imports - utilities
|
||||
from logs.custom_logger import logger
|
||||
|
||||
@@ -20,25 +23,24 @@ from .services import AdminReplyService
|
||||
|
||||
class GroupHandlers:
|
||||
"""Main handler class for group messages"""
|
||||
|
||||
|
||||
def __init__(self, db: AsyncBotDB, 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"])
|
||||
self.handle_message, ChatTypeFilter(chat_type=["group", "supergroup"])
|
||||
)
|
||||
|
||||
|
||||
@error_handler
|
||||
@track_errors("group_handlers", "handle_message")
|
||||
@track_time("handle_message", "group_handlers")
|
||||
@@ -46,44 +48,46 @@ class GroupHandlers:
|
||||
"""Handle admin reply to user through group chat"""
|
||||
|
||||
logger.info(
|
||||
f'Получено сообщение в группе {message.chat.title} (ID: {message.chat.id}) '
|
||||
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'админ не выделил сообщение для ответа.'
|
||||
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 = await 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})'
|
||||
f"Ошибка при поиске пользователя в базе для ответа на сообщение: {reply_text} "
|
||||
f"в группе {message.chat.title} (ID сообщения: {message.message_id})"
|
||||
)
|
||||
|
||||
|
||||
# Factory function to create handlers with dependencies
|
||||
def create_group_handlers(db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMarkup) -> GroupHandlers:
|
||||
def create_group_handlers(
|
||||
db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMarkup
|
||||
) -> GroupHandlers:
|
||||
"""Create group handlers instance with dependencies"""
|
||||
return GroupHandlers(db, keyboard_markup)
|
||||
|
||||
@@ -91,21 +95,23 @@ def create_group_handlers(db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMa
|
||||
# Legacy router for backward compatibility
|
||||
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.keyboards.keyboards import get_reply_keyboard_leave_chat
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
|
||||
|
||||
bdf = get_global_instance()
|
||||
#TODO: поменять архитектуру и подключить правильный BotDB
|
||||
# TODO: поменять архитектуру и подключить правильный BotDB
|
||||
db = bdf.get_db()
|
||||
keyboard_markup = get_reply_keyboard_leave_chat()
|
||||
|
||||
|
||||
handlers = create_group_handlers(db, keyboard_markup)
|
||||
group_router = handlers.router
|
||||
|
||||
|
||||
# Initialize legacy router
|
||||
init_legacy_router()
|
||||
|
||||
@@ -5,8 +5,10 @@ from typing import Optional, Protocol
|
||||
|
||||
# Third-party imports
|
||||
from aiogram import types
|
||||
|
||||
# Local imports
|
||||
from helper_bot.utils.helper_func import send_text_message
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
@@ -16,29 +18,32 @@ from .exceptions import NoReplyToMessageError, UserNotFoundError
|
||||
|
||||
class DatabaseProtocol(Protocol):
|
||||
"""Protocol for database operations"""
|
||||
|
||||
async def get_user_by_message_id(self, message_id: int) -> Optional[int]: ...
|
||||
async def add_message(self, message_text: str, user_id: int, message_id: int, date: int = None): ...
|
||||
async def add_message(
|
||||
self, message_text: str, user_id: int, message_id: int, date: int = None
|
||||
): ...
|
||||
|
||||
|
||||
class AdminReplyService:
|
||||
"""Service for admin reply operations"""
|
||||
|
||||
|
||||
def __init__(self, db: DatabaseProtocol) -> None:
|
||||
self.db = db
|
||||
|
||||
|
||||
@track_time("get_user_id_for_reply", "admin_reply_service")
|
||||
@track_errors("admin_reply_service", "get_user_id_for_reply")
|
||||
@db_query_time("get_user_id_for_reply", "users", "select")
|
||||
async 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
|
||||
"""
|
||||
@@ -46,19 +51,19 @@ class AdminReplyService:
|
||||
if user_id is None:
|
||||
raise UserNotFoundError(f"User not found for message_id: {message_id}")
|
||||
return user_id
|
||||
|
||||
|
||||
@track_time("send_reply_to_user", "admin_reply_service")
|
||||
@track_errors("admin_reply_service", "send_reply_to_user")
|
||||
async def send_reply_to_user(
|
||||
self,
|
||||
chat_id: int,
|
||||
message: types.Message,
|
||||
reply_text: str,
|
||||
markup: types.ReplyKeyboardMarkup
|
||||
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
|
||||
|
||||
@@ -4,28 +4,25 @@
|
||||
# Local imports - constants and utilities
|
||||
from .constants import BUTTON_TEXTS, ERROR_MESSAGES, FSM_STATES
|
||||
from .decorators import error_handler
|
||||
from .private_handlers import (PrivateHandlers, create_private_handlers,
|
||||
private_router)
|
||||
from .private_handlers import PrivateHandlers, create_private_handlers, private_router
|
||||
|
||||
# Local imports - services
|
||||
from .services import BotSettings, PostService, StickerService, UserService
|
||||
|
||||
__all__ = [
|
||||
# Main components
|
||||
'private_router',
|
||||
'create_private_handlers',
|
||||
'PrivateHandlers',
|
||||
|
||||
"private_router",
|
||||
"create_private_handlers",
|
||||
"PrivateHandlers",
|
||||
# Services
|
||||
'BotSettings',
|
||||
'UserService',
|
||||
'PostService',
|
||||
'StickerService',
|
||||
|
||||
"BotSettings",
|
||||
"UserService",
|
||||
"PostService",
|
||||
"StickerService",
|
||||
# Constants
|
||||
'FSM_STATES',
|
||||
'BUTTON_TEXTS',
|
||||
'ERROR_MESSAGES',
|
||||
|
||||
"FSM_STATES",
|
||||
"BUTTON_TEXTS",
|
||||
"ERROR_MESSAGES",
|
||||
# Utilities
|
||||
'error_handler'
|
||||
"error_handler",
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ FSM_STATES: Final[Dict[str, str]] = {
|
||||
"START": "START",
|
||||
"SUGGEST": "SUGGEST",
|
||||
"PRE_CHAT": "PRE_CHAT",
|
||||
"CHAT": "CHAT"
|
||||
"CHAT": "CHAT",
|
||||
}
|
||||
|
||||
# Button texts
|
||||
@@ -18,7 +18,7 @@ BUTTON_TEXTS: Final[Dict[str, str]] = {
|
||||
"RETURN_TO_BOT": "Вернуться в бота",
|
||||
"WANT_STICKERS": "🤪Хочу стикеры",
|
||||
"CONNECT_ADMIN": "📩Связаться с админами",
|
||||
"VOICE_BOT": "🎤Голосовой бот"
|
||||
"VOICE_BOT": "🎤Голосовой бот",
|
||||
}
|
||||
|
||||
# Button to command mapping for metrics
|
||||
@@ -29,15 +29,15 @@ BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"Вернуться в бота": "return_to_bot",
|
||||
"🤪Хочу стикеры": "want_stickers",
|
||||
"📩Связаться с админами": "connect_admin",
|
||||
"🎤Голосовой бот": "voice_bot"
|
||||
"🎤Голосовой бот": "voice_bot",
|
||||
}
|
||||
|
||||
# Error messages
|
||||
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
||||
"UNSUPPORTED_CONTENT": (
|
||||
'Я пока не умею работать с таким сообщением. '
|
||||
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
|
||||
'Мы добавим его к обработке если необходимо'
|
||||
"Я пока не умею работать с таким сообщением. "
|
||||
"Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n"
|
||||
"Мы добавим его к обработке если необходимо"
|
||||
),
|
||||
"STICKERS_LINK": "Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk"
|
||||
"STICKERS_LINK": "Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk",
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ 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)
|
||||
@@ -19,18 +21,23 @@ def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
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
|
||||
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']
|
||||
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()}"
|
||||
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
|
||||
|
||||
@@ -8,18 +8,23 @@ from datetime import datetime
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
# Local imports - filters and middlewares
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
|
||||
# Local imports - utilities
|
||||
from helper_bot.keyboards import (get_reply_keyboard,
|
||||
get_reply_keyboard_for_post)
|
||||
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.blacklist_middleware import BlacklistMiddleware
|
||||
from helper_bot.utils import messages
|
||||
from helper_bot.utils.helper_func import (check_user_emoji, get_first_name,
|
||||
update_user_info)
|
||||
from helper_bot.utils.helper_func import (
|
||||
check_user_emoji,
|
||||
get_first_name,
|
||||
update_user_info,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||||
|
||||
@@ -34,83 +39,138 @@ sleep = asyncio.sleep
|
||||
|
||||
class PrivateHandlers:
|
||||
"""Main handler class for private messages"""
|
||||
|
||||
|
||||
def __init__(self, db: AsyncBotDB, settings: BotSettings, s3_storage=None):
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
self.user_service = UserService(db, settings)
|
||||
self.post_service = PostService(db, settings, s3_storage)
|
||||
self.sticker_service = StickerService(settings)
|
||||
|
||||
|
||||
self.router = Router()
|
||||
self.router.message.middleware(AlbumMiddleware(latency=5.0))
|
||||
self.router.message.middleware(BlacklistMiddleware())
|
||||
|
||||
|
||||
# Register handlers
|
||||
self._register_handlers()
|
||||
|
||||
|
||||
def _register_handlers(self):
|
||||
"""Register all message handlers"""
|
||||
# Command handlers
|
||||
self.router.message.register(self.handle_emoji_message, ChatTypeFilter(chat_type=["private"]), Command("emoji"))
|
||||
self.router.message.register(self.handle_restart_message, ChatTypeFilter(chat_type=["private"]), Command("restart"))
|
||||
self.router.message.register(self.handle_start_message, ChatTypeFilter(chat_type=["private"]), Command("start"))
|
||||
self.router.message.register(self.handle_start_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["RETURN_TO_BOT"])
|
||||
|
||||
# Button handlers
|
||||
self.router.message.register(self.suggest_post, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["SUGGEST_POST"])
|
||||
self.router.message.register(self.end_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["SAY_GOODBYE"])
|
||||
self.router.message.register(self.end_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["LEAVE_CHAT"])
|
||||
self.router.message.register(self.stickers, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["WANT_STICKERS"])
|
||||
self.router.message.register(self.connect_with_admin, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["CONNECT_ADMIN"])
|
||||
self.router.message.register(
|
||||
self.handle_emoji_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("emoji"),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.handle_restart_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("restart"),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.handle_start_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("start"),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.handle_start_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["RETURN_TO_BOT"],
|
||||
)
|
||||
|
||||
# Button handlers
|
||||
self.router.message.register(
|
||||
self.suggest_post,
|
||||
StateFilter(FSM_STATES["START"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["SUGGEST_POST"],
|
||||
)
|
||||
self.router.message.register(
|
||||
self.end_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["SAY_GOODBYE"],
|
||||
)
|
||||
self.router.message.register(
|
||||
self.end_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["LEAVE_CHAT"],
|
||||
)
|
||||
self.router.message.register(
|
||||
self.stickers,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["WANT_STICKERS"],
|
||||
)
|
||||
self.router.message.register(
|
||||
self.connect_with_admin,
|
||||
StateFilter(FSM_STATES["START"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["CONNECT_ADMIN"],
|
||||
)
|
||||
|
||||
|
||||
# State handlers
|
||||
self.router.message.register(self.suggest_router, StateFilter(FSM_STATES["SUGGEST"]), ChatTypeFilter(chat_type=["private"]))
|
||||
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["PRE_CHAT"]), ChatTypeFilter(chat_type=["private"]))
|
||||
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["CHAT"]), ChatTypeFilter(chat_type=["private"]))
|
||||
self.router.message.register(
|
||||
self.suggest_router,
|
||||
StateFilter(FSM_STATES["SUGGEST"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.resend_message_in_group_for_message,
|
||||
StateFilter(FSM_STATES["PRE_CHAT"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.resend_message_in_group_for_message,
|
||||
StateFilter(FSM_STATES["CHAT"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
)
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "handle_emoji_message")
|
||||
@track_time("handle_emoji_message", "private_handlers")
|
||||
async def handle_emoji_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
async def handle_emoji_message(
|
||||
self, message: types.Message, state: FSMContext, **kwargs
|
||||
):
|
||||
"""Handle emoji command"""
|
||||
await self.user_service.log_user_message(message)
|
||||
user_emoji = await check_user_emoji(message)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
if user_emoji is not None:
|
||||
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
|
||||
|
||||
await message.answer(f"Твоя эмодзя - {user_emoji}", parse_mode="HTML")
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "handle_restart_message")
|
||||
@track_time("handle_restart_message", "private_handlers")
|
||||
async def handle_restart_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
async def handle_restart_message(
|
||||
self, message: types.Message, state: FSMContext, **kwargs
|
||||
):
|
||||
"""Handle restart command"""
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await self.user_service.log_user_message(message)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
await update_user_info('love', message)
|
||||
await update_user_info("love", message)
|
||||
await check_user_emoji(message)
|
||||
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
|
||||
|
||||
await message.answer("Я перезапущен!", reply_markup=markup, parse_mode="HTML")
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "handle_start_message")
|
||||
@track_time("handle_start_message", "private_handlers")
|
||||
async def handle_start_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
async def handle_start_message(
|
||||
self, message: types.Message, state: FSMContext, **kwargs
|
||||
):
|
||||
"""Handle start command and return to bot button with metrics tracking"""
|
||||
# User service operations with metrics
|
||||
await self.user_service.log_user_message(message)
|
||||
await self.user_service.ensure_user_exists(message)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
|
||||
# Send sticker with metrics
|
||||
await self.sticker_service.send_random_hello_sticker(message)
|
||||
|
||||
|
||||
# Send welcome message with metrics
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
|
||||
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
|
||||
|
||||
hello_message = messages.get_message(get_first_name(message), "HELLO_MESSAGE")
|
||||
await message.answer(hello_message, reply_markup=markup, parse_mode="HTML")
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "suggest_post")
|
||||
@track_time("suggest_post", "private_handlers")
|
||||
@@ -120,11 +180,11 @@ class PrivateHandlers:
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
await self.user_service.log_user_message(message)
|
||||
await state.set_state(FSM_STATES["SUGGEST"])
|
||||
|
||||
|
||||
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, reply_markup=markup)
|
||||
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "end_message")
|
||||
@track_time("end_message", "private_handlers")
|
||||
@@ -133,31 +193,35 @@ class PrivateHandlers:
|
||||
# User service operations with metrics
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
await self.user_service.log_user_message(message)
|
||||
|
||||
|
||||
# Send sticker
|
||||
await self.sticker_service.send_random_goodbye_sticker(message)
|
||||
|
||||
|
||||
# Send goodbye message
|
||||
markup = types.ReplyKeyboardRemove()
|
||||
bye_message = messages.get_message(get_first_name(message), 'BYE_MESSAGE')
|
||||
bye_message = messages.get_message(get_first_name(message), "BYE_MESSAGE")
|
||||
await message.answer(bye_message, reply_markup=markup)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "suggest_router")
|
||||
@track_time("suggest_router", "private_handlers")
|
||||
async def suggest_router(self, message: types.Message, state: FSMContext, album: list = None, **kwargs):
|
||||
async def suggest_router(
|
||||
self, message: types.Message, state: FSMContext, album: list = None, **kwargs
|
||||
):
|
||||
"""Handle post submission in suggest state"""
|
||||
# Проверяем, есть ли механизм для получения полной медиагруппы (для медиагрупп)
|
||||
album_getter = kwargs.get("album_getter")
|
||||
|
||||
|
||||
if album_getter and message.media_group_id:
|
||||
# Это медиагруппа - сразу отвечаем пользователю, обработку делаем в фоне
|
||||
markup_for_user = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE')
|
||||
success_send_message = messages.get_message(
|
||||
get_first_name(message), "SUCCESS_SEND_MESSAGE"
|
||||
)
|
||||
await message.answer(success_send_message, reply_markup=markup_for_user)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
|
||||
# В фоне ждем полную медиагруппу и обрабатываем пост
|
||||
async def process_media_group_background():
|
||||
try:
|
||||
@@ -165,14 +229,15 @@ class PrivateHandlers:
|
||||
full_album = await album_getter.get_album(timeout=10.0)
|
||||
if not full_album:
|
||||
return
|
||||
|
||||
|
||||
# Обрабатываем пост с полной медиагруппой
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
await self.post_service.process_post(message, full_album)
|
||||
except Exception as e:
|
||||
from logs.custom_logger import logger
|
||||
|
||||
logger.error(f"Ошибка при фоновой обработке медиагруппы: {e}")
|
||||
|
||||
|
||||
asyncio.create_task(process_media_group_background())
|
||||
else:
|
||||
# Обычное сообщение или медиагруппа уже собрана - обрабатываем синхронно
|
||||
@@ -181,10 +246,12 @@ class PrivateHandlers:
|
||||
await self.user_service.log_user_message(message)
|
||||
await self.post_service.process_post(message, album)
|
||||
markup_for_user = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE')
|
||||
success_send_message = messages.get_message(
|
||||
get_first_name(message), "SUCCESS_SEND_MESSAGE"
|
||||
)
|
||||
await message.answer(success_send_message, reply_markup=markup_for_user)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "stickers")
|
||||
@track_time("stickers", "private_handlers")
|
||||
@@ -195,41 +262,46 @@ class PrivateHandlers:
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await self.db.update_stickers_info(message.from_user.id)
|
||||
await self.user_service.log_user_message(message)
|
||||
await message.answer(
|
||||
text=ERROR_MESSAGES["STICKERS_LINK"],
|
||||
reply_markup=markup
|
||||
)
|
||||
await message.answer(text=ERROR_MESSAGES["STICKERS_LINK"], reply_markup=markup)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "connect_with_admin")
|
||||
@track_time("connect_with_admin", "private_handlers")
|
||||
async def connect_with_admin(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
async def connect_with_admin(
|
||||
self, message: types.Message, state: FSMContext, **kwargs
|
||||
):
|
||||
"""Handle connect with admin button"""
|
||||
# User service operations with metrics
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
admin_message = messages.get_message(get_first_name(message), 'CONNECT_WITH_ADMIN')
|
||||
admin_message = messages.get_message(
|
||||
get_first_name(message), "CONNECT_WITH_ADMIN"
|
||||
)
|
||||
await message.answer(admin_message, parse_mode="html")
|
||||
await self.user_service.log_user_message(message)
|
||||
await state.set_state(FSM_STATES["PRE_CHAT"])
|
||||
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "resend_message_in_group_for_message")
|
||||
@track_time("resend_message_in_group_for_message", "private_handlers")
|
||||
@db_query_time("resend_message_in_group_for_message", "messages", "insert")
|
||||
async def resend_message_in_group_for_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
async def resend_message_in_group_for_message(
|
||||
self, message: types.Message, state: FSMContext, **kwargs
|
||||
):
|
||||
"""Handle messages in admin chat states"""
|
||||
# User service operations with metrics
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
await message.forward(chat_id=self.settings.group_for_message)
|
||||
|
||||
|
||||
current_date = datetime.now()
|
||||
date = int(current_date.timestamp())
|
||||
await self.db.add_message(message.text, message.from_user.id, message.message_id + 1, date)
|
||||
|
||||
question = messages.get_message(get_first_name(message), 'QUESTION')
|
||||
await self.db.add_message(
|
||||
message.text, message.from_user.id, message.message_id + 1, date
|
||||
)
|
||||
|
||||
question = messages.get_message(get_first_name(message), "QUESTION")
|
||||
user_state = await state.get_state()
|
||||
|
||||
|
||||
if user_state == FSM_STATES["PRE_CHAT"]:
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await message.answer(question, reply_markup=markup)
|
||||
@@ -240,7 +312,9 @@ class PrivateHandlers:
|
||||
|
||||
|
||||
# Factory function to create handlers with dependencies
|
||||
def create_private_handlers(db: AsyncBotDB, settings: BotSettings, s3_storage=None) -> PrivateHandlers:
|
||||
def create_private_handlers(
|
||||
db: AsyncBotDB, settings: BotSettings, s3_storage=None
|
||||
) -> PrivateHandlers:
|
||||
"""Create private handlers instance with dependencies"""
|
||||
return PrivateHandlers(db, settings, s3_storage)
|
||||
|
||||
@@ -248,32 +322,34 @@ def create_private_handlers(db: AsyncBotDB, settings: BotSettings, s3_storage=No
|
||||
# Legacy router for backward compatibility
|
||||
private_router = Router()
|
||||
|
||||
|
||||
# Initialize with global dependencies (for backward compatibility)
|
||||
def init_legacy_router():
|
||||
"""Initialize legacy router with global dependencies"""
|
||||
global private_router
|
||||
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
|
||||
|
||||
bdf = get_global_instance()
|
||||
settings = BotSettings(
|
||||
group_for_posts=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']
|
||||
group_for_posts=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"],
|
||||
)
|
||||
|
||||
|
||||
db = bdf.get_db()
|
||||
s3_storage = bdf.get_s3_storage()
|
||||
handlers = create_private_handlers(db, settings, s3_storage)
|
||||
|
||||
|
||||
# Instead of trying to copy handlers, we'll use the new router directly
|
||||
# This maintains backward compatibility while using the new architecture
|
||||
private_router = handlers.router
|
||||
|
||||
|
||||
# Initialize legacy router
|
||||
init_legacy_router()
|
||||
|
||||
@@ -12,37 +12,61 @@ from typing import Any, Callable, Dict, Protocol, Union
|
||||
# Third-party imports
|
||||
from aiogram import types
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from database.models import TelegramPost, User
|
||||
from helper_bot.keyboards import get_reply_keyboard_for_post
|
||||
|
||||
# Local imports - utilities
|
||||
from helper_bot.utils.helper_func import (
|
||||
add_in_db_media, check_username_and_full_name, determine_anonymity,
|
||||
get_first_name, get_text_message, prepare_media_group_from_middlewares,
|
||||
send_audio_message, send_media_group_message_to_private_chat,
|
||||
send_photo_message, send_text_message, send_video_message,
|
||||
send_video_note_message, send_voice_message)
|
||||
add_in_db_media,
|
||||
check_username_and_full_name,
|
||||
determine_anonymity,
|
||||
get_first_name,
|
||||
get_text_message,
|
||||
prepare_media_group_from_middlewares,
|
||||
send_audio_message,
|
||||
send_media_group_message_to_private_chat,
|
||||
send_photo_message,
|
||||
send_text_message,
|
||||
send_video_message,
|
||||
send_video_note_message,
|
||||
send_voice_message,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (db_query_time, track_errors,
|
||||
track_file_operations,
|
||||
track_media_processing, track_time)
|
||||
from helper_bot.utils.metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_file_operations,
|
||||
track_media_processing,
|
||||
track_time,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class DatabaseProtocol(Protocol):
|
||||
"""Protocol for database operations"""
|
||||
|
||||
async def user_exists(self, user_id: int) -> bool: ...
|
||||
async def add_user(self, user: User) -> None: ...
|
||||
async def update_user_info(self, user_id: int, username: str = None, full_name: str = None) -> None: ...
|
||||
async def update_user_info(
|
||||
self, user_id: int, username: str = None, full_name: str = None
|
||||
) -> None: ...
|
||||
async def update_user_date(self, user_id: int) -> None: ...
|
||||
async def add_post(self, post: TelegramPost) -> None: ...
|
||||
async def update_stickers_info(self, user_id: int) -> None: ...
|
||||
async def add_message(self, message_text: str, user_id: int, message_id: int, date: int = None) -> None: ...
|
||||
async def update_helper_message(self, message_id: int, helper_message_id: int) -> None: ...
|
||||
async def add_message(
|
||||
self, message_text: str, user_id: int, message_id: int, date: int = None
|
||||
) -> None: ...
|
||||
async def update_helper_message(
|
||||
self, message_id: int, helper_message_id: int
|
||||
) -> None: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class BotSettings:
|
||||
"""Bot configuration settings"""
|
||||
|
||||
group_for_posts: str
|
||||
group_for_message: str
|
||||
main_public: str
|
||||
@@ -55,18 +79,18 @@ class BotSettings:
|
||||
|
||||
class UserService:
|
||||
"""Service for user-related operations"""
|
||||
|
||||
|
||||
def __init__(self, db: DatabaseProtocol, settings: BotSettings) -> None:
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
|
||||
|
||||
@track_time("update_user_activity", "user_service")
|
||||
@track_errors("user_service", "update_user_activity")
|
||||
@db_query_time("update_user_activity", "users", "update")
|
||||
async def update_user_activity(self, user_id: int) -> None:
|
||||
"""Update user's last activity timestamp with metrics tracking"""
|
||||
await self.db.update_user_date(user_id)
|
||||
|
||||
|
||||
@track_time("ensure_user_exists", "user_service")
|
||||
@track_errors("user_service", "ensure_user_exists")
|
||||
@db_query_time("ensure_user_exists", "users", "insert")
|
||||
@@ -78,7 +102,7 @@ class UserService:
|
||||
first_name = get_first_name(message)
|
||||
is_bot = message.from_user.is_bot
|
||||
language_code = message.from_user.language_code
|
||||
|
||||
|
||||
# Create User object with current timestamp
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
user = User(
|
||||
@@ -92,32 +116,38 @@ class UserService:
|
||||
has_stickers=False,
|
||||
date_added=current_timestamp,
|
||||
date_changed=current_timestamp,
|
||||
voice_bot_welcome_received=False
|
||||
voice_bot_welcome_received=False,
|
||||
)
|
||||
|
||||
|
||||
# Пытаемся создать пользователя (если уже существует - игнорируем)
|
||||
# Это устраняет race condition и упрощает логику
|
||||
await self.db.add_user(user)
|
||||
|
||||
|
||||
# Проверяем, нужно ли обновить информацию о существующем пользователе
|
||||
is_need_update = await check_username_and_full_name(user_id, username, full_name, self.db)
|
||||
is_need_update = await check_username_and_full_name(
|
||||
user_id, username, full_name, self.db
|
||||
)
|
||||
if is_need_update:
|
||||
await self.db.update_user_info(user_id, username, full_name)
|
||||
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||
safe_full_name = (
|
||||
html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||
)
|
||||
safe_username = html.escape(username) if username else "Без никнейма"
|
||||
|
||||
|
||||
await message.answer(
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}")
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}"
|
||||
)
|
||||
await message.bot.send_message(
|
||||
chat_id=self.settings.group_for_logs,
|
||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
|
||||
|
||||
text=f"Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}",
|
||||
)
|
||||
|
||||
await self.db.update_user_date(user_id)
|
||||
|
||||
async def log_user_message(self, message: types.Message) -> None:
|
||||
"""Forward user message to logs group with metrics tracking"""
|
||||
await message.forward(chat_id=self.settings.group_for_logs)
|
||||
|
||||
|
||||
def get_safe_user_info(self, message: types.Message) -> tuple[str, str]:
|
||||
"""Get safely escaped user information for logging"""
|
||||
full_name = message.from_user.full_name or "Неизвестный пользователь"
|
||||
@@ -127,44 +157,56 @@ class UserService:
|
||||
|
||||
class PostService:
|
||||
"""Service for post-related operations"""
|
||||
|
||||
def __init__(self, db: DatabaseProtocol, settings: BotSettings, s3_storage=None) -> None:
|
||||
|
||||
def __init__(
|
||||
self, db: DatabaseProtocol, settings: BotSettings, s3_storage=None
|
||||
) -> None:
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
self.s3_storage = s3_storage
|
||||
|
||||
async def _save_media_background(self, sent_message: types.Message, bot_db: Any, s3_storage) -> None:
|
||||
|
||||
async def _save_media_background(
|
||||
self, sent_message: types.Message, bot_db: Any, s3_storage
|
||||
) -> None:
|
||||
"""Сохраняет медиа в фоне, чтобы не блокировать ответ пользователю"""
|
||||
try:
|
||||
success = await add_in_db_media(sent_message, bot_db, s3_storage)
|
||||
if not success:
|
||||
logger.warning(f"_save_media_background: Не удалось сохранить медиа для поста {sent_message.message_id}")
|
||||
logger.warning(
|
||||
f"_save_media_background: Не удалось сохранить медиа для поста {sent_message.message_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"_save_media_background: Ошибка при сохранении медиа для поста {sent_message.message_id}: {e}")
|
||||
|
||||
logger.error(
|
||||
f"_save_media_background: Ошибка при сохранении медиа для поста {sent_message.message_id}: {e}"
|
||||
)
|
||||
|
||||
@track_time("handle_text_post", "post_service")
|
||||
@track_errors("post_service", "handle_text_post")
|
||||
@db_query_time("handle_text_post", "posts", "insert")
|
||||
async def handle_text_post(self, message: types.Message, first_name: str) -> None:
|
||||
"""Handle text post submission"""
|
||||
post_text = get_text_message(message.text.lower(), first_name, message.from_user.username)
|
||||
post_text = get_text_message(
|
||||
message.text.lower(), first_name, message.from_user.username
|
||||
)
|
||||
markup = get_reply_keyboard_for_post()
|
||||
|
||||
sent_message = await send_text_message(self.settings.group_for_posts, message, post_text, markup)
|
||||
|
||||
|
||||
sent_message = await send_text_message(
|
||||
self.settings.group_for_posts, message, post_text, markup
|
||||
)
|
||||
|
||||
# Сохраняем сырой текст и определяем анонимность
|
||||
raw_text = message.text or ""
|
||||
is_anonymous = determine_anonymity(raw_text)
|
||||
|
||||
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=raw_text,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
|
||||
|
||||
@track_time("handle_photo_post", "post_service")
|
||||
@track_errors("post_service", "handle_photo_post")
|
||||
@db_query_time("handle_photo_post", "posts", "insert")
|
||||
@@ -172,28 +214,36 @@ class PostService:
|
||||
"""Handle photo post submission"""
|
||||
post_caption = ""
|
||||
if message.caption:
|
||||
post_caption = get_text_message(message.caption.lower(), first_name, message.from_user.username)
|
||||
|
||||
post_caption = get_text_message(
|
||||
message.caption.lower(), first_name, message.from_user.username
|
||||
)
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
sent_message = await send_photo_message(
|
||||
self.settings.group_for_posts, message, message.photo[-1].file_id, post_caption, markup
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
message.photo[-1].file_id,
|
||||
post_caption,
|
||||
markup,
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем сырой caption и определяем анонимность
|
||||
raw_caption = message.caption or ""
|
||||
is_anonymous = determine_anonymity(raw_caption)
|
||||
|
||||
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
# Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
|
||||
@track_time("handle_video_post", "post_service")
|
||||
@track_errors("post_service", "handle_video_post")
|
||||
@db_query_time("handle_video_post", "posts", "insert")
|
||||
@@ -201,28 +251,36 @@ class PostService:
|
||||
"""Handle video post submission"""
|
||||
post_caption = ""
|
||||
if message.caption:
|
||||
post_caption = get_text_message(message.caption.lower(), first_name, message.from_user.username)
|
||||
|
||||
post_caption = get_text_message(
|
||||
message.caption.lower(), first_name, message.from_user.username
|
||||
)
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
sent_message = await send_video_message(
|
||||
self.settings.group_for_posts, message, message.video.file_id, post_caption, markup
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
message.video.file_id,
|
||||
post_caption,
|
||||
markup,
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем сырой caption и определяем анонимность
|
||||
raw_caption = message.caption or ""
|
||||
is_anonymous = determine_anonymity(raw_caption)
|
||||
|
||||
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
# Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
|
||||
@track_time("handle_video_note_post", "post_service")
|
||||
@track_errors("post_service", "handle_video_note_post")
|
||||
@db_query_time("handle_video_note_post", "posts", "insert")
|
||||
@@ -232,22 +290,24 @@ class PostService:
|
||||
sent_message = await send_video_note_message(
|
||||
self.settings.group_for_posts, message, message.video_note.file_id, markup
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем пустую строку, так как video_note не имеет caption
|
||||
raw_caption = ""
|
||||
is_anonymous = determine_anonymity(raw_caption)
|
||||
|
||||
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
# Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
|
||||
@track_time("handle_audio_post", "post_service")
|
||||
@track_errors("post_service", "handle_audio_post")
|
||||
@db_query_time("handle_audio_post", "posts", "insert")
|
||||
@@ -255,28 +315,36 @@ class PostService:
|
||||
"""Handle audio post submission"""
|
||||
post_caption = ""
|
||||
if message.caption:
|
||||
post_caption = get_text_message(message.caption.lower(), first_name, message.from_user.username)
|
||||
|
||||
post_caption = get_text_message(
|
||||
message.caption.lower(), first_name, message.from_user.username
|
||||
)
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
sent_message = await send_audio_message(
|
||||
self.settings.group_for_posts, message, message.audio.file_id, post_caption, markup
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
message.audio.file_id,
|
||||
post_caption,
|
||||
markup,
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем сырой caption и определяем анонимность
|
||||
raw_caption = message.caption or ""
|
||||
is_anonymous = determine_anonymity(raw_caption)
|
||||
|
||||
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
# Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
|
||||
@track_time("handle_voice_post", "post_service")
|
||||
@track_errors("post_service", "handle_voice_post")
|
||||
@db_query_time("handle_voice_post", "posts", "insert")
|
||||
@@ -286,106 +354,116 @@ class PostService:
|
||||
sent_message = await send_voice_message(
|
||||
self.settings.group_for_posts, message, message.voice.file_id, markup
|
||||
)
|
||||
|
||||
|
||||
# Сохраняем пустую строку, так как voice не имеет caption
|
||||
raw_caption = ""
|
||||
is_anonymous = determine_anonymity(raw_caption)
|
||||
|
||||
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
# Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
|
||||
@track_time("handle_media_group_post", "post_service")
|
||||
@track_errors("post_service", "handle_media_group_post")
|
||||
@track_errors("post_service", "handle_media_group_post")
|
||||
@db_query_time("handle_media_group_post", "posts", "insert")
|
||||
@track_media_processing("media_group")
|
||||
async def handle_media_group_post(self, message: types.Message, album: list, first_name: str) -> None:
|
||||
async def handle_media_group_post(
|
||||
self, message: types.Message, album: list, first_name: str
|
||||
) -> None:
|
||||
"""Handle media group post submission"""
|
||||
post_caption = " "
|
||||
raw_caption = ""
|
||||
|
||||
|
||||
if album and album[0].caption:
|
||||
raw_caption = album[0].caption or ""
|
||||
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
|
||||
)
|
||||
|
||||
is_anonymous = determine_anonymity(raw_caption)
|
||||
media_group = await prepare_media_group_from_middlewares(album, post_caption)
|
||||
|
||||
|
||||
media_group_message_ids = await send_media_group_message_to_private_chat(
|
||||
self.settings.group_for_posts, message, media_group, self.db, None, self.s3_storage
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
media_group,
|
||||
self.db,
|
||||
None,
|
||||
self.s3_storage,
|
||||
)
|
||||
|
||||
|
||||
main_post_id = media_group_message_ids[-1]
|
||||
|
||||
|
||||
main_post = TelegramPost(
|
||||
message_id=main_post_id,
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(main_post)
|
||||
|
||||
|
||||
for msg_id in media_group_message_ids:
|
||||
await self.db.add_message_link(main_post_id, msg_id)
|
||||
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
helper_message = await send_text_message(
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
"^",
|
||||
markup
|
||||
self.settings.group_for_posts, message, "^", markup
|
||||
)
|
||||
helper_message_id = helper_message.message_id
|
||||
|
||||
|
||||
helper_post = TelegramPost(
|
||||
message_id=helper_message_id,
|
||||
text="^",
|
||||
author_id=message.from_user.id,
|
||||
helper_text_message_id=main_post_id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
)
|
||||
await self.db.add_post(helper_post)
|
||||
|
||||
|
||||
await self.db.update_helper_message(
|
||||
message_id=main_post_id,
|
||||
helper_message_id=helper_message_id
|
||||
message_id=main_post_id, helper_message_id=helper_message_id
|
||||
)
|
||||
|
||||
|
||||
@track_time("process_post", "post_service")
|
||||
@track_errors("post_service", "process_post")
|
||||
@track_media_processing("media_group")
|
||||
async def process_post(self, message: types.Message, album: Union[list, None] = None) -> None:
|
||||
async def process_post(
|
||||
self, message: types.Message, album: Union[list, None] = None
|
||||
) -> None:
|
||||
"""Process post based on content type"""
|
||||
first_name = get_first_name(message)
|
||||
|
||||
|
||||
if message.media_group_id is not None:
|
||||
await self.handle_media_group_post(message, album, first_name)
|
||||
return
|
||||
|
||||
|
||||
content_handlers: Dict[str, Callable] = {
|
||||
'text': lambda: self.handle_text_post(message, first_name),
|
||||
'photo': lambda: self.handle_photo_post(message, first_name),
|
||||
'video': lambda: self.handle_video_post(message, first_name),
|
||||
'video_note': lambda: self.handle_video_note_post(message),
|
||||
'audio': lambda: self.handle_audio_post(message, first_name),
|
||||
'voice': lambda: self.handle_voice_post(message)
|
||||
"text": lambda: self.handle_text_post(message, first_name),
|
||||
"photo": lambda: self.handle_photo_post(message, first_name),
|
||||
"video": lambda: self.handle_video_post(message, first_name),
|
||||
"video_note": lambda: self.handle_video_note_post(message),
|
||||
"audio": lambda: self.handle_audio_post(message, first_name),
|
||||
"voice": lambda: self.handle_voice_post(message),
|
||||
}
|
||||
|
||||
|
||||
handler = content_handlers.get(message.content_type)
|
||||
if handler:
|
||||
await handler()
|
||||
else:
|
||||
from .constants import ERROR_MESSAGES
|
||||
|
||||
await message.bot.send_message(
|
||||
message.chat.id, ERROR_MESSAGES["UNSUPPORTED_CONTENT"]
|
||||
)
|
||||
@@ -393,29 +471,29 @@ class PostService:
|
||||
|
||||
class StickerService:
|
||||
"""Service for sticker-related operations"""
|
||||
|
||||
|
||||
def __init__(self, settings: BotSettings) -> None:
|
||||
self.settings = settings
|
||||
|
||||
|
||||
@track_time("send_random_hello_sticker", "sticker_service")
|
||||
@track_errors("sticker_service", "send_random_hello_sticker")
|
||||
@track_file_operations("sticker")
|
||||
async def send_random_hello_sticker(self, message: types.Message) -> None:
|
||||
"""Send random hello sticker with metrics tracking"""
|
||||
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 = FSInputFile(path=random_stick_hello)
|
||||
await message.answer_sticker(random_stick_hello)
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
|
||||
@track_time("send_random_goodbye_sticker", "sticker_service")
|
||||
@track_errors("sticker_service", "send_random_goodbye_sticker")
|
||||
@track_file_operations("sticker")
|
||||
async def send_random_goodbye_sticker(self, message: types.Message) -> None:
|
||||
"""Send random goodbye sticker with metrics tracking"""
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Утилиты для очистки и диагностики проблем с голосовыми файлами
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -12,108 +13,122 @@ from logs.custom_logger import logger
|
||||
|
||||
class VoiceFileCleanupUtils:
|
||||
"""Утилиты для очистки и диагностики голосовых файлов"""
|
||||
|
||||
|
||||
def __init__(self, bot_db):
|
||||
self.bot_db = bot_db
|
||||
|
||||
|
||||
async def find_orphaned_db_records(self) -> List[Tuple[str, int]]:
|
||||
"""Найти записи в БД, для которых нет соответствующих файлов"""
|
||||
try:
|
||||
# Получаем все записи из БД
|
||||
all_audio_records = await self.bot_db.get_all_audio_records()
|
||||
orphaned_records = []
|
||||
|
||||
|
||||
for record in all_audio_records:
|
||||
file_name = record.get('file_name', '')
|
||||
user_id = record.get('author_id', 0)
|
||||
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
file_name = record.get("file_name", "")
|
||||
user_id = record.get("author_id", 0)
|
||||
|
||||
file_path = f"{VOICE_USERS_DIR}/{file_name}.ogg"
|
||||
if not os.path.exists(file_path):
|
||||
orphaned_records.append((file_name, user_id))
|
||||
logger.warning(f"Найдена запись в БД без файла: {file_name} (user_id: {user_id})")
|
||||
|
||||
logger.info(f"Найдено {len(orphaned_records)} записей в БД без соответствующих файлов")
|
||||
logger.warning(
|
||||
f"Найдена запись в БД без файла: {file_name} (user_id: {user_id})"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Найдено {len(orphaned_records)} записей в БД без соответствующих файлов"
|
||||
)
|
||||
return orphaned_records
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске orphaned записей: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def find_orphaned_files(self) -> List[str]:
|
||||
"""Найти файлы на диске, для которых нет записей в БД"""
|
||||
try:
|
||||
if not os.path.exists(VOICE_USERS_DIR):
|
||||
logger.warning(f"Директория {VOICE_USERS_DIR} не существует")
|
||||
return []
|
||||
|
||||
|
||||
# Получаем все файлы .ogg в директории
|
||||
ogg_files = list(Path(VOICE_USERS_DIR).glob("*.ogg"))
|
||||
orphaned_files = []
|
||||
|
||||
|
||||
# Получаем все записи из БД
|
||||
all_audio_records = await self.bot_db.get_all_audio_records()
|
||||
db_file_names = {record.get('file_name', '') for record in all_audio_records}
|
||||
|
||||
db_file_names = {
|
||||
record.get("file_name", "") for record in all_audio_records
|
||||
}
|
||||
|
||||
for file_path in ogg_files:
|
||||
file_name = file_path.stem # Имя файла без расширения
|
||||
if file_name not in db_file_names:
|
||||
orphaned_files.append(str(file_path))
|
||||
logger.warning(f"Найден файл без записи в БД: {file_path}")
|
||||
|
||||
|
||||
logger.info(f"Найдено {len(orphaned_files)} файлов без записей в БД")
|
||||
return orphaned_files
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске orphaned файлов: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def cleanup_orphaned_db_records(self, dry_run: bool = True) -> int:
|
||||
"""Удалить записи в БД, для которых нет файлов"""
|
||||
try:
|
||||
orphaned_records = await self.find_orphaned_db_records()
|
||||
|
||||
|
||||
if not orphaned_records:
|
||||
logger.info("Нет orphaned записей для удаления")
|
||||
return 0
|
||||
|
||||
|
||||
if dry_run:
|
||||
logger.info(f"DRY RUN: Найдено {len(orphaned_records)} записей для удаления")
|
||||
logger.info(
|
||||
f"DRY RUN: Найдено {len(orphaned_records)} записей для удаления"
|
||||
)
|
||||
for file_name, user_id in orphaned_records:
|
||||
logger.info(f"DRY RUN: Будет удалена запись: {file_name} (user_id: {user_id})")
|
||||
logger.info(
|
||||
f"DRY RUN: Будет удалена запись: {file_name} (user_id: {user_id})"
|
||||
)
|
||||
return len(orphaned_records)
|
||||
|
||||
|
||||
# Удаляем записи
|
||||
deleted_count = 0
|
||||
for file_name, user_id in orphaned_records:
|
||||
try:
|
||||
await self.bot_db.delete_audio_record_by_file_name(file_name)
|
||||
deleted_count += 1
|
||||
logger.info(f"Удалена запись в БД: {file_name} (user_id: {user_id})")
|
||||
logger.info(
|
||||
f"Удалена запись в БД: {file_name} (user_id: {user_id})"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении записи {file_name}: {e}")
|
||||
|
||||
|
||||
logger.info(f"Удалено {deleted_count} orphaned записей из БД")
|
||||
return deleted_count
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при очистке orphaned записей: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def cleanup_orphaned_files(self, dry_run: bool = True) -> int:
|
||||
"""Удалить файлы на диске, для которых нет записей в БД"""
|
||||
try:
|
||||
orphaned_files = await self.find_orphaned_files()
|
||||
|
||||
|
||||
if not orphaned_files:
|
||||
logger.info("Нет orphaned файлов для удаления")
|
||||
return 0
|
||||
|
||||
|
||||
if dry_run:
|
||||
logger.info(f"DRY RUN: Найдено {len(orphaned_files)} файлов для удаления")
|
||||
logger.info(
|
||||
f"DRY RUN: Найдено {len(orphaned_files)} файлов для удаления"
|
||||
)
|
||||
for file_path in orphaned_files:
|
||||
logger.info(f"DRY RUN: Будет удален файл: {file_path}")
|
||||
return len(orphaned_files)
|
||||
|
||||
|
||||
# Удаляем файлы
|
||||
deleted_count = 0
|
||||
for file_path in orphaned_files:
|
||||
@@ -123,70 +138,76 @@ class VoiceFileCleanupUtils:
|
||||
logger.info(f"Удален файл: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении файла {file_path}: {e}")
|
||||
|
||||
|
||||
logger.info(f"Удалено {deleted_count} orphaned файлов")
|
||||
return deleted_count
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при очистке orphaned файлов: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def get_disk_usage_stats(self) -> dict:
|
||||
"""Получить статистику использования диска"""
|
||||
try:
|
||||
if not os.path.exists(VOICE_USERS_DIR):
|
||||
return {"error": f"Директория {VOICE_USERS_DIR} не существует"}
|
||||
|
||||
|
||||
total_size = 0
|
||||
file_count = 0
|
||||
|
||||
|
||||
for file_path in Path(VOICE_USERS_DIR).glob("*.ogg"):
|
||||
if file_path.is_file():
|
||||
total_size += file_path.stat().st_size
|
||||
file_count += 1
|
||||
|
||||
|
||||
return {
|
||||
"total_files": file_count,
|
||||
"total_size_bytes": total_size,
|
||||
"total_size_mb": round(total_size / (1024 * 1024), 2),
|
||||
"directory": VOICE_USERS_DIR
|
||||
"directory": VOICE_USERS_DIR,
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики диска: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
async def run_full_diagnostic(self) -> dict:
|
||||
"""Запустить полную диагностику"""
|
||||
try:
|
||||
logger.info("Запуск полной диагностики голосовых файлов...")
|
||||
|
||||
|
||||
# Статистика диска
|
||||
disk_stats = await self.get_disk_usage_stats()
|
||||
|
||||
|
||||
# Orphaned записи в БД
|
||||
orphaned_db_records = await self.find_orphaned_db_records()
|
||||
|
||||
|
||||
# Orphaned файлы
|
||||
orphaned_files = await self.find_orphaned_files()
|
||||
|
||||
|
||||
# Количество записей в БД
|
||||
all_audio_records = await self.bot_db.get_all_audio_records()
|
||||
db_records_count = len(all_audio_records)
|
||||
|
||||
|
||||
diagnostic_result = {
|
||||
"disk_stats": disk_stats,
|
||||
"db_records_count": db_records_count,
|
||||
"orphaned_db_records_count": len(orphaned_db_records),
|
||||
"orphaned_files_count": len(orphaned_files),
|
||||
"orphaned_db_records": orphaned_db_records[:10], # Первые 10 для примера
|
||||
"orphaned_db_records": orphaned_db_records[
|
||||
:10
|
||||
], # Первые 10 для примера
|
||||
"orphaned_files": orphaned_files[:10], # Первые 10 для примера
|
||||
"status": "healthy" if len(orphaned_db_records) == 0 and len(orphaned_files) == 0 else "issues_found"
|
||||
"status": (
|
||||
"healthy"
|
||||
if len(orphaned_db_records) == 0 and len(orphaned_files) == 0
|
||||
else "issues_found"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
logger.info(f"Диагностика завершена. Статус: {diagnostic_result['status']}")
|
||||
return diagnostic_result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при диагностике: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
@@ -17,10 +17,10 @@ CMD_REFRESH = "refresh"
|
||||
# Command to command mapping for metrics
|
||||
COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"start": "voice_start",
|
||||
"help": "voice_help",
|
||||
"help": "voice_help",
|
||||
"restart": "voice_restart",
|
||||
"emoji": "voice_emoji",
|
||||
"refresh": "voice_refresh"
|
||||
"refresh": "voice_refresh",
|
||||
}
|
||||
|
||||
# Button texts
|
||||
@@ -33,7 +33,7 @@ BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"🎧Послушать": "voice_listen",
|
||||
"Отменить": "voice_cancel",
|
||||
"🔄Сбросить прослушивания": "voice_refresh_listen",
|
||||
"😊Узнать эмодзи": "voice_emoji"
|
||||
"😊Узнать эмодзи": "voice_emoji",
|
||||
}
|
||||
|
||||
# Callback data
|
||||
@@ -43,7 +43,7 @@ CALLBACK_DELETE = "delete"
|
||||
# Callback to command mapping for metrics
|
||||
CALLBACK_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"save": "voice_save",
|
||||
"delete": "voice_delete"
|
||||
"delete": "voice_delete",
|
||||
}
|
||||
|
||||
# File paths
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
class VoiceBotError(Exception):
|
||||
"""Базовое исключение для voice_bot"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VoiceMessageError(VoiceBotError):
|
||||
"""Ошибка при работе с голосовыми сообщениями"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AudioProcessingError(VoiceBotError):
|
||||
"""Ошибка при обработке аудио"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseError(VoiceBotError):
|
||||
"""Ошибка базы данных"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FileOperationError(VoiceBotError):
|
||||
"""Ошибка при работе с файлами"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -7,16 +7,24 @@ from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from aiogram.types import FSInputFile
|
||||
from helper_bot.handlers.voice.constants import (MESSAGE_DELAY_1,
|
||||
MESSAGE_DELAY_2,
|
||||
MESSAGE_DELAY_3,
|
||||
MESSAGE_DELAY_4, STICK_DIR,
|
||||
STICK_PATTERN, STICKER_DELAY,
|
||||
VOICE_USERS_DIR)
|
||||
from helper_bot.handlers.voice.exceptions import (AudioProcessingError,
|
||||
DatabaseError,
|
||||
FileOperationError,
|
||||
VoiceMessageError)
|
||||
|
||||
from helper_bot.handlers.voice.constants import (
|
||||
MESSAGE_DELAY_1,
|
||||
MESSAGE_DELAY_2,
|
||||
MESSAGE_DELAY_3,
|
||||
MESSAGE_DELAY_4,
|
||||
STICK_DIR,
|
||||
STICK_PATTERN,
|
||||
STICKER_DELAY,
|
||||
VOICE_USERS_DIR,
|
||||
)
|
||||
from helper_bot.handlers.voice.exceptions import (
|
||||
AudioProcessingError,
|
||||
DatabaseError,
|
||||
FileOperationError,
|
||||
VoiceMessageError,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
@@ -24,19 +32,23 @@ from logs.custom_logger import logger
|
||||
|
||||
class VoiceMessage:
|
||||
"""Модель голосового сообщения"""
|
||||
def __init__(self, file_name: str, user_id: int, date_added: datetime, file_id: int):
|
||||
|
||||
def __init__(
|
||||
self, file_name: str, user_id: int, date_added: datetime, file_id: int
|
||||
):
|
||||
self.file_name = file_name
|
||||
self.user_id = user_id
|
||||
self.date_added = date_added
|
||||
self.file_id = file_id
|
||||
|
||||
|
||||
class VoiceBotService:
|
||||
"""Сервис для работы с голосовыми сообщениями"""
|
||||
|
||||
|
||||
def __init__(self, bot_db, settings):
|
||||
self.bot_db = bot_db
|
||||
self.settings = settings
|
||||
|
||||
|
||||
@track_time("get_welcome_sticker", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_welcome_sticker")
|
||||
async def get_welcome_sticker(self) -> Optional[FSInputFile]:
|
||||
@@ -45,17 +57,21 @@ class VoiceBotService:
|
||||
name_stick_hello = list(Path(STICK_DIR).rglob(STICK_PATTERN))
|
||||
if not name_stick_hello:
|
||||
return None
|
||||
|
||||
|
||||
random_stick_hello = random.choice(name_stick_hello)
|
||||
random_stick_hello = FSInputFile(path=random_stick_hello)
|
||||
logger.info(f"Стикер успешно получен. Наименование стикера: {random_stick_hello}")
|
||||
logger.info(
|
||||
f"Стикер успешно получен. Наименование стикера: {random_stick_hello}"
|
||||
)
|
||||
return random_stick_hello
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении стикера: {e}")
|
||||
if self.settings['Settings']['logs']:
|
||||
await self._send_error_to_logs(f'Отправка приветственных стикеров лажает. Ошибка: {e}')
|
||||
if self.settings["Settings"]["logs"]:
|
||||
await self._send_error_to_logs(
|
||||
f"Отправка приветственных стикеров лажает. Ошибка: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@track_time("send_welcome_messages", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "send_welcome_messages")
|
||||
async def send_welcome_messages(self, message, user_emoji: str):
|
||||
@@ -66,92 +82,94 @@ class VoiceBotService:
|
||||
if sticker:
|
||||
await message.answer_sticker(sticker)
|
||||
await asyncio.sleep(STICKER_DELAY)
|
||||
|
||||
|
||||
# Отправляем приветственное сообщение
|
||||
markup = self._get_main_keyboard()
|
||||
await message.answer(
|
||||
text="<b>Привет.</b>",
|
||||
parse_mode='html',
|
||||
text="<b>Привет.</b>",
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(STICKER_DELAY)
|
||||
|
||||
|
||||
# Отправляем описание
|
||||
await message.answer(
|
||||
text="<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска</i>",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_1)
|
||||
|
||||
|
||||
# Отправляем аналогию
|
||||
await message.answer(
|
||||
text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_2)
|
||||
|
||||
|
||||
# Отправляем правила
|
||||
await message.answer(
|
||||
text="Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя бы на 5-10 секунд</i>",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_3)
|
||||
|
||||
|
||||
# Отправляем информацию об анонимности
|
||||
await message.answer(
|
||||
text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
|
||||
# Отправляем предложения
|
||||
await message.answer(
|
||||
text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
|
||||
# Отправляем информацию об эмодзи
|
||||
await message.answer(
|
||||
text=f"Любые войсы будут помечены эмоджи. <b>Твой эмоджи - </b>{user_emoji}Таким эмоджи будут помечены твои сообщения для других Но другие люди не узнают кто за каким эмоджи скрывается:)",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
|
||||
# Отправляем информацию о помощи
|
||||
await message.answer(
|
||||
text="Так же можешь ознакомиться с инструкцией к боту по команде /help",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
|
||||
# Отправляем финальное сообщение
|
||||
await message.answer(
|
||||
text="<b>Ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке приветственных сообщений: {e}")
|
||||
raise VoiceMessageError(f"Не удалось отправить приветственные сообщения: {e}")
|
||||
|
||||
raise VoiceMessageError(
|
||||
f"Не удалось отправить приветственные сообщения: {e}"
|
||||
)
|
||||
|
||||
@track_time("get_random_audio", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_random_audio")
|
||||
async def get_random_audio(self, user_id: int) -> Optional[Tuple[str, str, str]]:
|
||||
@@ -159,25 +177,25 @@ class VoiceBotService:
|
||||
try:
|
||||
check_audio = await self.bot_db.check_listen_audio(user_id=user_id)
|
||||
list_audio = list(check_audio)
|
||||
|
||||
|
||||
if not list_audio:
|
||||
return None
|
||||
|
||||
|
||||
# Получаем случайное аудио
|
||||
number_element = random.randint(0, len(list_audio) - 1)
|
||||
audio_for_user = check_audio[number_element]
|
||||
|
||||
|
||||
# Получаем информацию об авторе
|
||||
user_id_author = await self.bot_db.get_user_id_by_file_name(audio_for_user)
|
||||
date_added = await self.bot_db.get_date_by_file_name(audio_for_user)
|
||||
user_emoji = await self.bot_db.get_user_emoji(user_id_author)
|
||||
|
||||
|
||||
return audio_for_user, date_added, user_emoji
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении случайного аудио: {e}")
|
||||
raise AudioProcessingError(f"Не удалось получить случайное аудио: {e}")
|
||||
|
||||
|
||||
@track_time("mark_audio_as_listened", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "mark_audio_as_listened")
|
||||
async def mark_audio_as_listened(self, file_name: str, user_id: int) -> None:
|
||||
@@ -187,7 +205,7 @@ class VoiceBotService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при пометке аудио как прослушанного: {e}")
|
||||
raise DatabaseError(f"Не удалось пометить аудио как прослушанное: {e}")
|
||||
|
||||
|
||||
@track_time("clear_user_listenings", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "clear_user_listenings")
|
||||
@db_query_time("clear_user_listenings", "audio_moderate", "delete")
|
||||
@@ -198,7 +216,7 @@ class VoiceBotService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при очистке прослушиваний: {e}")
|
||||
raise DatabaseError(f"Не удалось очистить прослушивания: {e}")
|
||||
|
||||
|
||||
@track_time("get_remaining_audio_count", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_remaining_audio_count")
|
||||
async def get_remaining_audio_count(self, user_id: int) -> int:
|
||||
@@ -209,25 +227,24 @@ class VoiceBotService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении количества аудио: {e}")
|
||||
raise DatabaseError(f"Не удалось получить количество аудио: {e}")
|
||||
|
||||
|
||||
@track_time("get_main_keyboard", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_main_keyboard")
|
||||
def _get_main_keyboard(self):
|
||||
"""Получить основную клавиатуру"""
|
||||
from helper_bot.keyboards.keyboards import get_main_keyboard
|
||||
|
||||
return get_main_keyboard()
|
||||
|
||||
|
||||
@track_time("send_error_to_logs", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "send_error_to_logs")
|
||||
async def _send_error_to_logs(self, message: str) -> None:
|
||||
"""Отправить ошибку в логи"""
|
||||
try:
|
||||
from helper_bot.utils.helper_func import send_voice_message
|
||||
|
||||
await send_voice_message(
|
||||
self.settings['Telegram']['important_logs'],
|
||||
None,
|
||||
None,
|
||||
None
|
||||
self.settings["Telegram"]["important_logs"], None, None, None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить ошибку в логи: {e}")
|
||||
@@ -235,45 +252,49 @@ class VoiceBotService:
|
||||
|
||||
class AudioFileService:
|
||||
"""Сервис для работы с аудио файлами"""
|
||||
|
||||
|
||||
def __init__(self, bot_db):
|
||||
self.bot_db = bot_db
|
||||
|
||||
|
||||
@track_time("generate_file_name", "audio_file_service")
|
||||
@track_errors("audio_file_service", "generate_file_name")
|
||||
async def generate_file_name(self, user_id: int) -> str:
|
||||
"""Сгенерировать имя файла для аудио"""
|
||||
try:
|
||||
# Проверяем есть ли запись о файле в базе данных
|
||||
user_audio_count = await self.bot_db.get_user_audio_records_count(user_id=user_id)
|
||||
|
||||
user_audio_count = await self.bot_db.get_user_audio_records_count(
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
if user_audio_count == 0:
|
||||
# Если нет, то генерируем имя файла
|
||||
file_name = f'message_from_{user_id}_number_1'
|
||||
file_name = f"message_from_{user_id}_number_1"
|
||||
else:
|
||||
# Иначе берем последнюю запись из БД, добавляем к ней 1
|
||||
file_name = await self.bot_db.get_path_for_audio_record(user_id=user_id)
|
||||
if file_name:
|
||||
# Извлекаем номер из имени файла и увеличиваем на 1
|
||||
try:
|
||||
current_number = int(file_name.split('_')[-1])
|
||||
current_number = int(file_name.split("_")[-1])
|
||||
new_number = current_number + 1
|
||||
except (ValueError, IndexError):
|
||||
new_number = user_audio_count + 1
|
||||
else:
|
||||
new_number = user_audio_count + 1
|
||||
|
||||
file_name = f'message_from_{user_id}_number_{new_number}'
|
||||
|
||||
|
||||
file_name = f"message_from_{user_id}_number_{new_number}"
|
||||
|
||||
return file_name
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при генерации имени файла: {e}")
|
||||
raise FileOperationError(f"Не удалось сгенерировать имя файла: {e}")
|
||||
|
||||
|
||||
@track_time("save_audio_file", "audio_file_service")
|
||||
@track_errors("audio_file_service", "save_audio_file")
|
||||
async def save_audio_file(self, file_name: str, user_id: int, date_added: datetime, file_id: str) -> None:
|
||||
async def save_audio_file(
|
||||
self, file_name: str, user_id: int, date_added: datetime, file_id: str
|
||||
) -> None:
|
||||
"""Сохранить информацию об аудио файле в базу данных"""
|
||||
try:
|
||||
# Проверяем существование файла перед сохранением в БД
|
||||
@@ -281,16 +302,20 @@ class AudioFileService:
|
||||
error_msg = f"Файл {file_name} не существует или поврежден, отменяем сохранение в БД"
|
||||
logger.error(error_msg)
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
|
||||
await self.bot_db.add_audio_record_simple(file_name, user_id, date_added)
|
||||
logger.info(f"Информация об аудио файле успешно сохранена в БД: {file_name}")
|
||||
logger.info(
|
||||
f"Информация об аудио файле успешно сохранена в БД: {file_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении аудио файла в БД: {e}")
|
||||
raise DatabaseError(f"Не удалось сохранить аудио файл в БД: {e}")
|
||||
|
||||
|
||||
@track_time("save_audio_file_with_transaction", "audio_file_service")
|
||||
@track_errors("audio_file_service", "save_audio_file_with_transaction")
|
||||
async def save_audio_file_with_transaction(self, file_name: str, user_id: int, date_added: datetime, file_id: str) -> None:
|
||||
async def save_audio_file_with_transaction(
|
||||
self, file_name: str, user_id: int, date_added: datetime, file_id: str
|
||||
) -> None:
|
||||
"""Сохранить информацию об аудио файле в базу данных с транзакцией"""
|
||||
try:
|
||||
# Проверяем существование файла перед сохранением в БД
|
||||
@@ -298,68 +323,80 @@ class AudioFileService:
|
||||
error_msg = f"Файл {file_name} не существует или поврежден, отменяем сохранение в БД"
|
||||
logger.error(error_msg)
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
|
||||
# Используем транзакцию для атомарности операции
|
||||
await self.bot_db.add_audio_record_simple(file_name, user_id, date_added)
|
||||
logger.info(f"Информация об аудио файле успешно сохранена в БД с транзакцией: {file_name}")
|
||||
logger.info(
|
||||
f"Информация об аудио файле успешно сохранена в БД с транзакцией: {file_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении аудио файла в БД с транзакцией: {e}")
|
||||
raise DatabaseError(f"Не удалось сохранить аудио файл в БД с транзакцией: {e}")
|
||||
|
||||
raise DatabaseError(
|
||||
f"Не удалось сохранить аудио файл в БД с транзакцией: {e}"
|
||||
)
|
||||
|
||||
@track_time("download_and_save_audio", "audio_file_service")
|
||||
@track_errors("audio_file_service", "download_and_save_audio")
|
||||
async def download_and_save_audio(self, bot, message, file_name: str, max_retries: int = 3) -> None:
|
||||
async def download_and_save_audio(
|
||||
self, bot, message, file_name: str, max_retries: int = 3
|
||||
) -> None:
|
||||
"""Скачать и сохранить аудио файл с retry механизмом"""
|
||||
last_exception = None
|
||||
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
logger.info(f"Попытка {attempt + 1}/{max_retries} скачивания и сохранения аудио: {file_name}")
|
||||
|
||||
logger.info(
|
||||
f"Попытка {attempt + 1}/{max_retries} скачивания и сохранения аудио: {file_name}"
|
||||
)
|
||||
|
||||
# Проверяем наличие голосового сообщения
|
||||
if not message or not message.voice:
|
||||
error_msg = "Сообщение или голосовое сообщение не найдено"
|
||||
logger.error(error_msg)
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
|
||||
file_id = message.voice.file_id
|
||||
logger.info(f"Получен file_id: {file_id}")
|
||||
|
||||
|
||||
# Получаем информацию о файле
|
||||
try:
|
||||
file_info = await bot.get_file(file_id=file_id)
|
||||
logger.info(f"Получена информация о файле: {file_info.file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении информации о файле: {e}")
|
||||
raise FileOperationError(f"Не удалось получить информацию о файле: {e}")
|
||||
|
||||
raise FileOperationError(
|
||||
f"Не удалось получить информацию о файле: {e}"
|
||||
)
|
||||
|
||||
# Скачиваем файл
|
||||
try:
|
||||
downloaded_file = await bot.download_file(file_path=file_info.file_path)
|
||||
downloaded_file = await bot.download_file(
|
||||
file_path=file_info.file_path
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при скачивании файла: {e}")
|
||||
raise FileOperationError(f"Не удалось скачать файл: {e}")
|
||||
|
||||
|
||||
# Проверяем что файл успешно скачан
|
||||
if not downloaded_file:
|
||||
error_msg = "Не удалось скачать файл - получен пустой объект"
|
||||
logger.error(error_msg)
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
|
||||
# Получаем размер файла без изменения позиции
|
||||
current_pos = downloaded_file.tell()
|
||||
downloaded_file.seek(0, 2) # Переходим в конец файла
|
||||
file_size = downloaded_file.tell()
|
||||
downloaded_file.seek(current_pos) # Возвращаемся в исходную позицию
|
||||
|
||||
|
||||
logger.info(f"Файл скачан, размер: {file_size} bytes")
|
||||
|
||||
|
||||
# Проверяем минимальный размер файла
|
||||
if file_size < 100: # Минимальный размер для аудио файла
|
||||
error_msg = f"Файл слишком маленький: {file_size} bytes"
|
||||
logger.error(error_msg)
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
|
||||
# Создаем директорию если она не существует
|
||||
try:
|
||||
os.makedirs(VOICE_USERS_DIR, exist_ok=True)
|
||||
@@ -367,27 +404,27 @@ class AudioFileService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании директории: {e}")
|
||||
raise FileOperationError(f"Не удалось создать директорию: {e}")
|
||||
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
|
||||
file_path = f"{VOICE_USERS_DIR}/{file_name}.ogg"
|
||||
logger.info(f"Сохраняем файл по пути: {file_path}")
|
||||
|
||||
|
||||
# Сбрасываем позицию в файле перед сохранением
|
||||
downloaded_file.seek(0)
|
||||
|
||||
|
||||
# Сохраняем файл
|
||||
try:
|
||||
with open(file_path, 'wb') as new_file:
|
||||
with open(file_path, "wb") as new_file:
|
||||
new_file.write(downloaded_file.read())
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при записи файла на диск: {e}")
|
||||
raise FileOperationError(f"Не удалось записать файл на диск: {e}")
|
||||
|
||||
|
||||
# Проверяем что файл действительно создался и имеет правильный размер
|
||||
if not os.path.exists(file_path):
|
||||
error_msg = f"Файл не был создан: {file_path}"
|
||||
logger.error(error_msg)
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
|
||||
saved_file_size = os.path.getsize(file_path)
|
||||
if saved_file_size != file_size:
|
||||
error_msg = f"Размер сохраненного файла не совпадает: ожидалось {file_size}, получено {saved_file_size}"
|
||||
@@ -398,48 +435,62 @@ class AudioFileService:
|
||||
except:
|
||||
pass
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
logger.info(f"Файл успешно сохранен: {file_path}, размер: {saved_file_size} bytes")
|
||||
|
||||
logger.info(
|
||||
f"Файл успешно сохранен: {file_path}, размер: {saved_file_size} bytes"
|
||||
)
|
||||
return # Успешное завершение
|
||||
|
||||
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
logger.error(f"Попытка {attempt + 1}/{max_retries} неудачна: {e}")
|
||||
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = (attempt + 1) * 2 # Экспоненциальная задержка: 2, 4, 6 секунд
|
||||
logger.info(f"Ожидание {wait_time} секунд перед следующей попыткой...")
|
||||
wait_time = (
|
||||
attempt + 1
|
||||
) * 2 # Экспоненциальная задержка: 2, 4, 6 секунд
|
||||
logger.info(
|
||||
f"Ожидание {wait_time} секунд перед следующей попыткой..."
|
||||
)
|
||||
await asyncio.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Все {max_retries} попыток скачивания неудачны")
|
||||
logger.error(f"Traceback последней ошибки: {traceback.format_exc()}")
|
||||
|
||||
logger.error(
|
||||
f"Traceback последней ошибки: {traceback.format_exc()}"
|
||||
)
|
||||
|
||||
# Если все попытки неудачны
|
||||
raise FileOperationError(f"Не удалось скачать и сохранить аудио после {max_retries} попыток. Последняя ошибка: {last_exception}")
|
||||
|
||||
raise FileOperationError(
|
||||
f"Не удалось скачать и сохранить аудио после {max_retries} попыток. Последняя ошибка: {last_exception}"
|
||||
)
|
||||
|
||||
@track_time("verify_file_exists", "audio_file_service")
|
||||
@track_errors("audio_file_service", "verify_file_exists")
|
||||
async def verify_file_exists(self, file_name: str) -> bool:
|
||||
"""Проверить существование и валидность файла"""
|
||||
try:
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
|
||||
file_path = f"{VOICE_USERS_DIR}/{file_name}.ogg"
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"Файл не существует: {file_path}")
|
||||
return False
|
||||
|
||||
|
||||
file_size = os.path.getsize(file_path)
|
||||
if file_size == 0:
|
||||
logger.warning(f"Файл пустой: {file_path}")
|
||||
return False
|
||||
|
||||
|
||||
if file_size < 100: # Минимальный размер для аудио файла
|
||||
logger.warning(f"Файл слишком маленький: {file_path}, размер: {file_size} bytes")
|
||||
logger.warning(
|
||||
f"Файл слишком маленький: {file_path}, размер: {file_size} bytes"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info(f"Файл проверен и валиден: {file_path}, размер: {file_size} bytes")
|
||||
|
||||
logger.info(
|
||||
f"Файл проверен и валиден: {file_path}, размер: {file_size} bytes"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке файла {file_name}: {e}")
|
||||
return False
|
||||
|
||||
@@ -18,31 +18,37 @@ def format_time_ago(date_from_db: str) -> Optional[str]:
|
||||
last_voice_time_timestamp = time.mktime(parse_date.timetuple())
|
||||
time_now_timestamp = time.time()
|
||||
date_difference = time_now_timestamp - last_voice_time_timestamp
|
||||
|
||||
|
||||
# Считаем минуты, часы, дни
|
||||
much_minutes_ago = round(date_difference / 60, 0)
|
||||
much_hour_ago = round(date_difference / 3600, 0)
|
||||
much_days_ago = int(round(much_hour_ago / 24, 0))
|
||||
|
||||
message_with_date = ''
|
||||
|
||||
message_with_date = ""
|
||||
if much_minutes_ago <= 60:
|
||||
word_minute = plural_time(1, much_minutes_ago)
|
||||
# Экранируем потенциально проблемные символы
|
||||
word_minute_escaped = html.escape(word_minute)
|
||||
message_with_date = f'<b>Последнее сообщение было записано {word_minute_escaped} назад</b>'
|
||||
message_with_date = (
|
||||
f"<b>Последнее сообщение было записано {word_minute_escaped} назад</b>"
|
||||
)
|
||||
elif much_minutes_ago > 60 and much_hour_ago <= 24:
|
||||
word_hour = plural_time(2, much_hour_ago)
|
||||
# Экранируем потенциально проблемные символы
|
||||
word_hour_escaped = html.escape(word_hour)
|
||||
message_with_date = f'<b>Последнее сообщение было записано {word_hour_escaped} назад</b>'
|
||||
message_with_date = (
|
||||
f"<b>Последнее сообщение было записано {word_hour_escaped} назад</b>"
|
||||
)
|
||||
elif much_hour_ago > 24:
|
||||
word_day = plural_time(3, much_days_ago)
|
||||
# Экранируем потенциально проблемные символы
|
||||
word_day_escaped = html.escape(word_day)
|
||||
message_with_date = f'<b>Последнее сообщение было записано {word_day_escaped} назад</b>'
|
||||
|
||||
message_with_date = (
|
||||
f"<b>Последнее сообщение было записано {word_day_escaped} назад</b>"
|
||||
)
|
||||
|
||||
return message_with_date
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при форматировании времени: {e}")
|
||||
return None
|
||||
@@ -52,11 +58,11 @@ def plural_time(type: int, n: float) -> str:
|
||||
"""Форматировать множественное число для времени"""
|
||||
word = []
|
||||
if type == 1:
|
||||
word = ['минуту', 'минуты', 'минут']
|
||||
word = ["минуту", "минуты", "минут"]
|
||||
elif type == 2:
|
||||
word = ['час', 'часа', 'часов']
|
||||
word = ["час", "часа", "часов"]
|
||||
elif type == 3:
|
||||
word = ['день', 'дня', 'дней']
|
||||
word = ["день", "дня", "дней"]
|
||||
else:
|
||||
return str(int(n))
|
||||
|
||||
@@ -66,9 +72,10 @@ def plural_time(type: int, n: float) -> str:
|
||||
p = 1
|
||||
else:
|
||||
p = 2
|
||||
|
||||
|
||||
new_number = int(n)
|
||||
return str(new_number) + ' ' + word[p]
|
||||
return str(new_number) + " " + word[p]
|
||||
|
||||
|
||||
@track_time("get_last_message_text", "voice_utils")
|
||||
@track_errors("voice_utils", "get_last_message_text")
|
||||
@@ -89,7 +96,8 @@ async def get_last_message_text(bot_db) -> Optional[str]:
|
||||
|
||||
async def validate_voice_message(message) -> bool:
|
||||
"""Проверить валидность голосового сообщения"""
|
||||
return message.content_type == 'voice'
|
||||
return message.content_type == "voice"
|
||||
|
||||
|
||||
@track_time("get_user_emoji_safe", "voice_utils")
|
||||
@track_errors("voice_utils", "get_user_emoji_safe")
|
||||
@@ -98,7 +106,11 @@ async def get_user_emoji_safe(bot_db, user_id: int) -> str:
|
||||
"""Безопасно получить эмодзи пользователя"""
|
||||
try:
|
||||
user_emoji = await bot_db.get_user_emoji(user_id)
|
||||
return user_emoji if user_emoji and user_emoji != "Смайл еще не определен" else "😊"
|
||||
return (
|
||||
user_emoji
|
||||
if user_emoji and user_emoji != "Смайл еще не определен"
|
||||
else "😊"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении эмодзи пользователя {user_id}: {e}")
|
||||
return "😊"
|
||||
|
||||
@@ -6,31 +6,44 @@ from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, MagicData, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.handlers.private.constants import BUTTON_TEXTS, FSM_STATES
|
||||
from helper_bot.handlers.voice.constants import *
|
||||
from helper_bot.handlers.voice.services import VoiceBotService
|
||||
from helper_bot.handlers.voice.utils import (get_last_message_text,
|
||||
get_user_emoji_safe,
|
||||
validate_voice_message)
|
||||
from helper_bot.handlers.voice.utils import (
|
||||
get_last_message_text,
|
||||
get_user_emoji_safe,
|
||||
validate_voice_message,
|
||||
)
|
||||
from helper_bot.keyboards import get_reply_keyboard
|
||||
from helper_bot.keyboards.keyboards import (get_main_keyboard,
|
||||
get_reply_keyboard_for_voice)
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
get_main_keyboard,
|
||||
get_reply_keyboard_for_voice,
|
||||
)
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import \
|
||||
DependenciesMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
from helper_bot.utils import messages
|
||||
from helper_bot.utils.helper_func import (check_user_emoji, get_first_name,
|
||||
send_voice_message, update_user_info)
|
||||
from helper_bot.utils.helper_func import (
|
||||
check_user_emoji,
|
||||
get_first_name,
|
||||
send_voice_message,
|
||||
update_user_info,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (db_query_time, track_errors,
|
||||
track_file_operations, track_time)
|
||||
from helper_bot.utils.metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_file_operations,
|
||||
track_time,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class VoiceHandlers:
|
||||
def __init__(self, db, settings):
|
||||
self.db = db.get_db() if hasattr(db, 'get_db') else db
|
||||
self.db = db.get_db() if hasattr(db, "get_db") else db
|
||||
self.settings = settings
|
||||
self.router = Router()
|
||||
self._setup_handlers()
|
||||
@@ -44,102 +57,114 @@ class VoiceHandlers:
|
||||
self.router.message.register(
|
||||
self.cancel_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "Отменить"
|
||||
F.text == "Отменить",
|
||||
)
|
||||
|
||||
|
||||
# Обработчик кнопки "Голосовой бот"
|
||||
self.router.message.register(
|
||||
self.voice_bot_button_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["VOICE_BOT"]
|
||||
F.text == BUTTON_TEXTS["VOICE_BOT"],
|
||||
)
|
||||
|
||||
|
||||
# Команды
|
||||
self.router.message.register(
|
||||
self.restart_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_RESTART)
|
||||
Command(CMD_RESTART),
|
||||
)
|
||||
|
||||
|
||||
self.router.message.register(
|
||||
self.handle_emoji_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_EMOJI)
|
||||
Command(CMD_EMOJI),
|
||||
)
|
||||
|
||||
|
||||
self.router.message.register(
|
||||
self.help_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_HELP)
|
||||
self.help_function, ChatTypeFilter(chat_type=["private"]), Command(CMD_HELP)
|
||||
)
|
||||
|
||||
|
||||
self.router.message.register(
|
||||
self.start,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_START)
|
||||
self.start, ChatTypeFilter(chat_type=["private"]), Command(CMD_START)
|
||||
)
|
||||
|
||||
|
||||
# Дополнительные команды
|
||||
self.router.message.register(
|
||||
self.refresh_listen_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_REFRESH)
|
||||
Command(CMD_REFRESH),
|
||||
)
|
||||
|
||||
|
||||
# Обработчики состояний и кнопок
|
||||
self.router.message.register(
|
||||
self.standup_write,
|
||||
StateFilter(STATE_START),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BTN_SPEAK
|
||||
F.text == BTN_SPEAK,
|
||||
)
|
||||
|
||||
|
||||
self.router.message.register(
|
||||
self.suggest_voice,
|
||||
StateFilter(STATE_STANDUP_WRITE),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
)
|
||||
|
||||
|
||||
self.router.message.register(
|
||||
self.standup_listen_audio,
|
||||
StateFilter(STATE_START),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BTN_LISTEN
|
||||
F.text == BTN_LISTEN,
|
||||
)
|
||||
|
||||
|
||||
# Новые обработчики кнопок
|
||||
self.router.message.register(
|
||||
self.refresh_listen_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "🔄Сбросить прослушивания"
|
||||
F.text == "🔄Сбросить прослушивания",
|
||||
)
|
||||
|
||||
|
||||
self.router.message.register(
|
||||
self.handle_emoji_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "😊Узнать эмодзи"
|
||||
F.text == "😊Узнать эмодзи",
|
||||
)
|
||||
|
||||
@track_time("voice_bot_button_handler", "voice_handlers")
|
||||
@track_errors("voice_handlers", "voice_bot_button_handler")
|
||||
async def voice_bot_button_handler(self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings")):
|
||||
async def voice_bot_button_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Обработчик кнопки 'Голосовой бот' из основной клавиатуры"""
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) нажал кнопку 'Голосовой бот'")
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) нажал кнопку 'Голосовой бот'"
|
||||
)
|
||||
try:
|
||||
# Проверяем, получал ли пользователь приветственное сообщение
|
||||
welcome_received = await bot_db.check_voice_bot_welcome_received(message.from_user.id)
|
||||
logger.info(f"Пользователь {message.from_user.id}: welcome_received = {welcome_received}")
|
||||
|
||||
welcome_received = await bot_db.check_voice_bot_welcome_received(
|
||||
message.from_user.id
|
||||
)
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id}: welcome_received = {welcome_received}"
|
||||
)
|
||||
|
||||
if welcome_received:
|
||||
# Если уже получал приветствие, вызываем restart_function
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызываем restart_function")
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id}: вызываем restart_function"
|
||||
)
|
||||
await self.restart_function(message, state, bot_db, settings)
|
||||
else:
|
||||
# Если не получал, вызываем start
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызываем start")
|
||||
await self.start(message, state, bot_db, settings)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке приветственного сообщения для пользователя {message.from_user.id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при проверке приветственного сообщения для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
# В случае ошибки вызываем start
|
||||
await self.start(message, state, bot_db, settings)
|
||||
|
||||
@@ -147,49 +172,49 @@ class VoiceHandlers:
|
||||
@track_errors("voice_handlers", "restart_function")
|
||||
async def restart_function(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызывается функция restart_function")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id}: вызывается функция restart_function"
|
||||
)
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
await check_user_emoji(message)
|
||||
markup = get_main_keyboard()
|
||||
await message.answer(text='🎤 Записывайся или слушай!', reply_markup=markup)
|
||||
await message.answer(text="🎤 Записывайся или слушай!", reply_markup=markup)
|
||||
await state.set_state(STATE_START)
|
||||
|
||||
@track_time("handle_emoji_message", "voice_handlers")
|
||||
@track_errors("voice_handlers", "handle_emoji_message")
|
||||
async def handle_emoji_message(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: MagicData("settings")
|
||||
self, message: types.Message, state: FSMContext, settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил информацию об эмодзи")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил информацию об эмодзи"
|
||||
)
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
user_emoji = await check_user_emoji(message)
|
||||
await state.set_state(STATE_START)
|
||||
if user_emoji is not None:
|
||||
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
|
||||
await message.answer(f"Твоя эмодзя - {user_emoji}", parse_mode="HTML")
|
||||
|
||||
@track_time("help_function", "voice_handlers")
|
||||
@track_errors("voice_handlers", "help_function")
|
||||
async def help_function(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: MagicData("settings")
|
||||
self, message: types.Message, state: FSMContext, settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию help_function")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию help_function"
|
||||
)
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
help_message = messages.get_message(get_first_name(message), 'HELP_MESSAGE')
|
||||
help_message = messages.get_message(get_first_name(message), "HELP_MESSAGE")
|
||||
await message.answer(
|
||||
text=help_message,
|
||||
disable_web_page_preview=not settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await state.set_state(STATE_START)
|
||||
|
||||
@@ -198,43 +223,53 @@ class VoiceHandlers:
|
||||
@db_query_time("mark_voice_bot_welcome_received", "audio_moderate", "update")
|
||||
async def start(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}): вызывается функция start")
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}): вызывается функция start"
|
||||
)
|
||||
await state.set_state(STATE_START)
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
user_emoji = await get_user_emoji_safe(bot_db, message.from_user.id)
|
||||
|
||||
|
||||
# Создаем сервис и отправляем приветственные сообщения
|
||||
voice_service = VoiceBotService(bot_db, settings)
|
||||
await voice_service.send_welcome_messages(message, user_emoji)
|
||||
logger.info(f"Приветственные сообщения отправлены пользователю {message.from_user.id}")
|
||||
|
||||
logger.info(
|
||||
f"Приветственные сообщения отправлены пользователю {message.from_user.id}"
|
||||
)
|
||||
|
||||
# Отмечаем, что пользователь получил приветственное сообщение
|
||||
try:
|
||||
await bot_db.mark_voice_bot_welcome_received(message.from_user.id)
|
||||
logger.info(f"Пользователь {message.from_user.id}: отмечен как получивший приветствие")
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id}: отмечен как получивший приветствие"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отметке получения приветствия для пользователя {message.from_user.id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при отметке получения приветствия для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
|
||||
@track_time("cancel_handler", "voice_handlers")
|
||||
@track_errors("voice_handlers", "cancel_handler")
|
||||
async def cancel_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Обработчик кнопки 'Отменить' - возвращает в начальное состояние"""
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await message.answer(text='Добро пожаловать в меню!', reply_markup=markup, parse_mode='HTML')
|
||||
await message.answer(
|
||||
text="Добро пожаловать в меню!", reply_markup=markup, parse_mode="HTML"
|
||||
)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
logger.info(f"Пользователь {message.from_user.id} возвращен в главное меню")
|
||||
|
||||
@@ -242,208 +277,253 @@ class VoiceHandlers:
|
||||
@track_errors("voice_handlers", "refresh_listen_function")
|
||||
async def refresh_listen_function(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию refresh_listen_function")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию refresh_listen_function"
|
||||
)
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
markup = get_main_keyboard()
|
||||
|
||||
|
||||
# Очищаем прослушивания через сервис
|
||||
voice_service = VoiceBotService(bot_db, settings)
|
||||
await voice_service.clear_user_listenings(message.from_user.id)
|
||||
|
||||
listenings_cleared_message = messages.get_message(get_first_name(message), 'LISTENINGS_CLEARED_MESSAGE')
|
||||
listenings_cleared_message = messages.get_message(
|
||||
get_first_name(message), "LISTENINGS_CLEARED_MESSAGE"
|
||||
)
|
||||
await message.answer(
|
||||
text=listenings_cleared_message,
|
||||
disable_web_page_preview=not settings['Telegram']['preview_link'],
|
||||
reply_markup=markup
|
||||
disable_web_page_preview=not settings["Telegram"]["preview_link"],
|
||||
reply_markup=markup,
|
||||
)
|
||||
await state.set_state(STATE_START)
|
||||
|
||||
|
||||
@track_time("standup_write", "voice_handlers")
|
||||
@track_errors("voice_handlers", "standup_write")
|
||||
async def standup_write(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию standup_write")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию standup_write"
|
||||
)
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
markup = types.ReplyKeyboardRemove()
|
||||
record_voice_message = messages.get_message(get_first_name(message), 'RECORD_VOICE_MESSAGE')
|
||||
record_voice_message = messages.get_message(
|
||||
get_first_name(message), "RECORD_VOICE_MESSAGE"
|
||||
)
|
||||
await message.answer(text=record_voice_message, reply_markup=markup)
|
||||
|
||||
|
||||
try:
|
||||
message_with_date = await get_last_message_text(bot_db)
|
||||
if message_with_date:
|
||||
await message.answer(text=message_with_date, parse_mode="html")
|
||||
except Exception as e:
|
||||
logger.error(f'Не удалось получить дату последнего сообщения для пользователя {message.from_user.id}: {e}')
|
||||
|
||||
await state.set_state(STATE_STANDUP_WRITE)
|
||||
logger.error(
|
||||
f"Не удалось получить дату последнего сообщения для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
|
||||
await state.set_state(STATE_STANDUP_WRITE)
|
||||
|
||||
@track_time("suggest_voice", "voice_handlers")
|
||||
@track_errors("voice_handlers", "suggest_voice")
|
||||
async def suggest_voice(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(
|
||||
f"Вызов функции suggest_voice. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}"
|
||||
)
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
markup = get_main_keyboard()
|
||||
|
||||
|
||||
if await validate_voice_message(message):
|
||||
markup_for_voice = get_reply_keyboard_for_voice()
|
||||
|
||||
|
||||
# Отправляем аудио в приватный канал
|
||||
sent_message = await send_voice_message(
|
||||
settings['Telegram']['group_for_posts'],
|
||||
settings["Telegram"]["group_for_posts"],
|
||||
message,
|
||||
message.voice.file_id,
|
||||
markup_for_voice
|
||||
message.voice.file_id,
|
||||
markup_for_voice,
|
||||
)
|
||||
logger.info(
|
||||
f"Голосовое сообщение пользователя {message.from_user.id} отправлено в группу постов (message_id: {sent_message.message_id})"
|
||||
)
|
||||
logger.info(f"Голосовое сообщение пользователя {message.from_user.id} отправлено в группу постов (message_id: {sent_message.message_id})")
|
||||
|
||||
# Сохраняем в базу инфо о посте
|
||||
await bot_db.set_user_id_and_message_id_for_voice_bot(sent_message.message_id, message.from_user.id)
|
||||
await bot_db.set_user_id_and_message_id_for_voice_bot(
|
||||
sent_message.message_id, message.from_user.id
|
||||
)
|
||||
|
||||
# Отправляем юзеру ответ и возвращаем его в меню
|
||||
voice_saved_message = messages.get_message(get_first_name(message), 'VOICE_SAVED_MESSAGE')
|
||||
voice_saved_message = messages.get_message(
|
||||
get_first_name(message), "VOICE_SAVED_MESSAGE"
|
||||
)
|
||||
await message.answer(text=voice_saved_message, reply_markup=markup)
|
||||
await state.set_state(STATE_START)
|
||||
else:
|
||||
logger.warning(f"Голосовое сообщение пользователя {message.from_user.id} не прошло валидацию")
|
||||
unknown_content_message = messages.get_message(get_first_name(message), 'UNKNOWN_CONTENT_MESSAGE')
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
logger.warning(
|
||||
f"Голосовое сообщение пользователя {message.from_user.id} не прошло валидацию"
|
||||
)
|
||||
unknown_content_message = messages.get_message(
|
||||
get_first_name(message), "UNKNOWN_CONTENT_MESSAGE"
|
||||
)
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
await message.answer(text=unknown_content_message, reply_markup=markup)
|
||||
await state.set_state(STATE_STANDUP_WRITE)
|
||||
|
||||
|
||||
@track_time("standup_listen_audio", "voice_handlers")
|
||||
@track_errors("voice_handlers", "standup_listen_audio")
|
||||
@track_file_operations("voice")
|
||||
@db_query_time("standup_listen_audio", "audio_moderate", "mixed")
|
||||
async def standup_listen_audio(
|
||||
self,
|
||||
message: types.Message,
|
||||
message: types.Message,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил прослушивание аудио")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил прослушивание аудио"
|
||||
)
|
||||
markup = get_main_keyboard()
|
||||
|
||||
|
||||
# Создаем сервис для работы с аудио
|
||||
voice_service = VoiceBotService(bot_db, settings)
|
||||
|
||||
|
||||
try:
|
||||
#TODO: удалить логику из хендлера
|
||||
# TODO: удалить логику из хендлера
|
||||
# Получаем случайное аудио
|
||||
audio_data = await voice_service.get_random_audio(message.from_user.id)
|
||||
|
||||
|
||||
if not audio_data:
|
||||
logger.warning(f"Для пользователя {message.from_user.id} не найдено доступных аудио для прослушивания")
|
||||
no_audio_message = messages.get_message(get_first_name(message), 'NO_AUDIO_MESSAGE')
|
||||
logger.warning(
|
||||
f"Для пользователя {message.from_user.id} не найдено доступных аудио для прослушивания"
|
||||
)
|
||||
no_audio_message = messages.get_message(
|
||||
get_first_name(message), "NO_AUDIO_MESSAGE"
|
||||
)
|
||||
await message.answer(text=no_audio_message, reply_markup=markup)
|
||||
try:
|
||||
message_with_date = await get_last_message_text(bot_db)
|
||||
if message_with_date:
|
||||
await message.answer(text=message_with_date, parse_mode="html")
|
||||
except Exception as e:
|
||||
logger.error(f'Не удалось получить последнюю дату для пользователя {message.from_user.id}: {e}')
|
||||
logger.error(
|
||||
f"Не удалось получить последнюю дату для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
audio_for_user, date_added, user_emoji = audio_data
|
||||
|
||||
|
||||
# Получаем путь к файлу
|
||||
path = Path(f'{VOICE_USERS_DIR}/{audio_for_user}.ogg')
|
||||
|
||||
path = Path(f"{VOICE_USERS_DIR}/{audio_for_user}.ogg")
|
||||
|
||||
# Проверяем существование файла
|
||||
if not path.exists():
|
||||
logger.error(f"Файл не найден: {path} для пользователя {message.from_user.id}")
|
||||
logger.error(
|
||||
f"Файл не найден: {path} для пользователя {message.from_user.id}"
|
||||
)
|
||||
# Дополнительная диагностика
|
||||
logger.error(f"Директория {VOICE_USERS_DIR} существует: {Path(VOICE_USERS_DIR).exists()}")
|
||||
logger.error(
|
||||
f"Директория {VOICE_USERS_DIR} существует: {Path(VOICE_USERS_DIR).exists()}"
|
||||
)
|
||||
if Path(VOICE_USERS_DIR).exists():
|
||||
files_in_dir = list(Path(VOICE_USERS_DIR).glob("*.ogg"))
|
||||
logger.error(f"Файлы в директории: {[f.name for f in files_in_dir]}")
|
||||
|
||||
logger.error(
|
||||
f"Файлы в директории: {[f.name for f in files_in_dir]}"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
text="Файл аудио не найден. Обратитесь к администратору.",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
# Проверяем размер файла
|
||||
if path.stat().st_size == 0:
|
||||
logger.error(f"Файл пустой: {path} для пользователя {message.from_user.id}")
|
||||
logger.error(
|
||||
f"Файл пустой: {path} для пользователя {message.from_user.id}"
|
||||
)
|
||||
await message.answer(
|
||||
text="Файл аудио поврежден. Обратитесь к администратору.",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
voice = FSInputFile(path)
|
||||
|
||||
# Формируем подпись
|
||||
if user_emoji:
|
||||
caption = f'{user_emoji}\nДата записи: {date_added}'
|
||||
caption = f"{user_emoji}\nДата записи: {date_added}"
|
||||
else:
|
||||
caption = f'Дата записи: {date_added}'
|
||||
|
||||
logger.info(f"Подготовлено голосовое сообщение для пользователя {message.from_user.id}: {caption}")
|
||||
|
||||
caption = f"Дата записи: {date_added}"
|
||||
|
||||
logger.info(
|
||||
f"Подготовлено голосовое сообщение для пользователя {message.from_user.id}: {caption}"
|
||||
)
|
||||
|
||||
try:
|
||||
from helper_bot.utils.rate_limiter import send_with_rate_limit
|
||||
|
||||
|
||||
async def _send_voice():
|
||||
return await message.bot.send_voice(
|
||||
chat_id=message.chat.id,
|
||||
voice=voice,
|
||||
caption=caption,
|
||||
reply_markup=markup
|
||||
chat_id=message.chat.id,
|
||||
voice=voice,
|
||||
caption=caption,
|
||||
reply_markup=markup,
|
||||
)
|
||||
|
||||
|
||||
await send_with_rate_limit(_send_voice, message.chat.id)
|
||||
|
||||
|
||||
# Маркируем сообщение как прослушанное только после успешной отправки
|
||||
await voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id)
|
||||
|
||||
# Получаем количество оставшихся аудио только после успешной отправки
|
||||
remaining_count = await voice_service.get_remaining_audio_count(message.from_user.id)
|
||||
await message.answer(
|
||||
text=f'Осталось непрослушанных: <b>{remaining_count}</b>',
|
||||
reply_markup=markup
|
||||
await voice_service.mark_audio_as_listened(
|
||||
audio_for_user, message.from_user.id
|
||||
)
|
||||
|
||||
|
||||
# Получаем количество оставшихся аудио только после успешной отправки
|
||||
remaining_count = await voice_service.get_remaining_audio_count(
|
||||
message.from_user.id
|
||||
)
|
||||
await message.answer(
|
||||
text=f"Осталось непрослушанных: <b>{remaining_count}</b>",
|
||||
reply_markup=markup,
|
||||
)
|
||||
|
||||
except Exception as voice_error:
|
||||
if "VOICE_MESSAGES_FORBIDDEN" in str(voice_error):
|
||||
# Если голосовые сообщения запрещены, отправляем информативное сообщение
|
||||
logger.warning(f"Пользователь {message.from_user.id} запретил получение голосовых сообщений")
|
||||
|
||||
logger.warning(
|
||||
f"Пользователь {message.from_user.id} запретил получение голосовых сообщений"
|
||||
)
|
||||
|
||||
privacy_message = "🔇 К сожалению, у тебя закрыт доступ к получению голосовых сообщений.\n\nДля продолжения взаимодействия с ботом необходимо дать возможность мне присылать войсы в настройках приватности Telegram.\n\n💡 Как это сделать:\n1. Открой настройки Telegram\n2. Перейди в 'Конфиденциальность и безопасность'\n3. Выбери 'Голосовые сообщения'\n4. Разреши получение от 'Всех' или добавь меня в исключения"
|
||||
|
||||
|
||||
await message.answer(text=privacy_message, reply_markup=markup)
|
||||
return # Выходим без записи о прослушивании
|
||||
|
||||
|
||||
else:
|
||||
logger.error(f"Ошибка при отправке голосового сообщения пользователю {message.from_user.id}: {voice_error}")
|
||||
logger.error(
|
||||
f"Ошибка при отправке голосового сообщения пользователю {message.from_user.id}: {voice_error}"
|
||||
)
|
||||
raise voice_error
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при прослушивании аудио для пользователя {message.from_user.id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при прослушивании аудио для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
await message.answer(
|
||||
text="Произошла ошибка при получении аудио. Попробуйте позже.",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user