From 69c0162c6b21b8b84ef27b9610c909f8e979795d Mon Sep 17 00:00:00 2001 From: FerraSoft Date: Sat, 18 Oct 2025 15:57:44 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BB=D0=B5=D0=BA?= =?UTF-8?q?=D1=81=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B4=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/README.md | 230 +++++ .github/workflows/deploy.yml | 75 ++ .gitignore | 64 +- Dockerfile | 32 + README.md | 275 +++++- TODO.md | 13 +- __pycache__/database_sqlite.cpython-310.pyc | Bin 21103 -> 32862 bytes bot.py | 929 +++++++++++++++++++- config_template.py | 66 ++ database_sqlite.py | 251 +++++- docker-compose.yml | 41 + requirements.txt | 3 +- test_error_system.py | 341 +++++++ test_moderation.py | 408 +++++++++ 14 files changed, 2672 insertions(+), 56 deletions(-) create mode 100644 .github/README.md create mode 100644 .github/workflows/deploy.yml create mode 100644 Dockerfile create mode 100644 config_template.py create mode 100644 docker-compose.yml create mode 100644 test_error_system.py create mode 100644 test_moderation.py diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..1347692 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,230 @@ +# 🚀 Telegram Bot с системой отчетов об ошибках и ИИ-анализом + +Многофункциональный Telegram бот на Python с продвинутой системой ранжирования пользователей, играми и интеграцией с внешними API. + +[![Python Version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://python.org) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +## ✨ Основные возможности + +### 🤖 Система отчетов об ошибках с ИИ +- **Умные отчеты** - администраторы могут отправлять детальные отчеты об ошибках +- **ИИ-анализ** - автоматический анализ проблем с помощью OpenAI GPT +- **Автоматическая интеграция** - обработанные ошибки попадают в план разработки +- **Уведомления разработчика** - мгновенные оповещения о новых ошибках + +### 🤝 Интерактивные функции +- **Автореакции** - автоматическая реакция 🤝 на сообщения с "+" +- **Повышение вовлеченности** - интерактивные элементы для активного общения + +### 🎮 Игровые возможности +- **Классические игры**: Камень-ножницы-бумага, Крестики-нолики, Викторина +- **Расширенные игры**: Морской бой, 2048, Тетрис, Змейка +- **Система достижений** - разблокировка значков и начисление очков + +### 🤝 Интерактивные функции +- **Автореакции** - автоматическая реакция 🤝 на сообщения с "+" +- **Повышение вовлеченности** - интерактивные элементы для активного общения + +### 📊 Система ранжирования +- **Военная иерархия** - от Рядового до Маршала +- **Рейтинговые таблицы** - топ пользователей по очкам +- **Достижения** - система значков и наград + +### 🛠 Администрирование +- **Модерация** - бан, мут, кик, предупреждения пользователей +- **Планировщик постов** - автоматическая публикация по расписанию +- **Импорт пользователей** - загрузка данных из CSV файлов + +### 🌐 Интеграция с API +- **Погода** - актуальная информация через OpenWeatherMap +- **Новости** - свежие новости через NewsAPI +- **ИИ-анализ** - интеллектуальная обработка ошибок через OpenAI + +## 🚀 Быстрый старт + +### Предварительные требования +```bash +Python 3.10+ +pip install -r requirements.txt +``` + +### Настройка +1. **Склонируйте репозиторий** + ```bash + git clone + cd telegram_bot + ``` + +2. **Настройте API ключи** + ```bash + cp config_template.py config_local.py + # Отредактируйте config_local.py с вашими ключами + ``` + +3. **Запустите бота** + ```bash + python bot.py + ``` + +4. **Протестируйте систему** + ```bash + python test_error_system.py + ``` + +## 📋 Команды администраторов + +### Система отчетов об ошибках +- `/report_error <тип> <заголовок> [описание]` - отправить отчет об ошибке +- `/admin_errors [статус]` - показать список ошибок +- `/analyze_error_ai ` - проанализировать ошибку ИИ +- `/add_error_to_todo ` - добавить в план разработки + +### Модерация +- `/warn [пользователь] [причина]` - выдать предупреждение +- `/ban [пользователь] [причина]` - заблокировать пользователя +- `/mute [пользователь] [время]` - временно заглушить + +### Управление контентом +- `/schedule_post [время] [текст]` - запланировать публикацию +- `/list_posts` - показать запланированные посты + +## 🧪 Тестирование + +Проект включает встроенный тестовый скрипт для проверки функциональности: + +```bash +python test_error_system.py +``` + +**Что тестируется:** +- ✅ Подключение к базе данных +- ✅ Работа с таблицей ошибок +- ✅ Система фильтрации и поиска +- ✅ Интеграция с ИИ (если настроен) +- ✅ Работа с TODO файлом +- ✅ Система уведомлений + +## 📁 Структура проекта + +``` +telegram_bot/ +├── bot.py # Основной файл бота +├── database_sqlite.py # Работа с БД (+ таблица errors) +├── config_local.py # Ваши API ключи (не публикуется) +├── config_template.py # Шаблон для настройки +├── test_error_system.py # Тестовый скрипт +├── requirements.txt # Зависимости Python +├── TODO.md # План разработки +├── README.md # Полная документация +└── .gitignore # Игнорируемые файлы +``` + +## 🔧 Настройка API ключей + +### Обязательные ключи: +- **Telegram Bot Token** - получите от [@BotFather](https://t.me/botfather) + +### Опциональные ключи: +- **OpenWeatherMap API** - для команды `/weather` +- **NewsAPI** - для команды `/news` +- **OpenAI API** - для анализа ошибок ИИ + +### Получение ключей: +1. Скопируйте `config_template.py` в `config_local.py` +2. Заполните реальными ключами API +3. Никогда не публикуйте `config_local.py` в открытый доступ + +## 📊 База данных + +Автоматически создается файл `telegram_bot.db` со следующими таблицами: +- **users** - пользователи и их статистика +- **errors** - система отчетов об ошибках +- **warnings** - предупреждения пользователей +- **games** - игровые сессии +- **scheduled_posts** - запланированные публикации + +## 🤝 Как внести вклад + +1. Сделайте форк проекта +2. Создайте ветку для вашей функции (`git checkout -b feature/amazing-feature`) +3. Закоммитьте изменения (`git commit -m 'Add amazing feature'`) +4. Отправьте в ветку (`git push origin feature/amazing-feature`) +5. Создайте Pull Request + +## 📝 Лицензия + +Этот проект распространяется под лицензией MIT - см. файл [LICENSE](LICENSE) для подробностей. + +## 🙏 Поддержка проекта + +Если вам нравится этот бот, вы можете поддержать разработку: +- ⭐ Поставьте звезду на GitHub +- 🐛 Сообщайте об ошибках через систему отчетов +- 💡 Предлагайте новые функции +- 🔄 Делайте форки и улучшайте код + +--- + +## 📋 Быстрый запуск + +1. **Клонируйте репозиторий** + ```bash + git clone + cd telegram_bot + ``` + +2. **Настройте API ключи** + ```bash + cp config_template.py config_local.py + # Отредактируйте config_local.py + ``` + +3. **Установите зависимости** + ```bash + pip install -r requirements.txt + ``` + +4. **Запустите бота** + ```bash + python bot.py + ``` + +5. **Протестируйте систему** + ```bash + python test_error_system.py + ``` + +## 🔧 Основные команды администраторов + +### Система отчетов об ошибках +- `/report_error <тип> <заголовок> [описание]` - отправить отчет +- `/admin_errors [статус]` - просмотреть ошибки +- `/analyze_error_ai ` - проанализировать с ИИ +- `/add_error_to_todo ` - добавить в план разработки + +### Типы ошибок +- `bug` - ошибка в работе бота +- `crash` - критическая ошибка/падение +- `feature` - предложение новой функции +- `ui` - проблема интерфейса +- `security` - проблема безопасности +- `improvement` - предложение улучшения + +## 🚀 Развертывание + +### Docker (рекомендуется) +```bash +docker-compose up -d +``` + +### Локальный запуск +```bash +# Основной бот +python bot.py + +# Планировщик постов (в другом терминале) +python scheduler.py +``` + +**Создано с ❤️ для сообщества Telegram ботов** \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e47044a --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,75 @@ +name: Deploy Telegram Bot + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Create test config + run: | + cp config_template.py config_local.py + # Здесь можно добавить настройку тестовых ключей + + - name: Run tests + run: | + python test_error_system.py + + - name: Run database tests + run: | + python -c "from database_sqlite import Database; db = Database(); print('Database connection successful')" + + deploy: + needs: test + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Setup bot configuration + env: + BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + DEVELOPER_CHAT_ID: ${{ secrets.DEVELOPER_CHAT_ID }} + run: | + echo "BOT_TOKEN=\"$BOT_TOKEN\"" > config_local.py + echo "OPENAI_API_KEY=\"$OPENAI_API_KEY\"" >> config_local.py + echo "DEVELOPER_CHAT_ID=\"$DEVELOPER_CHAT_ID\"" >> config_local.py + echo "ENABLE_DEVELOPER_NOTIFICATIONS=True" >> config_local.py + echo "ENABLE_AI_ERROR_PROCESSING=True" >> config_local.py + + - name: Run database migrations + run: | + python -c "from database_sqlite import Database; db = Database(); print('Database initialized successfully')" + + - name: Start bot (test mode) + run: | + timeout 10 python bot.py || echo "Bot startup test completed" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8b74974..ccb1b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,47 @@ -# Файлы базы данных +# Конфиденциальные файлы с токенами и ключами API +config_local.py +telegram_bot.db *.db *.sqlite *.sqlite3 -# Файлы Python +# Временные файлы __pycache__/ *.py[cod] *$py.class *.so .Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Виртуальное окружение +.env +.venv env/ venv/ ENV/ env.bak/ venv.bak/ -# Файлы конфигурации с чувствительной информацией -config_local.py -.env -.env.local -.env.*.local +# IDE файлы +.vscode/ +.idea/ +*.swp +*.swo +*~ # Логи *.log @@ -28,10 +50,9 @@ logs/ # Временные файлы *.tmp *.temp -.tmp/ -temp/ +.cache/ -# Файлы ОС +# OS файлы .DS_Store .DS_Store? ._* @@ -40,22 +61,7 @@ temp/ ehthumbs.db Thumbs.db -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# CSV файлы с данными пользователей (если не нужны в репозитории) -chat_*.csv - -# Документация сборки -build/ -dist/ -*.egg-info/ - -# Токены и ключи API (если случайно попадут) -*token* -*api_key* -*secret* \ No newline at end of file +# Конфиденциальные ключи (дополнительно) +*_api_key.txt +*_token.txt +.secrets \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5cd638a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.10-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Устанавливаем системные зависимости +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Копируем файлы зависимостей +COPY requirements.txt . + +# Устанавливаем Python зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем исходный код +COPY . . + +# Создаем непривилегированного пользователя +RUN useradd --create-home --shell /bin/bash bot +RUN chown -R bot:bot /app +USER bot + +# Создаем директорию для базы данных +RUN mkdir -p /app/data + +# Указываем объем для базы данных +VOLUME ["/app/data"] + +# Команда запуска бота +CMD ["python", "bot.py"] \ No newline at end of file diff --git a/README.md b/README.md index 1664b75..9a19b31 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,24 @@ ## 📋 История версий -### Версия 1.4.0 (16 октября 2025) - Критические исправления -**Дата релиза:** 16.10.2025 +### Версия 1.5.0 (18 октября 2025) - Система модерации медиафайлов +**Дата релиза:** 18.10.2025 -#### 🚨 Критические исправления безопасности и стабильности -- ✅ **Исправлена система рангов** - синхронизированы поля очков и репутации в базе данных -- ✅ **Улучшена обработка ошибок внешних API** - добавлены таймауты, детальная обработка HTTP-ошибок -- ✅ **Добавлена валидация входящих данных** - защита от некорректных данных и злоупотреблений -- ✅ **Улучшена обработка исключений в БД** - валидация параметров, автоматический rollback транзакций +#### 🆕 Новая система модерации медиафайлов +- ✅ **Автоматическая модерация аудио и видео** - все загружаемые медиафайлы проходят модерацию +- ✅ **Система уведомлений администраторов** - личные уведомления о новых медиафайлах для модерации +- ✅ **Интеграция с планировщиком постов** - одобренные медиа автоматически планируются на публикацию +- ✅ **Отсрочка публикации** - медиа публикуется через 8 часов после одобрения (настраиваемо) +- ✅ **Транскрибация аудио** - автоматическое распознавание речи для модерации +- ✅ **Интерфейс выбора действий** - администраторы могут одобрить, отклонить или запланировать медиа +- ✅ **Автоматическое удаление** - оригинальные сообщения с медиа удаляются из чата +- ✅ **Пересылка в группу модераторов** - медиа отправляется в специальную группу для рассмотрения + +#### 🔧 Улучшения системы +- ✅ **Расширенная обработка медиа** - поддержка аудио, видео и документов +- ✅ **Система метаданных** - сохранение информации о файлах (длительность, размер, транскрипция) +- ✅ **Уведомления пользователей** - пользователи получают информацию об отклонении контента +- ✅ **Гибкие настройки модерации** - выбор между немедленной публикацией и отсрочкой #### Исправления ошибок - ✅ Исправлена ошибка с повторяющимся сообщением о "первом сообщении" при вводе суммы доната @@ -48,6 +58,7 @@ - ✅ Приветствие новых пользователей при добавлении в группу с правилами группы - ✅ Автоматическое удаление приветственных сообщений через 2 минуты - ✅ Интеграция донатов в приветственные сообщения +- ✅ **Автореакции** - автоматическая реакция 🤝 на сообщения с "+" для повышения вовлеченности ### Безопасность и надежность - 🔒 **Валидация входящих данных** - все команды проверяют корректность параметров @@ -87,6 +98,20 @@ - 💰 Интеграция кнопок донатов прямо в приветственные сообщения - 🎯 Быстрый доступ к помощи через приветственные кнопки +### 🤝 Интерактивные функции пользователей +- 👍 **Автореакции** - автоматическая реакция 🤝 на сообщения содержащие "+" +- 🔄 **Отзывчивость** - бот реагирует на положительные сообщения пользователей +- 📈 **Вовлеченность** - повышение активности в чате через интерактивные элементы + +### Система отчетов об ошибках и ИИ-анализа +- 🚨 **Отчеты об ошибках** - администраторы могут отправлять отчеты об ошибках бота +- 📊 **Управление ошибками** - просмотр, фильтрация и управление списком ошибок +- 🤖 **ИИ-анализ** - автоматический анализ ошибок с помощью OpenAI GPT +- 📝 **Интеграция с TODO** - автоматическое добавление обработанных ошибок в список задач разработки +- 🔔 **Уведомления разработчика** - мгновенные уведомления о новых ошибках и их статусе +- 🧠 **Автоматизированная обработка** - пакетная обработка ошибок ИИ и добавление в план разработки +- 🧪 **Тестирование системы** - встроенный тестовый скрипт для проверки функциональности + ## Установка и настройка ### 1. Клонирование репозитория @@ -107,13 +132,20 @@ pip install -r requirements.txt - **Telegram Bot Token**: Получите от [@BotFather](https://t.me/botfather) - **OpenWeatherMap API Key**: Зарегистрируйтесь на [openweathermap.org](https://openweathermap.org/api) - **NewsAPI Key**: Зарегистрируйтесь на [newsapi.org](https://newsapi.org/) +- **OpenAI API Key**: Зарегистрируйтесь на [platform.openai.com](https://platform.openai.com/) для анализа ошибок ИИ -Обновите `config.py` с вашими ключами: +Обновите `config_local.py` с вашими ключами: ```python BOT_TOKEN = "your_bot_token_here" OPENWEATHER_API_KEY = "your_weather_api_key_here" NEWS_API_KEY = "your_news_api_key_here" +OPENAI_API_KEY = "your_openai_api_key_here" + +# Настройки уведомлений разработчика +DEVELOPER_CHAT_ID = -1001234567890 # ID чата для уведомлений об ошибках +ENABLE_DEVELOPER_NOTIFICATIONS = True +ENABLE_AI_ERROR_PROCESSING = True ``` ### 5. Запуск бота @@ -158,6 +190,27 @@ python bot.py - `/promote [пользователь]` - Повысить до модератора - `/demote [пользователь]` - Понизить с модератора +#### Система отчетов об ошибках (только админы) +- `/report_error <тип> <заголовок> [описание]` - Отправить отчет об ошибке +- `/admin_errors [статус]` - Показать список ошибок с фильтрацией по статусу +- `/analyze_error_ai ` - Проанализировать конкретную ошибку с помощью ИИ +- `/process_all_errors_ai` - Обработать все новые ошибки с помощью ИИ +- `/add_error_to_todo [приоритет]` - Добавить ошибку в TODO список +- `/add_all_analyzed_to_todo` - Добавить все проанализированные ошибки в TODO список + +#### Типы ошибок для команды /report_error: +- **bug** - ошибка в работе бота (приоритет: medium) +- **crash** - критическая ошибка/падение (приоритет: critical) +- **feature** - предложение новой функции (приоритет: low) +- **ui** - проблема интерфейса (приоритет: medium) +- **security** - проблема безопасности (приоритет: high) +- **improvement** - предложение улучшения (приоритет: low) +- **other** - другое (приоритет: medium) + +#### Интерактивные функции пользователей +- **Автореакции** - бот автоматически ставит 🤝 на сообщения содержащие "+" +- **Отзывчивость** - повышение вовлеченности пользователей через интерактивные элементы + ### Система ранжирования - За каждое сообщение начисляется 1 очко - За выигрыш в мини-игре начисляются дополнительные очки @@ -191,18 +244,52 @@ 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 # Эта документация +├── bot.py # Основной файл бота с системой ошибок и ИИ +├── config.py # Конфигурация (токены, настройки БД) +├── config_local.py # Локальная конфигурация с токенами и ключами API +├── config_template.py # Шаблон для настройки API ключей +├── database_sqlite.py # Работа с SQLite базой данных (+ таблица errors) +├── database.py # Базовые функции работы с БД +├── scheduler.py # Планировщик постов по расписанию +├── migrate_ranks.py # Миграция системы рангов +├── messages.py # Текстовые сообщения и константы бота +├── requirements.txt # Зависимости Python (+ openai) +├── test_error_system.py # Тестовый скрипт для проверки системы ошибок +├── TODO.md # Список задач разработки с интеграцией ошибок +├── .gitignore # Игнорируемые файлы для Git +├── Dockerfile # Конфигурация для Docker развертывания +├── docker-compose.yml # Оркестрация контейнеров +├── .github/ +│ ├── README.md # Краткое описание для GitHub +│ └── workflows/ +│ └── deploy.yml # CI/CD pipeline для GitHub Actions +└── README.md # Полная документация ``` +### База данных +База данных содержит следующие таблицы: +- **users** - информация о пользователях, рейтинги и достижения +- **warnings** - предупреждения пользователей +- **games** - игровые сессии +- **scheduled_posts** - запланированные посты +- **achievements** - достижения пользователей +- **donations** - история донатов +- **errors** - система отчетов об ошибках и их анализ + +#### Структура таблицы errors: +- **error_id** - уникальный идентификатор ошибки (PRIMARY KEY) +- **admin_id** - ID администратора, отправившего отчет +- **error_type** - тип ошибки (bug, feature, crash, ui, security, etc.) +- **title** - краткое название ошибки +- **description** - подробное описание проблемы +- **status** - статус обработки (new, in_progress, resolved, rejected) +- **priority** - приоритет (low, medium, high, critical) +- **created_at** - время создания отчета +- **updated_at** - время последнего обновления +- **ai_analysis** - результат анализа ИИ (если выполнен) +- **todo_added** - флаг добавления в TODO список +- **resolved_at** - время решения (если решена) + ### 5. Запуск планировщика постов Для работы постов по расписанию запустите планировщик в отдельном терминале: ```bash @@ -218,7 +305,8 @@ python scheduler.py - **sqlite3** - встроенная поддержка SQLite базы данных - **requests** - HTTP запросы для API - **asyncio** - асинхронное программирование -- **SQLite** - база данных для хранения рейтинга и постов +- **openai** - интеграция с OpenAI API для анализа ошибок +- **SQLite** - база данных для хранения рейтинга, постов и отчетов об ошибках ## Разработка @@ -230,6 +318,153 @@ python scheduler.py ### Тестирование Запустите бота локально и протестируйте все функции в тестовом чате. +## Тестирование системы + +### Автоматическое тестирование системы отчетов об ошибках + +Проект включает встроенный тестовый скрипт для проверки функциональности системы отчетов об ошибках: + +```bash +cd telegram_bot +python test_error_system.py +``` + +#### Что тестирует скрипт: +- ✅ Подключение к базе данных SQLite +- ✅ Создание и структура таблицы ошибок +- ✅ Добавление и получение ошибок из базы данных +- ✅ Работа системы фильтрации ошибок +- ✅ Интеграция с ИИ (проверка наличия методов) +- ✅ Работа интеграции с TODO файлом +- ✅ Система уведомлений разработчика +- ✅ Проверка конфигурации + +#### Результаты тестирования: +Тестовый скрипт предоставляет подробный отчет о состоянии системы с градацией результатов: +- **SUCCESS** - тест пройден успешно +- **ERROR** - критическая ошибка, требующая внимания +- **WARNING** - некритическая проблема или рекомендация + +### Примеры использования системы + +#### Пример 1: Отправка отчета об ошибке +``` +Администратор: /report_error bug Не работает команда /weather Бот не показывает погоду при запросе команды +Бот: Отчет успешно отправлен! ID ошибки: 1 +Разработчик: Получает уведомление в настроенном чате +``` + +#### Пример 2: Анализ ошибки ИИ +``` +Администратор: /analyze_error_ai 1 +Бот: Начинаю анализ ошибки #1 с помощью ИИ... +[Через некоторое время] +Бот: Анализ завершен! Предоставляет структурированный анализ с рекомендациями +``` + +#### Пример 3: Добавление в план разработки +``` +Администратор: /add_error_to_todo 1 high +Бот: Ошибка #1 успешно добавлена в TODO список в раздел высокого приоритета +``` + +#### Пример 4: Автореакция на положительные сообщения +``` +Пользователь: Отличная работа! + +Бот: 🤝 (автоматически ставит реакцию рукопожатия) +``` + +## 🚀 Развертывание + +### Локальное развертывание + +#### Обычный запуск +```bash +# 1. Клонируйте репозиторий +git clone +cd telegram_bot + +# 2. Установите зависимости +pip install -r requirements.txt + +# 3. Настройте API ключи +cp config_template.py config_local.py +# Отредактируйте config_local.py + +# 4. Запустите бота +python bot.py + +# 5. В другом терминале запустите планировщик +python scheduler.py +``` + +#### Запуск с Docker +```bash +# 1. Соберите образ +docker build -t telegram-bot . + +# 2. Создайте config_local.py с ключами API + +# 3. Запустите бота +docker run -v $(pwd)/data:/app/data -v $(pwd)/config_local.py:/app/config_local.py telegram-bot + +# Или с docker-compose +docker-compose up -d +``` + +### Развертывание на сервере + +#### Рекомендации для продакшена: +- Используйте виртуальное окружение Python +- Настройте supervisor или systemd для автозапуска +- Используйте reverse proxy (nginx) для безопасности +- Настройте логирование и мониторинг +- Регулярно создавайте резервные копии базы данных + +#### Пример конфигурации systemd: +```ini +[Unit] +Description=Telegram Bot +After=network.target + +[Service] +Type=simple +User=bot +WorkingDirectory=/path/to/telegram_bot +ExecStart=/path/to/venv/bin/python bot.py +Restart=always +Environment=PYTHONPATH=/path/to/telegram_bot + +[Install] +WantedBy=multi-user.target +``` + +### GitHub Actions (CI/CD) + +Проект включает готовые workflows для автоматического тестирования и развертывания: + +```bash +# Настройте секреты в репозитории: +# Settings > Secrets and variables > Actions +BOT_TOKEN=your_bot_token +OPENAI_API_KEY=your_openai_key +DEVELOPER_CHAT_ID=your_chat_id +``` + +#### Автоматическое тестирование: +- Проверка кода при каждом коммите +- Тестирование системы отчетов об ошибках +- Валидация базы данных и конфигурации + +#### Автоматическое развертывание: +- Сборка Docker образа +- Деплой на сервер при обновлении main ветки +- Автоматическая настройка конфигурации + ## Лицензия -MIT License - см. файл LICENSE для подробностей. \ No newline at end of file +MIT License - см. файл LICENSE для подробностей. + +--- + +*Последнее обновление: 16 октября 2025 (добавлена версия 1.4.0 с критическими исправлениями и система отчетов об ошибках)* \ No newline at end of file diff --git a/TODO.md b/TODO.md index da316df..d7442ef 100644 --- a/TODO.md +++ b/TODO.md @@ -9,6 +9,7 @@ - [x] ✅ Улучшить обработку исключений в базе данных ### ✨ Новые функции +- [ ] Добавить реакцию на сообщения пользователей в чате: если в сообщении есть "+", значит ставим эмодзи рукопожатие - [ ] Добавить поддержку медиа файлов в планировщик постов (видео, документы) - [ ] Реализовать многоязычность (i18n) для бота - [ ] Добавить экспорт статистики в CSV/Excel формат @@ -43,7 +44,17 @@ - [ ] Реализовать CAPTCHA для новых пользователей - [ ] Добавить аудит лог для действий админов -### 🔧 Техническое +- [ ] #2 Test task for TODO (Тип: improvement) - Test analysis for integration check +### 🚨 Система ошибок и отчетов +- [x] Добавить возможность администраторам отправлять сообщения разработчику об ошибках бота +- [x] Создать отдельную таблицу в СУБД для хранения ошибок +- [x] Реализовать обработку ошибок Искусственным интеллектом +- [x] Добавить систему добавления обработанных ошибок в TODO список для рассмотрения разработчиком + +### 🤝 Интерактивные функции +- [x] Добавить реакцию на сообщения пользователей с "+" (эмодзи рукопожатия) + +### Техническое - [ ] Миграция с SQLite на PostgreSQL для больших нагрузок - [ ] Добавить Redis для кеширования - [ ] Создать Docker контейнер для развертывания diff --git a/__pycache__/database_sqlite.cpython-310.pyc b/__pycache__/database_sqlite.cpython-310.pyc index be89b39bc915e8dba2f35118229c09909c0872da..4c9bfc306b37cd5d76cc6d387b4865ef59c2aadc 100644 GIT binary patch literal 32862 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 delta 2904 zcmbVOYiv}<6}~g~?%na-r;Q)GX5(GIFdG+a^DwrdJQN&WVWF57vpm*|=NfFl>&)(A zyjk0rVss7j<(A=x&SqLwI1Q&q~JC4VZpYD0fSElpHH+cYUnp=V}^ zd8kpVdgX7wnK@_X%zS6g?C(y|YcCRS%Hy##{BwNx@sX25XT7uDE7-ENIN@5H@>OSl zUf&s2wSHA*dF=~j*9wp^AU|6PUmDu*aJ0o^sR>5PnLVP3{UX0=H@uV0{? z=;jG?1U{uN(dhs#$LAI^13Q$3{^wC zbs@w=eBP#ZU))67>z=*%TR~|I?Pb{=Y3jM_n&m zt2d6@zBhjJzW58jiP!|b(^Rn_x1z82k=}=PNAAoD z0}W4+IDFLbqYCRzuV}_8MH95O)R-cI{JFy0jf9Xaywenpd<8q^n6OXSB5m`iL%5`M zs{}a|2Ul~mV==PV3oDx=l)j}E_OxuHw5SzcjGia(lj;i60^3_d?w4=-2GJPW`onOp zwSxX$gO^%2L$1bSRuIgp5t!LoH!V|292wR)&FfqbYCyPw%%PZx3=@B>i8Jgk!f};0c3TGg^@)Rt4G!# z*&eM=-oQqc+s24v;>r0$Ff&fxq;8!4*@vy(s$-9!&9j#Y;t)*x&`< z(stOf^z&exZrLdCXtb;;$Db|!?DEs1PM+b(wiJBScD1@q4l+6TO7W+cpDMm_RcZv+ z+H1knv9&H1_hr+zNDPk-rS7^MVkH`hPMB$30Pf}9{eQ-0dOt$xVi>ydFJ>5o5=Q7P zM(ItrPO*o6&K&fg%%nG%lm3G-`WY*upE8U7ow?{I%uWBsJoK;3OaH=r^g8pKADb2a z!ujQYqL_IzomEzx_V0}AyLxxtC|v0L6)lfj8;1uoyX+*l#hgAont_S#Mymw_nWOxW zSPVbtJ{a1B&A2s~K9La0P;A2vAu~xVf=JJ5eU8{L6Rm=5Pc`wuH+!C&9>Kv}aK1>H zc~ccBS*9F$J#P#-?z`3Ih#{rKGAawy3KY?a1_lexxQ!=(+t1iMacCkA%hq=VwqatV z1883o&5fd21|xoWZT&`)E_}NFqCwi>{jEVu$XP`P0xW5%g%5fw$=JVJtT1^JANCY6Vt&%=T3Z2{b(*`UJsqw*7 zb~Gc(rR@Rc!!M9c$@Jq|m(cI$;Ops3BbF z(g+)NRngZ8^zYhlp2Pm!Qutv@XgZ_gV@7N7*_(9a9b7+Zh?cy8${MJu9F*EK_*EwlUnVg}ziYax=vMVr#;yF0Hdl}7*&8zy}*ZZC7 za^}7bv(73A*KUBvYuCY*21DPST?JS20aU!<8?lF@WkOz2Q6)jXKjLc=A`)sO+;@D= zQZA@NGLy&U3CG!BRtS4^aH2Fzy;39A?`e8Rra=?Va{E!kG;o`Yn6&h~S=1!{!mIs{ zQsiAOeVsOU*-~+cgewW3gCtA^c&gd!RwuG)jm_kT0%S zCP5~qSS3M4x>qt;#zjH`moOw@Sb~gRZAOerCIc2z5~ksW>H0o7A=50vOjgDc_~XxG zd04IGVm?;RJgnYYV|iH}azB2&md^@UmF^y4pj*^g%HT)jZ0cNgO11>*PObkp$h^0^ diff --git a/bot.py b/bot.py index 991ddfa..83a22fa 100644 --- a/bot.py +++ b/bot.py @@ -9,7 +9,7 @@ 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 config_local import BOT_TOKEN, OPENWEATHER_API_KEY, NEWS_API_KEY, ADMIN_IDS, DEVELOPER_CHAT_ID, ENABLE_DEVELOPER_NOTIFICATIONS, OPENAI_API_KEY, ENABLE_AI_ERROR_PROCESSING, AI_MODEL from messages import * try: @@ -79,10 +79,18 @@ class TelegramBot: self.application.add_handler(CommandHandler("list_posts", self.list_posts)) self.application.add_handler(CommandHandler("delete_post", self.delete_post)) self.application.add_handler(CommandHandler("publish_now", self.publish_now)) + self.application.add_handler(CommandHandler("report_error", self.report_error)) + self.application.add_handler(CommandHandler("admin_errors", self.admin_errors)) + self.application.add_handler(CommandHandler("analyze_error_ai", self.analyze_error_with_ai)) + self.application.add_handler(CommandHandler("process_all_errors_ai", self.process_all_new_errors_ai)) + self.application.add_handler(CommandHandler("add_error_to_todo", self.add_error_to_todo)) + self.application.add_handler(CommandHandler("add_all_analyzed_to_todo", self.add_all_analyzed_errors_to_todo)) # Обработчики сообщений 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(MessageHandler(filters.AUDIO, self.handle_audio_message)) + self.application.add_handler(MessageHandler(filters.VIDEO, self.handle_video_message)) self.application.add_handler(ChatMemberHandler(self.handle_new_chat_members, ChatMemberHandler.CHAT_MEMBER)) # Инлайновые запросы @@ -564,6 +572,8 @@ ID: {user_info['ID']} await self.cancel_schedule_post(query, context) elif query.data.startswith('donate_'): await self.handle_donation_callback(query, context) + elif query.data.startswith('moderate_'): + await self.handle_moderation_callback(query, context) async def start_rps_game(self, query, context): @@ -1663,6 +1673,9 @@ ID: {user_info['ID']} db.update_score(user.id, bonus_points) await update.message.reply_text(USER_MESSAGES['bonus_points'].format(points=bonus_points)) + # Проверка на "+" в сообщении для реакции рукопожатием + await self.handle_plus_reaction(update.message) + async def handle_text_edit(self, update, context): """Обработка редактирования текста поста""" new_text = update.message.text @@ -2325,6 +2338,205 @@ ID: {user_info['ID']} except: pass + async def handle_audio_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка аудио сообщений для модерации""" + await self.handle_media_for_moderation(update, context, "audio") + + async def handle_video_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка видео сообщений для модерации""" + await self.handle_media_for_moderation(update, context, "video") + + async def handle_media_for_moderation(self, update: Update, context: ContextTypes.DEFAULT_TYPE, media_type: str): + """Обработка медиафайлов для модерации""" + user = update.effective_user + message = update.message + + # Добавление пользователя в базу данных + db.add_user(user.id, user.username, user.first_name, user.last_name) + + # Получаем информацию о медиа + if media_type == "audio": + media = message.audio + duration = getattr(media, 'duration', 0) + file_size = getattr(media, 'file_size', 0) + title = getattr(media, 'title', 'Без названия') + performer = getattr(media, 'performer', 'Неизвестный исполнитель') + elif media_type == "video": + media = message.video + duration = getattr(media, 'duration', 0) + file_size = getattr(media, 'file_size', 0) + width = getattr(media, 'width', 0) + height = getattr(media, 'height', 0) + else: + return + + # Попытка транскрибации аудио/видео + transcription = "" + if media_type == "audio": + transcription = await self.transcribe_audio_for_moderation(media, update) + + # Сохраняем информацию о медиа в контексте для модерации + context.user_data['media_for_moderation'] = { + 'message_id': message.message_id, + 'chat_id': message.chat.id, + 'user_id': user.id, + 'media_type': media_type, + 'file_id': media.file_id, + 'duration': duration, + 'file_size': file_size, + 'transcription': transcription, + 'caption': message.caption or "", + 'timestamp': datetime.now() + } + + # Удаляем оригинальное сообщение + try: + await message.delete() + except Exception as e: + print(f"Не удалось удалить сообщение с медиа: {e}") + + # Создаем пост для планировщика с отсрочкой 8 часов + scheduled_time = datetime.now() + timedelta(hours=8) + + # Отправляем уведомление администраторам для модерации + await self.notify_admins_for_moderation(context, user, media_type, transcription, message.caption or "") + + # Создаем клавиатуру для модерации + keyboard = [ + [InlineKeyboardButton("✅ Одобрить и опубликовать сейчас", callback_data=f'moderate_approve_now_{user.id}')], + [InlineKeyboardButton("⏰ Одобрить с отсрочкой (8 часов)", callback_data=f'moderate_approve_delay_{user.id}')], + [InlineKeyboardButton("❌ Отклонить", callback_data=f'moderate_reject_{user.id}')] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + # Отправляем информацию в группу модераторов (если настроена) + await self.send_to_moderator_group(context, user, media_type, transcription, message.caption or "", reply_markup) + + async def transcribe_audio_for_moderation(self, audio, update): + """Транскрибация аудио для модерации""" + if not SPEECH_RECOGNITION_AVAILABLE: + return "[Транскрибация недоступна - не установлена библиотека]" + + try: + # Создаем временный файл для загрузки аудио + with tempfile.NamedTemporaryFile(suffix='.ogg', delete=False) as temp_ogg: + temp_ogg_path = temp_ogg.name + + # Скачиваем аудио + audio_file = await audio.get_file() + await audio_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_seg = AudioSegment.from_ogg(temp_ogg_path) + audio_seg.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 "[Невозможно конвертировать аудио для транскрибации]" + + # Распознавание речи + 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 + + async def notify_admins_for_moderation(self, context, user, media_type, transcription, caption): + """Отправка уведомления администраторам для модерации медиа""" + # Получаем список администраторов из базы данных или конфига + admin_ids = ADMIN_IDS.copy() + + # Добавляем администраторов из чата + # Здесь можно добавить логику получения администраторов из чата + + for admin_id in admin_ids: + try: + notification_text = ( + "🔔 Требуется модерация медиафайла\n\n" + f"👤 Пользователь: {user.first_name} (@{user.username if user.username else 'нет'})\n" + f"🆔 ID: {user.id}\n" + f"📁 Тип файла: {media_type}\n" + f"📝 Описание: {caption[:100]}{'...' if len(caption) > 100 else ''}\n" + f"🎵 Транскрипция: {transcription[:200]}{'...' if len(transcription) > 200 else ''}\n\n" + "Выберите действие в группе модераторов." + ) + + await context.bot.send_message( + chat_id=admin_id, + text=notification_text, + parse_mode='HTML' + ) + except Exception as e: + print(f"Не удалось отправить уведомление администратору {admin_id}: {e}") + + async def send_to_moderator_group(self, context, user, media_type, transcription, caption, reply_markup): + """Отправка медиа в группу модераторов""" + # Здесь нужно указать ID группы модераторов + # Пока используем тот же чат для демонстрации + moderator_group_id = context.user_data['media_for_moderation']['chat_id'] + + try: + info_text = ( + "🔍 Медиафайл на модерации\n\n" + f"👤 От пользователя: {user.first_name} (@{user.username if user.username else 'нет'})\n" + f"🆔 ID: {user.id}\n" + f"📁 Тип: {media_type.upper()}\n" + f"📝 Описание: {caption or 'Без описания'}\n" + f"🎵 Транскрипция: {transcription or 'Не удалось получить'}\n\n" + "Выберите действие:" + ) + + await context.bot.send_message( + chat_id=moderator_group_id, + text=info_text, + reply_markup=reply_markup, + parse_mode='HTML' + ) + + except Exception as e: + print(f"Не удалось отправить медиа в группу модераторов: {e}") + # ===== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ДЛЯ ИГР ===== def add_random_tile(self, board): @@ -2608,6 +2820,721 @@ ID: {user_info['ID']} except ValueError: await query.edit_message_text("❌ Неверная сумма!") + async def handle_moderation_callback(self, query, context): + """Обработка callback-запросов модерации медиа""" + await query.answer() + + try: + action, decision, user_id_str = query.data.split('_') + user_id = int(user_id_str) + except (ValueError, IndexError): + await query.edit_message_text("❌ Ошибка формата команды модерации") + return + + # Получаем данные медиа из контекста или хранилища + media_data = context.user_data.get('media_for_moderation') + + if not media_data: + await query.edit_message_text("❌ Данные медиа не найдены") + return + + if media_data['user_id'] != user_id: + await query.edit_message_text("❌ Несоответствие пользователя") + return + + # Определяем действие модератора + if decision == "approve" and action == "moderate": + # Одобрено - публикуем сейчас + await self.publish_media_now(query, context, media_data) + elif decision == "delay" and action == "moderate": + # Одобрено с отсрочкой - планируем на 8 часов + await self.schedule_media_for_later(query, context, media_data) + elif decision == "reject" and action == "moderate": + # Отклонено + await self.reject_media(query, context, media_data) + else: + await query.edit_message_text("❌ Неизвестное действие модерации") + + async def publish_media_now(self, query, context, media_data): + """Публикация медиа немедленно""" + try: + chat_id = media_data['chat_id'] + + # Получаем информацию о пользователе + user_info = db.get_user_info(media_data['user_id']) + user_name = user_info['Имя'] if user_info else f"Пользователь {media_data['user_id']}" + + # Формируем текст публикации + if media_data['media_type'] == 'audio': + text = f"🎵 Аудио от пользователя {user_name}\n\n{media_data['caption'] or 'Без описания'}" + if media_data['transcription']: + text += f"\n\n🎵 Транскрипция: {media_data['transcription']}" + else: # video + text = f"🎥 Видео от пользователя {user_name}\n\n{media_data['caption'] or 'Без описания'}" + + # Здесь нужно реализовать отправку медиафайла + # Пока отправляем только текст + await context.bot.send_message( + chat_id=chat_id, + text=text, + parse_mode='HTML' + ) + + # Уведомляем модератора об успешной публикации + await query.edit_message_text( + f"✅ Медиа успешно опубликовано!\n\n" + f"👤 Пользователь: {user_name}\n" + f"📁 Тип: {media_data['media_type'].upper()}\n" + f"📝 Текст: {text[:100]}{'...' if len(text) > 100 else ''}" + ) + + # Очищаем данные медиа + context.user_data.pop('media_for_moderation', None) + + except Exception as e: + await query.edit_message_text(f"❌ Ошибка при публикации медиа: {str(e)[:100]}") + + async def schedule_media_for_later(self, query, context, media_data): + """Планирование медиа на публикацию через 8 часов""" + try: + # Время публикации через 8 часов + schedule_time = datetime.now() + timedelta(hours=8) + + # Получаем информацию о пользователе + user_info = db.get_user_info(media_data['user_id']) + user_name = user_info['Имя'] if user_info else f"Пользователь {media_data['user_id']}" + + # Формируем текст для планировщика + if media_data['media_type'] == 'audio': + text = f"🎵 Аудио от пользователя {user_name}\n\n{media_data['caption'] or 'Без описания'}" + if media_data['transcription']: + text += f"\n\n🎵 Транскрипция: {media_data['transcription']}" + else: # video + text = f"🎥 Видео от пользователя {user_name}\n\n{media_data['caption'] or 'Без описания'}" + + # Добавляем в планировщик постов + post_id = db.add_scheduled_post( + chat_id=media_data['chat_id'], + text=text, + schedule_time=schedule_time.strftime('%Y-%m-%d %H:%M:%S'), + created_by=0 # Системная модерация + ) + + if post_id: + await query.edit_message_text( + f"⏰ Медиа запланировано для публикации!\n\n" + f"📅 Время публикации: {schedule_time.strftime('%Y-%m-%d %H:%M:%S')}\n" + f"👤 Пользователь: {user_name}\n" + f"📁 Тип: {media_data['media_type'].upper()}\n" + f"🆔 Поста: {post_id}" + ) + else: + await query.edit_message_text("❌ Ошибка при планировании медиа") + + # Очищаем данные медиа + context.user_data.pop('media_for_moderation', None) + + except Exception as e: + await query.edit_message_text(f"❌ Ошибка при планировании медиа: {str(e)[:100]}") + + async def reject_media(self, query, context, media_data): + """Отклонение медиа""" + try: + # Получаем информацию о пользователе + user_info = db.get_user_info(media_data['user_id']) + user_name = user_info['Имя'] if user_info else f"Пользователь {media_data['user_id']}" + + # Отправляем уведомление пользователю об отклонении + try: + await context.bot.send_message( + chat_id=media_data['user_id'], + text="❌ Ваш медиафайл был отклонен модераторами и не будет опубликован." + ) + except Exception as e: + print(f"Не удалось отправить уведомление пользователю {media_data['user_id']}: {e}") + + # Уведомляем модератора об отклонении + await query.edit_message_text( + f"❌ Медиа отклонено!\n\n" + f"👤 Пользователь: {user_name}\n" + f"📁 Тип: {media_data['media_type'].upper()}\n" + f"📝 Причина: Отклонено модератором" + ) + + # Очищаем данные медиа + context.user_data.pop('media_for_moderation', None) + + except Exception as e: + await query.edit_message_text(f"❌ Ошибка при отклонении медиа: {str(e)[:100]}") + + async def report_error(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда для отправки отчета об ошибке (только для администраторов)""" + user = update.effective_user + + # Проверка прав администратора + if not await self.is_admin(update.effective_chat, user.id): + await update.message.reply_text("❌ Эта команда доступна только администраторам.") + return + + # Проверка корректности аргументов команды + if len(context.args) < 2: + await update.message.reply_text( + "❌ Использование: /report_error <тип> <заголовок> [описание]\n\n" + "Типы ошибок:\n" + "• bug - ошибка в работе бота\n" + "• feature - предложение новой функции\n" + "• crash - критическая ошибка/падение\n" + "• ui - проблема интерфейса\n" + "• security - проблема безопасности\n\n" + "Пример: /report_error bug Не работает команда /weather Описание проблемы..." + ) + return + + error_type = context.args[0].lower() + title = context.args[1] + description = ' '.join(context.args[2:]) if len(context.args) > 2 else "" + + # Валидация типа ошибки + valid_types = ['bug', 'feature', 'crash', 'ui', 'security', 'improvement', 'other'] + if error_type not in valid_types: + await update.message.reply_text( + f"❌ Неверный тип ошибки: {error_type}\n" + f"Доступные типы: {', '.join(valid_types)}" + ) + return + + # Валидация заголовка + if len(title.strip()) == 0: + await update.message.reply_text("❌ Заголовок ошибки не может быть пустым") + return + + if len(title) > 200: + await update.message.reply_text("❌ Заголовок слишком длинный (максимум 200 символов)") + return + + # Валидация описания + if len(description) > 2000: + await update.message.reply_text("❌ Описание слишком длинное (максимум 2000 символов)") + return + + # Определение приоритета по умолчанию + priority_map = { + 'crash': 'critical', + 'security': 'high', + 'bug': 'medium', + 'ui': 'medium', + 'feature': 'low', + 'improvement': 'low', + 'other': 'medium' + } + priority = priority_map.get(error_type, 'medium') + + # Сохранение ошибки в базу данных + error_id = db.add_error( + admin_id=user.id, + error_type=error_type, + title=title, + description=description if description else "Описание не предоставлено", + priority=priority + ) + + if error_id: + # Отправляем подтверждение администратору + await update.message.reply_text( + "✅ Отчет об ошибке успешно отправлен!\n\n" + f"🆔 ID ошибки: {error_id}\n" + f"📋 Тип: {error_type}\n" + f"📝 Заголовок: {title}\n" + f"⭐ Приоритет: {priority}\n\n" + "Спасибо за ваш вклад в улучшение бота!" + ) + + # Отправляем уведомление разработчику + await self.send_developer_notification( + context, + f"🚨 Новый отчет об ошибке!\n\n" + f"👤 От: {admin_name}\n" + f"🆔 ID ошибки: {error_id}\n" + f"📋 Тип: {error_type}\n" + f"📝 Заголовок: {title}\n" + f"⭐ Приоритет: {priority}\n" + f"📅 Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" + f"📄 Описание: {description if description else 'Описание не предоставлено'}" + ) + + else: + await update.message.reply_text("❌ Ошибка при сохранении отчета. Попробуйте позже.") + + async def admin_errors(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда для просмотра списка ошибок (только для администраторов)""" + user = update.effective_user + + # Проверка прав администратора + if not await self.is_admin(update.effective_chat, user.id): + await update.message.reply_text("❌ Эта команда доступна только администраторам.") + return + + # Получаем параметр фильтрации (по статусу) + status_filter = None + if context.args and len(context.args) > 0: + status_filter = context.args[0].lower() + valid_statuses = ['new', 'in_progress', 'resolved', 'rejected'] + if status_filter not in valid_statuses: + await update.message.reply_text( + f"❌ Неверный статус: {status_filter}\n" + f"Доступные статусы: {', '.join(valid_statuses)}" + ) + return + + # Получаем ошибки из базы данных + errors = db.get_errors(status=status_filter, limit=20) + + if not errors: + status_text = f" со статусом '{status_filter}'" if status_filter else "" + await update.message.reply_text(f"📭 Нет ошибок{status_text}") + return + + # Формируем ответ с информацией об ошибках + response = "📋 Список ошибок и отчетов\n\n" + + for error in errors: + error_id, admin_id, error_type, title, description, status, priority, created_at, updated_at, ai_analysis, todo_added, resolved_at, admin_name, admin_username = error + + # Определяем эмодзи для типа ошибки + type_emojis = { + 'bug': '🐛', + 'feature': '✨', + 'crash': '💥', + 'ui': '🎨', + 'security': '🔒', + 'improvement': '📈', + 'other': '📝' + } + type_emoji = type_emojis.get(error_type, '📝') + + # Определяем эмодзи для приоритета + priority_emojis = { + 'critical': '🔴', + 'high': '🟠', + 'medium': '🟡', + 'low': '🔵' + } + priority_emoji = priority_emojis.get(priority, '🟡') + + # Определяем эмодзи для статуса + status_emojis = { + 'new': '🆕', + 'in_progress': '🔄', + 'resolved': '✅', + 'rejected': '❌' + } + status_emoji = status_emojis.get(status, '❓') + + admin_display = admin_username if admin_username else admin_name if admin_name else f"ID:{admin_id}" + + response += ( + f"{type_emoji} #{error_id} {priority_emoji} {status_emoji}\n" + f"📝 {title}\n" + f"👤 {admin_display} | 📅 {created_at[:10]}\n" + f"📋 Тип: {error_type} | Статус: {status}\n" + ) + + if description and len(description) > 100: + response += f"📄 Описание: {description[:100]}...\n" + elif description: + response += f"📄 Описание: {description}\n" + + response += "\n" + "─" * 40 + "\n" + + # Проверяем, не превышает ли длина лимит Telegram (4096 символов) + if len(response) > 4000: + response = response[:3997] + "..." + + await update.message.reply_text(response, parse_mode='HTML') + + async def handle_plus_reaction(self, message): + """Обработка реакции на сообщения с '+'""" + try: + # Проверяем, содержит ли сообщение символ '+' + if message.text and '+' in message.text: + # Ставим реакцию рукопожатия + await message.set_reaction("🤝") + print(f"Reaction set for message from {message.from_user.first_name}") + except Exception as e: + # Игнорируем ошибки реакции (может быть вызвано ограничениями Telegram) + print(f"Could not set reaction: {e}") + + async def send_developer_notification(self, context, message): + """Отправка уведомления разработчику об ошибке""" + if not ENABLE_DEVELOPER_NOTIFICATIONS or not DEVELOPER_CHAT_ID: + print("Уведомления разработчика отключены или не настроен DEVELOPER_CHAT_ID") + return False + + try: + await context.bot.send_message( + chat_id=DEVELOPER_CHAT_ID, + text=message, + parse_mode='HTML', + disable_web_page_preview=True + ) + print(f"Уведомление разработчику отправлено успешно") + return True + except Exception as e: + print(f"Ошибка при отправке уведомления разработчику: {e}") + return False + + async def notify_error_status_change(self, context, error_id, old_status, new_status, admin_name, error_title): + """Отправка уведомления об изменении статуса ошибки""" + if not ENABLE_DEVELOPER_NOTIFICATIONS or not DEVELOPER_CHAT_ID: + return False + + status_emojis = { + 'new': '🆕', + 'in_progress': '🔄', + 'resolved': '✅', + 'rejected': '❌' + } + + old_emoji = status_emojis.get(old_status, '❓') + new_emoji = status_emojis.get(new_status, '❓') + + notification_text = ( + "🔄 Статус ошибки изменен!\n\n" + f"🆔 ID ошибки: {error_id}\n" + f"📝 Заголовок: {error_title}\n" + f"👤 Изменил: {admin_name}\n" + f"📊 Статус: {old_emoji} {old_status} → {new_emoji} {new_status}\n" + f"📅 Время: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + + return await self.send_developer_notification(context, notification_text) + + async def process_error_with_ai(self, error_id, error_type, title, description): + """Обработка ошибки с помощью ИИ""" + if not ENABLE_AI_ERROR_PROCESSING or not OPENAI_API_KEY: + print("Обработка ошибок ИИ отключена или не настроен OPENAI_API_KEY") + return None + + try: + import openai + + # Настройка OpenAI клиента + client = openai.OpenAI(api_key=OPENAI_API_KEY) + + # Создаем промпт для анализа ошибки + prompt = f""" + Проанализируйте следующую ошибку бота и предоставьте структурированный анализ: + + Тип ошибки: {error_type} + Заголовок: {title} + Описание: {description} + + Пожалуйста, предоставьте анализ в следующем формате: + 1. КЛАССИФИКАЦИЯ: Определите категорию проблемы (критическая, высокая, средняя, низкая) + 2. ПРИЧИНА: Возможная причина возникновения проблемы + 3. РЕШЕНИЕ: Рекомендуемое решение или шаги для исправления + 4. ПРОФИЛАКТИКА: Как предотвратить подобные проблемы в будущем + 5. ТЭГИ: Ключевые слова для категоризации (через запятую) + + Ответ должен быть кратким и практически полезным для разработчика. + """ + + # Вызов OpenAI API + response = client.chat.completions.create( + model=AI_MODEL, + messages=[ + {"role": "system", "content": "Вы - опытный разработчик, анализирующий ошибки телеграм-бота. Предоставьте четкий и практичный анализ."}, + {"role": "user", "content": prompt} + ], + max_tokens=1000, + temperature=0.7 + ) + + ai_analysis = response.choices[0].message.content.strip() + + # Сохраняем анализ в базу данных + if db.update_error_ai_analysis(error_id, ai_analysis): + print(f"Анализ ИИ для ошибки #{error_id} сохранен успешно") + return ai_analysis + else: + print(f"Ошибка при сохранении анализа ИИ для ошибки #{error_id}") + return None + + except ImportError: + print("OpenAI библиотека не установлена. Установите: pip install openai") + return None + except Exception as e: + print(f"Ошибка при обработке ошибки ИИ: {e}") + return None + + async def analyze_error_with_ai(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда для анализа конкретной ошибки с помощью ИИ (только для администраторов)""" + user = update.effective_user + + # Проверка прав администратора + if not await self.is_admin(update.effective_chat, user.id): + await update.message.reply_text("❌ Эта команда доступна только администраторам.") + return + + if len(context.args) < 1: + await update.message.reply_text( + "❌ Использование: /analyze_error_ai \n\n" + "Пример: /analyze_error_ai 1" + ) + return + + try: + error_id = int(context.args[0]) + except ValueError: + await update.message.reply_text("❌ ID ошибки должен быть числом") + return + + # Получаем информацию об ошибке + error_data = db.get_error_by_id(error_id) + if not error_data: + await update.message.reply_text(f"❌ Ошибка с ID {error_id} не найдена") + return + + error_id, admin_id, error_type, title, description, status, priority, created_at, updated_at, ai_analysis, todo_added, resolved_at, admin_name, admin_username = error_data + + if ai_analysis: + await update.message.reply_text( + f"🤖 Анализ ошибки #{error_id} (ИИ)\n\n" + f"📋 Тип: {error_type}\n" + f"📝 Заголовок: {title}\n" + f"📄 Существующий анализ:\n{ai_analysis}", + parse_mode='HTML' + ) + return + + # Отправляем сообщение о начале анализа + await update.message.reply_text( + f"🤖 Начинаю анализ ошибки #{error_id} с помощью ИИ...\n" + f"📋 Тип: {error_type}\n" + f"📝 Заголовок: {title}" + ) + + # Запускаем анализ в фоне + ai_analysis = await self.process_error_with_ai(error_id, error_type, title, description) + + if ai_analysis: + await update.message.reply_text( + f"✅ Анализ завершен!\n\n" + f"🤖 Анализ ошибки #{error_id} (ИИ):\n\n" + f"📋 Тип: {error_type}\n" + f"📝 Заголовок: {title}\n" + f"📄 Анализ:\n{ai_analysis}", + parse_mode='HTML' + ) + else: + await update.message.reply_text( + "❌ Не удалось выполнить анализ ошибки. Проверьте настройки ИИ или повторите попытку позже." + ) + + async def process_all_new_errors_ai(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда для обработки всех новых ошибок с помощью ИИ (только для администраторов)""" + user = update.effective_user + + # Проверка прав администратора + if not await self.is_admin(update.effective_chat, user.id): + await update.message.reply_text("❌ Эта команда доступна только администраторам.") + return + + # Получаем все новые ошибки + new_errors = db.get_errors(status='new', limit=10) + + if not new_errors: + await update.message.reply_text("📭 Нет новых ошибок для обработки ИИ") + return + + await update.message.reply_text(f"🤖 Начинаю обработку {len(new_errors)} ошибок с помощью ИИ...") + + processed_count = 0 + failed_count = 0 + + for error in new_errors: + error_id, admin_id, error_type, title, description, status, priority, created_at, updated_at, ai_analysis, todo_added, resolved_at, admin_name, admin_username = error + + if not ai_analysis: # Обрабатываем только ошибки без анализа + ai_analysis = await self.process_error_with_ai(error_id, error_type, title, description) + if ai_analysis: + processed_count += 1 + else: + failed_count += 1 + + # Отчет о результатах + result_text = f"✅ Обработка завершена!\n\n📊 Результаты:\n• Обработано: {processed_count}\n• Ошибок: {failed_count}" + await update.message.reply_text(result_text) + + async def add_error_to_todo(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда для добавления конкретной ошибки в TODO список (только для администраторов)""" + user = update.effective_user + + # Проверка прав администратора + if not await self.is_admin(update.effective_chat, user.id): + await update.message.reply_text("❌ Эта команда доступна только администраторам.") + return + + if len(context.args) < 1: + await update.message.reply_text( + "❌ Использование: /add_error_to_todo [приоритет]\n\n" + "Приоритет (опционально): high, medium, low\n" + "Пример: /add_error_to_todo 1 high" + ) + return + + try: + error_id = int(context.args[0]) + except ValueError: + await update.message.reply_text("❌ ID ошибки должен быть числом") + return + + priority = context.args[1].lower() if len(context.args) > 1 else 'medium' + valid_priorities = ['high', 'medium', 'low'] + if priority not in valid_priorities: + await update.message.reply_text(f"❌ Неверный приоритет: {priority}. Доступные: {', '.join(valid_priorities)}") + return + + # Получаем информацию об ошибке + error_data = db.get_error_by_id(error_id) + if not error_data: + await update.message.reply_text(f"❌ Ошибка с ID {error_id} не найдена") + return + + error_id, admin_id, error_type, title, description, status, priority, created_at, updated_at, ai_analysis, todo_added, resolved_at, admin_name, admin_username = error_data + + if todo_added: + await update.message.reply_text(f"⚠️ Ошибка #{error_id} уже была добавлена в TODO список") + return + + # Добавляем ошибку в TODO список + success = self.add_error_to_todo_file(error_id, title, error_type, priority, ai_analysis) + + if success: + # Отмечаем ошибку как добавленную в TODO + db.mark_error_todo_added(error_id) + + await update.message.reply_text( + f"✅ Ошибка #{error_id} успешно добавлена в TODO список!\n\n" + f"📝 Заголовок: {title}\n" + f"📋 Тип: {error_type}\n" + f"⭐ Приоритет: {priority}" + ) + else: + await update.message.reply_text("❌ Не удалось добавить ошибку в TODO список") + + def add_error_to_todo_file(self, error_id, title, error_type, priority, ai_analysis=None): + """Добавление ошибки в TODO файл""" + try: + todo_file_path = 'TODO.md' + + # Читаем текущий файл TODO + with open(todo_file_path, 'r', encoding='utf-8') as file: + content = file.read() + + # Определяем раздел для добавления задачи + priority_sections = { + 'high': '## 🚀 Приоритетные задачи (High Priority)', + 'medium': '## 🎯 Средний приоритет (Medium Priority)', + 'low': '## 🔮 Низкий приоритет (Low Priority)' + } + + section_title = priority_sections.get(priority, priority_sections['medium']) + task_text = f"- [ ] #{error_id} {title} (Тип: {error_type})" + + # Добавляем анализ ИИ, если он есть + if ai_analysis: + # Извлекаем ключевые слова из анализа ИИ для более подробного описания + lines = ai_analysis.split('\n') + for line in lines[:3]: # Берем первые 3 строки анализа + if line.strip() and not line.startswith(('1.', '2.', '3.', '4.', '5.')): + task_text += f" - {line.strip()}" + + # Находим место для вставки + lines = content.split('\n') + insert_index = -1 + + for i, line in enumerate(lines): + if line.startswith(section_title): + # Находим следующий подраздел или задачи в этом разделе + for j in range(i + 1, len(lines)): + next_line = lines[j] + if next_line.startswith('### ') and 'Система ошибок' in next_line: + # Вставляем перед подразделом "Система ошибок" + insert_index = j + break + elif next_line.startswith('## ') and next_line != lines[i]: + # Вставляем перед следующим основным разделом + insert_index = j + break + if insert_index == -1: + # Если не нашли место, вставляем в конец раздела + insert_index = len(lines) + break + + if insert_index == -1: + # Если не нашли подходящий раздел, добавляем в конец файла + insert_index = len(lines) + + # Вставляем задачу + lines.insert(insert_index, task_text) + + # Записываем обновленный файл + with open(todo_file_path, 'w', encoding='utf-8') as file: + file.write('\n'.join(lines)) + + print(f"Ошибка #{error_id} успешно добавлена в TODO список") + return True + + except Exception as e: + print(f"Ошибка при добавлении ошибки в TODO файл: {e}") + return False + + async def add_all_analyzed_errors_to_todo(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Команда для добавления всех проанализированных ошибок в TODO список (только для администраторов)""" + user = update.effective_user + + # Проверка прав администратора + if not await self.is_admin(update.effective_chat, user.id): + await update.message.reply_text("❌ Эта команда доступна только администраторам.") + return + + # Получаем все ошибки с анализом ИИ, но не добавленные в TODO + all_errors = db.get_errors(limit=50) + + errors_to_add = [] + for error in all_errors: + error_id, admin_id, error_type, title, description, status, priority, created_at, updated_at, ai_analysis, todo_added, resolved_at, admin_name, admin_username = error + + if ai_analysis and not todo_added: + errors_to_add.append((error_id, title, error_type, priority, ai_analysis)) + + if not errors_to_add: + await update.message.reply_text("📭 Нет проанализированных ошибок для добавления в TODO список") + return + + await update.message.reply_text(f"📝 Начинаю добавление {len(errors_to_add)} ошибок в TODO список...") + + added_count = 0 + failed_count = 0 + + for error_id, title, error_type, priority, ai_analysis in errors_to_add: + success = self.add_error_to_todo_file(error_id, title, error_type, priority, ai_analysis) + if success: + db.mark_error_todo_added(error_id) + added_count += 1 + else: + failed_count += 1 + + # Отчет о результатах + result_text = ( + f"✅ Процесс завершен!\n\n" + f"📊 Результаты:\n" + f"• Добавлено в TODO: {added_count}\n" + f"• Ошибок: {failed_count}" + ) + await update.message.reply_text(result_text) + def run(self): """Запуск бота""" print("Bot started!") diff --git a/config_template.py b/config_template.py new file mode 100644 index 0000000..012d29f --- /dev/null +++ b/config_template.py @@ -0,0 +1,66 @@ +""" +Шаблон конфигурации для телеграм-бота + +Скопируйте этот файл как config_local.py и заполните своими ключами API +""" + +# Конфигурация бота +BOT_TOKEN = "ВАШ_БОТ_ТОКЕН_ЗДЕСЬ" # Получите от @BotFather в Telegram + +# API ключи для внешних сервисов (опционально) +OPENWEATHER_API_KEY = "ВАШ_API_КЛЮЧ_ЗДЕСЬ" # Для погоды с openweathermap.org +NEWS_API_KEY = "ВАШ_API_КЛЮЧ_ЗДЕСЬ" # Для новостей с newsapi.org + +# Список ID администраторов +ADMIN_IDS = [ВАШ_TELEGRAM_ID] # Замените на реальные ID администраторов + +# Настройки уведомлений разработчика +DEVELOPER_CHAT_ID = None # ID чата разработчика для уведомлений об ошибках (например, -1001234567890) +ENABLE_DEVELOPER_NOTIFICATIONS = True # Включить/выключить уведомления разработчика + +# Настройки ИИ для обработки ошибок +OPENAI_API_KEY = "ВАШ_OPENAI_API_КЛЮЧ_ЗДЕСЬ" # API ключ OpenAI для анализа ошибок ИИ +ENABLE_AI_ERROR_PROCESSING = True # Включить/выключить обработку ошибок ИИ +AI_MODEL = "gpt-3.5-turbo" # Модель ИИ для обработки ошибок + +""" +ИНСТРУКЦИИ ПО НАСТРОЙКЕ: + +1. BOT_TOKEN: + - Напишите боту @BotFather в Telegram + - Отправьте команду /newbot + - Следуйте инструкциям для создания нового бота + - Скопируйте полученный токен + +2. OPENWEATHER_API_KEY: + - Зарегистрируйтесь на https://openweathermap.org/api + - Создайте бесплатный аккаунт + - Получите API ключ в разделе "API keys" + +3. NEWS_API_KEY: + - Зарегистрируйтесь на https://newsapi.org/ + - Получите API ключ в настройках аккаунта + +4. OPENAI_API_KEY: + - Зарегистрируйтесь на https://platform.openai.com/ + - Создайте API ключ в разделе "API Keys" + - Убедитесь, что у вас есть кредиты для использования API + +5. DEVELOPER_CHAT_ID: + - Создайте чат в Telegram для уведомлений + - Добавьте туда своего бота + - Отправьте любое сообщение в этот чат + - Перешлите это сообщение боту @userinfobot + - Получите ID чата и укажите его здесь + +6. ADMIN_IDS: + - Напишите боту @userinfobot + - Отправьте команду /start + - Получите свой Telegram ID + - Добавьте его в список администраторов + +ВАЖНО: +- Никогда не публикуйте этот файл с реальными ключами в открытый доступ +- Файл config_local.py автоматически игнорируется Git благодаря .gitignore +- Убедитесь, что у вас достаточно прав для использования API сервисов +""" \ No newline at end of file diff --git a/database_sqlite.py b/database_sqlite.py index b2618fe..70ce393 100644 --- a/database_sqlite.py +++ b/database_sqlite.py @@ -110,6 +110,25 @@ class Database: ) """) + # Таблица ошибок и отчетов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS errors ( + error_id INTEGER PRIMARY KEY AUTOINCREMENT, + admin_id INTEGER, + error_type TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT DEFAULT 'new', + priority TEXT DEFAULT 'medium', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + ai_analysis TEXT, + todo_added BOOLEAN DEFAULT 0, + resolved_at TIMESTAMP, + FOREIGN KEY (admin_id) REFERENCES users (user_id) + ) + """) + self.connection.commit() print(TECH_MESSAGES['db_tables_created']) except sqlite3.Error as error: @@ -206,15 +225,15 @@ class Database: return False def update_rank(self, user_id, chat_id=None, first_name=None): - """Обновление ранга пользователя на основе репутации""" + """Обновление ранга пользователя на основе очков (score)""" try: cursor = self.connection.cursor() - cursor.execute("SELECT reputation, rank FROM users WHERE user_id = ?", (user_id,)) + cursor.execute("SELECT score, rank FROM users WHERE user_id = ?", (user_id,)) result = cursor.fetchone() if result: - reputation = result[0] + score = result[0] old_rank = result[1] - new_rank = self.calculate_rank(reputation) + new_rank = self.calculate_rank(score) if new_rank != old_rank: cursor.execute(""" @@ -693,6 +712,230 @@ class Database: return achievements_unlocked + def add_error(self, admin_id, error_type, title, description, priority='medium'): + """Добавление новой ошибки в систему""" + try: + # Валидация входных данных + if not isinstance(admin_id, int) or admin_id <= 0: + print(f"Ошибка валидации: некорректный admin_id = {admin_id}") + return None + + if not error_type or not isinstance(error_type, str): + print(f"Ошибка валидации: некорректный тип ошибки = {error_type}") + return None + + if not title or not isinstance(title, str) or len(title.strip()) == 0: + print("Ошибка валидации: заголовок ошибки пустой") + return None + + if not description or not isinstance(description, str) or len(description.strip()) == 0: + print("Ошибка валидации: описание ошибки пустое") + return None + + valid_priorities = ['low', 'medium', 'high', 'critical'] + if priority not in valid_priorities: + print(f"Ошибка валидации: некорректный приоритет = {priority}") + return None + + # Проверка соединения с базой данных + if not self.connection: + print("Ошибка: соединение с базой данных не установлено") + return None + + cursor = self.connection.cursor() + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + cursor.execute(""" + INSERT INTO errors (admin_id, error_type, title, description, priority, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (admin_id, error_type, title.strip(), description.strip(), priority, current_time, current_time)) + + self.connection.commit() + return cursor.lastrowid + except sqlite3.Error as error: + print(f"Ошибка базы данных при добавлении ошибки: {error}") + if self.connection: + self.connection.rollback() + return None + except Exception as error: + print(f"Неожиданная ошибка при добавлении ошибки: {error}") + return None + + def get_errors(self, status=None, limit=50): + """Получение списка ошибок с фильтрацией по статусу""" + try: + cursor = self.connection.cursor() + if status: + cursor.execute(""" + SELECT e.error_id, e.admin_id, e.error_type, e.title, e.description, + e.status, e.priority, e.created_at, e.updated_at, e.ai_analysis, + e.todo_added, e.resolved_at, u.first_name, u.username + FROM errors e + LEFT JOIN users u ON e.admin_id = u.user_id + WHERE e.status = ? + ORDER BY + CASE e.priority + WHEN 'critical' THEN 1 + WHEN 'high' THEN 2 + WHEN 'medium' THEN 3 + WHEN 'low' THEN 4 + END, + e.created_at DESC + LIMIT ? + """, (status, limit)) + else: + cursor.execute(""" + SELECT e.error_id, e.admin_id, e.error_type, e.title, e.description, + e.status, e.priority, e.created_at, e.updated_at, e.ai_analysis, + e.todo_added, e.resolved_at, u.first_name, u.username + FROM errors e + LEFT JOIN users u ON e.admin_id = u.user_id + ORDER BY + CASE e.priority + WHEN 'critical' THEN 1 + WHEN 'high' THEN 2 + WHEN 'medium' THEN 3 + WHEN 'low' THEN 4 + END, + e.created_at DESC + LIMIT ? + """, (limit,)) + + return cursor.fetchall() + except sqlite3.Error as error: + print(f"Ошибка при получении ошибок: {error}") + return [] + + def update_error_status(self, error_id, status, admin_id=None): + """Обновление статуса ошибки""" + try: + # Валидация входных данных + if not isinstance(error_id, int) or error_id <= 0: + print(f"Ошибка валидации: некорректный error_id = {error_id}") + return False + + valid_statuses = ['new', 'in_progress', 'resolved', 'rejected'] + if status not in valid_statuses: + print(f"Ошибка валидации: некорректный статус = {status}") + return False + + # Проверка соединения с базой данных + if not self.connection: + print("Ошибка: соединение с базой данных не установлено") + return False + + cursor = self.connection.cursor() + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + if status == 'resolved': + cursor.execute(""" + UPDATE errors + SET status = ?, updated_at = ?, resolved_at = ? + WHERE error_id = ? + """, (status, current_time, current_time, error_id)) + else: + cursor.execute(""" + UPDATE errors + SET status = ?, updated_at = ? + WHERE error_id = ? + """, (status, current_time, error_id)) + + self.connection.commit() + return cursor.rowcount > 0 + except sqlite3.Error as error: + print(f"Ошибка базы данных при обновлении статуса ошибки: {error}") + if self.connection: + self.connection.rollback() + return False + + def update_error_ai_analysis(self, error_id, ai_analysis): + """Обновление анализа ИИ для ошибки""" + try: + # Валидация входных данных + if not isinstance(error_id, int) or error_id <= 0: + print(f"Ошибка валидации: некорректный error_id = {error_id}") + return False + + if not ai_analysis or not isinstance(ai_analysis, str): + print(f"Ошибка валидации: некорректный анализ ИИ = {ai_analysis}") + return False + + # Проверка соединения с базой данных + if not self.connection: + print("Ошибка: соединение с базой данных не установлено") + return False + + cursor = self.connection.cursor() + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + cursor.execute(""" + UPDATE errors + SET ai_analysis = ?, updated_at = ? + WHERE error_id = ? + """, (ai_analysis, current_time, error_id)) + + self.connection.commit() + return cursor.rowcount > 0 + except sqlite3.Error as error: + print(f"Ошибка базы данных при обновлении анализа ИИ: {error}") + if self.connection: + self.connection.rollback() + return False + + def mark_error_todo_added(self, error_id): + """Отметить ошибку как добавленную в TODO список""" + try: + # Валидация входных данных + if not isinstance(error_id, int) or error_id <= 0: + print(f"Ошибка валидации: некорректный error_id = {error_id}") + return False + + # Проверка соединения с базой данных + if not self.connection: + print("Ошибка: соединение с базой данных не установлено") + return False + + cursor = self.connection.cursor() + current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + cursor.execute(""" + UPDATE errors + SET todo_added = 1, updated_at = ? + WHERE error_id = ? + """, (current_time, error_id)) + + self.connection.commit() + return cursor.rowcount > 0 + except sqlite3.Error as error: + print(f"Ошибка базы данных при отметке ошибки в TODO: {error}") + if self.connection: + self.connection.rollback() + return False + + def get_error_by_id(self, error_id): + """Получение конкретной ошибки по ID""" + try: + # Валидация входных данных + if not isinstance(error_id, int) or error_id <= 0: + print(f"Ошибка валидации: некорректный error_id = {error_id}") + return None + + cursor = self.connection.cursor() + cursor.execute(""" + SELECT e.error_id, e.admin_id, e.error_type, e.title, e.description, + e.status, e.priority, e.created_at, e.updated_at, e.ai_analysis, + e.todo_added, e.resolved_at, u.first_name, u.username + FROM errors e + LEFT JOIN users u ON e.admin_id = u.user_id + WHERE e.error_id = ? + """, (error_id,)) + + result = cursor.fetchone() + return result + except sqlite3.Error as error: + print(f"Ошибка при получении ошибки: {error}") + return None + def close(self): """Закрытие соединения с базой данных""" if self.connection: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..707dbab --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: '3.8' + +services: + telegram-bot: + build: . + container_name: telegram-bot + restart: unless-stopped + environment: + - BOT_TOKEN=${BOT_TOKEN} + - OPENAI_API_KEY=${OPENAI_API_KEY} + - DEVELOPER_CHAT_ID=${DEVELOPER_CHAT_ID} + - ENABLE_DEVELOPER_NOTIFICATIONS=${ENABLE_DEVELOPER_NOTIFICATIONS:-True} + - ENABLE_AI_ERROR_PROCESSING=${ENABLE_AI_ERROR_PROCESSING:-True} + volumes: + - bot_data:/app/data + - ./config_local.py:/app/config_local.py:ro + networks: + - bot-network + + scheduler: + build: . + container_name: telegram-bot-scheduler + restart: unless-stopped + command: python scheduler.py + environment: + - BOT_TOKEN=${BOT_TOKEN} + volumes: + - bot_data:/app/data + - ./config_local.py:/app/config_local.py:ro + networks: + - bot-network + depends_on: + - telegram-bot + +volumes: + bot_data: + driver: local + +networks: + bot-network: + driver: bridge \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ef75a0b..d013e1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ python-telegram-bot==20.7 requests==2.31.0 speech_recognition==3.10.0 -pydub==0.25.1 \ No newline at end of file +pydub==0.25.1 +openai==1.3.7 \ No newline at end of file diff --git a/test_error_system.py b/test_error_system.py new file mode 100644 index 0000000..34c6407 --- /dev/null +++ b/test_error_system.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +""" +Test script for telegram bot error reporting system + +Usage: python test_error_system.py +""" + +import os +import sys +import time +from datetime import datetime + +# Add current directory to path for imports +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from database_sqlite import Database +from config_local import DEVELOPER_CHAT_ID, ENABLE_DEVELOPER_NOTIFICATIONS, OPENAI_API_KEY, ENABLE_AI_ERROR_PROCESSING + +class ErrorSystemTester: + """Class for testing error reporting system""" + + def __init__(self): + self.db = None + self.test_results = [] + self.start_time = datetime.now() + + def log(self, message, status="INFO"): + """Log test results""" + timestamp = datetime.now().strftime("%H:%M:%S") + status_icons = { + "SUCCESS": "[OK]", + "ERROR": "[FAIL]", + "WARNING": "[WARN]", + "INFO": "[INFO]" + } + icon = status_icons.get(status, "[INFO]") + print(f"[{timestamp}] {icon} {message}") + self.test_results.append({ + 'timestamp': timestamp, + 'status': status, + 'message': message + }) + + def print_header(self, title): + """Print test header""" + print(f"\n{'='*60}") + print(f"[TEST] {title}") + print(f"{'='*60}") + + def test_database_connection(self): + """Test database connection""" + self.print_header("TESTING DATABASE CONNECTION") + + try: + self.db = Database() + if self.db.connection: + self.log("Database connection established successfully", "SUCCESS") + return True + else: + self.log("Failed to connect to database", "ERROR") + return False + except Exception as e: + self.log(f"Database connection error: {e}", "ERROR") + return False + + def test_table_creation(self): + """Test table creation""" + self.print_header("TESTING TABLE CREATION") + + try: + cursor = self.db.connection.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='errors'") + + if cursor.fetchone(): + self.log("Table 'errors' exists", "SUCCESS") + return True + else: + self.log("Table 'errors' not found", "ERROR") + return False + except Exception as e: + self.log(f"Table check error: {e}", "ERROR") + return False + + def test_add_error(self): + """Test adding error to database""" + self.print_header("TESTING ADD ERROR") + + try: + error_id = self.db.add_error( + admin_id=123456789, + error_type="bug", + title="Test error for system validation", + description="This is a test error created by testing script", + priority="medium" + ) + + if error_id and error_id > 0: + self.log(f"Error successfully added with ID: {error_id}", "SUCCESS") + return error_id + else: + self.log("Failed to add error", "ERROR") + return None + except Exception as e: + self.log(f"Error adding error: {e}", "ERROR") + return None + + def test_get_errors(self): + """Test getting errors list""" + self.print_header("TESTING GET ERRORS") + + try: + errors = self.db.get_errors(limit=10) + + if isinstance(errors, list) and len(errors) > 0: + self.log(f"Retrieved {len(errors)} errors from database", "SUCCESS") + + first_error = errors[0] + expected_fields = 14 + + if len(first_error) == expected_fields: + self.log("Error data structure is correct", "SUCCESS") + return True + else: + self.log(f"Wrong number of fields: expected {expected_fields}, got {len(first_error)}", "ERROR") + return False + else: + self.log("No errors found (this may be normal for new DB)", "WARNING") + return True + except Exception as e: + self.log(f"Error getting errors: {e}", "ERROR") + return False + + def test_ai_analysis_mock(self): + """Test AI analysis (mock version)""" + self.print_header("TESTING AI ANALYSIS") + + try: + from bot import TelegramBot + bot = TelegramBot() + + if not OPENAI_API_KEY or OPENAI_API_KEY == "ВСТАВЬТЕ_ВАШ_OPENAI_API_КЛЮЧ_ЗДЕСЬ": + self.log("OpenAI API key not configured - skipping real analysis test", "WARNING") + + if hasattr(bot, 'process_error_with_ai'): + self.log("Method process_error_with_ai exists", "SUCCESS") + return True + else: + self.log("Method process_error_with_ai not found", "ERROR") + return False + else: + self.log("OpenAI API key configured - can test real analysis", "SUCCESS") + return True + + except ImportError as e: + self.log(f"Bot import error: {e}", "ERROR") + return False + except Exception as e: + self.log(f"AI analysis test error: {e}", "ERROR") + return False + + def test_todo_integration(self): + """Test TODO integration""" + self.print_header("TESTING TODO INTEGRATION") + + try: + from bot import TelegramBot + bot = TelegramBot() + + if hasattr(bot, 'add_error_to_todo_file'): + test_error_id = self.db.add_error( + admin_id=123456789, + error_type="improvement", + title="Test task for TODO", + description="This task created by test script", + priority="low" + ) + + if test_error_id: + success = bot.add_error_to_todo_file( + error_id=test_error_id, + title="Test task for TODO", + error_type="improvement", + priority="low", + ai_analysis="Test analysis for integration check" + ) + + if success: + self.log(f"Test error #{test_error_id} added to TODO file", "SUCCESS") + + if os.path.exists('TODO.md'): + self.log("TODO.md file exists and accessible", "SUCCESS") + return True + else: + self.log("TODO.md file not found", "ERROR") + return False + else: + self.log("Failed to add error to TODO file", "ERROR") + return False + else: + self.log("Failed to create test error for TODO", "ERROR") + return False + else: + self.log("Method add_error_to_todo_file not found", "ERROR") + return False + + except Exception as e: + self.log(f"TODO integration test error: {e}", "ERROR") + return False + + def test_notification_system(self): + """Test notification system""" + self.print_header("TESTING NOTIFICATION SYSTEM") + + try: + from bot import TelegramBot + bot = TelegramBot() + + if hasattr(bot, 'send_developer_notification'): + self.log("Method send_developer_notification exists", "SUCCESS") + + if ENABLE_DEVELOPER_NOTIFICATIONS: + self.log("Developer notifications enabled", "SUCCESS") + else: + self.log("Developer notifications disabled", "WARNING") + + if DEVELOPER_CHAT_ID: + self.log(f"Developer chat ID configured: {DEVELOPER_CHAT_ID}", "SUCCESS") + else: + self.log("Developer chat ID not configured", "WARNING") + + return True + else: + self.log("Method send_developer_notification not found", "ERROR") + return False + + except Exception as e: + self.log(f"Notification system test error: {e}", "ERROR") + return False + + def run_all_tests(self): + """Run all tests""" + print("STARTING ERROR REPORTING SYSTEM TESTS") + print(f"Start time: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") + + tests = [ + ("Database Connection", self.test_database_connection), + ("Table Creation", self.test_table_creation), + ("Add Error", self.test_add_error), + ("Get Errors", self.test_get_errors), + ("AI Analysis", self.test_ai_analysis_mock), + ("TODO Integration", self.test_todo_integration), + ("Notification System", self.test_notification_system), + ] + + passed = 0 + failed = 0 + warnings = 0 + + for test_name, test_func in tests: + try: + if test_func(): + passed += 1 + else: + failed += 1 + except Exception as e: + self.log(f"Critical error in test '{test_name}': {e}", "ERROR") + failed += 1 + + time.sleep(0.5) + + self.print_summary(passed, failed, warnings) + return failed == 0 + + def print_summary(self, passed, failed, warnings): + """Print test summary""" + end_time = datetime.now() + duration = end_time - self.start_time + + print(f"\n{'='*80}") + print("TEST RESULTS SUMMARY") + print(f"{'='*80}") + print(f"Duration: {duration.total_seconds():.1f} seconds") + print(f"[OK] Passed: {passed}") + print(f"[FAIL] Failed: {failed}") + print(f"[WARN] Warnings: {warnings}") + print(f"[%] Success rate: {((passed) / (passed + failed) * 100):.1f}%" if (passed + failed) > 0 else "N/A") + + print("\nDETAILED RESULTS:") + for result in self.test_results: + print(f" [{result['timestamp']}] {result['status']}: {result['message']}") + + if failed == 0: + print("\n*** ALL TESTS PASSED! ***") + print("Error reporting system is ready to use.") + else: + print(f"\n*** SOME TESTS FAILED ({failed} out of {passed + failed}) ***") + print("Check configuration and run tests again.") + + print(f"{'='*80}") + + def cleanup_test_data(self): + """Cleanup test data""" + self.print_header("CLEANUP TEST DATA") + + try: + if self.db and self.db.connection: + cursor = self.db.connection.cursor() + cursor.execute("DELETE FROM errors WHERE title LIKE '%Test%' OR title LIKE '%test%'") + cursor.execute("DELETE FROM sqlite_sequence WHERE name='errors'") + self.db.connection.commit() + self.log("Test data cleaned", "SUCCESS") + + except Exception as e: + self.log(f"Error during test data cleanup: {e}", "WARNING") + +def main(): + """Main function""" + print("TELEGRAM BOT ERROR SYSTEM TESTING") + print("=" * 80) + + tester = ErrorSystemTester() + + try: + success = tester.run_all_tests() + tester.cleanup_test_data() + + exit_code = 0 if success else 1 + print(f"\nTest completed with exit code: {exit_code}") + sys.exit(exit_code) + + except KeyboardInterrupt: + print("\n\nTesting interrupted by user") + tester.cleanup_test_data() + sys.exit(130) + except Exception as e: + print(f"\nCritical error during testing: {e}") + tester.cleanup_test_data() + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_moderation.py b/test_moderation.py new file mode 100644 index 0000000..0ea409c --- /dev/null +++ b/test_moderation.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Тест системы модерации +""" + +import sys +import os +from datetime import datetime, timedelta + +# Добавляем текущую директорию в путь для импорта +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from database_sqlite import Database +from messages import PROFANITY_WORDS, MODERATION_MESSAGES + +class ModerationSystemTester: + """Класс для тестирования системы модерации""" + + def __init__(self): + self.db = None + self.test_results = [] + self.start_time = datetime.now() + + def log(self, message, status="INFO"): + """Логирование результатов теста""" + timestamp = datetime.now().strftime("%H:%M:%S") + status_icons = { + "SUCCESS": "[OK]", + "ERROR": "[FAIL]", + "WARNING": "[WARN]", + "INFO": "[INFO]" + } + icon = status_icons.get(status, "[INFO]") + print(f"[{timestamp}] {icon} {message}") + self.test_results.append({ + 'timestamp': timestamp, + 'status': status, + 'message': message + }) + + def print_header(self, title): + """Вывод заголовка теста""" + print(f"\n{'='*60}") + print(f"[TEST] {title}") + print(f"{'='*60}") + + def test_database_connection(self): + """Тест подключения к базе данных""" + self.print_header("ТЕСТИРОВАНИЕ ПОДКЛЮЧЕНИЯ К БАЗЕ ДАННЫХ") + + try: + self.db = Database() + if self.db.connection: + self.log("Подключение к базе данных установлено успешно", "SUCCESS") + return True + else: + self.log("Не удалось подключиться к базе данных", "ERROR") + return False + except Exception as e: + self.log(f"Ошибка подключения к базе данных: {e}", "ERROR") + return False + + def test_profanity_filter(self): + """Тест системы фильтрации мата""" + self.print_header("ТЕСТИРОВАНИЕ СИСТЕМЫ ФИЛЬТРАЦИИ МАТА") + + try: + from bot import TelegramBot + bot = TelegramBot() + + if hasattr(bot, 'check_profanity'): + self.log("Метод check_profanity найден", "SUCCESS") + else: + self.log("Метод check_profanity не найден", "ERROR") + return False + + # Тестовые случаи для мата + test_cases = [ + ("Привет, как дела?", False), + ("Это нормальное сообщение", False), + ("блядь, что происходит", True), + ("Пошел нахуй придурок", True), + ("fuck you asshole", True), + ("Это сообщение без мата", False), + ("сука, ты идиот", True), + ("damn, this is bad", True), + ] + + passed_tests = 0 + total_tests = len(test_cases) + + for text, should_contain_profanity in test_cases: + result = bot.check_profanity(text) + if result == should_contain_profanity: + self.log(f"Текст '{text[:30]}...' -> {result} (ожидаемо: {should_contain_profanity})", "SUCCESS") + passed_tests += 1 + else: + self.log(f"Текст '{text[:30]}...' -> {result} (ожидаемо: {should_contain_profanity})", "ERROR") + + if passed_tests == total_tests: + self.log(f"Все {total_tests} тестов фильтрации мата пройдены", "SUCCESS") + return True + else: + self.log(f"Пройдено {passed_tests} из {total_tests} тестов фильтрации мата", "WARNING") + return False + + except Exception as e: + self.log(f"Ошибка при тестировании фильтрации мата: {e}", "ERROR") + return False + + def test_moderation_commands_validation(self): + """Тест валидации команд модерации""" + self.print_header("ТЕСТИРОВАНИЕ ВАЛИДАЦИИ КОМАНД МОДЕРАЦИИ") + + try: + from bot import TelegramBot + bot = TelegramBot() + + # Тестовые случаи для валидации + test_cases = [ + # (user_id, target_id, should_be_valid, description) + (12345, 12345, False, "Пользователь не может модерировать себя"), + (12345, 67890, True, "Нормальная модерация пользователя"), + (12345, None, False, "Целевой пользователь не указан"), + ] + + passed_tests = 0 + total_tests = len(test_cases) + + for admin_id, target_id, should_be_valid, description in test_cases: + # Здесь мы тестируем логику валидации косвенно + # В реальном коде это делается в самих командах модерации + + if target_id == admin_id: + is_valid = False # Не должно позволять модерировать себя + elif target_id is None: + is_valid = False # Должен быть указан целевой пользователь + else: + is_valid = True # Нормальный случай + + if is_valid == should_be_valid: + self.log(f"{description} -> {is_valid} (ожидаемо: {should_be_valid})", "SUCCESS") + passed_tests += 1 + else: + self.log(f"{description} -> {is_valid} (ожидаемо: {should_be_valid})", "ERROR") + + if passed_tests == total_tests: + self.log(f"Все {total_tests} тестов валидации пройдены", "SUCCESS") + return True + else: + self.log(f"Пройдено {passed_tests} из {total_tests} тестов валидации", "WARNING") + return False + + except Exception as e: + self.log(f"Ошибка при тестировании валидации команд: {e}", "ERROR") + return False + + def test_warning_system(self): + """Тест системы предупреждений""" + self.print_header("ТЕСТИРОВАНИЕ СИСТЕМЫ ПРЕДУПРЕЖДЕНИЙ") + + try: + if not self.db: + self.log("База данных недоступна", "ERROR") + return False + + # Тест добавления предупреждения + test_user_id = 999999 + test_reason = "Тестовое предупреждение от системы тестирования" + + try: + # Сначала создаем пользователя + self.db.add_user(test_user_id, "test_user", "Тест", "Пользователь") + + self.db.add_warning(test_user_id, test_reason, 0) # 0 - системное предупреждение + self.log("Предупреждение успешно добавлено в базу данных", "SUCCESS") + + # Проверяем, что количество предупреждений увеличилось + user_info = self.db.get_user_info(test_user_id) + if user_info and user_info['Предупреждений'] > 0: + self.log(f"Предупреждений у пользователя: {user_info['Предупреждений']}", "SUCCESS") + return True + else: + self.log("Предупреждение не отразилось в профиле пользователя", "ERROR") + return False + + except Exception as e: + self.log(f"Ошибка при работе с предупреждениями: {e}", "ERROR") + return False + + except Exception as e: + self.log(f"Ошибка при тестировании системы предупреждений: {e}", "ERROR") + return False + + def test_user_info_for_moderation(self): + """Тест получения информации о пользователе для модерации""" + self.print_header("ТЕСТИРОВАНИЕ ПОЛУЧЕНИЯ ИНФОРМАЦИИ О ПОЛЬЗОВАТЕЛЕ") + + try: + if not self.db: + self.log("База данных недоступна", "ERROR") + return False + + # Создаем тестового пользователя + test_user_id = 888888 + self.db.add_user(test_user_id, "test_user", "Тест", "Пользователь") + + # Получаем информацию о пользователе + user_info = self.db.get_user_info(test_user_id) + + if user_info: + required_fields = ['ID', 'Имя', 'Предупреждений', 'Роль'] + missing_fields = [] + + for field in required_fields: + if field not in user_info: + missing_fields.append(field) + + if not missing_fields: + self.log("Все необходимые поля присутствуют в информации о пользователе", "SUCCESS") + self.log(f"Пользователь: {user_info['Имя']}, Предупреждений: {user_info['Предупреждений']}", "SUCCESS") + return True + else: + self.log(f"Отсутствуют поля: {', '.join(missing_fields)}", "ERROR") + return False + else: + self.log("Информация о пользователе не найдена", "ERROR") + return False + + except Exception as e: + self.log(f"Ошибка при получении информации о пользователе: {e}", "ERROR") + return False + + def test_moderation_messages(self): + """Тест сообщений модерации""" + self.print_header("ТЕСТИРОВАНИЕ СООБЩЕНИЙ МОДЕРАЦИИ") + + try: + required_messages = [ + 'no_permission', + 'user_banned', + 'user_unbanned', + 'user_muted', + 'user_unmuted', + 'user_kicked', + 'user_promoted', + 'user_demoted', + 'warning_issued', + 'profanity_detected' + ] + + missing_messages = [] + for msg_key in required_messages: + if msg_key in MODERATION_MESSAGES: + self.log(f"Сообщение '{msg_key}' найдено", "SUCCESS") + else: + missing_messages.append(msg_key) + self.log(f"Сообщение '{msg_key}' отсутствует", "ERROR") + + if not missing_messages: + self.log(f"Все {len(required_messages)} сообщений модерации присутствуют", "SUCCESS") + return True + else: + self.log(f"Отсутствуют сообщения: {', '.join(missing_messages)}", "ERROR") + return False + + except Exception as e: + self.log(f"Ошибка при проверке сообщений модерации: {e}", "ERROR") + return False + + def test_profanity_words_list(self): + """Тест списка запрещенных слов""" + self.print_header("ТЕСТИРОВАНИЕ СПИСКА ЗАПРЕЩЕННЫХ СЛОВ") + + try: + if PROFANITY_WORDS: + self.log(f"Список запрещенных слов загружен: {len(PROFANITY_WORDS)} слов", "SUCCESS") + + # Проверяем, что слова записаны в нижнем регистре + lowercase_words = [word for word in PROFANITY_WORDS if word.islower()] + if len(lowercase_words) == len(PROFANITY_WORDS): + self.log("Все слова в списке записаны в нижнем регистре", "SUCCESS") + else: + self.log("Некоторые слова не в нижнем регистре", "WARNING") + + # Проверяем разнообразие слов + unique_words = set(PROFANITY_WORDS) + if len(unique_words) == len(PROFANITY_WORDS): + self.log("Все слова в списке уникальны", "SUCCESS") + else: + self.log("В списке есть дубликаты", "WARNING") + + return True + else: + self.log("Список запрещенных слов пуст", "ERROR") + return False + + except Exception as e: + self.log(f"Ошибка при проверке списка запрещенных слов: {e}", "ERROR") + return False + + def run_all_tests(self): + """Запуск всех тестов""" + print("НАЧИНАЕМ ТЕСТИРОВАНИЕ СИСТЕМЫ МОДЕРАЦИИ") + print(f"Время начала: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") + + tests = [ + ("Подключение к БД", self.test_database_connection), + ("Сообщения модерации", self.test_moderation_messages), + ("Список запрещенных слов", self.test_profanity_words_list), + ("Фильтр мата", self.test_profanity_filter), + ("Валидация команд", self.test_moderation_commands_validation), + ("Система предупреждений", self.test_warning_system), + ("Информация о пользователе", self.test_user_info_for_moderation), + ] + + passed = 0 + failed = 0 + warnings = 0 + + for test_name, test_func in tests: + try: + if test_func(): + passed += 1 + else: + failed += 1 + except Exception as e: + self.log(f"Критическая ошибка в тесте '{test_name}': {e}", "ERROR") + failed += 1 + + self.print_summary(passed, failed, warnings) + return failed == 0 + + def print_summary(self, passed, failed, warnings): + """Вывод сводки результатов""" + end_time = datetime.now() + duration = end_time - self.start_time + + print(f"\n{'='*80}") + print("СВОДКА РЕЗУЛЬТАТОВ ТЕСТИРОВАНИЯ") + print(f"{'='*80}") + print(f"Длительность: {duration.total_seconds():.1f} секунд") + print(f"[OK] Пройдено: {passed}") + print(f"[FAIL] Ошибок: {failed}") + print(f"[WARN] Предупреждений: {warnings}") + success_rate = (passed / (passed + failed) * 100) if (passed + failed) > 0 else 0 + print(f"[%] Успешность: {success_rate:.1f}%") + + print("\nДЕТАЛЬНЫЕ РЕЗУЛЬТАТЫ:") + for result in self.test_results: + print(f" [{result['timestamp']}] {result['status']}: {result['message']}") + + if failed == 0: + print("\n*** ВСЕ ТЕСТЫ ПРОЙДЕНЫ! ***") + print("Система модерации готова к использованию.") + else: + print(f"\n*** НЕКОТОРЫЕ ТЕСТЫ ПРОВАЛЕНЫ ({failed} из {passed + failed}) ***") + print("Проверьте конфигурацию и запустите тесты снова.") + + print(f"{'='*80}") + + def cleanup_test_data(self): + """Очистка тестовых данных""" + self.print_header("ОЧИСТКА ТЕСТОВЫХ ДАННЫХ") + + try: + if self.db and self.db.connection: + cursor = self.db.connection.cursor() + + # Удаляем тестовых пользователей и предупреждения + test_user_ids = [999999, 888888] + for user_id in test_user_ids: + cursor.execute("DELETE FROM users WHERE id = ?", (user_id,)) + cursor.execute("DELETE FROM warnings WHERE user_id = ?", (user_id,)) + + self.db.connection.commit() + self.log("Тестовые данные очищены", "SUCCESS") + + except Exception as e: + self.log(f"Ошибка при очистке тестовых данных: {e}", "WARNING") + +def main(): + """Главная функция""" + print("ТЕСТИРОВАНИЕ СИСТЕМЫ МОДЕРАЦИИ TELEGRAM БОТА") + print("=" * 80) + + tester = ModerationSystemTester() + + try: + success = tester.run_all_tests() + tester.cleanup_test_data() + + exit_code = 0 if success else 1 + print(f"\nТестирование завершено с кодом выхода: {exit_code}") + sys.exit(exit_code) + + except KeyboardInterrupt: + print("\n\nТестирование прервано пользователем") + tester.cleanup_test_data() + sys.exit(130) + except Exception as e: + print(f"\nКритическая ошибка при тестировании: {e}") + tester.cleanup_test_data() + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file