Files
telegram-helper-bot/helper_bot/handlers/callback/services.py
Andrey 1c6a37bc12 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.
2025-09-02 18:22:02 +03:00

257 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import html
from datetime import datetime, timedelta
from typing import Dict, Any
from aiogram import Bot
from aiogram.types import CallbackQuery
from helper_bot.utils.helper_func import (
send_text_message, send_photo_message, send_video_message,
send_video_note_message, send_audio_message, send_voice_message,
send_media_group_to_channel, delete_user_blacklist
)
from helper_bot.keyboards.keyboards import create_keyboard_for_ban_reason
from .exceptions import (
UserBlockedBotError, PostNotFoundError, UserNotFoundError,
PublishError, BanError
)
from .constants import (
CONTENT_TYPE_TEXT, CONTENT_TYPE_PHOTO, CONTENT_TYPE_VIDEO,
CONTENT_TYPE_VIDEO_NOTE, CONTENT_TYPE_AUDIO, CONTENT_TYPE_VOICE,
CONTENT_TYPE_MEDIA_GROUP, MESSAGE_POST_PUBLISHED, MESSAGE_POST_DECLINED,
MESSAGE_USER_BANNED_SPAM, ERROR_BOT_BLOCKED
)
from logs.custom_logger import logger
class PostPublishService:
def __init__(self, bot: Bot, db, settings: Dict[str, Any]):
# 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:
"""Основной метод публикации поста"""
content_type = call.message.content_type
if content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP:
await self._publish_text_post(call)
elif content_type == CONTENT_TYPE_PHOTO:
await self._publish_photo_post(call)
elif content_type == CONTENT_TYPE_VIDEO:
await self._publish_video_post(call)
elif content_type == CONTENT_TYPE_VIDEO_NOTE:
await self._publish_video_note_post(call)
elif content_type == CONTENT_TYPE_AUDIO:
await self._publish_audio_post(call)
elif content_type == CONTENT_TYPE_VOICE:
await self._publish_voice_post(call)
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
await self._publish_media_group(call)
else:
raise PublishError(f"Неподдерживаемый тип контента: {content_type}")
async def _publish_text_post(self, call: CallbackQuery) -> None:
"""Публикация текстового поста"""
text_post = html.escape(str(call.message.text))
author_id = 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)
logger.info(f'Текст сообщения опубликован в канале {self.main_public}.')
async def _publish_photo_post(self, call: CallbackQuery) -> None:
"""Публикация поста с фото"""
text_post_with_photo = html.escape(str(call.message.caption))
author_id = 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)
logger.info(f'Пост с фото опубликован в канале {self.main_public}.')
async def _publish_video_post(self, call: CallbackQuery) -> None:
"""Публикация поста с видео"""
text_post_with_photo = html.escape(str(call.message.caption))
author_id = 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)
logger.info(f'Пост с видео опубликован в канале {self.main_public}.')
async def _publish_video_note_post(self, call: CallbackQuery) -> None:
"""Публикация поста с кружком"""
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)
logger.info(f'Пост с кружком опубликован в канале {self.main_public}.')
async def _publish_audio_post(self, call: CallbackQuery) -> None:
"""Публикация поста с аудио"""
text_post_with_photo = html.escape(str(call.message.caption))
author_id = 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)
logger.info(f'Пост с аудио опубликован в канале {self.main_public}.')
async def _publish_voice_post(self, call: CallbackQuery) -> None:
"""Публикация поста с войсом"""
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)
logger.info(f'Пост с войсом опубликован в канале {self.main_public}.')
async def _publish_media_group(self, call: CallbackQuery) -> None:
"""Публикация медиагруппы"""
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 = await self._get_author_id_for_media_group(call.message.message_id)
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:
"""Отклонение поста"""
content_type = call.message.content_type
if (content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP) or \
content_type in [CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO, CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
await self._decline_single_post(call)
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
await self._decline_media_group(call)
else:
raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}")
async def _decline_single_post(self, call: CallbackQuery) -> None:
"""Отклонение одиночного поста"""
author_id = 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:
if str(e) == ERROR_BOT_BLOCKED:
raise UserBlockedBotError("Пользователь заблокировал бота")
raise
logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
async def _decline_media_group(self, call: CallbackQuery) -> None:
"""Отклонение медиагруппы"""
post_ids = 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 = 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:
if str(e) == ERROR_BOT_BLOCKED:
raise UserBlockedBotError("Пользователь заблокировал бота")
raise
async def _get_author_id(self, message_id: int) -> int:
"""Получение ID автора по 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
async def _get_author_id_for_media_group(self, message_id: int) -> int:
"""Получение 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._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:
if str(e) == ERROR_BOT_BLOCKED:
raise UserBlockedBotError("Пользователь заблокировал бота")
raise
async def _delete_media_group_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
"""Удаление медиагруппы и уведомление автора"""
post_ids = 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._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:
if str(e) == ERROR_BOT_BLOCKED:
raise UserBlockedBotError("Пользователь заблокировал бота")
raise
class BanService:
def __init__(self, bot: Bot, db, settings: Dict[str, Any]):
self.bot = bot
self.db = db
self.settings = settings
self.group_for_posts = settings['Telegram']['group_for_posts']
self.important_logs = settings['Telegram']['important_logs']
async def ban_user_from_post(self, call: CallbackQuery) -> None:
"""Бан пользователя за спам"""
author_id = await self.db.get_author_id_by_message_id(call.message.message_id)
if not author_id:
raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}")
current_date = datetime.now()
date_to_unban = int((current_date + timedelta(days=7)).timestamp())
await self.db.set_user_blacklist(
user_id=author_id,
user_name=None,
message_for_user="Спам",
date_to_unban=date_to_unban
)
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))
except Exception as e:
if str(e) == ERROR_BOT_BLOCKED:
raise UserBlockedBotError("Пользователь заблокировал бота")
raise
logger.info(f"Пользователь {author_id} заблокирован за спам до {date_str}")
async def ban_user(self, user_id: str, user_name: str) -> str:
"""Бан пользователя по ID"""
user_name = await self.db.get_username(int(user_id))
if not user_name:
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
return user_name
async def unlock_user(self, user_id: str) -> str:
"""Разблокировка пользователя"""
user_name = 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