diff --git a/src/config.py b/src/config.py index a3c27ef..b46f6ec 100644 --- a/src/config.py +++ b/src/config.py @@ -27,6 +27,9 @@ COMMAND_MESSAGES = { "• /unmute help - Снятие мута\n" "• /ban help - Инструкция по бану\n" "• /unban help - Снятие бана\n\n" + "⭐ Система кармы:\n" + "• /karma - Просмотр кармы\n" + "• /top - Топ пользователей по карме\n\n" "ℹ️ Для подробностей по конкретной команде используйте: /команда help" ), 'manual_mute': ( @@ -155,6 +158,30 @@ COMMAND_MESSAGES = { ), 'warned': '⚠️ Пользователь получил предупреждение.', 'warned_auto_mute_day': '⚠️ Пользователь получил предупреждение и автомут на 1 день (повторное нарушение за неделю).', - 'warned_auto_mute_week': '⚠️ Пользователь получил предупреждение и автомут на 7 дней (множественные нарушения).' + 'warned_auto_mute_week': '⚠️ Пользователь получил предупреждение и автомут на 7 дней (множественные нарушения).', + 'karma_help': ( + "⭐ Команда /karma\n\n" + "Показывает карму пользователя в этом чате\n\n" + "🎯 Способы использования:\n" + "1. Показать свою карму:\n" + " /karma\n" + "2. По тегу пользователя:\n" + " /karma @username\n" + "3. Ответ на сообщение:\n" + " Ответьте на сообщение: /karma\n\n" + "💡 Как начислить карму?\n" + "Ответьте на сообщение пользователя словами благодарности:\n" + "• спасибо\n" + "• благодарю\n" + "• спс, сенкс, thanks и др.\n\n" + "⏱ Одному пользователю можно давать карму раз в час" + ), + 'top_karma_help': ( + "🏆 Команда /top\n\n" + "Показывает топ-10 пользователей по карме в этом чате\n\n" + "🎯 Использование:\n" + " /top\n\n" + "💡 Система кармы поощряет активных и полезных участников чата!" + ) } \ No newline at end of file diff --git a/src/data/thank_words.json b/src/data/thank_words.json new file mode 100644 index 0000000..0a00c5c --- /dev/null +++ b/src/data/thank_words.json @@ -0,0 +1,25 @@ +{ + "thank_words": [ + "спасибо", + "благодарю", + "спс", + "сенкс", + "сенкью", + "thanks", + "thank you", + "thx", + "ty", + "дякую", + "дзякуй", + "рахмет", + "пасиб", + "пасибо", + "спасибочки", + "благодарочка", + "мерси", + "merci", + "danke", + "gracias", + "grazie" + ] +} \ No newline at end of file diff --git a/src/database.py b/src/database.py index 95822ae..b6ed952 100644 --- a/src/database.py +++ b/src/database.py @@ -45,6 +45,26 @@ class Database: # Инициализация класса FOREIGN KEY (user_id) REFERENCES users (id) ) ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS karma ( + user_id INTEGER NOT NULL, + chat_id INTEGER NOT NULL, + karma_points INTEGER DEFAULT 0, + PRIMARY KEY (user_id, chat_id), + FOREIGN KEY (user_id) REFERENCES users (id) + ) + ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS karma_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_user_id INTEGER NOT NULL, + to_user_id INTEGER NOT NULL, + chat_id INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + FOREIGN KEY (from_user_id) REFERENCES users (id), + FOREIGN KEY (to_user_id) REFERENCES users (id) + ) + ''') connect.commit() # Возвращает соединение с базой данных @@ -218,5 +238,83 @@ class Database: # Инициализация класса logger.info(f"Сброшено {deleted_count} предупреждений пользователя {user_id} в чате {chat_id}") return deleted_count + # Добавляет карму пользователю + def add_karma(self, user_id: int, chat_id: int, amount: int = 1): + with self._get_connection() as connect: + cursor = connect.cursor() + # Проверяем существование записи + cursor.execute('SELECT karma_points FROM karma WHERE user_id = ? AND chat_id = ?', (user_id, chat_id)) + result = cursor.fetchone() + + if result: + # Обновляем существующую карму + new_karma = result[0] + amount + cursor.execute(''' + UPDATE karma + SET karma_points = ? + WHERE user_id = ? AND chat_id = ? + ''', (new_karma, user_id, chat_id)) + else: + # Создаем новую запись + cursor.execute(''' + INSERT INTO karma (user_id, chat_id, karma_points) + VALUES (?, ?, ?) + ''', (user_id, chat_id, amount)) + + connect.commit() + logger.info(f"Пользователю {user_id} добавлено {amount} кармы в чате {chat_id}") + + # Получает карму пользователя + def get_karma(self, user_id: int, chat_id: int) -> int: + with self._get_connection() as connect: + cursor = connect.cursor() + cursor.execute(''' + SELECT karma_points + FROM karma + WHERE user_id = ? AND chat_id = ? + ''', (user_id, chat_id)) + result = cursor.fetchone() + return result[0] if result else 0 + + # Получает топ пользователей по карме + def get_top_karma(self, chat_id: int, limit: int = 10): + with self._get_connection() as connect: + cursor = connect.cursor() + cursor.execute(''' + SELECT k.user_id, u.nickname, u.tag, k.karma_points + FROM karma k + JOIN users u ON k.user_id = u.id + WHERE k.chat_id = ? AND k.karma_points > 0 + ORDER BY k.karma_points DESC + LIMIT ? + ''', (chat_id, limit)) + return cursor.fetchall() + + # Проверяет, может ли пользователь поблагодарить другого (проверка кулдауна) + def can_thank(self, from_user_id: int, to_user_id: int, chat_id: int, cooldown_seconds: int = 3600) -> bool: + import time + with self._get_connection() as connect: + cursor = connect.cursor() + cutoff_time = int(time.time()) - cooldown_seconds + cursor.execute(''' + SELECT COUNT(*) + FROM karma_history + WHERE from_user_id = ? AND to_user_id = ? AND chat_id = ? AND timestamp > ? + ''', (from_user_id, to_user_id, chat_id, cutoff_time)) + result = cursor.fetchone() + return result[0] == 0 if result else True + + # Добавляет запись в историю кармы + def add_karma_history(self, from_user_id: int, to_user_id: int, chat_id: int): + import time + with self._get_connection() as connect: + cursor = connect.cursor() + cursor.execute(''' + INSERT INTO karma_history (from_user_id, to_user_id, chat_id, timestamp) + VALUES (?, ?, ?, ?) + ''', (from_user_id, to_user_id, chat_id, int(time.time()))) + connect.commit() + logger.info(f"Пользователь {from_user_id} поблагодарил {to_user_id} в чате {chat_id}") + # Создаем экземпляр базы данных для импорта в других модулях db = Database() \ No newline at end of file diff --git a/src/main.py b/src/main.py index b891dfe..01ba25c 100644 --- a/src/main.py +++ b/src/main.py @@ -149,6 +149,8 @@ async def setup_bot_commands(): BotCommand("badwords", "Управление списком бранных слов. /badwords help"), BotCommand("reset_violations", "Сбросить счётчик нарушений пользователя"), BotCommand("botdata", "Получить данные бота (только для админов)"), + BotCommand("karma", "Просмотр кармы пользователя"), + BotCommand("top", "Топ-10 пользователей по карме"), ] await bot.set_my_commands(commands) diff --git a/src/modules/karma_commands.py b/src/modules/karma_commands.py new file mode 100644 index 0000000..0299860 --- /dev/null +++ b/src/modules/karma_commands.py @@ -0,0 +1,153 @@ +from telebot.async_telebot import AsyncTeleBot +from telebot.types import Message +import logging + +from database import db + +logger = logging.getLogger(__name__) + +def register_handlers(bot: AsyncTeleBot): + """Регистрирует обработчики команд для системы кармы""" + + @bot.message_handler(commands=['karma', 'rating']) + async def handle_karma_command(message: Message): + """ + Команда /karma - показывает карму пользователя + Использование: + /karma - показать свою карму + /karma @username - показать карму пользователя + /karma (в ответ на сообщение) - показать карму автора сообщения + """ + try: + # Проверяем, что это групповой чат + if message.chat.type not in ['group', 'supergroup']: + await bot.reply_to(message, "❌ Эта команда работает только в групповых чатах") + return + + chat_id = message.chat.id + target_user = None + target_user_id = None + + # Если команда - ответ на сообщение + if message.reply_to_message: + target_user = message.reply_to_message.from_user + target_user_id = target_user.id + + # Если указан username в команде + elif len(message.text.split()) > 1: + username_arg = message.text.split()[1] + # Убираем @ если есть + username = username_arg.lstrip('@') + + # Ищем пользователя в БД + user_data = db.get_user_by_username(username) + if user_data: + target_user_id = user_data[0] + target_user = type('User', (), { + 'id': user_data[0], + 'first_name': user_data[1], + 'username': user_data[2] + })() + else: + await bot.reply_to(message, f"❌ Пользователь @{username} не найден в базе данных") + return + + # Иначе показываем карму отправителя команды + else: + target_user = message.from_user + target_user_id = target_user.id + + # Получаем карму + karma = db.get_karma(target_user_id, chat_id) + + # Формируем имя пользователя + if hasattr(target_user, 'username') and target_user.username: + user_display = f"@{target_user.username}" + else: + user_display = target_user.first_name + + # Определяем эмодзи в зависимости от кармы + if karma == 0: + emoji = "😐" + elif karma < 5: + emoji = "🙂" + elif karma < 10: + emoji = "😊" + elif karma < 20: + emoji = "😄" + elif karma < 50: + emoji = "🌟" + else: + emoji = "⭐" + + response = f"{emoji} Карма пользователя {user_display}: {karma}" + + sent_message = await bot.reply_to(message, response) + + # Удаляем команду и ответ через 10 секунд + import asyncio + await asyncio.sleep(10) + try: + await bot.delete_message(chat_id, message.message_id) + 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"Ошибка при обработке команды /karma: {e}", exc_info=True) + await bot.reply_to(message, "❌ Произошла ошибка при получении кармы") + + @bot.message_handler(commands=['top', 'leaderboard', 'topkarma']) + async def handle_top_command(message: Message): + """ + Команда /top - показывает топ пользователей по карме + """ + try: + # Проверяем, что это групповой чат + if message.chat.type not in ['group', 'supergroup']: + await bot.reply_to(message, "❌ Эта команда работает только в групповых чатах") + return + + chat_id = message.chat.id + + # Получаем топ 10 пользователей + top_users = db.get_top_karma(chat_id, limit=10) + + if not top_users: + await bot.reply_to(message, "📊 В этом чате пока нет пользователей с кармой") + return + + # Формируем сообщение + response = "🏆 Топ-10 пользователей по карме:\n\n" + + medals = ["🥇", "🥈", "🥉"] + + for idx, (user_id, nickname, tag, karma_points) in enumerate(top_users, 1): + # Определяем медаль для топ-3 + if idx <= 3: + medal = medals[idx - 1] + else: + medal = f"{idx}." + + # Формируем отображение пользователя + if tag: + user_display = f"@{tag}" + else: + user_display = nickname or f"ID: {user_id}" + + response += f"{medal} {user_display} — {karma_points} кармы\n" + + sent_message = await bot.reply_to(message, response, parse_mode='HTML') + + # Удаляем команду и ответ через 30 секунд + import asyncio + await asyncio.sleep(30) + try: + await bot.delete_message(chat_id, message.message_id) + 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"Ошибка при обработке команды /top: {e}", exc_info=True) + await bot.reply_to(message, "❌ Произошла ошибка при получении топа") \ No newline at end of file diff --git a/src/modules/karma_tracker.py b/src/modules/karma_tracker.py new file mode 100644 index 0000000..a237766 --- /dev/null +++ b/src/modules/karma_tracker.py @@ -0,0 +1,81 @@ +from telebot.async_telebot import AsyncTeleBot +from telebot.types import Message +import logging + +from database import db +from thank_words import contains_thank_word + +logger = logging.getLogger(__name__) + +# Кулдаун для благодарностей (в секундах) - 1 час +THANK_COOLDOWN = 3600 + +def register_handlers(bot: AsyncTeleBot): + """Регистрирует обработчики для отслеживания благодарностей""" + + @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: + # Проверяем, что это групповой чат + if message.chat.type not in ['group', 'supergroup']: + return + + # Проверяем наличие благодарственных слов + if not contains_thank_word(message.text): + 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 + + # Проверяем кулдаун (можно благодарить одного пользователя раз в час) + if not db.can_thank(from_user.id, to_user.id, chat_id, THANK_COOLDOWN): + logger.info(f"Пользователь {from_user.id} уже благодарил {to_user.id} недавно") + # Молча игнорируем, чтобы не спамить + return + + # Начисляем карму + db.add_karma(to_user.id, chat_id, 1) + db.add_karma_history(from_user.id, to_user.id, chat_id) + + # Получаем новую карму пользователя + 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 + + # Отправляем уведомление + response = f"👍 Карма пользователя {to_user_display} увеличена! Текущая карма: {new_karma}" + + sent_message = await bot.reply_to(message, response) + + logger.info(f"Пользователь {from_user.id} поблагодарил {to_user.id}, карма: {new_karma}") + + # Удаляем уведомление через 5 секунд + import asyncio + await asyncio.sleep(5) + 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) \ No newline at end of file diff --git a/src/thank_words.py b/src/thank_words.py new file mode 100644 index 0000000..a2363ab --- /dev/null +++ b/src/thank_words.py @@ -0,0 +1,109 @@ +import json +import os +import logging + +logger = logging.getLogger(__name__) + +# Путь к файлу с благодарственными словами +THANK_WORDS_FILE = os.path.join(os.path.dirname(__file__), 'data', 'thank_words.json') + +# Кэш для быстрой проверки +_thank_words_cache = None + +def _load_thank_words(): + """Загружает список благодарственных слов из файла""" + global _thank_words_cache + try: + with open(THANK_WORDS_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + _thank_words_cache = [word.lower() for word in data.get('thank_words', [])] + logger.info(f"Загружено {len(_thank_words_cache)} благодарственных слов") + return _thank_words_cache + except FileNotFoundError: + logger.error(f"Файл {THANK_WORDS_FILE} не найден") + _thank_words_cache = [] + return [] + except json.JSONDecodeError as e: + logger.error(f"Ошибка разбора JSON: {e}") + _thank_words_cache = [] + return [] + +def get_thank_words(): + """Возвращает список благодарственных слов (с кэшированием)""" + global _thank_words_cache + if _thank_words_cache is None: + _load_thank_words() + return _thank_words_cache + +def contains_thank_word(text: str) -> bool: + """ + Проверяет, содержит ли текст благодарственные слова + + Args: + text: Текст для проверки + + Returns: + True если найдено хотя бы одно благодарственное слово + """ + if not text: + return False + + text_lower = text.lower() + thank_words = get_thank_words() + + # Разбиваем текст на слова для проверки + words = text_lower.split() + + # Проверяем каждое слово и фразы из 2 слов + for i, word in enumerate(words): + # Очищаем от знаков препинания + clean_word = ''.join(c for c in word if c.isalnum()) + if clean_word in thank_words: + return True + + # Проверяем фразы из двух слов (например, "thank you") + if i < len(words) - 1: + two_word_phrase = clean_word + ' ' + ''.join(c for c in words[i+1] if c.isalnum()) + if two_word_phrase in thank_words: + return True + + return False + +def get_thank_words_from_text(text: str) -> list: + """ + Возвращает список найденных благодарственных слов в тексте + + Args: + text: Текст для анализа + + Returns: + Список найденных благодарственных слов + """ + if not text: + return [] + + text_lower = text.lower() + thank_words = get_thank_words() + found_words = [] + + words = text_lower.split() + + for i, word in enumerate(words): + clean_word = ''.join(c for c in word if c.isalnum()) + if clean_word in thank_words and clean_word not in found_words: + found_words.append(clean_word) + + # Проверяем фразы из двух слов + if i < len(words) - 1: + clean_next = ''.join(c for c in words[i+1] if c.isalnum()) + two_word_phrase = clean_word + ' ' + clean_next + if two_word_phrase in thank_words and two_word_phrase not in found_words: + found_words.append(two_word_phrase) + + return found_words + +def reload_thank_words(): + """Перезагружает список благодарственных слов из файла""" + global _thank_words_cache + _thank_words_cache = None + return _load_thank_words() \ No newline at end of file