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 6737f1d..88346a6 100644
Binary files a/__pycache__/database_sqlite.cpython-310.pyc and b/__pycache__/database_sqlite.cpython-310.pyc differ
diff --git a/bot.py b/bot.py
index 9349f71..bf091ce 100644
--- a/bot.py
+++ b/bot.py
@@ -2,13 +2,35 @@ import logging
import random
import requests
import os
-from datetime import datetime
+import asyncio
+import tempfile
+import subprocess
+from datetime import datetime, timedelta
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputTextMessageContent
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters, InlineQueryHandler, ChatMemberHandler
from database_sqlite import Database
from config_local import BOT_TOKEN, OPENWEATHER_API_KEY, NEWS_API_KEY, ADMIN_IDS
from messages import *
+try:
+ import speech_recognition as sr
+ SPEECH_RECOGNITION_AVAILABLE = True
+ print("[+] Speech recognition library loaded successfully")
+except ImportError:
+ SPEECH_RECOGNITION_AVAILABLE = False
+ print("[-] Speech recognition library not available")
+ print(" To install: pip install SpeechRecognition")
+ print(" Alternative: pip install speech_recognition")
+
+try:
+ from pydub import AudioSegment
+ PYDUB_AVAILABLE = True
+ print("[+] Pydub library loaded successfully")
+except ImportError:
+ PYDUB_AVAILABLE = False
+ print("[-] Pydub library not available")
+ print(" To install: pip install pydub")
+
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -22,6 +44,10 @@ db = Database()
class TelegramBot:
def __init__(self):
+ # Система отслеживания приветственных сообщений
+ self.welcome_messages = {} # {chat_id: {'message_id': int, 'timestamp': datetime}}
+ self.cleanup_task = None
+
self.application = ApplicationBuilder().token(BOT_TOKEN).build()
self.setup_handlers()
@@ -39,6 +65,7 @@ class TelegramBot:
self.application.add_handler(CommandHandler("news", self.news))
self.application.add_handler(CommandHandler("translate", self.translate))
self.application.add_handler(CommandHandler("play_game", self.play_game))
+ self.application.add_handler(CommandHandler("donate", self.donate))
self.application.add_handler(CommandHandler("ban", self.ban_user))
self.application.add_handler(CommandHandler("mute", self.mute_user))
self.application.add_handler(CommandHandler("warn", self.warn_user))
@@ -55,6 +82,7 @@ class TelegramBot:
# Обработчики сообщений
self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message))
+ self.application.add_handler(MessageHandler(filters.VOICE, self.handle_voice_message))
self.application.add_handler(ChatMemberHandler(self.handle_new_chat_members, ChatMemberHandler.CHAT_MEMBER))
# Инлайновые запросы
@@ -64,8 +92,65 @@ class TelegramBot:
from telegram.ext import CallbackQueryHandler
self.application.add_handler(CallbackQueryHandler(self.handle_callback))
+ def start_cleanup_task(self):
+ """Запуск задачи очистки приветственных сообщений"""
+ if self.cleanup_task is None or self.cleanup_task.done():
+ self.cleanup_task = asyncio.create_task(self.cleanup_welcome_messages())
+
+ async def cleanup_welcome_messages(self):
+ """Периодическая очистка устаревших приветственных сообщений"""
+ while True:
+ try:
+ await asyncio.sleep(30) # Проверяем каждые 30 секунд
+ await self.delete_expired_welcome_messages()
+ except asyncio.CancelledError:
+ break
+ except Exception as e:
+ print(f"Ошибка при очистке приветственных сообщений: {e}")
+
+ async def delete_expired_welcome_messages(self):
+ """Удаление просроченных приветственных сообщений"""
+ current_time = datetime.now()
+ expired_chats = []
+
+ for chat_id, message_info in self.welcome_messages.items():
+ message_time = message_info['timestamp']
+ if current_time - message_time > 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
# Настройка логирования