Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
0bd399f121 | |||
c257c6c1a2 | |||
c7b2961ae1 | |||
8af2f128a7 | |||
4aba68d242 | |||
8bf512e509 | |||
459ed66e9a | |||
de1c82c267 | |||
4a2aa00eb7 | |||
c4400fc244 | |||
b7f09ae719 | |||
1619e82df1 | |||
0ee7cb3bd4 | |||
9b11f21bc1 | |||
2b9e819944 | |||
58daea0492 | |||
63ac924a3d | |||
be64915e9b | |||
61e9d31a75 | |||
6bdf996ca4 | |||
c07a082694 | |||
6b41e61d7e | |||
1595acb4bb | |||
44a8b54ddc | |||
50d137ffc8 | |||
1e43cb6c98 | |||
15ac2fdb07 | |||
a16b5322af | |||
47cf59f710 | |||
2709c8ce40 | |||
ab80af2744 | |||
383abb0235 | |||
295866ed2d | |||
ff397dc496 | |||
e0886fcfa8 | |||
313389d2bf | |||
7cdb7086a3 | |||
98d171dcf8 | |||
d8385d73f2 | |||
0426ae59cc | |||
69d55a68b9 | |||
ab10879c06 | |||
1d32ec94e5 | |||
7ce23f8142 | |||
96da8266e9 | |||
80d1a3994c | |||
f6b888c6d3 | |||
643ad9feda | |||
f7b3866b6a | |||
ebe2b2c0fd |
@@ -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 топика, брать из ссылки сообщения
|
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,6 +1,33 @@
|
||||
# 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
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
181
README.md
181
README.md
@@ -26,4 +26,183 @@ pip install -r requirements.txt
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
> Используется Python 3.11.0
|
||||
> Используется Python 3.11.0
|
||||
|
||||
### Обновление на сервере
|
||||
|
||||
Для обновления бота на продакшн-сервере используйте скрипт `update.sh`:
|
||||
|
||||
```sh
|
||||
./update.sh
|
||||
```
|
||||
|
||||
Скрипт автоматически:
|
||||
- Проверит наличие обновлений в git-репозитории
|
||||
- Загрузит изменения (`git pull`)
|
||||
- Перезапустит службу бота (`systemctl restart LGBot.service`)
|
||||
- Покажет статус работы бота
|
||||
|
||||
---
|
||||
|
||||
## Система автоматического мьюта
|
||||
|
||||
Бот автоматически отслеживает использование нецензурной лексики и применяет прогрессирующие наказания.
|
||||
|
||||
### Как работает
|
||||
|
||||
1. **Обнаружение нарушений:**
|
||||
- Каждое текстовое сообщение проверяется на наличие бранных слов
|
||||
- Проверяются только группы и супергруппы (не личные сообщения)
|
||||
- Команды (начинающиеся с `/`) не проверяются
|
||||
|
||||
2. **При обнаружении мата:**
|
||||
- Сообщение удаляется автоматически
|
||||
- Нарушение фиксируется в базе данных
|
||||
- Пользователь получает мут соответствующего уровня
|
||||
- Уведомление отправляется в чат и админ-чат
|
||||
|
||||
3. **Прогрессирующие наказания:**
|
||||
|
||||
| № нарушения | Длительность мута | № нарушения | Длительность мута |
|
||||
|-------------|-------------------|-------------|-------------------|
|
||||
| 1 | 5 минут | 9 | 1 день |
|
||||
| 2 | 15 минут | 10 | 2 дня |
|
||||
| 3 | 30 минут | 11 | 3 дня |
|
||||
| 4 | 1 час | 12 | 5 дней |
|
||||
| 5 | 2 часа | 13 | 7 дней |
|
||||
| 8 | 12 часов | 16+ | **НАВСЕГДА** |
|
||||
|
||||
4. **Накопительный эффект:**
|
||||
- Нарушения учитываются за последние **30 дней**
|
||||
- Старые нарушения автоматически удаляются из базы
|
||||
- Администраторы **освобождены** от автоматических мутов
|
||||
|
||||
### Управление списком бранных слов
|
||||
|
||||
⚠️ **Все команды доступны только администраторам чата**
|
||||
|
||||
#### Основные команды
|
||||
|
||||
```bash
|
||||
# Показать справку
|
||||
/badwords help
|
||||
|
||||
# Показать список бранных слов (первые 50)
|
||||
/badwords list
|
||||
|
||||
# Статистика
|
||||
/badwords count
|
||||
|
||||
# Добавить слово в список
|
||||
/badwords add <слово>
|
||||
|
||||
# Удалить слово из списка
|
||||
/badwords remove <слово>
|
||||
```
|
||||
|
||||
#### Исключения
|
||||
|
||||
Исключения — слова, содержащие бранные корни, но не являющиеся матом (например: "республика", "документ"):
|
||||
|
||||
```bash
|
||||
# Показать исключения
|
||||
/badwords exceptions
|
||||
|
||||
# Добавить исключение
|
||||
/badwords add_exception <слово>
|
||||
|
||||
# Удалить исключение
|
||||
/badwords remove_exception <слово>
|
||||
```
|
||||
|
||||
#### Служебные команды
|
||||
|
||||
```bash
|
||||
# Перезагрузить списки из файла
|
||||
/badwords reload
|
||||
```
|
||||
|
||||
### Рекомендации
|
||||
|
||||
1. **Используйте корни слов**, а не полные формы:
|
||||
- ✅ Правильно: `ебал` (поймает все формы)
|
||||
- ❌ Неправильно: `ебала` (пропустит другие формы)
|
||||
|
||||
2. **Избегайте коротких корней** (могут давать ложные срабатывания)
|
||||
|
||||
3. **Тестируйте после добавления** нового слова
|
||||
|
||||
### Хранение данных
|
||||
|
||||
Все списки хранятся в файле `src/data/bad_words.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"bad_words": ["слово1", "слово2", ...],
|
||||
"exceptions": ["исключение1", "исключение2", ...]
|
||||
}
|
||||
```
|
||||
|
||||
Изменения через команды применяются **немедленно**, перезапуск бота не требуется.
|
||||
|
||||
### Логирование
|
||||
|
||||
Все действия записываются в:
|
||||
- **bot.log** - файл логов
|
||||
- **Админ-чат** - уведомления о мутах
|
||||
|
||||
Примеры логов:
|
||||
```
|
||||
[INFO] Пользователь 123456789 получил автоматический мут на 5 минут за нецензурную лексику (нарушение #1)
|
||||
[INFO] Нарушение пользователя 123456789 зафиксировано в чате -100123456789
|
||||
[INFO] Администратор 987654321 добавил бранное слово: спам
|
||||
```
|
||||
|
||||
### База данных
|
||||
|
||||
Таблица `violations` хранит все нарушения:
|
||||
- `id` - уникальный идентификатор
|
||||
- `user_id` - ID пользователя
|
||||
- `chat_id` - ID чата
|
||||
- `violation_date` - время нарушения (unix timestamp)
|
||||
- `violation_type` - тип нарушения ('bad_language')
|
||||
|
||||
### Настройка системы
|
||||
|
||||
#### Изменение уровней мутов
|
||||
|
||||
Откройте `src/modules/auto_mute.py` и измените массив `MUTE_LEVELS`:
|
||||
|
||||
```python
|
||||
MUTE_LEVELS = [
|
||||
300, # 1. 5 минут
|
||||
900, # 2. 15 минут
|
||||
# ... добавьте или измените уровни
|
||||
None, # Перманентный мут
|
||||
]
|
||||
```
|
||||
|
||||
#### Изменение периода накопления
|
||||
|
||||
Измените `VIOLATIONS_PERIOD` в `src/modules/auto_mute.py`:
|
||||
|
||||
```python
|
||||
VIOLATIONS_PERIOD = 2592000 # 30 дней в секундах
|
||||
```
|
||||
|
||||
### Требования
|
||||
|
||||
- Python 3.7+
|
||||
- pyTelegramBotAPI (telebot)
|
||||
- SQLite3
|
||||
- Права бота в чате:
|
||||
- Удаление сообщений
|
||||
- Ограничение пользователей
|
||||
|
||||
### Устранение проблем
|
||||
|
||||
При возникновении проблем проверьте:
|
||||
1. Логи бота в файле `bot.log`
|
||||
2. Права бота в чате (удаление сообщений, ограничение пользователей)
|
||||
3. Корректность настроек в `.env`
|
||||
4. Наличие файла `src/data/bad_words.json`
|
@@ -5,7 +5,6 @@ asyncio==3.4.3
|
||||
attrs==25.3.0
|
||||
certifi==2025.6.15
|
||||
charset-normalizer==3.4.2
|
||||
dotenv==0.9.9
|
||||
frozenlist==1.7.0
|
||||
idna==3.10
|
||||
multidict==6.6.3
|
||||
|
@@ -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,11 +52,14 @@ 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
|
||||
|
||||
# Отправляет лог действия в админ-чат
|
||||
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: int, admin_id: Optional[int], reason: str, duration: str, photo_path: Optional[str] = None):
|
||||
try:
|
||||
|
||||
# Получаем информацию о пользователе и администраторе
|
||||
|
326
src/bad_words.py
Normal file
326
src/bad_words.py
Normal file
@@ -0,0 +1,326 @@
|
||||
# Система управления бранными словами
|
||||
# Список слов хранится в JSON файле для возможности управления через команды
|
||||
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
|
||||
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
|
||||
_whole_word_patterns_cache = None
|
||||
_contains_patterns_cache = None
|
||||
|
||||
def load_bad_words():
|
||||
"""
|
||||
Загружает список бранных слов из JSON файла.
|
||||
Поддерживает как новый формат (patterns), так и старый (bad_words).
|
||||
|
||||
Returns:
|
||||
tuple: (список бранных слов, список исключений)
|
||||
"""
|
||||
global _bad_words_cache, _exceptions_cache, _whole_word_patterns_cache, _contains_patterns_cache
|
||||
|
||||
try:
|
||||
with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Проверяем, какой формат используется
|
||||
if 'patterns' in data:
|
||||
# Новый формат с паттернами
|
||||
patterns = data['patterns']
|
||||
_whole_word_patterns_cache = patterns.get('whole_word', [])
|
||||
_contains_patterns_cache = patterns.get('contains', [])
|
||||
_exceptions_cache = data.get('exceptions', [])
|
||||
|
||||
# Для обратной совместимости объединяем в один список
|
||||
_bad_words_cache = _whole_word_patterns_cache + _contains_patterns_cache
|
||||
|
||||
logger.info(
|
||||
f"Загружено паттернов: {len(_whole_word_patterns_cache)} whole_word, "
|
||||
f"{len(_contains_patterns_cache)} contains, {len(_exceptions_cache)} исключений"
|
||||
)
|
||||
else:
|
||||
# Старый формат для обратной совместимости
|
||||
_bad_words_cache = data.get('bad_words', [])
|
||||
_exceptions_cache = data.get('exceptions', [])
|
||||
_whole_word_patterns_cache = []
|
||||
_contains_patterns_cache = _bad_words_cache.copy()
|
||||
|
||||
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()
|
||||
|
||||
def normalize_text(text: str) -> str:
|
||||
"""
|
||||
Нормализует текст для обхода обфускации.
|
||||
|
||||
Убирает:
|
||||
- Звездочки, точки, подчеркивания между буквами (х*й, х.у.й, х_у_й → хуй)
|
||||
- Повторяющиеся символы (хууууууй → хуй)
|
||||
- ОДИНОЧНЫЕ пробелы между ОДИНОЧНЫМИ буквами (х у й → хуй, но "не бу" остаётся "не бу")
|
||||
|
||||
Args:
|
||||
text: Исходный текст
|
||||
|
||||
Returns:
|
||||
Нормализованный текст
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Приводим к нижнему регистру
|
||||
normalized = text.lower()
|
||||
|
||||
# Циклически убираем обфускацию, пока что-то меняется
|
||||
max_iterations = 10
|
||||
for _ in range(max_iterations):
|
||||
before = normalized
|
||||
|
||||
# Убираем звёздочки, точки, дефисы, подчёркивания между буквами
|
||||
# х*й, х.у.й, х_у_й → хуй
|
||||
normalized = re.sub(r'([а-яё])[\*\.\-_]+([а-яё])', r'\1\2', normalized)
|
||||
|
||||
# Убираем ОДИНОЧНЫЕ пробелы между ОДИНОЧНЫМИ буквами (обфускация)
|
||||
# "х у й" → "хуй", но "не бу" → "не бу" (не склеиваем обычные слова)
|
||||
# Паттерн: одиночная буква + пробелы + одиночная буква
|
||||
normalized = re.sub(r'\b([а-яё])\s+([а-яё])\b', r'\1\2', normalized)
|
||||
|
||||
if before == normalized:
|
||||
break
|
||||
|
||||
# Убираем повторяющиеся буквы (более 2 подряд)
|
||||
# хууууууй → хуй, пииииздец → пиздец
|
||||
normalized = re.sub(r'([а-яё])\1{2,}', r'\1', normalized)
|
||||
|
||||
return normalized
|
||||
|
||||
# Предзагружаем слова при импорте модуля в кэш
|
||||
load_bad_words()
|
||||
|
||||
def contains_bad_word(text: str) -> bool:
|
||||
"""
|
||||
Проверяет, содержит ли текст бранные слова.
|
||||
|
||||
Использует:
|
||||
- Нормализацию текста для обхода обфускации
|
||||
- Проверку границ слов для whole_word паттернов
|
||||
- Проверку подстрок для contains паттернов
|
||||
- Список исключений
|
||||
|
||||
Args:
|
||||
text: Текст для проверки
|
||||
|
||||
Returns:
|
||||
True, если найдено бранное слово, иначе False
|
||||
"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
# Нормализуем текст (обход обфускации)
|
||||
normalized_text = normalize_text(text)
|
||||
|
||||
# Получаем паттерны и исключения
|
||||
global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
|
||||
|
||||
# Если кэш не загружен, загружаем
|
||||
if _whole_word_patterns_cache is None:
|
||||
load_bad_words()
|
||||
|
||||
whole_word_patterns = _whole_word_patterns_cache or []
|
||||
contains_patterns = _contains_patterns_cache or []
|
||||
exceptions = _exceptions_cache or []
|
||||
|
||||
# 1. Проверяем whole_word паттерны (только целые слова)
|
||||
for pattern in whole_word_patterns:
|
||||
# Используем границы слов \b для поиска только целых слов
|
||||
regex = r'\b' + re.escape(pattern) + r'\b'
|
||||
if re.search(regex, normalized_text, re.IGNORECASE):
|
||||
# Проверяем, не входит ли в исключения
|
||||
is_exception = False
|
||||
for exception in exceptions:
|
||||
if exception in normalized_text and pattern in exception:
|
||||
is_exception = True
|
||||
break
|
||||
|
||||
if not is_exception:
|
||||
return True
|
||||
|
||||
# 2. Проверяем contains паттерны (любое вхождение)
|
||||
for pattern in contains_patterns:
|
||||
if pattern in normalized_text:
|
||||
# Проверяем все вхождения паттерна
|
||||
start = 0
|
||||
while True:
|
||||
pos = normalized_text.find(pattern, start)
|
||||
if pos == -1:
|
||||
break
|
||||
|
||||
# Проверяем, не входит ли в исключение
|
||||
is_exception = False
|
||||
for exception in exceptions:
|
||||
if pattern in exception:
|
||||
exc_start = normalized_text.find(exception, max(0, pos - len(exception)))
|
||||
if exc_start != -1:
|
||||
exc_end = exc_start + len(exception)
|
||||
if exc_start <= pos < exc_end:
|
||||
is_exception = True
|
||||
break
|
||||
|
||||
if not is_exception:
|
||||
return True
|
||||
|
||||
start = pos + 1
|
||||
|
||||
return False
|
||||
|
||||
def get_bad_words_from_text(text: str) -> list:
|
||||
"""
|
||||
Возвращает список найденных бранных слов в тексте.
|
||||
|
||||
Использует:
|
||||
- Нормализацию текста для обхода обфускации
|
||||
- Проверку границ слов для whole_word паттернов
|
||||
- Проверку подстрок для contains паттернов
|
||||
- Список исключений
|
||||
|
||||
Args:
|
||||
text: Текст для проверки
|
||||
|
||||
Returns:
|
||||
Список найденных бранных слов
|
||||
"""
|
||||
if not text:
|
||||
return []
|
||||
|
||||
# Нормализуем текст (обход обфускации)
|
||||
normalized_text = normalize_text(text)
|
||||
found_words = []
|
||||
|
||||
# Получаем паттерны и исключения
|
||||
global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
|
||||
|
||||
# Если кэш не загружен, загружаем
|
||||
if _whole_word_patterns_cache is None:
|
||||
load_bad_words()
|
||||
|
||||
whole_word_patterns = _whole_word_patterns_cache or []
|
||||
contains_patterns = _contains_patterns_cache or []
|
||||
exceptions = _exceptions_cache or []
|
||||
|
||||
# 1. Проверяем whole_word паттерны (только целые слова)
|
||||
for pattern in whole_word_patterns:
|
||||
# Используем границы слов \b для поиска только целых слов
|
||||
regex = r'\b' + re.escape(pattern) + r'\b'
|
||||
if re.search(regex, normalized_text, re.IGNORECASE):
|
||||
# Проверяем, не входит ли в исключения
|
||||
is_exception = False
|
||||
for exception in exceptions:
|
||||
if exception in normalized_text and pattern in exception:
|
||||
is_exception = True
|
||||
break
|
||||
|
||||
if not is_exception and pattern not in found_words:
|
||||
found_words.append(pattern)
|
||||
|
||||
# 2. Проверяем contains паттерны (любое вхождение)
|
||||
for pattern in contains_patterns:
|
||||
if pattern in normalized_text:
|
||||
# Проверяем все вхождения паттерна
|
||||
start = 0
|
||||
word_is_valid = False
|
||||
while True:
|
||||
pos = normalized_text.find(pattern, start)
|
||||
if pos == -1:
|
||||
break
|
||||
|
||||
# Проверяем, не входит ли в исключение
|
||||
is_exception = False
|
||||
for exception in exceptions:
|
||||
if pattern in exception:
|
||||
exc_start = normalized_text.find(exception, max(0, pos - len(exception)))
|
||||
if exc_start != -1:
|
||||
exc_end = exc_start + len(exception)
|
||||
if exc_start <= pos < exc_end:
|
||||
is_exception = True
|
||||
break
|
||||
|
||||
if not is_exception:
|
||||
word_is_valid = True
|
||||
break
|
||||
|
||||
start = pos + 1
|
||||
|
||||
# Добавляем слово только если оно действительно найдено (не в исключении)
|
||||
if word_is_valid and pattern not in found_words:
|
||||
found_words.append(pattern)
|
||||
|
||||
return found_words
|
142
src/config.py
142
src/config.py
@@ -7,19 +7,46 @@ 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
|
||||
|
||||
# Сообщения команд
|
||||
COMMAND_MESSAGES = {
|
||||
'start': 'Бот-администратор для чата @linux_gaming_ru',
|
||||
'log': (
|
||||
"<b>📋 Как создать лог ошибки</b>\n\n"
|
||||
"Подробная инструкция по созданию логов для диагностики проблем:\n\n"
|
||||
"🔗 <a href='https://linux-gaming.ru/t/kak-sozdat-log-oshibki/25'>Перейти к инструкции</a>"
|
||||
),
|
||||
'help': (
|
||||
"<b>📚 Справочник команд администратора</b>\n\n"
|
||||
"<u>Основные команды:</u>\n"
|
||||
"• <code>/start</code> - Начало работы\n"
|
||||
"• <code>/help</code> - Этот справочник\n\n"
|
||||
"• <code>/help</code> - Этот справочник\n"
|
||||
"• <code>/log</code> - Инструкция по созданию логов\n\n"
|
||||
"<u>🛠 Команды модерации:</u>\n"
|
||||
"• <code>/warn help</code> - Выдать предупреждение\n"
|
||||
"• <code>/mute help</code> - Инструкция по муту\n"
|
||||
"• <code>/unmute help</code> - Снятие мута\n"
|
||||
"• <code>/ban help</code> - Инструкция по бану\n"
|
||||
"• <code>/unban help</code> - Снятие бана\n\n"
|
||||
"<u>⭐ Система кармы:</u>\n"
|
||||
"• <code>/karma</code> - Просмотр кармы\n"
|
||||
"• <code>/top</code> - Топ пользователей по карме\n\n"
|
||||
"<i>ℹ️ Для подробностей по конкретной команде используйте: /команда help</i>"
|
||||
),
|
||||
'manual_mute': (
|
||||
@@ -90,6 +117,117 @@ COMMAND_MESSAGES = {
|
||||
'banned': '✅ Пользователь успешно забанен.',
|
||||
'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>"
|
||||
),
|
||||
'reset_violations_help': (
|
||||
"<b>🔄 Команда /reset_violations</b>\n\n"
|
||||
"<i>Сбрасывает счётчик нарушений пользователя</i>\n\n"
|
||||
"<u>🎯 Способы использования:</u>\n"
|
||||
"1. Ответ на сообщение:\n"
|
||||
" <code>/reset_violations</code>\n"
|
||||
"2. По тегу пользователя:\n"
|
||||
" <code>/reset_violations @username</code>\n"
|
||||
"3. По ID пользователя:\n"
|
||||
" <code>/reset_violations 123456789</code>\n\n"
|
||||
"<i>ℹ️ Сбрасывает все записи об автомутах пользователя</i>"
|
||||
),
|
||||
'manual_warn': (
|
||||
"<b>⚠️ Команда /warn</b>\n\n"
|
||||
"<i>Выдает официальное предупреждение пользователю</i>\n\n"
|
||||
"<u>🎯 Способы использования:</u>\n"
|
||||
"1. Ответ на сообщение:\n"
|
||||
" <code>/warn причина</code>\n"
|
||||
"2. По тегу пользователя:\n"
|
||||
" <code>/warn @username причина</code>\n"
|
||||
"3. По ID пользователя:\n"
|
||||
" <code>/warn 123456789 причина</code>\n\n"
|
||||
"<b>📋 Система накопления:</b>\n"
|
||||
"• 1-й варн: просто предупреждение\n"
|
||||
"• 2-й варн за неделю: автомут на 7 дней (строгое)\n"
|
||||
"• 2-й варн за 2 недели: автомут на 1 день (мягкое)\n\n"
|
||||
"<i>ℹ️ Причину обязательно указывайте для прозрачности</i>"
|
||||
),
|
||||
'warned': '⚠️ Пользователь получил предупреждение.',
|
||||
'warned_auto_mute_day': '⚠️ Пользователь получил предупреждение и автомут на 1 день (2-е предупреждение за 2 недели).',
|
||||
'warned_auto_mute_week': '⚠️ Пользователь получил предупреждение и автомут на 7 дней (2-е предупреждение за неделю - строгое наказание).',
|
||||
'karma_help': (
|
||||
"<b>⭐ Команда /karma</b>\n\n"
|
||||
"<i>Показывает карму пользователя в этом чате</i>\n\n"
|
||||
"<u>🎯 Способы использования:</u>\n"
|
||||
"1. Показать свою карму:\n"
|
||||
" <code>/karma</code>\n"
|
||||
"2. По тегу пользователя:\n"
|
||||
" <code>/karma @username</code>\n"
|
||||
"3. Ответ на сообщение:\n"
|
||||
" Ответьте на сообщение: <code>/karma</code>\n\n"
|
||||
"<b>💡 Как начислить карму?</b>\n"
|
||||
"<u>Способ 1: Ответить на сообщение</u>\n"
|
||||
"• спасибо → +1 карма\n"
|
||||
"• благодарю → +1 карма\n"
|
||||
"• спс, сенкс, thanks и др. → +1 карма\n\n"
|
||||
"<u>Способ 2: Поставить реакцию (работает как переключатель)</u>\n"
|
||||
"• Поставил 👍 → +1 карма | Убрал 👍 → -1 карма\n"
|
||||
"• Поставил 👎 → -1 карма | Убрал 👎 → +1 карма\n"
|
||||
"• Поставил 🔥 → +2 кармы | Убрал 🔥 → -2 кармы\n"
|
||||
"• Поставил ❤ → +5 кармы | Убрал ❤ → -5 кармы\n"
|
||||
"• Поставил ❤🔥 → +10 кармы | Убрал ❤🔥 → -10 кармы\n"
|
||||
"• Нет ограничений по времени для реакций!\n\n"
|
||||
"<b>🔥 БОНУС: Благодарность с восклицательным знаком даёт x2 кармы!</b>\n"
|
||||
"• спасибо! → +2 кармы 👍👍\n"
|
||||
"• thanks! → +2 кармы 👍👍\n\n"
|
||||
"<b>⚠️ Снятие кармы:</b>\n"
|
||||
"• Предупреждение (/warn): -5 кармы\n"
|
||||
"• Мут (/mute или автомут): -10 кармы\n\n"
|
||||
"<i>⏱ Одному пользователю можно давать карму раз в час</i>"
|
||||
),
|
||||
'top_karma_help': (
|
||||
"<b>🏆 Команда /top</b>\n\n"
|
||||
"<i>Показывает топ-10 пользователей по карме в этом чате</i>\n\n"
|
||||
"<u>🎯 Использование:</u>\n"
|
||||
" <code>/top</code>\n\n"
|
||||
"<i>💡 Система кармы поощряет активных и полезных участников чата!</i>"
|
||||
),
|
||||
'setkarma_help': (
|
||||
"<b>🎚 Команда /setkarma</b>\n\n"
|
||||
"<i>Устанавливает карму пользователя в указанное значение (только для администраторов)</i>\n\n"
|
||||
"<u>🎯 Способы использования:</u>\n"
|
||||
"1. Ответ на сообщение:\n"
|
||||
" <code>/setkarma 100</code>\n"
|
||||
"2. По тегу пользователя:\n"
|
||||
" <code>/setkarma @username 50</code>\n"
|
||||
"3. По ID пользователя:\n"
|
||||
" <code>/setkarma 123456789 -10</code>\n\n"
|
||||
"<b>💡 Примеры:</b>\n"
|
||||
"• Установить карму на 0: <code>/setkarma @user 0</code>\n"
|
||||
"• Установить отрицательную карму: <code>/setkarma @user -50</code>\n"
|
||||
"• Установить высокую карму: <code>/setkarma @user 1000</code>\n\n"
|
||||
"<i>⚠️ Команда доступна только администраторам с правами ограничения</i>"
|
||||
)
|
||||
|
||||
}
|
96
src/data/bad_words.json
Normal file
96
src/data/bad_words.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"patterns": {
|
||||
"whole_word": [
|
||||
"гей",
|
||||
"гомик",
|
||||
"гомос",
|
||||
"даун",
|
||||
"дебил",
|
||||
"гандон",
|
||||
"мразь",
|
||||
"мраз",
|
||||
"козел",
|
||||
"козл",
|
||||
"урод",
|
||||
"урода",
|
||||
"тварь",
|
||||
"твар",
|
||||
"падла",
|
||||
"гнида",
|
||||
"гнид"
|
||||
],
|
||||
"contains": [
|
||||
"хуй",
|
||||
"хуе",
|
||||
"хуи",
|
||||
"хую",
|
||||
"хуя",
|
||||
"хер",
|
||||
"пизд",
|
||||
"пизж",
|
||||
"пезд",
|
||||
"ебал",
|
||||
"ебан",
|
||||
"ебат",
|
||||
"ебу",
|
||||
"ебош",
|
||||
"ебля",
|
||||
"ебет",
|
||||
"бля",
|
||||
"блядь",
|
||||
"блять",
|
||||
"сука",
|
||||
"суки",
|
||||
"сучк",
|
||||
"сучар",
|
||||
"мудак",
|
||||
"мудил",
|
||||
"муди",
|
||||
"долбоеб",
|
||||
"долбаеб",
|
||||
"уебан",
|
||||
"уебок",
|
||||
"хуесос",
|
||||
"пидор",
|
||||
"пидар",
|
||||
"педик",
|
||||
"педр",
|
||||
"шлюх",
|
||||
"шалав",
|
||||
"еблан",
|
||||
"говн",
|
||||
"срать",
|
||||
"сраль",
|
||||
"серун",
|
||||
"дрочи",
|
||||
"дроч",
|
||||
"жоп",
|
||||
"жёп",
|
||||
"залуп",
|
||||
"ублюдо",
|
||||
"ублюд",
|
||||
"сволочь",
|
||||
"сволоч",
|
||||
"выблядо",
|
||||
"хуета",
|
||||
"хуйн",
|
||||
"охуе",
|
||||
"охуи",
|
||||
"охуя",
|
||||
"нахуй",
|
||||
"нахер",
|
||||
"похуй",
|
||||
"похер",
|
||||
"захуя",
|
||||
"ахуе",
|
||||
"впизду",
|
||||
"попизд"
|
||||
]
|
||||
},
|
||||
"exceptions": [
|
||||
"республика",
|
||||
"документ",
|
||||
"документы"
|
||||
],
|
||||
"_comment": "whole_word - только целые слова (не часть другого слова), contains - любое вхождение подстроки"
|
||||
}
|
25
src/data/thank_words.json
Normal file
25
src/data/thank_words.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"thank_words": [
|
||||
"спасибо",
|
||||
"благодарю",
|
||||
"спс",
|
||||
"сенкс",
|
||||
"сенкью",
|
||||
"thanks",
|
||||
"thank you",
|
||||
"thx",
|
||||
"ty",
|
||||
"дякую",
|
||||
"дзякуй",
|
||||
"рахмет",
|
||||
"пасиб",
|
||||
"пасибо",
|
||||
"спасибочки",
|
||||
"благодарочка",
|
||||
"мерси",
|
||||
"merci",
|
||||
"danke",
|
||||
"gracias",
|
||||
"grazie"
|
||||
]
|
||||
}
|
388
src/database.py
388
src/database.py
@@ -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,93 @@ 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)
|
||||
''')
|
||||
|
||||
connect.commit()
|
||||
logger.info("База данных и индексы успешно инициализированы")
|
||||
|
||||
# Возвращает соединение с базой данных
|
||||
def _get_connection(self):
|
||||
@@ -70,15 +157,308 @@ 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
|
||||
|
||||
# Создаем экземпляр базы данных для импорта в других модулях
|
||||
db = Database()
|
0
src/lgbot.db
Normal file
0
src/lgbot.db
Normal 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():
|
||||
|
126
src/main.py
126
src/main.py
@@ -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.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,33 @@ 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("warn", "Выдать предупреждение. Использование: /warn help"),
|
||||
BotCommand("ban", "Забанить пользователя. Использование: /ban help"),
|
||||
BotCommand("unban", "Разбанить пользователя. Использование: /unban help"),
|
||||
BotCommand("mute", "Замутить пользователя. Использование: /mute help"),
|
||||
BotCommand("unmute", "Размутить пользователя. Использование: /unmute help"),
|
||||
BotCommand("badwords", "Управление списком бранных слов. /badwords help"),
|
||||
BotCommand("reset_violations", "Сбросить счётчик нарушений пользователя"),
|
||||
BotCommand("botdata", "Получить данные бота (только для админов)"),
|
||||
BotCommand("karma", "Просмотр кармы пользователя"),
|
||||
BotCommand("top", "Топ-10 пользователей по карме"),
|
||||
BotCommand("setkarma", "Установить карму пользователя. Использование: /setkarma help"),
|
||||
]
|
||||
|
||||
await bot.set_my_commands(commands)
|
||||
logger.info("Команды бота успешно установлены.")
|
||||
|
||||
async def main():
|
||||
# Инициализация логирования (должна быть первой)
|
||||
setup_logging()
|
||||
|
||||
# Очищаем терминал
|
||||
os.system('clear')
|
||||
@@ -119,8 +215,12 @@ async def main():
|
||||
# Проверяем и загружаем модули
|
||||
await load_modules()
|
||||
|
||||
# Запускаем бота
|
||||
await bot.infinity_polling()
|
||||
# Устанавливаем команды бота
|
||||
await setup_bot_commands()
|
||||
|
||||
# Запускаем бота с обработкой реакций
|
||||
logger.info("Запуск бота с allowed_updates: message, message_reaction, chat_member")
|
||||
await bot.infinity_polling(allowed_updates=['message', 'message_reaction', 'chat_member'])
|
||||
|
||||
except Exception as e:
|
||||
|
||||
|
332
src/modules/0_karma_tracker.py
Normal file
332
src/modules/0_karma_tracker.py
Normal file
@@ -0,0 +1,332 @@
|
||||
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 кармы
|
||||
"""
|
||||
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 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
|
||||
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
|
||||
|
||||
# Проверяем новые реакции
|
||||
new_thumbs_up = False
|
||||
new_thumbs_down = False
|
||||
new_heart = False
|
||||
new_fire_heart = False
|
||||
new_fire = 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
|
||||
|
||||
# Определяем изменение кармы (накапливаем все изменения)
|
||||
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)")
|
||||
|
||||
# Если нет изменений - выходим
|
||||
if karma_change == 0:
|
||||
logger.info(f"[KARMA] Нет изменений в реакциях")
|
||||
return
|
||||
|
||||
# Формируем текст действий для логирования
|
||||
action_text = ", ".join(actions)
|
||||
logger.info(f"[KARMA] {action_text} от {from_user.id} для {to_user_id}, итоговое изменение кармы: {karma_change}")
|
||||
|
||||
# Изменяем карму
|
||||
db.add_karma(to_user_id, chat_id, karma_change)
|
||||
|
||||
# Получаем новую карму
|
||||
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
|
||||
|
||||
# ВАЖНО: В топиках каждое сообщение технически является 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)
|
217
src/modules/auto_mute.py
Normal file
217
src/modules/auto_mute.py
Normal file
@@ -0,0 +1,217 @@
|
||||
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
|
||||
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,
|
||||
)
|
||||
|
||||
# Формируем причину с полным текстом сообщения
|
||||
reason_text = f"{words_info} (нарушение #{violations_count})\n\n📝 <b>Текст сообщения:</b>\n<code>{message.text}</code>"
|
||||
|
||||
# Отправляем сообщение-лог в админ-чат
|
||||
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)")
|
237
src/modules/badwords_manager.py
Normal file
237
src/modules/badwords_manager.py
Normal file
@@ -0,0 +1,237 @@
|
||||
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):
|
||||
"""Главная команда управления списком бранных слов"""
|
||||
|
||||
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}")
|
||||
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)
|
@@ -62,7 +62,7 @@ async def ban_command(bot: AsyncTeleBot, message: Message, photo_path: str = Non
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
if not message.reply_to_message or message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
@@ -76,7 +76,7 @@ async def ban_command(bot: AsyncTeleBot, message: Message, photo_path: str = Non
|
||||
reason = 'отсутствует'
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
elif message.reply_to_message and not message.is_topic_message:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
@@ -141,7 +141,7 @@ async def ban_command(bot: AsyncTeleBot, message: Message, photo_path: str = Non
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
if not message.reply_to_message or message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
@@ -155,7 +155,7 @@ async def ban_command(bot: AsyncTeleBot, message: Message, photo_path: str = Non
|
||||
reason = ' '.join(parts_msg[1:])
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
elif message.reply_to_message and not message.is_topic_message:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
|
@@ -4,16 +4,13 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from utils import delete_messages
|
||||
from utils import delete_messages, check_admin_status
|
||||
|
||||
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):
|
||||
|
||||
@@ -23,36 +20,28 @@ def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
try:
|
||||
|
||||
# Если id администратора совпадает
|
||||
if message.from_user.id in ADMIN_IDS:
|
||||
# Проверяем права администратора (без проверки прав на ограничение участников)
|
||||
admin_check = await check_admin_status(bot, message, check_restrict_rights=False)
|
||||
if admin_check == 1:
|
||||
logger.info(f"Пользователь {message.from_user.id} не является администратором")
|
||||
return
|
||||
|
||||
# Отправляем базу данных
|
||||
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')
|
||||
)
|
||||
# Отправляем базу данных
|
||||
await bot.send_document(
|
||||
chat_id=message.chat.id,
|
||||
document=open(DATABASE_NAME, 'rb')
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Администратор {message.from_user.id} запустил /botdata.")
|
||||
# Отправляем файл с логами
|
||||
await bot.send_document(
|
||||
chat_id=message.chat.id,
|
||||
document=open(LOG_FILE_NAME, 'rb')
|
||||
)
|
||||
|
||||
# Если 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.")
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Администратор {message.from_user.id} запустил /botdata.")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
|
155
src/modules/karma_commands.py
Normal file
155
src/modules/karma_commands.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from database import db
|
||||
|
||||
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.error(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 (в ответ на сообщение) - показать карму автора сообщения
|
||||
"""
|
||||
try:
|
||||
# Проверяем, что это групповой чат
|
||||
if message.chat.type not in ['group', 'supergroup']:
|
||||
await bot.reply_to(message, "❌ Эта команда работает только в групповых чатах")
|
||||
return
|
||||
|
||||
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(message.text.split()) > 1:
|
||||
username_arg = message.text.split()[1]
|
||||
# Убираем @ если есть
|
||||
username = username_arg.lstrip('@')
|
||||
|
||||
# Ищем пользователя в БД
|
||||
user_data = db.get_user_by_username(username)
|
||||
if user_data:
|
||||
target_user_id = user_data[0]
|
||||
logger.info(f"[KARMA CMD] Найден пользователь по username '{username}': id={user_data[0]}, nickname={user_data[1]}, tag={user_data[2]}")
|
||||
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
|
||||
|
||||
logger.info(f"[KARMA CMD] Показываем карму: user_id={target_user_id}, username={getattr(target_user, 'username', None)}, display={user_display}, karma={karma}")
|
||||
|
||||
# Определяем эмодзи в зависимости от кармы
|
||||
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)
|
||||
|
||||
# Удаляем команду через 20 секунд и ответ через 60 секунд
|
||||
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))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке команды /karma: {e}", exc_info=True)
|
||||
await bot.reply_to(message, "❌ Произошла ошибка при получении кармы")
|
||||
|
||||
@bot.message_handler(commands=['top', 'leaderboard', 'topkarma'])
|
||||
async def handle_top_command(message: Message):
|
||||
"""
|
||||
Команда /top - показывает топ пользователей по карме
|
||||
"""
|
||||
try:
|
||||
# Проверяем, что это групповой чат
|
||||
if message.chat.type not in ['group', 'supergroup']:
|
||||
await bot.reply_to(message, "❌ Эта команда работает только в групповых чатах")
|
||||
return
|
||||
|
||||
chat_id = message.chat.id
|
||||
|
||||
# Получаем топ 10 пользователей
|
||||
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):
|
||||
# Определяем медаль для топ-3
|
||||
if idx <= 3:
|
||||
medal = medals[idx - 1]
|
||||
else:
|
||||
medal = f"{idx}."
|
||||
|
||||
# Формируем отображение пользователя
|
||||
if tag:
|
||||
user_display = f"@{tag}"
|
||||
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)
|
||||
|
||||
# Удаляем команду через 20 секунд и ответ через 60 секунд
|
||||
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))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке команды /top: {e}", exc_info=True)
|
||||
await bot.reply_to(message, "❌ Произошла ошибка при получении топа")
|
87
src/modules/log.py
Normal file
87
src/modules/log.py
Normal 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)}")
|
@@ -33,7 +33,7 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
# Определяем целевого пользователя
|
||||
target_user = None
|
||||
|
||||
# Отпределяем время
|
||||
# Определяем время
|
||||
time_arg = None
|
||||
|
||||
# Определяем причину
|
||||
@@ -76,7 +76,7 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
if not message.reply_to_message or message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
@@ -91,7 +91,7 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
reason = 'отсутствует'
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
elif message.reply_to_message and not message.is_topic_message:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
@@ -158,7 +158,7 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
if message.is_topic_message:
|
||||
|
||||
# Если без ответа на сообщение
|
||||
if message.message_thread_id == message.reply_to_message.message_id:
|
||||
if not message.reply_to_message or message.message_thread_id == message.reply_to_message.message_id:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
@@ -173,7 +173,7 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
reason = ' '.join(parts_msg[2:])
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
elif message.reply_to_message and not message.is_topic_message:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
@@ -233,8 +233,11 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
||||
return
|
||||
|
||||
# Максимальное время мута - 30 дней (2592000 секунд)
|
||||
if mute_seconds > 2592000:
|
||||
# Импортируем максимальное время мута
|
||||
from config import MAX_MUTE_TIME
|
||||
|
||||
# Максимальное время мута - 30 дней
|
||||
if mute_seconds > MAX_MUTE_TIME:
|
||||
|
||||
# Отправляем предупреждение
|
||||
await bot.send_message(
|
||||
@@ -276,6 +279,10 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No
|
||||
until_date=until_date
|
||||
)
|
||||
|
||||
# Снимаем карму за мут
|
||||
db.add_karma(target_user.id, message.chat.id, -10)
|
||||
logger.info(f"Снято 10 кармы пользователю {target_user.id} за мут")
|
||||
|
||||
# Форматируем время в удобный формат
|
||||
time_display = format_mute_time(mute_seconds)
|
||||
|
||||
|
127
src/modules/reset_violations.py
Normal file
127
src/modules/reset_violations.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message
|
||||
import logging
|
||||
|
||||
from database import db
|
||||
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=['reset_violations'])
|
||||
async def reset_violations_command(message: Message):
|
||||
"""Команда для сброса счётчика нарушений пользователя"""
|
||||
|
||||
logger.info(f"Команда /reset_violations получена от пользователя {message.from_user.id}")
|
||||
|
||||
# Проверяем права администратора (без проверки can_restrict_members)
|
||||
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} прошел проверку прав администратора")
|
||||
|
||||
# Определяем целевого пользователя
|
||||
target_user = None
|
||||
target_user_id = None
|
||||
|
||||
# Парсим команду
|
||||
parts = message.text.split(maxsplit=1)
|
||||
|
||||
# Если есть аргументы в команде (username или ID) - используем их
|
||||
if len(parts) >= 2:
|
||||
identifier = parts[1].strip()
|
||||
|
||||
# Попытка получить по username
|
||||
if identifier.startswith('@'):
|
||||
username = identifier[1:]
|
||||
logger.info(f"Поиск пользователя по username: {username}")
|
||||
user_data = db.get_user_by_username(username)
|
||||
if user_data:
|
||||
target_user_id = user_data[0]
|
||||
logger.info(f"Найден пользователь в БД: id={user_data[0]}, nickname={user_data[1]}, tag={user_data[2]}")
|
||||
else:
|
||||
logger.warning(f"Пользователь с username {username} не найден в БД")
|
||||
await send_temp_message(
|
||||
bot,
|
||||
message,
|
||||
COMMAND_MESSAGES['user_not_found']
|
||||
)
|
||||
return
|
||||
|
||||
# Попытка получить по ID
|
||||
else:
|
||||
try:
|
||||
target_user_id = int(identifier)
|
||||
logger.info(f"Использован ID пользователя: {target_user_id}")
|
||||
except ValueError:
|
||||
await send_temp_message(
|
||||
bot,
|
||||
message,
|
||||
COMMAND_MESSAGES['user_not_found']
|
||||
)
|
||||
return
|
||||
|
||||
# Если аргументов нет - пробуем использовать reply_to_message
|
||||
elif message.reply_to_message:
|
||||
target_user = message.reply_to_message.from_user
|
||||
target_user_id = target_user.id
|
||||
logger.info(f"Использован пользователь из reply_to_message: id={target_user_id}")
|
||||
|
||||
# Если ни аргументов, ни reply нет - показываем help
|
||||
else:
|
||||
await send_temp_message(
|
||||
bot,
|
||||
message,
|
||||
COMMAND_MESSAGES['reset_violations_help']
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем, что нашли пользователя
|
||||
if not target_user_id:
|
||||
await send_temp_message(
|
||||
bot,
|
||||
message,
|
||||
COMMAND_MESSAGES['user_not_found']
|
||||
)
|
||||
return
|
||||
|
||||
# Получаем информацию о пользователе из базы
|
||||
user_info = db.get_user(target_user_id)
|
||||
|
||||
# Получаем текущее количество нарушений
|
||||
violations_count = db.get_violations_count(target_user_id, message.chat.id)
|
||||
|
||||
# Сбрасываем нарушения
|
||||
deleted_count = db.reset_user_violations(target_user_id, message.chat.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 send_temp_message(bot, message, response, time_sleep=30)
|
||||
logger.info(f"Администратор {message.from_user.id} сбросил счётчик нарушений пользователя {target_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)
|
198
src/modules/setkarma.py
Normal file
198
src/modules/setkarma.py
Normal file
@@ -0,0 +1,198 @@
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
from telebot.types import Message, User
|
||||
import logging
|
||||
|
||||
from database import db
|
||||
from action_reporter import action_reporter
|
||||
from utils import delete_messages, check_admin_status
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Регистрирует обработчик команды
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
# Обработчик команды /setkarma
|
||||
@bot.message_handler(commands=['setkarma'])
|
||||
async def _setkarma_command_wrapper(message: Message):
|
||||
await setkarma_command(bot, message)
|
||||
|
||||
# Основная функция команды /setkarma
|
||||
async def setkarma_command(bot: AsyncTeleBot, message: Message):
|
||||
"""Устанавливает карму пользователя в указанное значение"""
|
||||
|
||||
# Определяем целевого пользователя
|
||||
target_user = None
|
||||
|
||||
# Определяем новое значение кармы
|
||||
new_karma_value = None
|
||||
|
||||
# Разбиваем текст сообщения на части
|
||||
parts_msg = message.text.split()
|
||||
|
||||
# Команда /setkarma 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['setkarma_help'],
|
||||
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
|
||||
|
||||
# Проверяем, что это групповой чат
|
||||
if message.chat.type not in ['group', 'supergroup']:
|
||||
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 len(parts_msg) < 2:
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Команда через ответ на сообщение: /setkarma 100
|
||||
if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id):
|
||||
if len(parts_msg) >= 2:
|
||||
target_user = message.reply_to_message.from_user
|
||||
try:
|
||||
new_karma_value = int(parts_msg[1])
|
||||
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
|
||||
|
||||
# Команда с указанием пользователя: /setkarma @username 100 или /setkarma 123456789 100
|
||||
elif len(parts_msg) >= 3 and (parts_msg[1].strip().isdigit() or parts_msg[1].startswith('@')):
|
||||
identifier = parts_msg[1].strip()
|
||||
try:
|
||||
new_karma_value = int(parts_msg[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
|
||||
|
||||
# Поиск по 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
|
||||
)
|
||||
|
||||
# Если команда неправильная
|
||||
else:
|
||||
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 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,
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(
|
||||
f"Администратор {message.from_user.id} установил карму пользователя {target_user.id} "
|
||||
f"на {new_karma_value} (было {old_karma})"
|
||||
)
|
||||
|
||||
# Удаляем сообщения через 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"Общая ошибка в setkarma_command: {str(e)}")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
@@ -69,7 +69,7 @@ def register_handlers(bot: AsyncTeleBot):
|
||||
target_user = message.reply_to_message.from_user
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
elif message.reply_to_message and not message.is_topic_message:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
|
@@ -69,7 +69,7 @@ def register_handlers(bot: AsyncTeleBot):
|
||||
target_user = message.reply_to_message.from_user
|
||||
|
||||
# Если это General (обычный чат)
|
||||
elif message.reply_to_message and message.is_topic_message is None:
|
||||
elif message.reply_to_message and not message.is_topic_message:
|
||||
|
||||
# Собираем данные
|
||||
target_user = message.reply_to_message.from_user
|
||||
|
248
src/modules/warn.py
Normal file
248
src/modules/warn.py
Normal file
@@ -0,0 +1,248 @@
|
||||
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,
|
||||
)
|
||||
|
||||
from config import COMMAND_MESSAGES
|
||||
|
||||
# Получаем логгер для текущего модуля
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Регистрирует все обработчики команд
|
||||
def register_handlers(bot: AsyncTeleBot):
|
||||
|
||||
# Обработчик команды /warn
|
||||
@bot.message_handler(commands=['warn'])
|
||||
async def _warn_command_wrapper(message: Message):
|
||||
await warn_command(bot, message)
|
||||
|
||||
# Основная функция команды /warn
|
||||
async def warn_command(bot: AsyncTeleBot, message: Message):
|
||||
|
||||
# Определяем целевого пользователя
|
||||
target_user = None
|
||||
|
||||
# Определяем причину
|
||||
reason = None
|
||||
|
||||
# Разбиваем текст сообщения на части
|
||||
parts_msg = message.text.split()
|
||||
|
||||
# Команда /warn 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_warn'],
|
||||
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
|
||||
|
||||
# Если одно слово (/warn)
|
||||
if len(parts_msg) == 1:
|
||||
|
||||
# Удаляем сообщение через 3 секунды
|
||||
await delete_messages(bot, message, time_sleep=3, number_message=1)
|
||||
return
|
||||
|
||||
# Команда через ответ на сообщение, если два или более слов (/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 'отсутствует'
|
||||
|
||||
# Если второе слово это тег или ID
|
||||
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 'отсутствует'
|
||||
|
||||
# Делаем поиск по 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
|
||||
)
|
||||
|
||||
# Если команда неправильная
|
||||
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'],
|
||||
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
|
||||
|
||||
# Добавляем предупреждение в БД
|
||||
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}")
|
||||
|
||||
# Определяем, нужно ли применять мут
|
||||
mute_applied = False
|
||||
mute_duration = 0
|
||||
mute_duration_text = ""
|
||||
response_message = COMMAND_MESSAGES['warned']
|
||||
|
||||
# Если это уже 2+ предупреждение за неделю -> мут на неделю
|
||||
if warns_week >= 2:
|
||||
mute_duration = 604800 # 7 дней
|
||||
mute_duration_text = "7 дней"
|
||||
response_message = COMMAND_MESSAGES['warned_auto_mute_week']
|
||||
mute_applied = True
|
||||
logger.info(f"Применен мут на неделю (предупреждений за неделю: {warns_week})")
|
||||
|
||||
# Если это 2-е предупреждение за 2 недели (но не за неделю) -> мут на сутки
|
||||
elif warns_two_weeks >= 2:
|
||||
mute_duration = 86400 # 1 день
|
||||
mute_duration_text = "1 день"
|
||||
response_message = COMMAND_MESSAGES['warned_auto_mute_day']
|
||||
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,
|
||||
)
|
||||
|
||||
# Отправляем сообщение в чат
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=response_message,
|
||||
message_thread_id=message.message_thread_id,
|
||||
)
|
||||
|
||||
# Записываем действие в логи
|
||||
logger.info(f"Администратор {message.from_user.id} выдал предупреждение пользователю {target_user.id}. Причина: {reason}")
|
||||
|
||||
# Удаляем сообщения через 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"Общая ошибка в warn_command: {str(e)}")
|
||||
|
||||
# Удаляем сообщения через 5 секунд
|
||||
await delete_messages(bot, message, time_sleep=5, number_message=2)
|
109
src/thank_words.py
Normal file
109
src/thank_words.py
Normal 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()
|
140
src/utils.py
140
src/utils.py
@@ -3,33 +3,98 @@ from telebot.types import Message
|
||||
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_task():
|
||||
try:
|
||||
await asyncio.sleep(time_sleep)
|
||||
for i in range(number_message):
|
||||
try:
|
||||
await bot.delete_message(message.chat.id, message.message_id+i)
|
||||
except Exception as e:
|
||||
logger.debug(f"Не удалось удалить сообщение {message.message_id+i}: {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 +102,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 +118,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 +143,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 +193,7 @@ 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 parse_mute_time(time_str: str) -> int | None:
|
||||
|
77
update.sh
Executable file
77
update.sh
Executable 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 "🎉 Обновление завершено!"
|
Reference in New Issue
Block a user