✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
365 lines
14 KiB
Python
365 lines
14 KiB
Python
"""
|
||
Анти-спам — управление чёрным списком ссылок
|
||
Только для администраторов
|
||
"""
|
||
import logging
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message
|
||
from aiogram.filters import Command
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, func
|
||
from database.db import AsyncSessionLocal
|
||
from database.models import User, SpamWarning
|
||
from services.antispam import AntiSpamService
|
||
from utils.decorators import admin_only
|
||
import config
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = Router()
|
||
|
||
|
||
# =============================================================================
|
||
# КОМАНДЫ УПРАВЛЕНИЯ ЧЁРНЫМ СПИСКОМ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('add_domain'))
|
||
@admin_only
|
||
async def cmd_add_domain(message: Message):
|
||
"""
|
||
Добавить домен в чёрный список
|
||
Использование: /add_domain spam.com
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/add_domain spam.com</code>\n\n'
|
||
'Домен будет добавлен в чёрный список.',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
domain = args[1]
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
if antispam.add_domain(domain):
|
||
await message.answer(f'✅ Домен <b>{domain}</b> добавлен в чёрный список', parse_mode='HTML')
|
||
else:
|
||
await message.answer(f'ℹ️ Домен <b>{domain}</b> уже в чёрном списке', parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('remove_domain'))
|
||
@admin_only
|
||
async def cmd_remove_domain(message: Message):
|
||
"""
|
||
Удалить домен из чёрного списка
|
||
Использование: /remove_domain spam.com
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/remove_domain spam.com</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
domain = args[1]
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
if antispam.remove_domain(domain):
|
||
await message.answer(f'✅ Домен <b>{domain}</b> удалён из чёрного списка', parse_mode='HTML')
|
||
else:
|
||
await message.answer(f'ℹ️ Домен <b>{domain}</b> не найден в чёрном списке', parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('add_pattern'))
|
||
@admin_only
|
||
async def cmd_add_pattern(message: Message):
|
||
"""
|
||
Добавить паттерн в чёрный список
|
||
Использование: /add_pattern t.me/spam_
|
||
"""
|
||
args = message.text.split(maxsplit=1)
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/add_pattern t.me/spam_</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
pattern = args[1]
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
if antispam.add_pattern(pattern):
|
||
await message.answer(f'✅ Паттерн <b>{pattern}</b> добавлен в чёрный список', parse_mode='HTML')
|
||
else:
|
||
await message.answer(f'ℹ️ Паттерн <b>{pattern}</b> уже в чёрном списке', parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('remove_pattern'))
|
||
@admin_only
|
||
async def cmd_remove_pattern(message: Message):
|
||
"""
|
||
Удалить паттерн из чёрного списка
|
||
Использование: /remove_pattern t.me/spam_
|
||
"""
|
||
args = message.text.split(maxsplit=1)
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/remove_pattern t.me/spam_</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
pattern = args[1]
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
if antispam.remove_pattern(pattern):
|
||
await message.answer(f'✅ Паттерн <b>{pattern}</b> удалён из чёрного списка', parse_mode='HTML')
|
||
else:
|
||
await message.answer(f'ℹ️ Паттерн <b>{pattern}</b> не найден в чёрном списке', parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('add_keyword'))
|
||
@admin_only
|
||
async def cmd_add_keyword(message: Message):
|
||
"""
|
||
Добавить ключевое слово в чёрный список
|
||
Использование: /add_keyword выигрыш в лотерею
|
||
"""
|
||
args = message.text.split(maxsplit=1)
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/add_keyword выигрыш в лотерею</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
keyword = args[1]
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
if antispam.add_keyword(keyword):
|
||
await message.answer(f'✅ Ключ <b>{keyword}</b> добавлен в чёрный список', parse_mode='HTML')
|
||
else:
|
||
await message.answer(f'ℹ️ Ключ <b>{keyword}</b> уже в чёрном списке', parse_mode='HTML')
|
||
|
||
|
||
@router.message(Command('remove_keyword'))
|
||
@admin_only
|
||
async def cmd_remove_keyword(message: Message):
|
||
"""
|
||
Удалить ключевое слово из чёрного списка
|
||
Использование: /remove_keyword выигрыш в лотерею
|
||
"""
|
||
args = message.text.split(maxsplit=1)
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/remove_keyword выигрыш в лотерею</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
keyword = args[1]
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
if antispam.remove_keyword(keyword):
|
||
await message.answer(f'✅ Ключ <b>{keyword}</b> удалён из чёрного списка', parse_mode='HTML')
|
||
else:
|
||
await message.answer(f'ℹ️ Ключ <b>{keyword}</b> не найден в чёрном списке', parse_mode='HTML')
|
||
|
||
|
||
# =============================================================================
|
||
# ПРОСМОТР ЧЁРНОГО СПИСКА
|
||
# =============================================================================
|
||
|
||
@router.message(Command('domains'))
|
||
@admin_only
|
||
async def cmd_domains(message: Message):
|
||
"""Показать список запрещённых доменов"""
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
domains = antispam.get_all_domains()
|
||
stats = antispam.get_stats()
|
||
|
||
if not domains:
|
||
await message.answer('📋 Чёрный список доменов пуст')
|
||
return
|
||
|
||
text = f'📋 <b>Чёрный список доменов</b> ({len(domains)})\n\n'
|
||
for i, domain in enumerate(domains[:20], 1): # Показываем первые 20
|
||
text += f'{i}. {domain}\n'
|
||
|
||
if len(domains) > 20:
|
||
text += f'\n... и ещё {len(domains) - 20}'
|
||
|
||
await message.answer(text)
|
||
|
||
|
||
@router.message(Command('patterns'))
|
||
@admin_only
|
||
async def cmd_patterns(message: Message):
|
||
"""Показать список запрещённых паттернов"""
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
patterns = antispam.get_all_patterns()
|
||
stats = antispam.get_stats()
|
||
|
||
if not patterns:
|
||
await message.answer('📋 Список запрещённых паттернов пуст')
|
||
return
|
||
|
||
text = f'📋 <b>Запрещённые паттерны</b> ({len(patterns)})\n\n'
|
||
for i, pattern in enumerate(patterns[:20], 1):
|
||
text += f'{i}. {pattern}\n'
|
||
|
||
if len(patterns) > 20:
|
||
text += f'\n... и ещё {len(patterns) - 20}'
|
||
|
||
await message.answer(text)
|
||
|
||
|
||
@router.message(Command('keywords'))
|
||
@admin_only
|
||
async def cmd_keywords(message: Message):
|
||
"""Показать список запрещённых ключевых слов"""
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
keywords = antispam.get_all_keywords()
|
||
|
||
if not keywords:
|
||
await message.answer('📋 Список запрещённых ключей пуст')
|
||
return
|
||
|
||
text = f'📋 <b>Запрещённые ключевые слова</b> ({len(keywords)})\n\n'
|
||
for i, keyword in enumerate(keywords[:20], 1):
|
||
text += f'{i}. {keyword}\n'
|
||
|
||
if len(keywords) > 20:
|
||
text += f'\n... и ещё {len(keywords) - 20}'
|
||
|
||
await message.answer(text)
|
||
|
||
|
||
@router.message(Command('antispam_stats'))
|
||
@admin_only
|
||
async def cmd_antispam_stats(message: Message):
|
||
"""Показать статистику анти-спама"""
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
stats = antispam.get_stats()
|
||
|
||
# Считаем предупреждения
|
||
stmt = select(func.count(SpamWarning.warning_id)).where(SpamWarning.is_active == True)
|
||
result = await session.execute(stmt)
|
||
active_warnings = result.scalar()
|
||
|
||
# Считаем забаненных за спам
|
||
stmt = select(func.count(User.user_id)).where(User.spam_warning_count >= stats['settings'].get('ban_after_warnings', 3))
|
||
result = await session.execute(stmt)
|
||
banned_count = result.scalar()
|
||
|
||
text = (
|
||
f'📊 <b>Статистика анти-спама</b>\n\n'
|
||
f'📁 Чёрный список:\n'
|
||
f'├─ Домены: {stats["domains_count"]}\n'
|
||
f'├─ Паттерны: {stats["patterns_count"]}\n'
|
||
f'└─ Ключевые слова: {stats["keywords_count"]}\n\n'
|
||
f'⚠️ Предупреждения:\n'
|
||
f'├─ Активные: {active_warnings}\n'
|
||
f'└─ Забанено за спам: {banned_count}\n\n'
|
||
f'⚙️ Настройки:\n'
|
||
f'├─ Удалять сообщения: {"✅" if stats["settings"].get("delete_message") else "❌"}\n'
|
||
f'├─ Предупреждать пользователя: {"✅" if stats["settings"].get("warn_user") else "❌"}\n'
|
||
f'├─ Уведомлять админа: {"✅" if stats["settings"].get("notify_admin") else "❌"}\n'
|
||
f'└─ Бан после {stats["settings"].get("ban_after_warnings", 3)} предупреждений'
|
||
)
|
||
|
||
await message.answer(text)
|
||
|
||
|
||
# =============================================================================
|
||
# ПРЕДУПРЕЖДЕНИЯ ПОЛЬЗОВАТЕЛЕЙ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('warnings'))
|
||
@admin_only
|
||
async def cmd_warnings(message: Message):
|
||
"""Показать пользователей с предупреждениями"""
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = (
|
||
select(User)
|
||
.where(User.spam_warning_count > 0)
|
||
.order_by(User.spam_warning_count.desc())
|
||
.limit(20)
|
||
)
|
||
result = await session.execute(stmt)
|
||
users = list(result.scalars().all())
|
||
|
||
if not users:
|
||
await message.answer('✅ Нет пользователей с предупреждениями')
|
||
return
|
||
|
||
text = '⚠️ <b>Пользователи с предупреждениями</b>\n\n'
|
||
for user in users:
|
||
name = user.full_name or f'user{user.user_id}'
|
||
apt = user.apartment or '?'
|
||
text += f'• {name} (кв. {apt}) — {user.spam_warning_count} предупр.\n'
|
||
|
||
text += '\n💡 Для сброса: /clear_warnings <user_id>'
|
||
|
||
await message.answer(text)
|
||
|
||
|
||
@router.message(Command('clear_warnings'))
|
||
@admin_only
|
||
async def cmd_clear_warnings(message: Message):
|
||
"""
|
||
Сбросить предупреждения пользователя
|
||
Использование: /clear_warnings <user_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/clear_warnings 123456789</code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
# Сбрасываем счётчик
|
||
user = await session.get(User, user_id)
|
||
if user:
|
||
user.spam_warning_count = 0
|
||
await session.commit()
|
||
await message.answer(f'✅ Предупреждения пользователя {user_id} сброшены')
|
||
else:
|
||
await message.answer(f'❌ Пользователь {user_id} не найден')
|
||
|
||
|
||
@router.message(Command('reload_antispam'))
|
||
@admin_only
|
||
async def cmd_reload_antispam(message: Message):
|
||
"""Перезагрузить конфигурацию анти-спама из файла"""
|
||
async with AsyncSessionLocal() as session:
|
||
antispam = AntiSpamService(session)
|
||
antispam.reload_config()
|
||
await message.answer('✅ Конфигурация анти-спама перезаряжена')
|