Enhance bot functionality and refactor database interactions

- Added `ca-certificates` installation to Dockerfile for improved network security.
- Updated health check command in Dockerfile to include better timeout handling.
- Refactored `run_helper.py` to implement proper signal handling and logging during shutdown.
- Transitioned database operations to an asynchronous model in `async_db.py`, improving performance and responsiveness.
- Updated database schema to support new foreign key relationships and optimized indexing for better query performance.
- Enhanced various bot handlers to utilize async database methods, improving overall efficiency and user experience.
- Removed obsolete database and fix scripts to streamline the project structure.
This commit is contained in:
2025-09-02 18:22:02 +03:00
parent 013892dcb7
commit 1c6a37bc12
59 changed files with 5682 additions and 4204 deletions

View File

@@ -40,7 +40,8 @@ admin_router.message.middleware(AdminAccessMiddleware())
)
async def admin_panel(
message: types.Message,
state: FSMContext
state: FSMContext,
**kwargs
):
"""Главное меню администратора"""
try:
@@ -66,11 +67,11 @@ async def get_last_users(
try:
logger.info(f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}")
admin_service = AdminService(bot_db)
users = admin_service.get_last_users()
users = await admin_service.get_last_users()
# Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination)
users_data = [
(user.full_name, user.username) # (full_name, username) - формат кортежей
(user.full_name, user.user_id)
for user in users
]
@@ -97,7 +98,7 @@ async def get_banned_users(
try:
logger.info(f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}")
admin_service = AdminService(bot_db)
message_text, buttons_list = admin_service.get_banned_users_for_display(0)
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')
@@ -120,6 +121,7 @@ async def get_banned_users(
async def start_ban_process(
message: types.Message,
state: FSMContext,
**kwargs
):
"""Начало процесса блокировки пользователя"""
try:
@@ -151,15 +153,15 @@ async def process_ban_target(
# Определяем пользователя
if ban_type == "username":
user = admin_service.get_user_by_username(message.text)
user = await admin_service.get_user_by_username(message.text)
if not user:
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
await return_to_admin_menu(message, state)
return
else: # ban_type == "id"
try:
user_id = admin_service.validate_user_input(message.text)
user = admin_service.get_user_by_id(user_id)
user_id = await admin_service.validate_user_input(message.text)
user = await admin_service.get_user_by_id(user_id)
if not user:
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
await return_to_admin_menu(message, state)
@@ -195,7 +197,8 @@ async def process_ban_target(
)
async def process_ban_reason(
message: types.Message,
state: FSMContext
state: FSMContext,
**kwargs
):
"""Обработка причины блокировки"""
try:
@@ -218,6 +221,7 @@ async def process_ban_reason(
async def process_ban_duration(
message: types.Message,
state: FSMContext,
**kwargs
):
"""Обработка срока блокировки"""
try:
@@ -260,7 +264,8 @@ async def process_ban_duration(
async def confirm_ban(
message: types.Message,
state: FSMContext,
bot_db: MagicData("bot_db")
bot_db: MagicData("bot_db"),
**kwargs
):
"""Подтверждение блокировки пользователя"""
try:
@@ -269,7 +274,7 @@ async def confirm_ban(
# Выполняем блокировку
admin_service.ban_user(
await admin_service.ban_user(
user_id=user_data['target_user_id'],
username=user_data['target_username'],
reason=user_data['ban_reason'],
@@ -298,7 +303,8 @@ async def confirm_ban(
)
async def cancel_ban_process(
message: types.Message,
state: FSMContext
state: FSMContext,
**kwargs
):
"""Отмена процесса блокировки"""
try:
@@ -312,7 +318,8 @@ async def cancel_ban_process(
@admin_router.message(Command("test_metrics"))
async def test_metrics_handler(
message: types.Message,
bot_db: MagicData("bot_db")
bot_db: MagicData("bot_db"),
**kwargs
):
"""Тестовый хендлер для проверки метрик"""
from helper_bot.utils.metrics import metrics
@@ -325,18 +332,23 @@ async def test_metrics_handler(
# Проверяем активных пользователей
if hasattr(bot_db, 'connect') and hasattr(bot_db, 'cursor'):
# Используем UNIX timestamp для сравнения с date_changed
import time
current_timestamp = int(time.time())
one_day_ago = current_timestamp - (24 * 60 * 60) # 24 часа назад
active_users_query = """
SELECT COUNT(DISTINCT user_id) as active_users
FROM our_users
WHERE date_changed > datetime('now', '-1 day')
WHERE date_changed > ?
"""
try:
bot_db.connect()
bot_db.cursor.execute(active_users_query)
result = bot_db.cursor.fetchone()
await bot_db.connect()
await bot_db.cursor.execute(active_users_query, (one_day_ago,))
result = await bot_db.cursor.fetchone()
active_users = result[0] if result else 0
finally:
bot_db.close()
await bot_db.close()
else:
active_users = "N/A"

View File

@@ -17,6 +17,9 @@ class AdminAccessMiddleware(BaseMiddleware):
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})")
# Получаем bot_db из data (внедренного DependenciesMiddleware)
bot_db = data.get('bot_db')
@@ -25,7 +28,11 @@ class AdminAccessMiddleware(BaseMiddleware):
bdf = get_global_instance()
bot_db = bdf.get_db()
if not check_access(user_id, bot_db):
is_admin_result = await check_access(user_id, bot_db)
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('Доступ запрещен!')
return

View File

@@ -29,10 +29,10 @@ class AdminService:
def __init__(self, bot_db):
self.bot_db = bot_db
def get_last_users(self) -> List[User]:
async def get_last_users(self) -> List[User]:
"""Получить список последних пользователей"""
try:
users_data = self.bot_db.get_last_users_from_db()
users_data = await self.bot_db.get_last_users(30)
return [
User(
user_id=user[1],
@@ -45,31 +45,37 @@ class AdminService:
logger.error(f"Ошибка при получении списка последних пользователей: {e}")
raise
def get_banned_users(self) -> List[BannedUser]:
async def get_banned_users(self) -> List[BannedUser]:
"""Получить список заблокированных пользователей"""
try:
banned_users_data = self.bot_db.get_banned_users_from_db()
return [
BannedUser(
user_id=user[1], # user_id
username=user[0], # user_name
reason=user[2], # message_for_user
unban_date=user[3] # date_to_unban
)
for user in banned_users_data
]
banned_users_data = await self.bot_db.get_banned_users_from_db()
banned_users = []
for user_data in banned_users_data:
user_id, reason, unban_date = user_data
# Получаем username и full_name из таблицы users
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
))
return banned_users
except Exception as e:
logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}")
raise
def get_user_by_username(self, username: str) -> Optional[User]:
async def get_user_by_username(self, username: str) -> Optional[User]:
"""Получить пользователя по username"""
try:
user_id = self.bot_db.get_user_id_by_username(username)
user_id = await self.bot_db.get_user_id_by_username(username)
if not user_id:
return None
full_name = self.bot_db.get_full_name_by_id(user_id)
full_name = await self.bot_db.get_full_name_by_id(user_id)
return User(
user_id=user_id,
username=username,
@@ -79,27 +85,27 @@ class AdminService:
logger.error(f"Ошибка при поиске пользователя по username {username}: {e}")
raise
def get_user_by_id(self, user_id: int) -> Optional[User]:
async def get_user_by_id(self, user_id: int) -> Optional[User]:
"""Получить пользователя по ID"""
try:
user_info = self.bot_db.get_user_info_by_id(user_id)
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.get('username', 'Неизвестно'),
full_name=user_info.get('full_name', 'Неизвестно')
username=user_info.username or 'Неизвестно',
full_name=user_info.full_name or 'Неизвестно'
)
except Exception as e:
logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}")
raise
def ban_user(self, user_id: int, username: str, reason: str, ban_days: Optional[int]) -> None:
async def ban_user(self, user_id: int, username: str, reason: str, ban_days: Optional[int]) -> None:
"""Заблокировать пользователя"""
try:
# Проверяем, не заблокирован ли уже пользователь
if self.bot_db.check_user_in_blacklist(user_id):
if await self.bot_db.check_user_in_blacklist(user_id):
raise UserAlreadyBannedError(f"Пользователь {user_id} уже заблокирован")
# Рассчитываем дату разблокировки
@@ -107,8 +113,8 @@ class AdminService:
if ban_days is not None:
date_to_unban = add_days_to_date(ban_days)
# Сохраняем в БД
self.bot_db.set_user_blacklist(user_id, username, reason, date_to_unban)
# Сохраняем в БД (username больше не передается, так как не используется в новой схеме)
await self.bot_db.set_user_blacklist(user_id, None, reason, date_to_unban)
logger.info(f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней")
@@ -116,16 +122,16 @@ class AdminService:
logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}")
raise
def unban_user(self, user_id: int) -> None:
async def unban_user(self, user_id: int) -> None:
"""Разблокировать пользователя"""
try:
self.bot_db.delete_user_blacklist(user_id)
await self.bot_db.delete_user_blacklist(user_id)
logger.info(f"Пользователь {user_id} разблокирован")
except Exception as e:
logger.error(f"Ошибка при разблокировке пользователя {user_id}: {e}")
raise
def validate_user_input(self, input_text: str) -> int:
async def validate_user_input(self, input_text: str) -> int:
"""Валидация введенного ID пользователя"""
try:
user_id = int(input_text.strip())
@@ -135,11 +141,12 @@ class AdminService:
except ValueError:
raise InvalidInputError("ID пользователя должен быть числом")
def get_banned_users_for_display(self, page: int = 0) -> tuple[str, list]:
async def get_banned_users_for_display(self, page: int = 0) -> tuple[str, list]:
"""Получить данные заблокированных пользователей для отображения"""
try:
message_text = get_banned_users_list(page, self.bot_db)
buttons_list = get_banned_users_buttons(self.bot_db)
message_text = await get_banned_users_list(page, self.bot_db)
buttons_list = await get_banned_users_buttons(self.bot_db)
return message_text, buttons_list
except Exception as e:
logger.error(f"Ошибка при получении данных заблокированных пользователей: {e}")

View File

@@ -1,22 +1,15 @@
import html
import traceback
import time
from datetime import datetime
from aiogram import Router, F
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
from aiogram.filters import MagicData
from helper_bot.handlers.voice.constants import CALLBACK_SAVE, CALLBACK_DELETE
from helper_bot.handlers.voice.services import AudioFileService
from logs.custom_logger import logger
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from aiogram import F
from aiogram.filters import MagicData
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
create_keyboard_for_ban_reason
from helper_bot.utils.helper_func import get_banned_users_list, get_banned_users_buttons
@@ -96,7 +89,7 @@ async def decline_post_for_group(
@callback_router.callback_query(F.data == CALLBACK_BAN)
async def ban_user_from_post(call: CallbackQuery):
async def ban_user_from_post(call: CallbackQuery, **kwargs):
ban_service = get_ban_service()
# TODO: переделать на MagicData
try:
@@ -116,21 +109,29 @@ async def ban_user_from_post(call: CallbackQuery):
@callback_router.callback_query(F.data.contains(CALLBACK_BAN))
async def process_ban_user(call: CallbackQuery, state: FSMContext):
async def process_ban_user(call: CallbackQuery, state: FSMContext, **kwargs):
ban_service = get_ban_service()
# TODO: переделать на MagicData
user_id = call.data[4:]
logger.info(f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}")
# Проверяем, что user_id является валидным числом
try:
user_name = await ban_service.ban_user(user_id, "")
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None, date_to_unban=None)
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)
return
try:
user_name = await ban_service.ban_user(str(user_id_int), "")
await state.update_data(user_id=user_id_int, user_name=user_name, message_for_user=None, date_to_unban=None)
markup = create_keyboard_for_ban_reason()
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(call.message.from_user.full_name))
await call.message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
text=f"<b>Выбран пользователь:\nid:</b> {user_id_int}\n<b>username:</b> {user_name_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup
)
await state.set_state('BAN_2')
@@ -141,13 +142,21 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext):
@callback_router.callback_query(F.data.contains(CALLBACK_UNLOCK))
async def process_unlock_user(call: CallbackQuery):
async def process_unlock_user(call: CallbackQuery, **kwargs):
ban_service = get_ban_service()
# TODO: переделать на MagicData
user_id = call.data[7:]
# Проверяем, что user_id является валидным числом
try:
username = await ban_service.unlock_user(user_id)
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)
return
try:
username = await ban_service.unlock_user(str(user_id_int))
await call.answer(f'{MESSAGE_USER_UNLOCKED} {username}', show_alert=True)
except UserNotFoundError:
await call.answer(text='Пользователь не найден в базе', show_alert=True, cache_time=3)
@@ -157,7 +166,7 @@ async def process_unlock_user(call: CallbackQuery):
@callback_router.callback_query(F.data == CALLBACK_RETURN)
async def return_to_main_menu(call: CallbackQuery):
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()
@@ -167,14 +176,21 @@ async def return_to_main_menu(call: CallbackQuery):
@callback_router.callback_query(F.data.contains(CALLBACK_PAGE))
async def change_page(
call: CallbackQuery,
bot_db: MagicData("bot_db")
bot_db: MagicData("bot_db"),
**kwargs
):
page_number = int(call.data[5:])
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)
return
logger.info(f"Переход на страницу {page_number}")
if call.message.text == 'Список пользователей которые последними обращались к боту':
list_users = bot_db.get_last_users_from_db()
keyboard = create_keyboard_with_pagination(int(page_number), len(list_users), list_users, 'ban')
list_users = await bot_db.get_last_users(30)
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,
message_id=call.message.message_id,
@@ -189,7 +205,7 @@ async def change_page(
)
buttons = get_banned_users_buttons(bot_db)
keyboard = create_keyboard_with_pagination(int(call.data[5:]), 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,
message_id=call.message.message_id,
@@ -201,27 +217,31 @@ async def change_page(
async def save_voice_message(
call: CallbackQuery,
bot_db: MagicData("bot_db"),
settings: MagicData("settings")
settings: MagicData("settings"),
**kwargs
):
try:
# Создаем сервис для работы с аудио файлами
audio_service = AudioFileService(bot_db)
# Получаем ID пользователя из базы
user_id = 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)
# Генерируем имя файла
file_name = audio_service.generate_file_name(user_id)
file_name = await audio_service.generate_file_name(user_id)
# Собираем инфо о сообщении
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 ""
# Сохраняем в базу данных
audio_service.save_audio_file(file_name, user_id, date_added)
await audio_service.save_audio_file(file_name, user_id, date_added, file_id)
# Скачиваем и сохраняем файл
await audio_service.download_and_save_audio(call.bot, call.message.message_id, file_name)
await audio_service.download_and_save_audio(call.bot, call.message, file_name)
# Удаляем сообщение из предложки
await call.bot.delete_message(
@@ -240,7 +260,8 @@ async def save_voice_message(
async def delete_voice_message(
call: CallbackQuery,
bot_db: MagicData("bot_db"),
settings: MagicData("settings")
settings: MagicData("settings"),
**kwargs
):
try:
# Удаляем сообщение из предложки

View File

@@ -10,24 +10,16 @@ from .services import PostPublishService, BanService
def get_post_publish_service() -> PostPublishService:
"""Фабрика для PostPublishService"""
bdf = get_global_instance()
bot = Bot(
token=bdf.settings['Telegram']['bot_token'],
default=DefaultBotProperties(parse_mode='HTML'),
timeout=30.0
)
db = bdf.get_db()
settings = bdf.settings
return PostPublishService(bot, db, settings)
return PostPublishService(None, db, settings)
def get_ban_service() -> BanService:
"""Фабрика для BanService"""
bdf = get_global_instance()
bot = Bot(
token=bdf.settings['Telegram']['bot_token'],
default=DefaultBotProperties(parse_mode='HTML'),
timeout=30.0
)
db = bdf.get_db()
settings = bdf.settings
return BanService(bot, db, settings)
return BanService(None, db, settings)

View File

@@ -26,12 +26,19 @@ from logs.custom_logger import logger
class PostPublishService:
def __init__(self, bot: Bot, db, settings: Dict[str, Any]):
# bot может быть None - в этом случае используем бота из контекста сообщения
self.bot = bot
self.db = db
self.settings = settings
self.group_for_posts = settings['Telegram']['group_for_posts']
self.main_public = settings['Telegram']['main_public']
self.important_logs = settings['Telegram']['important_logs']
def _get_bot(self, message) -> Bot:
"""Получает бота из контекста сообщения или использует переданного"""
if self.bot:
return self.bot
return message.bot
async def publish_post(self, call: CallbackQuery) -> None:
"""Основной метод публикации поста"""
@@ -57,7 +64,7 @@ class PostPublishService:
async def _publish_text_post(self, call: CallbackQuery) -> None:
"""Публикация текстового поста"""
text_post = html.escape(str(call.message.text))
author_id = self._get_author_id(call.message.message_id)
author_id = await self._get_author_id(call.message.message_id)
await send_text_message(self.main_public, call.message, text_post)
await self._delete_post_and_notify_author(call, author_id)
@@ -66,7 +73,7 @@ class PostPublishService:
async def _publish_photo_post(self, call: CallbackQuery) -> None:
"""Публикация поста с фото"""
text_post_with_photo = html.escape(str(call.message.caption))
author_id = self._get_author_id(call.message.message_id)
author_id = await self._get_author_id(call.message.message_id)
await send_photo_message(self.main_public, call.message, call.message.photo[-1].file_id, text_post_with_photo)
await self._delete_post_and_notify_author(call, author_id)
@@ -75,7 +82,7 @@ class PostPublishService:
async def _publish_video_post(self, call: CallbackQuery) -> None:
"""Публикация поста с видео"""
text_post_with_photo = html.escape(str(call.message.caption))
author_id = self._get_author_id(call.message.message_id)
author_id = await self._get_author_id(call.message.message_id)
await send_video_message(self.main_public, call.message, call.message.video.file_id, text_post_with_photo)
await self._delete_post_and_notify_author(call, author_id)
@@ -83,7 +90,7 @@ class PostPublishService:
async def _publish_video_note_post(self, call: CallbackQuery) -> None:
"""Публикация поста с кружком"""
author_id = self._get_author_id(call.message.message_id)
author_id = await self._get_author_id(call.message.message_id)
await send_video_note_message(self.main_public, call.message, call.message.video_note.file_id)
await self._delete_post_and_notify_author(call, author_id)
@@ -92,7 +99,7 @@ class PostPublishService:
async def _publish_audio_post(self, call: CallbackQuery) -> None:
"""Публикация поста с аудио"""
text_post_with_photo = html.escape(str(call.message.caption))
author_id = self._get_author_id(call.message.message_id)
author_id = await self._get_author_id(call.message.message_id)
await send_audio_message(self.main_public, call.message, call.message.audio.file_id, text_post_with_photo)
await self._delete_post_and_notify_author(call, author_id)
@@ -100,7 +107,7 @@ class PostPublishService:
async def _publish_voice_post(self, call: CallbackQuery) -> None:
"""Публикация поста с войсом"""
author_id = self._get_author_id(call.message.message_id)
author_id = await self._get_author_id(call.message.message_id)
await send_voice_message(self.main_public, call.message, call.message.voice.file_id)
await self._delete_post_and_notify_author(call, author_id)
@@ -108,12 +115,12 @@ class PostPublishService:
async def _publish_media_group(self, call: CallbackQuery) -> None:
"""Публикация медиагруппы"""
post_content = self.db.get_post_content_from_telegram_by_last_id(call.message.message_id)
pre_text = self.db.get_post_text_from_telegram_by_last_id(call.message.message_id)
post_content = await self.db.get_post_content_from_telegram_by_last_id(call.message.message_id)
pre_text = await self.db.get_post_text_from_telegram_by_last_id(call.message.message_id)
post_text = html.escape(str(pre_text))
author_id = self._get_author_id_for_media_group(call.message.message_id)
author_id = await self._get_author_id_for_media_group(call.message.message_id)
await send_media_group_to_channel(bot=self.bot, chat_id=self.main_public, post_content=post_content, post_text=post_text)
await send_media_group_to_channel(bot=self._get_bot(call.message), chat_id=self.main_public, post_content=post_content, post_text=post_text)
await self._delete_media_group_and_notify_author(call, author_id)
async def decline_post(self, call: CallbackQuery) -> None:
@@ -130,8 +137,8 @@ class PostPublishService:
async def _decline_single_post(self, call: CallbackQuery) -> None:
"""Отклонение одиночного поста"""
author_id = self._get_author_id(call.message.message_id)
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
author_id = await self._get_author_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:
@@ -142,12 +149,12 @@ class PostPublishService:
async def _decline_media_group(self, call: CallbackQuery) -> None:
"""Отклонение медиагруппы"""
post_ids = self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
message_ids = [row[0] for row in post_ids]
message_ids.append(call.message.message_id)
author_id = self._get_author_id_for_media_group(call.message.message_id)
await self.bot.delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
author_id = await self._get_author_id_for_media_group(call.message.message_id)
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
try:
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
except Exception as e:
@@ -155,23 +162,24 @@ class PostPublishService:
raise UserBlockedBotError("Пользователь заблокировал бота")
raise
def _get_author_id(self, message_id: int) -> int:
async def _get_author_id(self, message_id: int) -> int:
"""Получение ID автора по ID сообщения"""
author_id = self.db.get_author_id_by_message_id(message_id)
author_id = await self.db.get_author_id_by_message_id(message_id)
if not author_id:
raise PostNotFoundError(f"Автор не найден для сообщения {message_id}")
return author_id
def _get_author_id_for_media_group(self, message_id: int) -> int:
async def _get_author_id_for_media_group(self, message_id: int) -> int:
"""Получение ID автора для медиагруппы"""
author_id = self.db.get_author_id_by_helper_message_id(message_id)
author_id = await self.db.get_author_id_by_helper_message_id(message_id)
if not author_id:
raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}")
return author_id
async def _delete_post_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
"""Удаление поста и уведомление автора"""
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
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:
@@ -181,10 +189,10 @@ class PostPublishService:
async def _delete_media_group_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
"""Удаление медиагруппы и уведомление автора"""
post_ids = self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
message_ids = [row[0] for row in post_ids]
message_ids.append(call.message.message_id)
await self.bot.delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
try:
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
except Exception as e:
@@ -203,24 +211,23 @@ class BanService:
async def ban_user_from_post(self, call: CallbackQuery) -> None:
"""Бан пользователя за спам"""
author_id = 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}")
user_name = self.db.get_username(user_id=author_id)
current_date = datetime.now()
date_to_unban = current_date + timedelta(days=7)
date_to_unban = int((current_date + timedelta(days=7)).timestamp())
self.db.set_user_blacklist(
await self.db.set_user_blacklist(
user_id=author_id,
user_name=user_name,
user_name=None,
message_for_user="Спам",
date_to_unban=date_to_unban
)
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
date_str = date_to_unban.strftime("%d.%m.%Y %H:%M")
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))
except Exception as e:
@@ -232,7 +239,7 @@ class BanService:
async def ban_user(self, user_id: str, user_name: str) -> str:
"""Бан пользователя по ID"""
user_name = self.db.get_username(user_id=user_id)
user_name = await self.db.get_username(int(user_id))
if not user_name:
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
@@ -240,10 +247,10 @@ class BanService:
async def unlock_user(self, user_id: str) -> str:
"""Разблокировка пользователя"""
user_name = self.db.get_username(user_id=user_id)
user_name = await self.db.get_username(int(user_id))
if not user_name:
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
delete_user_blacklist(user_id, self.db)
await delete_user_blacklist(int(user_id), self.db)
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
return user_name

View File

@@ -5,6 +5,7 @@ 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 - modular components
@@ -26,7 +27,7 @@ from helper_bot.utils.metrics import (
class GroupHandlers:
"""Main handler class for group messages"""
def __init__(self, db, keyboard_markup: types.ReplyKeyboardMarkup):
def __init__(self, db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMarkup):
self.db = db
self.keyboard_markup = keyboard_markup
self.admin_reply_service = AdminReplyService(db)
@@ -45,7 +46,7 @@ class GroupHandlers:
)
@error_handler
async def handle_message(self, message: types.Message, state: FSMContext):
async def handle_message(self, message: types.Message, state: FSMContext, **kwargs):
"""Handle admin reply to user through group chat"""
logger.info(
@@ -67,7 +68,7 @@ class GroupHandlers:
try:
# Get user ID for reply
chat_id = self.admin_reply_service.get_user_id_for_reply(message_id)
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(
@@ -86,7 +87,7 @@ class GroupHandlers:
# Factory function to create handlers with dependencies
def create_group_handlers(db, 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)
@@ -103,6 +104,7 @@ def init_legacy_router():
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
bdf = get_global_instance()
#TODO: поменять архитектуру и подключить правильный BotDB
db = bdf.get_db()
keyboard_markup = get_reply_keyboard_leave_chat()

View File

@@ -22,7 +22,8 @@ from helper_bot.utils.metrics import (
class DatabaseProtocol(Protocol):
"""Protocol for database operations"""
def get_user_by_message_id(self, message_id: int) -> Optional[int]: ...
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): ...
class AdminReplyService:
@@ -31,7 +32,7 @@ class AdminReplyService:
def __init__(self, db: DatabaseProtocol) -> None:
self.db = db
def get_user_id_for_reply(self, message_id: int) -> int:
async def get_user_id_for_reply(self, message_id: int) -> int:
"""
Get user ID for reply by message ID.
@@ -44,7 +45,7 @@ class AdminReplyService:
Raises:
UserNotFoundError: If user is not found in database
"""
user_id = self.db.get_user_by_message_id(message_id)
user_id = await self.db.get_user_by_message_id(message_id)
if user_id is None:
raise UserNotFoundError(f"User not found for message_id: {message_id}")
return user_id

View File

@@ -10,6 +10,7 @@ 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
from helper_bot.middlewares.album_middleware import AlbumMiddleware
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
@@ -43,7 +44,7 @@ sleep = asyncio.sleep
class PrivateHandlers:
"""Main handler class for private messages"""
def __init__(self, db, settings: BotSettings):
def __init__(self, db: AsyncBotDB, settings: BotSettings):
self.db = db
self.settings = settings
self.user_service = UserService(db, settings)
@@ -83,7 +84,7 @@ class PrivateHandlers:
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 = check_user_emoji(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')
@@ -91,11 +92,11 @@ class PrivateHandlers:
@error_handler
async def handle_restart_message(self, message: types.Message, state: FSMContext, **kwargs):
"""Handle restart command"""
markup = get_reply_keyboard(self.db, message.from_user.id)
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)
check_user_emoji(message)
await check_user_emoji(message)
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
@error_handler
@@ -110,7 +111,7 @@ class PrivateHandlers:
await self.sticker_service.send_random_hello_sticker(message)
# Send welcome message with metrics
markup = get_reply_keyboard(self.db, message.from_user.id)
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')
@@ -151,7 +152,7 @@ class PrivateHandlers:
await self.post_service.process_post(message, album)
# Send success message and return to start state
markup_for_user = get_reply_keyboard(self.db, message.from_user.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')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state(FSM_STATES["START"])
@@ -160,8 +161,8 @@ class PrivateHandlers:
async def stickers(self, message: types.Message, state: FSMContext, **kwargs):
"""Handle stickers request"""
# User service operations with metrics
markup = get_reply_keyboard(self.db, message.from_user.id)
self.db.update_info_about_stickers(user_id=message.from_user.id)
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"],
@@ -187,14 +188,14 @@ class PrivateHandlers:
await message.forward(chat_id=self.settings.group_for_message)
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
self.db.add_new_message_in_db(message.text, message.from_user.id, message.message_id + 1, date)
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')
user_state = await state.get_state()
if user_state == FSM_STATES["PRE_CHAT"]:
markup = get_reply_keyboard(self.db, message.from_user.id)
markup = await get_reply_keyboard(self.db, message.from_user.id)
await message.answer(question, reply_markup=markup)
await state.set_state(FSM_STATES["START"])
elif user_state == FSM_STATES["CHAT"]:
@@ -203,7 +204,7 @@ class PrivateHandlers:
# Factory function to create handlers with dependencies
def create_private_handlers(db, settings: BotSettings) -> PrivateHandlers:
def create_private_handlers(db: AsyncBotDB, settings: BotSettings) -> PrivateHandlers:
"""Create private handlers instance with dependencies"""
return PrivateHandlers(db, settings)

View File

@@ -12,6 +12,7 @@ from dataclasses import dataclass
# Third-party imports
from aiogram import types
from aiogram.types import FSInputFile
from database.models import TelegramPost, User
# Local imports - utilities
from helper_bot.utils.helper_func import (
@@ -41,16 +42,14 @@ from helper_bot.utils.metrics import (
class DatabaseProtocol(Protocol):
"""Protocol for database operations"""
def user_exists(self, user_id: int) -> bool: ...
def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str,
username: str, is_bot: bool, language_code: str,
emoji: str, created_date: str, updated_date: str) -> None: ...
def update_username_and_full_name(self, user_id: int, username: str, full_name: str) -> None: ...
def update_date_for_user(self, date: str, user_id: int) -> None: ...
def add_post_in_db(self, message_id: int, text: str, user_id: int) -> None: ...
def update_info_about_stickers(self, user_id: int) -> None: ...
def add_new_message_in_db(self, text: str, user_id: int, message_id: int, date: str) -> None: ...
def update_helper_message_in_db(self, message_id: int, helper_message_id: int) -> None: ...
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_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: ...
@dataclass
@@ -75,11 +74,10 @@ class UserService:
@track_time("update_user_activity", "user_service")
@track_errors("user_service", "update_user_activity")
@db_query_time("update_user_activity", "users", "update")
@db_query_time("update_user_activity", "user_service")
async def update_user_activity(self, user_id: int) -> None:
"""Update user's last activity timestamp with metrics tracking"""
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.db.update_date_for_user(current_date, user_id)
await self.db.update_user_date(user_id)
@track_time("ensure_user_exists", "user_service")
@track_errors("user_service", "ensure_user_exists")
@@ -92,19 +90,28 @@ class UserService:
is_bot = message.from_user.is_bot
language_code = message.from_user.language_code
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if not self.db.user_exists(user_id):
# Record database operation
self.db.add_new_user_in_db(
user_id, first_name, full_name, username, is_bot, language_code,
"", current_date, current_date
if not await self.db.user_exists(user_id):
# Create User object with current timestamp
current_timestamp = int(datetime.now().timestamp())
user = User(
user_id=user_id,
first_name=first_name,
full_name=full_name,
username=username,
is_bot=is_bot,
language_code=language_code,
emoji="",
has_stickers=False,
date_added=current_timestamp,
date_changed=current_timestamp,
voice_bot_welcome_received=False
)
metrics.record_db_query("add_new_user", 0.0, "users", "insert")
await self.db.add_user(user)
metrics.record_db_query("add_user", 0.0, "users", "insert")
else:
is_need_update = 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:
self.db.update_username_and_full_name(user_id, username, full_name)
await self.db.update_user_info(user_id, username, full_name)
metrics.record_db_query("update_username_fullname", 0.0, "users", "update")
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
safe_username = html.escape(username) if username else "Без никнейма"
@@ -115,8 +122,8 @@ class UserService:
chat_id=self.settings.group_for_logs,
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
self.db.update_date_for_user(current_date, user_id)
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
await self.db.update_user_date(user_id)
metrics.record_db_query("update_user_date", 0.0, "users", "update")
@track_errors("user_service", "log_user_message")
@@ -146,7 +153,13 @@ class PostService:
markup = get_reply_keyboard_for_post()
sent_message_id = await send_text_message(self.settings.group_for_posts, message, post_text, markup)
self.db.add_post_in_db(sent_message_id, message.text, message.from_user.id)
post = TelegramPost(
message_id=sent_message_id,
text=message.text,
author_id=message.from_user.id,
created_at=int(datetime.now().timestamp())
)
await self.db.add_post(post)
@track_time("handle_photo_post", "post_service")
@track_errors("post_service", "handle_photo_post")
@@ -161,7 +174,13 @@ class PostService:
self.settings.group_for_posts, message, message.photo[-1].file_id, post_caption, markup
)
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
post = TelegramPost(
message_id=sent_message.message_id,
text=sent_message.caption or "",
author_id=message.from_user.id,
created_at=int(datetime.now().timestamp())
)
await self.db.add_post(post)
await add_in_db_media(sent_message, self.db)
@track_time("handle_video_post", "post_service")
@@ -177,7 +196,13 @@ class PostService:
self.settings.group_for_posts, message, message.video.file_id, post_caption, markup
)
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
post = TelegramPost(
message_id=sent_message.message_id,
text=sent_message.caption or "",
author_id=message.from_user.id,
created_at=int(datetime.now().timestamp())
)
await self.db.add_post(post)
await add_in_db_media(sent_message, self.db)
@track_time("handle_video_note_post", "post_service")
@@ -189,7 +214,13 @@ class PostService:
self.settings.group_for_posts, message, message.video_note.file_id, markup
)
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
post = TelegramPost(
message_id=sent_message.message_id,
text=sent_message.caption or "",
author_id=message.from_user.id,
created_at=int(datetime.now().timestamp())
)
await self.db.add_post(post)
await add_in_db_media(sent_message, self.db)
@track_time("handle_audio_post", "post_service")
@@ -205,7 +236,13 @@ class PostService:
self.settings.group_for_posts, message, message.audio.file_id, post_caption, markup
)
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
post = TelegramPost(
message_id=sent_message.message_id,
text=sent_message.caption or "",
author_id=message.from_user.id,
created_at=int(datetime.now().timestamp())
)
await self.db.add_post(post)
await add_in_db_media(sent_message, self.db)
@track_time("handle_voice_post", "post_service")
@@ -217,7 +254,13 @@ class PostService:
self.settings.group_for_posts, message, message.voice.file_id, markup
)
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
post = TelegramPost(
message_id=sent_message.message_id,
text=sent_message.caption or "",
author_id=message.from_user.id,
created_at=int(datetime.now().timestamp())
)
await self.db.add_post(post)
await add_in_db_media(sent_message, self.db)
@track_time("handle_media_group_post", "post_service")
@@ -239,7 +282,7 @@ class PostService:
markup = get_reply_keyboard_for_post()
help_message_id = await send_text_message(self.settings.group_for_posts, message, "^", markup)
self.db.update_helper_message_in_db(
await self.db.update_helper_message(
message_id=media_group_message_id, helper_message_id=help_message_id
)

View File

@@ -22,7 +22,6 @@ class VoiceMessage:
self.date_added = date_added
self.file_id = file_id
class VoiceBotService:
"""Сервис для работы с голосовыми сообщениями"""
@@ -141,10 +140,10 @@ class VoiceBotService:
logger.error(f"Ошибка при отправке приветственных сообщений: {e}")
raise VoiceMessageError(f"Не удалось отправить приветственные сообщения: {e}")
def get_random_audio(self, user_id: int) -> Optional[Tuple[str, str, str]]:
async def get_random_audio(self, user_id: int) -> Optional[Tuple[str, str, str]]:
"""Получить случайное аудио для прослушивания"""
try:
check_audio = self.bot_db.check_listen_audio(user_id=user_id)
check_audio = await self.bot_db.check_listen_audio(user_id=user_id)
list_audio = list(check_audio)
if not list_audio:
@@ -155,9 +154,9 @@ class VoiceBotService:
audio_for_user = check_audio[number_element]
# Получаем информацию об авторе
user_id_author = self.bot_db.get_user_id_by_file_name(audio_for_user)
date_added = self.bot_db.get_date_by_file_name(audio_for_user)
user_emoji = self.bot_db.check_emoji_for_user(user_id_author)
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
@@ -165,26 +164,26 @@ class VoiceBotService:
logger.error(f"Ошибка при получении случайного аудио: {e}")
raise AudioProcessingError(f"Не удалось получить случайное аудио: {e}")
def mark_audio_as_listened(self, file_name: str, user_id: int) -> None:
async def mark_audio_as_listened(self, file_name: str, user_id: int) -> None:
"""Пометить аудио как прослушанное"""
try:
self.bot_db.mark_listened_audio(file_name, user_id=user_id)
await self.bot_db.mark_listened_audio(file_name, user_id=user_id)
except Exception as e:
logger.error(f"Ошибка при пометке аудио как прослушанного: {e}")
raise DatabaseError(f"Не удалось пометить аудио как прослушанное: {e}")
def clear_user_listenings(self, user_id: int) -> None:
async def clear_user_listenings(self, user_id: int) -> None:
"""Очистить прослушивания пользователя"""
try:
self.bot_db.delete_listen_count_for_user(user_id)
await self.bot_db.delete_listen_count_for_user(user_id)
except Exception as e:
logger.error(f"Ошибка при очистке прослушиваний: {e}")
raise DatabaseError(f"Не удалось очистить прослушивания: {e}")
def get_remaining_audio_count(self, user_id: int) -> int:
async def get_remaining_audio_count(self, user_id: int) -> int:
"""Получить количество оставшихся непрослушанных аудио"""
try:
check_audio = self.bot_db.check_listen_audio(user_id=user_id)
check_audio = await self.bot_db.check_listen_audio(user_id=user_id)
return len(list(check_audio))
except Exception as e:
logger.error(f"Ошибка при получении количества аудио: {e}")
@@ -215,23 +214,29 @@ class AudioFileService:
def __init__(self, bot_db):
self.bot_db = bot_db
def generate_file_name(self, user_id: int) -> str:
async def generate_file_name(self, user_id: int) -> str:
"""Сгенерировать имя файла для аудио"""
try:
# Проверяем есть ли запись о файле в базе данных
is_having_audio_from_user = self.bot_db.get_last_user_audio_record(user_id=user_id)
user_audio_count = await self.bot_db.get_user_audio_records_count(user_id=user_id)
if is_having_audio_from_user is False:
if user_audio_count == 0:
# Если нет, то генерируем имя файла
file_name = f'message_from_{user_id}_number_1'
else:
# Иначе берем последнюю запись из БД, добавляем к ней 1
file_name = self.bot_db.get_path_for_audio_record(user_id=user_id)
file_id = self.bot_db.get_id_for_audio_record(user_id) + 1
path = Path(f'voice_users/{file_name}.ogg')
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])
new_number = current_number + 1
except (ValueError, IndexError):
new_number = user_audio_count + 1
else:
new_number = user_audio_count + 1
if path.exists():
file_name = f'message_from_{user_id}_number_{file_id}'
file_name = f'message_from_{user_id}_number_{new_number}'
return file_name
@@ -239,23 +244,31 @@ class AudioFileService:
logger.error(f"Ошибка при генерации имени файла: {e}")
raise FileOperationError(f"Не удалось сгенерировать имя файла: {e}")
def save_audio_file(self, file_name: str, user_id: int, date_added: datetime) -> None:
async def save_audio_file(self, file_name: str, user_id: int, date_added: datetime, file_id: str) -> None:
"""Сохранить информацию об аудио файле в базу данных"""
try:
self.bot_db.add_audio_record(file_name, user_id, date_added)
await self.bot_db.add_audio_record_simple(file_name, user_id, date_added)
except Exception as e:
logger.error(f"Ошибка при сохранении аудио файла в БД: {e}")
raise DatabaseError(f"Не удалось сохранить аудио файл в БД: {e}")
async def download_and_save_audio(self, bot, message_id: int, file_name: str) -> None:
async def download_and_save_audio(self, bot, message, file_name: str) -> None:
"""Скачать и сохранить аудио файл"""
try:
# Получаем информацию о файле
file_info = await bot.get_file(file_id=bot.get_message(message_id).voice.file_id)
# Проверяем наличие голосового сообщения
if not message or not message.voice:
raise FileOperationError("Сообщение или голосовое сообщение не найдено")
file_id = message.voice.file_id
file_info = await bot.get_file(file_id=file_id)
downloaded_file = await bot.download_file(file_path=file_info.file_path)
# Создаем директорию если она не существует
import os
os.makedirs(VOICE_USERS_DIR, exist_ok=True)
# Сохраняем файл
with open(f'voice_users/{file_name}.ogg', 'wb') as new_file:
with open(f'{VOICE_USERS_DIR}/{file_name}.ogg', 'wb') as new_file:
new_file.write(downloaded_file.read())
except Exception as e:

View File

@@ -70,26 +70,30 @@ def plural_time(type: int, n: float) -> str:
return str(new_number) + ' ' + word[p]
def get_last_message_text(bot_db) -> Optional[str]:
async def get_last_message_text(bot_db) -> Optional[str]:
"""Получить текст сообщения о времени последней записи"""
try:
date_from_db = bot_db.last_date_audio()
return format_time_ago(date_from_db)
date_from_db = await bot_db.last_date_audio()
if date_from_db is None:
return None
# Преобразуем UNIX timestamp в строку для format_time_ago
date_string = datetime.fromtimestamp(date_from_db).strftime("%Y-%m-%d %H:%M:%S")
return format_time_ago(date_string)
except Exception as e:
logger.error(f"Не удалось получить дату последнего сообщения - {e}")
return None
def validate_voice_message(message) -> bool:
async def validate_voice_message(message) -> bool:
"""Проверить валидность голосового сообщения"""
return message.content_type == 'voice'
def get_user_emoji_safe(bot_db, user_id: int) -> str:
async def get_user_emoji_safe(bot_db, user_id: int) -> str:
"""Безопасно получить эмодзи пользователя"""
try:
user_emoji = bot_db.check_emoji_for_user(user_id)
return user_emoji if user_emoji else "😊"
user_emoji = await bot_db.get_user_emoji(user_id)
return user_emoji if user_emoji and user_emoji != "Смайл еще не определен" else "😊"
except Exception as e:
logger.error(f"Ошибка при получении эмодзи пользователя {user_id}: {e}")
return "😊"

View File

@@ -98,7 +98,7 @@ class VoiceHandlers:
"""Обработчик кнопки 'Голосовой бот' из основной клавиатуры"""
try:
# Проверяем, получал ли пользователь приветственное сообщение
welcome_received = bot_db.check_voice_bot_welcome_received(message.from_user.id)
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:
@@ -124,7 +124,7 @@ class VoiceHandlers:
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)
check_user_emoji(message)
await check_user_emoji(message)
markup = get_main_keyboard()
await message.answer(text='🎤 Записывайся или слушай!', reply_markup=markup)
await state.set_state(STATE_START)
@@ -136,7 +136,7 @@ class VoiceHandlers:
settings: MagicData("settings")
):
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
user_emoji = check_user_emoji(message)
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')
@@ -167,7 +167,7 @@ class VoiceHandlers:
await state.set_state(STATE_START)
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
await update_user_info(VOICE_BOT_NAME, message)
user_emoji = get_user_emoji_safe(bot_db, message.from_user.id)
user_emoji = await get_user_emoji_safe(bot_db, message.from_user.id)
# Создаем сервис и отправляем приветственные сообщения
voice_service = VoiceBotService(bot_db, settings)
@@ -175,7 +175,7 @@ class VoiceHandlers:
# Отмечаем, что пользователь получил приветственное сообщение
try:
bot_db.mark_voice_bot_welcome_received(message.from_user.id)
await bot_db.mark_voice_bot_welcome_received(message.from_user.id)
logger.info(f"Пользователь {message.from_user.id}: отмечен как получивший приветствие")
except Exception as e:
logger.error(f"Ошибка при отметке получения приветствия: {e}")
@@ -194,7 +194,7 @@ class VoiceHandlers:
# Очищаем прослушивания через сервис
voice_service = VoiceBotService(bot_db, settings)
voice_service.clear_user_listenings(message.from_user.id)
await voice_service.clear_user_listenings(message.from_user.id)
listenings_cleared_message = messages.get_message(get_first_name(message), 'LISTENINGS_CLEARED_MESSAGE')
await message.answer(
@@ -218,7 +218,7 @@ class VoiceHandlers:
await message.answer(text=record_voice_message, reply_markup=markup)
try:
message_with_date = get_last_message_text(bot_db)
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:
@@ -240,7 +240,7 @@ class VoiceHandlers:
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
markup = get_main_keyboard()
if validate_voice_message(message):
if await validate_voice_message(message):
markup_for_voice = get_reply_keyboard_for_voice()
# Отправляем аудио в приватный канал
@@ -252,7 +252,7 @@ class VoiceHandlers:
)
# Сохраняем в базу инфо о посте
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')
@@ -278,13 +278,13 @@ class VoiceHandlers:
try:
# Получаем случайное аудио
audio_data = voice_service.get_random_audio(message.from_user.id)
audio_data = await voice_service.get_random_audio(message.from_user.id)
if not audio_data:
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 = get_last_message_text(bot_db)
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:
@@ -331,10 +331,10 @@ class VoiceHandlers:
)
# Маркируем сообщение как прослушанное только после успешной отправки
voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id)
await voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id)
# Получаем количество оставшихся аудио только после успешной отправки
remaining_count = voice_service.get_remaining_audio_count(message.from_user.id) - 1
remaining_count = await voice_service.get_remaining_audio_count(message.from_user.id) - 1
await message.answer(
text=f'Осталось непрослушанных: <b>{remaining_count}</b>',
reply_markup=markup

View File

@@ -25,13 +25,13 @@ def get_reply_keyboard_for_post():
@track_time("get_reply_keyboard", "keyboard_service")
@track_errors("keyboard_service", "get_reply_keyboard")
def get_reply_keyboard(BotDB, user_id):
async def get_reply_keyboard(BotDB, user_id):
builder = ReplyKeyboardBuilder()
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
builder.row(types.KeyboardButton(text="📩Связаться с админами"))
builder.row(types.KeyboardButton(text=" 🎤Голосовой бот"))
builder.row(types.KeyboardButton(text="👋🏼Сказать пока!"))
if not BotDB.get_info_about_stickers(user_id=user_id):
if not await BotDB.get_stickers_info(user_id):
builder.row(types.KeyboardButton(text="🤪Хочу стикеры"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup

View File

@@ -3,6 +3,8 @@ from aiogram.client.default import DefaultBotProperties
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.strategy import FSMStrategy
import logging
import asyncio
from typing import Optional
from helper_bot.handlers.admin import admin_router
from helper_bot.handlers.callback import callback_router
@@ -15,12 +17,36 @@ from helper_bot.middlewares.metrics_middleware import MetricsMiddleware, ErrorMe
from helper_bot.server_prometheus import start_metrics_server, stop_metrics_server
async def start_bot_with_retry(bot: Bot, dp: Dispatcher, max_retries: int = 5, base_delay: float = 1.0):
"""Запуск бота с автоматическим перезапуском при сетевых ошибках"""
for attempt in range(max_retries):
try:
logging.info(f"Запуск бота (попытка {attempt + 1}/{max_retries})")
await dp.start_polling(bot, skip_updates=True)
break
except Exception as e:
error_msg = str(e).lower()
if any(keyword in error_msg for keyword in ['network', 'disconnected', 'timeout', 'connection']):
if attempt < max_retries - 1:
delay = base_delay * (2 ** attempt) # Exponential backoff
logging.warning(f"Сетевая ошибка при запуске бота: {e}. Повтор через {delay:.1f}с (попытка {attempt + 1}/{max_retries})")
await asyncio.sleep(delay)
continue
else:
logging.error(f"Превышено максимальное количество попыток запуска бота: {e}")
raise
else:
logging.error(f"Критическая ошибка при запуске бота: {e}")
raise
async def start_bot(bdf):
token = bdf.settings['Telegram']['bot_token']
bot = Bot(token=token, default=DefaultBotProperties(
parse_mode='HTML',
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
), timeout=30.0)
), timeout=60.0) # Увеличиваем timeout для стабильности
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
# ✅ Оптимизированная регистрация middleware
@@ -32,13 +58,19 @@ async def start_bot(bdf):
voice_handlers = VoiceHandlers(bdf, bdf.settings)
voice_router = voice_handlers.router
# Добавляем middleware напрямую к роутерам для тестирования
admin_router.message.middleware(MetricsMiddleware())
private_router.message.middleware(MetricsMiddleware())
callback_router.callback_query.middleware(MetricsMiddleware())
group_router.message.middleware(MetricsMiddleware())
voice_router.message.middleware(MetricsMiddleware())
# Middleware уже добавлены на уровне dispatcher
dp.include_routers(admin_router, private_router, callback_router, group_router, voice_router)
# Добавляем обработчик завершения для корректного закрытия
@dp.shutdown()
async def on_shutdown():
logging.info("Bot shutdown initiated, cleaning up resources...")
try:
await bot.session.close()
logging.info("Bot session closed successfully")
except Exception as e:
logging.error(f"Error closing bot session during shutdown: {e}")
await bot.delete_webhook(drop_pending_updates=True)
# Запускаем HTTP сервер для метрик параллельно с ботом
@@ -49,12 +81,23 @@ async def start_bot(bdf):
# Запускаем метрики сервер
await start_metrics_server(metrics_host, metrics_port)
# Запускаем бота
await dp.start_polling(bot, skip_updates=True)
# Запускаем бота с retry логикой
await start_bot_with_retry(bot, dp)
except Exception as e:
logging.error(f"Error in bot startup: {e}")
raise
finally:
# Останавливаем метрики сервер при завершении
await stop_metrics_server()
try:
await stop_metrics_server()
except Exception as e:
logging.error(f"Error stopping metrics server: {e}")
# Закрываем сессию бота
try:
await bot.session.close()
except Exception as e:
logging.error(f"Error closing bot session: {e}")
return bot

View File

@@ -1,5 +1,6 @@
from typing import Dict, Any
import html
from datetime import datetime
from aiogram import BaseMiddleware, types
from aiogram.types import TelegramObject, Message, CallbackQuery
@@ -26,12 +27,21 @@ class BlacklistMiddleware(BaseMiddleware):
logger.info(f'Вызов BlacklistMiddleware для пользователя {user.username}')
# Используем асинхронную версию для предотвращения блокировки
if await BotDB.check_user_in_blacklist_async(user_id=user.id):
if await BotDB.check_user_in_blacklist(user.id):
logger.info(f'BlacklistMiddleware результат для пользователя: {user.username} заблокирован!')
user_info = await BotDB.get_blacklist_users_by_id_async(user.id)
user_info = await BotDB.get_blacklist_users_by_id(user.id)
# Экранируем потенциально проблемные символы
reason = html.escape(str(user_info[2])) if user_info[2] else "Не указана"
date_unban = html.escape(str(user_info[3])) if user_info[3] else "Не указана"
reason = html.escape(str(user_info[1])) if user_info and user_info[1] else "Не указана"
# Преобразуем timestamp в человекочитаемый формат
if user_info and user_info[2]:
try:
timestamp = int(user_info[2])
date_unban = datetime.fromtimestamp(timestamp).strftime("%d-%m-%Y %H:%M")
except (ValueError, TypeError):
date_unban = "Не указана"
else:
date_unban = "Не указана"
# Отправляем сообщение в зависимости от типа события
if isinstance(event, Message):

124
helper_bot/scripts/monitor_bot.sh Executable file
View File

@@ -0,0 +1,124 @@
#!/bin/bash
# Script for monitoring and auto-restarting the Telegram bot
# Usage: ./monitor_bot.sh
set -e
# Configuration
BOT_CONTAINER="telegram-helper-bot"
HEALTH_ENDPOINT="http://localhost:8080/health"
CHECK_INTERVAL=60 # seconds
MAX_FAILURES=3
LOG_FILE="logs/bot_monitor.log"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Logging function
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# Check if container is running
check_container_running() {
if docker ps --format "table {{.Names}}" | grep -q "^${BOT_CONTAINER}$"; then
return 0
else
return 1
fi
}
# Check health endpoint
check_health() {
if curl -f --connect-timeout 5 --max-time 10 "$HEALTH_ENDPOINT" >/dev/null 2>&1; then
return 0
else
return 1
fi
}
# Restart container
restart_container() {
log "${YELLOW}Restarting container ${BOT_CONTAINER}...${NC}"
if docker restart "$BOT_CONTAINER" >/dev/null 2>&1; then
log "${GREEN}Container restarted successfully${NC}"
# Wait for container to be ready
log "Waiting for container to be ready..."
sleep 30
# Check if container is healthy
local attempts=0
while [ $attempts -lt 10 ]; do
if check_health; then
log "${GREEN}Container is healthy after restart${NC}"
return 0
fi
attempts=$((attempts + 1))
sleep 10
done
log "${RED}Container failed to become healthy after restart${NC}"
return 1
else
log "${RED}Failed to restart container${NC}"
return 1
fi
}
# Main monitoring loop
main() {
log "${GREEN}Starting bot monitoring...${NC}"
log "Container: $BOT_CONTAINER"
log "Health endpoint: $HEALTH_ENDPOINT"
log "Check interval: ${CHECK_INTERVAL}s"
log "Max failures: $MAX_FAILURES"
local failure_count=0
while true; do
# Check if container is running
if ! check_container_running; then
log "${RED}Container $BOT_CONTAINER is not running!${NC}"
if restart_container; then
failure_count=0
else
failure_count=$((failure_count + 1))
fi
else
# Check health endpoint
if check_health; then
if [ $failure_count -gt 0 ]; then
log "${GREEN}Container recovered, resetting failure count${NC}"
failure_count=0
fi
log "${GREEN}Container is healthy${NC}"
else
failure_count=$((failure_count + 1))
log "${YELLOW}Health check failed (${failure_count}/${MAX_FAILURES})${NC}"
if [ $failure_count -ge $MAX_FAILURES ]; then
log "${RED}Max failures reached, restarting container${NC}"
if restart_container; then
failure_count=0
else
log "${RED}Failed to restart container after max failures${NC}"
fi
fi
fi
fi
sleep "$CHECK_INTERVAL"
done
}
# Handle script interruption
trap 'log "Monitoring stopped by user"; exit 0' INT TERM
# Run main function
main "$@"

View File

@@ -59,10 +59,43 @@ class MetricsServer:
async def health_handler(self, request: web.Request) -> web.Response:
"""Handle /health endpoint for health checks."""
return web.Response(
text="OK",
content_type='text/plain'
)
try:
# Проверяем доступность метрик
if not metrics:
return web.Response(
text="ERROR: Metrics not available",
content_type='text/plain',
status=503
)
# Проверяем, что можем получить метрики
try:
metrics_data = metrics.get_metrics()
if not metrics_data:
return web.Response(
text="ERROR: Empty metrics",
content_type='text/plain',
status=503
)
except Exception as e:
return web.Response(
text=f"ERROR: Metrics generation failed: {e}",
content_type='text/plain',
status=503
)
return web.Response(
text="OK",
content_type='text/plain',
status=200
)
except Exception as e:
self.logger.error(f"Health check failed: {e}")
return web.Response(
text=f"ERROR: Health check failed: {e}",
content_type='text/plain',
status=500
)
async def start(self) -> None:
"""Start the HTTP server."""
@@ -122,5 +155,12 @@ async def stop_metrics_server() -> None:
"""Stop metrics server if running."""
global metrics_server
if metrics_server:
await metrics_server.stop()
metrics_server = None
try:
await metrics_server.stop()
logger = logging.getLogger(__name__)
logger.info("Metrics server stopped successfully")
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Error stopping metrics server: {e}")
finally:
metrics_server = None

View File

@@ -34,14 +34,13 @@ class AutoUnbanScheduler:
try:
logger.info("Запуск автоматического разбана пользователей")
# Получаем сегодняшнюю дату в формате YYYY-MM-DD
moscow_tz = timezone(timedelta(hours=3)) # UTC+3 для Москвы
today = datetime.now(moscow_tz).strftime("%Y-%m-%d")
# Получаем текущий UNIX timestamp
current_timestamp = int(datetime.now().timestamp())
logger.info(f"Поиск пользователей для разблокировки на дату: {today}")
logger.info(f"Поиск пользователей для разблокировки на timestamp: {current_timestamp}")
# Получаем список пользователей для разблокировки
users_to_unban = self.bot_db.get_users_for_unblock_today(today)
users_to_unban = await self.bot_db.get_users_for_unblock_today(current_timestamp)
if not users_to_unban:
logger.info("Нет пользователей для разблокировки сегодня")
@@ -55,20 +54,20 @@ class AutoUnbanScheduler:
failed_users = []
# Разблокируем каждого пользователя
for user_id, username in users_to_unban.items():
for user_id in users_to_unban:
try:
result = self.bot_db.delete_user_blacklist(user_id)
result = await self.bot_db.delete_user_blacklist(user_id)
if result:
success_count += 1
logger.info(f"Пользователь {user_id} ({username}) успешно разблокирован")
logger.info(f"Пользователь {user_id} успешно разблокирован")
else:
failed_count += 1
failed_users.append(f"{user_id} ({username})")
logger.error(f"Ошибка при разблокировке пользователя {user_id} ({username})")
failed_users.append(f"{user_id}")
logger.error(f"Ошибка при разблокировке пользователя {user_id}")
except Exception as e:
failed_count += 1
failed_users.append(f"{user_id} ({username})")
logger.error(f"Исключение при разблокировке пользователя {user_id} ({username}): {e}")
failed_users.append(f"{user_id}")
logger.error(f"Исключение при разблокировке пользователя {user_id}: {e}")
# Формируем отчет
report = self._generate_report(success_count, failed_count, failed_users, users_to_unban)
@@ -93,10 +92,9 @@ class AutoUnbanScheduler:
if success_count > 0:
report += "✅ <b>Разблокированные пользователи:</b>\n"
for user_id, username in all_users.items():
if f"{user_id} ({username})" not in failed_users:
safe_username = username if username else "Неизвестный пользователь"
report += f"• ID: {user_id}, Имя: {safe_username}\n"
for user_id in all_users:
if str(user_id) not in failed_users:
report += f"• ID: {user_id}\n"
report += "\n"
if failed_users:

View File

@@ -2,7 +2,7 @@ import os
import sys
from dotenv import load_dotenv
from database.db import BotDB
from database.async_db import AsyncBotDB
class BaseDependencyFactory:
@@ -18,10 +18,7 @@ class BaseDependencyFactory:
if not os.path.isabs(database_path):
database_path = os.path.join(project_dir, database_path)
database_dir = project_dir
database_name = database_path.replace(project_dir + '/', '')
self.database = BotDB(database_dir, database_name)
self.database = AsyncBotDB(database_path)
self._load_settings_from_env()
@@ -60,7 +57,7 @@ class BaseDependencyFactory:
def get_settings(self):
return self.settings
def get_db(self) -> BotDB:
def get_db(self) -> AsyncBotDB:
"""Возвращает подключение к базе данных."""
return self.database

View File

@@ -3,17 +3,21 @@ import os
import random
from datetime import datetime, timedelta
from time import sleep
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any, Optional, TYPE_CHECKING
try:
import emoji as _emoji_lib
except Exception:
_emoji_lib_available = True
except ImportError:
_emoji_lib = None
_emoji_lib_available = False
from aiogram import types
from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from logs.custom_logger import logger
from database.models import TelegramPost
# Local imports - metrics
from .metrics import (
@@ -24,10 +28,11 @@ from .metrics import (
)
bdf = get_global_instance()
#TODO: поменять архитектуру и подключить правильный BotDB
BotDB = bdf.get_db()
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
if _emoji_lib is not None:
if _emoji_lib_available and _emoji_lib is not None:
emoji_list = list(_emoji_lib.EMOJI_DATA.keys())
else:
# Fallback minimal emoji set for environments without the 'emoji' package (e.g., CI tests)
@@ -189,25 +194,25 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
async def add_in_db_media_mediagroup(sent_message, bot_db):
"""
Идентификатор медиа-группы
Добавляет контент медиа-группы в базу данных
Args:
sent_message: sent_message объект из Telegram API
bot_db: Экземпляр базы данных
Returns:
Список InputFile (FSInputFile).
None
"""
media_group_message_id = sent_message[-1].message_id # Получаем идентификатор медиа-группы
post_id = sent_message[-1].message_id # ID поста (первое сообщение в медиа-группе)
for i, message in enumerate(sent_message):
if message.photo:
file_id = message.photo[-1].file_id
file_path = await download_file(message, file_id=file_id)
bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo')
await bot_db.add_post_content(post_id, message.message_id, file_path, 'photo')
elif message.video:
file_id = message.video.file_id
file_path = await download_file(message, file_id=file_id)
bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video')
await bot_db.add_post_content(post_id, message.message_id, file_path, 'video')
else:
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение
continue
@@ -215,33 +220,36 @@ async def add_in_db_media_mediagroup(sent_message, bot_db):
async def add_in_db_media(sent_message, bot_db):
"""
Добавляет контент одиночного сообщения в базу данных
Args:
sent_message: sent_message объект из Telegram API
bot_db: Экземпляр базы данных
Returns:
Список InputFile (FSInputFile).
None
"""
post_id = sent_message.message_id # ID поста (это же сообщение)
if sent_message.photo:
file_id = sent_message.photo[-1].file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo')
await bot_db.add_post_content(post_id, sent_message.message_id, file_path, 'photo')
elif sent_message.video:
file_id = sent_message.video.file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video')
await bot_db.add_post_content(post_id, sent_message.message_id, file_path, 'video')
elif sent_message.voice:
file_id = sent_message.voice.file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice')
await bot_db.add_post_content(post_id, sent_message.message_id, file_path, 'voice')
elif sent_message.audio:
file_id = sent_message.audio.file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio')
await bot_db.add_post_content(post_id, sent_message.message_id, file_path, 'audio')
elif sent_message.video_note:
file_id = sent_message.video_note.file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video_note')
await bot_db.add_post_content(post_id, sent_message.message_id, file_path, 'video_note')
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
@@ -250,7 +258,13 @@ async def send_media_group_message_to_private_chat(chat_id: int, message: types.
chat_id=chat_id,
media=media_group,
)
bot_db.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id)
post = TelegramPost(
message_id=sent_message[-1].message_id,
text=sent_message[-1].caption or "",
author_id=message.from_user.id,
created_at=int(datetime.now().timestamp())
)
await bot_db.add_post(post)
await add_in_db_media_mediagroup(sent_message, bot_db)
message_id = sent_message[-1].message_id
return message_id
@@ -404,20 +418,22 @@ async def send_voice_message(chat_id, message: types.Message, voice: str,
return sent_message
def check_access(user_id: int, bot_db):
async def check_access(user_id: int, bot_db):
"""Проверка прав на совершение действий"""
return bot_db.is_admin(user_id)
from logs.custom_logger import logger
result = await bot_db.is_admin(user_id)
logger.info(f"check_access: пользователь {user_id} - результат: {result}")
return result
def add_days_to_date(days: int):
"""Прибавляет указанное количество дней к текущей дате и возвращает дату в формате DD-MM-YYYY."""
"""Прибавляет указанное количество дней к текущей дате и возвращает UNIX timestamp."""
current_date = datetime.now()
future_date = current_date + timedelta(days=days)
formatted_date = future_date.strftime("%d-%m-%Y")
return formatted_date
return int(future_date.timestamp())
def get_banned_users_list(offset: int, bot_db):
async def get_banned_users_list(offset: int, bot_db):
"""
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
@@ -429,22 +445,43 @@ def get_banned_users_list(offset: int, bot_db):
message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)]
"""
users = bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
users = await bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
message = "Список заблокированных пользователей:\n"
for user in users:
# Экранируем пользовательские данные для безопасного использования
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
safe_ban_reason = html.escape(str(user[2])) if user[2] else "Причина не указана"
safe_unban_date = html.escape(str(user[3])) if user[3] else "Дата не указана"
user_id, ban_reason, unban_date = user
# Получаем имя пользователя из таблицы users
username = await bot_db.get_username(user_id)
full_name = await bot_db.get_full_name_by_id(user_id)
safe_user_name = username or full_name or f"User_{user_id}"
message += f"Пользователь: {safe_user_name}\n"
message += f"Причина бана: {safe_ban_reason}\n"
message += f"Дата разбана: {safe_unban_date}\n\n"
# Экранируем пользовательские данные для безопасного использования
safe_user_name = html.escape(str(safe_user_name))
safe_ban_reason = html.escape(str(ban_reason)) if ban_reason else "Причина не указана"
# Форматируем дату разбана в человекочитаемый формат
if unban_date:
try:
# Предполагаем, что unban_date это UNIX timestamp
if isinstance(unban_date, (int, float)):
unban_datetime = datetime.fromtimestamp(unban_date)
safe_unban_date = unban_datetime.strftime("%d-%m-%Y %H:%M")
else:
# Если это уже datetime объект
safe_unban_date = unban_date.strftime("%d-%m-%Y %H:%M")
except (ValueError, TypeError, OSError):
# В случае ошибки показываем исходное значение
safe_unban_date = html.escape(str(unban_date))
else:
safe_unban_date = "Дата не указана"
message += f"**Пользователь:** {safe_user_name}\n"
message += f"**Причина бана:** {safe_ban_reason}\n"
message += f"**Дата разбана:** {safe_unban_date}\n\n"
return message
def get_banned_users_buttons(bot_db):
async def get_banned_users_buttons(bot_db):
"""
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
@@ -455,42 +492,58 @@ def get_banned_users_buttons(bot_db):
message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)]
"""
users = bot_db.get_banned_users_from_db()
users = await bot_db.get_banned_users_from_db()
user_ids = []
for user in users:
user_id, ban_reason, unban_date = user
# Получаем имя пользователя из таблицы users
username = await bot_db.get_username(user_id)
full_name = await bot_db.get_full_name_by_id(user_id)
safe_user_name = username or full_name or f"User_{user_id}"
# Экранируем user_name для безопасного использования
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
user_ids.append((safe_user_name, user[1]))
safe_user_name = html.escape(str(safe_user_name))
user_ids.append((safe_user_name, user_id))
return user_ids
def delete_user_blacklist(user_id: int, bot_db):
return bot_db.delete_user_blacklist(user_id=user_id)
async def delete_user_blacklist(user_id: int, bot_db):
return await bot_db.delete_user_blacklist(user_id=user_id)
@track_time("check_username_and_full_name", "helper_func")
@track_errors("helper_func", "check_username_and_full_name")
@db_query_time("get_username_and_full_name", "users", "select")
def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
username_db, full_name_db = bot_db.get_username_and_full_name(user_id=user_id)
return username != username_db or full_name != full_name_db
@db_query_time("check_username_and_full_name", "users", "select")
async def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
"""Проверяет, изменились ли username или full_name пользователя"""
try:
username_db = await bot_db.get_username(user_id)
full_name_db = await bot_db.get_full_name_by_id(user_id)
return username != username_db or full_name != full_name_db
except Exception as e:
logger.error(f"Ошибка при проверке username и full_name: {e}")
return False
def unban_notifier(self):
# Получение сегодняшней даты в формате DD-MM-YYYY
async def unban_notifier(bot, BotDB, GROUP_FOR_MESSAGE):
# Получение текущего UNIX timestamp
current_date = datetime.now()
today = current_date.strftime("%d-%m-%Y")
current_timestamp = int(current_date.timestamp())
# Получение списка разблокированных пользователей
unblocked_users = self.BotDB.get_users_for_unblock_today(today)
unblocked_users = await BotDB.get_users_for_unblock_today(current_timestamp)
message = "Разблокированные пользователи:\n"
for user_id, user_name in unblocked_users.items():
for user_id in unblocked_users:
# Получаем имя пользователя из таблицы users
username = await BotDB.get_username(user_id)
full_name = await BotDB.get_full_name_by_id(user_id)
user_name = username or full_name or f"User_{user_id}"
# Экранируем user_name для безопасного использования
safe_user_name = html.escape(str(user_name)) if user_name else "Неизвестный пользователь"
safe_user_name = html.escape(str(user_name))
message += f"ID: {user_id}, Имя: {safe_user_name}\n"
# Отправка сообщения в канал
self.bot.send_message(self.GROUP_FOR_MESSAGE, message)
await bot.send_message(GROUP_FOR_MESSAGE, message)
@track_time("update_user_info", "helper_func")
@@ -503,51 +556,65 @@ async def update_user_info(source: str, message: types.Message):
is_bot = message.from_user.is_bot
language_code = message.from_user.language_code
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
# Выбираем эмодзю, пробегаемся циклом и смотрим что в базе такого еще не было
user_emoji = get_random_emoji()
if not BotDB.user_exists(user_id):
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, user_emoji, date,
date)
metrics.record_db_query("add_new_user_in_db", 0.0, "users", "insert")
user_emoji = await get_random_emoji()
if not await BotDB.user_exists(user_id):
# Create User object with current timestamp
from database.models import User
current_timestamp = int(datetime.now().timestamp())
user = User(
user_id=user_id,
first_name=first_name,
full_name=full_name,
username=username,
is_bot=is_bot,
language_code=language_code,
emoji=user_emoji,
has_stickers=False,
date_added=current_timestamp,
date_changed=current_timestamp,
voice_bot_welcome_received=False
)
await BotDB.add_user(user)
metrics.record_db_query("add_user", 0.0, "users", "insert")
else:
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
is_need_update = await check_username_and_full_name(user_id, username, full_name, BotDB)
if is_need_update:
BotDB.update_username_and_full_name(user_id, username, full_name)
metrics.record_db_query("update_username_and_full_name", 0.0, "users", "update")
await BotDB.update_user_info(user_id, username, full_name)
metrics.record_db_query("update_user_info", 0.0, "users", "update")
if source != 'voice':
await message.answer(
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
sleep(1)
BotDB.update_date_for_user(date, user_id)
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
await BotDB.update_user_date(user_id)
metrics.record_db_query("update_user_date", 0.0, "users", "update")
@track_time("check_user_emoji", "helper_func")
@track_errors("helper_func", "check_user_emoji")
@db_query_time("check_emoji_for_user", "users", "select")
def check_user_emoji(message: types.Message):
async def check_user_emoji(message: types.Message):
user_id = message.from_user.id
user_emoji = BotDB.check_emoji_for_user(user_id=user_id)
user_emoji = await BotDB.get_stickers_info(user_id=user_id)
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
user_emoji = get_random_emoji()
BotDB.update_emoji_for_user(user_id=user_id, emoji=user_emoji)
metrics.record_db_query("update_emoji_for_user", 0.0, "users", "update")
user_emoji = await get_random_emoji()
await BotDB.update_user_emoji(user_id=user_id, emoji=user_emoji)
metrics.record_db_query("update_user_emoji", 0.0, "users", "update")
return user_emoji
@track_time("get_random_emoji", "helper_func")
@track_errors("helper_func", "get_random_emoji")
@db_query_time("check_emoji", "users", "select")
def get_random_emoji():
async def get_random_emoji():
attempts = 0
while attempts < 100:
user_emoji = random.choice(emoji_list)
if not BotDB.check_emoji(user_emoji):
if not await BotDB.check_emoji_exists(user_emoji):
return user_emoji
attempts += 1
logger.error("Не удалось найти уникальный эмодзи после нескольких попыток.")

View File

@@ -8,12 +8,13 @@ import logging
from aiohttp import web
from typing import Optional, Dict, Any, Protocol
from .metrics import metrics
import time
class DatabaseProvider(Protocol):
"""Protocol for database operations."""
async def fetch_one(self, query: str) -> Optional[Dict[str, Any]]:
async def fetch_one(self, query: str, params: tuple = ()) -> Optional[Dict[str, Any]]:
"""Execute query and return single result."""
...
@@ -37,12 +38,16 @@ class UserMetricsCollector:
try:
# Проверяем, есть ли метод fetch_one (асинхронная БД)
if hasattr(db, 'fetch_one'):
# Используем UNIX timestamp для сравнения с date_changed
current_timestamp = int(time.time())
one_day_ago = current_timestamp - (24 * 60 * 60) # 24 часа назад
active_users_query = """
SELECT COUNT(DISTINCT user_id) as active_users
FROM our_users
WHERE date_changed > datetime('now', '-1 day')
WHERE date_changed > ?
"""
result = await db.fetch_one(active_users_query)
result = await db.fetch_one(active_users_query, (one_day_ago,))
if result:
metrics.set_active_users(result['active_users'], 'daily')
self.logger.debug(f"Updated active users: {result['active_users']}")
@@ -55,16 +60,19 @@ class UserMetricsCollector:
import asyncio
from concurrent.futures import ThreadPoolExecutor
current_timestamp = int(time.time())
one_day_ago = current_timestamp - (24 * 60 * 60) # 24 часа назад
active_users_query = """
SELECT COUNT(DISTINCT user_id) as active_users
FROM our_users
WHERE date_changed > datetime('now', '-1 day')
WHERE date_changed > ?
"""
def sync_db_query():
try:
db.connect()
db.cursor.execute(active_users_query)
db.cursor.execute(active_users_query, (one_day_ago,))
result = db.cursor.fetchone()
return result[0] if result else 0
finally:
@@ -172,11 +180,24 @@ class MetricsExporter:
async def stop(self):
"""Stop the metrics server."""
if self.site:
await self.site.stop()
if self.runner:
await self.runner.cleanup()
self.logger.info("Metrics server stopped")
try:
if self.site:
await self.site.stop()
self.logger.info("Metrics server site stopped")
if self.runner:
await self.runner.cleanup()
self.logger.info("Metrics server runner cleaned up")
except Exception as e:
self.logger.error(f"Error stopping metrics server: {e}")
finally:
# Очищаем ссылки
self.site = None
self.runner = None
# Даем время на закрытие всех соединений
await asyncio.sleep(0.1)
self.logger.info("Metrics server stopped")
async def metrics_handler(self, request: web.Request) -> web.Response:
"""Handle /metrics endpoint for Prometheus."""
@@ -249,10 +270,21 @@ class MetricsManager:
async def stop(self):
"""Stop metrics collection and export."""
try:
await self.collector.stop()
await self.exporter.stop()
self.logger.info("Metrics manager stopped successfully")
# Останавливаем background collector
if hasattr(self, 'collector'):
await self.collector.stop()
self.logger.info("Background metrics collector stopped")
# Останавливаем exporter
if hasattr(self, 'exporter'):
await self.exporter.stop()
self.logger.info("Metrics exporter stopped")
except Exception as e:
self.logger.error(f"Error stopping metrics manager: {e}")
raise
# Не вызываем raise, чтобы не прерывать процесс завершения
finally:
# Очищаем ссылки
self.collector = None
self.exporter = None
self.logger.info("Metrics manager stopped successfully")