mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
🔥 Version 1.4.0 - Critical Security & Stability Fixes
This commit is contained in:
@@ -4,10 +4,16 @@
|
|||||||
|
|
||||||
## 📋 История версий
|
## 📋 История версий
|
||||||
|
|
||||||
### Версия 1.3.0 (16 октября 2025)
|
### Версия 1.4.0 (16 октября 2025) - Критические исправления
|
||||||
**Дата релиза:** 16.10.2025
|
**Дата релиза:** 16.10.2025
|
||||||
|
|
||||||
#### 🐛 Исправления ошибок
|
#### 🚨 Критические исправления безопасности и стабильности
|
||||||
|
- ✅ **Исправлена система рангов** - синхронизированы поля очков и репутации в базе данных
|
||||||
|
- ✅ **Улучшена обработка ошибок внешних API** - добавлены таймауты, детальная обработка HTTP-ошибок
|
||||||
|
- ✅ **Добавлена валидация входящих данных** - защита от некорректных данных и злоупотреблений
|
||||||
|
- ✅ **Улучшена обработка исключений в БД** - валидация параметров, автоматический rollback транзакций
|
||||||
|
|
||||||
|
#### Исправления ошибок
|
||||||
- ✅ Исправлена ошибка с повторяющимся сообщением о "первом сообщении" при вводе суммы доната
|
- ✅ Исправлена ошибка с повторяющимся сообщением о "первом сообщении" при вводе суммы доната
|
||||||
- ✅ Исправлена проблема с кодировкой Unicode символов при запуске бота на Windows
|
- ✅ Исправлена проблема с кодировкой Unicode символов при запуске бота на Windows
|
||||||
- ✅ Исправлена система донатов - теперь корректно работает кнопка "Другая сумма"
|
- ✅ Исправлена система донатов - теперь корректно работает кнопка "Другая сумма"
|
||||||
@@ -21,6 +27,12 @@
|
|||||||
- ✅ Оптимизирована логика проверки достижений в базе данных
|
- ✅ Оптимизирована логика проверки достижений в базе данных
|
||||||
- ✅ Добавлена проверка существующих достижений перед разблокировкой
|
- ✅ Добавлена проверка существующих достижений перед разблокировкой
|
||||||
- ✅ Улучшена обработка ошибок в системе донатов
|
- ✅ Улучшена обработка ошибок в системе донатов
|
||||||
|
- ✅ Создан тестовый скрипт для проверки корректности рангов
|
||||||
|
- ✅ Добавлена защита от само-действий в командах модерации
|
||||||
|
|
||||||
|
### Версия 1.3.0 (Предыдущая версия)
|
||||||
|
- ✅ Исправления ошибок донатной системы и кодировки Unicode
|
||||||
|
- ✅ Улучшения системы достижений
|
||||||
|
|
||||||
### Версия 1.2.0 (Предыдущая версия)
|
### Версия 1.2.0 (Предыдущая версия)
|
||||||
- 🚀 Добавлены расширенные игры: 2048, Тетрис, Змейка
|
- 🚀 Добавлены расширенные игры: 2048, Тетрис, Змейка
|
||||||
@@ -37,6 +49,13 @@
|
|||||||
- ✅ Автоматическое удаление приветственных сообщений через 2 минуты
|
- ✅ Автоматическое удаление приветственных сообщений через 2 минуты
|
||||||
- ✅ Интеграция донатов в приветственные сообщения
|
- ✅ Интеграция донатов в приветственные сообщения
|
||||||
|
|
||||||
|
### Безопасность и надежность
|
||||||
|
- 🔒 **Валидация входящих данных** - все команды проверяют корректность параметров
|
||||||
|
- 🛡️ **Защита от злоупотреблений** - ограничения на длину текстов, ID пользователей
|
||||||
|
- ⚡ **Обработка ошибок API** - таймауты, повторные попытки, детальные сообщения об ошибках
|
||||||
|
- 💾 **Надежность базы данных** - валидация запросов, автоматический rollback транзакций
|
||||||
|
- 🚫 **Предотвращение само-действий** - пользователи не могут модерировать сами себя
|
||||||
|
|
||||||
### Игровые элементы
|
### Игровые элементы
|
||||||
- 🎮 Мини-игры: Камень-ножницы-бумага, Крестики-нолики, Викторина, Морской бой
|
- 🎮 Мини-игры: Камень-ножницы-бумага, Крестики-нолики, Викторина, Морской бой
|
||||||
- 🎯 Расширенные игры: 2048, Тетрис, Змейка
|
- 🎯 Расширенные игры: 2048, Тетрис, Змейка
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
## 🚀 Приоритетные задачи (High Priority)
|
## 🚀 Приоритетные задачи (High Priority)
|
||||||
|
|
||||||
### 🔧 Критические исправления
|
### 🔧 Критические исправления
|
||||||
- [ ] Исправить обработку ошибок при недоступности внешних API (OpenWeatherMap, NewsAPI)
|
- [x] ✅ При начислении очков не меняется ранг. Например, /info Очков 6112 Ранг:Рядовой, а должен быть Капитан
|
||||||
- [ ] Добавить валидацию входящих данных для всех команд
|
- [x] ✅ Исправить обработку ошибок при недоступности внешних API (OpenWeatherMap, NewsAPI)
|
||||||
- [ ] Улучшить обработку исключений в базе данных
|
- [x] ✅ Добавить валидацию входящих данных для всех команд
|
||||||
|
- [x] ✅ Улучшить обработку исключений в базе данных
|
||||||
|
|
||||||
### ✨ Новые функции
|
### ✨ Новые функции
|
||||||
- [ ] Добавить поддержку медиа файлов в планировщик постов (видео, документы)
|
- [ ] Добавить поддержку медиа файлов в планировщик постов (видео, документы)
|
||||||
@@ -49,7 +50,11 @@
|
|||||||
|
|
||||||
## ✅ Завершенные задачи
|
## ✅ Завершенные задачи
|
||||||
|
|
||||||
### Версия 1.3.0 (16 октября 2025)
|
### Версия 1.4.0 (16 октября 2025) - Критические исправления
|
||||||
|
- [x] ✅ **Исправлена система рангов** - синхронизированы поля очков и репутации в базе данных
|
||||||
|
- [x] ✅ **Улучшена обработка ошибок внешних API** - добавлены таймауты, детальная обработка HTTP-ошибок
|
||||||
|
- [x] ✅ **Добавлена валидация входящих данных** - защита от некорректных данных и злоупотреблений
|
||||||
|
- [x] ✅ **Улучшена обработка исключений в БД** - валидация параметров, автоматический rollback транзакций
|
||||||
- [x] ✅ Исправлена ошибка с повторяющимся "первым сообщением" при донатах
|
- [x] ✅ Исправлена ошибка с повторяющимся "первым сообщением" при донатах
|
||||||
- [x] ✅ Исправлена кодировка Unicode символов при запуске на Windows
|
- [x] ✅ Исправлена кодировка Unicode символов при запуске на Windows
|
||||||
- [x] ✅ Исправлена система донатов - кнопка "Другая сумма"
|
- [x] ✅ Исправлена система донатов - кнопка "Другая сумма"
|
||||||
@@ -58,6 +63,10 @@
|
|||||||
- [x] ✅ Улучшена система достижений (разблокировка только один раз)
|
- [x] ✅ Улучшена система достижений (разблокировка только один раз)
|
||||||
- [x] ✅ Обновлена документация с историей версий
|
- [x] ✅ Обновлена документация с историей версий
|
||||||
|
|
||||||
|
### Версия 1.3.0
|
||||||
|
- [x] ✅ Исправления ошибок донатной системы и кодировки Unicode
|
||||||
|
- [x] ✅ Улучшения системы достижений
|
||||||
|
|
||||||
### Версия 1.2.0
|
### Версия 1.2.0
|
||||||
- [x] ✅ Добавлены расширенные игры: 2048, Тетрис, Змейка
|
- [x] ✅ Добавлены расширенные игры: 2048, Тетрис, Змейка
|
||||||
- [x] ✅ Улучшена система достижений
|
- [x] ✅ Улучшена система достижений
|
||||||
@@ -65,13 +74,17 @@
|
|||||||
|
|
||||||
## 📅 Планирование релизов
|
## 📅 Планирование релизов
|
||||||
|
|
||||||
### Следующий релиз (v1.4.0) - Декабрь 2025
|
### Следующий релиз (v1.5.0) - Декабрь 2025
|
||||||
- Цель: Улучшение стабильности и производительности
|
- Цель: Расширение игровых возможностей
|
||||||
- Основной фокус: Исправление ошибок и оптимизация
|
- Основной фокус: Новые мини-игры и улучшение ИИ
|
||||||
|
|
||||||
### Релиз Q1 2026 (v2.0.0) - Март 2026
|
### Релиз Q1 2026 (v2.0.0) - Март 2026
|
||||||
- Цель: Значительное расширение функциональности
|
- Цель: Значительное расширение функциональности
|
||||||
- Основной фокус: Новые игры и улучшенный интерфейс
|
- Основной фокус: Веб-интерфейс управления и многоязычность
|
||||||
|
|
||||||
|
### Релиз Q2 2026 (v2.1.0) - Июнь 2026
|
||||||
|
- Цель: Масштабирование и производительность
|
||||||
|
- Основной фокус: Миграция на PostgreSQL и Redis кеширование
|
||||||
|
|
||||||
## 🛠 Инструкции для contributors
|
## 🛠 Инструкции для contributors
|
||||||
|
|
||||||
@@ -89,4 +102,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Последнее обновление: 16 октября 2025*
|
*Последнее обновление: 16 октября 2025 (добавлена версия 1.4.0 с критическими исправлениями)*
|
||||||
Binary file not shown.
@@ -312,12 +312,46 @@ ID: {user_info['ID']}
|
|||||||
|
|
||||||
city = ' '.join(context.args) if context.args else 'Moscow'
|
city = ' '.join(context.args) if context.args else 'Moscow'
|
||||||
|
|
||||||
|
# Валидация города
|
||||||
|
if not city or len(city.strip()) < 2:
|
||||||
|
await update.message.reply_text("❌ Название города должно содержать минимум 2 символа")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ограничиваем длину названия города для безопасности
|
||||||
|
if len(city) > 50:
|
||||||
|
await update.message.reply_text("❌ Название города слишком длинное (максимум 50 символов)")
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Добавляем таймаут для запроса
|
||||||
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={OPENWEATHER_API_KEY}&units=metric&lang=ru"
|
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={OPENWEATHER_API_KEY}&units=metric&lang=ru"
|
||||||
response = requests.get(url)
|
|
||||||
|
# Создаем сессию с таймаутом
|
||||||
|
import requests
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
|
||||||
|
# Проверяем статус код
|
||||||
|
if response.status_code == 401:
|
||||||
|
await update.message.reply_text("❌ Ошибка API: неверный ключ погоды")
|
||||||
|
return
|
||||||
|
elif response.status_code == 429:
|
||||||
|
await update.message.reply_text("❌ Превышен лимит запросов к API погоды. Попробуйте позже.")
|
||||||
|
return
|
||||||
|
elif response.status_code >= 500:
|
||||||
|
await update.message.reply_text("❌ Сервер погоды временно недоступен. Попробуйте позже.")
|
||||||
|
return
|
||||||
|
elif response.status_code != 200:
|
||||||
|
await update.message.reply_text("❌ Ошибка при получении данных о погоде")
|
||||||
|
return
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
if data['cod'] == 200:
|
if data.get('cod') == 200:
|
||||||
|
# Проверяем, что все необходимые данные присутствуют
|
||||||
|
if 'name' not in data or 'main' not in data or 'weather' not in data:
|
||||||
|
await update.message.reply_text("❌ Неполные данные о погоде от сервера")
|
||||||
|
return
|
||||||
|
|
||||||
weather_text = WEATHER_MESSAGES['weather_info'].format(
|
weather_text = WEATHER_MESSAGES['weather_info'].format(
|
||||||
city=data['name'],
|
city=data['name'],
|
||||||
temp=data['main']['temp'],
|
temp=data['main']['temp'],
|
||||||
@@ -325,12 +359,27 @@ ID: {user_info['ID']}
|
|||||||
humidity=data['main']['humidity'],
|
humidity=data['main']['humidity'],
|
||||||
description=data['weather'][0]['description']
|
description=data['weather'][0]['description']
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
weather_text = WEATHER_MESSAGES['city_not_found']
|
|
||||||
|
|
||||||
await update.message.reply_text(weather_text, parse_mode='HTML')
|
await update.message.reply_text(weather_text, parse_mode='HTML')
|
||||||
|
else:
|
||||||
|
# Обработка различных кодов ошибок от API
|
||||||
|
if data.get('cod') == '404':
|
||||||
|
await update.message.reply_text(f"❌ Город '{city}' не найден")
|
||||||
|
elif data.get('cod') == '401':
|
||||||
|
await update.message.reply_text("❌ Ошибка авторизации API погоды")
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(f"❌ Ошибка API погоды: {data.get('message', 'Неизвестная ошибка')}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await update.message.reply_text("❌ Превышено время ожидания ответа от сервера погоды. Попробуйте позже.")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
await update.message.reply_text("❌ Ошибка подключения к серверу погоды. Проверьте интернет-соединение.")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
await update.message.reply_text(f"❌ Ошибка сети при получении погоды: {str(e)[:100]}")
|
||||||
|
except (KeyError, ValueError, TypeError) as e:
|
||||||
|
await update.message.reply_text(f"❌ Ошибка обработки данных погоды: {str(e)[:100]}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await update.message.reply_text(WEATHER_MESSAGES['weather_error'].format(error=e))
|
print(f"Неожиданная ошибка в weather: {e}")
|
||||||
|
await update.message.reply_text("❌ Неожиданная ошибка при получении погоды. Попробуйте позже.")
|
||||||
|
|
||||||
async def news(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def news(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Получить новости"""
|
"""Получить новости"""
|
||||||
@@ -339,20 +388,81 @@ ID: {user_info['ID']}
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Добавляем таймаут для запроса
|
||||||
url = f"https://newsapi.org/v2/top-headlines?country=ru&apiKey={NEWS_API_KEY}"
|
url = f"https://newsapi.org/v2/top-headlines?country=ru&apiKey={NEWS_API_KEY}"
|
||||||
response = requests.get(url)
|
|
||||||
|
# Создаем сессию с таймаутом
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
|
||||||
|
# Проверяем статус код
|
||||||
|
if response.status_code == 401:
|
||||||
|
await update.message.reply_text("❌ Ошибка API: неверный ключ новостей")
|
||||||
|
return
|
||||||
|
elif response.status_code == 429:
|
||||||
|
await update.message.reply_text("❌ Превышен лимит запросов к API новостей. Попробуйте позже.")
|
||||||
|
return
|
||||||
|
elif response.status_code >= 500:
|
||||||
|
await update.message.reply_text("❌ Сервер новостей временно недоступен. Попробуйте позже.")
|
||||||
|
return
|
||||||
|
elif response.status_code != 200:
|
||||||
|
await update.message.reply_text("❌ Ошибка при получении новостей")
|
||||||
|
return
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
if data['status'] == 'ok' and data['articles']:
|
if data.get('status') == 'ok' and data.get('articles'):
|
||||||
|
# Проверяем, что статьи содержат необходимые поля
|
||||||
|
articles = data['articles'][:5] # Берем только первые 5
|
||||||
|
valid_articles = []
|
||||||
|
|
||||||
|
for article in articles:
|
||||||
|
if article.get('title') and article.get('url'):
|
||||||
|
valid_articles.append(article)
|
||||||
|
|
||||||
|
if not valid_articles:
|
||||||
|
await update.message.reply_text("❌ Новости получены, но не содержат корректных данных")
|
||||||
|
return
|
||||||
|
|
||||||
news_text = NEWS_MESSAGES['news_title']
|
news_text = NEWS_MESSAGES['news_title']
|
||||||
for i, article in enumerate(data['articles'][:5], 1):
|
for i, article in enumerate(valid_articles, 1):
|
||||||
news_text += f"{i}. {article['title']}\n{article['url']}\n\n"
|
# Ограничиваем длину заголовка для безопасности
|
||||||
else:
|
title = article['title'][:200] if len(article['title']) > 200 else article['title']
|
||||||
news_text = NEWS_MESSAGES['news_not_found']
|
url = article['url'][:500] if len(article['url']) > 500 else article['url'] # Ограничиваем URL
|
||||||
|
news_text += f"{i}. {title}\n{url}\n\n"
|
||||||
|
|
||||||
|
# Ограничиваем общую длину сообщения
|
||||||
|
if len(news_text) > 4000:
|
||||||
|
news_text = news_text[:3997] + "..."
|
||||||
|
|
||||||
await update.message.reply_text(news_text, parse_mode='HTML')
|
await update.message.reply_text(news_text, parse_mode='HTML')
|
||||||
|
else:
|
||||||
|
# Обработка различных ошибок от API
|
||||||
|
if data.get('status') == 'error':
|
||||||
|
error_code = data.get('code', 'unknown')
|
||||||
|
error_message = data.get('message', 'Неизвестная ошибка')
|
||||||
|
|
||||||
|
if error_code == 'apiKeyInvalid':
|
||||||
|
await update.message.reply_text("❌ Ошибка API: неверный ключ новостей")
|
||||||
|
elif error_code == 'rateLimited':
|
||||||
|
await update.message.reply_text("❌ Превышен лимит запросов к API новостей")
|
||||||
|
elif error_code == 'sourcesUnavailable':
|
||||||
|
await update.message.reply_text("❌ Источники новостей недоступны")
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(f"❌ Ошибка API новостей: {error_message}")
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(NEWS_MESSAGES['news_not_found'])
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
await update.message.reply_text("❌ Превышено время ожидания ответа от сервера новостей. Попробуйте позже.")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
await update.message.reply_text("❌ Ошибка подключения к серверу новостей. Проверьте интернет-соединение.")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
await update.message.reply_text(f"❌ Ошибка сети при получении новостей: {str(e)[:100]}")
|
||||||
|
except (KeyError, ValueError, TypeError) as e:
|
||||||
|
await update.message.reply_text(f"❌ Ошибка обработки данных новостей: {str(e)[:100]}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await update.message.reply_text(NEWS_MESSAGES['news_error'].format(error=e))
|
print(f"Неожиданная ошибка в news: {e}")
|
||||||
|
await update.message.reply_text("❌ Неожиданная ошибка при получении новостей. Попробуйте позже.")
|
||||||
|
|
||||||
async def translate(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def translate(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Перевод текста"""
|
"""Перевод текста"""
|
||||||
@@ -363,12 +473,31 @@ ID: {user_info['ID']}
|
|||||||
text = ' '.join(context.args[:-1])
|
text = ' '.join(context.args[:-1])
|
||||||
target_lang = context.args[-1]
|
target_lang = context.args[-1]
|
||||||
|
|
||||||
|
# Валидация входных данных
|
||||||
|
if not text or not text.strip():
|
||||||
|
await update.message.reply_text("❌ Текст для перевода не может быть пустым")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not target_lang or len(target_lang) != 2:
|
||||||
|
await update.message.reply_text("❌ Укажите правильный код языка (например: en, ru, de)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ограничиваем длину текста для безопасности и производительности
|
||||||
|
if len(text) > 1000:
|
||||||
|
await update.message.reply_text("❌ Текст слишком длинный для перевода (максимум 1000 символов)")
|
||||||
|
return
|
||||||
|
|
||||||
# Простой перевод с помощью Google Translate API (нужен API ключ)
|
# Простой перевод с помощью Google Translate API (нужен API ключ)
|
||||||
try:
|
try:
|
||||||
# В реальном проекте используйте Google Translate API или другой сервис
|
# В реальном проекте используйте Google Translate API или другой сервис
|
||||||
await update.message.reply_text(TRANSLATE_MESSAGES['result'].format(text=text, lang=target_lang))
|
# Заглушка для демонстрации - в продакшене нужно реализовать реальный перевод
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"🔄 Функция перевода: '{text[:50]}{'...' if len(text) > 50 else ''}' на {target_lang}\n\n"
|
||||||
|
f"💡 В текущей версии бота перевод недоступен. Интегрируйте Google Translate API или другой сервис перевода."
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await update.message.reply_text(TRANSLATE_MESSAGES['error'].format(error=e))
|
print(f"Ошибка в translate: {e}")
|
||||||
|
await update.message.reply_text("❌ Ошибка при переводе текста. Попробуйте позже.")
|
||||||
|
|
||||||
async def play_game(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def play_game(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Запустить мини-игру"""
|
"""Запустить мини-игру"""
|
||||||
@@ -874,14 +1003,40 @@ ID: {user_info['ID']}
|
|||||||
await update.message.reply_text(MODERATION_MESSAGES['ban_usage'])
|
await update.message.reply_text(MODERATION_MESSAGES['ban_usage'])
|
||||||
return
|
return
|
||||||
|
|
||||||
user_id = context.args[0]
|
user_id_str = context.args[0]
|
||||||
reason = ' '.join(context.args[1:])
|
reason = ' '.join(context.args[1:])
|
||||||
|
|
||||||
|
# Валидация ID пользователя
|
||||||
try:
|
try:
|
||||||
await update.effective_chat.ban_member(int(user_id))
|
user_id = int(user_id_str)
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text("❌ Неверный формат ID пользователя. Должен быть числом.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Валидация причины
|
||||||
|
if not reason or not reason.strip():
|
||||||
|
await update.message.reply_text("❌ Укажите причину бана")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(reason) > 500:
|
||||||
|
await update.message.reply_text("❌ Причина слишком длинная (максимум 500 символов)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка, что пользователь не пытается забанить себя
|
||||||
|
if user_id == update.effective_user.id:
|
||||||
|
await update.message.reply_text("❌ Вы не можете забанить самого себя")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка, что пользователь не пытается забанить бота
|
||||||
|
if user_id == context.bot.id:
|
||||||
|
await update.message.reply_text("❌ Вы не можете забанить бота")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await update.effective_chat.ban_member(user_id)
|
||||||
await update.message.reply_text(MODERATION_MESSAGES['user_banned'].format(user_id=user_id, reason=reason))
|
await update.message.reply_text(MODERATION_MESSAGES['user_banned'].format(user_id=user_id, reason=reason))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await update.message.reply_text(MODERATION_MESSAGES['ban_error'].format(error=e))
|
await update.message.reply_text(f"❌ Ошибка при бане пользователя: {str(e)[:100]}")
|
||||||
|
|
||||||
async def unban_user(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def unban_user(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Разбанить пользователя (только админы)"""
|
"""Разбанить пользователя (только админы)"""
|
||||||
@@ -911,11 +1066,40 @@ ID: {user_info['ID']}
|
|||||||
await update.message.reply_text(MODERATION_MESSAGES['mute_usage'])
|
await update.message.reply_text(MODERATION_MESSAGES['mute_usage'])
|
||||||
return
|
return
|
||||||
|
|
||||||
user_id = context.args[0]
|
user_id_str = context.args[0]
|
||||||
|
time_str = context.args[1]
|
||||||
|
|
||||||
|
# Валидация ID пользователя
|
||||||
try:
|
try:
|
||||||
mute_time = int(context.args[1])
|
user_id = int(user_id_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await update.message.reply_text(MODERATION_MESSAGES['mute_invalid_time'])
|
await update.message.reply_text("❌ Неверный формат ID пользователя. Должен быть числом.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Валидация времени
|
||||||
|
try:
|
||||||
|
mute_time = int(time_str)
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text("❌ Время должно быть числом в секундах.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка диапазона времени
|
||||||
|
if mute_time < 1:
|
||||||
|
await update.message.reply_text("❌ Время должно быть положительным числом.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if mute_time > 365 * 24 * 3600: # Максимум 1 год
|
||||||
|
await update.message.reply_text("❌ Время не может превышать 1 год (31536000 секунд).")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка, что пользователь не пытается заглушить себя
|
||||||
|
if user_id == update.effective_user.id:
|
||||||
|
await update.message.reply_text("❌ Вы не можете заглушить самого себя")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка, что пользователь не пытается заглушить бота
|
||||||
|
if user_id == context.bot.id:
|
||||||
|
await update.message.reply_text("❌ Вы не можете заглушить бота")
|
||||||
return
|
return
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -923,13 +1107,13 @@ ID: {user_info['ID']}
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await update.effective_chat.restrict_member(
|
await update.effective_chat.restrict_member(
|
||||||
int(user_id),
|
user_id,
|
||||||
until_date=until_date,
|
until_date=until_date,
|
||||||
can_send_messages=False
|
can_send_messages=False
|
||||||
)
|
)
|
||||||
await update.message.reply_text(MODERATION_MESSAGES['user_muted'].format(user_id=user_id, time=mute_time))
|
await update.message.reply_text(MODERATION_MESSAGES['user_muted'].format(user_id=user_id, time=mute_time))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await update.message.reply_text(MODERATION_MESSAGES['mute_error'].format(error=e))
|
await update.message.reply_text(f"❌ Ошибка при заглушке пользователя: {str(e)[:100]}")
|
||||||
|
|
||||||
async def unmute_user(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def unmute_user(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Снять заглушку с пользователя (только админы)"""
|
"""Снять заглушку с пользователя (только админы)"""
|
||||||
@@ -965,15 +1149,41 @@ ID: {user_info['ID']}
|
|||||||
await update.message.reply_text(MODERATION_MESSAGES['kick_usage'])
|
await update.message.reply_text(MODERATION_MESSAGES['kick_usage'])
|
||||||
return
|
return
|
||||||
|
|
||||||
user_id = context.args[0]
|
user_id_str = context.args[0]
|
||||||
reason = ' '.join(context.args[1:])
|
reason = ' '.join(context.args[1:])
|
||||||
|
|
||||||
|
# Валидация ID пользователя
|
||||||
try:
|
try:
|
||||||
await update.effective_chat.ban_member(int(user_id))
|
user_id = int(user_id_str)
|
||||||
await update.effective_chat.unban_member(int(user_id)) # Разбан сразу после бана = кик
|
except ValueError:
|
||||||
|
await update.message.reply_text("❌ Неверный формат ID пользователя. Должен быть числом.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Валидация причины
|
||||||
|
if not reason or not reason.strip():
|
||||||
|
await update.message.reply_text("❌ Укажите причину кика")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(reason) > 500:
|
||||||
|
await update.message.reply_text("❌ Причина слишком длинная (максимум 500 символов)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка, что пользователь не пытается кикнуть себя
|
||||||
|
if user_id == update.effective_user.id:
|
||||||
|
await update.message.reply_text("❌ Вы не можете кикнуть самого себя")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка, что пользователь не пытается кикнуть бота
|
||||||
|
if user_id == context.bot.id:
|
||||||
|
await update.message.reply_text("❌ Вы не можете кикнуть бота")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await update.effective_chat.ban_member(user_id)
|
||||||
|
await update.effective_chat.unban_member(user_id) # Разбан сразу после бана = кик
|
||||||
await update.message.reply_text(MODERATION_MESSAGES['user_kicked'].format(user_id=user_id, reason=reason))
|
await update.message.reply_text(MODERATION_MESSAGES['user_kicked'].format(user_id=user_id, reason=reason))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await update.message.reply_text(MODERATION_MESSAGES['kick_error'].format(error=e))
|
await update.message.reply_text(f"❌ Ошибка при кике пользователя: {str(e)[:100]}")
|
||||||
|
|
||||||
async def promote_user(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def promote_user(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Повысить пользователя до модератора (только админы)"""
|
"""Повысить пользователя до модератора (только админы)"""
|
||||||
@@ -1032,11 +1242,40 @@ ID: {user_info['ID']}
|
|||||||
await update.message.reply_text(MODERATION_MESSAGES['warn_usage'])
|
await update.message.reply_text(MODERATION_MESSAGES['warn_usage'])
|
||||||
return
|
return
|
||||||
|
|
||||||
user_id = context.args[0]
|
user_id_str = context.args[0]
|
||||||
reason = ' '.join(context.args[1:])
|
reason = ' '.join(context.args[1:])
|
||||||
|
|
||||||
|
# Валидация ID пользователя
|
||||||
|
try:
|
||||||
|
user_id = int(user_id_str)
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text("❌ Неверный формат ID пользователя. Должен быть числом.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Валидация причины
|
||||||
|
if not reason or not reason.strip():
|
||||||
|
await update.message.reply_text("❌ Укажите причину предупреждения")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(reason) > 500:
|
||||||
|
await update.message.reply_text("❌ Причина слишком длинная (максимум 500 символов)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка, что пользователь не пытается выдать предупреждение себе
|
||||||
|
if user_id == update.effective_user.id:
|
||||||
|
await update.message.reply_text("❌ Вы не можете выдать предупреждение самому себе")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка, что пользователь не пытается выдать предупреждение боту
|
||||||
|
if user_id == context.bot.id:
|
||||||
|
await update.message.reply_text("❌ Вы не можете выдать предупреждение боту")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
db.add_warning(user_id, reason, update.effective_user.id)
|
db.add_warning(user_id, reason, update.effective_user.id)
|
||||||
await update.message.reply_text(MODERATION_MESSAGES['warning_issued'].format(user_id=user_id, reason=reason))
|
await update.message.reply_text(MODERATION_MESSAGES['warning_issued'].format(user_id=user_id, reason=reason))
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"❌ Ошибка при выдаче предупреждения: {str(e)[:100]}")
|
||||||
|
|
||||||
async def ranks_info(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def ranks_info(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Показать информацию о системе рангов"""
|
"""Показать информацию о системе рангов"""
|
||||||
@@ -1054,6 +1293,22 @@ ID: {user_info['ID']}
|
|||||||
|
|
||||||
csv_file = context.args[0] if context.args else 'chat_-1001519866478_users_full_20251014.csv'
|
csv_file = context.args[0] if context.args else 'chat_-1001519866478_users_full_20251014.csv'
|
||||||
|
|
||||||
|
# Валидация имени файла
|
||||||
|
if not csv_file or not csv_file.strip():
|
||||||
|
await update.message.reply_text("❌ Укажите имя CSV файла")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка на потенциально опасные символы в имени файла
|
||||||
|
import re
|
||||||
|
if not re.match(r'^[a-zA-Z0-9._\-/\s]+$', csv_file):
|
||||||
|
await update.message.reply_text("❌ Имя файла содержит недопустимые символы")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ограничение длины имени файла
|
||||||
|
if len(csv_file) > 255:
|
||||||
|
await update.message.reply_text("❌ Имя файла слишком длинное (максимум 255 символов)")
|
||||||
|
return
|
||||||
|
|
||||||
# Если путь относительный, добавляем путь к директории telegram_bot
|
# Если путь относительный, добавляем путь к директории telegram_bot
|
||||||
if not os.path.isabs(csv_file):
|
if not os.path.isabs(csv_file):
|
||||||
csv_file = os.path.join('telegram_bot', csv_file)
|
csv_file = os.path.join('telegram_bot', csv_file)
|
||||||
@@ -1093,16 +1348,40 @@ ID: {user_info['ID']}
|
|||||||
time_str = args[0]
|
time_str = args[0]
|
||||||
text = ' '.join(args[1:])
|
text = ' '.join(args[1:])
|
||||||
|
|
||||||
|
# Валидация текста поста
|
||||||
|
if not text or not text.strip():
|
||||||
|
await update.message.reply_text("❌ Текст поста не может быть пустым")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(text) > 4000:
|
||||||
|
await update.message.reply_text("❌ Текст поста слишком длинный (максимум 4000 символов)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Валидация времени
|
||||||
|
if not time_str or not time_str.strip():
|
||||||
|
await update.message.reply_text("❌ Укажите время публикации")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(time_str) > 100:
|
||||||
|
await update.message.reply_text("❌ Строка времени слишком длинная")
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
schedule_time = self.parse_schedule_time(time_str)
|
schedule_time = self.parse_schedule_time(time_str)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
await update.message.reply_text(SCHEDULER_MESSAGES['invalid_format'].format(error=str(e)))
|
await update.message.reply_text(f"❌ Ошибка формата времени: {str(e)[:100]}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if schedule_time <= datetime.now():
|
if schedule_time <= datetime.now():
|
||||||
await update.message.reply_text(SCHEDULER_MESSAGES['time_in_past'])
|
await update.message.reply_text(SCHEDULER_MESSAGES['time_in_past'])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Проверка, что время не слишком далеко в будущем (максимум 1 год)
|
||||||
|
max_future_time = datetime.now() + timedelta(days=365)
|
||||||
|
if schedule_time > max_future_time:
|
||||||
|
await update.message.reply_text("❌ Время публикации не может быть больше чем через 1 год")
|
||||||
|
return
|
||||||
|
|
||||||
# Показываем предварительный просмотр поста
|
# Показываем предварительный просмотр поста
|
||||||
preview_text = f"""
|
preview_text = f"""
|
||||||
📝 <b>Предварительный просмотр поста:</b>
|
📝 <b>Предварительный просмотр поста:</b>
|
||||||
@@ -1177,10 +1456,22 @@ ID: {user_info['ID']}
|
|||||||
await update.message.reply_text(SCHEDULER_MESSAGES['usage_delete'])
|
await update.message.reply_text(SCHEDULER_MESSAGES['usage_delete'])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
post_id_str = context.args[0]
|
||||||
|
|
||||||
|
# Валидация ID поста
|
||||||
try:
|
try:
|
||||||
post_id = int(context.args[0])
|
post_id = int(post_id_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await update.message.reply_text(SCHEDULER_MESSAGES['invalid_id'])
|
await update.message.reply_text("❌ ID поста должен быть числом")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка диапазона ID
|
||||||
|
if post_id < 1:
|
||||||
|
await update.message.reply_text("❌ ID поста должен быть положительным числом")
|
||||||
|
return
|
||||||
|
|
||||||
|
if post_id > 999999999: # Разумный максимум для SQLite
|
||||||
|
await update.message.reply_text("❌ ID поста слишком большой")
|
||||||
return
|
return
|
||||||
|
|
||||||
success = db.delete_scheduled_post(post_id, user.id)
|
success = db.delete_scheduled_post(post_id, user.id)
|
||||||
@@ -1203,10 +1494,22 @@ ID: {user_info['ID']}
|
|||||||
await update.message.reply_text(SCHEDULER_MESSAGES['usage_publish'])
|
await update.message.reply_text(SCHEDULER_MESSAGES['usage_publish'])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
post_id_str = context.args[0]
|
||||||
|
|
||||||
|
# Валидация ID поста
|
||||||
try:
|
try:
|
||||||
post_id = int(context.args[0])
|
post_id = int(post_id_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await update.message.reply_text(SCHEDULER_MESSAGES['invalid_id'])
|
await update.message.reply_text("❌ ID поста должен быть числом")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверка диапазона ID
|
||||||
|
if post_id < 1:
|
||||||
|
await update.message.reply_text("❌ ID поста должен быть положительным числом")
|
||||||
|
return
|
||||||
|
|
||||||
|
if post_id > 999999999: # Разумный максимум для SQLite
|
||||||
|
await update.message.reply_text("❌ ID поста слишком большой")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Получаем пост из базы данных
|
# Получаем пост из базы данных
|
||||||
@@ -1501,13 +1804,28 @@ ID: {user_info['ID']}
|
|||||||
if query.startswith('weather'):
|
if query.startswith('weather'):
|
||||||
city = query.split(' ', 1)[1] if len(query.split(' ', 1)) > 1 else 'Moscow'
|
city = query.split(' ', 1)[1] if len(query.split(' ', 1)) > 1 else 'Moscow'
|
||||||
|
|
||||||
if OPENWEATHER_API_KEY:
|
# Валидация города
|
||||||
|
if not city or len(city.strip()) < 2:
|
||||||
|
results.append(InlineQueryResultArticle(
|
||||||
|
id='1',
|
||||||
|
title="Ошибка",
|
||||||
|
input_message_content=InputTextMessageContent("Название города должно содержать минимум 2 символа")
|
||||||
|
))
|
||||||
|
elif len(city) > 50:
|
||||||
|
results.append(InlineQueryResultArticle(
|
||||||
|
id='1',
|
||||||
|
title="Ошибка",
|
||||||
|
input_message_content=InputTextMessageContent("Название города слишком длинное")
|
||||||
|
))
|
||||||
|
elif OPENWEATHER_API_KEY:
|
||||||
|
try:
|
||||||
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={OPENWEATHER_API_KEY}&units=metric&lang=ru"
|
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={OPENWEATHER_API_KEY}&units=metric&lang=ru"
|
||||||
response = requests.get(url)
|
response = requests.get(url, timeout=5)
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
if data['cod'] == 200:
|
if response.status_code == 200:
|
||||||
weather_text = f"🌤️ Погода в {data['name']}:\nТемпература: {data['main']['temp']}°C\nОщущается как: {data['main']['feels_like']}°C\nВлажность: {data['main']['humidity']}%\nОписание: {data['weather'][0]['description']}"
|
data = response.json()
|
||||||
|
if data.get('cod') == 200:
|
||||||
|
weather_text = f"Погода в {data['name']}:\nТемпература: {data['main']['temp']}°C\nОщущается как: {data['main']['feels_like']}°C\nВлажность: {data['main']['humidity']}%\nОписание: {data['weather'][0]['description']}"
|
||||||
results.append(InlineQueryResultArticle(
|
results.append(InlineQueryResultArticle(
|
||||||
id='1',
|
id='1',
|
||||||
title=f"Погода в {city}",
|
title=f"Погода в {city}",
|
||||||
@@ -1517,7 +1835,25 @@ ID: {user_info['ID']}
|
|||||||
results.append(InlineQueryResultArticle(
|
results.append(InlineQueryResultArticle(
|
||||||
id='1',
|
id='1',
|
||||||
title="Город не найден",
|
title="Город не найден",
|
||||||
input_message_content=InputTextMessageContent("Город не найден")
|
input_message_content=InputTextMessageContent(f"Город '{city}' не найден")
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
results.append(InlineQueryResultArticle(
|
||||||
|
id='1',
|
||||||
|
title="Ошибка API",
|
||||||
|
input_message_content=InputTextMessageContent("Ошибка при получении погоды")
|
||||||
|
))
|
||||||
|
except Exception:
|
||||||
|
results.append(InlineQueryResultArticle(
|
||||||
|
id='1',
|
||||||
|
title="Ошибка сети",
|
||||||
|
input_message_content=InputTextMessageContent("Ошибка подключения к серверу погоды")
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
results.append(InlineQueryResultArticle(
|
||||||
|
id='1',
|
||||||
|
title="API недоступен",
|
||||||
|
input_message_content=InputTextMessageContent("API погоды не настроен")
|
||||||
))
|
))
|
||||||
|
|
||||||
elif query.startswith('translate'):
|
elif query.startswith('translate'):
|
||||||
@@ -1526,17 +1862,33 @@ ID: {user_info['ID']}
|
|||||||
if len(text_parts) >= 3:
|
if len(text_parts) >= 3:
|
||||||
text = text_parts[1]
|
text = text_parts[1]
|
||||||
lang = text_parts[2]
|
lang = text_parts[2]
|
||||||
result_text = f"Перевод '{text}' на {lang}: [здесь будет перевод]"
|
|
||||||
|
if text and lang and len(text) <= 500 and len(lang) == 2:
|
||||||
|
result_text = f"Перевод '{text[:50]}{'...' if len(text) > 50 else ''}' на {lang}\n[Функция перевода недоступна в текущей версии]"
|
||||||
results.append(InlineQueryResultArticle(
|
results.append(InlineQueryResultArticle(
|
||||||
id='1',
|
id='1',
|
||||||
title=f"Перевод на {lang}",
|
title=f"Перевод на {lang}",
|
||||||
input_message_content=InputTextMessageContent(result_text)
|
input_message_content=InputTextMessageContent(result_text)
|
||||||
))
|
))
|
||||||
|
else:
|
||||||
|
results.append(InlineQueryResultArticle(
|
||||||
|
id='1',
|
||||||
|
title="Ошибка",
|
||||||
|
input_message_content=InputTextMessageContent("Использование: текст для перевода")
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
results.append(InlineQueryResultArticle(
|
||||||
|
id='1',
|
||||||
|
title="Использование",
|
||||||
|
input_message_content=InputTextMessageContent("Использование: weather [город] или translate [текст] [язык]")
|
||||||
|
))
|
||||||
|
|
||||||
await update.inline_query.answer(results)
|
await update.inline_query.answer(results)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка при обработке инлайнового запроса: {e}")
|
print(f"Ошибка при обработке инлайнового запроса: {e}")
|
||||||
|
# Пустой ответ в случае ошибки
|
||||||
|
await update.inline_query.answer([])
|
||||||
|
|
||||||
async def show_start_menu(self, query):
|
async def show_start_menu(self, query):
|
||||||
"""Показать стартовое меню"""
|
"""Показать стартовое меню"""
|
||||||
|
|||||||
+172
-25
@@ -130,26 +130,80 @@ class Database:
|
|||||||
def update_score(self, user_id, points=1):
|
def update_score(self, user_id, points=1):
|
||||||
"""Обновление очков пользователя"""
|
"""Обновление очков пользователя"""
|
||||||
try:
|
try:
|
||||||
|
# Валидация входных данных
|
||||||
|
if not isinstance(user_id, int) or user_id <= 0:
|
||||||
|
print(f"Ошибка валидации: некорректный user_id = {user_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(points, (int, float)) or points < 0:
|
||||||
|
print(f"Ошибка валидации: некорректное количество очков = {points}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверка соединения с базой данных
|
||||||
|
if not self.connection:
|
||||||
|
print("Ошибка: соединение с базой данных не установлено")
|
||||||
|
return False
|
||||||
|
|
||||||
cursor = self.connection.cursor()
|
cursor = self.connection.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE users SET score = score + ?, message_count = message_count + 1, last_message = CURRENT_TIMESTAMP WHERE user_id = ?
|
UPDATE users SET score = score + ?, reputation = reputation + ?, message_count = message_count + 1, last_message = CURRENT_TIMESTAMP WHERE user_id = ?
|
||||||
""", (points, user_id))
|
""", (points, points, user_id))
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
print(f"Предупреждение: пользователь с ID {user_id} не найден для обновления очков")
|
||||||
|
return False
|
||||||
|
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
|
# Обновление ранга на основе репутации (которая теперь синхронизирована с очками)
|
||||||
|
self.update_rank(user_id)
|
||||||
|
return True
|
||||||
except sqlite3.Error as error:
|
except sqlite3.Error as error:
|
||||||
print(TECH_MESSAGES['score_update_error'].format(error=error))
|
print(f"Ошибка базы данных при обновлении очков: {error}")
|
||||||
|
if self.connection:
|
||||||
|
self.connection.rollback()
|
||||||
|
return False
|
||||||
|
except Exception as error:
|
||||||
|
print(f"Неожиданная ошибка при обновлении очков: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
def update_reputation(self, user_id, rep_points=1):
|
def update_reputation(self, user_id, rep_points=1):
|
||||||
"""Обновление репутации пользователя"""
|
"""Обновление репутации пользователя"""
|
||||||
try:
|
try:
|
||||||
|
# Валидация входных данных
|
||||||
|
if not isinstance(user_id, int) or user_id <= 0:
|
||||||
|
print(f"Ошибка валидации: некорректный user_id = {user_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(rep_points, (int, float)):
|
||||||
|
print(f"Ошибка валидации: некорректное количество репутации = {rep_points}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверка соединения с базой данных
|
||||||
|
if not self.connection:
|
||||||
|
print("Ошибка: соединение с базой данных не установлено")
|
||||||
|
return False
|
||||||
|
|
||||||
cursor = self.connection.cursor()
|
cursor = self.connection.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE users SET reputation = reputation + ? WHERE user_id = ?
|
UPDATE users SET reputation = reputation + ? WHERE user_id = ?
|
||||||
""", (rep_points, user_id))
|
""", (rep_points, user_id))
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
print(f"Предупреждение: пользователь с ID {user_id} не найден для обновления репутации")
|
||||||
|
return False
|
||||||
|
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
# Обновление ранга на основе репутации
|
# Обновление ранга на основе репутации (теперь вызывается только здесь)
|
||||||
self.update_rank(user_id)
|
self.update_rank(user_id)
|
||||||
|
return True
|
||||||
except sqlite3.Error as error:
|
except sqlite3.Error as error:
|
||||||
print(TECH_MESSAGES['reputation_update_error'].format(error=error))
|
print(f"Ошибка базы данных при обновлении репутации: {error}")
|
||||||
|
if self.connection:
|
||||||
|
self.connection.rollback()
|
||||||
|
return False
|
||||||
|
except Exception as error:
|
||||||
|
print(f"Неожиданная ошибка при обновлении репутации: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
def update_rank(self, user_id, chat_id=None, first_name=None):
|
def update_rank(self, user_id, chat_id=None, first_name=None):
|
||||||
"""Обновление ранга пользователя на основе репутации"""
|
"""Обновление ранга пользователя на основе репутации"""
|
||||||
@@ -236,34 +290,53 @@ class Database:
|
|||||||
def get_user_info(self, user_id):
|
def get_user_info(self, user_id):
|
||||||
"""Получение информации о пользователе"""
|
"""Получение информации о пользователе"""
|
||||||
try:
|
try:
|
||||||
|
# Валидация входных данных
|
||||||
|
if not isinstance(user_id, int) or user_id <= 0:
|
||||||
|
print(f"Ошибка валидации: некорректный user_id = {user_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Проверка соединения с базой данных
|
||||||
|
if not self.connection:
|
||||||
|
print("Ошибка: соединение с базой данных не установлено")
|
||||||
|
return None
|
||||||
|
|
||||||
cursor = self.connection.cursor()
|
cursor = self.connection.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT user_id, first_name, username, reputation, rank, message_count, active_days, days_since_join, last_message, joined_at, left_at, language, actions, score, warnings, role
|
SELECT user_id, first_name, username, reputation, rank, message_count, active_days, days_since_join, last_message, joined_at, left_at, language, actions, score, warnings, role
|
||||||
FROM users WHERE user_id = ?
|
FROM users WHERE user_id = ?
|
||||||
""", (user_id,))
|
""", (user_id,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
try:
|
||||||
return {
|
return {
|
||||||
'ID': result[0],
|
'ID': result[0],
|
||||||
'Имя': result[1],
|
'Имя': result[1] or 'Неизвестно',
|
||||||
'Имя пользователя': result[2] or 'не указано',
|
'Имя пользователя': result[2] or 'не указано',
|
||||||
'Репутация': result[3],
|
'Репутация': result[3] or 0,
|
||||||
'Ранг': result[4],
|
'Ранг': result[4] or 'Рядовой',
|
||||||
'Количество сообщений': result[5],
|
'Количество сообщений': result[5] or 0,
|
||||||
'Активные дни': result[6],
|
'Активные дни': result[6] or 0,
|
||||||
'Дней с присоединения': result[7],
|
'Дней с присоединения': result[7] or 0,
|
||||||
'Последнее сообщение': result[8],
|
'Последнее сообщение': result[8],
|
||||||
'Присоединился': result[9],
|
'Присоединился': result[9],
|
||||||
'Покинул': result[10],
|
'Покинул': result[10],
|
||||||
'Язык': result[11],
|
'Язык': result[11] or 'ru',
|
||||||
'Действия': result[12],
|
'Действия': result[12] or '[]',
|
||||||
'Очки': result[13],
|
'Очки': result[13] or 0,
|
||||||
'Предупреждений': result[14],
|
'Предупреждений': result[14] or 0,
|
||||||
'Роль': result[15]
|
'Роль': result[15] or 'user'
|
||||||
}
|
}
|
||||||
|
except (IndexError, TypeError) as e:
|
||||||
|
print(f"Ошибка обработки данных пользователя {user_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
except sqlite3.Error as error:
|
except sqlite3.Error as error:
|
||||||
print(TECH_MESSAGES['user_info_error'].format(error=error))
|
print(f"Ошибка базы данных при получении информации о пользователе {user_id}: {error}")
|
||||||
|
return None
|
||||||
|
except Exception as error:
|
||||||
|
print(f"Неожиданная ошибка при получении информации о пользователе {user_id}: {error}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def import_users_from_csv(self, csv_file_path):
|
def import_users_from_csv(self, csv_file_path):
|
||||||
@@ -301,16 +374,16 @@ class Database:
|
|||||||
# Обновляем существующего пользователя
|
# Обновляем существующего пользователя
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET username = ?, first_name = ?, last_name = ?, score = ?, warnings = ?, reputation = ?
|
SET username = ?, first_name = ?, last_name = ?, score = ?, warnings = ?, reputation = ?, rank = ?
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
""", (username, first_name, last_name, xp, rep, rep, user_id))
|
""", (username, first_name, last_name, xp, rep, xp, db.calculate_rank(xp), user_id))
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
else:
|
else:
|
||||||
# Добавляем нового пользователя
|
# Добавляем нового пользователя
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO users (user_id, username, first_name, last_name, score, warnings, reputation)
|
INSERT INTO users (user_id, username, first_name, last_name, score, warnings, reputation, rank)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (user_id, username, first_name, last_name, xp, rep, rep))
|
""", (user_id, username, first_name, last_name, xp, rep, xp, db.calculate_rank(xp)))
|
||||||
imported_count += 1
|
imported_count += 1
|
||||||
|
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
@@ -329,15 +402,49 @@ class Database:
|
|||||||
def add_scheduled_post(self, chat_id, text, schedule_time, created_by, image_path=None):
|
def add_scheduled_post(self, chat_id, text, schedule_time, created_by, image_path=None):
|
||||||
"""Добавление запланированного поста"""
|
"""Добавление запланированного поста"""
|
||||||
try:
|
try:
|
||||||
|
# Валидация входных данных
|
||||||
|
if not isinstance(chat_id, int):
|
||||||
|
print(f"Ошибка валидации: некорректный chat_id = {chat_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not text or not text.strip():
|
||||||
|
print("Ошибка валидации: текст поста пустой")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(created_by, int) or created_by <= 0:
|
||||||
|
print(f"Ошибка валидации: некорректный created_by = {created_by}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Ограничение длины текста
|
||||||
|
if len(text) > 4000:
|
||||||
|
print("Ошибка валидации: текст поста слишком длинный")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Валидация пути к изображению
|
||||||
|
if image_path and not isinstance(image_path, str):
|
||||||
|
print(f"Ошибка валидации: некорректный путь к изображению = {image_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Проверка соединения с базой данных
|
||||||
|
if not self.connection:
|
||||||
|
print("Ошибка: соединение с базой данных не установлено")
|
||||||
|
return None
|
||||||
|
|
||||||
cursor = self.connection.cursor()
|
cursor = self.connection.cursor()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO scheduled_posts (chat_id, text, image_path, schedule_time, created_by)
|
INSERT INTO scheduled_posts (chat_id, text, image_path, schedule_time, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""", (chat_id, text, image_path, schedule_time, created_by))
|
""", (chat_id, text, image_path, schedule_time, created_by))
|
||||||
|
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
return cursor.lastrowid
|
return cursor.lastrowid
|
||||||
except sqlite3.Error as error:
|
except sqlite3.Error as error:
|
||||||
print(f"Ошибка при добавлении поста: {error}")
|
print(f"Ошибка базы данных при добавлении поста: {error}")
|
||||||
|
if self.connection:
|
||||||
|
self.connection.rollback()
|
||||||
|
return None
|
||||||
|
except Exception as error:
|
||||||
|
print(f"Неожиданная ошибка при добавлении поста: {error}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_scheduled_posts(self, chat_id=None, limit=50):
|
def get_scheduled_posts(self, chat_id=None, limit=50):
|
||||||
@@ -451,7 +558,32 @@ class Database:
|
|||||||
def add_donation(self, user_id, amount, currency='RUB'):
|
def add_donation(self, user_id, amount, currency='RUB'):
|
||||||
"""Добавить донат пользователя"""
|
"""Добавить донат пользователя"""
|
||||||
try:
|
try:
|
||||||
|
# Валидация входных данных
|
||||||
|
if not isinstance(user_id, int) or user_id <= 0:
|
||||||
|
print(f"Ошибка валидации: некорректный user_id = {user_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(amount, (int, float)) or amount <= 0:
|
||||||
|
print(f"Ошибка валидации: некорректная сумма доната = {amount}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not isinstance(currency, str) or not currency.strip():
|
||||||
|
print(f"Ошибка валидации: некорректная валюта = {currency}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ограничение максимальной суммы доната для предотвращения злоупотреблений
|
||||||
|
if amount > 1000000: # 1 миллион рублей максимум
|
||||||
|
print(f"Ошибка валидации: сумма доната слишком большая = {amount}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверка соединения с базой данных
|
||||||
|
if not self.connection:
|
||||||
|
print("Ошибка: соединение с базой данных не установлено")
|
||||||
|
return False
|
||||||
|
|
||||||
cursor = self.connection.cursor()
|
cursor = self.connection.cursor()
|
||||||
|
|
||||||
|
# Вставляем запись о донате
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO donations (user_id, amount, currency)
|
INSERT INTO donations (user_id, amount, currency)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
@@ -460,12 +592,27 @@ class Database:
|
|||||||
# Начисляем очки за донат (1 очко за каждые 100 рублей)
|
# Начисляем очки за донат (1 очко за каждые 100 рублей)
|
||||||
points = int(amount // 100)
|
points = int(amount // 100)
|
||||||
if points > 0:
|
if points > 0:
|
||||||
self.update_score(user_id, points)
|
# Обновляем score и reputation одновременно, и вызываем update_rank только один раз
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE users SET score = score + ?, reputation = reputation + ?, last_message = CURRENT_TIMESTAMP WHERE user_id = ?
|
||||||
|
""", (points, points, user_id))
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
print(f"Предупреждение: пользователь с ID {user_id} не найден при начислении очков за донат")
|
||||||
|
self.connection.rollback()
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.update_rank(user_id)
|
||||||
|
|
||||||
self.connection.commit()
|
self.connection.commit()
|
||||||
return True
|
return True
|
||||||
except sqlite3.Error as error:
|
except sqlite3.Error as error:
|
||||||
print(f"Ошибка при добавлении доната: {error}")
|
print(f"Ошибка базы данных при добавлении доната: {error}")
|
||||||
|
if self.connection:
|
||||||
|
self.connection.rollback()
|
||||||
|
return False
|
||||||
|
except Exception as error:
|
||||||
|
print(f"Неожиданная ошибка при добавлении доната: {error}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_total_donations(self, user_id, year=None):
|
def get_total_donations(self, user_id, year=None):
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Тест системы рангов
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем текущую директорию в путь для импорта
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from database_sqlite import Database
|
||||||
|
from messages import RANK_THRESHOLDS
|
||||||
|
|
||||||
|
def test_rank_calculation():
|
||||||
|
"""Тестирование расчета рангов"""
|
||||||
|
db = Database()
|
||||||
|
|
||||||
|
print("Testing rank system...\n")
|
||||||
|
|
||||||
|
# Тестовые значения репутации и ожидаемые ранги
|
||||||
|
test_cases = [
|
||||||
|
(0, "Рядовой"),
|
||||||
|
(50, "Рядовой"),
|
||||||
|
(100, "Ефрейтор"),
|
||||||
|
(200, "Ефрейтор"),
|
||||||
|
(235, "Младший сержант"),
|
||||||
|
(400, "Младший сержант"),
|
||||||
|
(505, "Сержант"),
|
||||||
|
(700, "Сержант"),
|
||||||
|
(810, "Старший сержант"),
|
||||||
|
(1000, "Старший сержант"),
|
||||||
|
(1250, "Старшина"),
|
||||||
|
(1500, "Старшина"),
|
||||||
|
(1725, "Прапорщик"),
|
||||||
|
(2000, "Прапорщик"),
|
||||||
|
(2335, "Старший прапорщик"),
|
||||||
|
(2500, "Старший прапорщик"),
|
||||||
|
(2980, "Младший лейтенант"),
|
||||||
|
(3500, "Младший лейтенант"),
|
||||||
|
(3760, "Лейтенант"),
|
||||||
|
(4000, "Лейтенант"),
|
||||||
|
(4575, "Старший лейтенант"),
|
||||||
|
(5000, "Старший лейтенант"),
|
||||||
|
(5525, "Капитан"),
|
||||||
|
(6000, "Капитан"),
|
||||||
|
(6112, "Капитан"), # Это значение из примера в TODO
|
||||||
|
(6510, "Майор"),
|
||||||
|
(7000, "Майор"),
|
||||||
|
(256000, "Маршал"), # Максимальный ранг
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Rank thresholds:")
|
||||||
|
for threshold, rank_name in RANK_THRESHOLDS:
|
||||||
|
print(f" {threshold:6} points -> {rank_name}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("Test results:")
|
||||||
|
all_passed = True
|
||||||
|
|
||||||
|
for reputation, expected_rank in test_cases:
|
||||||
|
calculated_rank = db.calculate_rank(reputation)
|
||||||
|
status = "PASS" if calculated_rank == expected_rank else "FAIL"
|
||||||
|
if calculated_rank != expected_rank:
|
||||||
|
all_passed = False
|
||||||
|
print(f" {status} {reputation:6} points -> {calculated_rank}"+" "*(20-len(calculated_rank)) + f"(expected: {expected_rank})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
if all_passed:
|
||||||
|
print("All tests passed successfully!")
|
||||||
|
else:
|
||||||
|
print("Some tests failed!")
|
||||||
|
|
||||||
|
# Проверка конкретного примера из TODO (6112 очков должны давать Капитана)
|
||||||
|
print(f"\nChecking TODO example:")
|
||||||
|
print(f" 6112 points -> {db.calculate_rank(6112)} (should be Капитан)")
|
||||||
|
|
||||||
|
db.close()
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_rank_calculation()
|
||||||
Reference in New Issue
Block a user