from telebot.async_telebot import AsyncTeleBot from telebot.types import Message, MessageReactionUpdated, ReactionTypeEmoji import asyncio import logging from database import db from thank_words import contains_thank_word from bad_words import contains_bad_word from config import THANK_COOLDOWN logger = logging.getLogger(__name__) # Фоновая задача для автоочистки старых сообщений _cleanup_task = None def _cache_message(chat_id: int, message_id: int, user_id: int, message_thread_id: int = None): """Добавляет сообщение в кэш БД""" db.cache_message(chat_id, message_id, user_id, message_thread_id) def _get_cached_message(chat_id: int, message_id: int): """Получает (user_id, message_thread_id) из кэша БД""" return db.get_cached_message(chat_id, message_id) async def _cleanup_old_cache(): """Фоновая задача для очистки старых сообщений из кэша каждый час""" while True: try: await asyncio.sleep(3600) # Ждём 1 час deleted = db.cleanup_old_messages(max_age_seconds=86400) # Удаляем старше 24 часов cache_size = db.get_cache_size() logger.info(f"[CACHE CLEANUP] Размер кэша: {cache_size} сообщений") except Exception as e: logger.error(f"[CACHE CLEANUP] Ошибка очистки кэша: {e}", exc_info=True) def register_handlers(bot: AsyncTeleBot): """Регистрирует обработчики для отслеживания благодарностей""" logger.info("Регистрация обработчика благодарностей (karma_tracker)") # Запускаем фоновую задачу очистки старых сообщений из кэша global _cleanup_task if _cleanup_task is None or _cleanup_task.done(): _cleanup_task = asyncio.create_task(_cleanup_old_cache()) cache_size = db.get_cache_size() logger.info(f"[CACHE] Запущена автоочистка кэша. Текущий размер: {cache_size} сообщений") @bot.message_reaction_handler(func=lambda m: True) async def handle_reaction(reaction: MessageReactionUpdated): """ Обрабатывает реакции на сообщения. Реакции работают как переключатель: - Поставил 👍 → +1 карма | Убрал 👍 → -1 карма - Поставил 👎 → -1 карма | Убрал 👎 → +1 карма - Поставил 🔥 → +2 кармы | Убрал 🔥 → -2 кармы - Поставил ❤ → +5 кармы | Убрал ❤ → -5 кармы - Поставил ❤‍🔥 → +10 кармы | Убрал ❤‍🔥 → -10 кармы """ try: logger.info(f"[KARMA] Получена реакция от {reaction.user.id}") # Проверяем, что это групповой чат if reaction.chat.type not in ['group', 'supergroup']: logger.info(f"[KARMA] Пропуск реакции - не групповой чат") return from_user = reaction.user chat_id = reaction.chat.id # Получаем автора сообщения и топик из кэша cached_data = _get_cached_message(chat_id, reaction.message_id) if not cached_data: logger.warning(f"[KARMA] Сообщение {reaction.message_id} не найдено в кэше") return to_user_id, message_thread_id = cached_data # Защита от самооценки if from_user.id == to_user_id: logger.info(f"Пользователь {from_user.id} попытался поставить реакцию на своё сообщение") return # Получаем информацию о пользователе из БД to_user_info = db.get_user(to_user_id) if not to_user_info: logger.warning(f"[KARMA] Пользователь {to_user_id} не найден в БД") return # Проверяем старые реакции old_thumbs_up = False old_thumbs_down = False old_heart = False old_fire_heart = False old_fire = False if reaction.old_reaction: for react in reaction.old_reaction: if isinstance(react, ReactionTypeEmoji): if react.emoji == "👍": old_thumbs_up = True elif react.emoji == "👎": old_thumbs_down = True elif react.emoji == "❤": old_heart = True elif react.emoji == "❤‍🔥": old_fire_heart = True elif react.emoji == "🔥": old_fire = True # Проверяем новые реакции new_thumbs_up = False new_thumbs_down = False new_heart = False new_fire_heart = False new_fire = False if reaction.new_reaction: for react in reaction.new_reaction: if isinstance(react, ReactionTypeEmoji): if react.emoji == "👍": new_thumbs_up = True elif react.emoji == "👎": new_thumbs_down = True elif react.emoji == "❤": new_heart = True elif react.emoji == "❤‍🔥": new_fire_heart = True elif react.emoji == "🔥": new_fire = True # Определяем изменение кармы karma_change = 0 action_emoji = "" action_text = "" # Логика изменения кармы if new_thumbs_up and not old_thumbs_up: # Добавили 👍 karma_change = 1 action_emoji = "👍" action_text = "поставил 👍" elif old_thumbs_up and not new_thumbs_up: # Убрали 👍 karma_change = -1 action_emoji = "👍" action_text = "убрал 👍" elif new_thumbs_down and not old_thumbs_down: # Добавили 👎 karma_change = -1 action_emoji = "👎" action_text = "поставил 👎" elif old_thumbs_down and not new_thumbs_down: # Убрали 👎 karma_change = 1 action_emoji = "👎" action_text = "убрал 👎" elif new_heart and not old_heart: # Добавили ❤ karma_change = 5 action_emoji = "❤" action_text = "поставил ❤" elif old_heart and not new_heart: # Убрали ❤ karma_change = -5 action_emoji = "❤" action_text = "убрал ❤" elif new_fire_heart and not old_fire_heart: # Добавили ❤‍🔥 karma_change = 10 action_emoji = "❤‍🔥" action_text = "поставил ❤‍🔥" elif old_fire_heart and not new_fire_heart: # Убрали ❤‍🔥 karma_change = -10 action_emoji = "❤‍🔥" action_text = "убрал ❤‍🔥" elif new_fire and not old_fire: # Добавили 🔥 karma_change = 2 action_emoji = "🔥" action_text = "поставил 🔥" elif old_fire and not new_fire: # Убрали 🔥 karma_change = -2 action_emoji = "🔥" action_text = "убрал 🔥" # Если нет изменений - выходим if karma_change == 0: logger.info(f"[KARMA] Нет изменений в реакциях") return logger.info(f"[KARMA] {action_text} от {from_user.id} для {to_user_id}, изменение кармы: {karma_change}") # Изменяем карму db.add_karma(to_user_id, chat_id, karma_change) # Получаем новую карму new_karma = db.get_karma(to_user_id, chat_id) # Формируем имя пользователя (из БД: id, nickname, tag) to_user_display = f"@{to_user_info[2]}" if to_user_info[2] else to_user_info[1] # Отправляем уведомление karma_sign = f"+{karma_change}" if karma_change > 0 else str(karma_change) change_word = "увеличена" if karma_change > 0 else "уменьшена" response = f"{action_emoji} Карма пользователя {to_user_display} {change_word} ({karma_sign})! Текущая карма: {new_karma}" logger.info(f"[KARMA] Отправка уведомления в чат {chat_id}") try: sent_message = await bot.send_message( chat_id, response, message_thread_id=message_thread_id ) logger.info(f"[KARMA] Уведомление отправлено успешно, message_id={sent_message.message_id}") # Удаляем уведомление через 10 секунд В ФОНЕ (не блокируя обработку других реакций) async def delete_notification(): try: await asyncio.sleep(10) await bot.delete_message(chat_id, sent_message.message_id) logger.info(f"[KARMA] Уведомление удалено") except Exception as e: logger.error(f"Не удалось удалить уведомление о карме: {e}") # Запускаем удаление в фоне asyncio.create_task(delete_notification()) except Exception as e: logger.error(f"Ошибка отправки уведомления о карме: {e}", exc_info=True) except Exception as e: logger.error(f"Ошибка при обработке реакции: {e}", exc_info=True) @bot.message_handler(func=lambda message: message.reply_to_message is not None and message.text and not message.text.startswith('/')) async def handle_thank_message(message: Message): """ Обрабатывает сообщения, которые являются ответами на другие сообщения. Если сообщение содержит благодарность, начисляет карму автору оригинального сообщения. """ try: logger.info(f"[KARMA] Получено reply-сообщение: {message.text[:50]}") # Проверяем, что это групповой чат if message.chat.type not in ['group', 'supergroup']: logger.info(f"[KARMA] Пропуск - не групповой чат: {message.chat.type}") return # ВАЖНО: В топиках каждое сообщение технически является reply на первое сообщение топика # Проверяем, что это реальный reply на сообщение пользователя, а не просто сообщение в топике if message.is_topic_message and message.reply_to_message.message_id == message.message_thread_id: logger.info(f"[KARMA] Пропуск - это сообщение в топике (не reply на пользователя)") return # Проверяем наличие благодарственных слов if not contains_thank_word(message.text): logger.info(f"[KARMA] Нет слов благодарности в: {message.text[:50]}") return logger.info(f"[KARMA] Обнаружена благодарность от {message.from_user.id}: {message.text[:50]}") # Проверяем, что в сообщении нет мата (не начисляем карму за мат) if contains_bad_word(message.text): logger.info(f"Пользователь {message.from_user.id} написал благодарность с матом - карма не начислена") return from_user = message.from_user to_user = message.reply_to_message.from_user chat_id = message.chat.id # Защита от самоблагодарности if from_user.id == to_user.id: logger.info(f"Пользователь {from_user.id} попытался поблагодарить сам себя") return # Проверяем, не является ли благодарность ботам if to_user.is_bot: logger.info(f"Пользователь {from_user.id} попытался поблагодарить бота") return # Атомарно проверяем кулдаун и записываем благодарность # Это предотвращает race condition при параллельных запросах if not db.try_add_karma_thank(from_user.id, to_user.id, chat_id, THANK_COOLDOWN): logger.info(f"Пользователь {from_user.id} уже благодарил {to_user.id} недавно") # Молча игнорируем, чтобы не спамить return # Определяем количество кармы: x2 если есть восклицательный знак karma_amount = 2 if '!' in message.text else 1 # Начисляем карму (благодарность уже записана атомарно выше) db.add_karma(to_user.id, chat_id, karma_amount) # Получаем новую карму пользователя new_karma = db.get_karma(to_user.id, chat_id) # Формируем имя пользователя для отображения to_user_name = to_user.first_name if to_user.username: to_user_display = f"@{to_user.username}" else: to_user_display = to_user_name # Отправляем уведомление с указанием количества кармы karma_emoji = "👍👍" if karma_amount == 2 else "👍" karma_change = f"+{karma_amount}" response = f"{karma_emoji} Карма пользователя {to_user_display} увеличена ({karma_change})! Текущая карма: {new_karma}" sent_message = await bot.reply_to(message, response) logger.info(f"Пользователь {from_user.id} поблагодарил {to_user.id}, карма: {new_karma}") # Удаляем уведомление через 25 секунд В ФОНЕ async def delete_thank_notification(): try: await asyncio.sleep(25) await bot.delete_message(chat_id, sent_message.message_id) except Exception as e: logger.error(f"Не удалось удалить уведомление о карме: {e}") # Запускаем удаление в фоне asyncio.create_task(delete_thank_notification()) except Exception as e: logger.error(f"Ошибка при обработке благодарности: {e}", exc_info=True)