forked from Muzifs/LGBot
321 lines
16 KiB
Python
321 lines
16 KiB
Python
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) |