From de13bc3e31fb0c2601e516d17d41f6a0eb42ea2a Mon Sep 17 00:00:00 2001 From: FerraSoft Date: Thu, 16 Oct 2025 15:17:58 +0300 Subject: [PATCH] Version 1.3.0 - Bug fixes and donation system improvements --- README.md | 55 +- TODO.md | 92 +++ __pycache__/database_sqlite.cpython-310.pyc | Bin 15862 -> 20839 bytes bot.py | 852 +++++++++++++++++++- database_sqlite.py | 150 ++++ messages.py | 60 ++ requirements.txt | 4 +- scheduler.py | 2 +- 8 files changed, 1196 insertions(+), 19 deletions(-) create mode 100644 TODO.md diff --git a/README.md b/README.md index bed0c80..2b8d919 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,45 @@ Многофункциональный Telegram бот на Python с системой ранжирования пользователей, играми и интеграцией с внешними API. +## 📋 История версий + +### Версия 1.3.0 (16 октября 2025) +**Дата релиза:** 16.10.2025 + +#### 🐛 Исправления ошибок +- ✅ Исправлена ошибка с повторяющимся сообщением о "первом сообщении" при вводе суммы доната +- ✅ Исправлена проблема с кодировкой Unicode символов при запуске бота на Windows +- ✅ Исправлена система донатов - теперь корректно работает кнопка "Другая сумма" + +#### ✨ Новые функции +- ✅ Добавлена команда `/donate` для поддержки проекта +- ✅ Добавлена информация о донатах в справку `/help` +- ✅ Улучшена система достижений - достижения разблокируются только один раз + +#### 🔧 Технические улучшения +- ✅ Оптимизирована логика проверки достижений в базе данных +- ✅ Добавлена проверка существующих достижений перед разблокировкой +- ✅ Улучшена обработка ошибок в системе донатов + +### Версия 1.2.0 (Предыдущая версия) +- 🚀 Добавлены расширенные игры: 2048, Тетрис, Змейка +- 🎯 Улучшена система достижений +- 📅 Расширенная система планировщика постов + ## Функции ### Основные возможности - ✅ Отвечать на сообщения пользователей предопределенными ответами - ✅ Обрабатывать инлайновые запросы и команды -- ✅ Система ранжирования пользователей за участие в чате (хранение в PostgreSQL) -- ✅ Приветствие новых пользователей при добавлении в группу +- ✅ Система ранжирования пользователей за участие в чате (хранение в SQLite) +- ✅ Приветствие новых пользователей при добавлении в группу с правилами группы +- ✅ Автоматическое удаление приветственных сообщений через 2 минуты +- ✅ Интеграция донатов в приветственные сообщения ### Игровые элементы -- 🎮 Мини-игры: Угадай число, Камень-ножницы-бумага -- 🧠 Викторина с вопросами +- 🎮 Мини-игры: Камень-ножницы-бумага, Крестики-нолики, Викторина, Морской бой +- 🎯 Расширенные игры: 2048, Тетрис, Змейка +- 🧠 Система достижений и начисления очков - Начисление дополнительных очков за участие в играх ### Администрирование чата @@ -33,6 +61,13 @@ - 📰 Последние новости (NewsAPI) - 🌐 Перевод текста (Google Translate) - базовая реализация +### Система приветствий и донатов +- 🎉 Автоматическое приветствие новых пользователей с правилами группы +- ⏰ Автоматическое удаление приветственных сообщений через 2 минуты +- 👥 Обработка нескольких пользователей (удаление старого, создание нового приветствия) +- 💰 Интеграция кнопок донатов прямо в приветственные сообщения +- 🎯 Быстрый доступ к помощи через приветственные кнопки + ## Установка и настройка ### 1. Клонирование репозитория @@ -86,6 +121,7 @@ python bot.py #### Игры - `/play_game` - Запустить мини-игру +- Доступные игры: Камень-ножницы-бумага, Крестики-нолики, Викторина, Морской бой, 2048, Тетрис, Змейка #### Посты по расписанию (только админы) - `/schedule_post [время] [текст]` - Запланировать пост @@ -108,6 +144,8 @@ python bot.py - За выигрыш в мини-игре начисляются дополнительные очки - Военная иерархия званий от Рядового до Маршала - Рейтинг сохраняется в SQLite базе данных +- Начисление очков за вступление в группу +- Система достижений с разблокировкой значков ### Посты по расписанию - Планирование публикации постов в указанное время @@ -117,6 +155,12 @@ python bot.py - Управление постами только администраторами чата - Все посты сохраняются в базе данных с историей публикаций +### Автоматическая система приветствий +- Приветствие новых пользователей с подробными правилами группы +- Автоматическое удаление приветствий через 2 минуты для чистоты чата +- Интеллектуальная обработка нескольких пользователей одновременно +- Интеграция донатов и быстрого доступа к помощи в приветственных сообщениях + #### Примеры использования планировщика: ``` /schedule_post +1h Добро пожаловать в наш чат! @@ -130,9 +174,12 @@ python bot.py telegram_bot/ ├── bot.py # Основной файл бота ├── config.py # Конфигурация (токены, настройки БД) +├── config_local.py # Локальная конфигурация с токенами ├── database_sqlite.py # Работа с SQLite базой данных +├── database.py # Базовые функции работы с БД ├── scheduler.py # Планировщик постов по расписанию ├── migrate_ranks.py # Миграция системы рангов +├── messages.py # Текстовые сообщения и константы бота ├── requirements.txt # Зависимости Python └── README.md # Эта документация ``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..033aef5 --- /dev/null +++ b/TODO.md @@ -0,0 +1,92 @@ +# 📋 TODO - Список задач разработки + +## 🚀 Приоритетные задачи (High Priority) + +### 🔧 Критические исправления +- [ ] Исправить обработку ошибок при недоступности внешних API (OpenWeatherMap, NewsAPI) +- [ ] Добавить валидацию входящих данных для всех команд +- [ ] Улучшить обработку исключений в базе данных + +### ✨ Новые функции +- [ ] Добавить поддержку медиа файлов в планировщик постов (видео, документы) +- [ ] Реализовать многоязычность (i18n) для бота +- [ ] Добавить экспорт статистики в CSV/Excel формат +- [ ] Создать веб-интерфейс для управления ботом + +## 🎯 Средний приоритет (Medium Priority) + +### 🎮 Игры +- [ ] Добавить новые мини-игры (Шахматы, Судоку, Словесные игры) +- [ ] Улучшить ИИ для игр (особенно крестики-нолики) +- [ ] Добавить турнирную систему для игр + +### 💰 Монетизация +- [ ] Интегрировать платежные системы (ЮMoney, QIWI, банковские карты) +- [ ] Добавить премиум функции для донатеров +- [ ] Создать систему подписок + +### 📊 Аналитика +- [ ] Добавить подробную статистику использования команд +- [ ] Создать графики активности пользователей +- [ ] Добавить мониторинг производительности бота + +## 🔮 Низкий приоритет (Low Priority) + +### 🎨 Интерфейс +- [ ] Добавить кастомные клавиатуры для всех команд +- [ ] Улучшить форматирование сообщений (больше эмодзи, форматирование) +- [ ] Добавить интерактивные меню + +### 🔒 Безопасность +- [ ] Добавить rate limiting для команд +- [ ] Реализовать CAPTCHA для новых пользователей +- [ ] Добавить аудит лог для действий админов + +### 🔧 Техническое +- [ ] Миграция с SQLite на PostgreSQL для больших нагрузок +- [ ] Добавить Redis для кеширования +- [ ] Создать Docker контейнер для развертывания + +## ✅ Завершенные задачи + +### Версия 1.3.0 (16 октября 2025) +- [x] ✅ Исправлена ошибка с повторяющимся "первым сообщением" при донатах +- [x] ✅ Исправлена кодировка Unicode символов при запуске на Windows +- [x] ✅ Исправлена система донатов - кнопка "Другая сумма" +- [x] ✅ Добавлена команда /donate для поддержки проекта +- [x] ✅ Добавлена информация о донатах в справку /help +- [x] ✅ Улучшена система достижений (разблокировка только один раз) +- [x] ✅ Обновлена документация с историей версий + +### Версия 1.2.0 +- [x] ✅ Добавлены расширенные игры: 2048, Тетрис, Змейка +- [x] ✅ Улучшена система достижений +- [x] ✅ Расширенная система планировщика постов + +## 📅 Планирование релизов + +### Следующий релиз (v1.4.0) - Декабрь 2025 +- Цель: Улучшение стабильности и производительности +- Основной фокус: Исправление ошибок и оптимизация + +### Релиз Q1 2026 (v2.0.0) - Март 2026 +- Цель: Значительное расширение функциональности +- Основной фокус: Новые игры и улучшенный интерфейс + +## 🛠 Инструкции для contributors + +1. **Создание задач:** Добавляйте новые задачи в соответствующий раздел с приоритетом +2. **Форматирование:** Используйте формат [ ] для незавершенных и [x] для завершенных задач +3. **Описание:** Добавляйте подробное описание для сложных задач +4. **Присвоение:** Присваивайте задачи себе с помощью @username +5. **Тестирование:** Все изменения должны быть протестированы перед коммитом + +## 📞 Контакты команды разработки + +- **Tech Lead:** [Имя Фамилия] - email@example.com +- **Backend Developer:** [Имя Фамилия] - email@example.com +- **QA Engineer:** [Имя Фамилия] - email@example.com + +--- + +*Последнее обновление: 16 октября 2025* \ No newline at end of file diff --git a/__pycache__/database_sqlite.cpython-310.pyc b/__pycache__/database_sqlite.cpython-310.pyc index 6737f1de14db717308821f57ed92b1812c9b562d..88346a69f708b091f9d2f347a8480dff6c877865 100644 GIT binary patch delta 4114 zcma)9YitzP6`p%%c6N65;q}`FV-J*Iw%Cr1DGwVHxdGZ##3npKaf7y-y<=k=ubr7) zz{{9*Y(i-%CFr95XbZ7ZsR}rfAC00`ZKJe3XV$yS zf-5nW&)m89oNvyY@0@e*{^QgquanSZFsKS}z4iOQ9epEyDYO{+7@l-th%jM@vMrb1 zUER8s`B~r$8S=8NEXYDIQrI>YW|c7Vu_{)5Mi^3A5BHFWzn8Tz4d%maJKshk-EFKL zR_j{P7e}ch7uuN>1Zg8!kA)F z=DR=&nukGmhMuNqQ5c;dCxnPUwHaP)FWT?fZ`(KR>-M{L_L}rNg~sOiUggP9EPixU zKdw*ciKMBm=aq78z%>=+SCwekJ&~NA()lO;wQ?$PY%)I1KUO2W*;~iY`Sz065z)^k z6S3szWMT=Q}??@6WCc4O4PFdp>-hkh$#1 zDla7$va>Z6BKcAF)%r0)Ze~Ak&?tF7+q7~ICBMy%t`aHvV|JpYM~xS##T z+ADI^Gw_BXHEte{8hVWBhB3(h^<|R=Nj9$3YXSaMLkHE8Wl!wDjJhQcqjPg%|_59~sYsuN{hg-i!={1Funi{V4 zuJ(;Y={8Vp0zZY(A3BZbY3_skQ(B#XLQN(P7Xho_D=@@l6`7?0FYyd+{ z6`9DQ^o1$~_>PXcWt4e);|lo&P)=9!U#@EAjHLSbUp8)NeU$oNKzI!X-bSPc z(Zx_^YDe^B)M4G_n0XVUSyzgt{>?KW((9ZY&tQ^EmQ+k~OB@y#q3PyAuR#qrZi)+)CS{q;8zJ_0E3I+d+;dV;oKxzv=+}<7s zS_+u#hifR;9AR^ue%;n`mW6dkYhFU;r!G4p;2V#sx zi@tt{R5k-Zp)eFEjDW0=08yz7bcW#1p|rL{soOd(f@SfNVNfFRd+?BsxEd8Ts-0$) zfo_HL&68n@R3*I5G_}Yuai|j)qyp(oY!r-EOTzmh0c#5-Y=E9Id3q;M>2av^GRg9k zQR$iSoQ9+T3uoj|DW?X&BdErW50xV@oBJ-*=MOaKhx7J3_B*+Gr#&u_0X=hxoJ4(wzOob zu`-jB$=I<{DwrQ(E@%)Y%moSBq$aoG}C5U?(nVTBMYQ~DCWJ;)~ zzqWKXxRsmDU9@lI=CoyZXebY7?U4;6cX7bz10Vw%NgPNnT@*&xBTV@JtO>-+(3_9= zX0Qt|p;2IDoIVe2`Xy-R;E(7ec|JHiYM9CBgl?L#5q-c2fZ;WoryZSnziS_I<_jVS z8hPLL5j~-woHBO&6MXm#(~i!r5YpR9*&08p$H$|w1UnXe^o(G3Y=={9?3lTuqXcA} z0uTee|G%GQq+zw_d7!Jav*@>3aNMx?bA8qOGobZfaLbs5f$=hk(LX_C9uGDMu*>7_ zLtGicT)>?}@JEUEvRm#=nQuVKpN#;HXhC~$+o3%m|s?sVV zQP{fSNhL7GZ5-!I<2{NwiKf>*S})7_!0-0{NdH``z6bJ#rslg5iEbQEJKj-g1PK*pT}FplF0NjC5RVVvjp`sy1DtYkbQ vnj1kV0(_IK_>?F=)z`FE2`eGBLakA20zHl*oP?aC(Ov{_(&qrb(YNb=g|Dh# delta 595 zcmXAlKWLLd7{>3teED*@{P~hp^UqS#AjS`D^s5j_4Z(sT6{Ky*APvP*bE!401i!`> ztF=QrTC^NOmxyt4a_|OG(8WP1C@z8y9qiJj*rj$TqTaPP{CM7%=XqYZt>>_J&)@L* zD3b5Z<(I7!&_GwgINZ_E8sxS+N^29Izy`V|9R*XmlnpybzDOu3&gA zHWCgDpTy5(p9AiQ&Xa9`Z{mHbrH2M&dxchKX2IIG$AAitHK!Fxz-rr%=d6)kID z1Cy_1s&G;K$$Z6dO?=K80E}<+?Rsi5Z%-(Md8rWPFY`LzzhJ-(e%p%jWbOzw#Q9ti z%ZkN(#(h&cV-mb!#rgMq7#8_XzMuac{Gd timedelta(minutes=2): + try: + await self.application.bot.delete_message(chat_id, message_info['message_id']) + expired_chats.append(chat_id) + except Exception as e: + print(f"Не удалось удалить приветственное сообщение в чате {chat_id}: {e}") + expired_chats.append(chat_id) + + # Удаляем записи о просроченных сообщениях + for chat_id in expired_chats: + self.welcome_messages.pop(chat_id, None) + + async def delete_welcome_message(self, chat_id): + """Удаление приветственного сообщения для конкретного чата""" + if chat_id in self.welcome_messages: + try: + message_info = self.welcome_messages[chat_id] + await self.application.bot.delete_message(chat_id, message_info['message_id']) + except Exception as e: + print(f"Не удалось удалить приветственное сообщение в чате {chat_id}: {e}") + finally: + self.welcome_messages.pop(chat_id, None) + + async def set_welcome_message(self, chat_id, message_id): + """Сохранение информации о приветственном сообщении""" + self.welcome_messages[chat_id] = { + 'message_id': message_id, + 'timestamp': datetime.now() + } + async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработчик команды /start""" + # Запускаем задачу очистки приветственных сообщений при первом использовании + if self.cleanup_task is None or self.cleanup_task.done(): + self.start_cleanup_task() + user = update.effective_user db.add_user(user.id, user.username, user.first_name, user.last_name) @@ -135,7 +220,15 @@ class TelegramBot: try: target_user_id = int(target_user) user_info = db.get_user_info(target_user_id) + achievements = db.get_user_achievements(target_user_id) + total_donations = db.get_total_donations(target_user_id) if user_info: + # Формируем строку значков + badge_str = "" + for achievement_type, _ in achievements: + if achievement_type in ACHIEVEMENT_BADGES: + badge_str += ACHIEVEMENT_BADGES[achievement_type] + " " + info_text = f""" 👤 Информация о пользователе: @@ -155,6 +248,8 @@ ID: {user_info['ID']} Очки: {user_info['Очки']} Предупреждений: {user_info['Предупреждений']} Роль: {user_info['Роль']} +{f"🏆 Достижения: {badge_str.strip()}" if badge_str else ""} +{f"💰 Всего донатов: {total_donations} RUB" if total_donations > 0 else ""} """ else: info_text = f"Пользователь с ID {target_user_id} не найден." @@ -164,8 +259,23 @@ ID: {user_info['ID']} # Обычный пользователь запрашивает свою информацию db.add_user(user.id, user.username, user.first_name, user.last_name) user_info = db.get_user_info(user.id) + achievements = db.get_user_achievements(user.id) + total_donations = db.get_total_donations(user.id) if user_info: + # Формируем строку значков + badge_str = "" + achievement_list = [] + for achievement_type, unlocked_at in achievements: + if achievement_type in ACHIEVEMENT_BADGES: + badge_str += ACHIEVEMENT_BADGES[achievement_type] + " " + achievement_list.append(f"{ACHIEVEMENT_BADGES[achievement_type]} {achievement_type.replace('_', ' ').title()}") + + # Создаем список достижений отдельно + achievements_text = "" + if achievement_list: + achievements_text = f"\n📋 Список достижений:\n" + '\n'.join(achievement_list) + info_text = f""" 👤 Информация о вас: @@ -185,6 +295,9 @@ ID: {user_info['ID']} Очки: {user_info['Очки']} Предупреждений: {user_info['Предупреждений']} Роль: {user_info['Роль']} +{f"🏆 Достижения: {badge_str.strip()}" if badge_str else ""} +{f"💰 Всего донатов: {total_donations} RUB" if total_donations > 0 else ""} +{achievements_text} """ else: info_text = USER_MESSAGES['info_not_found'] @@ -259,19 +372,14 @@ ID: {user_info['ID']} async def play_game(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Запустить мини-игру""" - keyboard = [ - [InlineKeyboardButton("Камень-ножницы-бумага", callback_data='game_rps')], - [InlineKeyboardButton("Крестики-нолики", callback_data='game_tic_tac_toe')], - [InlineKeyboardButton("Викторина", callback_data='game_quiz')], - [InlineKeyboardButton("⬅️ Назад", callback_data='cmd_start')] - ] - reply_markup = InlineKeyboardMarkup(keyboard) - keyboard = [ [InlineKeyboardButton("Камень-ножницы-бумага", callback_data='game_rps')], [InlineKeyboardButton("Крестики-нолики", callback_data='game_tic_tac_toe')], [InlineKeyboardButton("Викторина", callback_data='game_quiz')], [InlineKeyboardButton("Морской бой", callback_data='game_battleship')], + [InlineKeyboardButton("2048", callback_data='game_2048')], + [InlineKeyboardButton("Тетрис", callback_data='game_tetris')], + [InlineKeyboardButton("Змейка", callback_data='game_snake')], [InlineKeyboardButton("⬅️ Назад", callback_data='cmd_start')] ] reply_markup = InlineKeyboardMarkup(keyboard) @@ -297,8 +405,20 @@ ID: {user_info['ID']} await self.start_quiz_game(query, context) elif query.data == 'game_battleship': await self.start_battleship_game(query, context) + elif query.data == 'game_2048': + await self.start_2048_game(query, context) + elif query.data == 'game_tetris': + await self.start_tetris_game(query, context) + elif query.data == 'game_snake': + await self.start_snake_game(query, context) elif query.data.startswith('bs_'): await self.handle_battleship_shot(query, context) + elif query.data.startswith('2048_'): + await self.handle_2048_move(query, context) + elif query.data.startswith('tetris_'): + await self.handle_tetris_move(query, context) + elif query.data.startswith('snake_'): + await self.handle_snake_move(query, context) elif query.data.startswith('rps_'): await self.handle_rps(query, context) elif query.data.startswith('tictactoe_'): @@ -313,6 +433,8 @@ ID: {user_info['ID']} await self.add_schedule_image(query, context) elif query.data == 'cancel_schedule': await self.cancel_schedule_post(query, context) + elif query.data.startswith('donate_'): + await self.handle_donation_callback(query, context) async def start_rps_game(self, query, context): @@ -1176,8 +1298,18 @@ ID: {user_info['ID']} await self.handle_text_edit(update, context) return + # Проверяем, ждет ли пользователь ввода суммы доната + if context.user_data.get('waiting_for_donation_amount'): + await self.handle_donation_amount_input(update, context) + return + message_text = update.message.text.lower() + # Автофильтр нецензурной лексики + if self.check_profanity(message_text): + await self.handle_profanity_violation(update, user) + return + # Добавление пользователя в базу данных db.add_user(user.id, user.username, user.first_name, user.last_name) @@ -1261,14 +1393,103 @@ ID: {user_info['ID']} context.user_data.pop('waiting_for_text_edit', None) + async def handle_donation_amount_input(self, update, context): + """Обработка ввода суммы доната""" + try: + amount = float(update.message.text.strip()) + if amount <= 0: + raise ValueError() + + user = update.effective_user + + # Сохраняем донат в базу данных + if db.add_donation(user.id, amount): + points = int(amount // 100) + await update.message.reply_text( + DONATION_MESSAGES['donate_success'].format( + amount=amount, + currency='RUB', + points=points + ), + parse_mode='HTML' + ) + + # Проверяем достижения + current_year = datetime.now().year + total_yearly = db.get_total_donations(user.id, current_year) + achievements = db.check_and_unlock_achievements(user.id, donations=total_yearly) + + # Сообщаем о новых достижениях + for achievement in achievements: + if achievement in ACHIEVEMENT_MESSAGES: + await update.effective_chat.send_message( + ACHIEVEMENT_MESSAGES[achievement].format(name=user.first_name), + parse_mode='HTML' + ) + else: + await update.message.reply_text(DONATION_MESSAGES['donate_error']) + + except ValueError: + await update.message.reply_text("❌ Неверная сумма! Введите положительное число.") + + context.user_data.pop('waiting_for_donation_amount', None) + async def handle_new_chat_members(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработка новых участников чата""" - for member in update.chat_member.new_chat_members: - db.add_user(member.id, member.username, member.first_name, member.last_name) + chat_id = update.effective_chat.id - welcome_text = USER_MESSAGES['welcome_new_user'].format(name=member.first_name) + # Удаляем предыдущее приветственное сообщение, если оно есть + await self.delete_welcome_message(chat_id) - await update.effective_chat.send_message(welcome_text) + # Создаем новое приветственное сообщение с правилами и кнопками донатов + welcome_text = self.get_welcome_message_with_rules() + + # Создаем клавиатуру с кнопками донатов + donation_keyboard = [ + [InlineKeyboardButton("💰 100 ₽", callback_data='donate_100')], + [InlineKeyboardButton("💰 500 ₽", callback_data='donate_500')], + [InlineKeyboardButton("💰 1000 ₽", callback_data='donate_1000')], + [InlineKeyboardButton("🎯 Помощь", callback_data='cmd_help')] + ] + reply_markup = InlineKeyboardMarkup(donation_keyboard) + + try: + message = await update.effective_chat.send_message(welcome_text, parse_mode='HTML', reply_markup=reply_markup) + # Сохраняем информацию о сообщении для автодаления + await self.set_welcome_message(chat_id, message.message_id) + + # Обрабатываем каждого нового участника + for member in update.chat_member.new_chat_members: + db.add_user(member.id, member.username, member.first_name, member.last_name) + + # Начисление очков за вступление в чат + db.update_score(member.id, SCORE_VALUES['join_chat']) + + # Обновление репутации за вступление в чат + db.update_reputation(member.id, SCORE_VALUES['reputation_per_message']) + + except Exception as e: + print(f"Ошибка при отправке приветственного сообщения: {e}") + + def get_welcome_message_with_rules(self): + """Генерация приветственного сообщения с правилами группы""" + welcome_text = """🎉 Добро пожаловать в нашу группу! + +👋 Приветствуем всех новых участников! + +📋 Правила нашей группы: +• Соблюдайте уважительное общение +• Не используйте нецензурную лексику +• Спам и реклама запрещены +• Уважайте мнение других участников +• Используйте /help для получения справки о командах бота + +🎮 В группе работает система рейтингов и игр! +🏆 Используйте /rank для просмотра вашего рейтинга + +💰 Поддержите проект: +Если вам нравится бот, вы можете поддержать его развитие!""" + return welcome_text async def handle_inline_query(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработка инлайновых запросов""" @@ -1357,6 +1578,9 @@ ID: {user_info['ID']} [InlineKeyboardButton("Крестики-нолики", callback_data='game_tic_tac_toe')], [InlineKeyboardButton("Викторина", callback_data='game_quiz')], [InlineKeyboardButton("Морской бой", callback_data='game_battleship')], + [InlineKeyboardButton("2048", callback_data='game_2048')], + [InlineKeyboardButton("Тетрис", callback_data='game_tetris')], + [InlineKeyboardButton("Змейка", callback_data='game_snake')], [InlineKeyboardButton("⬅️ Назад", callback_data='cmd_start')] ] reply_markup = InlineKeyboardMarkup(keyboard) @@ -1430,6 +1654,608 @@ ID: {user_info['ID']} await query.edit_message_text("❌ Планирование поста отменено.") + # ===== НОВЫЕ ИГРЫ ===== + + async def start_2048_game(self, query, context): + """Запуск игры 2048""" + # Инициализация поля 4x4 + board = [[0 for _ in range(4)] for _ in range(4)] + + # Добавляем два начальных числа + self.add_random_tile(board) + self.add_random_tile(board) + + context.user_data['2048_board'] = board + context.user_data['2048_score'] = 0 + + keyboard = self.create_2048_keyboard(board, 0) + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text(GAME_MESSAGES['2048_start'], reply_markup=reply_markup, parse_mode='HTML') + + async def start_tetris_game(self, query, context): + """Запуск игры Тетрис""" + # Инициализация поля 10x20 (ширина x высота) + board = [[0 for _ in range(10)] for _ in range(20)] + current_piece = self.get_random_tetris_piece() + context.user_data['tetris_board'] = board + context.user_data['tetris_current_piece'] = current_piece + context.user_data['tetris_score'] = 0 + context.user_data['tetris_lines'] = 0 + + keyboard = self.create_tetris_keyboard(board, current_piece, 0, 0) + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text(GAME_MESSAGES['tetris_start'], reply_markup=reply_markup, parse_mode='HTML') + + async def start_snake_game(self, query, context): + """Запуск игры Змейка""" + # Инициализация поля 10x10 + board = [[0 for _ in range(10)] for _ in range(10)] + snake = [(5, 5)] # Начальная позиция змейки + food = self.place_food(board, snake) + direction = 'right' + + context.user_data['snake_board'] = board + context.user_data['snake_body'] = snake + context.user_data['snake_food'] = food + context.user_data['snake_direction'] = direction + context.user_data['snake_score'] = 0 + + keyboard = self.create_snake_keyboard(board, snake, food, 0) + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text(GAME_MESSAGES['snake_start'], reply_markup=reply_markup, parse_mode='HTML') + + async def handle_2048_move(self, query, context): + """Обработка ходов в игре 2048""" + move = query.data.split('_')[1] + + board = context.user_data.get('2048_board') + score = context.user_data.get('2048_score', 0) + + if not board: + await query.answer("Игра не найдена") + return + + moved, new_score = self.move_2048(board, move) + score += new_score + + if moved: + self.add_random_tile(board) + context.user_data['2048_score'] = score + + # Проверяем победу + if self.check_2048_win(board): + db.update_score(query.from_user.id, 20) + keyboard = self.create_2048_keyboard(board, score, game_over=True) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(GAME_MESSAGES['2048_win'], reply_markup=reply_markup) + return + + # Проверяем окончание игры + if not self.can_move_2048(board): + keyboard = self.create_2048_keyboard(board, score, game_over=True) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(GAME_MESSAGES['2048_game_over'], reply_markup=reply_markup) + return + + keyboard = self.create_2048_keyboard(board, score) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(GAME_MESSAGES['2048_start'], reply_markup=reply_markup, parse_mode='HTML') + else: + await query.answer("Невозможный ход!") + + async def handle_tetris_move(self, query, context): + """Обработка ходов в игре Тетрис""" + move = query.data.split('_')[1] + + board = context.user_data.get('tetris_board') + piece = context.user_data.get('tetris_current_piece') + score = context.user_data.get('tetris_score', 0) + lines = context.user_data.get('tetris_lines', 0) + + if not board or not piece: + await query.answer("Игра не найдена") + return + + # Обработка движений + if move == 'left': + piece['x'] -= 1 + if not self.is_valid_position(board, piece): + piece['x'] += 1 + elif move == 'right': + piece['x'] += 1 + if not self.is_valid_position(board, piece): + piece['x'] -= 1 + elif move == 'down': + piece['y'] += 1 + if not self.is_valid_position(board, piece): + piece['y'] -= 1 + # Фиксируем фигуру и создаем новую + self.place_piece(board, piece) + lines_cleared = self.clear_lines(board) + score += lines_cleared * 10 + lines += lines_cleared + + new_piece = self.get_random_tetris_piece() + if not self.is_valid_position(board, new_piece): + # Игра окончена + keyboard = self.create_tetris_keyboard(board, piece, score, lines, game_over=True) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(GAME_MESSAGES['tetris_game_over'].format(score=score), reply_markup=reply_markup) + return + piece = new_piece + elif move == 'rotate': + old_shape = piece['shape'][:] + piece['shape'] = list(zip(*piece['shape'][::-1])) + if not self.is_valid_position(board, piece): + piece['shape'] = old_shape + + context.user_data['tetris_current_piece'] = piece + context.user_data['tetris_score'] = score + context.user_data['tetris_lines'] = lines + + keyboard = self.create_tetris_keyboard(board, piece, score, lines) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(GAME_MESSAGES['tetris_start'], reply_markup=reply_markup, parse_mode='HTML') + + async def handle_snake_move(self, query, context): + """Обработка движений в игре Змейка""" + new_direction = query.data.split('_')[1] + + board = context.user_data.get('snake_board') + snake = context.user_data.get('snake_body') + food = context.user_data.get('snake_food') + direction = context.user_data.get('snake_direction', 'right') + score = context.user_data.get('snake_score', 0) + + if not board or not snake: + await query.answer("Игра не найдена") + return + + # Не позволяем двигаться в обратном направлении + opposites = {'up': 'down', 'down': 'up', 'left': 'right', 'right': 'left'} + if new_direction == opposites.get(direction): + await query.answer("Невозможное направление!") + return + + # Двигаем змейку + head = snake[0] + if new_direction == 'up': + new_head = (head[0] - 1, head[1]) + elif new_direction == 'down': + new_head = (head[0] + 1, head[1]) + elif new_direction == 'left': + new_head = (head[0], head[1] - 1) + elif new_direction == 'right': + new_head = (head[0], head[1] + 1) + + # Проверяем столкновения + if (new_head[0] < 0 or new_head[0] >= 10 or + new_head[1] < 0 or new_head[1] >= 10 or + new_head in snake): + # Игра окончена + db.update_score(query.from_user.id, score) + keyboard = self.create_snake_keyboard(board, snake, food, score, game_over=True) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(GAME_MESSAGES['snake_game_over'].format(length=len(snake), score=score), reply_markup=reply_markup) + return + + snake.insert(0, new_head) + + # Проверяем еду + if new_head == food: + score += 10 + food = self.place_food(board, snake) + else: + snake.pop() # Удаляем хвост + + context.user_data['snake_body'] = snake + context.user_data['snake_food'] = food + context.user_data['snake_direction'] = new_direction + context.user_data['snake_score'] = score + + keyboard = self.create_snake_keyboard(board, snake, food, score) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(GAME_MESSAGES['snake_start'], reply_markup=reply_markup, parse_mode='HTML') + + # ===== ДОНАТЫ И ДОСТИЖЕНИЯ ===== + + async def donate(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда доната""" + keyboard = [ + [InlineKeyboardButton("100 ₽", callback_data='donate_100')], + [InlineKeyboardButton("500 ₽", callback_data='donate_500')], + [InlineKeyboardButton("1000 ₽", callback_data='donate_1000')], + [InlineKeyboardButton("2500 ₽", callback_data='donate_2500')], + [InlineKeyboardButton("5000 ₽", callback_data='donate_5000')], + [InlineKeyboardButton("Другая сумма", callback_data='donate_custom')] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + DONATION_MESSAGES['donate_welcome'], + reply_markup=reply_markup, + parse_mode='HTML' + ) + + async def handle_voice_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка голосовых сообщений""" + user = update.effective_user + voice = update.message.voice + + # Добавление пользователя в базу данных + db.add_user(user.id, user.username, user.first_name, user.last_name) + + # Начисление очков за голосовое сообщение + db.update_score(user.id, SCORE_VALUES['message']) + + duration = voice.duration + + # Попытка транскрибации голосового сообщения + transcription = await self.transcribe_voice_message(voice, update) + + await update.message.reply_text( + f"🎤 Голосовое сообщение от {user.first_name} ({duration} сек)\n\n" + f"🔄 Транскрибация: {transcription}\n\n" + f"💡 +1 очко за активность!", + reply_to_message_id=update.message.message_id + ) + + async def transcribe_voice_message(self, voice, update): + """Транскрибация голосового сообщения с использованием speech recognition""" + if not SPEECH_RECOGNITION_AVAILABLE: + return "[Speech Recognition не установлен. Установите: pip install SpeechRecognition]" + + try: + # Создаем временный файл для загрузки аудио + with tempfile.NamedTemporaryFile(suffix='.ogg', delete=False) as temp_ogg: + temp_ogg_path = temp_ogg.name + + # Скачиваем голосовое сообщение + voice_file = await voice.get_file() + await voice_file.download_to_drive(temp_ogg_path) + + # Конвертируем OGG в WAV для speech_recognition + temp_wav_path = temp_ogg_path.replace('.ogg', '.wav') + + if PYDUB_AVAILABLE: + # Используем pydub для конвертации + audio = AudioSegment.from_ogg(temp_ogg_path) + audio.export(temp_wav_path, format='wav') + else: + # Используем ffmpeg для конвертации (если доступен) + try: + subprocess.run([ + 'ffmpeg', '-i', temp_ogg_path, '-acodec', 'pcm_s16le', + '-ar', '16000', '-ac', '1', temp_wav_path + ], check=True, capture_output=True) + except (subprocess.CalledProcessError, FileNotFoundError): + # Очищаем временные файлы и возвращаем ошибку + os.unlink(temp_ogg_path) + return "[невозможно конвертировать аудио - установите ffmpeg или pydub]\n💡 Для полной работы: pip install SpeechRecognition pydub ffmpeg-python" + + # Распознавание речи + recognizer = sr.Recognizer() + + with sr.AudioFile(temp_wav_path) as source: + audio_data = recognizer.record(source) + + # Пробуем разные API для распознавания + try: + # Сначала пробуем Google Speech Recognition (бесплатно) + text = recognizer.recognize_google(audio_data, language='ru-RU') + return text + except sr.UnknownValueError: + return "[не удалось распознать речь]" + except sr.RequestError: + # Если Google API недоступен, пробуем Sphinx (локальный, но менее точный) + try: + text = recognizer.recognize_sphinx(audio_data, language='ru-RU') + return text + except sr.UnknownValueError: + return "[не удалось распознать речь (локально)]" + except sr.RequestError: + return "[сервисы распознавания речи недоступны]" + + except Exception as e: + print(f"Ошибка при транскрибации: {e}") + return "[ошибка при обработке голосового сообщения]" + + finally: + # Очищаем временные файлы + try: + if 'temp_ogg_path' in locals(): + os.unlink(temp_ogg_path) + if 'temp_wav_path' in locals(): + os.unlink(temp_wav_path) + except: + pass + + # ===== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ДЛЯ ИГР ===== + + def add_random_tile(self, board): + """Добавляет случайную плитку (2 или 4) в пустую клетку""" + empty_cells = [(i, j) for i in range(4) for j in range(4) if board[i][j] == 0] + if empty_cells: + i, j = random.choice(empty_cells) + board[i][j] = 2 if random.random() < 0.9 else 4 + + def move_2048(self, board, direction): + """Двигает плитки в указанном направлении""" + def move_row_left(row): + """Двигает строку влево""" + new_row = [i for i in row if i != 0] + for i in range(len(new_row) - 1): + if new_row[i] == new_row[i + 1]: + new_row[i] *= 2 + new_row[i + 1] = 0 + new_row = [i for i in new_row if i != 0] + return new_row + [0] * (4 - len(new_row)) + + moved = False + score = 0 + + if direction == 'left': + for i in range(4): + old_row = board[i][:] + board[i] = move_row_left(board[i]) + if board[i] != old_row: + moved = True + elif direction == 'right': + for i in range(4): + old_row = board[i][:] + board[i] = move_row_left(board[i][::-1])[::-1] + if board[i] != old_row: + moved = True + elif direction == 'up': + for j in range(4): + col = [board[i][j] for i in range(4)] + old_col = col[:] + new_col = move_row_left(col) + for i in range(4): + board[i][j] = new_col[i] + if new_col != old_col: + moved = True + elif direction == 'down': + for j in range(4): + col = [board[i][j] for i in range(4)] + old_col = col[:] + new_col = move_row_left(col[::-1])[::-1] + for i in range(4): + board[i][j] = new_col[i] + if new_col != old_col: + moved = True + + return moved, score + + def can_move_2048(self, board): + """Проверяет, возможны ли ходы""" + for i in range(4): + for j in range(4): + if board[i][j] == 0: + return True + if i > 0 and board[i][j] == board[i-1][j]: + return True + if j > 0 and board[i][j] == board[i][j-1]: + return True + return False + + def check_2048_win(self, board): + """Проверяет победу (наличие 2048)""" + return any(2048 in row for row in board) + + def create_2048_keyboard(self, board, score, game_over=False): + """Создает клавиатуру для игры 2048""" + keyboard = [] + for i in range(4): + row = [] + for j in range(4): + cell = board[i][j] + text = str(cell) if cell != 0 else ' ' + row.append(InlineKeyboardButton(text, callback_data=f'2048_cell_{i}_{j}')) + keyboard.append(row) + + # Кнопки управления + keyboard.append([ + InlineKeyboardButton("⬅️", callback_data='2048_left'), + InlineKeyboardButton("⬆️", callback_data='2048_up'), + InlineKeyboardButton("⬇️", callback_data='2048_down'), + InlineKeyboardButton("➡️", callback_data='2048_right') + ]) + + keyboard.append([ + InlineKeyboardButton("🔄 Новая игра", callback_data='game_2048'), + InlineKeyboardButton("⬅️ Назад к играм", callback_data='cmd_play_game') + ]) + + return keyboard + + def get_random_tetris_piece(self): + """Возвращает случайную тетрис-фигуру""" + pieces = [ + {'shape': [[1, 1, 1, 1]], 'x': 3, 'y': 0}, # I + {'shape': [[1, 1], [1, 1]], 'x': 4, 'y': 0}, # O + {'shape': [[1, 0, 0], [1, 1, 1]], 'x': 3, 'y': 0}, # J + {'shape': [[0, 0, 1], [1, 1, 1]], 'x': 3, 'y': 0}, # L + {'shape': [[0, 1, 1], [1, 1, 0]], 'x': 3, 'y': 0}, # S + {'shape': [[1, 1, 0], [0, 1, 1]], 'x': 3, 'y': 0}, # Z + {'shape': [[0, 1, 0], [1, 1, 1]], 'x': 3, 'y': 0}, # T + ] + return random.choice(pieces) + + def is_valid_position(self, board, piece): + """Проверяет, можно ли разместить фигуру в данной позиции""" + for i, row in enumerate(piece['shape']): + for j, cell in enumerate(row): + if cell: + x, y = piece['x'] + j, piece['y'] + i + if (x < 0 or x >= 10 or y >= 20 or + (y >= 0 and board[y][x] != 0)): + return False + return True + + def place_piece(self, board, piece): + """Размещает фигуру на поле""" + for i, row in enumerate(piece['shape']): + for j, cell in enumerate(row): + if cell: + x, y = piece['x'] + j, piece['y'] + i + if 0 <= y < 20 and 0 <= x < 10: + board[y][x] = 1 + + def clear_lines(self, board): + """Очищает заполненные линии""" + lines_cleared = 0 + y = 19 + while y >= 0: + if all(board[y]): + del board[y] + board.insert(0, [0] * 10) + lines_cleared += 1 + else: + y -= 1 + return lines_cleared + + def create_tetris_keyboard(self, board, piece, score, lines, game_over=False): + """Создает клавиатуру для игры Тетрис""" + # Упрощенная визуализация - показываем только нижнюю часть поля + display_board = board[-10:] if len(board) > 10 else board + + keyboard = [] + for row in display_board: + display_row = [] + for cell in row: + text = '⬛' if cell else '⬜' + display_row.append(InlineKeyboardButton(text, callback_data='tetris_display')) + keyboard.append(display_row) + + # Кнопки управления + keyboard.append([ + InlineKeyboardButton("⬅️", callback_data='tetris_left'), + InlineKeyboardButton("🔄", callback_data='tetris_rotate'), + InlineKeyboardButton("➡️", callback_data='tetris_right') + ]) + keyboard.append([ + InlineKeyboardButton("⬇️", callback_data='tetris_down'), + InlineKeyboardButton("🔄 Новая игра", callback_data='game_tetris'), + InlineKeyboardButton("⬅️ Назад к играм", callback_data='cmd_play_game') + ]) + + return keyboard + + def place_food(self, board, snake): + """Размещает еду в случайной свободной клетке""" + empty_cells = [(i, j) for i in range(10) for j in range(10) if (i, j) not in snake] + if empty_cells: + return random.choice(empty_cells) + return None + + def create_snake_keyboard(self, board, snake, food, score, game_over=False): + """Создает клавиатуру для игры Змейка""" + keyboard = [] + for i in range(10): + row = [] + for j in range(10): + if (i, j) == food: + text = '🍎' + elif (i, j) in snake: + text = '🟢' if (i, j) == snake[0] else '🟩' + else: + text = '⬜' + row.append(InlineKeyboardButton(text, callback_data=f'snake_cell_{i}_{j}')) + keyboard.append(row) + + # Кнопки управления + keyboard.append([ + InlineKeyboardButton("⬅️", callback_data='snake_left'), + InlineKeyboardButton("⬆️", callback_data='snake_up'), + InlineKeyboardButton("⬇️", callback_data='snake_down'), + InlineKeyboardButton("➡️", callback_data='snake_right') + ]) + + keyboard.append([ + InlineKeyboardButton("🔄 Новая игра", callback_data='game_snake'), + InlineKeyboardButton("⬅️ Назад к играм", callback_data='cmd_play_game') + ]) + + return keyboard + + def check_profanity(self, text): + """Проверка текста на наличие нецензурной лексики""" + text_lower = text.lower() + for word in PROFANITY_WORDS: + if word in text_lower: + return True + return False + + async def handle_profanity_violation(self, update, user): + """Обработка нарушения с нецензурной лексикой""" + # Удаление сообщения с матом + try: + await update.message.delete() + except Exception as e: + print(f"Не удалось удалить сообщение: {e}") + + # Отправка предупреждения в чат + username = user.username if user.username else user.first_name + await update.effective_chat.send_message( + MODERATION_MESSAGES['profanity_detected'].format(username=username), + parse_mode='HTML' + ) + + # Начисление предупреждения пользователю + db.add_warning(user.id, "Нецензурная лексика", 0) # 0 - системное предупреждение + + async def handle_donation_callback(self, query, context): + """Обработка callback'ов донатов""" + await query.answer() + + amount_str = query.data.split('_')[1] + if amount_str == 'custom': + # Запрос ввода суммы + await query.edit_message_text( + "💰 Введите сумму доната в рублях:", + reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("❌ Отмена", callback_data='donate_cancel')]]) + ) + context.user_data['waiting_for_donation_amount'] = True + return + + try: + amount = float(amount_str) + user = query.from_user + + # Сохраняем донат в базу данных + if db.add_donation(user.id, amount): + points = int(amount // 100) + await query.edit_message_text( + DONATION_MESSAGES['donate_success'].format( + amount=amount, + currency='RUB', + points=points + ), + parse_mode='HTML' + ) + + # Проверяем достижения + current_year = datetime.now().year + total_yearly = db.get_total_donations(user.id, current_year) + achievements = db.check_and_unlock_achievements(user.id, donations=total_yearly) + + # Сообщаем о новых достижениях + for achievement in achievements: + if achievement in ACHIEVEMENT_MESSAGES: + await query.message.chat.send_message( + ACHIEVEMENT_MESSAGES[achievement].format(name=user.first_name), + parse_mode='HTML' + ) + else: + await query.edit_message_text(DONATION_MESSAGES['donate_error']) + + except ValueError: + await query.edit_message_text("❌ Неверная сумма!") + def run(self): """Запуск бота""" print("Bot started!") diff --git a/database_sqlite.py b/database_sqlite.py index f8d547d..0d5da63 100644 --- a/database_sqlite.py +++ b/database_sqlite.py @@ -87,6 +87,29 @@ class Database: ) """) + # Таблица достижений + cursor.execute(""" + CREATE TABLE IF NOT EXISTS achievements ( + achievement_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + achievement_type TEXT NOT NULL, + unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (user_id) + ) + """) + + # Таблица донатов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS donations ( + donation_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + amount REAL NOT NULL, + currency TEXT DEFAULT 'RUB', + donated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (user_id) + ) + """) + self.connection.commit() print(TECH_MESSAGES['db_tables_created']) except sqlite3.Error as error: @@ -396,6 +419,133 @@ class Database: print(f"Ошибка при удалении поста: {error}") return False + def unlock_achievement(self, user_id, achievement_type): + """Разблокировать достижение для пользователя""" + try: + cursor = self.connection.cursor() + cursor.execute(""" + INSERT OR IGNORE INTO achievements (user_id, achievement_type) + VALUES (?, ?) + """, (user_id, achievement_type)) + self.connection.commit() + return cursor.rowcount > 0 # True если достижение было разблокировано впервые + except sqlite3.Error as error: + print(f"Ошибка при разблокировке достижения: {error}") + return False + + def get_user_achievements(self, user_id): + """Получить достижения пользователя""" + try: + cursor = self.connection.cursor() + cursor.execute(""" + SELECT achievement_type, unlocked_at + FROM achievements + WHERE user_id = ? + ORDER BY unlocked_at DESC + """, (user_id,)) + return cursor.fetchall() + except sqlite3.Error as error: + print(f"Ошибка при получении достижений: {error}") + return [] + + def add_donation(self, user_id, amount, currency='RUB'): + """Добавить донат пользователя""" + try: + cursor = self.connection.cursor() + cursor.execute(""" + INSERT INTO donations (user_id, amount, currency) + VALUES (?, ?, ?) + """, (user_id, amount, currency)) + + # Начисляем очки за донат (1 очко за каждые 100 рублей) + points = int(amount // 100) + if points > 0: + self.update_score(user_id, points) + + self.connection.commit() + return True + except sqlite3.Error as error: + print(f"Ошибка при добавлении доната: {error}") + return False + + def get_total_donations(self, user_id, year=None): + """Получить общую сумму донатов пользователя""" + try: + cursor = self.connection.cursor() + if year: + cursor.execute(""" + SELECT SUM(amount) FROM donations + WHERE user_id = ? AND strftime('%Y', donated_at) = ? + """, (user_id, str(year))) + else: + cursor.execute(""" + SELECT SUM(amount) FROM donations + WHERE user_id = ? + """, (user_id,)) + result = cursor.fetchone() + return result[0] if result[0] else 0 + except sqlite3.Error as error: + print(f"Ошибка при получении донатов: {error}") + return 0 + + def check_and_unlock_achievements(self, user_id, message_count=None, games_won=None, donations=None): + """Проверяет и разблокирует достижения""" + achievements_unlocked = [] + + # Достижения за сообщения + if message_count is None: + user_info = self.get_user_info(user_id) + if user_info: + message_count = user_info['Количество сообщений'] + + if message_count >= 1: + # Проверяем, было ли уже разблокировано достижение первого сообщения + existing_achievements = self.get_user_achievements(user_id) + has_first_message = any(achievement[0] == 'first_message' for achievement in existing_achievements) + + if not has_first_message: + if self.unlock_achievement(user_id, 'first_message'): + achievements_unlocked.append('first_message') + if message_count >= 100: + existing_achievements = self.get_user_achievements(user_id) + has_100_messages = any(achievement[0] == '100_messages' for achievement in existing_achievements) + if not has_100_messages: + if self.unlock_achievement(user_id, '100_messages'): + achievements_unlocked.append('100_messages') + + if message_count >= 1000: + existing_achievements = self.get_user_achievements(user_id) + has_1000_messages = any(achievement[0] == '1000_messages' for achievement in existing_achievements) + if not has_1000_messages: + if self.unlock_achievement(user_id, '1000_messages'): + achievements_unlocked.append('1000_messages') + + # Достижения за игры + if games_won is None: + # Здесь можно добавить логику подсчета побед в играх + pass + + if games_won and games_won >= 10: + existing_achievements = self.get_user_achievements(user_id) + has_games_master = any(achievement[0] == 'games_master' for achievement in existing_achievements) + if not has_games_master: + if self.unlock_achievement(user_id, 'games_master'): + achievements_unlocked.append('games_master') + + # Достижения за донаты + if donations is None: + current_year = datetime.now().year + donations = self.get_total_donations(user_id, current_year) + + if donations >= 100000: # 100k рублей в год + existing_achievements = self.get_user_achievements(user_id) + has_sponsor_year = any(achievement[0] == 'sponsor_year' for achievement in existing_achievements) + if not has_sponsor_year: + if self.unlock_achievement(user_id, 'sponsor_year'): + achievements_unlocked.append('sponsor_year') + + return achievements_unlocked + def close(self): """Закрытие соединения с базой данных""" if self.connection: diff --git a/messages.py b/messages.py index e5ed8ab..6bd37b0 100644 --- a/messages.py +++ b/messages.py @@ -34,6 +34,9 @@ HELP_TEXT = """ Прочее: Введите "реквизиты" - Получить реквизиты для оплаты +Поддержка проекта: +/donate - Поддержать проект донатом (начисление очков за поддержку) + Игры: /play_game - Запустить мини-игру @@ -158,6 +161,33 @@ MODERATION_MESSAGES = { 'kick_error': '❌ Ошибка при кике пользователя: {error}', 'promote_error': '❌ Ошибка при повышении пользователя: {error}', 'demote_error': '❌ Ошибка при понижении пользователя: {error}', + 'profanity_detected': '🚫 Обнаружена нецензурная лексика!\n\nСообщение пользователя @{username} удалено.\nПричина: использование запрещенных выражений.', +} + +# Сообщения достижений +ACHIEVEMENT_MESSAGES = { + 'first_message': '🎉 Новое достижение!\n\n🏆 Первое сообщение!\nВы отправили первое сообщение в чате!', + '100_messages': '🎊 Новое достижение!\n\n💯 100 сообщений!\nВы отправили 100 сообщений в чате!', + '1000_messages': '🏅 Новое достижение!\n\n🎖 1000 сообщений!\nВы - настоящий участник чата!', + 'games_master': '🎮 Новое достижение!\n\n🏆 Мастер игр!\nВы выиграли 10 игр!', + 'sponsor_year': '💰 Новое достижение!\n\n👑 Спонсор года!\nВы задонатили более 100 000 рублей в этом году!', +} + +# Значки достижений +ACHIEVEMENT_BADGES = { + 'first_message': '💬', + '100_messages': '💯', + '1000_messages': '🎖', + 'games_master': '🎮', + 'sponsor_year': '👑', +} + +# Сообщения донатов +DONATION_MESSAGES = { + 'donate_welcome': '💰 Поддержите проект!\n\nВаша поддержка помогает развивать бота и добавлять новые функции.\n\nВыберите сумму пожертвования:', + 'donate_success': '✅ Спасибо за поддержку!\n\n💰 Сумма: {amount} {currency}\n🎁 Начислено очков: {points}', + 'donate_error': '❌ Ошибка при обработке доната. Попробуйте позже.', + 'donate_cancel': '❌ Донат отменен.', } # Сообщения игр @@ -185,6 +215,17 @@ GAME_MESSAGES = { 'battleship_lose': '😞 Вы проиграли! Все корабли потоплены.', 'battleship_already_shot': 'Вы уже стреляли в эту клетку!', 'battleship_invalid_coord': 'Неверные координаты!', + # Новые игры + '2048_start': '🧩 2048\n\nЦель: соединяйте одинаковые числа, чтобы получить 2048!\n\nИспользуйте стрелки для движения плиток.', + '2048_win': '🎉 Поздравляем! Вы достигли 2048! +20 очков', + '2048_game_over': '😞 Игра окончена! Попробуйте снова.', + '2048_score': 'Текущий счет: {score}', + 'tetris_start': '🧱 Тетрис\n\nИспользуйте стрелки для управления фигурами.\nСобирайте полные линии!', + 'tetris_line_cleared': '✨ Линия очищена! +{points} очков', + 'tetris_game_over': '😞 Игра окончена! Счет: {score}', + 'snake_start': '🐍 Змейка\n\nСобирайте 🍎, чтобы расти!\nИспользуйте стрелки для движения.', + 'snake_game_over': '😞 Игра окончена! Длина: {length}, Счет: {score}', + 'snake_score': '🍎 Съедено: {score}, Длина: {length}', } # Сообщения погоды @@ -365,8 +406,27 @@ SCORE_VALUES = { 'bonus_min': 1, 'bonus_max': 5, 'reputation_per_message': 1, + 'join_chat': 5, + 'reputation_per_join': 2, } +# Список запрещенных слов для автофильтра +PROFANITY_WORDS = [ + # Русская нецензурная лексика (основные вариации) + 'блядь', 'бля', 'блять', 'блядский', 'блядство', 'блядник', + 'хуй', 'хуйня', 'хуйло', 'хуесос', 'хуеглот', 'хуяк', + 'пизда', 'пиздец', 'пиздеть', 'пиздилка', 'пиздобол', + 'ебать', 'ебаный', 'ебанат', 'еблан', 'ебло', 'ебануться', + 'сука', 'сукин', 'сучий', 'сучка', 'суко', + 'мудак', 'мудацкий', 'мудило', 'мудизм', + 'говно', 'говнос', 'говнюк', 'говнище', + 'дрочить', 'дрочер', 'дрочила', + 'залупа', 'залупный', + 'жопа', 'жопный', 'жопник', + # Английская нецензурная лексика + 'fuck', 'shit', 'damn', 'asshole', 'bastard', 'cunt', 'dick', 'pussy', 'cock', 'bitch' +] + # Константы планировщика SCHEDULER_CONFIG = { 'check_interval': 30, # секунд diff --git a/requirements.txt b/requirements.txt index 5a9cbc7..ef75a0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ python-telegram-bot==20.7 -requests==2.31.0 \ No newline at end of file +requests==2.31.0 +speech_recognition==3.10.0 +pydub==0.25.1 \ No newline at end of file diff --git a/scheduler.py b/scheduler.py index 121001f..195a78e 100644 --- a/scheduler.py +++ b/scheduler.py @@ -12,7 +12,7 @@ import sys from datetime import datetime from telegram import Bot from database_sqlite import Database -from config import BOT_TOKEN +from config_local import BOT_TOKEN from messages import TECH_MESSAGES, SCHEDULER_CONFIG # Настройка логирования