mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
Version 1.3.0 - Bug fixes and donation system improvements
This commit is contained in:
@@ -2,17 +2,45 @@
|
|||||||
|
|
||||||
Многофункциональный Telegram бот на Python с системой ранжирования пользователей, играми и интеграцией с внешними API.
|
Многофункциональный 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)
|
- 📰 Последние новости (NewsAPI)
|
||||||
- 🌐 Перевод текста (Google Translate) - базовая реализация
|
- 🌐 Перевод текста (Google Translate) - базовая реализация
|
||||||
|
|
||||||
|
### Система приветствий и донатов
|
||||||
|
- 🎉 Автоматическое приветствие новых пользователей с правилами группы
|
||||||
|
- ⏰ Автоматическое удаление приветственных сообщений через 2 минуты
|
||||||
|
- 👥 Обработка нескольких пользователей (удаление старого, создание нового приветствия)
|
||||||
|
- 💰 Интеграция кнопок донатов прямо в приветственные сообщения
|
||||||
|
- 🎯 Быстрый доступ к помощи через приветственные кнопки
|
||||||
|
|
||||||
## Установка и настройка
|
## Установка и настройка
|
||||||
|
|
||||||
### 1. Клонирование репозитория
|
### 1. Клонирование репозитория
|
||||||
@@ -86,6 +121,7 @@ python bot.py
|
|||||||
|
|
||||||
#### Игры
|
#### Игры
|
||||||
- `/play_game` - Запустить мини-игру
|
- `/play_game` - Запустить мини-игру
|
||||||
|
- Доступные игры: Камень-ножницы-бумага, Крестики-нолики, Викторина, Морской бой, 2048, Тетрис, Змейка
|
||||||
|
|
||||||
#### Посты по расписанию (только админы)
|
#### Посты по расписанию (только админы)
|
||||||
- `/schedule_post [время] [текст]` - Запланировать пост
|
- `/schedule_post [время] [текст]` - Запланировать пост
|
||||||
@@ -108,6 +144,8 @@ python bot.py
|
|||||||
- За выигрыш в мини-игре начисляются дополнительные очки
|
- За выигрыш в мини-игре начисляются дополнительные очки
|
||||||
- Военная иерархия званий от Рядового до Маршала
|
- Военная иерархия званий от Рядового до Маршала
|
||||||
- Рейтинг сохраняется в SQLite базе данных
|
- Рейтинг сохраняется в SQLite базе данных
|
||||||
|
- Начисление очков за вступление в группу
|
||||||
|
- Система достижений с разблокировкой значков
|
||||||
|
|
||||||
### Посты по расписанию
|
### Посты по расписанию
|
||||||
- Планирование публикации постов в указанное время
|
- Планирование публикации постов в указанное время
|
||||||
@@ -117,6 +155,12 @@ python bot.py
|
|||||||
- Управление постами только администраторами чата
|
- Управление постами только администраторами чата
|
||||||
- Все посты сохраняются в базе данных с историей публикаций
|
- Все посты сохраняются в базе данных с историей публикаций
|
||||||
|
|
||||||
|
### Автоматическая система приветствий
|
||||||
|
- Приветствие новых пользователей с подробными правилами группы
|
||||||
|
- Автоматическое удаление приветствий через 2 минуты для чистоты чата
|
||||||
|
- Интеллектуальная обработка нескольких пользователей одновременно
|
||||||
|
- Интеграция донатов и быстрого доступа к помощи в приветственных сообщениях
|
||||||
|
|
||||||
#### Примеры использования планировщика:
|
#### Примеры использования планировщика:
|
||||||
```
|
```
|
||||||
/schedule_post +1h Добро пожаловать в наш чат!
|
/schedule_post +1h Добро пожаловать в наш чат!
|
||||||
@@ -130,9 +174,12 @@ python bot.py
|
|||||||
telegram_bot/
|
telegram_bot/
|
||||||
├── bot.py # Основной файл бота
|
├── bot.py # Основной файл бота
|
||||||
├── config.py # Конфигурация (токены, настройки БД)
|
├── config.py # Конфигурация (токены, настройки БД)
|
||||||
|
├── config_local.py # Локальная конфигурация с токенами
|
||||||
├── database_sqlite.py # Работа с SQLite базой данных
|
├── database_sqlite.py # Работа с SQLite базой данных
|
||||||
|
├── database.py # Базовые функции работы с БД
|
||||||
├── scheduler.py # Планировщик постов по расписанию
|
├── scheduler.py # Планировщик постов по расписанию
|
||||||
├── migrate_ranks.py # Миграция системы рангов
|
├── migrate_ranks.py # Миграция системы рангов
|
||||||
|
├── messages.py # Текстовые сообщения и константы бота
|
||||||
├── requirements.txt # Зависимости Python
|
├── requirements.txt # Зависимости Python
|
||||||
└── README.md # Эта документация
|
└── README.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*
|
||||||
Binary file not shown.
@@ -2,13 +2,35 @@ import logging
|
|||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
import os
|
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 import Update, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputTextMessageContent
|
||||||
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters, InlineQueryHandler, ChatMemberHandler
|
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters, InlineQueryHandler, ChatMemberHandler
|
||||||
from database_sqlite import Database
|
from database_sqlite import Database
|
||||||
from config_local import BOT_TOKEN, OPENWEATHER_API_KEY, NEWS_API_KEY, ADMIN_IDS
|
from config_local import BOT_TOKEN, OPENWEATHER_API_KEY, NEWS_API_KEY, ADMIN_IDS
|
||||||
from messages import *
|
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(
|
logging.basicConfig(
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
@@ -22,6 +44,10 @@ db = Database()
|
|||||||
|
|
||||||
class TelegramBot:
|
class TelegramBot:
|
||||||
def __init__(self):
|
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.application = ApplicationBuilder().token(BOT_TOKEN).build()
|
||||||
self.setup_handlers()
|
self.setup_handlers()
|
||||||
|
|
||||||
@@ -39,6 +65,7 @@ class TelegramBot:
|
|||||||
self.application.add_handler(CommandHandler("news", self.news))
|
self.application.add_handler(CommandHandler("news", self.news))
|
||||||
self.application.add_handler(CommandHandler("translate", self.translate))
|
self.application.add_handler(CommandHandler("translate", self.translate))
|
||||||
self.application.add_handler(CommandHandler("play_game", self.play_game))
|
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("ban", self.ban_user))
|
||||||
self.application.add_handler(CommandHandler("mute", self.mute_user))
|
self.application.add_handler(CommandHandler("mute", self.mute_user))
|
||||||
self.application.add_handler(CommandHandler("warn", self.warn_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.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))
|
self.application.add_handler(ChatMemberHandler(self.handle_new_chat_members, ChatMemberHandler.CHAT_MEMBER))
|
||||||
|
|
||||||
# Инлайновые запросы
|
# Инлайновые запросы
|
||||||
@@ -64,8 +92,65 @@ class TelegramBot:
|
|||||||
from telegram.ext import CallbackQueryHandler
|
from telegram.ext import CallbackQueryHandler
|
||||||
self.application.add_handler(CallbackQueryHandler(self.handle_callback))
|
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):
|
async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Обработчик команды /start"""
|
"""Обработчик команды /start"""
|
||||||
|
# Запускаем задачу очистки приветственных сообщений при первом использовании
|
||||||
|
if self.cleanup_task is None or self.cleanup_task.done():
|
||||||
|
self.start_cleanup_task()
|
||||||
|
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
db.add_user(user.id, user.username, user.first_name, user.last_name)
|
db.add_user(user.id, user.username, user.first_name, user.last_name)
|
||||||
|
|
||||||
@@ -135,7 +220,15 @@ class TelegramBot:
|
|||||||
try:
|
try:
|
||||||
target_user_id = int(target_user)
|
target_user_id = int(target_user)
|
||||||
user_info = db.get_user_info(target_user_id)
|
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:
|
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"""
|
info_text = f"""
|
||||||
👤 <b>Информация о пользователе:</b>
|
👤 <b>Информация о пользователе:</b>
|
||||||
|
|
||||||
@@ -155,6 +248,8 @@ ID: {user_info['ID']}
|
|||||||
Очки: {user_info['Очки']}
|
Очки: {user_info['Очки']}
|
||||||
Предупреждений: {user_info['Предупреждений']}
|
Предупреждений: {user_info['Предупреждений']}
|
||||||
Роль: {user_info['Роль']}
|
Роль: {user_info['Роль']}
|
||||||
|
{f"🏆 Достижения: {badge_str.strip()}" if badge_str else ""}
|
||||||
|
{f"💰 Всего донатов: {total_donations} RUB" if total_donations > 0 else ""}
|
||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
info_text = f"Пользователь с ID {target_user_id} не найден."
|
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)
|
db.add_user(user.id, user.username, user.first_name, user.last_name)
|
||||||
user_info = db.get_user_info(user.id)
|
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:
|
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"""
|
info_text = f"""
|
||||||
👤 <b>Информация о вас:</b>
|
👤 <b>Информация о вас:</b>
|
||||||
|
|
||||||
@@ -185,6 +295,9 @@ ID: {user_info['ID']}
|
|||||||
Очки: {user_info['Очки']}
|
Очки: {user_info['Очки']}
|
||||||
Предупреждений: {user_info['Предупреждений']}
|
Предупреждений: {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:
|
else:
|
||||||
info_text = USER_MESSAGES['info_not_found']
|
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):
|
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 = [
|
keyboard = [
|
||||||
[InlineKeyboardButton("Камень-ножницы-бумага", callback_data='game_rps')],
|
[InlineKeyboardButton("Камень-ножницы-бумага", callback_data='game_rps')],
|
||||||
[InlineKeyboardButton("Крестики-нолики", callback_data='game_tic_tac_toe')],
|
[InlineKeyboardButton("Крестики-нолики", callback_data='game_tic_tac_toe')],
|
||||||
[InlineKeyboardButton("Викторина", callback_data='game_quiz')],
|
[InlineKeyboardButton("Викторина", callback_data='game_quiz')],
|
||||||
[InlineKeyboardButton("Морской бой", callback_data='game_battleship')],
|
[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')]
|
[InlineKeyboardButton("⬅️ Назад", callback_data='cmd_start')]
|
||||||
]
|
]
|
||||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||||
@@ -297,8 +405,20 @@ ID: {user_info['ID']}
|
|||||||
await self.start_quiz_game(query, context)
|
await self.start_quiz_game(query, context)
|
||||||
elif query.data == 'game_battleship':
|
elif query.data == 'game_battleship':
|
||||||
await self.start_battleship_game(query, context)
|
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_'):
|
elif query.data.startswith('bs_'):
|
||||||
await self.handle_battleship_shot(query, context)
|
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_'):
|
elif query.data.startswith('rps_'):
|
||||||
await self.handle_rps(query, context)
|
await self.handle_rps(query, context)
|
||||||
elif query.data.startswith('tictactoe_'):
|
elif query.data.startswith('tictactoe_'):
|
||||||
@@ -313,6 +433,8 @@ ID: {user_info['ID']}
|
|||||||
await self.add_schedule_image(query, context)
|
await self.add_schedule_image(query, context)
|
||||||
elif query.data == 'cancel_schedule':
|
elif query.data == 'cancel_schedule':
|
||||||
await self.cancel_schedule_post(query, context)
|
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):
|
async def start_rps_game(self, query, context):
|
||||||
@@ -1176,8 +1298,18 @@ ID: {user_info['ID']}
|
|||||||
await self.handle_text_edit(update, context)
|
await self.handle_text_edit(update, context)
|
||||||
return
|
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()
|
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)
|
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)
|
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):
|
async def handle_new_chat_members(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Обработка новых участников чата"""
|
"""Обработка новых участников чата"""
|
||||||
for member in update.chat_member.new_chat_members:
|
chat_id = update.effective_chat.id
|
||||||
db.add_user(member.id, member.username, member.first_name, member.last_name)
|
|
||||||
|
|
||||||
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 = """🎉 <b>Добро пожаловать в нашу группу!</b>
|
||||||
|
|
||||||
|
👋 Приветствуем всех новых участников!
|
||||||
|
|
||||||
|
📋 <b>Правила нашей группы:</b>
|
||||||
|
• Соблюдайте уважительное общение
|
||||||
|
• Не используйте нецензурную лексику
|
||||||
|
• Спам и реклама запрещены
|
||||||
|
• Уважайте мнение других участников
|
||||||
|
• Используйте /help для получения справки о командах бота
|
||||||
|
|
||||||
|
🎮 В группе работает система рейтингов и игр!
|
||||||
|
🏆 Используйте /rank для просмотра вашего рейтинга
|
||||||
|
|
||||||
|
💰 <b>Поддержите проект:</b>
|
||||||
|
Если вам нравится бот, вы можете поддержать его развитие!"""
|
||||||
|
return welcome_text
|
||||||
|
|
||||||
async def handle_inline_query(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
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_tic_tac_toe')],
|
||||||
[InlineKeyboardButton("Викторина", callback_data='game_quiz')],
|
[InlineKeyboardButton("Викторина", callback_data='game_quiz')],
|
||||||
[InlineKeyboardButton("Морской бой", callback_data='game_battleship')],
|
[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')]
|
[InlineKeyboardButton("⬅️ Назад", callback_data='cmd_start')]
|
||||||
]
|
]
|
||||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||||
@@ -1430,6 +1654,608 @@ ID: {user_info['ID']}
|
|||||||
|
|
||||||
await query.edit_message_text("❌ Планирование поста отменено.")
|
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):
|
def run(self):
|
||||||
"""Запуск бота"""
|
"""Запуск бота"""
|
||||||
print("Bot started!")
|
print("Bot started!")
|
||||||
|
|||||||
@@ -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()
|
self.connection.commit()
|
||||||
print(TECH_MESSAGES['db_tables_created'])
|
print(TECH_MESSAGES['db_tables_created'])
|
||||||
except sqlite3.Error as error:
|
except sqlite3.Error as error:
|
||||||
@@ -396,6 +419,133 @@ class Database:
|
|||||||
print(f"Ошибка при удалении поста: {error}")
|
print(f"Ошибка при удалении поста: {error}")
|
||||||
return False
|
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):
|
def close(self):
|
||||||
"""Закрытие соединения с базой данных"""
|
"""Закрытие соединения с базой данных"""
|
||||||
if self.connection:
|
if self.connection:
|
||||||
|
|||||||
+60
@@ -34,6 +34,9 @@ HELP_TEXT = """
|
|||||||
<b>Прочее:</b>
|
<b>Прочее:</b>
|
||||||
Введите "реквизиты" - Получить реквизиты для оплаты
|
Введите "реквизиты" - Получить реквизиты для оплаты
|
||||||
|
|
||||||
|
<b>Поддержка проекта:</b>
|
||||||
|
/donate - Поддержать проект донатом (начисление очков за поддержку)
|
||||||
|
|
||||||
<b>Игры:</b>
|
<b>Игры:</b>
|
||||||
/play_game - Запустить мини-игру
|
/play_game - Запустить мини-игру
|
||||||
|
|
||||||
@@ -158,6 +161,33 @@ MODERATION_MESSAGES = {
|
|||||||
'kick_error': '❌ Ошибка при кике пользователя: {error}',
|
'kick_error': '❌ Ошибка при кике пользователя: {error}',
|
||||||
'promote_error': '❌ Ошибка при повышении пользователя: {error}',
|
'promote_error': '❌ Ошибка при повышении пользователя: {error}',
|
||||||
'demote_error': '❌ Ошибка при понижении пользователя: {error}',
|
'demote_error': '❌ Ошибка при понижении пользователя: {error}',
|
||||||
|
'profanity_detected': '🚫 <b>Обнаружена нецензурная лексика!</b>\n\nСообщение пользователя @{username} удалено.\nПричина: использование запрещенных выражений.',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сообщения достижений
|
||||||
|
ACHIEVEMENT_MESSAGES = {
|
||||||
|
'first_message': '🎉 <b>Новое достижение!</b>\n\n🏆 <b>Первое сообщение!</b>\nВы отправили первое сообщение в чате!',
|
||||||
|
'100_messages': '🎊 <b>Новое достижение!</b>\n\n💯 <b>100 сообщений!</b>\nВы отправили 100 сообщений в чате!',
|
||||||
|
'1000_messages': '🏅 <b>Новое достижение!</b>\n\n🎖 <b>1000 сообщений!</b>\nВы - настоящий участник чата!',
|
||||||
|
'games_master': '🎮 <b>Новое достижение!</b>\n\n🏆 <b>Мастер игр!</b>\nВы выиграли 10 игр!',
|
||||||
|
'sponsor_year': '💰 <b>Новое достижение!</b>\n\n👑 <b>Спонсор года!</b>\nВы задонатили более 100 000 рублей в этом году!',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Значки достижений
|
||||||
|
ACHIEVEMENT_BADGES = {
|
||||||
|
'first_message': '💬',
|
||||||
|
'100_messages': '💯',
|
||||||
|
'1000_messages': '🎖',
|
||||||
|
'games_master': '🎮',
|
||||||
|
'sponsor_year': '👑',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сообщения донатов
|
||||||
|
DONATION_MESSAGES = {
|
||||||
|
'donate_welcome': '💰 <b>Поддержите проект!</b>\n\nВаша поддержка помогает развивать бота и добавлять новые функции.\n\nВыберите сумму пожертвования:',
|
||||||
|
'donate_success': '✅ <b>Спасибо за поддержку!</b>\n\n💰 Сумма: {amount} {currency}\n🎁 Начислено очков: {points}',
|
||||||
|
'donate_error': '❌ Ошибка при обработке доната. Попробуйте позже.',
|
||||||
|
'donate_cancel': '❌ Донат отменен.',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Сообщения игр
|
# Сообщения игр
|
||||||
@@ -185,6 +215,17 @@ GAME_MESSAGES = {
|
|||||||
'battleship_lose': '😞 Вы проиграли! Все корабли потоплены.',
|
'battleship_lose': '😞 Вы проиграли! Все корабли потоплены.',
|
||||||
'battleship_already_shot': 'Вы уже стреляли в эту клетку!',
|
'battleship_already_shot': 'Вы уже стреляли в эту клетку!',
|
||||||
'battleship_invalid_coord': 'Неверные координаты!',
|
'battleship_invalid_coord': 'Неверные координаты!',
|
||||||
|
# Новые игры
|
||||||
|
'2048_start': '🧩 <b>2048</b>\n\nЦель: соединяйте одинаковые числа, чтобы получить 2048!\n\nИспользуйте стрелки для движения плиток.',
|
||||||
|
'2048_win': '🎉 Поздравляем! Вы достигли 2048! +20 очков',
|
||||||
|
'2048_game_over': '😞 Игра окончена! Попробуйте снова.',
|
||||||
|
'2048_score': 'Текущий счет: {score}',
|
||||||
|
'tetris_start': '🧱 <b>Тетрис</b>\n\nИспользуйте стрелки для управления фигурами.\nСобирайте полные линии!',
|
||||||
|
'tetris_line_cleared': '✨ Линия очищена! +{points} очков',
|
||||||
|
'tetris_game_over': '😞 Игра окончена! Счет: {score}',
|
||||||
|
'snake_start': '🐍 <b>Змейка</b>\n\nСобирайте 🍎, чтобы расти!\nИспользуйте стрелки для движения.',
|
||||||
|
'snake_game_over': '😞 Игра окончена! Длина: {length}, Счет: {score}',
|
||||||
|
'snake_score': '🍎 Съедено: {score}, Длина: {length}',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Сообщения погоды
|
# Сообщения погоды
|
||||||
@@ -365,8 +406,27 @@ SCORE_VALUES = {
|
|||||||
'bonus_min': 1,
|
'bonus_min': 1,
|
||||||
'bonus_max': 5,
|
'bonus_max': 5,
|
||||||
'reputation_per_message': 1,
|
'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 = {
|
SCHEDULER_CONFIG = {
|
||||||
'check_interval': 30, # секунд
|
'check_interval': 30, # секунд
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
python-telegram-bot==20.7
|
python-telegram-bot==20.7
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
|
speech_recognition==3.10.0
|
||||||
|
pydub==0.25.1
|
||||||
+1
-1
@@ -12,7 +12,7 @@ import sys
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from telegram import Bot
|
from telegram import Bot
|
||||||
from database_sqlite import Database
|
from database_sqlite import Database
|
||||||
from config import BOT_TOKEN
|
from config_local import BOT_TOKEN
|
||||||
from messages import TECH_MESSAGES, SCHEDULER_CONFIG
|
from messages import TECH_MESSAGES, SCHEDULER_CONFIG
|
||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
|
|||||||
Reference in New Issue
Block a user