Compare commits

...
64 Commits
Author SHA1 Message Date
xpamych d5c103aca1 Исправлена кнопка капчи «Я не бот»
Добавлено:
- подписка бота на события нажатий inline-кнопок
- логирование успешного прохождения капчи
- логирование отмены таймаута кика при прохождении капчи
- логирование ошибок при нажатии кнопки другим пользователем

Изменено:
- обработчик нажатия кнопки капчи теперь получает события из Telegram
- ошибки при снятии ограничений теперь логируются с полным стектрейсом
2026-07-11 15:39:09 +03:00
xpamych 4fd0ade03c Добавлен каталог runtime-данных в игнорирование git
Добавлено:
- каталог data/ в .gitignore для исключения пользовательских списков слов из отслеживания git
2026-07-10 12:46:49 +03:00
xpamych 4b97170abd Локализация сообщений капчи по языку пользователя
Добавлено:
- выбор языка капчи на основе языковых настроек Telegram пользователя
- переводы сообщений капчи, кнопки и уведомлений на 16 языков
- тесты для проверки локализации капчи

Изменено:
- отправка капчи и обработка нажатий кнопки теперь используют локализованные тексты
2026-07-10 12:42:58 +03:00
xpamych ee1d8793c6 Добавлено:
- проверка новых участников по списку CAS с автоматическим баном спамеров
- капча-кнопка для новых участников с таймаутом 60 секунд
- команда /guard для включения и отключения капчи администраторами чата
- хранение настроек чата в базе данных
- тесты для защиты входа

Изменено:
- справка администратора теперь включает раздел защиты чата
- меню команд бота дополнено командой /guard
2026-07-05 16:48:30 +03:00
xpamych 7cf97e6a23 Добавлено:
- тесты для команды /warn
- логирование действий при обработке /warn

Изменено:
- обработка /warn в ответ на сообщение теперь работает без указания причины
- обработка /warn с упоминанием бота (@botname)
- проверка топика при сбросе варнов через /warn reset
2026-06-24 22:11:39 +03:00
xpamych deab385de5 Добавлено:
- тест на ложное срабатывание фильтра при склеивании слов "багов не"

Изменено:
- логика удаления пробелов между буквами, чтобы не склеивать обычные слова в одно
2026-06-24 19:04:48 +03:00
xpamych 3526990573 Исправление логирования модерации
Добавлено:
- цитирование исходного сообщения в логах админ-чата
- отправка медиа из исходного сообщения в админ-чат

Изменено:
- убрана цитата исходного сообщения из публичных сообщений о бане/муте/варне
- длина цитаты в логе ограничена в зависимости от типа сообщения

Удалено:
- лишние импорты и параметры форматирования из модулей модерации
2026-06-22 22:50:18 +03:00
xpamych 20433059a0 Улучшение сообщений модерации и подсказки /help
Добавлено:
- цитирование исходного сообщения в сообщениях о бане, муте, варне и автомуте
- возможность удалять сообщения по точным идентификаторам

Изменено:
- время отображения подсказки /help увеличено до 2 минут
- удаление сообщений при /help теперь использует реальные id сообщений

Удалено:
- ненадёжное определение id удаляемых сообщений через message_id+i
2026-06-20 16:53:37 +03:00
xpamych cfc52247a0 Улучшение системы фильтрации матов
Добавлено:
- разделение словаря на базовый (в репозитории) и кастомный (добавления модераторов)
- поддержка английских бранных слов и их обфускации
- тесты для проверки ловли матов и ложных срабатываний
- автоматическая категоризация добавленных модераторами слов

Изменено:
- переработан механизм проверки текста на маты
- базовый словарь объединён с серверным и переведён в новый формат
- короткие слова теперь проверяются как начало слова, а не пропускаются

Удалено:
- повреждённая запись exception урод из базового словаря
2026-06-20 16:53:20 +03:00
xpamych b38c24f775 Уменьшение времени удаления сообщения /help до 3 секунд 2026-02-09 18:55:33 +03:00
xpamych 50d3e894bc Объединение команд модерации в подкоманды
- /mute off вместо /unmute, /mute reset для сброса автомутов
- /ban off вместо /unban
- /warn reset для сброса предупреждений
- /karma top, set, disable, enable, status вместо отдельных команд
- Разделение /help для пользователей и администраторов
- Удалены устаревшие модули: botdata, unmute, unban, setkarma, reset_violations
2026-02-09 18:51:27 +03:00
xpamych f75b37b56e Улучшение системы модерации и логирования наказаний
- Сообщение нарушителя удаляется при выдаче наказания
- Уведомление о наказании остаётся в чате с информацией: кто наказан, кем, причина
- Выделение найденных плохих слов в логах автомута
- Прикрепление медиа из сообщения нарушителя к логу наказания
2026-02-09 18:30:23 +03:00
xpamych fe60efaae5 Расширение списка исключений для фильтра мата
Добавлено 219 новых исключений из серверного файла:
- Словоформы с суффиксами (влюблять, расслаблять, оскорблять и др.)
- Технические термины (гейм, даунгрейд, ребаланс и др.)
- Географические названия (прибалт, прибалтика, любляна и др.)
- Производные слова (гребля, сабля, корабля и др.)

Общее количество исключений: 241 (было 22)
Это значительно снизит количество ложных срабатываний фильтра
2025-11-18 18:26:23 +03:00
xpamych 6a7df9bc55 Улучшение системы фильтрации мата и управления кармой
Фильтрация бранных слов:
- Добавлена полная транслитерация латиница→кириллица (все 26 букв)
- Добавлен маппинг цифр→буквы (0→о, 3→з, 4→ч, 6→б, 8→в и др.)
- Добавлено определение ASCII-art паттернов (><→х, }|{→ж и др.)
- Реализована 3-уровневая система категоризации паттернов:
  * whole_word - только целые слова с границами \b
  * word_start - только начало слова (новая категория)
  * contains - любое вхождение (минимум 4 буквы)
- Добавлен расширенный список исключений для предотвращения ложных срабатываний
  (требует, употреблять, скребу, гребу, республика, документ и др.)

Система кармы:
- Добавлен штраф за эмодзи клоуна 🤡 (-20 кармы тому, кто добавил)
- Карма не возвращается при снятии клоуна
- Реализована возможность отключения кармы в конкретных топиках:
  * Новая таблица disabled_karma_topics в БД
  * Методы: is_karma_disabled_in_topic(), disable/enable_karma_in_topic()
- Перенесены команды управления кармой из /badwords в /karma:
  * /karma disable - отключить карму в текущем топике
  * /karma enable - включить карму в текущем топике
  * /karma status - показать статус кармы во всех топиках
- Убраны упоминания пользователей в команде /top (используется <code>)

Аудит и логирование:
- Добавлена отправка в канал аудита для операций со словами:
  * Добавление/удаление бранных слов
  * Добавление/удаление исключений
- Расширен action_reporter для поддержки операций без привязки к пользователю
2025-11-18 18:16:59 +03:00
xpamych 0bd399f121 Исправление синтаксической ошибки 2025-10-20 09:23:50 +03:00
xpamych c257c6c1a2 Исправление логики работы кармы
Добавление дополнительных обработчиков ошибок в работе кармы
Убрано кэширование сообщений от ботов3
2025-10-20 09:19:04 +03:00
xpamych c7b2961ae1 Убран медленный режим 2025-10-20 01:33:17 +03:00
xpamych 8af2f128a7 Исправление дополнительное логирование /karma 2025-10-19 19:58:37 +03:00
xpamych 4aba68d242 Исправление с границами слов
Исправление команды карма с юзернеймом
2025-10-19 19:50:32 +03:00
xpamych 8bf512e509 Изменение увеличение кэша до 7 дней 2025-10-19 19:29:36 +03:00
xpamych 459ed66e9a Изменение кэширования сообщений 2025-10-19 19:22:07 +03:00
xpamych de1c82c267 Исправления зависания 2 2025-10-19 19:08:52 +03:00
xpamych 4a2aa00eb7 Исправления зависания 2025-10-19 18:42:10 +03:00
xpamych c4400fc244 Изменение логики медленного режима 2025-10-19 17:37:52 +03:00
xpamych b7f09ae719 Добавление ещё нескольких эмодзи 2025-10-19 14:54:46 +03:00
xpamych 1619e82df1 Исправление канала с отправкой уведомления о карме в правильный канал 2025-10-19 14:32:55 +03:00
xpamych 0ee7cb3bd4 Изменение логики эмодзи кармы как переключатель 2025-10-19 14:28:01 +03:00
xpamych 9b11f21bc1 Добавление уведомления за карму от эмодзи 2025-10-19 14:18:59 +03:00
xpamych 2b9e819944 Добавление команды установки кармы 2025-10-19 14:14:35 +03:00
xpamych 58daea0492 Добавление логирования кэширования 2025-10-19 14:05:21 +03:00
xpamych 63ac924a3d Добавление медленного режима в зависимости от кармы 2025-10-19 13:56:21 +03:00
xpamych be64915e9b Добавление кармы за реакцию большого пальца 2025-10-19 13:34:56 +03:00
xpamych 61e9d31a75 Исключение ложного срабатывания при благодарности без ответа 2025-10-19 13:27:44 +03:00
xpamych 6bdf996ca4 Увеличение времени на отображения сообщения с результатом увеличения кармы
Увеличение очков кармы за благодарность с !
2025-10-19 13:21:19 +03:00
xpamych c07a082694 Увеличение времени на отображения сообщения с результатом увеличения кармы 2025-10-19 13:18:23 +03:00
xpamych 6b41e61d7e Исправление конкуренции обработчиков 2025-10-19 13:16:46 +03:00
xpamych 1595acb4bb Улучшение обнаружения мата 2025-10-19 13:08:26 +03:00
xpamych 44a8b54ddc Исправления критикал проблем 2025-10-19 12:52:54 +03:00
xpamych 50d137ffc8 Исправление по карме 1 2025-10-19 12:21:19 +03:00
xpamych 1e43cb6c98 Добавление логирования для дебага 2025-10-19 01:29:58 +03:00
xpamych 15ac2fdb07 Добавление исправление ошибки работы кармы 2025-10-18 21:34:40 +03:00
xpamych a16b5322af Добавление возможности использования кармы 2025-10-18 21:17:41 +03:00
xpamych 47cf59f710 Добавление команды /warn 2025-10-18 13:57:30 +03:00
xpamych 2709c8ce40 Исправление определения пользователя для сброса нарушений 2025-10-18 13:41:35 +03:00
xpamych ab80af2744 Исправление определения прав 2025-10-18 13:27:05 +03:00
xpamych 383abb0235 Исправление обработки прав администратора
Изменение сообщений о банах
2025-10-18 13:21:47 +03:00
xpamych 295866ed2d Добавление обработки ошибок и валидации 2025-10-18 13:14:23 +03:00
xpamych ff397dc496 Исправления для работы в супергруппе 2 2025-10-13 14:55:37 +03:00
xpamych e0886fcfa8 Исправления для работы в супергруппе 1 2025-10-13 14:51:23 +03:00
xpamych 313389d2bf Исправления для работы в супергруппе 2025-10-13 14:47:23 +03:00
xpamych 7cdb7086a3 Исправление обработки ошибки админских сообщений 2 2025-10-13 14:35:53 +03:00
xpamych 98d171dcf8 Исправление обработки ошибки админских сообщений 1 2025-10-13 14:34:03 +03:00
xpamych d8385d73f2 Исправление обработки ошибки админских сообщений 2025-10-13 14:31:06 +03:00
xpamych 0426ae59cc исправление импорта 2025-10-13 14:25:16 +03:00
xpamych 69d55a68b9 исправление работы списков слов
добавление сброса счётчиков нарушений
2025-10-13 14:20:18 +03:00
xpamych ab10879c06 исправление работы списков слов 2025-10-13 14:05:47 +03:00
xpamych 1d32ec94e5 Исправление автомута 2025-10-12 16:33:28 +03:00
xpamych 7ce23f8142 Добавлено логирование всех входящих сообщений в middleware 2025-10-12 16:28:20 +03:00
xpamych 96da8266e9 Добавлено логирование в badwords_manager 2025-10-12 16:21:16 +03:00
xpamych 80d1a3994c Исправление описаний 2025-10-12 16:11:27 +03:00
xpamych f6b888c6d3 Добавление автомута 2025-10-12 16:00:02 +03:00
xpamych 643ad9feda Добавление скрипта обновления 2025-10-12 12:23:13 +03:00
xpamych f7b3866b6a Добавление скрипта обновления 2025-10-12 12:20:47 +03:00
xpamych ebe2b2c0fd Добавление подсказок по командом
Добавление сообщений о создании лога
2025-10-12 12:11:41 +03:00
33 changed files with 6190 additions and 1027 deletions

No files matched your search

+1 -2
View File
@@ -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
View File
@@ -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
+180 -1
View File
@@ -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`
-1
View File
@@ -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
View File
@@ -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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
# Обрезаем длинный текст
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)}")
+765
View File
@@ -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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
highlighted = f'🔴<b>{escaped}</b>🔴'
result_list[start:end] = highlighted
return ''.join(result_list)
# Предзагружаем слова при импорте модуля
load_bad_words()
+343 -55
View File
@@ -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]
+401
View File
@@ -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",
"аблятив",
"абляут",
"абляционный",
"абляция",
"ассамбляж",
"ассамбляжем",
"блямба",
"бляха",
"бляшечка",
"бляшка",
"взвихренный",
"витеблянин",
"влюблявшийся",
"влюблять",
"влюбляться",
"влюбляющийся",
"всколебать",
"всколебаться",
"выгребать",
"выгребаться",
"выскребать",
"выхлебать",
"выщерблять",
"гейм",
"гейтвей",
"гребля",
"гребляк",
"гребу",
"гребут",
"даунгрейд",
"даунгрейт",
"догребать",
"документ",
"документы",
"доскребать",
"дохлебать",
"дубляж",
"дубляжем",
"дубляжом",
"забляковать",
"завихрение",
"завихренние",
"заглублявший",
"заглублять",
"заглубляющий",
"загребать",
"заколебать",
"заколебаться",
"зарыблять",
"застрахуйся",
"злоупотреблявший",
"злоупотреблял",
"злоупотреблять",
"злоупотребляющий",
"истреблявший",
"истреблявшийся",
"истребляемый",
"истреблять",
"истребляться",
"истребляющий",
"истребляющийся",
"козловский",
"козловского",
"колебаний",
"колебать",
"колебаться",
"колебля",
"колеблясь",
"корабля",
"кулдаун",
"любляна",
"люблянский",
"люблянцев",
"люблянцем",
"нагребать",
"наскребать",
"нахлебаться",
"небалансным",
"небудь",
"обляпавший",
"обляпался",
"обляпанный",
"обляпать",
"обляпаться",
"обляпывать",
"обляпываться",
"облёк",
"облёкся",
"обособлявший",
"обособлявшийся",
"обособляйся",
"обособлять",
"обособляться",
"обособляющий",
"обособляющийся",
"оглобля",
"огребать",
"огребаться",
"огрублявший",
"огрублять",
"огрубляющий",
"озлоблявший",
"озлоблявшийся",
"озлобляемый",
"озлоблять",
"озлобляться",
"озлобляющий",
"озлобляющийся",
"оскорблявший",
"оскорблявшийся",
"оскорбляемый",
"оскорбляет",
"оскорбляешь",
"оскорблять",
"оскорбляться",
"оскорбляющий",
"оскорбляющийся",
"оскребать",
"ослаблявший",
"ослаблявшийся",
"ослабляемый",
"ослаблять",
"ослабляться",
"ослабляющий",
"ослабляющийся",
"отгребать",
"отгребаться",
"отскребать",
"отхлебать",
"перегребать",
"перестрахуйся",
"плохую",
"погребать",
"подгребание",
"подгребать",
"подгребаться",
"подскребать",
"подсоблять",
"поколебать",
"поколебаться",
"послаблять",
"пособлявший",
"пособлять",
"пособляющий",
"потребление",
"потреблявший",
"потребляемый",
"потребляет",
"потребляла",
"потреблялась",
"потреблять",
"потребляться",
"потребляют",
"потребляющий",
"потребует",
"потребуется",
"похлебать",
"прибалт",
"прибалтика",
"пригребать",
"пригребаться",
"приспособлявший",
"приспособлявшийся",
"приспособляемость",
"приспособляемый",
"приспособлялся",
"приспособлять",
"приспособляться",
"приспособляющий",
"приспособляющийся",
"прогребать",
"проскребать",
"разгребать",
"разгребаться",
"раздроблявший",
"раздроблявшийся",
"раздроблять",
"раздробляться",
"раздробляющий",
"раздробляющийся",
"разлюблять",
"расподобляться",
"расслаблявший",
"расслаблявшийся",
"расслаблять",
"расслабляться",
"расслабляющий",
"расслабляющийся",
"расхлебать",
"ребаланс",
"ребут",
"республика",
"сабля",
"сабляница",
"сгребать",
"сгребаться",
"сиагейт",
"сиагейты",
"скоблящий",
"скоблящийся",
"скорблящий",
"скребу",
"скребут",
"слюбляться",
"соскребать",
"сподоблять",
"сподобляться",
"стебать",
"стебля",
"схлебать",
"твар",
"требование",
"требовать",
"требуем",
"требует",
"требуется",
"требуха",
"требую",
"требуют",
"углублявший",
"углублявшийся",
"углублять",
"углубляться",
"углубляющий",
"угребать",
"уподоблявший",
"уподоблявшийся",
"уподоблять",
"уподобляться",
"уподобляющий",
"уподобляющийся",
"употребление",
"употреблявший",
"употреблявшийся",
"употребляемый",
"употреблять",
"употребляться",
"употребляю",
"употребляющий",
"употребляющийся",
"усугублявший",
"усугублявшийся",
"усугубляемый",
"усугублять",
"усугубляться",
"усугубляющий",
"усугубляющийся",
"ущерблять",
"ущербляться",
"херметичность",
"хлебать",
"чебурнет",
"шоудаун",
"эчпочмак"
]
}
+25
View File
@@ -0,0 +1,25 @@
{
"thank_words": [
"спасибо",
"благодарю",
"спс",
"сенкс",
"сенкью",
"thanks",
"thank you",
"thx",
"ty",
"дякую",
"дзякуй",
"рахмет",
"пасиб",
"пасибо",
"спасибочки",
"благодарочка",
"мерси",
"merci",
"danke",
"gracias",
"grazie"
]
}
+540 -4
View File
@@ -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()
View File
Whitespace-only changes.
+4 -2
View File
@@ -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
View File
@@ -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:
+393
View File
@@ -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)
+218
View File
@@ -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)")
+279
View File
@@ -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
View File
@@ -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)}")
-67
View File
@@ -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)}")
+309
View File
@@ -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
View File
@@ -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)}")
+387
View File
@@ -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)
+87
View File
@@ -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
View File
@@ -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)}")
-202
View File
@@ -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)
-215
View File
@@ -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)
+336
View File
@@ -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)}")
+109
View File
@@ -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
View File
@@ -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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
# Обрезаем очень длинные сообщения
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:
+201
View File
@@ -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()
+79
View File
@@ -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()
+190
View File
@@ -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()
+302
View File
@@ -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()
Executable
+77
View File
@@ -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 "🎉 Обновление завершено!"