from telebot.async_telebot import AsyncTeleBot from telebot.types import Message, MessageReactionUpdated, ReactionTypeEmoji import asyncio import logging from collections import OrderedDict 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__) # Кэш для хранения message_id -> (user_id, message_thread_id) (последние 1000 сообщений) # Используем OrderedDict для автоматического удаления старых записей _message_cache = OrderedDict() _MAX_CACHE_SIZE = 1000 def _cache_message(chat_id: int, message_id: int, user_id: int, message_thread_id: int = None): """Добавляет сообщение в кэш""" key = f"{chat_id}:{message_id}" _message_cache[key] = (user_id, message_thread_id) # Удаляем старые записи, если кэш переполнен if len(_message_cache) > _MAX_CACHE_SIZE: _message_cache.popitem(last=False) def _get_cached_message(chat_id: int, message_id: int): """Получает (user_id, message_thread_id) из кэша""" key = f"{chat_id}:{message_id}" return _message_cache.get(key) def register_handlers(bot: AsyncTeleBot): """Регистрирует обработчики для отслеживания благодарностей""" logger.info("Регистрация обработчика благодарностей (karma_tracker)") @bot.message_reaction_handler(func=lambda m: True) async def handle_reaction(reaction: MessageReactionUpdated): """ Обрабатывает реакции на сообщения. Реакции работают как переключатель: - Поставил 👍 → +1 карма - Убрал 👍 → -1 карма - Поставил 👎 → -1 карма - Убрал 👎 → +1 карма """ 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 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 # Проверяем новые реакции new_thumbs_up = False new_thumbs_down = 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 # Определяем изменение кармы 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 = "убрал 👎" # Если нет изменений - выходим 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 секунд await asyncio.sleep(10) try: await bot.delete_message(chat_id, sent_message.message_id) logger.info(f"[KARMA] Уведомление удалено") except Exception as e: logger.error(f"Не удалось удалить уведомление о карме: {e}") 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 секунд await asyncio.sleep(25) try: await bot.delete_message(chat_id, sent_message.message_id) except Exception as e: logger.error(f"Не удалось удалить уведомление о карме: {e}") except Exception as e: logger.error(f"Ошибка при обработке благодарности: {e}", exc_info=True)