Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5c103aca1 | ||
|
|
4fd0ade03c | ||
|
|
4b97170abd | ||
|
|
ee1d8793c6 | ||
|
|
7cf97e6a23 | ||
|
|
deab385de5 | ||
|
|
3526990573 | ||
|
|
20433059a0 | ||
|
|
cfc52247a0 | ||
|
|
b38c24f775 | ||
|
|
50d3e894bc | ||
|
|
f75b37b56e | ||
|
|
fe60efaae5 | ||
|
|
6a7df9bc55 | ||
|
|
0bd399f121 | ||
|
|
c257c6c1a2 | ||
|
|
c7b2961ae1 | ||
|
|
8af2f128a7 | ||
|
|
4aba68d242 | ||
|
|
8bf512e509 | ||
|
|
459ed66e9a | ||
|
|
de1c82c267 | ||
|
|
4a2aa00eb7 | ||
|
|
c4400fc244 | ||
|
|
b7f09ae719 | ||
|
|
1619e82df1 | ||
|
|
0ee7cb3bd4 | ||
|
|
9b11f21bc1 | ||
|
|
2b9e819944 | ||
|
|
58daea0492 | ||
|
|
63ac924a3d | ||
|
|
be64915e9b | ||
|
|
61e9d31a75 | ||
|
|
6bdf996ca4 | ||
|
|
c07a082694 | ||
|
|
6b41e61d7e | ||
|
|
1595acb4bb | ||
|
|
44a8b54ddc | ||
|
|
50d137ffc8 | ||
|
|
1e43cb6c98 | ||
|
|
15ac2fdb07 | ||
|
|
a16b5322af | ||
|
|
47cf59f710 | ||
|
|
2709c8ce40 | ||
|
|
ab80af2744 | ||
|
|
383abb0235 | ||
|
|
295866ed2d | ||
|
|
ff397dc496 | ||
|
|
e0886fcfa8 | ||
|
|
313389d2bf | ||
|
|
7cdb7086a3 | ||
|
|
98d171dcf8 | ||
|
|
d8385d73f2 | ||
|
|
0426ae59cc | ||
|
|
69d55a68b9 | ||
|
|
ab10879c06 | ||
|
|
1d32ec94e5 | ||
|
|
7ce23f8142 | ||
|
|
96da8266e9 | ||
|
|
80d1a3994c | ||
|
|
f6b888c6d3 | ||
|
|
643ad9feda | ||
|
|
f7b3866b6a | ||
|
|
ebe2b2c0fd |
No files matched your search
+1
-2
@@ -1,4 +1,3 @@
|
||||
BOT_TOKEN = "..." # Токен бота получать у @BotFather
|
||||
ADMIN_CHAT_ID = -1001111111111 # ID админ-чата получать у @username_to_id_bot
|
||||
LOG_THREAD_ID = 2 # ID топика, брать из ссылки сообщения
|
||||
ADMIN_IDS = "11111,22222" # ID администраторов получать у @username_to_id_bot
|
||||
LOG_THREAD_ID = 2 # ID топика, брать из ссылки сообщения
|
||||
+32
-1
@@ -1,6 +1,37 @@
|
||||
# Python
|
||||
.venv/
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.gigaide/
|
||||
.claude/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
# Build
|
||||
target/
|
||||
build/
|
||||
dist/
|
||||
*.class
|
||||
|
||||
# Logs
|
||||
bot.log
|
||||
users.db
|
||||
*.log
|
||||
|
||||
# Databases
|
||||
users.db
|
||||
|
||||
# Custom moderator wordlist (generated at runtime)
|
||||
src/data/bad_words_custom.json
|
||||
data/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -26,4 +26,183 @@ pip install -r requirements.txt
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
> Используется Python 3.11.0
|
||||
> Используется Python 3.11.0
|
||||
|
||||
### Обновление на сервере
|
||||
|
||||
Для обновления бота на продакшн-сервере используйте скрипт `update.sh`:
|
||||
|
||||
```sh
|
||||
./update.sh
|
||||
```
|
||||
|
||||
Скрипт автоматически:
|
||||
- Проверит наличие обновлений в git-репозитории
|
||||
- Загрузит изменения (`git pull`)
|
||||
- Перезапустит службу бота (`systemctl restart LGBot.service`)
|
||||
- Покажет статус работы бота
|
||||
|
||||
---
|
||||
|
||||
## Система автоматического мьюта
|
||||
|
||||
Бот автоматически отслеживает использование нецензурной лексики и применяет прогрессирующие наказания.
|
||||
|
||||
### Как работает
|
||||
|
||||
1. **Обнаружение нарушений:**
|
||||
- Каждое текстовое сообщение проверяется на наличие бранных слов
|
||||
- Проверяются только группы и супергруппы (не личные сообщения)
|
||||
- Команды (начинающиеся с `/`) не проверяются
|
||||
|
||||
2. **При обнаружении мата:**
|
||||
- Сообщение удаляется автоматически
|
||||
- Нарушение фиксируется в базе данных
|
||||
- Пользователь получает мут соответствующего уровня
|
||||
- Уведомление отправляется в чат и админ-чат
|
||||
|
||||
3. **Прогрессирующие наказания:**
|
||||
|
||||
| № нарушения | Длительность мута | № нарушения | Длительность мута |
|
||||
|-------------|-------------------|-------------|-------------------|
|
||||
| 1 | 5 минут | 9 | 1 день |
|
||||
| 2 | 15 минут | 10 | 2 дня |
|
||||
| 3 | 30 минут | 11 | 3 дня |
|
||||
| 4 | 1 час | 12 | 5 дней |
|
||||
| 5 | 2 часа | 13 | 7 дней |
|
||||
| 8 | 12 часов | 16+ | **НАВСЕГДА** |
|
||||
|
||||
4. **Накопительный эффект:**
|
||||
- Нарушения учитываются за последние **30 дней**
|
||||
- Старые нарушения автоматически удаляются из базы
|
||||
- Администраторы **освобождены** от автоматических мутов
|
||||
|
||||
### Управление списком бранных слов
|
||||
|
||||
⚠️ **Все команды доступны только администраторам чата**
|
||||
|
||||
#### Основные команды
|
||||
|
||||
```bash
|
||||
# Показать справку
|
||||
/badwords help
|
||||
|
||||
# Показать список бранных слов (первые 50)
|
||||
/badwords list
|
||||
|
||||
# Статистика
|
||||
/badwords count
|
||||
|
||||
# Добавить слово в список
|
||||
/badwords add <слово>
|
||||
|
||||
# Удалить слово из списка
|
||||
/badwords remove <слово>
|
||||
```
|
||||
|
||||
#### Исключения
|
||||
|
||||
Исключения — слова, содержащие бранные корни, но не являющиеся матом (например: "республика", "документ"):
|
||||
|
||||
```bash
|
||||
# Показать исключения
|
||||
/badwords exceptions
|
||||
|
||||
# Добавить исключение
|
||||
/badwords add_exception <слово>
|
||||
|
||||
# Удалить исключение
|
||||
/badwords remove_exception <слово>
|
||||
```
|
||||
|
||||
#### Служебные команды
|
||||
|
||||
```bash
|
||||
# Перезагрузить списки из файла
|
||||
/badwords reload
|
||||
```
|
||||
|
||||
### Рекомендации
|
||||
|
||||
1. **Используйте корни слов**, а не полные формы:
|
||||
- ✅ Правильно: `ебал` (поймает все формы)
|
||||
- ❌ Неправильно: `ебала` (пропустит другие формы)
|
||||
|
||||
2. **Избегайте коротких корней** (могут давать ложные срабатывания)
|
||||
|
||||
3. **Тестируйте после добавления** нового слова
|
||||
|
||||
### Хранение данных
|
||||
|
||||
Все списки хранятся в файле `src/data/bad_words.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"bad_words": ["слово1", "слово2", ...],
|
||||
"exceptions": ["исключение1", "исключение2", ...]
|
||||
}
|
||||
```
|
||||
|
||||
Изменения через команды применяются **немедленно**, перезапуск бота не требуется.
|
||||
|
||||
### Логирование
|
||||
|
||||
Все действия записываются в:
|
||||
- **bot.log** - файл логов
|
||||
- **Админ-чат** - уведомления о мутах
|
||||
|
||||
Примеры логов:
|
||||
```
|
||||
[INFO] Пользователь 123456789 получил автоматический мут на 5 минут за нецензурную лексику (нарушение #1)
|
||||
[INFO] Нарушение пользователя 123456789 зафиксировано в чате -100123456789
|
||||
[INFO] Администратор 987654321 добавил бранное слово: спам
|
||||
```
|
||||
|
||||
### База данных
|
||||
|
||||
Таблица `violations` хранит все нарушения:
|
||||
- `id` - уникальный идентификатор
|
||||
- `user_id` - ID пользователя
|
||||
- `chat_id` - ID чата
|
||||
- `violation_date` - время нарушения (unix timestamp)
|
||||
- `violation_type` - тип нарушения ('bad_language')
|
||||
|
||||
### Настройка системы
|
||||
|
||||
#### Изменение уровней мутов
|
||||
|
||||
Откройте `src/modules/auto_mute.py` и измените массив `MUTE_LEVELS`:
|
||||
|
||||
```python
|
||||
MUTE_LEVELS = [
|
||||
300, # 1. 5 минут
|
||||
900, # 2. 15 минут
|
||||
# ... добавьте или измените уровни
|
||||
None, # Перманентный мут
|
||||
]
|
||||
```
|
||||
|
||||
#### Изменение периода накопления
|
||||
|
||||
Измените `VIOLATIONS_PERIOD` в `src/modules/auto_mute.py`:
|
||||
|
||||
```python
|
||||
VIOLATIONS_PERIOD = 2592000 # 30 дней в секундах
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Python 3.7+
|
||||
- pyTelegramBotAPI (telebot)
|
||||
- SQLite3
|
||||
- Права бота в чате:
|
||||
- Удаление сообщений
|
||||
- Ограничение пользователей
|
||||
|
||||
### Устранение проблем
|
||||
|
||||
При возникновении проблем проверьте:
|
||||
1. Логи бота в файле `bot.log`
|
||||
2. Права бота в чате (удаление сообщений, ограничение пользователей)
|
||||
3. Корректность настроек в `.env`
|
||||
4. Наличие файла `src/data/bad_words.json`
|
||||
@@ -5,7 +5,6 @@ asyncio==3.4.3
|
||||
attrs==25.3.0
|
||||
certifi==2025.6.15
|
||||
charset-normalizer==3.4.2
|
||||
dotenv==0.9.9
|
||||
frozenlist==1.7.0
|
||||
idna==3.10
|
||||
multidict==6.6.3
|
||||
|
||||
+145
-15
@@ -1,5 +1,6 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message
|
||||
from typing import Optional
|
||||
import logging
|
||||
import os
|
||||
from database import db
|
||||
@@ -26,11 +27,18 @@ class ActionReporter:
|
||||
if tag:
|
||||
text += f"• Tag: <code>@{tag}</code>\n"
|
||||
text += f"• ID: <code>{user_id}</code>"
|
||||
else:
|
||||
# Пользователь не найден в БД
|
||||
text = f"👤 <b>Пользователь:</b>\n• ID: <code>{user_id}</code>"
|
||||
|
||||
return text
|
||||
|
||||
# Получает информацию об администраторе
|
||||
async def _get_admin_info(self, admin_id: int) -> str:
|
||||
async def _get_admin_info(self, admin_id: Optional[int]) -> str:
|
||||
# Если админ не указан (автоматическое действие)
|
||||
if admin_id is None:
|
||||
return "🤖 <b>Администратор:</b> Автоматическое действие"
|
||||
|
||||
admin_info = db.get_user(admin_id)
|
||||
if admin_info:
|
||||
|
||||
@@ -44,37 +52,159 @@ class ActionReporter:
|
||||
if tag:
|
||||
text += f"• Tag: <code>@{tag}</code>\n"
|
||||
text += f"• ID: <code>{admin_id}</code>"
|
||||
else:
|
||||
# Администратор не найден в БД
|
||||
text = f"🛡 <b>Администратор:</b>\n• ID: <code>{admin_id}</code>"
|
||||
|
||||
return text
|
||||
|
||||
def _format_source_text(self, source_message: Optional[Message], max_length: int = 400) -> str:
|
||||
"""Форматирует текст исходного сообщения для лога админ-чата."""
|
||||
if not source_message:
|
||||
return ""
|
||||
|
||||
text = source_message.text or source_message.caption
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
# Экранируем HTML
|
||||
text = text.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
# Обрезаем длинный текст
|
||||
if len(text) > max_length:
|
||||
text = text[:max_length].rstrip() + "…"
|
||||
|
||||
return f"\n\n📝 <b>Сообщение:</b>\n<blockquote>{text}</blockquote>"
|
||||
|
||||
# Отправляет лог действия в админ-чат
|
||||
async def log_action(self, action: str, user_id: int, admin_id: int, reason: str, duration: str, photo_path: str):
|
||||
async def log_action(self, action: str, user_id: Optional[int], admin_id: Optional[int], reason: str, duration: str, photo_path: Optional[str] = None, source_message: Optional[Message] = None):
|
||||
try:
|
||||
|
||||
# Получаем информацию о пользователе и администраторе
|
||||
user_info = await self._get_user_info(user_id)
|
||||
admin_info = await self._get_admin_info(admin_id)
|
||||
|
||||
|
||||
# Формируем сообщение
|
||||
msg = f"⚙️ <b>Действие:</b> {action}\n"
|
||||
|
||||
|
||||
if duration:
|
||||
msg += f"⏱ <b>Длительность:</b> {duration}\n"
|
||||
|
||||
|
||||
if reason:
|
||||
msg += f"📝 <b>Причина:</b> <i>{reason}</i>\n"
|
||||
|
||||
msg += f"\n{user_info}\n\n{admin_info}"
|
||||
|
||||
# Отправляем лог с изображением
|
||||
if photo_path and os.path.exists(photo_path):
|
||||
with open(photo_path, 'rb') as photo_file:
|
||||
await self.bot.send_photo(self.log_chat_id, photo_file, caption=msg, message_thread_id=self.log_thread_id)
|
||||
# Определяем, есть ли медиа в исходном сообщении
|
||||
has_caption_media = (
|
||||
source_message
|
||||
and any([
|
||||
source_message.photo,
|
||||
source_message.video,
|
||||
source_message.animation,
|
||||
source_message.document,
|
||||
source_message.voice,
|
||||
])
|
||||
)
|
||||
|
||||
# Отправляем лог без изображения
|
||||
# Добавляем текст исходного сообщения
|
||||
# Для медиа с caption ограничиваем длину, чтобы уложиться в 1024 символа
|
||||
source_max_length = 250 if has_caption_media else 600
|
||||
source_text = self._format_source_text(source_message, max_length=source_max_length)
|
||||
if source_text:
|
||||
msg += source_text
|
||||
|
||||
# Добавляем информацию о пользователе, если указан
|
||||
if user_id is not None:
|
||||
user_info = await self._get_user_info(user_id)
|
||||
msg += f"\n\n{user_info}\n\n{admin_info}"
|
||||
else:
|
||||
await self.bot.send_message(self.log_chat_id, msg, message_thread_id=self.log_thread_id)
|
||||
|
||||
msg += f"\n\n{admin_info}"
|
||||
|
||||
# Отправляем медиа из исходного сообщения
|
||||
media_sent = False
|
||||
|
||||
if source_message:
|
||||
# Проверяем различные типы медиа
|
||||
if source_message.photo:
|
||||
# Берём самое большое фото (последнее в массиве)
|
||||
photo = source_message.photo[-1]
|
||||
await self.bot.send_photo(
|
||||
self.log_chat_id,
|
||||
photo.file_id,
|
||||
caption=msg,
|
||||
message_thread_id=self.log_thread_id,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
media_sent = True
|
||||
|
||||
elif source_message.video:
|
||||
await self.bot.send_video(
|
||||
self.log_chat_id,
|
||||
source_message.video.file_id,
|
||||
caption=msg,
|
||||
message_thread_id=self.log_thread_id,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
media_sent = True
|
||||
|
||||
elif source_message.animation:
|
||||
await self.bot.send_animation(
|
||||
self.log_chat_id,
|
||||
source_message.animation.file_id,
|
||||
caption=msg,
|
||||
message_thread_id=self.log_thread_id,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
media_sent = True
|
||||
|
||||
elif source_message.document:
|
||||
await self.bot.send_document(
|
||||
self.log_chat_id,
|
||||
source_message.document.file_id,
|
||||
caption=msg,
|
||||
message_thread_id=self.log_thread_id,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
media_sent = True
|
||||
|
||||
elif source_message.voice:
|
||||
await self.bot.send_voice(
|
||||
self.log_chat_id,
|
||||
source_message.voice.file_id,
|
||||
caption=msg,
|
||||
message_thread_id=self.log_thread_id,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
media_sent = True
|
||||
|
||||
elif source_message.video_note:
|
||||
# video_note не поддерживает caption, отправляем отдельно
|
||||
await self.bot.send_message(self.log_chat_id, msg, message_thread_id=self.log_thread_id, parse_mode='HTML')
|
||||
await self.bot.send_video_note(
|
||||
self.log_chat_id,
|
||||
source_message.video_note.file_id,
|
||||
message_thread_id=self.log_thread_id
|
||||
)
|
||||
media_sent = True
|
||||
|
||||
elif source_message.sticker:
|
||||
# sticker не поддерживает caption, отправляем отдельно
|
||||
await self.bot.send_message(self.log_chat_id, msg, message_thread_id=self.log_thread_id, parse_mode='HTML')
|
||||
await self.bot.send_sticker(
|
||||
self.log_chat_id,
|
||||
source_message.sticker.file_id,
|
||||
message_thread_id=self.log_thread_id
|
||||
)
|
||||
media_sent = True
|
||||
|
||||
# Отправляем лог с изображением из файла (старый способ)
|
||||
if not media_sent and photo_path and os.path.exists(photo_path):
|
||||
with open(photo_path, 'rb') as photo_file:
|
||||
await self.bot.send_photo(self.log_chat_id, photo_file, caption=msg, message_thread_id=self.log_thread_id, parse_mode='HTML')
|
||||
media_sent = True
|
||||
|
||||
# Отправляем лог без медиа
|
||||
if not media_sent:
|
||||
await self.bot.send_message(self.log_chat_id, msg, message_thread_id=self.log_thread_id, parse_mode='HTML')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка отправки лога: {str(e)}")
|
||||
|
||||
|
||||
@@ -0,0 +1,765 @@
|
||||
# Система управления бранными словами
|
||||
# Список слов хранится в JSON файле для возможности управления через команды
|
||||
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Базовый словарь (в индексе git)
|
||||
BASE_BAD_WORDS_FILE = os.path.join(os.path.dirname(__file__), 'data', 'bad_words.json')
|
||||
|
||||
# Кастомный словарь модераторов (не в индексе git)
|
||||
CUSTOM_BAD_WORDS_FILE = os.path.join(os.path.dirname(__file__), 'data', 'bad_words_custom.json')
|
||||
|
||||
# Минимальная длина паттерна для проверки "contains" (любое вхождение внутри слова)
|
||||
# Короткие паттерны проверяются как "word_start" (начало слова), чтобы снизить ложные срабатывания
|
||||
MIN_CONTAINS_LENGTH = 4
|
||||
|
||||
# Кэш базового словаря
|
||||
_base_whole_word_patterns_cache = None
|
||||
_base_word_start_patterns_cache = None
|
||||
_base_contains_patterns_cache = None
|
||||
_base_en_patterns_cache = None
|
||||
_base_exceptions_cache = None
|
||||
|
||||
# Кэш кастомного словаря
|
||||
_custom_whole_word_patterns_cache = None
|
||||
_custom_word_start_patterns_cache = None
|
||||
_custom_contains_patterns_cache = None
|
||||
_custom_en_patterns_cache = None
|
||||
_custom_exceptions_cache = None
|
||||
|
||||
# Кэш удалённых из базового словаря записей
|
||||
_removed_whole_word_patterns_cache = None
|
||||
_removed_word_start_patterns_cache = None
|
||||
_removed_contains_patterns_cache = None
|
||||
_removed_en_patterns_cache = None
|
||||
_removed_exceptions_cache = None
|
||||
|
||||
# Итоговый объединённый кэш
|
||||
_bad_words_cache = None
|
||||
_exceptions_cache = None
|
||||
_en_patterns_cache = None
|
||||
_whole_word_patterns_cache = None
|
||||
_word_start_patterns_cache = None
|
||||
_contains_patterns_cache = None
|
||||
|
||||
|
||||
def _load_json_file(file_path: str, default=None):
|
||||
"""Загружает JSON файл, возвращает default при ошибках."""
|
||||
if default is None:
|
||||
default = {}
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
return default
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Ошибка чтения JSON {file_path}: {e}")
|
||||
return default
|
||||
|
||||
|
||||
def _load_patterns(data: dict) -> tuple:
|
||||
"""Извлекает паттерны из данных в новом или старом формате."""
|
||||
if 'patterns' in data:
|
||||
patterns = data['patterns']
|
||||
return (
|
||||
patterns.get('whole_word', []),
|
||||
patterns.get('word_start', []),
|
||||
patterns.get('contains', []),
|
||||
patterns.get('en', []),
|
||||
data.get('exceptions', []),
|
||||
)
|
||||
|
||||
# Старый формат: плоский список bad_words
|
||||
bad_words = data.get('bad_words', [])
|
||||
exceptions = data.get('exceptions', [])
|
||||
whole_word = []
|
||||
word_start = [w for w in bad_words if len(w) < MIN_CONTAINS_LENGTH]
|
||||
contains = [w for w in bad_words if len(w) >= MIN_CONTAINS_LENGTH]
|
||||
en = []
|
||||
return whole_word, word_start, contains, en, exceptions
|
||||
|
||||
|
||||
def _categorize_word(word: str) -> str:
|
||||
"""Определяет категорию паттерна по эвристикам."""
|
||||
word = word.lower().strip()
|
||||
if not word:
|
||||
return None
|
||||
|
||||
# Только латиница -> английский паттерн
|
||||
if re.fullmatch(r'[a-z0-9*\.\-_]+', word):
|
||||
return 'en'
|
||||
|
||||
# Короткие русские слова обычно являются началом слова
|
||||
if len(word) < MIN_CONTAINS_LENGTH:
|
||||
return 'word_start'
|
||||
|
||||
return 'contains'
|
||||
|
||||
|
||||
def _merge_pattern_lists(base_list: list, custom_list: list, removed_list: list) -> list:
|
||||
"""Объединяет базовый и кастомный списки, исключая удалённые."""
|
||||
removed_set = set(removed_list or [])
|
||||
result = set(custom_list or []) | (set(base_list or []) - removed_set)
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def load_bad_words():
|
||||
"""
|
||||
Загружает базовый и кастомный словари, объединяет их.
|
||||
|
||||
Returns:
|
||||
tuple: (список бранных слов, список исключений)
|
||||
"""
|
||||
global _base_whole_word_patterns_cache, _base_word_start_patterns_cache, _base_contains_patterns_cache, _base_en_patterns_cache, _base_exceptions_cache, _custom_whole_word_patterns_cache, _custom_word_start_patterns_cache, _custom_contains_patterns_cache, _custom_en_patterns_cache, _custom_exceptions_cache, _removed_whole_word_patterns_cache, _removed_word_start_patterns_cache, _removed_contains_patterns_cache, _removed_en_patterns_cache, _removed_exceptions_cache, _whole_word_patterns_cache, _word_start_patterns_cache, _contains_patterns_cache, _en_patterns_cache, _exceptions_cache, _bad_words_cache
|
||||
|
||||
# Загружаем базовый словарь
|
||||
base_data = _load_json_file(BASE_BAD_WORDS_FILE)
|
||||
(
|
||||
_base_whole_word_patterns_cache,
|
||||
_base_word_start_patterns_cache,
|
||||
_base_contains_patterns_cache,
|
||||
_base_en_patterns_cache,
|
||||
_base_exceptions_cache,
|
||||
) = _load_patterns(base_data)
|
||||
|
||||
# Загружаем кастомный словарь модераторов
|
||||
custom_data = _load_json_file(CUSTOM_BAD_WORDS_FILE, {
|
||||
'patterns': {'whole_word': [], 'word_start': [], 'contains': [], 'en': []},
|
||||
'exceptions': [],
|
||||
'removed': {'patterns': {'whole_word': [], 'word_start': [], 'contains': [], 'en': []}, 'exceptions': []},
|
||||
})
|
||||
|
||||
(
|
||||
_custom_whole_word_patterns_cache,
|
||||
_custom_word_start_patterns_cache,
|
||||
_custom_contains_patterns_cache,
|
||||
_custom_en_patterns_cache,
|
||||
_custom_exceptions_cache,
|
||||
) = _load_patterns(custom_data)
|
||||
|
||||
removed_data = custom_data.get('removed', {})
|
||||
(
|
||||
_removed_whole_word_patterns_cache,
|
||||
_removed_word_start_patterns_cache,
|
||||
_removed_contains_patterns_cache,
|
||||
_removed_en_patterns_cache,
|
||||
_removed_exceptions_cache,
|
||||
) = _load_patterns(removed_data)
|
||||
|
||||
# Объединяем
|
||||
_whole_word_patterns_cache = _merge_pattern_lists(
|
||||
_base_whole_word_patterns_cache,
|
||||
_custom_whole_word_patterns_cache,
|
||||
_removed_whole_word_patterns_cache,
|
||||
)
|
||||
_word_start_patterns_cache = _merge_pattern_lists(
|
||||
_base_word_start_patterns_cache,
|
||||
_custom_word_start_patterns_cache,
|
||||
_removed_word_start_patterns_cache,
|
||||
)
|
||||
_contains_patterns_cache = _merge_pattern_lists(
|
||||
_base_contains_patterns_cache,
|
||||
_custom_contains_patterns_cache,
|
||||
_removed_contains_patterns_cache,
|
||||
)
|
||||
_en_patterns_cache = _merge_pattern_lists(
|
||||
_base_en_patterns_cache,
|
||||
_custom_en_patterns_cache,
|
||||
_removed_en_patterns_cache,
|
||||
)
|
||||
_exceptions_cache = _merge_pattern_lists(
|
||||
_base_exceptions_cache,
|
||||
_custom_exceptions_cache,
|
||||
_removed_exceptions_cache,
|
||||
)
|
||||
|
||||
_bad_words_cache = (
|
||||
_whole_word_patterns_cache
|
||||
+ _word_start_patterns_cache
|
||||
+ _contains_patterns_cache
|
||||
+ _en_patterns_cache
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Загружено паттернов: "
|
||||
f"{len(_whole_word_patterns_cache)} whole_word, "
|
||||
f"{len(_word_start_patterns_cache)} word_start, "
|
||||
f"{len(_contains_patterns_cache)} contains, "
|
||||
f"{len(_en_patterns_cache)} en, "
|
||||
f"{len(_exceptions_cache)} исключений "
|
||||
f"(база + кастом)"
|
||||
)
|
||||
|
||||
return _bad_words_cache, _exceptions_cache
|
||||
|
||||
|
||||
def _flatten_patterns(
|
||||
whole_word: list, word_start: list, contains: list, en: list
|
||||
) -> set:
|
||||
"""Объединяет все категории паттернов в одно множество."""
|
||||
return set(whole_word or []) | set(word_start or []) | set(contains or []) | set(en or [])
|
||||
|
||||
|
||||
def _categorize_added_words(
|
||||
added_words: set,
|
||||
base_whole: set, base_start: set, base_contains: set, base_en: set,
|
||||
) -> tuple:
|
||||
"""Распределяет добавленные слова по категориям с сохранением базовой разметки."""
|
||||
whole_word = []
|
||||
word_start = []
|
||||
contains = []
|
||||
en = []
|
||||
|
||||
for word in sorted(added_words):
|
||||
if word in base_whole:
|
||||
whole_word.append(word)
|
||||
elif word in base_start:
|
||||
word_start.append(word)
|
||||
elif word in base_contains:
|
||||
contains.append(word)
|
||||
elif word in base_en:
|
||||
en.append(word)
|
||||
else:
|
||||
category = _categorize_word(word)
|
||||
if category == 'whole_word':
|
||||
whole_word.append(word)
|
||||
elif category == 'word_start':
|
||||
word_start.append(word)
|
||||
elif category == 'en':
|
||||
en.append(word)
|
||||
else:
|
||||
contains.append(word)
|
||||
|
||||
return whole_word, word_start, contains, en
|
||||
|
||||
|
||||
def save_bad_words(bad_words: list, exceptions: list):
|
||||
"""
|
||||
Сохраняет изменения, сделанные модераторами, в кастомный файл.
|
||||
Базовый словарь при этом не изменяется.
|
||||
|
||||
Args:
|
||||
bad_words: Текущий полный список бранных слов
|
||||
exceptions: Текущий полный список исключений
|
||||
|
||||
Returns:
|
||||
bool: True если успешно, иначе False
|
||||
"""
|
||||
global _base_whole_word_patterns_cache, _base_word_start_patterns_cache, _base_contains_patterns_cache, _base_en_patterns_cache, _base_exceptions_cache, _custom_whole_word_patterns_cache, _custom_word_start_patterns_cache, _custom_contains_patterns_cache, _custom_en_patterns_cache, _custom_exceptions_cache, _removed_whole_word_patterns_cache, _removed_word_start_patterns_cache, _removed_contains_patterns_cache, _removed_en_patterns_cache, _removed_exceptions_cache, _whole_word_patterns_cache, _word_start_patterns_cache, _contains_patterns_cache, _en_patterns_cache, _exceptions_cache, _bad_words_cache
|
||||
|
||||
try:
|
||||
# Убедимся, что базовый словарь загружен
|
||||
if _base_whole_word_patterns_cache is None:
|
||||
load_bad_words()
|
||||
|
||||
# Создаем директорию, если её нет
|
||||
os.makedirs(os.path.dirname(CUSTOM_BAD_WORDS_FILE), exist_ok=True)
|
||||
|
||||
# Нормализуем входные данные
|
||||
current_words = set(
|
||||
w.lower().strip() for w in bad_words if w and w.strip()
|
||||
)
|
||||
current_exceptions = set(
|
||||
e.lower().strip() for e in exceptions if e and e.strip()
|
||||
)
|
||||
|
||||
# Базовые множества
|
||||
base_words = _flatten_patterns(
|
||||
_base_whole_word_patterns_cache,
|
||||
_base_word_start_patterns_cache,
|
||||
_base_contains_patterns_cache,
|
||||
_base_en_patterns_cache,
|
||||
)
|
||||
base_exceptions = set(_base_exceptions_cache or [])
|
||||
|
||||
# Вычисляем разницу
|
||||
removed_words = base_words - current_words
|
||||
added_words = current_words - base_words
|
||||
removed_exceptions = base_exceptions - current_exceptions
|
||||
added_exceptions = current_exceptions - base_exceptions
|
||||
|
||||
# Категоризируем добавленные слова
|
||||
added_whole, added_start, added_contains, added_en = _categorize_added_words(
|
||||
added_words,
|
||||
set(_base_whole_word_patterns_cache or []),
|
||||
set(_base_word_start_patterns_cache or []),
|
||||
set(_base_contains_patterns_cache or []),
|
||||
set(_base_en_patterns_cache or []),
|
||||
)
|
||||
|
||||
# Категоризируем удалённые слова по базовым категориям
|
||||
removed_whole = sorted(removed_words & set(_base_whole_word_patterns_cache or []))
|
||||
removed_start = sorted(removed_words & set(_base_word_start_patterns_cache or []))
|
||||
removed_contains = sorted(removed_words & set(_base_contains_patterns_cache or []))
|
||||
removed_en = sorted(removed_words & set(_base_en_patterns_cache or []))
|
||||
|
||||
custom_data = {
|
||||
'patterns': {
|
||||
'whole_word': added_whole,
|
||||
'word_start': added_start,
|
||||
'contains': added_contains,
|
||||
'en': added_en,
|
||||
},
|
||||
'exceptions': sorted(added_exceptions),
|
||||
'removed': {
|
||||
'patterns': {
|
||||
'whole_word': removed_whole,
|
||||
'word_start': removed_start,
|
||||
'contains': removed_contains,
|
||||
'en': removed_en,
|
||||
},
|
||||
'exceptions': sorted(removed_exceptions),
|
||||
},
|
||||
}
|
||||
|
||||
with open(CUSTOM_BAD_WORDS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(custom_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# Обновляем кастомный кэш
|
||||
(
|
||||
_custom_whole_word_patterns_cache,
|
||||
_custom_word_start_patterns_cache,
|
||||
_custom_contains_patterns_cache,
|
||||
_custom_en_patterns_cache,
|
||||
_custom_exceptions_cache,
|
||||
) = _load_patterns(custom_data)
|
||||
|
||||
removed_data = custom_data.get('removed', {})
|
||||
(
|
||||
_removed_whole_word_patterns_cache,
|
||||
_removed_word_start_patterns_cache,
|
||||
_removed_contains_patterns_cache,
|
||||
_removed_en_patterns_cache,
|
||||
_removed_exceptions_cache,
|
||||
) = _load_patterns(removed_data)
|
||||
|
||||
# Пересчитываем итоговый кэш
|
||||
_whole_word_patterns_cache = _merge_pattern_lists(
|
||||
_base_whole_word_patterns_cache,
|
||||
_custom_whole_word_patterns_cache,
|
||||
_removed_whole_word_patterns_cache,
|
||||
)
|
||||
_word_start_patterns_cache = _merge_pattern_lists(
|
||||
_base_word_start_patterns_cache,
|
||||
_custom_word_start_patterns_cache,
|
||||
_removed_word_start_patterns_cache,
|
||||
)
|
||||
_contains_patterns_cache = _merge_pattern_lists(
|
||||
_base_contains_patterns_cache,
|
||||
_custom_contains_patterns_cache,
|
||||
_removed_contains_patterns_cache,
|
||||
)
|
||||
_en_patterns_cache = _merge_pattern_lists(
|
||||
_base_en_patterns_cache,
|
||||
_custom_en_patterns_cache,
|
||||
_removed_en_patterns_cache,
|
||||
)
|
||||
_exceptions_cache = _merge_pattern_lists(
|
||||
_base_exceptions_cache,
|
||||
_custom_exceptions_cache,
|
||||
_removed_exceptions_cache,
|
||||
)
|
||||
_bad_words_cache = (
|
||||
_whole_word_patterns_cache
|
||||
+ _word_start_patterns_cache
|
||||
+ _contains_patterns_cache
|
||||
+ _en_patterns_cache
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Сохранено кастомных: +{len(added_words)} слов, "
|
||||
f"-{len(removed_words)} базовых слов, "
|
||||
f"+{len(added_exceptions)} исключений, "
|
||||
f"-{len(removed_exceptions)} базовых исключений"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сохранения: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_bad_words():
|
||||
"""Возвращает объединённый список бранных слов (с кэшированием)"""
|
||||
global _bad_words_cache
|
||||
if _bad_words_cache is None:
|
||||
load_bad_words()
|
||||
return _bad_words_cache or []
|
||||
|
||||
|
||||
def get_exceptions():
|
||||
"""Возвращает объединённый список исключений (с кэшированием)"""
|
||||
global _exceptions_cache
|
||||
if _exceptions_cache is None:
|
||||
load_bad_words()
|
||||
return _exceptions_cache or []
|
||||
|
||||
|
||||
def reload_words():
|
||||
"""Перезагружает списки из файлов (сбрасывает кэш)"""
|
||||
global _base_whole_word_patterns_cache, _base_word_start_patterns_cache, _base_contains_patterns_cache, _base_en_patterns_cache, _base_exceptions_cache, _custom_whole_word_patterns_cache, _custom_word_start_patterns_cache, _custom_contains_patterns_cache, _custom_en_patterns_cache, _custom_exceptions_cache, _removed_whole_word_patterns_cache, _removed_word_start_patterns_cache, _removed_contains_patterns_cache, _removed_en_patterns_cache, _removed_exceptions_cache, _whole_word_patterns_cache, _word_start_patterns_cache, _contains_patterns_cache, _en_patterns_cache, _exceptions_cache, _bad_words_cache
|
||||
_base_whole_word_patterns_cache = None
|
||||
_base_word_start_patterns_cache = None
|
||||
_base_contains_patterns_cache = None
|
||||
_base_en_patterns_cache = None
|
||||
_base_exceptions_cache = None
|
||||
_custom_whole_word_patterns_cache = None
|
||||
_custom_word_start_patterns_cache = None
|
||||
_custom_contains_patterns_cache = None
|
||||
_custom_en_patterns_cache = None
|
||||
_custom_exceptions_cache = None
|
||||
_removed_whole_word_patterns_cache = None
|
||||
_removed_word_start_patterns_cache = None
|
||||
_removed_contains_patterns_cache = None
|
||||
_removed_en_patterns_cache = None
|
||||
_removed_exceptions_cache = None
|
||||
_whole_word_patterns_cache = None
|
||||
_word_start_patterns_cache = None
|
||||
_contains_patterns_cache = None
|
||||
_en_patterns_cache = None
|
||||
_exceptions_cache = None
|
||||
_bad_words_cache = None
|
||||
return load_bad_words()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Нормализация текста
|
||||
# ============================================================
|
||||
|
||||
# Транслитерация латиницы -> кириллица (по визуальному сходству)
|
||||
TRANSLITERATION_MAP = {
|
||||
'a': 'а', 'b': 'в', 'c': 'с', 'd': 'д', 'e': 'е', 'f': 'ф',
|
||||
'g': 'г', 'h': 'х', 'i': 'и', 'j': 'ж', 'k': 'к', 'l': 'л',
|
||||
'm': 'м', 'n': 'н', 'o': 'о', 'p': 'р', 'q': 'к', 'r': 'р',
|
||||
's': 'с', 't': 'т', 'u': 'и', 'v': 'в', 'w': 'в', 'x': 'х',
|
||||
'y': 'у', 'z': 'з',
|
||||
}
|
||||
|
||||
# Замена цифр на похожие буквы
|
||||
DIGIT_MAP = {
|
||||
'0': 'о', '1': 'и', '3': 'з', '4': 'ч', '6': 'б', '8': 'в',
|
||||
}
|
||||
|
||||
|
||||
def _remove_obfuscation(text: str, letter_class: str) -> str:
|
||||
"""Убирает разделители между буквами одного алфавита."""
|
||||
# Повторяем, пока что-то меняется
|
||||
for _ in range(10):
|
||||
before = text
|
||||
# Разделители: * . - _
|
||||
text = re.sub(rf'([{letter_class}])[\*\.\-_]+([{letter_class}])', r'\1\2', text)
|
||||
if before == text:
|
||||
break
|
||||
return text
|
||||
|
||||
|
||||
def _remove_spaces_between_letters(text: str) -> str:
|
||||
"""Убирает пробелы между одиночными буквами (х у й -> хуй, f u c k -> fuck),
|
||||
не трогая пробелы между обычными словами (багов не -> багов не)."""
|
||||
pattern = re.compile(r'(?<![a-zа-яё])([a-zа-яё])(?:\s+[a-zа-яё])+(?![a-zа-яё])', re.IGNORECASE)
|
||||
return pattern.sub(lambda m: m.group(0).replace(' ', '').replace('\t', ''), text)
|
||||
|
||||
|
||||
def _collapse_repeated_letters(text: str, letter_class: str) -> str:
|
||||
"""Схлопывает повторяющиеся буквы (3+ -> 1)."""
|
||||
return re.sub(rf'([{letter_class}])\1{{2,}}', r'\1', text)
|
||||
|
||||
|
||||
def normalize_text(text: str) -> str:
|
||||
"""
|
||||
Нормализует текст для обхода обфускации (русская нормализация).
|
||||
|
||||
Убирает:
|
||||
- Транслитерацию латиницы в кириллицу
|
||||
- Цифры похожие на буквы
|
||||
- Языковые варианты (ё→е)
|
||||
- ASCII-имитации (><→х, ||→и, /\→л)
|
||||
- Звездочки, точки, дефисы, подчёркивания между буквами
|
||||
- Одиночные пробелы между одиночными буквами
|
||||
- Повторяющиеся символы
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
normalized = text.lower()
|
||||
|
||||
# ASCII-имитации букв (делаем ДО удаления других символов)
|
||||
ascii_patterns = [
|
||||
(r'\|\|', 'и'),
|
||||
(r'/\\', 'л'),
|
||||
(r'><', 'х'),
|
||||
(r'\}\{', 'х'),
|
||||
(r'\)\(', 'х'),
|
||||
(r'>\|<', 'ж'),
|
||||
(r'\}\|\{', 'ж'),
|
||||
]
|
||||
for pattern, replacement in ascii_patterns:
|
||||
normalized = re.sub(pattern, replacement, normalized)
|
||||
|
||||
# Транслитерация латиницы -> кириллица
|
||||
for lat, cyr in TRANSLITERATION_MAP.items():
|
||||
normalized = normalized.replace(lat, cyr)
|
||||
|
||||
# Замена цифр на буквы
|
||||
for digit, letter in DIGIT_MAP.items():
|
||||
normalized = normalized.replace(digit, letter)
|
||||
|
||||
# ё -> е
|
||||
normalized = normalized.replace('ё', 'е')
|
||||
|
||||
# Убираем обфускацию
|
||||
normalized = _remove_obfuscation(normalized, r'а-яё')
|
||||
|
||||
# Убираем пробелы между буквами (обфускация)
|
||||
normalized = _remove_spaces_between_letters(normalized)
|
||||
|
||||
# Схлопываем повторяющиеся буквы
|
||||
normalized = _collapse_repeated_letters(normalized, r'а-яё')
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_english_text(text: str) -> str:
|
||||
"""
|
||||
Нормализует английский текст для проверки английских бранных слов.
|
||||
|
||||
Убирает:
|
||||
- Звездочки, точки, дефисы, подчёркивания между буквами
|
||||
- Одиночные пробелы между одиночными буквами
|
||||
- Повторяющиеся символы
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
normalized = text.lower()
|
||||
|
||||
# Убираем обфускацию
|
||||
normalized = _remove_obfuscation(normalized, r'a-z')
|
||||
|
||||
# Убираем пробелы между буквами (обфускация)
|
||||
normalized = _remove_spaces_between_letters(normalized)
|
||||
|
||||
# Схлопываем повторяющиеся буквы
|
||||
normalized = _collapse_repeated_letters(normalized, r'a-z')
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Внутренние функции проверки
|
||||
# ============================================================
|
||||
|
||||
def _extract_words(text: str) -> list[str]:
|
||||
"""Извлекает слова из текста (последовательности букв и цифр)."""
|
||||
return re.findall(r'[a-zA-Zа-яёА-ЯЁ0-9]+', text)
|
||||
|
||||
|
||||
def _check_russian_word(normalized_word: str) -> bool:
|
||||
"""Проверяет нормализованное русское слово по паттернам."""
|
||||
if not normalized_word:
|
||||
return False
|
||||
|
||||
whole_word_patterns = _whole_word_patterns_cache or []
|
||||
word_start_patterns = _word_start_patterns_cache or []
|
||||
contains_patterns = _contains_patterns_cache or []
|
||||
|
||||
# Точное совпадение
|
||||
if normalized_word in whole_word_patterns:
|
||||
return True
|
||||
|
||||
# Начало слова
|
||||
for pattern in word_start_patterns:
|
||||
if normalized_word.startswith(pattern):
|
||||
return True
|
||||
|
||||
# Вхождение подстроки (только для длинных паттернов)
|
||||
for pattern in contains_patterns:
|
||||
if len(pattern) >= MIN_CONTAINS_LENGTH and pattern in normalized_word:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _check_english_word(normalized_word: str) -> bool:
|
||||
"""Проверяет нормализованное английское слово по паттернам."""
|
||||
if not normalized_word:
|
||||
return False
|
||||
|
||||
en_patterns = _en_patterns_cache or []
|
||||
|
||||
for pattern in en_patterns:
|
||||
if normalized_word == pattern or pattern in normalized_word:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _prepare_exceptions() -> set[str]:
|
||||
"""Возвращает нормализованное множество исключений."""
|
||||
exceptions = _exceptions_cache or []
|
||||
return set(normalize_text(e) for e in exceptions)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Публичные функции проверки
|
||||
# ============================================================
|
||||
|
||||
def contains_bad_word(text: str) -> bool:
|
||||
"""
|
||||
Проверяет, содержит ли текст бранные слова.
|
||||
|
||||
Поддерживает русский и английский языки, обфускацию, транслитерацию.
|
||||
Учитывает список исключений для избежания ложных срабатываний.
|
||||
"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
if _whole_word_patterns_cache is None:
|
||||
load_bad_words()
|
||||
|
||||
normalized_exceptions = _prepare_exceptions()
|
||||
|
||||
# Проверяем два варианта:
|
||||
# 1. Пословная нормализация (для обычного текста)
|
||||
# 2. Нормализация всего текста с удалением пробелов (для обфускации типа "х е р", "f u c k")
|
||||
|
||||
# Вариант 1: пословная
|
||||
for word in _extract_words(text):
|
||||
ru_normalized = normalize_text(word)
|
||||
if ru_normalized not in normalized_exceptions and _check_russian_word(ru_normalized):
|
||||
return True
|
||||
|
||||
en_normalized = normalize_english_text(word)
|
||||
if _check_english_word(en_normalized):
|
||||
return True
|
||||
|
||||
# Вариант 2: с удалением пробелов
|
||||
ru_text = normalize_text(text)
|
||||
for word in _extract_words(ru_text):
|
||||
if word not in normalized_exceptions and _check_russian_word(word):
|
||||
return True
|
||||
|
||||
en_text = normalize_english_text(text)
|
||||
for word in _extract_words(en_text):
|
||||
if _check_english_word(word):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_bad_words_from_text(text: str) -> list[str]:
|
||||
"""Возвращает список найденных в тексте бранных слов (паттернов)."""
|
||||
if not text:
|
||||
return []
|
||||
|
||||
if _whole_word_patterns_cache is None:
|
||||
load_bad_words()
|
||||
|
||||
normalized_exceptions = _prepare_exceptions()
|
||||
found = []
|
||||
|
||||
def check_russian_word(word):
|
||||
if word in normalized_exceptions:
|
||||
return
|
||||
|
||||
# Проверяем whole_word
|
||||
if word in (_whole_word_patterns_cache or []):
|
||||
if word not in found:
|
||||
found.append(word)
|
||||
return
|
||||
|
||||
# Проверяем word_start
|
||||
for pattern in (_word_start_patterns_cache or []):
|
||||
if word.startswith(pattern):
|
||||
if pattern not in found:
|
||||
found.append(pattern)
|
||||
break
|
||||
|
||||
# Проверяем contains
|
||||
for pattern in (_contains_patterns_cache or []):
|
||||
if len(pattern) >= MIN_CONTAINS_LENGTH and pattern in word:
|
||||
if pattern not in found:
|
||||
found.append(pattern)
|
||||
break
|
||||
|
||||
def check_english_word(word):
|
||||
for pattern in (_en_patterns_cache or []):
|
||||
if word == pattern or pattern in word:
|
||||
if pattern not in found:
|
||||
found.append(pattern)
|
||||
break
|
||||
|
||||
# Вариант 1: пословная проверка
|
||||
for word in _extract_words(text):
|
||||
check_russian_word(normalize_text(word))
|
||||
check_english_word(normalize_english_text(word))
|
||||
|
||||
# Вариант 2: с удалением пробелов
|
||||
ru_text = normalize_text(text)
|
||||
for word in _extract_words(ru_text):
|
||||
check_russian_word(word)
|
||||
|
||||
en_text = normalize_english_text(text)
|
||||
for word in _extract_words(en_text):
|
||||
check_english_word(word)
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def highlight_bad_words_in_text(text: str) -> str:
|
||||
"""Возвращает текст с выделенными бранными словами (🔴<b>слово</b>🔴)."""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
if _whole_word_patterns_cache is None:
|
||||
load_bad_words()
|
||||
|
||||
normalized_exceptions = _prepare_exceptions()
|
||||
positions = []
|
||||
|
||||
# Проверяем каждое исходное слово отдельно, чтобы позиции выделения
|
||||
# соответствовали оригинальному тексту.
|
||||
for match in re.finditer(r'[a-zA-Zа-яёА-ЯЁ0-9]+', text):
|
||||
word = match.group()
|
||||
start, end = match.start(), match.end()
|
||||
|
||||
ru_normalized = normalize_text(word)
|
||||
is_bad = (
|
||||
ru_normalized not in normalized_exceptions
|
||||
and _check_russian_word(ru_normalized)
|
||||
)
|
||||
|
||||
if not is_bad:
|
||||
en_normalized = normalize_english_text(word)
|
||||
is_bad = _check_english_word(en_normalized)
|
||||
|
||||
if is_bad:
|
||||
positions.append((start, end))
|
||||
|
||||
# Объединяем перекрывающиеся интервалы
|
||||
positions.sort()
|
||||
merged = []
|
||||
for start, end in positions:
|
||||
if merged and start <= merged[-1][1]:
|
||||
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
|
||||
else:
|
||||
merged.append((start, end))
|
||||
|
||||
# Выделяем, идём с конца
|
||||
result_list = list(text)
|
||||
for start, end in reversed(merged):
|
||||
word = text[start:end]
|
||||
escaped = word.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
highlighted = f'🔴<b>{escaped}</b>🔴'
|
||||
result_list[start:end] = highlighted
|
||||
|
||||
return ''.join(result_list)
|
||||
|
||||
|
||||
# Предзагружаем слова при импорте модуля
|
||||
load_bad_words()
|
||||
+343
-55
@@ -7,20 +7,76 @@ DATABASE_NAME = 'users.db'
|
||||
# Название файла для логов
|
||||
LOG_FILE_NAME = 'bot.log'
|
||||
|
||||
# ===========================================
|
||||
# Временные константы (в секундах)
|
||||
# ===========================================
|
||||
|
||||
# Период учёта нарушений (30 дней)
|
||||
VIOLATIONS_PERIOD = 2592000
|
||||
|
||||
# Кулдаун для благодарностей (1 час)
|
||||
THANK_COOLDOWN = 3600
|
||||
|
||||
# Периоды для предупреждений
|
||||
ONE_WEEK = 604800 # 7 дней
|
||||
TWO_WEEKS = 1209600 # 14 дней
|
||||
|
||||
# Максимальное время мута (30 дней)
|
||||
MAX_MUTE_TIME = 2592000
|
||||
|
||||
# ===========================================
|
||||
# Настройки капчи / защиты входа
|
||||
# ===========================================
|
||||
|
||||
# Время на прохождение капчи в секундах
|
||||
CAPTCHA_TIMEOUT = 60
|
||||
|
||||
# Ключ настройки чата для включения/отключения капчи
|
||||
GUARD_SETTING_KEY = 'guard'
|
||||
|
||||
# Значение по умолчанию: 1 — капча включена
|
||||
GUARD_DEFAULT_VALUE = '1'
|
||||
|
||||
# Сообщения команд
|
||||
COMMAND_MESSAGES = {
|
||||
'start': 'Бот-администратор для чата @linux_gaming_ru',
|
||||
'help': (
|
||||
'log': (
|
||||
"<b>📋 Как создать лог ошибки</b>\n\n"
|
||||
"Подробная инструкция по созданию логов для диагностики проблем:\n\n"
|
||||
"🔗 <a href='https://linux-gaming.ru/t/kak-sozdat-log-oshibki/25'>Перейти к инструкции</a>"
|
||||
),
|
||||
'help_user': (
|
||||
"<b>📚 Справочник команд</b>\n\n"
|
||||
"<u>Основные команды:</u>\n"
|
||||
"• <code>/start</code> - Начало работы\n"
|
||||
"• <code>/help</code> - Этот справочник\n"
|
||||
"• <code>/log</code> - Инструкция по созданию логов\n\n"
|
||||
"<u>⭐ Система кармы:</u>\n"
|
||||
"• <code>/karma</code> - Просмотр своей кармы\n"
|
||||
"• <code>/karma @user</code> - Просмотр кармы пользователя\n"
|
||||
"• <code>/karma top</code> - Топ пользователей\n"
|
||||
"• <code>/karma help</code> - Подробнее о системе кармы"
|
||||
),
|
||||
'help_admin': (
|
||||
"<b>📚 Справочник команд администратора</b>\n\n"
|
||||
"<u>Основные команды:</u>\n"
|
||||
"• <code>/start</code> - Начало работы\n"
|
||||
"• <code>/help</code> - Этот справочник\n\n"
|
||||
"<u>🛠 Команды модерации:</u>\n"
|
||||
"• <code>/mute help</code> - Инструкция по муту\n"
|
||||
"• <code>/unmute help</code> - Снятие мута\n"
|
||||
"• <code>/ban help</code> - Инструкция по бану\n"
|
||||
"• <code>/unban help</code> - Снятие бана\n\n"
|
||||
"<i>ℹ️ Для подробностей по конкретной команде используйте: /команда help</i>"
|
||||
"• <code>/help</code> - Этот справочник\n"
|
||||
"• <code>/log</code> - Инструкция по созданию логов\n\n"
|
||||
"<u>🛠 Модерация:</u>\n"
|
||||
"• <code>/warn</code> - Предупреждение (<code>help</code>, <code>reset</code>)\n"
|
||||
"• <code>/mute</code> - Ограничение (<code>help</code>, <code>off</code>, <code>reset</code>)\n"
|
||||
"• <code>/ban</code> - Блокировка (<code>help</code>, <code>off</code>)\n\n"
|
||||
"<u>⭐ Система кармы:</u>\n"
|
||||
"• <code>/karma</code> - Просмотр кармы\n"
|
||||
"• <code>/karma top</code> - Топ пользователей\n"
|
||||
"• <code>/karma set</code> - Установить карму\n"
|
||||
"• <code>/karma disable/enable/status</code> - Управление в топиках\n\n"
|
||||
"<u>🛡 Защита чата:</u>\n"
|
||||
"• <code>/guard</code> - Капча для новых участников\n\n"
|
||||
"<u>⚙️ Управление ботом:</u>\n"
|
||||
"• <code>/badwords help</code> - Управление списком запрещённых слов\n\n"
|
||||
"<i>ℹ️ Для подробностей: /команда help</i>"
|
||||
),
|
||||
'manual_mute': (
|
||||
"<b>🔇 Команда /mute</b>\n\n"
|
||||
@@ -29,53 +85,31 @@ COMMAND_MESSAGES = {
|
||||
"• Минуты: <code>10м</code>, <code>30м</code>\n"
|
||||
"• Часы: <code>1ч</code>, <code>3ч</code>\n"
|
||||
"• Дни: <code>1д</code>, <code>7д</code>\n\n"
|
||||
"<u>🎯 Способы использования:</u>\n"
|
||||
"1. Ответ на сообщение:\n"
|
||||
" <code>/mute 30м причина</code>\n"
|
||||
"2. По тегу пользователя:\n"
|
||||
" <code>/mute @username 1ч спам</code>\n"
|
||||
"3. По ID пользователя:\n"
|
||||
" <code>/mute 123456789 1д нарушение правил</code>\n\n"
|
||||
"<b>⚠️ Максимальный срок: 30 дней</b>\n"
|
||||
"<i>ℹ️ Причину стараться указывать</i>"
|
||||
),
|
||||
'manual_unmute': (
|
||||
"<b>🔊 Команда /unmute</b>\n\n"
|
||||
"<i>Снимает ограничения с пользователя</i>\n\n"
|
||||
"<u>🎯 Способы использования:</u>\n"
|
||||
"1. Ответ на сообщение:\n"
|
||||
" <code>/unmute</code>\n"
|
||||
"2. По тегу пользователя:\n"
|
||||
" <code>/unmute @username</code>\n"
|
||||
"3. По ID пользователя:\n"
|
||||
" <code>/unmute 123456789</code>\n\n"
|
||||
"<i>ℹ️ Работает только для временно замученных пользователей</i>"
|
||||
"<u>🎯 Выдать мут:</u>\n"
|
||||
"• <code>/mute 30м причина</code> (ответом)\n"
|
||||
"• <code>/mute @user 1ч спам</code>\n"
|
||||
"• <code>/mute 123456789 1д нарушение</code>\n\n"
|
||||
"<u>🔊 Снять мут:</u>\n"
|
||||
"• <code>/mute off</code> (ответом)\n"
|
||||
"• <code>/mute off @user</code>\n"
|
||||
"• <code>/mute off 123456789</code>\n\n"
|
||||
"<u>🔄 Сбросить счётчик автомутов:</u>\n"
|
||||
"• <code>/mute reset</code> (ответом)\n"
|
||||
"• <code>/mute reset @user</code>\n\n"
|
||||
"<b>⚠️ Максимальный срок: 30 дней</b>"
|
||||
),
|
||||
'manual_ban': (
|
||||
"<b>🚫 Команда /ban</b>\n\n"
|
||||
"<i>Навсегда исключает пользователя из чата</i>\n\n"
|
||||
"<u>🎯 Способы использования:</u>\n"
|
||||
"1. Ответ на сообщение:\n"
|
||||
" <code>/ban причина</code>\n"
|
||||
"2. По тегу пользователя:\n"
|
||||
" <code>/ban @username спам</code>\n"
|
||||
"3. По ID пользователя:\n"
|
||||
" <code>/ban 123456789 нарушение правил</code>\n\n"
|
||||
"<b>⚠️ Добавляет в ЧС</b>\n"
|
||||
"<i>ℹ️ Для разбана используйте /unban</i>"
|
||||
),
|
||||
'manual_unban': (
|
||||
"<b>✅ Команда /unban</b>\n\n"
|
||||
"<i>Снимает бан с пользователя</i>\n\n"
|
||||
"<u>🎯 Способы использования:</u>\n"
|
||||
"1. Ответ на сообщение:\n"
|
||||
" <code>/unban</code>\n"
|
||||
"2. По тегу пользователя:\n"
|
||||
" <code>/unban @username</code>\n"
|
||||
"3. По ID пользователя:\n"
|
||||
" <code>/unban 123456789</code>\n\n"
|
||||
"<b>⚠️ Работает только для забаненных через /ban</b>\n"
|
||||
"<i>ℹ️ Пользователь сможет снова присоединиться</i>"
|
||||
"<u>🎯 Забанить:</u>\n"
|
||||
"• <code>/ban причина</code> (ответом)\n"
|
||||
"• <code>/ban @user спам</code>\n"
|
||||
"• <code>/ban 123456789 нарушение</code>\n\n"
|
||||
"<u>✅ Разбанить:</u>\n"
|
||||
"• <code>/ban off</code> (ответом)\n"
|
||||
"• <code>/ban off @user</code>\n"
|
||||
"• <code>/ban off 123456789</code>\n\n"
|
||||
"<b>⚠️ Бан добавляет в чёрный список</b>"
|
||||
),
|
||||
'no_admin_rights': '❌ Только администраторы могут использовать эту команду.',
|
||||
'no_restrict_rights': '❌ У вас недостаточно прав.',
|
||||
@@ -85,11 +119,265 @@ COMMAND_MESSAGES = {
|
||||
'max_mute': '❌ Максимальное время мута - 30 дней.',
|
||||
'cant_mute_admin': '❌ Невозможно замутить администратора.',
|
||||
'cant_ban_admin': '❌ Невозможно забанить администратора.',
|
||||
'muted': '✅ Пользователь замучен на {time_display}.',
|
||||
'muted': '🔇 <b>МУТ</b>\n👤 {target_user}\n👮 {admin_user}\n⏱ {time_display}\n📝 {reason}',
|
||||
'unmuted': '✅ Пользователь размучен.',
|
||||
'banned': '✅ Пользователь успешно забанен.',
|
||||
'banned': '🚫 <b>БАН</b>\n👤 {target_user}\n👮 {admin_user}\n📝 {reason}',
|
||||
'unbanned': '✅ Пользователь успешно разбанен.',
|
||||
'error': '⚠️ Ошибка: {e}',
|
||||
'general_error': '⚠️ Произошла непредвиденная ошибка.'
|
||||
'general_error': '⚠️ Произошла непредвиденная ошибка.',
|
||||
'auto_mute_warning': (
|
||||
'⚠️ Пользователь <b>{user_name}</b> получил мут на <b>{duration}</b> '
|
||||
'за использование нецензурной лексики.\n\n'
|
||||
'📊 Нарушение #{count}\n'
|
||||
'💡 При повторных нарушениях время мута будет увеличиваться.'
|
||||
),
|
||||
'auto_mute_permanent': (
|
||||
'⛔️ Пользователь <b>{user_name}</b> получил перманентный мут '
|
||||
'за злостное нарушение правил чата (использование нецензурной лексики).\n\n'
|
||||
'📊 Количество нарушений: <b>{count}</b>\n'
|
||||
'🔒 Режим: только чтение (навсегда)'
|
||||
),
|
||||
'badwords_help': (
|
||||
"<b>🔧 Управление списком бранных слов</b>\n\n"
|
||||
"<u>Основные команды:</u>\n"
|
||||
"• <code>/badwords list</code> - Показать список слов\n"
|
||||
"• <code>/badwords count</code> - Статистика\n"
|
||||
"• <code>/badwords add [слово]</code> - Добавить слово\n"
|
||||
"• <code>/badwords remove [слово]</code> - Удалить слово\n\n"
|
||||
"<u>Исключения:</u>\n"
|
||||
"• <code>/badwords exceptions</code> - Список исключений\n"
|
||||
"• <code>/badwords add_exception [слово]</code> - Добавить\n"
|
||||
"• <code>/badwords remove_exception [слово]</code> - Удалить\n\n"
|
||||
"<u>Прочее:</u>\n"
|
||||
"• <code>/badwords reload</code> - Перезагрузить из файла\n\n"
|
||||
"<i>💡 Все изменения применяются немедленно</i>"
|
||||
),
|
||||
'manual_warn': (
|
||||
"<b>⚠️ Команда /warn</b>\n\n"
|
||||
"<i>Выдаёт официальное предупреждение пользователю</i>\n\n"
|
||||
"<u>🎯 Выдать предупреждение:</u>\n"
|
||||
"• <code>/warn причина</code> (ответом)\n"
|
||||
"• <code>/warn @user причина</code>\n"
|
||||
"• <code>/warn 123456789 причина</code>\n\n"
|
||||
"<u>🔄 Сбросить счётчик предупреждений:</u>\n"
|
||||
"• <code>/warn reset</code> (ответом)\n"
|
||||
"• <code>/warn reset @user</code>\n"
|
||||
"• <code>/warn reset 123456789</code>\n\n"
|
||||
"<b>📋 Система накопления:</b>\n"
|
||||
"• 1-й варн: предупреждение\n"
|
||||
"• 2-й варн за неделю: +мут 7 дней\n"
|
||||
"• 2-й варн за 2 недели: +мут 1 день\n\n"
|
||||
"<i>ℹ️ Причину обязательно указывайте</i>"
|
||||
),
|
||||
'warned': '⚠️ <b>ПРЕДУПРЕЖДЕНИЕ</b>\n👤 {target_user}\n👮 {admin_user}\n📝 {reason}',
|
||||
'warned_auto_mute_day': '⚠️ <b>ПРЕДУПРЕЖДЕНИЕ + МУТ</b>\n👤 {target_user}\n👮 {admin_user}\n⏱ 1 день (2-е предупреждение за 2 недели)\n📝 {reason}',
|
||||
'warned_auto_mute_week': '⚠️ <b>ПРЕДУПРЕЖДЕНИЕ + МУТ</b>\n👤 {target_user}\n👮 {admin_user}\n⏱ 7 дней (2-е предупреждение за неделю)\n📝 {reason}',
|
||||
'karma_help': (
|
||||
"<b>⭐ Команда /karma</b>\n\n"
|
||||
"<u>🎯 Просмотр кармы:</u>\n"
|
||||
"• <code>/karma</code> - своя карма\n"
|
||||
"• <code>/karma @user</code> - карма пользователя\n"
|
||||
"• <code>/karma top</code> - топ-10 пользователей\n\n"
|
||||
"<u>🔧 Для администраторов:</u>\n"
|
||||
"• <code>/karma set @user 100</code> - установить карму\n"
|
||||
"• <code>/karma disable</code> - отключить в топике\n"
|
||||
"• <code>/karma enable</code> - включить в топике\n"
|
||||
"• <code>/karma status</code> - статус топиков\n\n"
|
||||
"<b>💡 Как начислить карму?</b>\n"
|
||||
"<u>Способ 1: Ответить на сообщение</u>\n"
|
||||
"• спасибо, благодарю, спс, thanks → +1\n"
|
||||
"• С восклицательным знаком: x2!\n\n"
|
||||
"<u>Способ 2: Реакции</u>\n"
|
||||
"• 👍 → +1 | 👎 → -1\n"
|
||||
"• 🔥 → +2 | ❤ → +5 | ❤🔥 → +10\n\n"
|
||||
"<b>⚠️ Штрафы:</b>\n"
|
||||
"• /warn: -5 | /mute: -10\n\n"
|
||||
"<i>⏱ Кулдаун благодарности: 1 час</i>"
|
||||
),
|
||||
'guard_help': (
|
||||
"<b>🛡 Команда /guard</b>\n\n"
|
||||
"<i>Управление капчей для новых участников</i>\n\n"
|
||||
"<u>Доступно только администраторам чата.</u>\n\n"
|
||||
"• <code>/guard</code> - текущий статус\n"
|
||||
"• <code>/guard on</code> - включить капчу\n"
|
||||
"• <code>/guard off</code> - отключить капчу\n\n"
|
||||
"<i>ℹ️ Проверка по спам-листу CAS работает всегда, независимо от этой настройки.</i>"
|
||||
),
|
||||
'guard_status': '🔐 <b>Капча при входе:</b> {status}',
|
||||
'guard_enabled': '✅ Капча при входе включена.',
|
||||
'guard_disabled': '✅ Капча при входе отключена.',
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, для подтверждения входа нажмите кнопку '
|
||||
'в течение {timeout} сек. В противном случае вы будете удалены из чата.'
|
||||
),
|
||||
'captcha_welcome': 'Добро пожаловать! ✅',
|
||||
'captcha_wrong_user': 'Эта кнопка не для тебя!'
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
# ===========================================
|
||||
# Локализация капчи по языку пользователя
|
||||
# ===========================================
|
||||
|
||||
CAPTCHA_LOCALES = {
|
||||
'ru': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, для подтверждения входа нажмите кнопку '
|
||||
'в течение {timeout} сек. В противном случае вы будете удалены из чата.'
|
||||
),
|
||||
'captcha_button': '✅ Я не бот',
|
||||
'captcha_welcome': 'Добро пожаловать! ✅',
|
||||
'captcha_wrong_user': 'Эта кнопка не для тебя!',
|
||||
},
|
||||
'en': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, please press the button within {timeout} seconds '
|
||||
'to confirm your entry. Otherwise, you will be removed from the chat.'
|
||||
),
|
||||
'captcha_button': '✅ I am not a bot',
|
||||
'captcha_welcome': 'Welcome! ✅',
|
||||
'captcha_wrong_user': 'This button is not for you!',
|
||||
},
|
||||
'uk': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, для підтвердження входу натисніть кнопку '
|
||||
'протягом {timeout} сек. Інакше ви будете видалені з чату.'
|
||||
),
|
||||
'captcha_button': '✅ Я не бот',
|
||||
'captcha_welcome': 'Ласкаво просимо! ✅',
|
||||
'captcha_wrong_user': 'Ця кнопка не для тебе!',
|
||||
},
|
||||
'be': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, для пацверджання ўваходу націсніце кнопку '
|
||||
'на працягу {timeout} сек. У адваротным выпадку вы будзеце выдалены з чату.'
|
||||
),
|
||||
'captcha_button': '✅ Я не бот',
|
||||
'captcha_welcome': 'Сардэчна запрашаем! ✅',
|
||||
'captcha_wrong_user': 'Гэта кнопка не для цябе!',
|
||||
},
|
||||
'de': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, bitte drücken Sie die Schaltfläche innerhalb von '
|
||||
'{timeout} Sekunden, um den Eintritt zu bestätigen. Andernfalls werden '
|
||||
'Sie aus dem Chat entfernt.'
|
||||
),
|
||||
'captcha_button': '✅ Ich bin kein Bot',
|
||||
'captcha_welcome': 'Willkommen! ✅',
|
||||
'captcha_wrong_user': 'Diese Schaltfläche ist nicht für dich!',
|
||||
},
|
||||
'fr': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, veuillez appuyer sur le bouton dans les {timeout} '
|
||||
'secondes pour confirmer votre entrée. Sinon, vous serez retiré du chat.'
|
||||
),
|
||||
'captcha_button': '✅ Je ne suis pas un robot',
|
||||
'captcha_welcome': 'Bienvenue! ✅',
|
||||
'captcha_wrong_user': "Ce bouton n'est pas pour toi!",
|
||||
},
|
||||
'es': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, por favor presiona el botón dentro de {timeout} '
|
||||
'segundos para confirmar tu entrada. De lo contrario, serás eliminado del chat.'
|
||||
),
|
||||
'captcha_button': '✅ No soy un bot',
|
||||
'captcha_welcome': '¡Bienvenido! ✅',
|
||||
'captcha_wrong_user': '¡Este botón no es para ti!',
|
||||
},
|
||||
'it': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, premi il pulsante entro {timeout} secondi per '
|
||||
"confermare l'ingresso. Altrimenti verrai rimosso dalla chat."
|
||||
),
|
||||
'captcha_button': '✅ Non sono un bot',
|
||||
'captcha_welcome': 'Benvenuto! ✅',
|
||||
'captcha_wrong_user': 'Questo pulsante non è per te!',
|
||||
},
|
||||
'pl': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, naciśnij przycisk w ciągu {timeout} sekund, aby '
|
||||
'potwierdzić wejście. W przeciwnym razie zostaniesz usunięty z czatu.'
|
||||
),
|
||||
'captcha_button': '✅ Nie jestem botem',
|
||||
'captcha_welcome': 'Witamy! ✅',
|
||||
'captcha_wrong_user': 'Ten przycisk nie jest dla ciebie!',
|
||||
},
|
||||
'pt': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, por favor pressione o botão em até {timeout} '
|
||||
'segundos para confirmar sua entrada. Caso contrário, você será removido do chat.'
|
||||
),
|
||||
'captcha_button': '✅ Não sou um bot',
|
||||
'captcha_welcome': 'Bem-vindo! ✅',
|
||||
'captcha_wrong_user': 'Este botão não é para você!',
|
||||
},
|
||||
'zh': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention},请在 {timeout} 秒内按下按钮以确认进入。'
|
||||
'否则您将被移出群组。'
|
||||
),
|
||||
'captcha_button': '✅ 我不是机器人',
|
||||
'captcha_welcome': '欢迎!✅',
|
||||
'captcha_wrong_user': '这个按钮不是给你的!',
|
||||
},
|
||||
'ja': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}、入室を確認するために {timeout} 秒以内にボタンを押してください。'
|
||||
'そうしないと、チャットから削除されます。'
|
||||
),
|
||||
'captcha_button': '✅ 私はロボットではありません',
|
||||
'captcha_welcome': 'ようこそ!✅',
|
||||
'captcha_wrong_user': 'このボタンはあなた用ではありません!',
|
||||
},
|
||||
'ko': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, 입장을 확인하려면 {timeout}초 이내에 버튼을 눌러주세요. '
|
||||
'그렇지 않으면 채팅에서 제거됩니다.'
|
||||
),
|
||||
'captcha_button': '✅ 저는 봇이 아닙니다',
|
||||
'captcha_welcome': '환영합니다! ✅',
|
||||
'captcha_wrong_user': '이 버튼은 당신을 위한 것이 아닙니다!',
|
||||
},
|
||||
'ar': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}، يرجى الضغط على الزر خلال {timeout} ثانية لتأكيد الدخول. '
|
||||
'وإلا سيتم إزالتك من الدردشة.'
|
||||
),
|
||||
'captcha_button': '✅ أنا لست روبوتًا',
|
||||
'captcha_welcome': 'أهلاً بك! ✅',
|
||||
'captcha_wrong_user': 'هذا الزر ليس لك!',
|
||||
},
|
||||
'he': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, אנא לחץ על הכפתור בתוך {timeout} שניות כדי לאשר את הכניסה. '
|
||||
'אחרת תוסר מהצ\'אט.'
|
||||
),
|
||||
'captcha_button': '✅ אני לא בוט',
|
||||
'captcha_welcome': 'ברוך הבא! ✅',
|
||||
'captcha_wrong_user': 'הכפתור הזה לא בשבילך!',
|
||||
},
|
||||
'tr': {
|
||||
'captcha_message': (
|
||||
'👋 {user_mention}, girişi onaylamak için lütfen {timeout} saniye içinde '
|
||||
'düğmeye basın. Aksi takdirde sohbetten çıkarılacaksınız.'
|
||||
),
|
||||
'captcha_button': '✅ Ben bot değilim',
|
||||
'captcha_welcome': 'Hoş geldiniz! ✅',
|
||||
'captcha_wrong_user': 'Bu düğme senin için değil!',
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_CAPTCHA_LANGUAGE = 'ru'
|
||||
|
||||
|
||||
def get_captcha_text(language_code: str | None, key: str = 'captcha_message') -> str:
|
||||
"""Вернуть локализованный текст капчи по языку пользователя.
|
||||
|
||||
Telegram передаёт language_code в формате IETF (например, 'ru', 'en-US').
|
||||
Если язык неизвестен или не задан, используется русский (DEFAULT_CAPTCHA_LANGUAGE).
|
||||
"""
|
||||
if not language_code:
|
||||
return CAPTCHA_LOCALES[DEFAULT_CAPTCHA_LANGUAGE][key]
|
||||
|
||||
lang = language_code.lower().split('-')[0]
|
||||
return CAPTCHA_LOCALES.get(lang, CAPTCHA_LOCALES[DEFAULT_CAPTCHA_LANGUAGE])[key]
|
||||
@@ -0,0 +1,401 @@
|
||||
{
|
||||
"patterns": {
|
||||
"whole_word": [
|
||||
"гандон",
|
||||
"гей",
|
||||
"гнид",
|
||||
"гнида",
|
||||
"гомик",
|
||||
"гомос",
|
||||
"даун",
|
||||
"дебил",
|
||||
"козел",
|
||||
"козл",
|
||||
"мраз",
|
||||
"мразь",
|
||||
"падла",
|
||||
"твар",
|
||||
"тварь",
|
||||
"урод",
|
||||
"урода"
|
||||
],
|
||||
"word_start": [
|
||||
"бля",
|
||||
"ебал",
|
||||
"ебан",
|
||||
"ебат",
|
||||
"ебет",
|
||||
"ебля",
|
||||
"ебош",
|
||||
"ебут",
|
||||
"ебуч",
|
||||
"жоп",
|
||||
"жёп",
|
||||
"пезд",
|
||||
"пизд",
|
||||
"пизж",
|
||||
"хер",
|
||||
"хуе",
|
||||
"хуи",
|
||||
"хуй",
|
||||
"хую",
|
||||
"хуя"
|
||||
],
|
||||
"contains": [
|
||||
"ахуе",
|
||||
"блядь",
|
||||
"блять",
|
||||
"впизду",
|
||||
"выблядо",
|
||||
"говн",
|
||||
"долбаеб",
|
||||
"долбоеб",
|
||||
"дроч",
|
||||
"дрочи",
|
||||
"еблан",
|
||||
"залуп",
|
||||
"захуя",
|
||||
"звиздюк",
|
||||
"мудак",
|
||||
"муди",
|
||||
"мудил",
|
||||
"нахер",
|
||||
"нахуй",
|
||||
"охуе",
|
||||
"охуи",
|
||||
"охуя",
|
||||
"педик",
|
||||
"педр",
|
||||
"пидар",
|
||||
"пидор",
|
||||
"попизд",
|
||||
"похер",
|
||||
"похуй",
|
||||
"сволоч",
|
||||
"сволочь",
|
||||
"серун",
|
||||
"сирик",
|
||||
"срака",
|
||||
"сраль",
|
||||
"срать",
|
||||
"ссался",
|
||||
"сука",
|
||||
"суки",
|
||||
"сучар",
|
||||
"сучк",
|
||||
"сытса",
|
||||
"ублюд",
|
||||
"ублюдо",
|
||||
"уебан",
|
||||
"уебок",
|
||||
"хуесос",
|
||||
"хуета",
|
||||
"хуйн",
|
||||
"шалав",
|
||||
"шлюх"
|
||||
],
|
||||
"en": [
|
||||
"asshole",
|
||||
"assholes",
|
||||
"bitch",
|
||||
"bitches",
|
||||
"bitchy",
|
||||
"bliad",
|
||||
"bliat",
|
||||
"blya",
|
||||
"blyat",
|
||||
"ebal",
|
||||
"ebany",
|
||||
"ebat",
|
||||
"fck",
|
||||
"fuck",
|
||||
"fucked",
|
||||
"fucker",
|
||||
"fucking",
|
||||
"fucks",
|
||||
"fuk",
|
||||
"fuuk",
|
||||
"gandon",
|
||||
"gandona",
|
||||
"hui",
|
||||
"huilo",
|
||||
"huy",
|
||||
"mudak",
|
||||
"mudila",
|
||||
"nahui",
|
||||
"pedik",
|
||||
"pidor",
|
||||
"pizda",
|
||||
"pizdec",
|
||||
"pizdets",
|
||||
"pizdetsa",
|
||||
"pizdu",
|
||||
"pohui",
|
||||
"shit",
|
||||
"shitting",
|
||||
"shitty",
|
||||
"suchka",
|
||||
"suka",
|
||||
"suki",
|
||||
"xyu",
|
||||
"xyй"
|
||||
]
|
||||
},
|
||||
"exceptions": [
|
||||
"bloatware",
|
||||
"аблятив",
|
||||
"абляут",
|
||||
"абляционный",
|
||||
"абляция",
|
||||
"ассамбляж",
|
||||
"ассамбляжем",
|
||||
"блямба",
|
||||
"бляха",
|
||||
"бляшечка",
|
||||
"бляшка",
|
||||
"взвихренный",
|
||||
"витеблянин",
|
||||
"влюблявшийся",
|
||||
"влюблять",
|
||||
"влюбляться",
|
||||
"влюбляющийся",
|
||||
"всколебать",
|
||||
"всколебаться",
|
||||
"выгребать",
|
||||
"выгребаться",
|
||||
"выскребать",
|
||||
"выхлебать",
|
||||
"выщерблять",
|
||||
"гейм",
|
||||
"гейтвей",
|
||||
"гребля",
|
||||
"гребляк",
|
||||
"гребу",
|
||||
"гребут",
|
||||
"даунгрейд",
|
||||
"даунгрейт",
|
||||
"догребать",
|
||||
"документ",
|
||||
"документы",
|
||||
"доскребать",
|
||||
"дохлебать",
|
||||
"дубляж",
|
||||
"дубляжем",
|
||||
"дубляжом",
|
||||
"забляковать",
|
||||
"завихрение",
|
||||
"завихренние",
|
||||
"заглублявший",
|
||||
"заглублять",
|
||||
"заглубляющий",
|
||||
"загребать",
|
||||
"заколебать",
|
||||
"заколебаться",
|
||||
"зарыблять",
|
||||
"застрахуйся",
|
||||
"злоупотреблявший",
|
||||
"злоупотреблял",
|
||||
"злоупотреблять",
|
||||
"злоупотребляющий",
|
||||
"истреблявший",
|
||||
"истреблявшийся",
|
||||
"истребляемый",
|
||||
"истреблять",
|
||||
"истребляться",
|
||||
"истребляющий",
|
||||
"истребляющийся",
|
||||
"козловский",
|
||||
"козловского",
|
||||
"колебаний",
|
||||
"колебать",
|
||||
"колебаться",
|
||||
"колебля",
|
||||
"колеблясь",
|
||||
"корабля",
|
||||
"кулдаун",
|
||||
"любляна",
|
||||
"люблянский",
|
||||
"люблянцев",
|
||||
"люблянцем",
|
||||
"нагребать",
|
||||
"наскребать",
|
||||
"нахлебаться",
|
||||
"небалансным",
|
||||
"небудь",
|
||||
"обляпавший",
|
||||
"обляпался",
|
||||
"обляпанный",
|
||||
"обляпать",
|
||||
"обляпаться",
|
||||
"обляпывать",
|
||||
"обляпываться",
|
||||
"облёк",
|
||||
"облёкся",
|
||||
"обособлявший",
|
||||
"обособлявшийся",
|
||||
"обособляйся",
|
||||
"обособлять",
|
||||
"обособляться",
|
||||
"обособляющий",
|
||||
"обособляющийся",
|
||||
"оглобля",
|
||||
"огребать",
|
||||
"огребаться",
|
||||
"огрублявший",
|
||||
"огрублять",
|
||||
"огрубляющий",
|
||||
"озлоблявший",
|
||||
"озлоблявшийся",
|
||||
"озлобляемый",
|
||||
"озлоблять",
|
||||
"озлобляться",
|
||||
"озлобляющий",
|
||||
"озлобляющийся",
|
||||
"оскорблявший",
|
||||
"оскорблявшийся",
|
||||
"оскорбляемый",
|
||||
"оскорбляет",
|
||||
"оскорбляешь",
|
||||
"оскорблять",
|
||||
"оскорбляться",
|
||||
"оскорбляющий",
|
||||
"оскорбляющийся",
|
||||
"оскребать",
|
||||
"ослаблявший",
|
||||
"ослаблявшийся",
|
||||
"ослабляемый",
|
||||
"ослаблять",
|
||||
"ослабляться",
|
||||
"ослабляющий",
|
||||
"ослабляющийся",
|
||||
"отгребать",
|
||||
"отгребаться",
|
||||
"отскребать",
|
||||
"отхлебать",
|
||||
"перегребать",
|
||||
"перестрахуйся",
|
||||
"плохую",
|
||||
"погребать",
|
||||
"подгребание",
|
||||
"подгребать",
|
||||
"подгребаться",
|
||||
"подскребать",
|
||||
"подсоблять",
|
||||
"поколебать",
|
||||
"поколебаться",
|
||||
"послаблять",
|
||||
"пособлявший",
|
||||
"пособлять",
|
||||
"пособляющий",
|
||||
"потребление",
|
||||
"потреблявший",
|
||||
"потребляемый",
|
||||
"потребляет",
|
||||
"потребляла",
|
||||
"потреблялась",
|
||||
"потреблять",
|
||||
"потребляться",
|
||||
"потребляют",
|
||||
"потребляющий",
|
||||
"потребует",
|
||||
"потребуется",
|
||||
"похлебать",
|
||||
"прибалт",
|
||||
"прибалтика",
|
||||
"пригребать",
|
||||
"пригребаться",
|
||||
"приспособлявший",
|
||||
"приспособлявшийся",
|
||||
"приспособляемость",
|
||||
"приспособляемый",
|
||||
"приспособлялся",
|
||||
"приспособлять",
|
||||
"приспособляться",
|
||||
"приспособляющий",
|
||||
"приспособляющийся",
|
||||
"прогребать",
|
||||
"проскребать",
|
||||
"разгребать",
|
||||
"разгребаться",
|
||||
"раздроблявший",
|
||||
"раздроблявшийся",
|
||||
"раздроблять",
|
||||
"раздробляться",
|
||||
"раздробляющий",
|
||||
"раздробляющийся",
|
||||
"разлюблять",
|
||||
"расподобляться",
|
||||
"расслаблявший",
|
||||
"расслаблявшийся",
|
||||
"расслаблять",
|
||||
"расслабляться",
|
||||
"расслабляющий",
|
||||
"расслабляющийся",
|
||||
"расхлебать",
|
||||
"ребаланс",
|
||||
"ребут",
|
||||
"республика",
|
||||
"сабля",
|
||||
"сабляница",
|
||||
"сгребать",
|
||||
"сгребаться",
|
||||
"сиагейт",
|
||||
"сиагейты",
|
||||
"скоблящий",
|
||||
"скоблящийся",
|
||||
"скорблящий",
|
||||
"скребу",
|
||||
"скребут",
|
||||
"слюбляться",
|
||||
"соскребать",
|
||||
"сподоблять",
|
||||
"сподобляться",
|
||||
"стебать",
|
||||
"стебля",
|
||||
"схлебать",
|
||||
"твар",
|
||||
"требование",
|
||||
"требовать",
|
||||
"требуем",
|
||||
"требует",
|
||||
"требуется",
|
||||
"требуха",
|
||||
"требую",
|
||||
"требуют",
|
||||
"углублявший",
|
||||
"углублявшийся",
|
||||
"углублять",
|
||||
"углубляться",
|
||||
"углубляющий",
|
||||
"угребать",
|
||||
"уподоблявший",
|
||||
"уподоблявшийся",
|
||||
"уподоблять",
|
||||
"уподобляться",
|
||||
"уподобляющий",
|
||||
"уподобляющийся",
|
||||
"употребление",
|
||||
"употреблявший",
|
||||
"употреблявшийся",
|
||||
"употребляемый",
|
||||
"употреблять",
|
||||
"употребляться",
|
||||
"употребляю",
|
||||
"употребляющий",
|
||||
"употребляющийся",
|
||||
"усугублявший",
|
||||
"усугублявшийся",
|
||||
"усугубляемый",
|
||||
"усугублять",
|
||||
"усугубляться",
|
||||
"усугубляющий",
|
||||
"усугубляющийся",
|
||||
"ущерблять",
|
||||
"ущербляться",
|
||||
"херметичность",
|
||||
"хлебать",
|
||||
"чебурнет",
|
||||
"шоудаун",
|
||||
"эчпочмак"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"thank_words": [
|
||||
"спасибо",
|
||||
"благодарю",
|
||||
"спс",
|
||||
"сенкс",
|
||||
"сенкью",
|
||||
"thanks",
|
||||
"thank you",
|
||||
"thx",
|
||||
"ty",
|
||||
"дякую",
|
||||
"дзякуй",
|
||||
"рахмет",
|
||||
"пасиб",
|
||||
"пасибо",
|
||||
"спасибочки",
|
||||
"благодарочка",
|
||||
"мерси",
|
||||
"merci",
|
||||
"danke",
|
||||
"gracias",
|
||||
"grazie"
|
||||
]
|
||||
}
|
||||
+540
-4
@@ -1,5 +1,6 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import time
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import logging
|
||||
@@ -13,7 +14,7 @@ class Database: # Инициализация класса
|
||||
self.db_name = db_name
|
||||
self._init_db()
|
||||
|
||||
# Инициализирует базу данных и создает таблицу, если она не существует
|
||||
# Инициализирует базу данных и создает таблицы, если они не существуют
|
||||
def _init_db(self):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
@@ -24,7 +25,112 @@ class Database: # Инициализация класса
|
||||
tag TEXT
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS violations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
chat_id INTEGER NOT NULL,
|
||||
violation_date INTEGER NOT NULL,
|
||||
violation_type TEXT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS warnings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
chat_id INTEGER NOT NULL,
|
||||
warn_date INTEGER NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
admin_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS karma (
|
||||
user_id INTEGER NOT NULL,
|
||||
chat_id INTEGER NOT NULL,
|
||||
karma_points INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (user_id, chat_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS karma_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
from_user_id INTEGER NOT NULL,
|
||||
to_user_id INTEGER NOT NULL,
|
||||
chat_id INTEGER NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
FOREIGN KEY (from_user_id) REFERENCES users (id),
|
||||
FOREIGN KEY (to_user_id) REFERENCES users (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Создаём индексы для оптимизации часто используемых запросов
|
||||
# Индекс для проверки нарушений пользователя в чате за период
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_violations_user_chat_date
|
||||
ON violations(user_id, chat_id, violation_date)
|
||||
''')
|
||||
|
||||
# Индекс для проверки предупреждений пользователя в чате за период
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_warnings_user_chat_date
|
||||
ON warnings(user_id, chat_id, warn_date)
|
||||
''')
|
||||
|
||||
# Индекс для проверки истории кармы (кулдаун благодарностей)
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_karma_history_cooldown
|
||||
ON karma_history(from_user_id, to_user_id, chat_id, timestamp)
|
||||
''')
|
||||
|
||||
# Индекс для поиска пользователя по username
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_users_tag
|
||||
ON users(tag COLLATE NOCASE)
|
||||
''')
|
||||
|
||||
# Таблица для кэша сообщений (для обработки реакций)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS message_cache (
|
||||
chat_id INTEGER NOT NULL,
|
||||
message_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
thread_id INTEGER,
|
||||
timestamp INTEGER NOT NULL,
|
||||
PRIMARY KEY (chat_id, message_id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Индекс для быстрой очистки старых записей
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_message_cache_timestamp
|
||||
ON message_cache(timestamp)
|
||||
''')
|
||||
|
||||
# Таблица для отключенных топиков (где не начисляется карма)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS disabled_karma_topics (
|
||||
chat_id INTEGER NOT NULL,
|
||||
thread_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (chat_id, thread_id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Таблица настроек чата (например, guard — капча при входе)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS chat_settings (
|
||||
chat_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (chat_id, key)
|
||||
)
|
||||
''')
|
||||
|
||||
connect.commit()
|
||||
logger.info("База данных и индексы успешно инициализированы")
|
||||
|
||||
# Возвращает соединение с базой данных
|
||||
def _get_connection(self):
|
||||
@@ -70,15 +176,445 @@ class Database: # Инициализация класса
|
||||
def get_user_by_username(self, username: str) -> Optional[Tuple]:
|
||||
if not username:
|
||||
return None
|
||||
|
||||
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT id, nickname, tag
|
||||
FROM users
|
||||
SELECT id, nickname, tag
|
||||
FROM users
|
||||
WHERE LOWER(tag) = LOWER(?)
|
||||
''', (username,))
|
||||
return cursor.fetchone()
|
||||
|
||||
# Добавляет нарушение в базу данных
|
||||
def add_violation(self, user_id: int, chat_id: int, violation_type: str = 'bad_language'):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO violations (user_id, chat_id, violation_date, violation_type)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (user_id, chat_id, int(time.time()), violation_type))
|
||||
connect.commit()
|
||||
logger.info(f"Нарушение пользователя {user_id} зафиксировано в чате {chat_id}")
|
||||
|
||||
# Получает количество нарушений за период (по умолчанию - за последний месяц)
|
||||
def get_violations_count(self, user_id: int, chat_id: int, period_seconds: int = 2592000) -> int:
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cutoff_time = int(time.time()) - period_seconds
|
||||
cursor.execute('''
|
||||
SELECT COUNT(*)
|
||||
FROM violations
|
||||
WHERE user_id = ? AND chat_id = ? AND violation_date > ?
|
||||
''', (user_id, chat_id, cutoff_time))
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
|
||||
# Получает все нарушения пользователя
|
||||
def get_user_violations(self, user_id: int, chat_id: int):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT id, violation_date, violation_type
|
||||
FROM violations
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
ORDER BY violation_date DESC
|
||||
''', (user_id, chat_id))
|
||||
return cursor.fetchall()
|
||||
|
||||
# Очищает старые нарушения (старше указанного периода)
|
||||
def clean_old_violations(self, period_seconds: int = 2592000):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cutoff_time = int(time.time()) - period_seconds
|
||||
cursor.execute('''
|
||||
DELETE FROM violations
|
||||
WHERE violation_date < ?
|
||||
''', (cutoff_time,))
|
||||
deleted_count = cursor.rowcount
|
||||
connect.commit()
|
||||
logger.info(f"Удалено старых нарушений: {deleted_count}")
|
||||
return deleted_count
|
||||
|
||||
# Сбрасывает все нарушения пользователя в чате
|
||||
def reset_user_violations(self, user_id: int, chat_id: int):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM violations
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
''', (user_id, chat_id))
|
||||
deleted_count = cursor.rowcount
|
||||
connect.commit()
|
||||
logger.info(f"Сброшено {deleted_count} нарушений пользователя {user_id} в чате {chat_id}")
|
||||
return deleted_count
|
||||
|
||||
# Добавляет предупреждение в базу данных
|
||||
def add_warning(self, user_id: int, chat_id: int, reason: str, admin_id: int):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO warnings (user_id, chat_id, warn_date, reason, admin_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (user_id, chat_id, int(time.time()), reason, admin_id))
|
||||
connect.commit()
|
||||
logger.info(f"Предупреждение пользователю {user_id} выдано администратором {admin_id} в чате {chat_id}")
|
||||
|
||||
# Получает количество предупреждений за период
|
||||
def get_warnings_count(self, user_id: int, chat_id: int, period_seconds: int) -> int:
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cutoff_time = int(time.time()) - period_seconds
|
||||
cursor.execute('''
|
||||
SELECT COUNT(*)
|
||||
FROM warnings
|
||||
WHERE user_id = ? AND chat_id = ? AND warn_date > ?
|
||||
''', (user_id, chat_id, cutoff_time))
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
|
||||
# Получает все предупреждения пользователя
|
||||
def get_user_warnings(self, user_id: int, chat_id: int):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT id, warn_date, reason, admin_id
|
||||
FROM warnings
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
ORDER BY warn_date DESC
|
||||
''', (user_id, chat_id))
|
||||
return cursor.fetchall()
|
||||
|
||||
# Сбрасывает все предупреждения пользователя в чате
|
||||
def reset_user_warnings(self, user_id: int, chat_id: int):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM warnings
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
''', (user_id, chat_id))
|
||||
deleted_count = cursor.rowcount
|
||||
connect.commit()
|
||||
logger.info(f"Сброшено {deleted_count} предупреждений пользователя {user_id} в чате {chat_id}")
|
||||
return deleted_count
|
||||
|
||||
# Добавляет карму пользователю
|
||||
def add_karma(self, user_id: int, chat_id: int, amount: int = 1):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
# Проверяем существование записи
|
||||
cursor.execute('SELECT karma_points FROM karma WHERE user_id = ? AND chat_id = ?', (user_id, chat_id))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
# Обновляем существующую карму
|
||||
new_karma = result[0] + amount
|
||||
cursor.execute('''
|
||||
UPDATE karma
|
||||
SET karma_points = ?
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
''', (new_karma, user_id, chat_id))
|
||||
else:
|
||||
# Создаем новую запись
|
||||
cursor.execute('''
|
||||
INSERT INTO karma (user_id, chat_id, karma_points)
|
||||
VALUES (?, ?, ?)
|
||||
''', (user_id, chat_id, amount))
|
||||
|
||||
connect.commit()
|
||||
logger.info(f"Пользователю {user_id} добавлено {amount} кармы в чате {chat_id}")
|
||||
|
||||
# Устанавливает карму пользователя в указанное значение
|
||||
def set_karma(self, user_id: int, chat_id: int, karma_value: int):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
# Проверяем существование записи
|
||||
cursor.execute('SELECT karma_points FROM karma WHERE user_id = ? AND chat_id = ?', (user_id, chat_id))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
# Обновляем существующую карму
|
||||
cursor.execute('''
|
||||
UPDATE karma
|
||||
SET karma_points = ?
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
''', (karma_value, user_id, chat_id))
|
||||
else:
|
||||
# Создаем новую запись
|
||||
cursor.execute('''
|
||||
INSERT INTO karma (user_id, chat_id, karma_points)
|
||||
VALUES (?, ?, ?)
|
||||
''', (user_id, chat_id, karma_value))
|
||||
|
||||
connect.commit()
|
||||
logger.info(f"Карма пользователя {user_id} установлена на {karma_value} в чате {chat_id}")
|
||||
|
||||
# Получает карму пользователя
|
||||
def get_karma(self, user_id: int, chat_id: int) -> int:
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT karma_points
|
||||
FROM karma
|
||||
WHERE user_id = ? AND chat_id = ?
|
||||
''', (user_id, chat_id))
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
|
||||
# Получает топ пользователей по карме
|
||||
def get_top_karma(self, chat_id: int, limit: int = 10):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT k.user_id, u.nickname, u.tag, k.karma_points
|
||||
FROM karma k
|
||||
JOIN users u ON k.user_id = u.id
|
||||
WHERE k.chat_id = ? AND k.karma_points > 0
|
||||
ORDER BY k.karma_points DESC
|
||||
LIMIT ?
|
||||
''', (chat_id, limit))
|
||||
return cursor.fetchall()
|
||||
|
||||
# Атомарно проверяет кулдаун и добавляет запись в историю кармы
|
||||
# Возвращает True если благодарность засчитана, False если кулдаун не прошёл
|
||||
def try_add_karma_thank(self, from_user_id: int, to_user_id: int, chat_id: int, cooldown_seconds: int = 3600) -> bool:
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
current_time = int(time.time())
|
||||
cutoff_time = current_time - cooldown_seconds
|
||||
|
||||
# Проверяем кулдаун и добавляем запись в одной транзакции
|
||||
# Это предотвращает race condition при параллельных запросах
|
||||
cursor.execute('''
|
||||
SELECT COUNT(*)
|
||||
FROM karma_history
|
||||
WHERE from_user_id = ? AND to_user_id = ? AND chat_id = ? AND timestamp > ?
|
||||
''', (from_user_id, to_user_id, chat_id, cutoff_time))
|
||||
result = cursor.fetchone()
|
||||
|
||||
# Если кулдаун не прошёл, возвращаем False
|
||||
if result and result[0] > 0:
|
||||
return False
|
||||
|
||||
# Кулдаун прошёл, добавляем запись в той же транзакции
|
||||
cursor.execute('''
|
||||
INSERT INTO karma_history (from_user_id, to_user_id, chat_id, timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (from_user_id, to_user_id, chat_id, current_time))
|
||||
|
||||
connect.commit()
|
||||
logger.info(f"Пользователь {from_user_id} поблагодарил {to_user_id} в чате {chat_id}")
|
||||
return True
|
||||
|
||||
# УСТАРЕВШИЙ МЕТОД - оставлен для обратной совместимости
|
||||
# Используйте try_add_karma_thank() для атомарной операции без race condition
|
||||
def can_thank(self, from_user_id: int, to_user_id: int, chat_id: int, cooldown_seconds: int = 3600) -> bool:
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cutoff_time = int(time.time()) - cooldown_seconds
|
||||
cursor.execute('''
|
||||
SELECT COUNT(*)
|
||||
FROM karma_history
|
||||
WHERE from_user_id = ? AND to_user_id = ? AND chat_id = ? AND timestamp > ?
|
||||
''', (from_user_id, to_user_id, chat_id, cutoff_time))
|
||||
result = cursor.fetchone()
|
||||
return result[0] == 0 if result else True
|
||||
|
||||
# УСТАРЕВШИЙ МЕТОД - оставлен для обратной совместимости
|
||||
# Используйте try_add_karma_thank() для атомарной операции без race condition
|
||||
def add_karma_history(self, from_user_id: int, to_user_id: int, chat_id: int):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO karma_history (from_user_id, to_user_id, chat_id, timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (from_user_id, to_user_id, chat_id, int(time.time())))
|
||||
connect.commit()
|
||||
logger.info(f"Пользователь {from_user_id} поблагодарил {to_user_id} в чате {chat_id}")
|
||||
|
||||
# Добавляет сообщение в кэш
|
||||
def cache_message(self, chat_id: int, message_id: int, user_id: int, thread_id: Optional[int] = None):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
current_time = int(time.time())
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO message_cache (chat_id, message_id, user_id, thread_id, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (chat_id, message_id, user_id, thread_id, current_time))
|
||||
connect.commit()
|
||||
|
||||
# Получает информацию о сообщении из кэша
|
||||
# Возвращает (user_id, thread_id) или None если не найдено
|
||||
def get_cached_message(self, chat_id: int, message_id: int) -> Optional[Tuple[int, Optional[int]]]:
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT user_id, thread_id
|
||||
FROM message_cache
|
||||
WHERE chat_id = ? AND message_id = ?
|
||||
''', (chat_id, message_id))
|
||||
result = cursor.fetchone()
|
||||
return result if result else None
|
||||
|
||||
# Очищает сообщения старше указанного времени (по умолчанию 24 часа)
|
||||
def cleanup_old_messages(self, max_age_seconds: int = 86400):
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cutoff_time = int(time.time()) - max_age_seconds
|
||||
cursor.execute('''
|
||||
DELETE FROM message_cache
|
||||
WHERE timestamp < ?
|
||||
''', (cutoff_time,))
|
||||
deleted_count = cursor.rowcount
|
||||
connect.commit()
|
||||
if deleted_count > 0:
|
||||
logger.info(f"Удалено {deleted_count} старых сообщений из кэша")
|
||||
return deleted_count
|
||||
|
||||
# Получает количество сообщений в кэше
|
||||
def get_cache_size(self) -> int:
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('SELECT COUNT(*) FROM message_cache')
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
|
||||
# Проверяет, отключена ли карма в топике
|
||||
def is_karma_disabled_in_topic(self, chat_id: int, thread_id: Optional[int]) -> bool:
|
||||
"""
|
||||
Проверяет, отключена ли карма в данном топике.
|
||||
|
||||
Args:
|
||||
chat_id: ID чата
|
||||
thread_id: ID топика (None для главного чата)
|
||||
|
||||
Returns:
|
||||
True если карма отключена, иначе False
|
||||
"""
|
||||
# Если thread_id None, считаем что это главный чат (карма всегда включена)
|
||||
if thread_id is None:
|
||||
return False
|
||||
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT 1 FROM disabled_karma_topics
|
||||
WHERE chat_id = ? AND thread_id = ?
|
||||
''', (chat_id, thread_id))
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
# Отключает карму в топике
|
||||
def disable_karma_in_topic(self, chat_id: int, thread_id: int) -> bool:
|
||||
"""
|
||||
Отключает начисление кармы в данном топике.
|
||||
|
||||
Args:
|
||||
chat_id: ID чата
|
||||
thread_id: ID топика
|
||||
|
||||
Returns:
|
||||
True если успешно, False если уже было отключено
|
||||
"""
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
|
||||
# Проверяем, не отключено ли уже
|
||||
if self.is_karma_disabled_in_topic(chat_id, thread_id):
|
||||
return False
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO disabled_karma_topics (chat_id, thread_id)
|
||||
VALUES (?, ?)
|
||||
''', (chat_id, thread_id))
|
||||
connect.commit()
|
||||
logger.info(f"Карма отключена в топике {thread_id} чата {chat_id}")
|
||||
return True
|
||||
|
||||
# Включает карму в топике
|
||||
def enable_karma_in_topic(self, chat_id: int, thread_id: int) -> bool:
|
||||
"""
|
||||
Включает начисление кармы в данном топике.
|
||||
|
||||
Args:
|
||||
chat_id: ID чата
|
||||
thread_id: ID топика
|
||||
|
||||
Returns:
|
||||
True если успешно, False если уже было включено
|
||||
"""
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM disabled_karma_topics
|
||||
WHERE chat_id = ? AND thread_id = ?
|
||||
''', (chat_id, thread_id))
|
||||
deleted = cursor.rowcount > 0
|
||||
connect.commit()
|
||||
if deleted:
|
||||
logger.info(f"Карма включена в топике {thread_id} чата {chat_id}")
|
||||
return deleted
|
||||
|
||||
# Получает список отключенных топиков для чата
|
||||
def get_disabled_topics(self, chat_id: int) -> list:
|
||||
"""
|
||||
Возвращает список ID топиков, где отключена карма.
|
||||
|
||||
Args:
|
||||
chat_id: ID чата
|
||||
|
||||
Returns:
|
||||
Список ID топиков
|
||||
"""
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT thread_id FROM disabled_karma_topics
|
||||
WHERE chat_id = ?
|
||||
''', (chat_id,))
|
||||
return [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Получает настройку чата по ключу
|
||||
def get_chat_setting(self, chat_id: int, key: str, default: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Возвращает значение настройки чата или default, если не задано.
|
||||
"""
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
SELECT value FROM chat_settings
|
||||
WHERE chat_id = ? AND key = ?
|
||||
''', (chat_id, key))
|
||||
row = cursor.fetchone()
|
||||
return row[0] if row else default
|
||||
|
||||
# Устанавливает настройку чата
|
||||
def set_chat_setting(self, chat_id: int, key: str, value: str) -> None:
|
||||
"""
|
||||
Сохраняет или обновляет настройку чата.
|
||||
"""
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO chat_settings (chat_id, key, value)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(chat_id, key) DO UPDATE SET value = excluded.value
|
||||
''', (chat_id, key, value))
|
||||
connect.commit()
|
||||
|
||||
# Удаляет настройку чата
|
||||
def delete_chat_setting(self, chat_id: int, key: str) -> bool:
|
||||
"""
|
||||
Удаляет настройку чата. Возвращает True, если запись была удалена.
|
||||
"""
|
||||
with self._get_connection() as connect:
|
||||
cursor = connect.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM chat_settings
|
||||
WHERE chat_id = ? AND key = ?
|
||||
''', (chat_id, key))
|
||||
deleted = cursor.rowcount > 0
|
||||
connect.commit()
|
||||
return deleted
|
||||
|
||||
# Создаем экземпляр базы данных для импорта в других модулях
|
||||
db = Database()
|
||||
Whitespace-only changes.
+4
-2
@@ -6,13 +6,15 @@ from config import LOG_FILE_NAME
|
||||
|
||||
class ColoredFormatter(logging.Formatter): # Цветные логи (для терминала)
|
||||
LEVEL_COLORS = {
|
||||
logging.DEBUG: '\033[96m', # Голубой для DEBUG
|
||||
logging.INFO: '\033[92m',
|
||||
logging.WARNING: '\033[93m',
|
||||
logging.ERROR: '\033[91m',
|
||||
logging.CRITICAL: '\033[91m'
|
||||
}
|
||||
|
||||
|
||||
LEVEL_NAMES = {
|
||||
logging.DEBUG: "D",
|
||||
logging.INFO: "I",
|
||||
logging.WARNING: "W",
|
||||
logging.ERROR: "E",
|
||||
@@ -50,7 +52,7 @@ def setup_logging(): # Инициализирует систему логиро
|
||||
|
||||
# Создаем корневой логгер
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.setLevel(logging.INFO) # INFO для продакшена
|
||||
|
||||
# Проверяем, не настроен ли логгер ранее
|
||||
if not logger.hasHandlers():
|
||||
|
||||
+109
-14
@@ -18,25 +18,60 @@ from config import MODULES_DIR
|
||||
|
||||
# Загружаем токен бота из .env
|
||||
load_dotenv()
|
||||
|
||||
# Валидация переменных окружения
|
||||
def validate_env_vars():
|
||||
"""Проверяет наличие всех необходимых переменных окружения"""
|
||||
required_vars = {
|
||||
"BOT_TOKEN": "Токен бота",
|
||||
"ADMIN_CHAT_ID": "ID админ-чата",
|
||||
"LOG_THREAD_ID": "ID топика для логов"
|
||||
}
|
||||
|
||||
missing_vars = []
|
||||
for var_name, description in required_vars.items():
|
||||
value = os.getenv(var_name)
|
||||
if not value or value.strip() == "" or value == "...":
|
||||
missing_vars.append(f"{var_name} ({description})")
|
||||
|
||||
if missing_vars:
|
||||
print("\n❌ ОШИБКА: Не заполнены необходимые переменные окружения в файле .env:")
|
||||
for var in missing_vars:
|
||||
print(f" • {var}")
|
||||
print("\nПожалуйста, заполните файл .env на основе .env.example")
|
||||
sys.exit(1)
|
||||
|
||||
# Проверяем переменные окружения
|
||||
validate_env_vars()
|
||||
|
||||
bot = AsyncTeleBot(os.getenv("BOT_TOKEN"), parse_mode="html")
|
||||
|
||||
# Загружаем ID админ-чата из .env и инициализируемся для логов в чат
|
||||
init_action_reporter(bot, os.getenv("ADMIN_CHAT_ID"), os.getenv("LOG_THREAD_ID"))
|
||||
init_action_reporter(bot, int(os.getenv("ADMIN_CHAT_ID")), int(os.getenv("LOG_THREAD_ID")))
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Middleware для автоматического обновления информации о пользователях в базе данных
|
||||
# И проверки на нецензурную лексику (выполняется ДО всех обработчиков)
|
||||
class UserUpdateMiddleware(BaseMiddleware):
|
||||
def __init__(self, db):
|
||||
def __init__(self, db, bot):
|
||||
super().__init__()
|
||||
# message - все обычные сообщения
|
||||
# chat_member - события изменения статуса участников чата
|
||||
self.update_types = ['message', 'chat_member']
|
||||
self.update_types = ['message', 'chat_member', 'callback_query']
|
||||
self.db = db
|
||||
|
||||
self.bot = bot
|
||||
|
||||
# Обработчик, вызываемый ДО обработки сообщения основными хэндлерами
|
||||
async def pre_process(self, message, data):
|
||||
# Проверяем, что это действительно сообщение (а не ChatMemberUpdated)
|
||||
if not hasattr(message, 'content_type'):
|
||||
# Это не Message объект (например ChatMemberUpdated), пропускаем
|
||||
return data
|
||||
|
||||
# Логируем ВСЕ входящие сообщения для отладки
|
||||
logger.info(f"[MIDDLEWARE] Получено сообщение от {message.from_user.id}, тип: {message.content_type}, текст: {message.text if hasattr(message, 'text') else 'N/A'}")
|
||||
|
||||
# Обработка пользователей, отправившие сообщение
|
||||
if message.content_type == 'text':
|
||||
@@ -45,9 +80,25 @@ class UserUpdateMiddleware(BaseMiddleware):
|
||||
nickname=message.from_user.first_name,
|
||||
tag=message.from_user.username
|
||||
)
|
||||
|
||||
|
||||
# ВАЖНО: Проверяем на мат ДО передачи другим обработчикам
|
||||
# Это позволяет auto_mute работать независимо от karma_tracker
|
||||
await self._check_profanity(message)
|
||||
|
||||
# ВАЖНО: Кэшируем ВСЕ сообщения для обработки реакций (не только текстовые!)
|
||||
# Пользователи могут ставить реакции на фото, видео, стикеры и т.д.
|
||||
try:
|
||||
karma_module = importlib.import_module("modules.0_karma_tracker")
|
||||
if message.chat.type in ['group', 'supergroup']:
|
||||
# Передаём message_thread_id для правильной отправки уведомлений в топики
|
||||
thread_id = getattr(message, 'message_thread_id', None)
|
||||
karma_module._cache_message(message.chat.id, message.message_id, message.from_user.id, thread_id)
|
||||
logger.info(f"[CACHE] Сообщение {message.message_id} от {message.from_user.id} добавлено в кэш, chat_id={message.chat.id}, thread_id={thread_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка кэширования сообщения: {e}", exc_info=True)
|
||||
|
||||
# Обработка новых участников группы
|
||||
elif message.content_type == 'new_chat_members':
|
||||
if message.content_type == 'new_chat_members':
|
||||
for new_member in message.new_chat_members:
|
||||
self.db.add_or_update_user(
|
||||
user_id=new_member.id,
|
||||
@@ -55,20 +106,39 @@ class UserUpdateMiddleware(BaseMiddleware):
|
||||
tag=new_member.username
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
# Проверка на нецензурную лексику (вызывается в middleware)
|
||||
async def _check_profanity(self, message):
|
||||
"""Проверяет сообщение на мат и применяет мут если нужно"""
|
||||
from bad_words import contains_bad_word
|
||||
|
||||
# Только для групповых чатов и не команды
|
||||
if message.chat.type not in ['group', 'supergroup']:
|
||||
return
|
||||
|
||||
if not message.text or message.text.startswith('/'):
|
||||
return
|
||||
|
||||
# Проверяем наличие мата
|
||||
if not contains_bad_word(message.text):
|
||||
return
|
||||
|
||||
# Импортируем функцию проверки из auto_mute (если модуль загружен)
|
||||
try:
|
||||
from modules.auto_mute import check_message_for_profanity
|
||||
await check_message_for_profanity(self.bot, message)
|
||||
except ImportError:
|
||||
logger.warning("Модуль auto_mute не загружен, пропускаем проверку мата")
|
||||
|
||||
# Обработчик, вызываемый ПОСЛЕ обработки сообщения основными хэндлерами
|
||||
async def post_process(self, message, data, exception):
|
||||
pass
|
||||
|
||||
# Регистрируем middleware
|
||||
bot.setup_middleware(UserUpdateMiddleware(db))
|
||||
bot.setup_middleware(UserUpdateMiddleware(db, bot))
|
||||
|
||||
# Загружает все модули из директории /modules
|
||||
async def load_modules():
|
||||
|
||||
# Инициализация логирования
|
||||
setup_logging()
|
||||
|
||||
# Переменная для подсчёта модулей
|
||||
loaded_count = 0
|
||||
|
||||
@@ -109,7 +179,28 @@ async def load_modules():
|
||||
# Записываем отчет о модулях в логи
|
||||
logger.info(f"Загружено модулей: {loaded_count} шт. Бот запущен.")
|
||||
|
||||
# Устанавливаем меню команд бота
|
||||
async def setup_bot_commands():
|
||||
from telebot.types import BotCommand
|
||||
|
||||
commands = [
|
||||
BotCommand("start", "Начало работы с ботом"),
|
||||
BotCommand("help", "Справка по всем командам"),
|
||||
BotCommand("log", "Инструкция по созданию лога ошибки"),
|
||||
BotCommand("karma", "Карма: top, set, disable, enable, status"),
|
||||
BotCommand("warn", "Предупреждение: help, reset"),
|
||||
BotCommand("mute", "Мут: help, off, reset"),
|
||||
BotCommand("ban", "Бан: help, off"),
|
||||
BotCommand("guard", "Капча для новых участников"),
|
||||
BotCommand("badwords", "Управление бранными словами: help"),
|
||||
]
|
||||
|
||||
await bot.set_my_commands(commands)
|
||||
logger.info("Команды бота успешно установлены.")
|
||||
|
||||
async def main():
|
||||
# Инициализация логирования (должна быть первой)
|
||||
setup_logging()
|
||||
|
||||
# Очищаем терминал
|
||||
os.system('clear')
|
||||
@@ -119,8 +210,12 @@ async def main():
|
||||
# Проверяем и загружаем модули
|
||||
await load_modules()
|
||||
|
||||
# Запускаем бота
|
||||
await bot.infinity_polling()
|
||||
# Устанавливаем команды бота
|
||||
await setup_bot_commands()
|
||||
|
||||
# Запускаем бота с обработкой реакций и нажатий на кнопки капчи
|
||||
logger.info("Запуск бота с allowed_updates: message, message_reaction, chat_member, callback_query")
|
||||
await bot.infinity_polling(allowed_updates=['message', 'message_reaction', 'chat_member', 'callback_query'])
|
||||
|
||||
except Exception as e:
|
||||
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, MessageReactionUpdated, ReactionTypeEmoji
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from database import db
|
||||
from thank_words import contains_thank_word
|
||||
from bad_words import contains_bad_word
|
||||
from config import THANK_COOLDOWN
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Фоновая задача для автоочистки старых сообщений
|
||||
_cleanup_task = None
|
||||
|
||||
def _cache_message(chat_id: int, message_id: int, user_id: int, message_thread_id: int = None):
|
||||
"""Добавляет сообщение в кэш БД"""
|
||||
db.cache_message(chat_id, message_id, user_id, message_thread_id)
|
||||
|
||||
def _get_cached_message(chat_id: int, message_id: int):
|
||||
"""Получает (user_id, message_thread_id) из кэша БД"""
|
||||
return db.get_cached_message(chat_id, message_id)
|
||||
|
||||
async def _cleanup_old_cache():
|
||||
"""Фоновая задача для очистки старых сообщений из кэша каждые 6 часов"""
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(21600) # Ждём 6 часов
|
||||
deleted = db.cleanup_old_messages(max_age_seconds=604800) # Удаляем старше 7 дней
|
||||
cache_size = db.get_cache_size()
|
||||
logger.info(f"[CACHE CLEANUP] Удалено: {deleted}, размер кэша: {cache_size} сообщений")
|
||||
except Exception as e:
|
||||
logger.error(f"[CACHE CLEANUP] Ошибка очистки кэша: {e}", exc_info=True)
|
||||
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
"""Регистрирует обработчики для отслеживания благодарностей"""
|
||||
logger.info("Регистрация обработчика благодарностей (karma_tracker)")
|
||||
|
||||
# Запускаем фоновую задачу очистки старых сообщений из кэша
|
||||
global _cleanup_task
|
||||
if _cleanup_task is None or _cleanup_task.done():
|
||||
_cleanup_task = asyncio.create_task(_cleanup_old_cache())
|
||||
cache_size = db.get_cache_size()
|
||||
logger.info(f"[CACHE] Запущена автоочистка кэша. Текущий размер: {cache_size} сообщений")
|
||||
|
||||
@bot.message_reaction_handler(func=lambda m: True)
|
||||
async def handle_reaction(reaction: MessageReactionUpdated):
|
||||
"""
|
||||
Обрабатывает реакции на сообщения.
|
||||
Реакции работают как переключатель:
|
||||
- Поставил 👍 → +1 карма | Убрал 👍 → -1 карма
|
||||
- Поставил 👎 → -1 карма | Убрал 👎 → +1 карма
|
||||
- Поставил 🔥 → +2 кармы | Убрал 🔥 → -2 кармы
|
||||
- Поставил ❤ → +5 кармы | Убрал ❤ → -5 кармы
|
||||
- Поставил ❤🔥 → +10 кармы | Убрал ❤🔥 → -10 кармы
|
||||
- Поставил 🤡 → -20 кармы ПОСТАВИВШЕМУ (не возвращается при снятии!)
|
||||
"""
|
||||
try:
|
||||
logger.info(f"[KARMA] Получена реакция от {reaction.user.id}")
|
||||
|
||||
# Проверяем, что это групповой чат
|
||||
if reaction.chat.type not in ['group', 'supergroup']:
|
||||
logger.info(f"[KARMA] Пропуск реакции - не групповой чат")
|
||||
return
|
||||
|
||||
from_user = reaction.user
|
||||
chat_id = reaction.chat.id
|
||||
|
||||
# Получаем автора сообщения и топик из кэша
|
||||
cached_data = _get_cached_message(chat_id, reaction.message_id)
|
||||
if not cached_data:
|
||||
logger.warning(f"[KARMA] Сообщение {reaction.message_id} не найдено в кэше")
|
||||
return
|
||||
|
||||
to_user_id, message_thread_id = cached_data
|
||||
|
||||
# Проверяем, отключена ли карма в этом топике
|
||||
if db.is_karma_disabled_in_topic(chat_id, message_thread_id):
|
||||
logger.info(f"[KARMA] Пропуск реакции - карма отключена в топике {message_thread_id}")
|
||||
return
|
||||
|
||||
# Защита от самооценки
|
||||
if from_user.id == to_user_id:
|
||||
logger.info(f"Пользователь {from_user.id} попытался поставить реакцию на своё сообщение")
|
||||
return
|
||||
|
||||
# Получаем информацию о пользователе из БД
|
||||
to_user_info = db.get_user(to_user_id)
|
||||
if not to_user_info:
|
||||
logger.warning(f"[KARMA] Пользователь {to_user_id} не найден в БД")
|
||||
return
|
||||
|
||||
# Примечание: мы не проверяем является ли to_user_id ботом, т.к.:
|
||||
# 1. Сообщения ботов не кэшируются (только пользовательские)
|
||||
# 2. Если бот все же попал в кэш, это исключительный случай и не критично
|
||||
|
||||
# Проверяем старые реакции
|
||||
old_thumbs_up = False
|
||||
old_thumbs_down = False
|
||||
old_heart = False
|
||||
old_fire_heart = False
|
||||
old_fire = False
|
||||
old_clown = False
|
||||
if reaction.old_reaction:
|
||||
for react in reaction.old_reaction:
|
||||
if isinstance(react, ReactionTypeEmoji):
|
||||
if react.emoji == "👍":
|
||||
old_thumbs_up = True
|
||||
elif react.emoji == "👎":
|
||||
old_thumbs_down = True
|
||||
elif react.emoji == "❤":
|
||||
old_heart = True
|
||||
elif react.emoji == "❤🔥":
|
||||
old_fire_heart = True
|
||||
elif react.emoji == "🔥":
|
||||
old_fire = True
|
||||
elif react.emoji == "🤡":
|
||||
old_clown = True
|
||||
|
||||
# Проверяем новые реакции
|
||||
new_thumbs_up = False
|
||||
new_thumbs_down = False
|
||||
new_heart = False
|
||||
new_fire_heart = False
|
||||
new_fire = False
|
||||
new_clown = False
|
||||
if reaction.new_reaction:
|
||||
for react in reaction.new_reaction:
|
||||
if isinstance(react, ReactionTypeEmoji):
|
||||
if react.emoji == "👍":
|
||||
new_thumbs_up = True
|
||||
elif react.emoji == "👎":
|
||||
new_thumbs_down = True
|
||||
elif react.emoji == "❤":
|
||||
new_heart = True
|
||||
elif react.emoji == "❤🔥":
|
||||
new_fire_heart = True
|
||||
elif react.emoji == "🔥":
|
||||
new_fire = True
|
||||
elif react.emoji == "🤡":
|
||||
new_clown = True
|
||||
|
||||
# Определяем изменение кармы (накапливаем все изменения)
|
||||
karma_change = 0
|
||||
actions = [] # Список всех действий для логирования
|
||||
|
||||
# Логика изменения кармы - проверяем ВСЕ реакции (не elif!)
|
||||
# Это важно, т.к. пользователь может менять реакции (убрать 👍 и поставить 🔥)
|
||||
|
||||
# Проверяем 👍
|
||||
if new_thumbs_up and not old_thumbs_up:
|
||||
karma_change += 1
|
||||
actions.append("поставил 👍 (+1)")
|
||||
elif old_thumbs_up and not new_thumbs_up:
|
||||
karma_change -= 1
|
||||
actions.append("убрал 👍 (-1)")
|
||||
|
||||
# Проверяем 👎
|
||||
if new_thumbs_down and not old_thumbs_down:
|
||||
karma_change -= 1
|
||||
actions.append("поставил 👎 (-1)")
|
||||
elif old_thumbs_down and not new_thumbs_down:
|
||||
karma_change += 1
|
||||
actions.append("убрал 👎 (+1)")
|
||||
|
||||
# Проверяем ❤
|
||||
if new_heart and not old_heart:
|
||||
karma_change += 5
|
||||
actions.append("поставил ❤ (+5)")
|
||||
elif old_heart and not new_heart:
|
||||
karma_change -= 5
|
||||
actions.append("убрал ❤ (-5)")
|
||||
|
||||
# Проверяем ❤🔥
|
||||
if new_fire_heart and not old_fire_heart:
|
||||
karma_change += 10
|
||||
actions.append("поставил ❤🔥 (+10)")
|
||||
elif old_fire_heart and not new_fire_heart:
|
||||
karma_change -= 10
|
||||
actions.append("убрал ❤🔥 (-10)")
|
||||
|
||||
# Проверяем 🔥
|
||||
if new_fire and not old_fire:
|
||||
karma_change += 2
|
||||
actions.append("поставил 🔥 (+2)")
|
||||
elif old_fire and not new_fire:
|
||||
karma_change -= 2
|
||||
actions.append("убрал 🔥 (-2)")
|
||||
|
||||
# Проверяем 🤡 (ОСОБАЯ ЛОГИКА: минусует карму поставившему!)
|
||||
clown_penalty = 0
|
||||
if new_clown and not old_clown:
|
||||
# Поставил клоуна → минус 20 кармы ПОСТАВИВШЕМУ
|
||||
clown_penalty = -20
|
||||
logger.info(f"[KARMA] Пользователь {from_user.id} поставил клоуна → -20 кармы ему самому")
|
||||
# При снятии клоуна ничего не делаем (карма не возвращается)
|
||||
|
||||
# Если нет изменений - выходим
|
||||
if karma_change == 0 and clown_penalty == 0:
|
||||
logger.info(f"[KARMA] Нет изменений в реакциях")
|
||||
return
|
||||
|
||||
# Формируем текст действий для логирования
|
||||
action_text = ", ".join(actions) if actions else ""
|
||||
logger.info(f"[KARMA] {action_text} от {from_user.id} для {to_user_id}, итоговое изменение кармы: {karma_change}")
|
||||
|
||||
# Изменяем карму получателю (если есть изменения)
|
||||
if karma_change != 0:
|
||||
db.add_karma(to_user_id, chat_id, karma_change)
|
||||
|
||||
# Применяем штраф за клоуна к ПОСТАВИВШЕМУ
|
||||
if clown_penalty != 0:
|
||||
db.add_karma(from_user.id, chat_id, clown_penalty)
|
||||
clown_karma = db.get_karma(from_user.id, chat_id)
|
||||
|
||||
# Формируем имя поставившего
|
||||
from_user_name = from_user.first_name
|
||||
from_user_display = f"@{from_user.username}" if from_user.username else from_user_name
|
||||
|
||||
# Отправляем уведомление о штрафе за клоуна
|
||||
clown_message = f"🤡 {from_user_display} поставил клоуна и получил -20 кармы! Текущая карма: {clown_karma}"
|
||||
try:
|
||||
sent_clown_msg = await bot.send_message(
|
||||
chat_id,
|
||||
clown_message,
|
||||
message_thread_id=message_thread_id
|
||||
)
|
||||
logger.info(f"[KARMA] Уведомление о клоуне отправлено")
|
||||
|
||||
# Удаляем уведомление через 15 секунд
|
||||
async def delete_clown_notification():
|
||||
try:
|
||||
await asyncio.sleep(15)
|
||||
await bot.delete_message(chat_id, sent_clown_msg.message_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось удалить уведомление о клоуне: {e}")
|
||||
|
||||
asyncio.create_task(delete_clown_notification())
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о клоуне: {e}", exc_info=True)
|
||||
|
||||
# Если нет обычных изменений кармы - выходим (клоун уже обработан)
|
||||
if karma_change == 0:
|
||||
return
|
||||
|
||||
# Получаем новую карму получателя
|
||||
new_karma = db.get_karma(to_user_id, chat_id)
|
||||
|
||||
# Формируем имя пользователя (из БД: id, nickname, tag)
|
||||
to_user_display = f"@{to_user_info[2]}" if to_user_info[2] else to_user_info[1]
|
||||
|
||||
# Формируем эмодзи для уведомления (берем первое действие или дефолтное)
|
||||
notification_emoji = "⭐"
|
||||
if "👍" in action_text:
|
||||
notification_emoji = "👍"
|
||||
elif "👎" in action_text:
|
||||
notification_emoji = "👎"
|
||||
elif "🔥" in action_text and "❤🔥" not in action_text:
|
||||
notification_emoji = "🔥"
|
||||
elif "❤🔥" in action_text:
|
||||
notification_emoji = "❤🔥"
|
||||
elif "❤" in action_text:
|
||||
notification_emoji = "❤"
|
||||
|
||||
# Отправляем уведомление
|
||||
karma_sign = f"+{karma_change}" if karma_change > 0 else str(karma_change)
|
||||
change_word = "увеличена" if karma_change > 0 else "уменьшена"
|
||||
response = f"{notification_emoji} Карма пользователя {to_user_display} {change_word} ({karma_sign})! Текущая карма: {new_karma}"
|
||||
logger.info(f"[KARMA] Отправка уведомления в чат {chat_id}")
|
||||
|
||||
try:
|
||||
sent_message = await bot.send_message(
|
||||
chat_id,
|
||||
response,
|
||||
message_thread_id=message_thread_id
|
||||
)
|
||||
logger.info(f"[KARMA] Уведомление отправлено успешно, message_id={sent_message.message_id}")
|
||||
|
||||
# Удаляем уведомление через 10 секунд В ФОНЕ (не блокируя обработку других реакций)
|
||||
async def delete_notification():
|
||||
try:
|
||||
await asyncio.sleep(10)
|
||||
await bot.delete_message(chat_id, sent_message.message_id)
|
||||
logger.info(f"[KARMA] Уведомление удалено")
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось удалить уведомление о карме: {e}")
|
||||
|
||||
# Запускаем удаление в фоне
|
||||
asyncio.create_task(delete_notification())
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о карме: {e}", exc_info=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке реакции: {e}", exc_info=True)
|
||||
|
||||
@bot.message_handler(func=lambda message: message.reply_to_message is not None and message.text and not message.text.startswith('/'))
|
||||
async def handle_thank_message(message: Message):
|
||||
"""
|
||||
Обрабатывает сообщения, которые являются ответами на другие сообщения.
|
||||
Если сообщение содержит благодарность, начисляет карму автору оригинального сообщения.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"[KARMA] Получено reply-сообщение: {message.text[:50]}")
|
||||
|
||||
# Проверяем, что это групповой чат
|
||||
if message.chat.type not in ['group', 'supergroup']:
|
||||
logger.info(f"[KARMA] Пропуск - не групповой чат: {message.chat.type}")
|
||||
return
|
||||
|
||||
# Проверяем, отключена ли карма в этом топике
|
||||
if db.is_karma_disabled_in_topic(message.chat.id, message.message_thread_id):
|
||||
logger.info(f"[KARMA] Пропуск благодарности - карма отключена в топике {message.message_thread_id}")
|
||||
return
|
||||
|
||||
# ВАЖНО: В топиках каждое сообщение технически является reply на первое сообщение топика
|
||||
# Проверяем, что это реальный reply на сообщение пользователя, а не просто сообщение в топике
|
||||
if message.is_topic_message and message.reply_to_message.message_id == message.message_thread_id:
|
||||
logger.info(f"[KARMA] Пропуск - это сообщение в топике (не reply на пользователя)")
|
||||
return
|
||||
|
||||
# Проверяем наличие благодарственных слов
|
||||
if not contains_thank_word(message.text):
|
||||
logger.info(f"[KARMA] Нет слов благодарности в: {message.text[:50]}")
|
||||
return
|
||||
|
||||
logger.info(f"[KARMA] Обнаружена благодарность от {message.from_user.id}: {message.text[:50]}")
|
||||
|
||||
# Проверяем, что в сообщении нет мата (не начисляем карму за мат)
|
||||
if contains_bad_word(message.text):
|
||||
logger.info(f"Пользователь {message.from_user.id} написал благодарность с матом - карма не начислена")
|
||||
return
|
||||
|
||||
from_user = message.from_user
|
||||
to_user = message.reply_to_message.from_user
|
||||
chat_id = message.chat.id
|
||||
|
||||
# Защита от самоблагодарности
|
||||
if from_user.id == to_user.id:
|
||||
logger.info(f"Пользователь {from_user.id} попытался поблагодарить сам себя")
|
||||
return
|
||||
|
||||
# Проверяем, не является ли благодарность ботам
|
||||
if to_user.is_bot:
|
||||
logger.info(f"Пользователь {from_user.id} попытался поблагодарить бота")
|
||||
return
|
||||
|
||||
# Атомарно проверяем кулдаун и записываем благодарность
|
||||
# Это предотвращает race condition при параллельных запросах
|
||||
if not db.try_add_karma_thank(from_user.id, to_user.id, chat_id, THANK_COOLDOWN):
|
||||
logger.info(f"Пользователь {from_user.id} уже благодарил {to_user.id} недавно")
|
||||
# Молча игнорируем, чтобы не спамить
|
||||
return
|
||||
|
||||
# Определяем количество кармы: x2 если есть восклицательный знак
|
||||
karma_amount = 2 if '!' in message.text else 1
|
||||
|
||||
# Начисляем карму (благодарность уже записана атомарно выше)
|
||||
db.add_karma(to_user.id, chat_id, karma_amount)
|
||||
|
||||
# Получаем новую карму пользователя
|
||||
new_karma = db.get_karma(to_user.id, chat_id)
|
||||
|
||||
# Формируем имя пользователя для отображения
|
||||
to_user_name = to_user.first_name
|
||||
if to_user.username:
|
||||
to_user_display = f"@{to_user.username}"
|
||||
else:
|
||||
to_user_display = to_user_name
|
||||
|
||||
# Отправляем уведомление с указанием количества кармы
|
||||
karma_emoji = "👍👍" if karma_amount == 2 else "👍"
|
||||
karma_change = f"+{karma_amount}"
|
||||
response = f"{karma_emoji} Карма пользователя {to_user_display} увеличена ({karma_change})! Текущая карма: {new_karma}"
|
||||
|
||||
try:
|
||||
sent_message = await bot.reply_to(message, response)
|
||||
logger.info(f"Пользователь {from_user.id} поблагодарил {to_user.id}, карма: {new_karma}")
|
||||
|
||||
# Удаляем уведомление через 25 секунд В ФОНЕ
|
||||
async def delete_thank_notification():
|
||||
try:
|
||||
await asyncio.sleep(25)
|
||||
await bot.delete_message(chat_id, sent_message.message_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось удалить уведомление о карме: {e}")
|
||||
|
||||
# Запускаем удаление в фоне
|
||||
asyncio.create_task(delete_thank_notification())
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о благодарности: {e}", exc_info=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке благодарности: {e}", exc_info=True)
|
||||
@@ -0,0 +1,218 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, ChatPermissions
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from database import db
|
||||
from bad_words import contains_bad_word, get_bad_words_from_text, highlight_bad_words_in_text
|
||||
from action_reporter import action_reporter
|
||||
from utils import delete_messages, format_mute_time
|
||||
from config import VIOLATIONS_PERIOD
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Система прогрессирующих мутов (в секундах)
|
||||
# Более плавная прогрессия для накопительного эффекта
|
||||
MUTE_LEVELS = [
|
||||
300, # 1. 5 минут (первое нарушение - символический мут)
|
||||
900, # 2. 15 минут
|
||||
1800, # 3. 30 минут
|
||||
3600, # 4. 1 час
|
||||
7200, # 5. 2 часа
|
||||
14400, # 6. 4 часа
|
||||
28800, # 7. 8 часов
|
||||
43200, # 8. 12 часов
|
||||
86400, # 9. 1 день
|
||||
172800, # 10. 2 дня
|
||||
259200, # 11. 3 дня
|
||||
432000, # 12. 5 дней
|
||||
604800, # 13. 7 дней
|
||||
None, # 14. Перманентный мут (режим только чтения навсегда)
|
||||
]
|
||||
|
||||
def get_mute_duration(violations_count: int) -> int:
|
||||
"""
|
||||
Определяет длительность мута на основе количества нарушений.
|
||||
|
||||
Args:
|
||||
violations_count: Количество нарушений пользователя
|
||||
|
||||
Returns:
|
||||
Длительность мута в секундах (или None для перманентного мута)
|
||||
"""
|
||||
if violations_count < 1:
|
||||
return MUTE_LEVELS[0]
|
||||
|
||||
# Индекс уровня мута (количество нарушений - 1, т.к. начинаем с 0)
|
||||
level_index = violations_count - 1
|
||||
|
||||
# Если превысили количество уровней, возвращаем перманентный мут
|
||||
if level_index >= len(MUTE_LEVELS):
|
||||
return None
|
||||
|
||||
return MUTE_LEVELS[level_index]
|
||||
|
||||
async def apply_mute(bot: AsyncTeleBot, message: Message, user_id: int, duration: int, violations_count: int, bad_words_found: list = None):
|
||||
"""
|
||||
Применяет мут к пользователю.
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
message: Сообщение, которое вызвало мут
|
||||
user_id: ID пользователя
|
||||
duration: Длительность мута в секундах (None для перманентного)
|
||||
violations_count: Количество нарушений
|
||||
bad_words_found: Список найденных плохих слов
|
||||
"""
|
||||
try:
|
||||
# Устанавливаем ограничения (только чтение)
|
||||
permissions = ChatPermissions(
|
||||
can_send_messages=False,
|
||||
can_send_media_messages=False,
|
||||
can_send_polls=False,
|
||||
can_send_other_messages=False,
|
||||
can_add_web_page_previews=False,
|
||||
can_change_info=False,
|
||||
can_invite_users=False,
|
||||
can_pin_messages=False,
|
||||
)
|
||||
|
||||
# Вычисляем время окончания мута
|
||||
until_date = None if duration is None else int(time.time()) + duration
|
||||
|
||||
# Выполняем мут
|
||||
await bot.restrict_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id,
|
||||
permissions=permissions,
|
||||
until_date=until_date
|
||||
)
|
||||
|
||||
# Снимаем карму за автомут
|
||||
db.add_karma(user_id, message.chat.id, -10)
|
||||
logger.info(f"Снято 10 кармы пользователю {user_id} за автомут")
|
||||
|
||||
# Удаляем сообщение с матом
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=message.message_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить сообщение: {e}")
|
||||
|
||||
# Формируем информацию о найденных словах
|
||||
words_info = ""
|
||||
if bad_words_found:
|
||||
words_list = ", ".join([f"«{word}»" for word in bad_words_found])
|
||||
words_info = f"Найдено слов: {words_list}"
|
||||
else:
|
||||
words_info = "Использование нецензурной лексики"
|
||||
|
||||
# Формируем сообщение о муте
|
||||
if duration is None:
|
||||
time_display = "навсегда"
|
||||
warning_msg = (
|
||||
f"⛔️ Пользователь <b>{message.from_user.first_name}</b> получил перманентный мут "
|
||||
f"за злостное нарушение правил чата (использование нецензурной лексики).\n\n"
|
||||
f"📊 Количество нарушений: <b>{violations_count}</b>\n"
|
||||
f"🔒 Режим: только чтение (навсегда)"
|
||||
)
|
||||
else:
|
||||
time_display = format_mute_time(duration)
|
||||
warning_msg = (
|
||||
f"⚠️ Пользователь <b>{message.from_user.first_name}</b> получил мут на <b>{time_display}</b> "
|
||||
f"за использование нецензурной лексики.\n\n"
|
||||
f"📊 Нарушение #{violations_count}\n"
|
||||
f"💡 При повторных нарушениях время мута будет увеличиваться."
|
||||
)
|
||||
|
||||
# Отправляем сообщение в чат
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=warning_msg,
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Формируем причину с выделением плохих слов в тексте
|
||||
highlighted_text = highlight_bad_words_in_text(message.text)
|
||||
reason_text = f"{words_info} (нарушение #{violations_count})\n\n📝 <b>Текст сообщения:</b>\n{highlighted_text}"
|
||||
|
||||
# Отправляем сообщение-лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="АВТОМУТ",
|
||||
user_id=user_id,
|
||||
admin_id=None, # Автоматическое действие
|
||||
reason=reason_text,
|
||||
duration=time_display,
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(
|
||||
f"Пользователь {user_id} получил автоматический мут на {time_display} "
|
||||
f"за нецензурную лексику (нарушение #{violations_count})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при применении мута: {e}")
|
||||
|
||||
async def check_message_for_profanity(bot: AsyncTeleBot, message: Message):
|
||||
"""
|
||||
Проверяет сообщение на наличие бранных слов и применяет мут при необходимости.
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
message: Сообщение для проверки
|
||||
"""
|
||||
# Проверяем только текстовые сообщения
|
||||
if not message.text:
|
||||
return
|
||||
|
||||
# Не проверяем команды
|
||||
if message.text.startswith('/'):
|
||||
return
|
||||
|
||||
# Проверяем, содержит ли сообщение бранные слова
|
||||
if not contains_bad_word(message.text):
|
||||
return
|
||||
|
||||
# Получаем список найденных плохих слов
|
||||
bad_words_found = get_bad_words_from_text(message.text)
|
||||
|
||||
# Получаем ID пользователя и чата
|
||||
user_id = message.from_user.id
|
||||
chat_id = message.chat.id
|
||||
|
||||
# Проверяем, является ли отправитель администратором
|
||||
try:
|
||||
chat_member = await bot.get_chat_member(chat_id, user_id)
|
||||
if chat_member.status in ['administrator', 'creator']:
|
||||
logger.info(f"Администратор {user_id} использовал нецензурную лексику, мут не применен")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки статуса пользователя: {e}")
|
||||
return
|
||||
|
||||
# Добавляем нарушение в базу данных
|
||||
db.add_violation(user_id, chat_id, violation_type='bad_language')
|
||||
|
||||
# Получаем количество нарушений за последний месяц
|
||||
violations_count = db.get_violations_count(user_id, chat_id, VIOLATIONS_PERIOD)
|
||||
|
||||
# Определяем длительность мута
|
||||
mute_duration = get_mute_duration(violations_count)
|
||||
|
||||
# Применяем мут
|
||||
await apply_mute(bot, message, user_id, mute_duration, violations_count, bad_words_found)
|
||||
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
"""
|
||||
Регистрирует обработчики для автоматического мута.
|
||||
|
||||
ПРИМЕЧАНИЕ: Фактическая проверка мата происходит в middleware (main.py),
|
||||
а не в обработчике. Это позволяет проверять все сообщения ДО того, как
|
||||
они попадут к другим обработчикам (например, karma_tracker).
|
||||
|
||||
Этот метод оставлен для совместимости с архитектурой модулей.
|
||||
"""
|
||||
|
||||
logger.info("Модуль автоматического мута успешно загружен (проверка в middleware)")
|
||||
@@ -0,0 +1,279 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message
|
||||
import logging
|
||||
|
||||
from bad_words import (
|
||||
get_bad_words,
|
||||
get_exceptions,
|
||||
save_bad_words,
|
||||
reload_words
|
||||
)
|
||||
from database import db
|
||||
from utils import check_admin_status, delete_messages
|
||||
from config import COMMAND_MESSAGES
|
||||
from action_reporter import action_reporter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
"""Регистрирует обработчики команд управления бранными словами"""
|
||||
|
||||
@bot.message_handler(commands=['badwords'])
|
||||
async def badwords_command(message: Message):
|
||||
"""Главная команда управления списком бранных слов"""
|
||||
|
||||
logger.info(f"Команда /badwords получена от пользователя {message.from_user.id}")
|
||||
|
||||
# Проверяем права администратора (без проверки прав на ограничение участников)
|
||||
admin_check = await check_admin_status(bot, message, check_restrict_rights=False)
|
||||
if admin_check == 1:
|
||||
logger.info(f"Пользователь {message.from_user.id} не является администратором")
|
||||
return
|
||||
|
||||
logger.info(f"Пользователь {message.from_user.id} прошел проверку прав администратора")
|
||||
|
||||
parts = message.text.split(maxsplit=2)
|
||||
|
||||
# /badwords без параметров - показываем help
|
||||
if len(parts) == 1:
|
||||
await show_help(bot, message)
|
||||
return
|
||||
|
||||
subcommand = parts[1].lower()
|
||||
|
||||
# Обработка подкоманд
|
||||
if subcommand == 'help':
|
||||
await show_help(bot, message)
|
||||
elif subcommand == 'list':
|
||||
await list_bad_words(bot, message)
|
||||
elif subcommand == 'count':
|
||||
await count_words(bot, message)
|
||||
elif subcommand == 'add':
|
||||
if len(parts) < 3:
|
||||
await send_temp_message(bot, message, "❌ Укажите слово для добавления: /badwords add <слово>")
|
||||
else:
|
||||
await add_bad_word(bot, message, parts[2])
|
||||
elif subcommand == 'remove':
|
||||
if len(parts) < 3:
|
||||
await send_temp_message(bot, message, "❌ Укажите слово для удаления: /badwords remove <слово>")
|
||||
else:
|
||||
await remove_bad_word(bot, message, parts[2])
|
||||
elif subcommand == 'exceptions':
|
||||
await list_exceptions(bot, message)
|
||||
elif subcommand == 'add_exception':
|
||||
if len(parts) < 3:
|
||||
await send_temp_message(bot, message, "❌ Укажите слово для добавления в исключения: /badwords add_exception <слово>")
|
||||
else:
|
||||
await add_exception(bot, message, parts[2])
|
||||
elif subcommand == 'remove_exception':
|
||||
if len(parts) < 3:
|
||||
await send_temp_message(bot, message, "❌ Укажите слово для удаления из исключений: /badwords remove_exception <слово>")
|
||||
else:
|
||||
await remove_exception(bot, message, parts[2])
|
||||
elif subcommand == 'reload':
|
||||
await reload_wordlist(bot, message)
|
||||
else:
|
||||
await send_temp_message(bot, message, f"❌ Неизвестная команда: {subcommand}\n\nИспользуйте /badwords help")
|
||||
|
||||
async def show_help(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает справку по командам управления бранными словами"""
|
||||
help_text = COMMAND_MESSAGES['badwords_help']
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=help_text,
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=60, number_message=2)
|
||||
|
||||
async def list_bad_words(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает список бранных слов (первые 50)"""
|
||||
words = get_bad_words()
|
||||
|
||||
if not words:
|
||||
text = "📝 Список бранных слов пуст."
|
||||
else:
|
||||
# Показываем только первые 50 слов
|
||||
display_words = words[:50]
|
||||
text = f"📝 <b>Бранные слова</b> (всего: {len(words)})\n\n"
|
||||
text += ", ".join([f"<code>{word}</code>" for word in display_words])
|
||||
|
||||
if len(words) > 50:
|
||||
text += f"\n\n<i>...и ещё {len(words) - 50} слов</i>"
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=text,
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
|
||||
async def count_words(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает статистику по спискам"""
|
||||
words = get_bad_words()
|
||||
exceptions = get_exceptions()
|
||||
|
||||
text = (
|
||||
f"📊 <b>Статистика списков</b>\n\n"
|
||||
f"🚫 Бранных слов: <b>{len(words)}</b>\n"
|
||||
f"✅ Исключений: <b>{len(exceptions)}</b>"
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=text,
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=15, number_message=2)
|
||||
|
||||
async def add_bad_word(bot: AsyncTeleBot, message: Message, word: str):
|
||||
"""Добавляет слово в список бранных"""
|
||||
word = word.lower().strip()
|
||||
|
||||
words = get_bad_words()
|
||||
exceptions = get_exceptions()
|
||||
|
||||
if word in words:
|
||||
await send_temp_message(bot, message, f"⚠️ Слово '<code>{word}</code>' уже есть в списке.")
|
||||
return
|
||||
|
||||
words.append(word)
|
||||
if save_bad_words(words, exceptions):
|
||||
reload_words() # Перезагружаем кэш
|
||||
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' добавлено в список бранных.")
|
||||
logger.info(f"Администратор {message.from_user.id} добавил бранное слово: {word}")
|
||||
|
||||
# Отправляем в канал аудита
|
||||
if action_reporter:
|
||||
await action_reporter.log_action(
|
||||
action=f"Добавление бранного слова",
|
||||
user_id=None, # Не применимо к конкретному пользователю
|
||||
admin_id=message.from_user.id,
|
||||
reason=f"Слово: <code>{word}</code>",
|
||||
duration=None,
|
||||
)
|
||||
else:
|
||||
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
||||
|
||||
async def remove_bad_word(bot: AsyncTeleBot, message: Message, word: str):
|
||||
"""Удаляет слово из списка бранных"""
|
||||
word = word.lower().strip()
|
||||
|
||||
words = get_bad_words()
|
||||
exceptions = get_exceptions()
|
||||
|
||||
if word not in words:
|
||||
await send_temp_message(bot, message, f"⚠️ Слово '<code>{word}</code>' не найдено в списке.")
|
||||
return
|
||||
|
||||
words.remove(word)
|
||||
if save_bad_words(words, exceptions):
|
||||
reload_words() # Перезагружаем кэш
|
||||
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' удалено из списка бранных.")
|
||||
logger.info(f"Администратор {message.from_user.id} удалил бранное слово: {word}")
|
||||
|
||||
# Отправляем в канал аудита
|
||||
if action_reporter:
|
||||
await action_reporter.log_action(
|
||||
action=f"Удаление бранного слова",
|
||||
user_id=None,
|
||||
admin_id=message.from_user.id,
|
||||
reason=f"Слово: <code>{word}</code>",
|
||||
duration=None,
|
||||
)
|
||||
else:
|
||||
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
||||
|
||||
async def list_exceptions(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает список исключений"""
|
||||
exceptions = get_exceptions()
|
||||
|
||||
if not exceptions:
|
||||
text = "📝 Список исключений пуст."
|
||||
else:
|
||||
text = f"📝 <b>Исключения</b> (всего: {len(exceptions)})\n\n"
|
||||
text += ", ".join([f"<code>{word}</code>" for word in exceptions])
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=text,
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
|
||||
async def add_exception(bot: AsyncTeleBot, message: Message, word: str):
|
||||
"""Добавляет слово в список исключений"""
|
||||
word = word.lower().strip()
|
||||
|
||||
words = get_bad_words()
|
||||
exceptions = get_exceptions()
|
||||
|
||||
if word in exceptions:
|
||||
await send_temp_message(bot, message, f"⚠️ Слово '<code>{word}</code>' уже есть в исключениях.")
|
||||
return
|
||||
|
||||
exceptions.append(word)
|
||||
if save_bad_words(words, exceptions):
|
||||
reload_words() # Перезагружаем кэш
|
||||
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' добавлено в исключения.")
|
||||
logger.info(f"Администратор {message.from_user.id} добавил исключение: {word}")
|
||||
|
||||
# Отправляем в канал аудита
|
||||
if action_reporter:
|
||||
await action_reporter.log_action(
|
||||
action=f"Добавление исключения",
|
||||
user_id=None,
|
||||
admin_id=message.from_user.id,
|
||||
reason=f"Слово: <code>{word}</code>",
|
||||
duration=None,
|
||||
)
|
||||
else:
|
||||
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
||||
|
||||
async def remove_exception(bot: AsyncTeleBot, message: Message, word: str):
|
||||
"""Удаляет слово из списка исключений"""
|
||||
word = word.lower().strip()
|
||||
|
||||
words = get_bad_words()
|
||||
exceptions = get_exceptions()
|
||||
|
||||
if word not in exceptions:
|
||||
await send_temp_message(bot, message, f"⚠️ Слово '<code>{word}</code>' не найдено в исключениях.")
|
||||
return
|
||||
|
||||
exceptions.remove(word)
|
||||
if save_bad_words(words, exceptions):
|
||||
reload_words() # Перезагружаем кэш
|
||||
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' удалено из исключений.")
|
||||
logger.info(f"Администратор {message.from_user.id} удалил исключение: {word}")
|
||||
|
||||
# Отправляем в канал аудита
|
||||
if action_reporter:
|
||||
await action_reporter.log_action(
|
||||
action=f"Удаление исключения",
|
||||
user_id=None,
|
||||
admin_id=message.from_user.id,
|
||||
reason=f"Слово: <code>{word}</code>",
|
||||
duration=None,
|
||||
)
|
||||
else:
|
||||
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
||||
|
||||
async def reload_wordlist(bot: AsyncTeleBot, message: Message):
|
||||
"""Перезагружает списки слов из файла"""
|
||||
words, exceptions = reload_words()
|
||||
text = (
|
||||
f"🔄 <b>Списки перезагружены</b>\n\n"
|
||||
f"🚫 Бранных слов: <b>{len(words)}</b>\n"
|
||||
f"✅ Исключений: <b>{len(exceptions)}</b>"
|
||||
)
|
||||
await send_temp_message(bot, message, text)
|
||||
logger.info(f"Администратор {message.from_user.id} перезагрузил списки слов")
|
||||
|
||||
async def send_temp_message(bot: AsyncTeleBot, message: Message, text: str, time_sleep: int = 10):
|
||||
"""Отправляет временное сообщение, которое удаляется через указанное время"""
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=text,
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=time_sleep, number_message=2)
|
||||
+186
-186
@@ -1,6 +1,5 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, User
|
||||
import asyncio
|
||||
import logging
|
||||
from database import db
|
||||
|
||||
@@ -9,241 +8,242 @@ from utils import (
|
||||
delete_messages,
|
||||
check_admin_status,
|
||||
check_target_status,
|
||||
format_user_mention,
|
||||
delete_moderation_messages,
|
||||
)
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
logger = logging.getLogger(__name__) # Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Регистрирует все обработчики команд
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
"""Регистрирует обработчик команды /ban"""
|
||||
|
||||
# Обработчик команды /ban
|
||||
@bot.message_handler(commands=['ban'])
|
||||
async def _ban_command_wrapper(message: Message):
|
||||
parts = message.text.split()
|
||||
|
||||
# Определяем подкоманду
|
||||
if len(parts) >= 2:
|
||||
subcommand = parts[1].lower()
|
||||
|
||||
if subcommand in ('help', 'помощь'):
|
||||
await ban_help(bot, message)
|
||||
return
|
||||
elif subcommand == 'off':
|
||||
await ban_off(bot, message, parts)
|
||||
return
|
||||
|
||||
# Основная команда бана
|
||||
await ban_command(bot, message)
|
||||
|
||||
# Основная функция команды /ban
|
||||
async def ban_command(bot: AsyncTeleBot, message: Message, photo_path: str = None):
|
||||
|
||||
# Определяем целевого пользователя
|
||||
async def ban_help(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает справку по команде /ban"""
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['manual_ban'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
|
||||
|
||||
async def ban_off(bot: AsyncTeleBot, message: Message, parts: list):
|
||||
"""Снимает бан с пользователя (/ban off)"""
|
||||
|
||||
target_user = None
|
||||
|
||||
# Определяем причину
|
||||
reason = None
|
||||
|
||||
# Разбиваем текст сообщения на части
|
||||
parts_msg = message.text.split()
|
||||
|
||||
# Команда /ban help
|
||||
if len(parts_msg) == 2 and parts_msg[1].strip() in ('help', 'помощь'):
|
||||
|
||||
# Отправляем инструкцию
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['manual_ban'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 30 секунд
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
# Проверяем, является ли отправитель администратором
|
||||
# Проверяем права администратора
|
||||
if await check_admin_status(bot, message) == 1:
|
||||
return
|
||||
|
||||
# Команда через ответ на сообщение, если одно слово (/ban)
|
||||
if len(parts_msg) == 1:
|
||||
|
||||
# Если это топик
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Если с ответом на сообщение
|
||||
else:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
reason = 'отсутствует'
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
|
||||
# Собираем данные
|
||||
# /ban off (ответом на сообщение)
|
||||
if len(parts) == 2:
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
target_user = message.reply_to_message.from_user
|
||||
reason = 'отсутствует'
|
||||
message.message_thread_id = None
|
||||
|
||||
# Если команда неправильная
|
||||
else:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# В сообщении больше одного слова
|
||||
else:
|
||||
|
||||
# Если второе слово это тег или ID
|
||||
if parts_msg[1].strip().isdigit() or parts_msg[1].startswith('@'):
|
||||
|
||||
# Собираем данные
|
||||
identifier = parts_msg[1].strip()
|
||||
reason = ' '.join(parts_msg[2:]) if parts_msg[2:] != [] else 'отсутствует'
|
||||
|
||||
# Делаем поиск по ID
|
||||
if identifier.isdigit():
|
||||
# /ban off @username или /ban off 123456789
|
||||
elif len(parts) >= 3:
|
||||
identifier = parts[2].strip()
|
||||
|
||||
# Ищем пользователя в базе данных
|
||||
user_info = db.get_user(int(identifier))
|
||||
if identifier.isdigit():
|
||||
user_info = db.get_user(int(identifier))
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
elif identifier.startswith('@'):
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Если нашли пользователя
|
||||
if user_info:
|
||||
if not target_user:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Создаем объект пользователя
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Делаем поиск по тегу
|
||||
elif identifier.startswith('@'):
|
||||
# Снимаем бан
|
||||
await bot.unban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
)
|
||||
|
||||
# Ищем пользователя в базе данных (убрали @)
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
# Лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="РАЗБАН",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=None,
|
||||
duration=None,
|
||||
)
|
||||
|
||||
# Если нашли пользователя
|
||||
if user_info:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['unbanned'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Создаем объект пользователя
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
logger.info(f"Пользователь {target_user.id} разбанен администратором {message.from_user.id}")
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
# Команда через ответ на сообщение с указанием причины
|
||||
except Exception as e:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
logger.error(f"Ошибка в ban_off: {str(e)}")
|
||||
|
||||
|
||||
async def ban_command(bot: AsyncTeleBot, message: Message, photo_path: str = None):
|
||||
"""Основная функция бана пользователя"""
|
||||
|
||||
target_user = None
|
||||
reason = None
|
||||
|
||||
parts_msg = message.text.split()
|
||||
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if await check_admin_status(bot, message) == 1:
|
||||
return
|
||||
|
||||
# /ban (ответом на сообщение)
|
||||
if len(parts_msg) == 1:
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
target_user = message.reply_to_message.from_user
|
||||
reason = 'отсутствует'
|
||||
else:
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Если это топик
|
||||
if message.is_topic_message:
|
||||
# Более одного слова
|
||||
else:
|
||||
# /ban @user причина или /ban 123456789 причина
|
||||
if parts_msg[1].strip().isdigit() or parts_msg[1].startswith('@'):
|
||||
identifier = parts_msg[1].strip()
|
||||
reason = ' '.join(parts_msg[2:]) if len(parts_msg) > 2 else 'отсутствует'
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
if identifier.isdigit():
|
||||
user_info = db.get_user(int(identifier))
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
elif identifier.startswith('@'):
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Если с ответом на сообщение
|
||||
else:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
reason = ' '.join(parts_msg[1:])
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
|
||||
# Собираем данные
|
||||
# /ban причина (ответом на сообщение)
|
||||
else:
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
target_user = message.reply_to_message.from_user
|
||||
reason = ' '.join(parts_msg[1:])
|
||||
message.message_thread_id = None
|
||||
|
||||
# Если команда неправильная
|
||||
else:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Если пользователь не найден в базе данных
|
||||
if not target_user:
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
|
||||
# Проверяем статус целевого пользователя
|
||||
if await check_target_status(bot, message, target_user) == 1:
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
# Выполняем бан
|
||||
await bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
)
|
||||
|
||||
# Отправляем лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="БАН",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=reason,
|
||||
duration=None,
|
||||
photo_path=photo_path,
|
||||
)
|
||||
|
||||
# Отправляем сообщения, что пользователь получил бан
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['banned'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Пользователь {target_user.id} забанен администратором {message.from_user.id}.")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['error'].format(e=str(e)),
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Ошибка бана: {str(e)}")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
# Выполняем бан
|
||||
await bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Общая ошибка в ban_command: {str(e)}")
|
||||
target_mention = format_user_mention(target_user)
|
||||
admin_mention = format_user_mention(message.from_user)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
# Лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="БАН",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=reason,
|
||||
duration=None,
|
||||
photo_path=photo_path,
|
||||
source_message=message.reply_to_message,
|
||||
)
|
||||
|
||||
# Удаляем команду и сообщение нарушителя
|
||||
await delete_moderation_messages(bot, message)
|
||||
|
||||
# Отправляем сообщение о бане
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['banned'].format(
|
||||
target_user=target_mention,
|
||||
admin_user=admin_mention,
|
||||
reason=reason,
|
||||
),
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
|
||||
logger.info(f"Пользователь {target_user.id} забанен администратором {message.from_user.id}")
|
||||
|
||||
except Exception as e:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
logger.error(f"Ошибка в ban_command: {str(e)}")
|
||||
@@ -1,67 +0,0 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from utils import delete_messages
|
||||
|
||||
from config import COMMAND_MESSAGES, DATABASE_NAME, LOG_FILE_NAME
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Загружаем id администраторов из .env
|
||||
ADMIN_IDS = [int(id_str.strip()) for id_str in os.getenv('ADMIN_IDS').split(',')]
|
||||
|
||||
# Регистрирует все обработчики команд
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
# Обработчик команды /botdata
|
||||
@bot.message_handler(commands=['botdata'])
|
||||
async def botdata_command(message: Message):
|
||||
|
||||
try:
|
||||
|
||||
# Если id администратора совпадает
|
||||
if message.from_user.id in ADMIN_IDS:
|
||||
|
||||
# Отправляем базу данных
|
||||
await bot.send_document(
|
||||
chat_id=message.chat.id,
|
||||
document=open(DATABASE_NAME, 'rb')
|
||||
)
|
||||
|
||||
# Отправляем файл с логами
|
||||
await bot.send_document(
|
||||
chat_id=message.chat.id,
|
||||
document=open(LOG_FILE_NAME, 'rb')
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Администратор {message.from_user.id} запустил /botdata.")
|
||||
|
||||
# Если id администратора не совпадает
|
||||
else:
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['no_admin_rights'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Пользователь {message.from_user.id} запустил /botdata.")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Общая ошибка в botdata_command: {str(e)}")
|
||||
@@ -0,0 +1,309 @@
|
||||
import asyncio
|
||||
import html
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import (
|
||||
ChatMemberUpdated,
|
||||
ChatPermissions,
|
||||
InlineKeyboardMarkup,
|
||||
InlineKeyboardButton,
|
||||
CallbackQuery,
|
||||
Message,
|
||||
)
|
||||
|
||||
from database import db
|
||||
from utils import delete_messages
|
||||
from config import (
|
||||
COMMAND_MESSAGES,
|
||||
CAPTCHA_TIMEOUT,
|
||||
GUARD_SETTING_KEY,
|
||||
GUARD_DEFAULT_VALUE,
|
||||
get_captcha_text,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Ожидающие капчи: {chat_id: {user_id: (message_id, asyncio.Task)}}
|
||||
_pending: dict[int, dict[int, tuple[int, asyncio.Task]]] = {}
|
||||
|
||||
|
||||
async def _check_cas(user_id: int) -> bool:
|
||||
"""Проверить пользователя в базе CAS. True = спамер."""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
f"https://api.cas.chat/check?user_id={user_id}",
|
||||
timeout=aiohttp.ClientTimeout(total=5),
|
||||
) as resp:
|
||||
data = await resp.json()
|
||||
return data.get("ok", False)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось проверить CAS для {user_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def _kick_on_timeout(bot: AsyncTeleBot, chat_id: int, user_id: int, message_id: int):
|
||||
"""Кикнуть пользователя, не прошедшего капчу за CAPTCHA_TIMEOUT секунд."""
|
||||
await asyncio.sleep(CAPTCHA_TIMEOUT)
|
||||
|
||||
if chat_id in _pending:
|
||||
_pending[chat_id].pop(user_id, None)
|
||||
|
||||
try:
|
||||
await bot.ban_chat_member(chat_id, user_id)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
try:
|
||||
await bot.unban_chat_member(chat_id, user_id, only_if_banned=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
await bot.delete_message(chat_id, message_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _guard_enabled(chat_id: int) -> bool:
|
||||
"""Проверить, включена ли капча для чата."""
|
||||
value = db.get_chat_setting(chat_id, GUARD_SETTING_KEY, GUARD_DEFAULT_VALUE)
|
||||
return value == "1"
|
||||
|
||||
|
||||
async def _is_admin(bot: AsyncTeleBot, chat_id: int, user_id: int) -> bool:
|
||||
"""Проверить, является ли пользователь админом/создателем чата."""
|
||||
try:
|
||||
member = await bot.get_chat_member(chat_id, user_id)
|
||||
return member.status in ("administrator", "creator")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def guard_command(bot: AsyncTeleBot, message: Message):
|
||||
"""Управление капчей при входе (/guard on/off)."""
|
||||
if message.chat.type not in ("group", "supergroup"):
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['guard_help'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
return
|
||||
|
||||
if not await _is_admin(bot, message.chat.id, message.from_user.id):
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['no_admin_rights'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
parts = message.text.split()
|
||||
|
||||
# /guard — показать статус
|
||||
if len(parts) == 1:
|
||||
enabled = _guard_enabled(message.chat.id)
|
||||
status_text = "включена ✅" if enabled else "отключена ❌"
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['guard_status'].format(status=status_text),
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=10, number_message=2)
|
||||
return
|
||||
|
||||
arg = parts[1].lower()
|
||||
|
||||
if arg in ("on", "1", "вкл", "да", "yes"):
|
||||
db.set_chat_setting(message.chat.id, GUARD_SETTING_KEY, "1")
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['guard_enabled'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
elif arg in ("off", "0", "выкл", "нет", "no"):
|
||||
db.set_chat_setting(message.chat.id, GUARD_SETTING_KEY, "0")
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['guard_disabled'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['guard_help'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
await delete_messages(bot, message, time_sleep=10, number_message=2)
|
||||
|
||||
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
"""Регистрирует обработчики защиты входа."""
|
||||
|
||||
@bot.message_handler(commands=['guard'])
|
||||
async def _guard_command_wrapper(message: Message):
|
||||
await guard_command(bot, message)
|
||||
|
||||
@bot.chat_member_handler()
|
||||
async def on_chat_member_updated(event: ChatMemberUpdated):
|
||||
"""Обработка входа нового участника: CAS → капча."""
|
||||
old_status = event.old_chat_member.status
|
||||
new_status = event.new_chat_member.status
|
||||
|
||||
# Только новые входы (left/kicked → member)
|
||||
if new_status != "member":
|
||||
return
|
||||
if old_status not in ("left", "kicked"):
|
||||
return
|
||||
|
||||
user = event.new_chat_member.user
|
||||
chat_id = event.chat.id
|
||||
|
||||
if user.is_bot:
|
||||
return
|
||||
|
||||
# ── Шаг 1: проверка CAS (всегда активна) ─────────────────────────────────
|
||||
is_spammer = await _check_cas(user.id)
|
||||
if is_spammer:
|
||||
try:
|
||||
await bot.ban_chat_member(chat_id, user.id)
|
||||
logger.info(f"CAS: забанен спамер {user.id} в чате {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"CAS: не удалось забанить {user.id} в чате {chat_id}: {e}")
|
||||
return
|
||||
|
||||
# ── Шаг 2: капча (управляется через /guard) ──────────────────────────────
|
||||
if not _guard_enabled(chat_id):
|
||||
return
|
||||
|
||||
try:
|
||||
await bot.restrict_chat_member(
|
||||
chat_id=chat_id,
|
||||
user_id=user.id,
|
||||
permissions=ChatPermissions(
|
||||
can_send_messages=False,
|
||||
can_send_media_messages=False,
|
||||
can_send_polls=False,
|
||||
can_send_other_messages=False,
|
||||
can_add_web_page_previews=False,
|
||||
can_change_info=False,
|
||||
can_invite_users=False,
|
||||
can_pin_messages=False,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось ограничить нового участника {user.id}: {e}")
|
||||
return
|
||||
|
||||
name = html.escape(user.full_name or str(user.id))
|
||||
user_mention = f'<a href="tg://user?id={user.id}">{name}</a>'
|
||||
user_language = getattr(user, 'language_code', None)
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=get_captcha_text(user_language, 'captcha_button'),
|
||||
callback_data=f"captcha:{user.id}",
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
msg = await bot.send_message(
|
||||
chat_id,
|
||||
get_captcha_text(user_language, 'captcha_message').format(
|
||||
user_mention=user_mention,
|
||||
timeout=CAPTCHA_TIMEOUT,
|
||||
),
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить капчу для {user.id}: {e}")
|
||||
return
|
||||
|
||||
# Если для пользователя уже была активна капча — отменяем старую задачу
|
||||
chat_pending = _pending.setdefault(chat_id, {})
|
||||
if user.id in chat_pending:
|
||||
_, old_task = chat_pending[user.id]
|
||||
old_task.cancel()
|
||||
|
||||
task = asyncio.create_task(
|
||||
_kick_on_timeout(bot, chat_id, user.id, msg.message_id)
|
||||
)
|
||||
|
||||
chat_pending[user.id] = (msg.message_id, task)
|
||||
|
||||
@bot.callback_query_handler(func=lambda call: call.data.startswith("captcha:"))
|
||||
async def on_captcha_click(call: CallbackQuery):
|
||||
"""Пользователь нажал кнопку капчи."""
|
||||
expected_user_id = int(call.data.split(":")[1])
|
||||
|
||||
# Кнопку может нажать только тот, кому она адресована
|
||||
if call.from_user.id != expected_user_id:
|
||||
try:
|
||||
await bot.answer_callback_query(
|
||||
call.id,
|
||||
text=get_captcha_text(
|
||||
getattr(call.from_user, 'language_code', None),
|
||||
'captcha_wrong_user',
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось ответить чужому пользователю на кнопку капчи: {e}")
|
||||
return
|
||||
|
||||
chat_id = call.message.chat.id
|
||||
user_id = call.from_user.id
|
||||
|
||||
logger.info(f"[CAPTCHA] Пользователь {user_id} проходит капчу в чате {chat_id}")
|
||||
|
||||
# Отменяем таймаут кика
|
||||
chat_pending = _pending.get(chat_id, {})
|
||||
if user_id in chat_pending:
|
||||
_, task = chat_pending.pop(user_id)
|
||||
task.cancel()
|
||||
logger.info(f"[CAPTCHA] Таймаут-кик для {user_id} отменён")
|
||||
|
||||
# Снимаем ограничения
|
||||
try:
|
||||
await bot.restrict_chat_member(
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
permissions=ChatPermissions(
|
||||
can_send_messages=True,
|
||||
can_send_media_messages=True,
|
||||
can_send_polls=True,
|
||||
can_send_other_messages=True,
|
||||
can_add_web_page_previews=True,
|
||||
can_change_info=True,
|
||||
can_invite_users=True,
|
||||
can_pin_messages=True,
|
||||
),
|
||||
)
|
||||
logger.info(f"[CAPTCHA] Ограничения сняты с пользователя {user_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось снять ограничения с {user_id}: {e}", exc_info=True)
|
||||
|
||||
try:
|
||||
await bot.answer_callback_query(
|
||||
call.id,
|
||||
text=get_captcha_text(
|
||||
getattr(call.from_user, 'language_code', None),
|
||||
'captcha_welcome',
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось ответить на callback пользователя {user_id}: {e}")
|
||||
|
||||
try:
|
||||
await bot.delete_message(chat_id, call.message.message_id)
|
||||
logger.info(f"[CAPTCHA] Сообщение капчи для {user_id} удалено")
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить сообщение капчи для {user_id}: {e}")
|
||||
|
||||
+26
-20
@@ -1,48 +1,54 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from utils import delete_messages
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Регистрирует все обработчики команд
|
||||
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
# Обработчик команды /help
|
||||
@bot.message_handler(commands=['help'])
|
||||
async def help_command(message: Message):
|
||||
|
||||
try:
|
||||
# Определяем какой help показывать
|
||||
help_key = 'help_user'
|
||||
|
||||
# Отправляем сообщение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['help'],
|
||||
# В групповых чатах проверяем права
|
||||
if message.chat.type in ['group', 'supergroup']:
|
||||
try:
|
||||
member = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
if member.status in ['creator', 'administrator']:
|
||||
help_key = 'help_admin'
|
||||
except Exception as e:
|
||||
logger.debug(f"Не удалось проверить права: {e}")
|
||||
|
||||
sent_message = await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES[help_key],
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Пользователь {message.from_user.id} запустил /help.")
|
||||
logger.info(f"Пользователь {message.from_user.id} запустил /help ({help_key})")
|
||||
|
||||
# Если пользователь писал в чат
|
||||
# Если пользователь писал в чат, удаляем сообщение с командой и ответ бота
|
||||
if message.chat.id != message.from_user.id:
|
||||
|
||||
# Удаляем сообщения через 30 секунд
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
await delete_messages(
|
||||
bot,
|
||||
message,
|
||||
time_sleep=120,
|
||||
message_ids=[message.message_id, sent_message.message_id],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Общая ошибка в help_command: {str(e)}")
|
||||
logger.error(f"Ошибка в help_command: {str(e)}")
|
||||
@@ -0,0 +1,387 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, User
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from database import db
|
||||
from action_reporter import action_reporter
|
||||
from utils import check_admin_status, delete_messages
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _delete_message_delayed(bot: AsyncTeleBot, chat_id: int, message_id: int, delay: int):
|
||||
"""Удаляет сообщение с задержкой"""
|
||||
try:
|
||||
await asyncio.sleep(delay)
|
||||
await bot.delete_message(chat_id, message_id)
|
||||
except Exception as e:
|
||||
logger.debug(f"Не удалось удалить сообщение {message_id}: {e}")
|
||||
|
||||
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
"""Регистрирует обработчики команд для системы кармы"""
|
||||
|
||||
@bot.message_handler(commands=['karma', 'rating'])
|
||||
async def handle_karma_command(message: Message):
|
||||
"""
|
||||
Команда /karma - управление кармой
|
||||
|
||||
Подкоманды:
|
||||
/karma - показать свою карму
|
||||
/karma @username - показать карму пользователя
|
||||
/karma top - топ-10 пользователей
|
||||
/karma set @user 100 - установить карму (админ)
|
||||
/karma help - справка
|
||||
/karma disable - отключить карму в топике (админ)
|
||||
/karma enable - включить карму в топике (админ)
|
||||
/karma status - статус кармы в топиках (админ)
|
||||
"""
|
||||
try:
|
||||
if message.chat.type not in ['group', 'supergroup']:
|
||||
await bot.reply_to(message, "❌ Эта команда работает только в групповых чатах")
|
||||
return
|
||||
|
||||
chat_id = message.chat.id
|
||||
parts = message.text.split()
|
||||
|
||||
# Проверяем подкоманды
|
||||
if len(parts) > 1:
|
||||
subcommand = parts[1].lower()
|
||||
|
||||
if subcommand in ('help', 'помощь'):
|
||||
await karma_help(bot, message)
|
||||
return
|
||||
elif subcommand == 'top':
|
||||
await karma_top(bot, message)
|
||||
return
|
||||
elif subcommand == 'set':
|
||||
await karma_set(bot, message, parts)
|
||||
return
|
||||
elif subcommand == 'disable':
|
||||
admin_check = await check_admin_status(bot, message, check_restrict_rights=False)
|
||||
if admin_check == 1:
|
||||
return
|
||||
await disable_karma_in_topic(bot, message)
|
||||
return
|
||||
elif subcommand == 'enable':
|
||||
admin_check = await check_admin_status(bot, message, check_restrict_rights=False)
|
||||
if admin_check == 1:
|
||||
return
|
||||
await enable_karma_in_topic(bot, message)
|
||||
return
|
||||
elif subcommand == 'status':
|
||||
admin_check = await check_admin_status(bot, message, check_restrict_rights=False)
|
||||
if admin_check == 1:
|
||||
return
|
||||
await show_karma_status(bot, message)
|
||||
return
|
||||
|
||||
# Показ кармы
|
||||
await show_karma(bot, message, parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке команды /karma: {e}", exc_info=True)
|
||||
await bot.reply_to(message, "❌ Произошла ошибка при получении кармы")
|
||||
|
||||
|
||||
async def karma_help(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает справку по команде /karma"""
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['karma_help'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=60, number_message=2)
|
||||
|
||||
|
||||
async def show_karma(bot: AsyncTeleBot, message: Message, parts: list):
|
||||
"""Показывает карму пользователя"""
|
||||
chat_id = message.chat.id
|
||||
target_user = None
|
||||
target_user_id = None
|
||||
|
||||
# Ответ на сообщение
|
||||
if message.reply_to_message:
|
||||
target_user = message.reply_to_message.from_user
|
||||
target_user_id = target_user.id
|
||||
|
||||
# Указан username
|
||||
elif len(parts) > 1:
|
||||
username_arg = parts[1]
|
||||
username = username_arg.lstrip('@')
|
||||
|
||||
user_data = db.get_user_by_username(username)
|
||||
if user_data:
|
||||
target_user_id = user_data[0]
|
||||
target_user = type('User', (), {
|
||||
'id': user_data[0],
|
||||
'first_name': user_data[1],
|
||||
'username': user_data[2]
|
||||
})()
|
||||
else:
|
||||
await bot.reply_to(message, f"❌ Пользователь @{username} не найден в базе данных")
|
||||
return
|
||||
|
||||
# Своя карма
|
||||
else:
|
||||
target_user = message.from_user
|
||||
target_user_id = target_user.id
|
||||
|
||||
karma = db.get_karma(target_user_id, chat_id)
|
||||
|
||||
if hasattr(target_user, 'username') and target_user.username:
|
||||
user_display = f"@{target_user.username}"
|
||||
else:
|
||||
user_display = target_user.first_name
|
||||
|
||||
# Эмодзи в зависимости от кармы
|
||||
if karma == 0:
|
||||
emoji = "😐"
|
||||
elif karma < 5:
|
||||
emoji = "🙂"
|
||||
elif karma < 10:
|
||||
emoji = "😊"
|
||||
elif karma < 20:
|
||||
emoji = "😄"
|
||||
elif karma < 50:
|
||||
emoji = "🌟"
|
||||
else:
|
||||
emoji = "⭐"
|
||||
|
||||
response = f"{emoji} Карма пользователя {user_display}: {karma}"
|
||||
|
||||
sent_message = await bot.reply_to(message, response)
|
||||
|
||||
asyncio.create_task(_delete_message_delayed(bot, chat_id, message.message_id, 20))
|
||||
asyncio.create_task(_delete_message_delayed(bot, chat_id, sent_message.message_id, 60))
|
||||
|
||||
|
||||
async def karma_top(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает топ-10 пользователей по карме"""
|
||||
chat_id = message.chat.id
|
||||
|
||||
top_users = db.get_top_karma(chat_id, limit=10)
|
||||
|
||||
if not top_users:
|
||||
await bot.reply_to(message, "📊 В этом чате пока нет пользователей с кармой")
|
||||
return
|
||||
|
||||
response = "🏆 <b>Топ-10 пользователей по карме:</b>\n\n"
|
||||
|
||||
medals = ["🥇", "🥈", "🥉"]
|
||||
|
||||
for idx, (user_id, nickname, tag, karma_points) in enumerate(top_users, 1):
|
||||
if idx <= 3:
|
||||
medal = medals[idx - 1]
|
||||
else:
|
||||
medal = f"{idx}."
|
||||
|
||||
if tag:
|
||||
user_display = f"<code>@{tag}</code>"
|
||||
else:
|
||||
user_display = nickname or f"ID: {user_id}"
|
||||
|
||||
response += f"{medal} {user_display} — <b>{karma_points}</b> кармы\n"
|
||||
|
||||
sent_message = await bot.reply_to(message, response, parse_mode='HTML')
|
||||
|
||||
asyncio.create_task(_delete_message_delayed(bot, chat_id, message.message_id, 20))
|
||||
asyncio.create_task(_delete_message_delayed(bot, chat_id, sent_message.message_id, 60))
|
||||
|
||||
|
||||
async def karma_set(bot: AsyncTeleBot, message: Message, parts: list):
|
||||
"""Устанавливает карму пользователя (/karma set)"""
|
||||
|
||||
target_user = None
|
||||
new_karma_value = None
|
||||
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if await check_admin_status(bot, message) == 1:
|
||||
return
|
||||
|
||||
# /karma set 100 (ответом на сообщение)
|
||||
if len(parts) == 3 and message.reply_to_message:
|
||||
target_user = message.reply_to_message.from_user
|
||||
try:
|
||||
new_karma_value = int(parts[2])
|
||||
except ValueError:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text="❌ Неверный формат кармы. Укажите целое число.",
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# /karma set @username 100 или /karma set 123456789 100
|
||||
elif len(parts) >= 4:
|
||||
identifier = parts[2].strip()
|
||||
try:
|
||||
new_karma_value = int(parts[3])
|
||||
except ValueError:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text="❌ Неверный формат кармы. Укажите целое число.",
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
if identifier.isdigit():
|
||||
user_info = db.get_user(int(identifier))
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
elif identifier.startswith('@'):
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text="❌ Использование: /karma set @user 100",
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
if not target_user:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Нельзя устанавливать карму себе
|
||||
if message.from_user.id == target_user.id:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text="❌ Нельзя устанавливать карму самому себе.",
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Получаем текущую карму
|
||||
old_karma = db.get_karma(target_user.id, message.chat.id)
|
||||
|
||||
# Устанавливаем новую карму
|
||||
db.set_karma(target_user.id, message.chat.id, new_karma_value)
|
||||
|
||||
target_user_display = f"@{target_user.username}" if target_user.username else target_user.first_name
|
||||
|
||||
karma_diff = new_karma_value - old_karma
|
||||
diff_sign = "+" if karma_diff > 0 else ""
|
||||
|
||||
# Лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="УСТАНОВКА КАРМЫ",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=f"Карма изменена: {old_karma} → {new_karma_value} ({diff_sign}{karma_diff})",
|
||||
duration=None,
|
||||
)
|
||||
|
||||
response = (
|
||||
f"✅ Карма пользователя {target_user_display} установлена на <b>{new_karma_value}</b>\n"
|
||||
f"Было: {old_karma} → Стало: {new_karma_value} ({diff_sign}{karma_diff})"
|
||||
)
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=response,
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
|
||||
logger.info(f"Администратор {message.from_user.id} установил карму пользователя {target_user.id} на {new_karma_value}")
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
logger.error(f"Ошибка в karma_set: {str(e)}")
|
||||
|
||||
|
||||
async def disable_karma_in_topic(bot: AsyncTeleBot, message: Message):
|
||||
"""Отключает начисление кармы в текущем топике"""
|
||||
if not message.is_topic_message:
|
||||
await send_temp_message(bot, message, "❌ Эта команда работает только в топиках!")
|
||||
return
|
||||
|
||||
thread_id = message.message_thread_id
|
||||
chat_id = message.chat.id
|
||||
|
||||
if db.disable_karma_in_topic(chat_id, thread_id):
|
||||
await send_temp_message(
|
||||
bot, message,
|
||||
f"🚫 Начисление кармы отключено в этом топике (ID: {thread_id})"
|
||||
)
|
||||
logger.info(f"Администратор {message.from_user.id} отключил карму в топике {thread_id}")
|
||||
else:
|
||||
await send_temp_message(bot, message, "⚠️ Карма уже отключена в этом топике")
|
||||
|
||||
|
||||
async def enable_karma_in_topic(bot: AsyncTeleBot, message: Message):
|
||||
"""Включает начисление кармы в текущем топике"""
|
||||
if not message.is_topic_message:
|
||||
await send_temp_message(bot, message, "❌ Эта команда работает только в топиках!")
|
||||
return
|
||||
|
||||
thread_id = message.message_thread_id
|
||||
chat_id = message.chat.id
|
||||
|
||||
if db.enable_karma_in_topic(chat_id, thread_id):
|
||||
await send_temp_message(
|
||||
bot, message,
|
||||
f"✅ Начисление кармы включено в этом топике (ID: {thread_id})"
|
||||
)
|
||||
logger.info(f"Администратор {message.from_user.id} включил карму в топике {thread_id}")
|
||||
else:
|
||||
await send_temp_message(bot, message, "⚠️ Карма уже включена в этом топике")
|
||||
|
||||
|
||||
async def show_karma_status(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает статус кармы во всех топиках чата"""
|
||||
chat_id = message.chat.id
|
||||
|
||||
disabled_topics = db.get_disabled_topics(chat_id)
|
||||
|
||||
if not disabled_topics:
|
||||
text = "✅ Карма включена во всех топиках"
|
||||
else:
|
||||
text = f"📊 <b>Статус кармы в топиках</b>\n\n"
|
||||
text += f"🚫 Отключено в топиках (ID): {', '.join(map(str, disabled_topics))}\n"
|
||||
text += f"\n💡 Используйте <code>/karma enable</code> в топике, чтобы включить карму"
|
||||
|
||||
await send_temp_message(bot, message, text, time_sleep=20)
|
||||
|
||||
|
||||
async def send_temp_message(bot: AsyncTeleBot, message: Message, text: str, time_sleep: int = 10):
|
||||
"""Отправляет временное сообщение"""
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=text,
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=time_sleep, number_message=2)
|
||||
@@ -0,0 +1,87 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message
|
||||
import logging
|
||||
import re
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Функция для отправки сообщения с инструкцией по логам
|
||||
async def send_log_instruction(bot: AsyncTeleBot, message: Message):
|
||||
try:
|
||||
# Отправляем сообщение со ссылкой на инструкцию
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['log'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
except Exception as e:
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Ошибка отправки инструкции по логам: {str(e)}")
|
||||
|
||||
# Функция проверки наличия триггерных фраз
|
||||
def contains_log_trigger(text: str) -> bool:
|
||||
if not text:
|
||||
return False
|
||||
|
||||
# Список триггерных фраз (регистронезависимый поиск)
|
||||
triggers = [
|
||||
r'лог\?',
|
||||
r'приложите\s+лог',
|
||||
r'приложи\s+лог'
|
||||
]
|
||||
|
||||
text_lower = text.lower()
|
||||
|
||||
for trigger in triggers:
|
||||
if re.search(trigger, text_lower):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# Регистрирует все обработчики команд
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
# Обработчик команды /log
|
||||
@bot.message_handler(commands=['log'])
|
||||
async def log_command(message: Message):
|
||||
|
||||
try:
|
||||
|
||||
# Отправляем инструкцию
|
||||
await send_log_instruction(bot, message)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Пользователь {message.from_user.id} запустил /log.")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Общая ошибка в log_command: {str(e)}")
|
||||
|
||||
# Автоматический обработчик триггерных фраз
|
||||
@bot.message_handler(func=lambda message: message.content_type == 'text' and contains_log_trigger(message.text))
|
||||
async def auto_log_trigger(message: Message):
|
||||
|
||||
try:
|
||||
|
||||
# Отправляем инструкцию
|
||||
await send_log_instruction(bot, message)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Автоматически отправлена инструкция по логам в ответ на сообщение от {message.from_user.id}.")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Ошибка в auto_log_trigger: {str(e)}")
|
||||
+289
-212
@@ -1,6 +1,5 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, User, ChatPermissions
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from database import db
|
||||
@@ -12,6 +11,8 @@ from utils import (
|
||||
check_target_status,
|
||||
parse_mute_time,
|
||||
format_mute_time,
|
||||
format_user_mention,
|
||||
delete_moderation_messages,
|
||||
)
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
@@ -19,131 +20,268 @@ from config import COMMAND_MESSAGES
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Регистрирует все обработчики команд
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
# Обработчик команды /mute
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
"""Регистрирует обработчик команды /mute"""
|
||||
|
||||
@bot.message_handler(commands=['mute'])
|
||||
async def _mute_command_wrapper(message: Message):
|
||||
parts = message.text.split()
|
||||
|
||||
# Определяем подкоманду
|
||||
if len(parts) >= 2:
|
||||
subcommand = parts[1].lower()
|
||||
|
||||
if subcommand in ('help', 'помощь'):
|
||||
await mute_help(bot, message)
|
||||
return
|
||||
elif subcommand == 'off':
|
||||
await mute_off(bot, message, parts)
|
||||
return
|
||||
elif subcommand == 'reset':
|
||||
await mute_reset(bot, message, parts)
|
||||
return
|
||||
|
||||
# Основная команда мута
|
||||
await mute_command(bot, message)
|
||||
|
||||
# Основная функция команды /mute
|
||||
async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = None):
|
||||
|
||||
# Определяем целевого пользователя
|
||||
async def mute_help(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает справку по команде /mute"""
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['manual_mute'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
|
||||
|
||||
async def mute_off(bot: AsyncTeleBot, message: Message, parts: list):
|
||||
"""Снимает мут с пользователя (/mute off)"""
|
||||
|
||||
target_user = None
|
||||
|
||||
# Отпределяем время
|
||||
time_arg = None
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if await check_admin_status(bot, message) == 1:
|
||||
return
|
||||
|
||||
# Определяем причину
|
||||
reason = None
|
||||
# /mute off (ответом на сообщение)
|
||||
if len(parts) == 2:
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
target_user = message.reply_to_message.from_user
|
||||
else:
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Разбиваем текст сообщения на части
|
||||
parts_msg = message.text.split()
|
||||
# /mute off @username или /mute off 123456789
|
||||
elif len(parts) >= 3:
|
||||
identifier = parts[2].strip()
|
||||
|
||||
# Команда /mute help
|
||||
if len(parts_msg) == 2 and parts_msg[1].strip() in ('help', 'помощь'):
|
||||
if identifier.isdigit():
|
||||
user_info = db.get_user(int(identifier))
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
elif identifier.startswith('@'):
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
if not target_user:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Снимаем ограничения
|
||||
permissions = ChatPermissions(
|
||||
can_send_messages=True,
|
||||
can_send_media_messages=True,
|
||||
can_send_polls=True,
|
||||
can_send_other_messages=True,
|
||||
can_add_web_page_previews=True,
|
||||
can_change_info=False,
|
||||
can_invite_users=True,
|
||||
can_pin_messages=False
|
||||
)
|
||||
|
||||
await bot.restrict_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
permissions=permissions,
|
||||
)
|
||||
|
||||
# Лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="РАЗМУТ",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=None,
|
||||
duration=None,
|
||||
)
|
||||
|
||||
# Отправляем инструкцию
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['manual_mute'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['unmuted'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 30 секунд
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
return
|
||||
logger.info(f"Пользователь {target_user.id} размучен администратором {message.from_user.id}")
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
logger.error(f"Ошибка в mute_off: {str(e)}")
|
||||
|
||||
|
||||
async def mute_reset(bot: AsyncTeleBot, message: Message, parts: list):
|
||||
"""Сбрасывает счётчик автомутов пользователя (/mute reset)"""
|
||||
|
||||
target_user_id = None
|
||||
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if await check_admin_status(bot, message, check_restrict_rights=False) == 1:
|
||||
return
|
||||
|
||||
# Проверяем, является ли отправитель администратором
|
||||
# /mute reset (ответом на сообщение)
|
||||
if len(parts) == 2:
|
||||
if message.reply_to_message:
|
||||
target_user_id = message.reply_to_message.from_user.id
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text="❌ Укажите пользователя или ответьте на его сообщение",
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# /mute reset @username или /mute reset 123456789
|
||||
elif len(parts) >= 3:
|
||||
identifier = parts[2].strip()
|
||||
|
||||
if identifier.startswith('@'):
|
||||
user_data = db.get_user_by_username(identifier[1:])
|
||||
if user_data:
|
||||
target_user_id = user_data[0]
|
||||
elif identifier.isdigit():
|
||||
target_user_id = int(identifier)
|
||||
|
||||
if not target_user_id:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Сбрасываем нарушения
|
||||
deleted_count = db.reset_user_violations(target_user_id, message.chat.id)
|
||||
|
||||
# Получаем информацию о пользователе
|
||||
user_info = db.get_user(target_user_id)
|
||||
if user_info:
|
||||
_, nickname, tag = user_info
|
||||
user_display = f"<b>{nickname}</b>"
|
||||
if tag:
|
||||
user_display += f" (@{tag})"
|
||||
else:
|
||||
user_display = f"<code>{target_user_id}</code>"
|
||||
|
||||
response = (
|
||||
f"✅ Счётчик автомутов сброшен\n\n"
|
||||
f"👤 Пользователь: {user_display}\n"
|
||||
f"📊 Удалено записей: <b>{deleted_count}</b>"
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=response,
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
|
||||
logger.info(f"Администратор {message.from_user.id} сбросил счётчик автомутов пользователя {target_user_id}")
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
logger.error(f"Ошибка в mute_reset: {str(e)}")
|
||||
|
||||
|
||||
async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = None):
|
||||
"""Основная функция мута пользователя"""
|
||||
|
||||
target_user = None
|
||||
time_arg = None
|
||||
reason = None
|
||||
|
||||
parts_msg = message.text.split()
|
||||
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if await check_admin_status(bot, message) == 1:
|
||||
return
|
||||
|
||||
# Если одно слово (/mute)
|
||||
if len(parts_msg) == 1:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Команда через ответ на сообщение, если два слова (/mute 2m)
|
||||
elif len(parts_msg) == 2:
|
||||
|
||||
# Если это топик
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Если с ответом на сообщение
|
||||
else:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
time_arg = parts_msg[1]
|
||||
reason = 'отсутствует'
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
|
||||
# Собираем данные
|
||||
# /mute 2m (ответом на сообщение)
|
||||
if len(parts_msg) == 2:
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
target_user = message.reply_to_message.from_user
|
||||
time_arg = parts_msg[1]
|
||||
reason = 'отсутствует'
|
||||
message.message_thread_id = None
|
||||
|
||||
# Если команда неправильная
|
||||
else:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# В сообщении больше двух слов
|
||||
# Более двух слов
|
||||
else:
|
||||
|
||||
# Если второе слово это тег или ID
|
||||
# /mute @user 1ч причина или /mute 123456789 1ч причина
|
||||
if parts_msg[1].strip().isdigit() or parts_msg[1].startswith('@'):
|
||||
|
||||
# Собираем данные
|
||||
identifier = parts_msg[1].strip()
|
||||
time_arg = parts_msg[2]
|
||||
reason = ' '.join(parts_msg[3:]) if parts_msg[3:] != [] else 'отсутствует'
|
||||
time_arg = parts_msg[2] if len(parts_msg) > 2 else None
|
||||
reason = ' '.join(parts_msg[3:]) if len(parts_msg) > 3 else 'отсутствует'
|
||||
|
||||
# Делаем поиск по ID
|
||||
if identifier.isdigit():
|
||||
|
||||
# Ищем пользователя в базе данных
|
||||
user_info = db.get_user(int(identifier))
|
||||
|
||||
# Если нашли пользователя
|
||||
if user_info:
|
||||
|
||||
# Создаем объект пользователя
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Делаем поиск по тегу
|
||||
elif identifier.startswith('@'):
|
||||
|
||||
# Ищем пользователя в базе данных (убрали @)
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
|
||||
# Если нашли пользователя
|
||||
if user_info:
|
||||
|
||||
# Создаем объект пользователя
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
@@ -151,99 +289,53 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Команда через ответ на сообщение с указанием причины
|
||||
# /mute 1ч причина (ответом на сообщение)
|
||||
else:
|
||||
|
||||
# Если это топик
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Если с ответом на сообщение
|
||||
else:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
time_arg = parts_msg[1]
|
||||
reason = ' '.join(parts_msg[2:])
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
|
||||
# Собираем данные
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
target_user = message.reply_to_message.from_user
|
||||
time_arg = parts_msg[1]
|
||||
reason = ' '.join(parts_msg[2:])
|
||||
message.message_thread_id = None
|
||||
|
||||
# Если команда неправильная
|
||||
else:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Если пользователь не найден в базе данных
|
||||
if not target_user:
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Парсинг времени мута
|
||||
mute_seconds = parse_mute_time(time_arg)
|
||||
# Парсинг времени
|
||||
mute_seconds = parse_mute_time(time_arg) if time_arg else None
|
||||
|
||||
# Если не указали время
|
||||
if mute_seconds is None:
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['incorrect_time_format'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['incorrect_time_format'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Минимальное время мута - 1 минута (60 секунд)
|
||||
if mute_seconds < 60:
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['min_mute'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['min_mute'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Максимальное время мута - 30 дней (2592000 секунд)
|
||||
if mute_seconds > 2592000:
|
||||
|
||||
# Отправляем предупреждение
|
||||
from config import MAX_MUTE_TIME
|
||||
if mute_seconds > MAX_MUTE_TIME:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['max_mute'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['max_mute'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
@@ -251,83 +343,68 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
if await check_target_status(bot, message, target_user) == 1:
|
||||
return
|
||||
|
||||
try:
|
||||
# Выполняем мут
|
||||
until_date = int(time.time()) + mute_seconds
|
||||
|
||||
# Вычисляем время окончания мута
|
||||
until_date = int(time.time()) + mute_seconds
|
||||
|
||||
# Устанавливаем ограничения (только чтение)
|
||||
permissions = ChatPermissions(
|
||||
can_send_messages=False,
|
||||
can_send_media_messages=False,
|
||||
can_send_polls=False,
|
||||
can_send_other_messages=False,
|
||||
can_add_web_page_previews=False,
|
||||
can_change_info=False,
|
||||
can_invite_users=False,
|
||||
can_pin_messages=False,
|
||||
)
|
||||
|
||||
# Выполняем мут
|
||||
await bot.restrict_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
permissions=permissions,
|
||||
until_date=until_date
|
||||
)
|
||||
|
||||
# Форматируем время в удобный формат
|
||||
time_display = format_mute_time(mute_seconds)
|
||||
|
||||
# Отправляем сообщение-лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="МУТ",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=reason,
|
||||
duration=time_display,
|
||||
photo_path=photo_path,
|
||||
)
|
||||
|
||||
# Отправляем сообщение, что пользователь получил мут
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['muted'].format(time_display=time_display),
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Пользователь {target_user.id} получил мут на {time_display} от администратора {message.from_user.id}.")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['error'].format(e=str(e)),
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Ошибка мута: {str(e)}")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
permissions = ChatPermissions(
|
||||
can_send_messages=False,
|
||||
can_send_media_messages=False,
|
||||
can_send_polls=False,
|
||||
can_send_other_messages=False,
|
||||
can_add_web_page_previews=False,
|
||||
can_change_info=False,
|
||||
can_invite_users=False,
|
||||
can_pin_messages=False,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Общая ошибка в mute_command: {str(e)}")
|
||||
await bot.restrict_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
permissions=permissions,
|
||||
until_date=until_date
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
# Снимаем карму
|
||||
db.add_karma(target_user.id, message.chat.id, -10)
|
||||
logger.info(f"Снято 10 кармы пользователю {target_user.id} за мут")
|
||||
|
||||
time_display = format_mute_time(mute_seconds)
|
||||
target_mention = format_user_mention(target_user)
|
||||
admin_mention = format_user_mention(message.from_user)
|
||||
|
||||
# Лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="МУТ",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=reason,
|
||||
duration=time_display,
|
||||
photo_path=photo_path,
|
||||
source_message=message.reply_to_message,
|
||||
)
|
||||
|
||||
# Удаляем команду и сообщение нарушителя
|
||||
await delete_moderation_messages(bot, message)
|
||||
|
||||
# Отправляем сообщение о муте
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['muted'].format(
|
||||
target_user=target_mention,
|
||||
admin_user=admin_mention,
|
||||
time_display=time_display,
|
||||
reason=reason,
|
||||
),
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
|
||||
logger.info(f"Пользователь {target_user.id} получил мут на {time_display} от администратора {message.from_user.id}")
|
||||
|
||||
except Exception as e:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
logger.error(f"Ошибка в mute_command: {str(e)}")
|
||||
@@ -1,202 +0,0 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, User, ChatPermissions
|
||||
import asyncio
|
||||
import logging
|
||||
from database import db
|
||||
|
||||
from action_reporter import action_reporter
|
||||
from utils import (
|
||||
delete_messages,
|
||||
check_admin_status,
|
||||
check_target_status,
|
||||
)
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Регистрирует все обработчики команд
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
# Обработчик команды /unban
|
||||
@bot.message_handler(commands=['unban'])
|
||||
async def unban_command(message: Message):
|
||||
|
||||
# Определяем целевого пользователя
|
||||
target_user = None
|
||||
|
||||
# Разбиваем текст сообщения на части
|
||||
parts_msg = message.text.split()
|
||||
|
||||
# Команда /unban help
|
||||
if len(parts_msg) == 2 and parts_msg[1].strip() in ('help', 'помощь'):
|
||||
|
||||
# Отправляем инструкцию
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['manual_unban'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 30 секунд
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
# Проверяем, является ли отправитель администратором
|
||||
if await check_admin_status(bot, message) == 1:
|
||||
return
|
||||
|
||||
# Команда через ответ на сообщение, если одно слово (/unban)
|
||||
if len(parts_msg) == 1:
|
||||
|
||||
# Если это топик
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Если с ответом на сообщение
|
||||
else:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
message.message_thread_id = None
|
||||
|
||||
# Удаляем сообщение, если команда неправильная
|
||||
else:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Команда через тег или ID, если два слова
|
||||
elif len(parts_msg) == 2:
|
||||
|
||||
# Собираем данные
|
||||
identifier = parts_msg[1].strip()
|
||||
|
||||
# Делаем поиск по ID
|
||||
if identifier.isdigit():
|
||||
|
||||
# Ищем пользователя в базе данных
|
||||
user_info = db.get_user(int(identifier))
|
||||
|
||||
# Если нашли пользователя
|
||||
if user_info:
|
||||
|
||||
# Создаем объект пользователя
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Делаем поиск по тегу
|
||||
elif identifier.startswith('@'):
|
||||
|
||||
# Ищем пользователя в базе данных
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
|
||||
# Если нашли пользователя
|
||||
if user_info:
|
||||
|
||||
# Создаем объект пользователя
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Если пользователь не найден в базе данных
|
||||
if not target_user:
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Проверяем статус целевого пользователя
|
||||
if await check_target_status(bot, message, target_user) == 1:
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
# Выполняем разбан
|
||||
await bot.unban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
)
|
||||
|
||||
# Отправляем лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="РАЗБАН",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=None,
|
||||
duration=None,
|
||||
photo_path=None,
|
||||
)
|
||||
|
||||
# Отправляем сообщения, что пользователь получил разбан
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['unbanned'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Пользователь {target_user.id} разбанен администратором {message.from_user.id}.")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['error'].format(e=str(e)),
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Ошибка размута: {str(e)}")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Общая ошибка в unban_command: {str(e)}")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
@@ -1,215 +0,0 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, User, ChatPermissions
|
||||
import asyncio
|
||||
import logging
|
||||
from database import db
|
||||
|
||||
from action_reporter import action_reporter
|
||||
from utils import (
|
||||
delete_messages,
|
||||
check_admin_status,
|
||||
check_target_status,
|
||||
)
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Регистрирует все обработчики команд
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
# Обработчик команды /unmute
|
||||
@bot.message_handler(commands=['unmute'])
|
||||
async def unmute_command(message: Message):
|
||||
|
||||
# Определяем целевого пользователя
|
||||
target_user = None
|
||||
|
||||
# Разбиваем текст сообщения на части
|
||||
parts_msg = message.text.split()
|
||||
|
||||
# Команда /unmute help
|
||||
if len(parts_msg) == 2 and parts_msg[1].strip() in ('help', 'помощь'):
|
||||
|
||||
# Отправляем инструкцию
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['manual_unmute'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 30 секунд
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
# Проверяем, является ли отправитель администратором
|
||||
if await check_admin_status(bot, message) == 1:
|
||||
return
|
||||
|
||||
# Команда через ответ на сообщение, если одно слово (/unmute)
|
||||
if len(parts_msg) == 1:
|
||||
|
||||
# Если это топик
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Если с ответом на сообщение
|
||||
else:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
message.message_thread_id = None
|
||||
|
||||
# Удаляем сообщение, если команда неправильная
|
||||
else:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Команда через тег или ID, если два слова
|
||||
elif len(parts_msg) == 2:
|
||||
|
||||
# Собираем данные
|
||||
identifier = parts_msg[1].strip()
|
||||
|
||||
# Делаем поиск по ID
|
||||
if identifier.isdigit():
|
||||
|
||||
# Ищем пользователя в базе данных
|
||||
user_info = db.get_user(int(identifier))
|
||||
|
||||
# Если нашли пользователя
|
||||
if user_info:
|
||||
|
||||
# Создаем объект пользователя
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Делаем поиск по тегу
|
||||
elif identifier.startswith('@'):
|
||||
|
||||
# Ищем пользователя в базе данных
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
|
||||
# Если нашли пользователя
|
||||
if user_info:
|
||||
|
||||
# Создаем объект пользователя
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# Если пользователь не найден в базе данных
|
||||
if not target_user:
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Проверяем статус целевого пользователя
|
||||
if await check_target_status(bot, message, target_user) == 1:
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
# Убираем ограничения (можно писать в чат)
|
||||
permissions = ChatPermissions(
|
||||
can_send_messages=True,
|
||||
can_send_media_messages=True,
|
||||
can_send_polls=True,
|
||||
can_send_other_messages=True,
|
||||
can_add_web_page_previews=True,
|
||||
can_change_info=False,
|
||||
can_invite_users=True,
|
||||
can_pin_messages=False
|
||||
)
|
||||
|
||||
# Выполняем размут
|
||||
await bot.restrict_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
permissions=permissions,
|
||||
)
|
||||
|
||||
# Отправляем лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="РАЗМУТ",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=None,
|
||||
duration=None,
|
||||
photo_path=None,
|
||||
)
|
||||
|
||||
# Отправляем сообщения, что пользователь получил размут
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['unmuted'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Пользователь {target_user.id} получил размут от администратора {message.from_user.id}.")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['error'].format(e=str(e)),
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Ошибка размута: {str(e)}")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем ошибку в логи
|
||||
logger.error(f"Общая ошибка в unmute_command: {str(e)}")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
@@ -0,0 +1,336 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, User, ChatPermissions
|
||||
import logging
|
||||
import time
|
||||
|
||||
from database import db
|
||||
from action_reporter import action_reporter
|
||||
from utils import (
|
||||
delete_messages,
|
||||
check_admin_status,
|
||||
check_target_status,
|
||||
format_user_mention,
|
||||
delete_moderation_messages,
|
||||
)
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalize_command_text(text: str, command: str) -> str:
|
||||
"""Удаляет @botname из команды, оставляя только /command"""
|
||||
if not text:
|
||||
return text
|
||||
parts = text.split()
|
||||
if not parts:
|
||||
return text
|
||||
first = parts[0]
|
||||
if '@' in first:
|
||||
# Разделяем только первое слово по @
|
||||
cmd_part = first.split('@', 1)[0]
|
||||
return ' '.join([cmd_part] + parts[1:])
|
||||
return text
|
||||
|
||||
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
"""Регистрирует обработчик команды /warn"""
|
||||
|
||||
@bot.message_handler(commands=['warn'])
|
||||
async def _warn_command_wrapper(message: Message):
|
||||
parts = message.text.split()
|
||||
|
||||
# Определяем подкоманду
|
||||
if len(parts) >= 2:
|
||||
subcommand = parts[1].lower()
|
||||
|
||||
if subcommand in ('help', 'помощь'):
|
||||
await warn_help(bot, message)
|
||||
return
|
||||
elif subcommand == 'reset':
|
||||
await warn_reset(bot, message, parts)
|
||||
return
|
||||
|
||||
# Основная команда варна
|
||||
await warn_command(bot, message)
|
||||
|
||||
|
||||
async def warn_help(bot: AsyncTeleBot, message: Message):
|
||||
"""Показывает справку по команде /warn"""
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['manual_warn'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
|
||||
|
||||
async def warn_reset(bot: AsyncTeleBot, message: Message, parts: list):
|
||||
"""Сбрасывает счётчик варнов пользователя (/warn reset)"""
|
||||
|
||||
target_user_id = None
|
||||
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if await check_admin_status(bot, message, check_restrict_rights=False) == 1:
|
||||
return
|
||||
|
||||
# /warn reset (ответом на сообщение)
|
||||
if len(parts) == 2:
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
target_user_id = message.reply_to_message.from_user.id
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text="❌ Укажите пользователя или ответьте на его сообщение",
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# /warn reset @username или /warn reset 123456789
|
||||
elif len(parts) >= 3:
|
||||
identifier = parts[2].strip()
|
||||
|
||||
if identifier.startswith('@'):
|
||||
user_data = db.get_user_by_username(identifier[1:])
|
||||
if user_data:
|
||||
target_user_id = user_data[0]
|
||||
elif identifier.isdigit():
|
||||
target_user_id = int(identifier)
|
||||
|
||||
if not target_user_id:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Сбрасываем варны
|
||||
deleted_count = db.reset_user_warnings(target_user_id, message.chat.id)
|
||||
|
||||
# Получаем информацию о пользователе
|
||||
user_info = db.get_user(target_user_id)
|
||||
if user_info:
|
||||
_, nickname, tag = user_info
|
||||
user_display = f"<b>{nickname}</b>"
|
||||
if tag:
|
||||
user_display += f" (@{tag})"
|
||||
else:
|
||||
user_display = f"<code>{target_user_id}</code>"
|
||||
|
||||
response = (
|
||||
f"✅ Счётчик предупреждений сброшен\n\n"
|
||||
f"👤 Пользователь: {user_display}\n"
|
||||
f"📊 Удалено варнов: <b>{deleted_count}</b>"
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=response,
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
|
||||
logger.info(f"Администратор {message.from_user.id} сбросил счётчик варнов пользователя {target_user_id}")
|
||||
await delete_messages(bot, message, time_sleep=30, number_message=2)
|
||||
|
||||
except Exception as e:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
logger.error(f"Ошибка в warn_reset: {str(e)}")
|
||||
|
||||
|
||||
async def warn_command(bot: AsyncTeleBot, message: Message):
|
||||
"""Основная функция выдачи предупреждения"""
|
||||
|
||||
target_user = None
|
||||
reason = None
|
||||
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if await check_admin_status(bot, message) == 1:
|
||||
return
|
||||
|
||||
# Нормализуем текст команды (убираем @botname)
|
||||
normalized_text = _normalize_command_text(message.text, 'warn')
|
||||
parts_msg = normalized_text.split()
|
||||
|
||||
logger.info(
|
||||
f"Обработка /warn: текст='{message.text}', нормализован='{normalized_text}', "
|
||||
f"reply={message.reply_to_message is not None}, parts={parts_msg}"
|
||||
)
|
||||
|
||||
# /warn причина (ответом на сообщение)
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
target_user = message.reply_to_message.from_user
|
||||
reason = ' '.join(parts_msg[1:]) if len(parts_msg) > 1 else 'отсутствует'
|
||||
logger.info(f"Целевой пользователь определен через reply: {target_user.id}, причина: {reason}")
|
||||
|
||||
# /warn @user причина или /warn 123456789 причина
|
||||
elif len(parts_msg) >= 2 and (parts_msg[1].strip().isdigit() or parts_msg[1].startswith('@')):
|
||||
identifier = parts_msg[1].strip()
|
||||
reason = ' '.join(parts_msg[2:]) if len(parts_msg) > 2 else 'отсутствует'
|
||||
|
||||
if identifier.isdigit():
|
||||
user_info = db.get_user(int(identifier))
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
elif identifier.startswith('@'):
|
||||
user_info = db.get_user_by_username(identifier[1:])
|
||||
if user_info:
|
||||
target_user = User(
|
||||
id=user_info[0],
|
||||
first_name=user_info[1],
|
||||
username=user_info[2],
|
||||
is_bot=False
|
||||
)
|
||||
|
||||
# /warn (без аргументов, не reply)
|
||||
elif len(parts_msg) == 1:
|
||||
logger.info("Команда /warn без аргументов и не в reply, удаляем")
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
else:
|
||||
logger.info("Нераспознанный формат /warn, удаляем")
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
if not target_user:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['user_not_found'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Проверяем статус целевого пользователя
|
||||
if await check_target_status(bot, message, target_user) == 1:
|
||||
return
|
||||
|
||||
# Добавляем предупреждение в БД
|
||||
db.add_warning(
|
||||
user_id=target_user.id,
|
||||
chat_id=message.chat.id,
|
||||
reason=reason,
|
||||
admin_id=message.from_user.id
|
||||
)
|
||||
|
||||
# Снимаем карму
|
||||
db.add_karma(target_user.id, message.chat.id, -5)
|
||||
logger.info(f"Снято 5 кармы пользователю {target_user.id} за предупреждение")
|
||||
|
||||
from config import ONE_WEEK, TWO_WEEKS
|
||||
|
||||
warns_week = db.get_warnings_count(target_user.id, message.chat.id, ONE_WEEK)
|
||||
warns_two_weeks = db.get_warnings_count(target_user.id, message.chat.id, TWO_WEEKS)
|
||||
|
||||
logger.info(f"Предупреждений за неделю: {warns_week}, за 2 недели: {warns_two_weeks}")
|
||||
|
||||
target_mention = format_user_mention(target_user)
|
||||
admin_mention = format_user_mention(message.from_user)
|
||||
|
||||
mute_applied = False
|
||||
mute_duration = 0
|
||||
mute_duration_text = ""
|
||||
response_message = COMMAND_MESSAGES['warned'].format(
|
||||
target_user=target_mention,
|
||||
admin_user=admin_mention,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
# 2+ варна за неделю -> мут на 7 дней
|
||||
if warns_week >= 2:
|
||||
mute_duration = 604800
|
||||
mute_duration_text = "7 дней"
|
||||
response_message = COMMAND_MESSAGES['warned_auto_mute_week'].format(
|
||||
target_user=target_mention,
|
||||
admin_user=admin_mention,
|
||||
reason=reason,
|
||||
)
|
||||
mute_applied = True
|
||||
logger.info(f"Применен мут на неделю (предупреждений за неделю: {warns_week})")
|
||||
|
||||
# 2 варна за 2 недели -> мут на 1 день
|
||||
elif warns_two_weeks >= 2:
|
||||
mute_duration = 86400
|
||||
mute_duration_text = "1 день"
|
||||
response_message = COMMAND_MESSAGES['warned_auto_mute_day'].format(
|
||||
target_user=target_mention,
|
||||
admin_user=admin_mention,
|
||||
reason=reason,
|
||||
)
|
||||
mute_applied = True
|
||||
logger.info(f"Применен мут на сутки (предупреждений за 2 недели: {warns_two_weeks})")
|
||||
|
||||
# Применяем мут
|
||||
if mute_applied:
|
||||
try:
|
||||
until_date = int(time.time()) + mute_duration
|
||||
|
||||
permissions = ChatPermissions(
|
||||
can_send_messages=False,
|
||||
can_send_media_messages=False,
|
||||
can_send_polls=False,
|
||||
can_send_other_messages=False,
|
||||
can_add_web_page_previews=False,
|
||||
can_change_info=False,
|
||||
can_invite_users=False,
|
||||
can_pin_messages=False,
|
||||
)
|
||||
|
||||
await bot.restrict_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
permissions=permissions,
|
||||
until_date=until_date
|
||||
)
|
||||
|
||||
logger.info(f"Пользователь {target_user.id} получил автомут на {mute_duration_text} после варна")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при применении мута после варна: {str(e)}")
|
||||
|
||||
# Лог в админ-чат
|
||||
await action_reporter.log_action(
|
||||
action="ВАРН" if not mute_applied else f"ВАРН + МУТ ({mute_duration_text})",
|
||||
user_id=target_user.id,
|
||||
admin_id=message.from_user.id,
|
||||
reason=reason,
|
||||
duration=mute_duration_text if mute_applied else None,
|
||||
source_message=message.reply_to_message,
|
||||
)
|
||||
|
||||
# Удаляем команду и сообщение нарушителя
|
||||
await delete_moderation_messages(bot, message)
|
||||
|
||||
# Отправляем сообщение о варне
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=response_message,
|
||||
message_thread_id=message.message_thread_id,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
|
||||
logger.info(f"Администратор {message.from_user.id} выдал предупреждение пользователю {target_user.id}. Причина: {reason}")
|
||||
|
||||
except Exception as e:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['general_error'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
logger.error(f"Ошибка в warn_command: {str(e)}")
|
||||
@@ -0,0 +1,109 @@
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Путь к файлу с благодарственными словами
|
||||
THANK_WORDS_FILE = os.path.join(os.path.dirname(__file__), 'data', 'thank_words.json')
|
||||
|
||||
# Кэш для быстрой проверки
|
||||
_thank_words_cache = None
|
||||
|
||||
def _load_thank_words():
|
||||
"""Загружает список благодарственных слов из файла"""
|
||||
global _thank_words_cache
|
||||
try:
|
||||
with open(THANK_WORDS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
_thank_words_cache = [word.lower() for word in data.get('thank_words', [])]
|
||||
logger.info(f"Загружено {len(_thank_words_cache)} благодарственных слов")
|
||||
return _thank_words_cache
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Файл {THANK_WORDS_FILE} не найден")
|
||||
_thank_words_cache = []
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Ошибка разбора JSON: {e}")
|
||||
_thank_words_cache = []
|
||||
return []
|
||||
|
||||
def get_thank_words():
|
||||
"""Возвращает список благодарственных слов (с кэшированием)"""
|
||||
global _thank_words_cache
|
||||
if _thank_words_cache is None:
|
||||
_load_thank_words()
|
||||
return _thank_words_cache
|
||||
|
||||
def contains_thank_word(text: str) -> bool:
|
||||
"""
|
||||
Проверяет, содержит ли текст благодарственные слова
|
||||
|
||||
Args:
|
||||
text: Текст для проверки
|
||||
|
||||
Returns:
|
||||
True если найдено хотя бы одно благодарственное слово
|
||||
"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
text_lower = text.lower()
|
||||
thank_words = get_thank_words()
|
||||
|
||||
# Разбиваем текст на слова для проверки
|
||||
words = text_lower.split()
|
||||
|
||||
# Проверяем каждое слово и фразы из 2 слов
|
||||
for i, word in enumerate(words):
|
||||
# Очищаем от знаков препинания
|
||||
clean_word = ''.join(c for c in word if c.isalnum())
|
||||
if clean_word in thank_words:
|
||||
return True
|
||||
|
||||
# Проверяем фразы из двух слов (например, "thank you")
|
||||
if i < len(words) - 1:
|
||||
two_word_phrase = clean_word + ' ' + ''.join(c for c in words[i+1] if c.isalnum())
|
||||
if two_word_phrase in thank_words:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_thank_words_from_text(text: str) -> list:
|
||||
"""
|
||||
Возвращает список найденных благодарственных слов в тексте
|
||||
|
||||
Args:
|
||||
text: Текст для анализа
|
||||
|
||||
Returns:
|
||||
Список найденных благодарственных слов
|
||||
"""
|
||||
if not text:
|
||||
return []
|
||||
|
||||
text_lower = text.lower()
|
||||
thank_words = get_thank_words()
|
||||
found_words = []
|
||||
|
||||
words = text_lower.split()
|
||||
|
||||
for i, word in enumerate(words):
|
||||
clean_word = ''.join(c for c in word if c.isalnum())
|
||||
if clean_word in thank_words and clean_word not in found_words:
|
||||
found_words.append(clean_word)
|
||||
|
||||
# Проверяем фразы из двух слов
|
||||
if i < len(words) - 1:
|
||||
clean_next = ''.join(c for c in words[i+1] if c.isalnum())
|
||||
two_word_phrase = clean_word + ' ' + clean_next
|
||||
if two_word_phrase in thank_words and two_word_phrase not in found_words:
|
||||
found_words.append(two_word_phrase)
|
||||
|
||||
return found_words
|
||||
|
||||
def reload_thank_words():
|
||||
"""Перезагружает список благодарственных слов из файла"""
|
||||
global _thank_words_cache
|
||||
_thank_words_cache = None
|
||||
return _load_thank_words()
|
||||
+177
-30
@@ -1,35 +1,109 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message
|
||||
from telebot.types import Message, User
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Удаляет определённое количество сообщения
|
||||
async def delete_messages(bot: AsyncTeleBot, message: Message, time_sleep: int, number_message: int):
|
||||
await asyncio.sleep(time_sleep)
|
||||
for i in range(number_message):
|
||||
await bot.delete_message(message.chat.id, message.message_id+i)
|
||||
# Удаляет определённое количество сообщения В ФОНЕ (не блокирует обработку других событий)
|
||||
async def delete_messages(bot: AsyncTeleBot, message: Message, time_sleep: int, number_message: int = None, message_ids: list = None):
|
||||
async def _delete_task():
|
||||
try:
|
||||
await asyncio.sleep(time_sleep)
|
||||
|
||||
# Формируем список ID для удаления
|
||||
ids_to_delete = []
|
||||
if message_ids:
|
||||
ids_to_delete.extend(message_ids)
|
||||
elif number_message is not None:
|
||||
for i in range(number_message):
|
||||
ids_to_delete.append(message.message_id + i)
|
||||
|
||||
for msg_id in set(ids_to_delete):
|
||||
try:
|
||||
await bot.delete_message(message.chat.id, msg_id)
|
||||
except Exception as e:
|
||||
logger.debug(f"Не удалось удалить сообщение {msg_id}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в задаче удаления сообщений: {e}")
|
||||
|
||||
# Запускаем удаление в фоне
|
||||
asyncio.create_task(_delete_task())
|
||||
|
||||
# Проверяет, является ли отправитель администратором
|
||||
async def check_admin_status(bot: AsyncTeleBot, message: Message):
|
||||
async def check_admin_status(bot: AsyncTeleBot, message: Message, check_restrict_rights: bool = True):
|
||||
|
||||
if message.reply_to_message and message.is_topic_message is None:
|
||||
message.message_thread_id = None
|
||||
# Сохраняем оригинальный thread_id для восстановления после проверки
|
||||
original_thread_id = message.message_thread_id
|
||||
|
||||
# ВАЖНО: Временно сбрасываем thread_id для проверки прав на уровне ВСЕЙ группы
|
||||
# Иначе Telegram вернет статус в контексте топика, а не группы
|
||||
message.message_thread_id = None
|
||||
|
||||
try:
|
||||
# Логируем параметры проверки
|
||||
logger.info(f"Проверка прав: chat_id={message.chat.id}, user_id={message.from_user.id}, "
|
||||
f"is_topic={message.is_topic_message}, original_thread_id={original_thread_id}")
|
||||
|
||||
# Получаем статус отправителя
|
||||
# Получаем статус отправителя на уровне ВСЕЙ группы (без thread_id)
|
||||
admin_status = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
|
||||
# Логируем полученный статус
|
||||
logger.info(f"Получен статус пользователя {message.from_user.id}: status={admin_status.status}, "
|
||||
f"can_restrict={getattr(admin_status, 'can_restrict_members', None)}")
|
||||
|
||||
# Детальное логирование всех прав (для отладки)
|
||||
logger.debug(f"Детальный статус пользователя {message.from_user.id}: "
|
||||
f"can_be_edited={getattr(admin_status, 'can_be_edited', None)}, "
|
||||
f"can_manage_chat={getattr(admin_status, 'can_manage_chat', None)}, "
|
||||
f"can_delete_messages={getattr(admin_status, 'can_delete_messages', None)}, "
|
||||
f"can_restrict_members={getattr(admin_status, 'can_restrict_members', None)}, "
|
||||
f"can_promote_members={getattr(admin_status, 'can_promote_members', None)}, "
|
||||
f"can_change_info={getattr(admin_status, 'can_change_info', None)}, "
|
||||
f"can_invite_users={getattr(admin_status, 'can_invite_users', None)}, "
|
||||
f"can_post_messages={getattr(admin_status, 'can_post_messages', None)}, "
|
||||
f"can_edit_messages={getattr(admin_status, 'can_edit_messages', None)}, "
|
||||
f"can_pin_messages={getattr(admin_status, 'can_pin_messages', None)}, "
|
||||
f"user={getattr(admin_status, 'user', None)}")
|
||||
|
||||
# ВАЖНО: В каналах администраторы могут иметь статус 'left', но при этом иметь права администратора
|
||||
# Проверяем наличие прав администратора через can_* поля
|
||||
is_admin = admin_status.status in ('administrator', 'creator')
|
||||
|
||||
logger.debug(f"Начальная проверка: is_admin={is_admin}, status={admin_status.status}")
|
||||
|
||||
# Для каналов: если статус 'left', но есть админские права - считаем администратором
|
||||
if admin_status.status == 'left':
|
||||
logger.debug(f"Обнаружен статус 'left', проверяем админские права...")
|
||||
# Проверяем наличие любых админских прав
|
||||
has_admin_rights = any([
|
||||
getattr(admin_status, 'can_be_edited', False),
|
||||
getattr(admin_status, 'can_manage_chat', False),
|
||||
getattr(admin_status, 'can_delete_messages', False),
|
||||
getattr(admin_status, 'can_restrict_members', False),
|
||||
getattr(admin_status, 'can_promote_members', False),
|
||||
getattr(admin_status, 'can_change_info', False),
|
||||
getattr(admin_status, 'can_invite_users', False),
|
||||
getattr(admin_status, 'can_post_messages', False),
|
||||
getattr(admin_status, 'can_edit_messages', False),
|
||||
getattr(admin_status, 'can_pin_messages', False),
|
||||
])
|
||||
if has_admin_rights:
|
||||
is_admin = True
|
||||
logger.info(f"Пользователь {message.from_user.id} имеет статус 'left', но обнаружены админские права")
|
||||
|
||||
# Проверка наличия прав администратора/создателя
|
||||
if admin_status.status not in ('administrator', 'creator'):
|
||||
if not is_admin:
|
||||
# Восстанавливаем thread_id для отправки сообщения в правильный топик
|
||||
message.message_thread_id = original_thread_id
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['no_admin_rights'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['no_admin_rights'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
@@ -37,13 +111,15 @@ async def check_admin_status(bot: AsyncTeleBot, message: Message):
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return 1
|
||||
|
||||
# Проверка права на ограничение участников
|
||||
if admin_status.status == 'administrator' and not admin_status.can_restrict_members:
|
||||
# Проверка права на ограничение участников (опционально)
|
||||
if check_restrict_rights and admin_status.status == 'administrator' and not admin_status.can_restrict_members:
|
||||
# Восстанавливаем thread_id для отправки сообщения в правильный топик
|
||||
message.message_thread_id = original_thread_id
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['no_restrict_rights'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['no_restrict_rights'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
@@ -51,12 +127,18 @@ async def check_admin_status(bot: AsyncTeleBot, message: Message):
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return 1
|
||||
|
||||
# Восстанавливаем thread_id после успешной проверки
|
||||
message.message_thread_id = original_thread_id
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
# Восстанавливаем thread_id для отправки сообщения об ошибке
|
||||
message.message_thread_id = original_thread_id
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['error'].format(e=str(e)),
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['error'].format(e=str(e)),
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
@@ -70,37 +152,48 @@ async def check_admin_status(bot: AsyncTeleBot, message: Message):
|
||||
# Проверяет статус целевого пользователя
|
||||
async def check_target_status(bot: AsyncTeleBot, message: Message, target_user):
|
||||
|
||||
if message.reply_to_message and message.is_topic_message is None:
|
||||
message.message_thread_id = None
|
||||
# Сохраняем оригинальный thread_id
|
||||
original_thread_id = message.message_thread_id
|
||||
|
||||
# Временно сбрасываем thread_id для проверки статуса на уровне ВСЕЙ группы
|
||||
message.message_thread_id = None
|
||||
|
||||
try:
|
||||
|
||||
# Получаем статус пользователя
|
||||
# Получаем статус пользователя на уровне ВСЕЙ группы
|
||||
target_status = await bot.get_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user.id,
|
||||
)
|
||||
|
||||
# Проверяем, является ли цель администратором или создателем
|
||||
if target_status.status in ('administrator', 'creator'):
|
||||
|
||||
# Восстанавливаем thread_id для отправки сообщения
|
||||
message.message_thread_id = original_thread_id
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['cant_mute_admin'],
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['cant_mute_admin'],
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
return 1
|
||||
|
||||
# Восстанавливаем thread_id после успешной проверки
|
||||
message.message_thread_id = original_thread_id
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
# Восстанавливаем thread_id для отправки сообщения об ошибке
|
||||
message.message_thread_id = original_thread_id
|
||||
|
||||
# Отправляем ошибку
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['error'].format(e=str(e)),
|
||||
chat_id=message.chat.id,
|
||||
text=COMMAND_MESSAGES['error'].format(e=str(e)),
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
@@ -109,7 +202,61 @@ async def check_target_status(bot: AsyncTeleBot, message: Message, target_user):
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
return 1
|
||||
|
||||
# Форматирует упоминание пользователя для сообщений
|
||||
def format_user_mention(user: User) -> str:
|
||||
"""Возвращает форматированную строку с упоминанием пользователя."""
|
||||
if user.username:
|
||||
return f"@{user.username}"
|
||||
else:
|
||||
# Если нет username, используем имя со ссылкой на профиль
|
||||
name = user.first_name or "Пользователь"
|
||||
return f'<a href="tg://user?id={user.id}">{name}</a>'
|
||||
|
||||
|
||||
# Форматирует цитату исходного сообщения для сообщений о модерации
|
||||
def format_source_message(source_message: Message, max_length: int = 500) -> str:
|
||||
"""
|
||||
Возвращает отформатированную цитату исходного сообщения.
|
||||
Экранирует HTML-символы и обрезает длинный текст.
|
||||
"""
|
||||
if not source_message:
|
||||
return ""
|
||||
|
||||
# Берём текст или подпись к медиа
|
||||
text = source_message.text or source_message.caption
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
# Экранируем HTML-спецсимволы
|
||||
text = text.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
# Обрезаем очень длинные сообщения
|
||||
if len(text) > max_length:
|
||||
text = text[:max_length].rstrip() + "…"
|
||||
|
||||
return f"\n\n📝 <b>Сообщение:</b>\n<blockquote>{text}</blockquote>"
|
||||
|
||||
|
||||
# Удаляет сообщение команды модератора и сообщение нарушителя (если есть reply)
|
||||
async def delete_moderation_messages(bot: AsyncTeleBot, message: Message):
|
||||
"""Удаляет команду модератора и исходное сообщение нарушителя."""
|
||||
# Удаляем команду модератора
|
||||
try:
|
||||
await bot.delete_message(message.chat.id, message.message_id)
|
||||
except Exception as e:
|
||||
logger.debug(f"Не удалось удалить сообщение команды {message.message_id}: {e}")
|
||||
|
||||
# Удаляем сообщение нарушителя (если команда была ответом)
|
||||
if message.reply_to_message:
|
||||
# Проверяем, что это не ответ на сообщение топика
|
||||
if not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id:
|
||||
try:
|
||||
await bot.delete_message(message.chat.id, message.reply_to_message.message_id)
|
||||
except Exception as e:
|
||||
logger.debug(f"Не удалось удалить сообщение нарушителя {message.reply_to_message.message_id}: {e}")
|
||||
|
||||
|
||||
# Возвращает количество секунд
|
||||
def parse_mute_time(time_str: str) -> int | None:
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# Тесты для системы обнаружения бранных слов
|
||||
# Запуск: python -m unittest tests.test_bad_words
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
|
||||
# Добавляем src в путь для импорта
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
|
||||
from bad_words import contains_bad_word, get_bad_words_from_text, normalize_text
|
||||
|
||||
|
||||
class TestNormalizeText(unittest.TestCase):
|
||||
"""Тесты нормализации текста"""
|
||||
|
||||
def test_lowercase(self):
|
||||
self.assertEqual(normalize_text('ХЕР'), 'хер')
|
||||
|
||||
def test_remove_yo(self):
|
||||
self.assertEqual(normalize_text('хЁр'), 'хер')
|
||||
|
||||
def test_remove_obfuscation_ru(self):
|
||||
self.assertEqual(normalize_text('х*е*р'), 'хер')
|
||||
self.assertEqual(normalize_text('х.е.р'), 'хер')
|
||||
self.assertEqual(normalize_text('х_е_р'), 'хер')
|
||||
self.assertEqual(normalize_text('х-е-р'), 'хер')
|
||||
|
||||
def test_collapse_spaces_ru(self):
|
||||
self.assertEqual(normalize_text('х е р'), 'хер')
|
||||
self.assertEqual(normalize_text('х е р'), 'хер')
|
||||
|
||||
def test_collapse_repeated_letters_ru(self):
|
||||
self.assertEqual(normalize_text('хххеееррр'), 'хер')
|
||||
self.assertEqual(normalize_text('хууууй'), 'хуй')
|
||||
|
||||
|
||||
class TestContainsBadWordPositive(unittest.TestCase):
|
||||
"""Позитивные тесты: должны ловиться"""
|
||||
|
||||
def test_russian_short_word_start(self):
|
||||
"""Короткие слова, которые являются началом слова"""
|
||||
self.assertTrue(contains_bad_word('хер'))
|
||||
self.assertTrue(contains_bad_word('херня'))
|
||||
self.assertTrue(contains_bad_word('нахер'))
|
||||
self.assertTrue(contains_bad_word('похер'))
|
||||
self.assertTrue(contains_bad_word('херовый'))
|
||||
self.assertTrue(contains_bad_word('бля'))
|
||||
self.assertTrue(contains_bad_word('блядь'))
|
||||
self.assertTrue(contains_bad_word('хуй'))
|
||||
self.assertTrue(contains_bad_word('хуйло'))
|
||||
self.assertTrue(contains_bad_word('жопа'))
|
||||
|
||||
def test_russian_contains(self):
|
||||
"""Слова, содержащие бранные подстроки"""
|
||||
self.assertTrue(contains_bad_word('сука'))
|
||||
self.assertTrue(contains_bad_word('пиздец'))
|
||||
self.assertTrue(contains_bad_word('ебать'))
|
||||
self.assertTrue(contains_bad_word('говно'))
|
||||
self.assertTrue(contains_bad_word('дрочить'))
|
||||
self.assertTrue(contains_bad_word('пидор'))
|
||||
self.assertTrue(contains_bad_word('мудак'))
|
||||
self.assertTrue(contains_bad_word('шлюха'))
|
||||
self.assertTrue(contains_bad_word('ублюдок'))
|
||||
|
||||
def test_russian_whole_word(self):
|
||||
"""Точные слова-оскорбления"""
|
||||
self.assertTrue(contains_bad_word('даун'))
|
||||
self.assertTrue(contains_bad_word('дебил'))
|
||||
self.assertTrue(contains_bad_word('урод'))
|
||||
self.assertTrue(contains_bad_word('мразь'))
|
||||
|
||||
def test_english_bad_words(self):
|
||||
"""Английские бранные слова"""
|
||||
self.assertTrue(contains_bad_word('fuck'))
|
||||
self.assertTrue(contains_bad_word('fucking'))
|
||||
self.assertTrue(contains_bad_word('fucker'))
|
||||
self.assertTrue(contains_bad_word('fuck you'))
|
||||
self.assertTrue(contains_bad_word('shit'))
|
||||
self.assertTrue(contains_bad_word('bitch'))
|
||||
self.assertTrue(contains_bad_word('asshole'))
|
||||
|
||||
def test_obfuscated_english(self):
|
||||
"""Обфусцированные английские слова"""
|
||||
self.assertTrue(contains_bad_word('f*ck'))
|
||||
self.assertTrue(contains_bad_word('f.u.c.k'))
|
||||
self.assertTrue(contains_bad_word('f u c k'))
|
||||
self.assertTrue(contains_bad_word('fuuuck'))
|
||||
self.assertTrue(contains_bad_word('f-u-c-k'))
|
||||
|
||||
def test_transliterated_russian(self):
|
||||
"""Русские маты, написанные латиницей"""
|
||||
self.assertTrue(contains_bad_word('xyu'))
|
||||
self.assertTrue(contains_bad_word('hui'))
|
||||
self.assertTrue(contains_bad_word('xyй'))
|
||||
self.assertTrue(contains_bad_word('pizdec'))
|
||||
|
||||
def test_mixed_text(self):
|
||||
"""Бранные слова внутри обычного текста"""
|
||||
self.assertTrue(contains_bad_word('Это просто херня какая-то'))
|
||||
self.assertTrue(contains_bad_word('Ты что, дебил?'))
|
||||
self.assertTrue(contains_bad_word('Он сказал fuck off'))
|
||||
|
||||
|
||||
class TestContainsBadWordNegative(unittest.TestCase):
|
||||
"""Негативные тесты: НЕ должны ловиться"""
|
||||
|
||||
def test_russian_exceptions_from_list(self):
|
||||
"""Слова из списка исключений"""
|
||||
self.assertFalse(contains_bad_word('аблятив'))
|
||||
self.assertFalse(contains_bad_word('абляция'))
|
||||
self.assertFalse(contains_bad_word('влюблять'))
|
||||
self.assertFalse(contains_bad_word('гребля'))
|
||||
self.assertFalse(contains_bad_word('даунгрейд'))
|
||||
self.assertFalse(contains_bad_word('гейтвей'))
|
||||
self.assertFalse(contains_bad_word('гейм'))
|
||||
|
||||
def test_normal_words_with_bad_substrings(self):
|
||||
"""Обычные слова, содержащие бранные корни"""
|
||||
self.assertFalse(contains_bad_word('херметичность'))
|
||||
self.assertFalse(contains_bad_word('скорблящий'))
|
||||
self.assertFalse(contains_bad_word('забляковать'))
|
||||
self.assertFalse(contains_bad_word('обляпать'))
|
||||
self.assertFalse(contains_bad_word('находится')) # содержит "хуй"? нет
|
||||
self.assertFalse(contains_bad_word('класс')) # содержит "асс" - не мат
|
||||
|
||||
def test_english_normal_words(self):
|
||||
"""Обычные английские слова"""
|
||||
self.assertFalse(contains_bad_word('hello'))
|
||||
self.assertFalse(contains_bad_word('assume'))
|
||||
self.assertFalse(contains_bad_word('passion'))
|
||||
self.assertFalse(contains_bad_word('class'))
|
||||
self.assertFalse(contains_bad_word('association'))
|
||||
self.assertFalse(contains_bad_word('document'))
|
||||
|
||||
def test_empty_and_short(self):
|
||||
"""Пустой текст и короткие безобидные строки"""
|
||||
self.assertFalse(contains_bad_word(''))
|
||||
self.assertFalse(contains_bad_word('а'))
|
||||
self.assertFalse(contains_bad_word('привет'))
|
||||
|
||||
|
||||
class TestGetBadWordsFromText(unittest.TestCase):
|
||||
"""Тесты функции получения списка найденных слов"""
|
||||
|
||||
def test_single_word(self):
|
||||
found = get_bad_words_from_text('херня')
|
||||
self.assertIn('хер', found)
|
||||
|
||||
def test_multiple_words(self):
|
||||
found = get_bad_words_from_text('херня и fuck')
|
||||
self.assertIn('хер', found)
|
||||
self.assertIn('fuck', found)
|
||||
|
||||
def test_no_bad_words(self):
|
||||
found = get_bad_words_from_text('абляция и документ')
|
||||
self.assertEqual(found, [])
|
||||
|
||||
|
||||
class TestEdgeCases(unittest.TestCase):
|
||||
"""Дополнительные проверки на ложные срабатывания и сложные обходы"""
|
||||
|
||||
def test_english_word_boundaries(self):
|
||||
"""Обычные английские слова не должны ловиться"""
|
||||
self.assertFalse(contains_bad_word('class'))
|
||||
self.assertFalse(contains_bad_word('passion'))
|
||||
self.assertFalse(contains_bad_word('assignment'))
|
||||
self.assertFalse(contains_bad_word('shifty')) # содержит shift, не shit
|
||||
|
||||
def test_russian_word_boundaries(self):
|
||||
"""Обычные русские слова с бранными корнями"""
|
||||
self.assertFalse(contains_bad_word('херметичность'))
|
||||
self.assertFalse(contains_bad_word('скорблящий'))
|
||||
self.assertFalse(contains_bad_word('забляковать'))
|
||||
self.assertFalse(contains_bad_word('обляпать'))
|
||||
|
||||
def test_no_false_positive_on_adjacent_words(self):
|
||||
"""Слова, стоящие рядом, не должны склеиваться в бранное слово"""
|
||||
self.assertFalse(contains_bad_word('багов не было'))
|
||||
self.assertFalse(contains_bad_word('Мне тут втирают, что с монками проблем не было и багов не было в ВОВ со старта сезона'))
|
||||
|
||||
def test_mixed_scripts(self):
|
||||
"""Смешанные латиница и кириллица"""
|
||||
self.assertTrue(contains_bad_word('xyй'))
|
||||
self.assertTrue(contains_bad_word('х у й'))
|
||||
|
||||
def test_case_insensitive(self):
|
||||
"""Регистронезависимость"""
|
||||
self.assertTrue(contains_bad_word('FUCK'))
|
||||
self.assertTrue(contains_bad_word('ХерНя'))
|
||||
self.assertTrue(contains_bad_word('БЛЯ'))
|
||||
|
||||
def test_with_punctuation(self):
|
||||
"""Бранные слова с пунктуацией"""
|
||||
self.assertTrue(contains_bad_word('херня!'))
|
||||
self.assertTrue(contains_bad_word('fuck!!!'))
|
||||
self.assertTrue(contains_bad_word('ты, блядь, кто?'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,79 @@
|
||||
# Тесты локализации капчи
|
||||
# Запуск: python -m unittest tests.test_captcha_locale
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
|
||||
from config import get_captcha_text, DEFAULT_CAPTCHA_LANGUAGE, CAPTCHA_LOCALES
|
||||
|
||||
|
||||
class TestCaptchaLocale(unittest.TestCase):
|
||||
"""Тесты выбора языка для сообщений капчи."""
|
||||
|
||||
def test_default_language_is_russian(self):
|
||||
self.assertEqual(DEFAULT_CAPTCHA_LANGUAGE, 'ru')
|
||||
|
||||
def test_unknown_language_falls_back_to_russian(self):
|
||||
text = get_captcha_text('xx', 'captcha_message')
|
||||
self.assertEqual(text, CAPTCHA_LOCALES['ru']['captcha_message'])
|
||||
|
||||
def test_none_language_falls_back_to_russian(self):
|
||||
text = get_captcha_text(None, 'captcha_button')
|
||||
self.assertEqual(text, CAPTCHA_LOCALES['ru']['captcha_button'])
|
||||
|
||||
def test_empty_language_falls_back_to_russian(self):
|
||||
text = get_captcha_text('', 'captcha_welcome')
|
||||
self.assertEqual(text, CAPTCHA_LOCALES['ru']['captcha_welcome'])
|
||||
|
||||
def test_russian_language(self):
|
||||
text = get_captcha_text('ru', 'captcha_message')
|
||||
self.assertIn('подтверждения входа', text)
|
||||
|
||||
def test_english_language(self):
|
||||
text = get_captcha_text('en', 'captcha_message')
|
||||
self.assertIn('please press the button', text)
|
||||
|
||||
def test_english_region_variant(self):
|
||||
text = get_captcha_text('en-US', 'captcha_button')
|
||||
self.assertEqual(text, CAPTCHA_LOCALES['en']['captcha_button'])
|
||||
|
||||
def test_ukrainian_language(self):
|
||||
text = get_captcha_text('uk', 'captcha_message')
|
||||
self.assertIn('підтвердження входу', text)
|
||||
|
||||
def test_case_insensitivity(self):
|
||||
text_lower = get_captcha_text('en', 'captcha_button')
|
||||
text_upper = get_captcha_text('EN', 'captcha_button')
|
||||
self.assertEqual(text_lower, text_upper)
|
||||
|
||||
def test_all_locales_have_required_keys(self):
|
||||
required_keys = {
|
||||
'captcha_message',
|
||||
'captcha_button',
|
||||
'captcha_welcome',
|
||||
'captcha_wrong_user',
|
||||
}
|
||||
for lang, translations in CAPTCHA_LOCALES.items():
|
||||
missing = required_keys - set(translations.keys())
|
||||
self.assertFalse(
|
||||
missing,
|
||||
f"Language {lang!r} is missing keys: {missing}",
|
||||
)
|
||||
|
||||
def test_captcha_message_is_formattable(self):
|
||||
"""Шаблон сообщения должен допускать подстановку переменных."""
|
||||
for lang in CAPTCHA_LOCALES:
|
||||
with self.subTest(lang=lang):
|
||||
text = get_captcha_text(lang, 'captcha_message').format(
|
||||
user_mention='<a href="tg://user?id=1">User</a>',
|
||||
timeout=60,
|
||||
)
|
||||
self.assertIn('User', text)
|
||||
self.assertIn('60', text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,190 @@
|
||||
# Тесты для модуля защиты входа (/guard, CAS, капча)
|
||||
# Запуск: python -m unittest tests.test_guard
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
"""Поддельный aiohttp-ответ для использования в async with."""
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
|
||||
async def json(self):
|
||||
return self._data
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class FakeSession:
|
||||
"""Поддельная aiohttp-сессия."""
|
||||
def __init__(self, response_data=None, raise_on_get=None):
|
||||
self._response = FakeResponse(response_data) if response_data is not None else None
|
||||
self._raise_on_get = raise_on_get
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
pass
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
if self._raise_on_get:
|
||||
raise self._raise_on_get
|
||||
return self._response
|
||||
|
||||
|
||||
class TestGuardEnabled(unittest.TestCase):
|
||||
"""Тесты проверки включения капчи"""
|
||||
|
||||
def test_guard_default_enabled(self):
|
||||
from modules import guard
|
||||
with patch.object(guard.db, 'get_chat_setting', return_value='1'):
|
||||
self.assertTrue(guard._guard_enabled(-1001234567890))
|
||||
|
||||
def test_guard_disabled(self):
|
||||
from modules import guard
|
||||
with patch.object(guard.db, 'get_chat_setting', return_value='0'):
|
||||
self.assertFalse(guard._guard_enabled(-1001234567890))
|
||||
|
||||
def test_guard_default_when_not_set(self):
|
||||
from modules import guard
|
||||
with patch.object(guard.db, 'get_chat_setting', side_effect=lambda chat_id, key, default: default):
|
||||
self.assertTrue(guard._guard_enabled(-1001234567890))
|
||||
|
||||
|
||||
class TestIsAdmin(unittest.IsolatedAsyncioTestCase):
|
||||
"""Тесты проверки прав администратора"""
|
||||
|
||||
async def test_admin_status(self):
|
||||
from modules import guard
|
||||
bot = AsyncMock()
|
||||
member = MagicMock()
|
||||
member.status = 'administrator'
|
||||
bot.get_chat_member.return_value = member
|
||||
self.assertTrue(await guard._is_admin(bot, -1001234567890, 999))
|
||||
|
||||
async def test_creator_status(self):
|
||||
from modules import guard
|
||||
bot = AsyncMock()
|
||||
member = MagicMock()
|
||||
member.status = 'creator'
|
||||
bot.get_chat_member.return_value = member
|
||||
self.assertTrue(await guard._is_admin(bot, -1001234567890, 999))
|
||||
|
||||
async def test_member_status(self):
|
||||
from modules import guard
|
||||
bot = AsyncMock()
|
||||
member = MagicMock()
|
||||
member.status = 'member'
|
||||
bot.get_chat_member.return_value = member
|
||||
self.assertFalse(await guard._is_admin(bot, -1001234567890, 999))
|
||||
|
||||
|
||||
class TestCheckCas(unittest.IsolatedAsyncioTestCase):
|
||||
"""Тесты проверки CAS"""
|
||||
|
||||
async def test_spammer_detected(self):
|
||||
from modules import guard
|
||||
with patch('aiohttp.ClientSession', return_value=FakeSession({"ok": True, "result": {"offenses": 1}})):
|
||||
self.assertTrue(await guard._check_cas(123456))
|
||||
|
||||
async def test_clean_user(self):
|
||||
from modules import guard
|
||||
with patch('aiohttp.ClientSession', return_value=FakeSession({"ok": False})):
|
||||
self.assertFalse(await guard._check_cas(123456))
|
||||
|
||||
async def test_cas_request_error(self):
|
||||
from modules import guard
|
||||
with patch('aiohttp.ClientSession', return_value=FakeSession(raise_on_get=Exception("Connection error"))):
|
||||
self.assertFalse(await guard._check_cas(123456))
|
||||
|
||||
|
||||
class TestGuardCommand(unittest.IsolatedAsyncioTestCase):
|
||||
"""Тесты команды /guard"""
|
||||
|
||||
def _create_message(self, text: str, chat_type: str = "supergroup"):
|
||||
message = MagicMock()
|
||||
message.text = text
|
||||
message.chat.id = -1001234567890
|
||||
message.chat.type = chat_type
|
||||
message.message_id = 1000
|
||||
message.message_thread_id = None
|
||||
message.from_user = MagicMock(id=999)
|
||||
return message
|
||||
|
||||
async def test_guard_status(self):
|
||||
from modules import guard
|
||||
bot = AsyncMock()
|
||||
message = self._create_message('/guard')
|
||||
|
||||
with patch.object(guard.db, 'get_chat_setting', return_value='1'), \
|
||||
patch('modules.guard._is_admin', new=AsyncMock(return_value=True)), \
|
||||
patch('modules.guard.delete_messages', new=AsyncMock()):
|
||||
await guard.guard_command(bot, message)
|
||||
|
||||
bot.send_message.assert_called_once()
|
||||
call_kwargs = bot.send_message.call_args.kwargs
|
||||
self.assertIn('включена', call_kwargs['text'])
|
||||
|
||||
async def test_guard_enable(self):
|
||||
from modules import guard
|
||||
bot = AsyncMock()
|
||||
message = self._create_message('/guard on')
|
||||
|
||||
with patch.object(guard.db, 'set_chat_setting') as mock_set, \
|
||||
patch('modules.guard._is_admin', new=AsyncMock(return_value=True)), \
|
||||
patch('modules.guard.delete_messages', new=AsyncMock()):
|
||||
await guard.guard_command(bot, message)
|
||||
|
||||
mock_set.assert_called_once_with(-1001234567890, guard.GUARD_SETTING_KEY, '1')
|
||||
bot.send_message.assert_called_once()
|
||||
|
||||
async def test_guard_disable(self):
|
||||
from modules import guard
|
||||
bot = AsyncMock()
|
||||
message = self._create_message('/guard off')
|
||||
|
||||
with patch.object(guard.db, 'set_chat_setting') as mock_set, \
|
||||
patch('modules.guard._is_admin', new=AsyncMock(return_value=True)), \
|
||||
patch('modules.guard.delete_messages', new=AsyncMock()):
|
||||
await guard.guard_command(bot, message)
|
||||
|
||||
mock_set.assert_called_once_with(-1001234567890, guard.GUARD_SETTING_KEY, '0')
|
||||
bot.send_message.assert_called_once()
|
||||
|
||||
async def test_guard_no_admin_rights(self):
|
||||
from modules import guard
|
||||
bot = AsyncMock()
|
||||
message = self._create_message('/guard on')
|
||||
|
||||
with patch('modules.guard._is_admin', new=AsyncMock(return_value=False)), \
|
||||
patch('modules.guard.delete_messages', new=AsyncMock()):
|
||||
await guard.guard_command(bot, message)
|
||||
|
||||
bot.send_message.assert_called_once()
|
||||
call_kwargs = bot.send_message.call_args.kwargs
|
||||
self.assertIn('Только администраторы', call_kwargs['text'])
|
||||
|
||||
async def test_guard_private_chat(self):
|
||||
from modules import guard
|
||||
bot = AsyncMock()
|
||||
message = self._create_message('/guard', chat_type='private')
|
||||
|
||||
await guard.guard_command(bot, message)
|
||||
|
||||
bot.send_message.assert_called_once()
|
||||
call_kwargs = bot.send_message.call_args.kwargs
|
||||
self.assertIn('/guard', call_kwargs['text'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,302 @@
|
||||
# Тесты для команды /warn
|
||||
# Запуск: python -m unittest tests.test_warn
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
# Добавляем src в путь для импорта
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
|
||||
|
||||
class TestWarnCommand(unittest.IsolatedAsyncioTestCase):
|
||||
"""Тесты основной команды /warn"""
|
||||
|
||||
def _create_bot(self):
|
||||
bot = AsyncMock()
|
||||
return bot
|
||||
|
||||
def _create_message(
|
||||
self,
|
||||
text: str,
|
||||
reply_to_message=None,
|
||||
is_topic_message: bool = False,
|
||||
message_thread_id=None,
|
||||
):
|
||||
message = MagicMock()
|
||||
message.text = text
|
||||
message.reply_to_message = reply_to_message
|
||||
message.is_topic_message = is_topic_message
|
||||
message.message_thread_id = message_thread_id
|
||||
message.chat.id = -1001234567890
|
||||
message.message_id = 1000
|
||||
message.from_user = MagicMock(id=999, first_name='Admin', username='admin')
|
||||
return message
|
||||
|
||||
def _create_reply_message(self, user_id=123, first_name='User', username='user', message_id=100):
|
||||
reply = MagicMock()
|
||||
reply.from_user = MagicMock(id=user_id, first_name=first_name, username=username)
|
||||
reply.message_id = message_id
|
||||
reply.text = 'нарушение'
|
||||
reply.caption = None
|
||||
return reply
|
||||
|
||||
def _mock_get_user(self, user_id):
|
||||
"""Возвращает пользователя с соответствующим ID"""
|
||||
return (user_id, 'User', 'user')
|
||||
|
||||
async def _run_warn(self, message, bot=None):
|
||||
from modules import warn
|
||||
if bot is None:
|
||||
bot = self._create_bot()
|
||||
|
||||
with patch.object(warn.db, 'add_warning') as mock_add_warning, \
|
||||
patch.object(warn.db, 'add_karma') as mock_add_karma, \
|
||||
patch.object(warn.db, 'get_warnings_count', return_value=1) as mock_get_warnings, \
|
||||
patch.object(warn.db, 'get_user', side_effect=self._mock_get_user) as mock_get_user, \
|
||||
patch.object(warn.db, 'get_user_by_username', return_value=(123, 'User', 'user')) as mock_get_user_by_username, \
|
||||
patch('modules.warn.check_admin_status', new=AsyncMock(return_value=0)) as mock_check_admin, \
|
||||
patch('modules.warn.check_target_status', new=AsyncMock(return_value=0)) as mock_check_target, \
|
||||
patch('modules.warn.delete_messages', new=AsyncMock()) as mock_delete_messages, \
|
||||
patch('modules.warn.delete_moderation_messages', new=AsyncMock()) as mock_delete_mod, \
|
||||
patch('modules.warn.action_reporter', new=AsyncMock()) as mock_log_action:
|
||||
|
||||
await warn.warn_command(bot, message)
|
||||
|
||||
return {
|
||||
'add_warning': mock_add_warning,
|
||||
'add_karma': mock_add_karma,
|
||||
'get_warnings_count': mock_get_warnings,
|
||||
'get_user': mock_get_user,
|
||||
'get_user_by_username': mock_get_user_by_username,
|
||||
'check_admin_status': mock_check_admin,
|
||||
'check_target_status': mock_check_target,
|
||||
'delete_messages': mock_delete_messages,
|
||||
'delete_moderation_messages': mock_delete_mod,
|
||||
'log_action': mock_log_action,
|
||||
'bot': bot,
|
||||
}
|
||||
|
||||
async def test_warn_reply_without_reason(self):
|
||||
"""/warn в ответ на сообщение без причины должна выдавать варн"""
|
||||
from modules import warn
|
||||
|
||||
reply = self._create_reply_message()
|
||||
message = self._create_message('/warn', reply_to_message=reply)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_called_once()
|
||||
call_kwargs = mocks['add_warning'].call_args.kwargs
|
||||
self.assertEqual(call_kwargs['user_id'], 123)
|
||||
self.assertEqual(call_kwargs['reason'], 'отсутствует')
|
||||
mocks['bot'].send_message.assert_called_once()
|
||||
|
||||
async def test_warn_reply_with_bot_username_without_reason(self):
|
||||
"""/warn@botname в ответ на сообщение без причины должна выдавать варн"""
|
||||
from modules import warn
|
||||
|
||||
reply = self._create_reply_message()
|
||||
message = self._create_message('/warn@linux_gaming_ru_bot', reply_to_message=reply)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_called_once()
|
||||
call_kwargs = mocks['add_warning'].call_args.kwargs
|
||||
self.assertEqual(call_kwargs['user_id'], 123)
|
||||
self.assertEqual(call_kwargs['reason'], 'отсутствует')
|
||||
mocks['bot'].send_message.assert_called_once()
|
||||
|
||||
async def test_warn_reply_with_reason(self):
|
||||
"""/warn причина в ответе должна сохранять причину"""
|
||||
from modules import warn
|
||||
|
||||
reply = self._create_reply_message()
|
||||
message = self._create_message('/warn мат запрещен', reply_to_message=reply)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_called_once()
|
||||
call_kwargs = mocks['add_warning'].call_args.kwargs
|
||||
self.assertEqual(call_kwargs['user_id'], 123)
|
||||
self.assertEqual(call_kwargs['reason'], 'мат запрещен')
|
||||
mocks['bot'].send_message.assert_called_once()
|
||||
|
||||
async def test_warn_reply_with_bot_username_and_reason(self):
|
||||
"""/warn@botname причина в ответе должна сохранять причину"""
|
||||
from modules import warn
|
||||
|
||||
reply = self._create_reply_message()
|
||||
message = self._create_message('/warn@linux_gaming_ru_bot мат запрещен', reply_to_message=reply)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_called_once()
|
||||
call_kwargs = mocks['add_warning'].call_args.kwargs
|
||||
self.assertEqual(call_kwargs['user_id'], 123)
|
||||
self.assertEqual(call_kwargs['reason'], 'мат запрещен')
|
||||
mocks['bot'].send_message.assert_called_once()
|
||||
|
||||
async def test_warn_no_reply_no_args(self):
|
||||
"""/warn без reply и без аргументов должна просто удаляться"""
|
||||
from modules import warn
|
||||
|
||||
message = self._create_message('/warn', reply_to_message=None)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_not_called()
|
||||
mocks['delete_messages'].assert_called_once()
|
||||
mocks['bot'].send_message.assert_not_called()
|
||||
|
||||
async def test_warn_by_username(self):
|
||||
"""/warn @user причина должна находить пользователя по username"""
|
||||
from modules import warn
|
||||
|
||||
message = self._create_message('/warn @user спам', reply_to_message=None)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_called_once()
|
||||
call_kwargs = mocks['add_warning'].call_args.kwargs
|
||||
self.assertEqual(call_kwargs['user_id'], 123)
|
||||
self.assertEqual(call_kwargs['reason'], 'спам')
|
||||
mocks['bot'].send_message.assert_called_once()
|
||||
|
||||
async def test_warn_by_user_id(self):
|
||||
"""/warn 123456789 причина должна находить пользователя по ID"""
|
||||
from modules import warn
|
||||
|
||||
message = self._create_message('/warn 123456789 спам', reply_to_message=None)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_called_once()
|
||||
call_kwargs = mocks['add_warning'].call_args.kwargs
|
||||
self.assertEqual(call_kwargs['user_id'], 123456789)
|
||||
self.assertEqual(call_kwargs['reason'], 'спам')
|
||||
mocks['bot'].send_message.assert_called_once()
|
||||
|
||||
async def test_warn_reply_in_topic(self):
|
||||
"""/warn в ответ на обычное сообщение в топике должна работать"""
|
||||
from modules import warn
|
||||
|
||||
reply = self._create_reply_message(message_id=100)
|
||||
message = self._create_message('/warn', reply_to_message=reply, is_topic_message=True, message_thread_id=10)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_called_once()
|
||||
mocks['bot'].send_message.assert_called_once()
|
||||
|
||||
async def test_warn_reply_to_topic_message(self):
|
||||
"""/warn в ответ на первое сообщение топика не должна выдавать варн"""
|
||||
from modules import warn
|
||||
|
||||
reply = self._create_reply_message(message_id=10)
|
||||
message = self._create_message('/warn', reply_to_message=reply, is_topic_message=True, message_thread_id=10)
|
||||
|
||||
mocks = await self._run_warn(message)
|
||||
|
||||
mocks['add_warning'].assert_not_called()
|
||||
|
||||
|
||||
class TestWarnSubcommands(unittest.IsolatedAsyncioTestCase):
|
||||
"""Тесты подкоманд /warn"""
|
||||
|
||||
def _create_message(self, text: str, reply_to_message=None, is_topic_message=False, message_thread_id=None):
|
||||
message = MagicMock()
|
||||
message.text = text
|
||||
message.reply_to_message = reply_to_message
|
||||
message.is_topic_message = is_topic_message
|
||||
message.message_thread_id = message_thread_id
|
||||
message.chat.id = -1001234567890
|
||||
message.message_id = 1000
|
||||
message.from_user = MagicMock(id=999, first_name='Admin', username='admin')
|
||||
return message
|
||||
|
||||
async def test_warn_help(self):
|
||||
"""/warn help должен показывать справку"""
|
||||
from modules import warn
|
||||
|
||||
bot = AsyncMock()
|
||||
message = self._create_message('/warn help')
|
||||
|
||||
with patch('modules.warn.delete_messages', new=AsyncMock()) as mock_delete:
|
||||
await warn.warn_help(bot, message)
|
||||
|
||||
bot.send_message.assert_called_once()
|
||||
mock_delete.assert_called_once()
|
||||
|
||||
async def test_warn_reset_reply(self):
|
||||
"""/warn reset в ответе должен сбрасывать варны"""
|
||||
from modules import warn
|
||||
|
||||
bot = AsyncMock()
|
||||
reply = MagicMock()
|
||||
reply.from_user = MagicMock(id=123)
|
||||
reply.message_id = 100
|
||||
message = self._create_message('/warn reset', reply_to_message=reply)
|
||||
|
||||
with patch.object(warn.db, 'reset_user_warnings', return_value=3) as mock_reset, \
|
||||
patch.object(warn.db, 'get_user', return_value=(123, 'User', 'user')) as mock_get_user, \
|
||||
patch('modules.warn.check_admin_status', new=AsyncMock(return_value=0)) as mock_check_admin, \
|
||||
patch('modules.warn.delete_messages', new=AsyncMock()) as mock_delete:
|
||||
|
||||
await warn.warn_reset(bot, message, ['/warn', 'reset'])
|
||||
|
||||
mock_reset.assert_called_once_with(123, -1001234567890)
|
||||
bot.send_message.assert_called_once()
|
||||
|
||||
async def test_warn_reset_reply_to_topic_message(self):
|
||||
"""/warn reset в ответе на первое сообщение топика не должен сбрасывать варны"""
|
||||
from modules import warn
|
||||
|
||||
bot = AsyncMock()
|
||||
reply = MagicMock()
|
||||
reply.from_user = MagicMock(id=123)
|
||||
reply.message_id = 10
|
||||
message = self._create_message('/warn reset', reply_to_message=reply, is_topic_message=True, message_thread_id=10)
|
||||
|
||||
with patch.object(warn.db, 'reset_user_warnings', return_value=3) as mock_reset, \
|
||||
patch('modules.warn.check_admin_status', new=AsyncMock(return_value=0)) as mock_check_admin, \
|
||||
patch('modules.warn.delete_messages', new=AsyncMock()) as mock_delete:
|
||||
|
||||
await warn.warn_reset(bot, message, ['/warn', 'reset'])
|
||||
|
||||
mock_reset.assert_not_called()
|
||||
|
||||
|
||||
class TestNormalizeCommandText(unittest.TestCase):
|
||||
"""Тесты нормализации текста команды"""
|
||||
|
||||
def test_plain_command(self):
|
||||
from modules.warn import _normalize_command_text
|
||||
self.assertEqual(_normalize_command_text('/warn причина', 'warn'), '/warn причина')
|
||||
|
||||
def test_command_with_bot_username(self):
|
||||
from modules.warn import _normalize_command_text
|
||||
self.assertEqual(
|
||||
_normalize_command_text('/warn@linux_gaming_ru_bot причина', 'warn'),
|
||||
'/warn причина'
|
||||
)
|
||||
|
||||
def test_command_with_bot_username_no_reason(self):
|
||||
from modules.warn import _normalize_command_text
|
||||
self.assertEqual(
|
||||
_normalize_command_text('/warn@linux_gaming_ru_bot', 'warn'),
|
||||
'/warn'
|
||||
)
|
||||
|
||||
def test_reason_with_at_symbol(self):
|
||||
from modules.warn import _normalize_command_text
|
||||
self.assertEqual(
|
||||
_normalize_command_text('/warn@bot упоминание @user', 'warn'),
|
||||
'/warn упоминание @user'
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт для обновления бота из git-репозитория
|
||||
# Запускается на сервере в директории /opt/LGBot
|
||||
|
||||
echo "🔄 Обновление LGBot..."
|
||||
echo "================================"
|
||||
|
||||
# Проверяем, находимся ли мы в git-репозитории
|
||||
if [ ! -d ".git" ]; then
|
||||
echo "❌ Ошибка: это не git-репозиторий"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Показываем текущую ветку
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
echo "📍 Текущая ветка: $CURRENT_BRANCH"
|
||||
|
||||
# Проверяем наличие изменений
|
||||
echo ""
|
||||
echo "📊 Статус репозитория:"
|
||||
git status --short
|
||||
|
||||
# Получаем изменения из удаленного репозитория
|
||||
echo ""
|
||||
echo "📥 Получение изменений из репозитория..."
|
||||
git fetch origin
|
||||
|
||||
# Проверяем, есть ли обновления
|
||||
LOCAL=$(git rev-parse HEAD)
|
||||
REMOTE=$(git rev-parse origin/$CURRENT_BRANCH)
|
||||
|
||||
if [ "$LOCAL" = "$REMOTE" ]; then
|
||||
echo "✅ Бот уже обновлен до последней версии"
|
||||
echo ""
|
||||
echo "Последний коммит:"
|
||||
git log -1 --pretty=format:"%h - %s (%ar)" --abbrev-commit
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Показываем, какие коммиты будут применены
|
||||
echo ""
|
||||
echo "📝 Новые коммиты:"
|
||||
git log HEAD..origin/$CURRENT_BRANCH --pretty=format:"%h - %s (%ar)" --abbrev-commit
|
||||
|
||||
# Обновляем код
|
||||
echo ""
|
||||
echo "⬇️ Применение обновлений..."
|
||||
git pull origin $CURRENT_BRANCH
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Ошибка при обновлении из git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Код успешно обновлен"
|
||||
|
||||
# Перезапускаем бота
|
||||
echo ""
|
||||
echo "🔄 Перезапуск бота..."
|
||||
systemctl restart LGBot.service
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Бот успешно перезапущен"
|
||||
|
||||
# Ждем 2 секунды и проверяем статус
|
||||
sleep 2
|
||||
echo ""
|
||||
echo "📊 Статус службы:"
|
||||
systemctl status LGBot.service --no-pager -l | head -10
|
||||
else
|
||||
echo "❌ Ошибка при перезапуске бота"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 Обновление завершено!"
|
||||
Reference in new issue
Block a user