forked from Muzifs/LGBot
Добавление автомута
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
## changed at Sun Oct 12 12:00:13 MSK 2025
|
## changed at Sun Oct 12 15:29:33 MSK 2025
|
||||||
#Sun Oct 12 12:00:13 MSK 2025
|
#Sun Oct 12 15:29:33 MSK 2025
|
||||||
com.gigaide.elements.ext.marker.solution.BeanMarkedPsi.shouldMark=true
|
com.gigaide.elements.ext.marker.solution.BeanMarkedPsi.shouldMark=true
|
||||||
com.gigaide.elements.ext.marker.solution.ConfigMarkedPsi.shouldMark=true
|
com.gigaide.elements.ext.marker.solution.ConfigMarkedPsi.shouldMark=true
|
||||||
com.gigaide.elements.ext.marker.solution.DataMarkedPsi.shouldMark=true
|
com.gigaide.elements.ext.marker.solution.DataMarkedPsi.shouldMark=true
|
||||||
|
167
README.md
167
README.md
@@ -40,4 +40,169 @@ python src/main.py
|
|||||||
- Проверит наличие обновлений в git-репозитории
|
- Проверит наличие обновлений в git-репозитории
|
||||||
- Загрузит изменения (`git pull`)
|
- Загрузит изменения (`git pull`)
|
||||||
- Перезапустит службу бота (`systemctl restart LGBot.service`)
|
- Перезапустит службу бота (`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`
|
153
src/bad_words.py
Normal file
153
src/bad_words.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Система управления бранными словами
|
||||||
|
# Список слов хранится в JSON файле для возможности управления через команды
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Путь к файлу с бранными словами
|
||||||
|
BAD_WORDS_FILE = os.path.join(os.path.dirname(__file__), 'data', 'bad_words.json')
|
||||||
|
|
||||||
|
# Кэш для загруженных слов
|
||||||
|
_bad_words_cache = None
|
||||||
|
_exceptions_cache = None
|
||||||
|
|
||||||
|
def load_bad_words():
|
||||||
|
"""
|
||||||
|
Загружает список бранных слов из JSON файла.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (список бранных слов, список исключений)
|
||||||
|
"""
|
||||||
|
global _bad_words_cache, _exceptions_cache
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
_bad_words_cache = data.get('bad_words', [])
|
||||||
|
_exceptions_cache = data.get('exceptions', [])
|
||||||
|
logger.info(f"Загружено {len(_bad_words_cache)} бранных слов и {len(_exceptions_cache)} исключений")
|
||||||
|
return _bad_words_cache, _exceptions_cache
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error(f"Файл {BAD_WORDS_FILE} не найден")
|
||||||
|
return [], []
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Ошибка чтения JSON: {e}")
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
def save_bad_words(bad_words: list, exceptions: list):
|
||||||
|
"""
|
||||||
|
Сохраняет список бранных слов в JSON файл.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bad_words: Список бранных слов
|
||||||
|
exceptions: Список исключений
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если успешно, иначе False
|
||||||
|
"""
|
||||||
|
global _bad_words_cache, _exceptions_cache
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем директорию, если её нет
|
||||||
|
os.makedirs(os.path.dirname(BAD_WORDS_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'bad_words': sorted(list(set(bad_words))), # Убираем дубликаты и сортируем
|
||||||
|
'exceptions': sorted(list(set(exceptions)))
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(BAD_WORDS_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# Обновляем кэш
|
||||||
|
_bad_words_cache = data['bad_words']
|
||||||
|
_exceptions_cache = data['exceptions']
|
||||||
|
|
||||||
|
logger.info(f"Сохранено {len(_bad_words_cache)} бранных слов и {len(_exceptions_cache)} исключений")
|
||||||
|
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 _bad_words_cache, _exceptions_cache
|
||||||
|
_bad_words_cache = None
|
||||||
|
_exceptions_cache = None
|
||||||
|
return load_bad_words()
|
||||||
|
|
||||||
|
# Загружаем слова при импорте модуля
|
||||||
|
BAD_WORDS, EXCEPTIONS = load_bad_words()
|
||||||
|
|
||||||
|
def contains_bad_word(text: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, содержит ли текст бранные слова.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Текст для проверки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True, если найдено бранное слово, иначе False
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Приводим к нижнему регистру для проверки
|
||||||
|
text_lower = text.lower()
|
||||||
|
|
||||||
|
# Проверяем исключения
|
||||||
|
for exception in EXCEPTIONS:
|
||||||
|
if exception in text_lower:
|
||||||
|
text_lower = text_lower.replace(exception, '')
|
||||||
|
|
||||||
|
# Проверяем бранные слова
|
||||||
|
for bad_word in BAD_WORDS:
|
||||||
|
if bad_word in text_lower:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_bad_words_from_text(text: str) -> list:
|
||||||
|
"""
|
||||||
|
Возвращает список найденных бранных слов в тексте.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Текст для проверки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список найденных бранных слов
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
|
||||||
|
text_lower = text.lower()
|
||||||
|
found_words = []
|
||||||
|
|
||||||
|
# Проверяем исключения
|
||||||
|
for exception in EXCEPTIONS:
|
||||||
|
if exception in text_lower:
|
||||||
|
text_lower = text_lower.replace(exception, '')
|
||||||
|
|
||||||
|
# Ищем бранные слова
|
||||||
|
for bad_word in BAD_WORDS:
|
||||||
|
if bad_word in text_lower:
|
||||||
|
found_words.append(bad_word)
|
||||||
|
|
||||||
|
return found_words
|
@@ -96,6 +96,33 @@ COMMAND_MESSAGES = {
|
|||||||
'banned': '✅ Пользователь успешно забанен.',
|
'banned': '✅ Пользователь успешно забанен.',
|
||||||
'unbanned': '✅ Пользователь успешно разбанен.',
|
'unbanned': '✅ Пользователь успешно разбанен.',
|
||||||
'error': '⚠️ Ошибка: {e}',
|
'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>"
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
47
src/data/bad_words.json
Normal file
47
src/data/bad_words.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"bad_words": [
|
||||||
|
"хуй", "хуе", "хуи", "хую", "хуя", "хер",
|
||||||
|
"пизд", "пизж", "пезд",
|
||||||
|
"ебал", "ебан", "ебат", "ебу", "ебош", "ебля", "ебет",
|
||||||
|
"бля", "блядь", "блять",
|
||||||
|
"сука", "суки", "сучк", "сучар",
|
||||||
|
"мудак", "мудил", "муди",
|
||||||
|
"гандон",
|
||||||
|
"даун",
|
||||||
|
"дебил",
|
||||||
|
"долбоеб", "долбаеб",
|
||||||
|
"уебан", "уебок",
|
||||||
|
"хуесос",
|
||||||
|
"пидор", "пидар", "педик", "педр",
|
||||||
|
"гей", "гомик", "гомос",
|
||||||
|
"шлюх", "шалав",
|
||||||
|
"еблан",
|
||||||
|
"говн",
|
||||||
|
"срать", "сраль", "серун",
|
||||||
|
"дрочи", "дроч",
|
||||||
|
"жоп", "жёп",
|
||||||
|
"залуп",
|
||||||
|
"мразь", "мраз",
|
||||||
|
"козел", "козл",
|
||||||
|
"урод", "урода",
|
||||||
|
"ублюдо", "ублюд",
|
||||||
|
"тварь", "твар",
|
||||||
|
"падла",
|
||||||
|
"сволочь", "сволоч",
|
||||||
|
"гнида", "гнид",
|
||||||
|
"выблядо",
|
||||||
|
"хуета", "хуйн",
|
||||||
|
"охуе", "охуи", "охуя",
|
||||||
|
"нахуй", "нахер",
|
||||||
|
"похуй", "похер",
|
||||||
|
"захуя",
|
||||||
|
"ахуе",
|
||||||
|
"впизду",
|
||||||
|
"попизд"
|
||||||
|
],
|
||||||
|
"exceptions": [
|
||||||
|
"республика",
|
||||||
|
"документ",
|
||||||
|
"документы"
|
||||||
|
]
|
||||||
|
}
|
@@ -13,7 +13,7 @@ class Database: # Инициализация класса
|
|||||||
self.db_name = db_name
|
self.db_name = db_name
|
||||||
self._init_db()
|
self._init_db()
|
||||||
|
|
||||||
# Инициализирует базу данных и создает таблицу, если она не существует
|
# Инициализирует базу данных и создает таблицы, если они не существуют
|
||||||
def _init_db(self):
|
def _init_db(self):
|
||||||
with self._get_connection() as connect:
|
with self._get_connection() as connect:
|
||||||
cursor = connect.cursor()
|
cursor = connect.cursor()
|
||||||
@@ -24,6 +24,16 @@ class Database: # Инициализация класса
|
|||||||
tag TEXT
|
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)
|
||||||
|
)
|
||||||
|
''')
|
||||||
connect.commit()
|
connect.commit()
|
||||||
|
|
||||||
# Возвращает соединение с базой данных
|
# Возвращает соединение с базой данных
|
||||||
@@ -70,15 +80,68 @@ class Database: # Инициализация класса
|
|||||||
def get_user_by_username(self, username: str) -> Optional[Tuple]:
|
def get_user_by_username(self, username: str) -> Optional[Tuple]:
|
||||||
if not username:
|
if not username:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with self._get_connection() as connect:
|
with self._get_connection() as connect:
|
||||||
cursor = connect.cursor()
|
cursor = connect.cursor()
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT id, nickname, tag
|
SELECT id, nickname, tag
|
||||||
FROM users
|
FROM users
|
||||||
WHERE LOWER(tag) = LOWER(?)
|
WHERE LOWER(tag) = LOWER(?)
|
||||||
''', (username,))
|
''', (username,))
|
||||||
return cursor.fetchone()
|
return cursor.fetchone()
|
||||||
|
|
||||||
|
# Добавляет нарушение в базу данных
|
||||||
|
def add_violation(self, user_id: int, chat_id: int, violation_type: str = 'bad_language'):
|
||||||
|
import time
|
||||||
|
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:
|
||||||
|
import time
|
||||||
|
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):
|
||||||
|
import time
|
||||||
|
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
|
||||||
|
|
||||||
# Создаем экземпляр базы данных для импорта в других модулях
|
# Создаем экземпляр базы данных для импорта в других модулях
|
||||||
db = Database()
|
db = Database()
|
@@ -121,6 +121,7 @@ async def setup_bot_commands():
|
|||||||
BotCommand("unban", "Разбанить пользователя"),
|
BotCommand("unban", "Разбанить пользователя"),
|
||||||
BotCommand("mute", "Замутить пользователя"),
|
BotCommand("mute", "Замутить пользователя"),
|
||||||
BotCommand("unmute", "Размутить пользователя"),
|
BotCommand("unmute", "Размутить пользователя"),
|
||||||
|
BotCommand("badwords", "Управление списком бранных слов"),
|
||||||
BotCommand("botdata", "Получить данные бота (только админы)"),
|
BotCommand("botdata", "Получить данные бота (только админы)"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
199
src/modules/auto_mute.py
Normal file
199
src/modules/auto_mute.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
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
|
||||||
|
from action_reporter import action_reporter
|
||||||
|
from utils import delete_messages, format_mute_time
|
||||||
|
|
||||||
|
# Получаем логгер для текущего модуля
|
||||||
|
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, # 16. Перманентный мут (режим только чтения навсегда)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Период для подсчета нарушений (30 дней в секундах)
|
||||||
|
VIOLATIONS_PERIOD = 2592000
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Применяет мут к пользователю.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: Экземпляр бота
|
||||||
|
message: Сообщение, которое вызвало мут
|
||||||
|
user_id: ID пользователя
|
||||||
|
duration: Длительность мута в секундах (None для перманентного)
|
||||||
|
violations_count: Количество нарушений
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Удаляем сообщение с матом
|
||||||
|
try:
|
||||||
|
await bot.delete_message(chat_id=message.chat.id, message_id=message.message_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Не удалось удалить сообщение: {e}")
|
||||||
|
|
||||||
|
# Формируем сообщение о муте
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отправляем сообщение-лог в админ-чат
|
||||||
|
await action_reporter.log_action(
|
||||||
|
action="АВТОМУТ",
|
||||||
|
user_id=user_id,
|
||||||
|
admin_id=None, # Автоматическое действие
|
||||||
|
reason=f"Использование нецензурной лексики (нарушение #{violations_count})",
|
||||||
|
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
|
||||||
|
|
||||||
|
# Получаем 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)
|
||||||
|
|
||||||
|
def register_handlers(bot: AsyncTeleBot):
|
||||||
|
"""
|
||||||
|
Регистрирует обработчики для автоматического мута.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Обработчик всех текстовых сообщений
|
||||||
|
@bot.message_handler(func=lambda message: message.text and message.chat.type in ['group', 'supergroup'])
|
||||||
|
async def handle_text_message(message: Message):
|
||||||
|
await check_message_for_profanity(bot, message)
|
||||||
|
|
||||||
|
logger.info("Модуль автоматического мута успешно загружен")
|
231
src/modules/badwords_manager.py
Normal file
231
src/modules/badwords_manager.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
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 utils import check_admin_status, delete_messages
|
||||||
|
from config import COMMAND_MESSAGES
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def register_handlers(bot: AsyncTeleBot):
|
||||||
|
"""Регистрирует обработчики команд управления бранными словами"""
|
||||||
|
|
||||||
|
@bot.message_handler(commands=['badwords'])
|
||||||
|
async def badwords_command(message: Message):
|
||||||
|
"""Главная команда управления списком бранных слов"""
|
||||||
|
|
||||||
|
# Проверяем права администратора
|
||||||
|
if await check_admin_status(bot, message) == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
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}")
|
||||||
|
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}")
|
||||||
|
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}")
|
||||||
|
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}")
|
||||||
|
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)
|
Reference in New Issue
Block a user