✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
322 lines
12 KiB
Python
322 lines
12 KiB
Python
"""
|
||
Анти-спам лимиты — мут/размут пользователей
|
||
Только для администраторов
|
||
"""
|
||
import logging
|
||
from datetime import datetime
|
||
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
|
||
from database.db import AsyncSessionLocal
|
||
from database.models import User
|
||
from services.rate_limiter import RateLimiter
|
||
from utils.decorators import admin_only
|
||
import config
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = Router()
|
||
|
||
|
||
# =============================================================================
|
||
# МУТ/РАЗМУТ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('mute'))
|
||
@admin_only
|
||
async def cmd_mute(message: Message):
|
||
"""
|
||
Замутить пользователя
|
||
Использование: /mute <user_id> [минуты]
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/mute <user_id> [минуты]</code>\n\n'
|
||
'Пример: /mute 123456789 30',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
duration = int(args[2]) if len(args) > 2 else None
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
rate_limiter = RateLimiter(session)
|
||
mute_until = await rate_limiter.mute_user(user_id, duration)
|
||
|
||
if mute_until:
|
||
user = await session.get(User, user_id)
|
||
name = user.full_name if user else f'user{user_id}'
|
||
|
||
await message.answer(
|
||
f'🔇 <b>Пользователь замучен</b>\n\n'
|
||
f'Пользователь: {name}\n'
|
||
f'ID: <code>{user_id}</code>\n'
|
||
f'До: {mute_until.strftime("%d.%m.%Y %H:%M")}\n\n'
|
||
f'Пользователь не сможет писать в чат до указанного времени.',
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
await message.answer(f'❌ Пользователь {user_id} не найден')
|
||
|
||
|
||
@router.message(Command('unmute'))
|
||
@admin_only
|
||
async def cmd_unmute(message: Message):
|
||
"""
|
||
Размутить пользователя
|
||
Использование: /unmute <user_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/unmute <user_id></code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
rate_limiter = RateLimiter(session)
|
||
if await rate_limiter.unmute_user(user_id):
|
||
user = await session.get(User, user_id)
|
||
name = user.full_name if user else f'user{user_id}'
|
||
|
||
await message.answer(
|
||
f'🔊 <b>Пользователь размучен</b>\n\n'
|
||
f'Пользователь: {name}\n'
|
||
f'ID: <code>{user_id}</code>\n\n'
|
||
f'Пользователь снова может писать в чат.',
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
await message.answer(f'❌ Пользователь {user_id} не найден или не был замучен')
|
||
|
||
|
||
@router.message(Command('check_mute'))
|
||
@admin_only
|
||
async def cmd_check_mute(message: Message):
|
||
"""
|
||
Проверить истёк ли мут у пользователя
|
||
Использование: /check_mute <user_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/check_mute <user_id></code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
rate_limiter = RateLimiter(session)
|
||
expired = await rate_limiter.check_mute_expired(user_id)
|
||
user = await session.get(User, user_id)
|
||
|
||
if not user:
|
||
await message.answer(f'❌ Пользователь {user_id} не найден')
|
||
return
|
||
|
||
name = user.full_name if user else f'user{user_id}'
|
||
|
||
if user.is_muted and user.mute_until:
|
||
if expired:
|
||
await message.answer(
|
||
f'✅ <b>Мут истёк</b>\n\n'
|
||
f'Пользователь: {name}\n'
|
||
f'ID: <code>{user_id}</code>\n\n'
|
||
f'Пользователь размучен автоматически.',
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
remaining = user.mute_until - datetime.utcnow()
|
||
minutes = int(remaining.total_seconds() / 60)
|
||
|
||
await message.answer(
|
||
f'⏳ <b>Мут активен</b>\n\n'
|
||
f'Пользователь: {name}\n'
|
||
f'ID: <code>{user_id}</code>\n'
|
||
f'Осталось: {minutes} мин.\n'
|
||
f'До: {user.mute_until.strftime("%d.%m.%Y %H:%M")}',
|
||
parse_mode='HTML'
|
||
)
|
||
else:
|
||
await message.answer(f'ℹ️ Пользователь {name} не замучен')
|
||
|
||
|
||
# =============================================================================
|
||
# СТАТИСТИКА
|
||
# =============================================================================
|
||
|
||
@router.message(Command('muted'))
|
||
@admin_only
|
||
async def cmd_muted(message: Message):
|
||
"""Показать всех замученных пользователей"""
|
||
async with AsyncSessionLocal() as session:
|
||
stmt = (
|
||
select(User)
|
||
.where(User.is_muted == True)
|
||
.order_by(User.mute_until)
|
||
)
|
||
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 '?'
|
||
remaining = user.mute_until - datetime.utcnow() if user.mute_until else None
|
||
minutes = int(remaining.total_seconds() / 60) if remaining and remaining.total_seconds() > 0 else 0
|
||
|
||
if minutes > 0:
|
||
text += f'• {name} (кв. {apt}) — ещё {minutes} мин.\n'
|
||
else:
|
||
text += f'• {name} (кв. {apt}) — мут истёк (нужно проверить)\n'
|
||
|
||
await message.answer(text)
|
||
|
||
|
||
@router.message(Command('rate_stats'))
|
||
@admin_only
|
||
async def cmd_rate_stats(message: Message):
|
||
"""Показать статистику лимитов"""
|
||
async with AsyncSessionLocal() as session:
|
||
rate_limiter = RateLimiter(session)
|
||
stats = rate_limiter.get_stats()
|
||
violators = await rate_limiter.get_all_violators()
|
||
|
||
text = (
|
||
f'📊 <b>Статистика лимитов сообщений</b>\n\n'
|
||
f'⚙️ Настройки:\n'
|
||
f'├─ Максимум сообщений: {stats["max_messages"]}\n'
|
||
f'├─ За период: {stats["period_seconds"]} сек\n'
|
||
f'├─ Длительность мута: {stats["mute_duration_minutes"]} мин\n'
|
||
f'└─ Предупреждений перед мутом: {stats["warnings_before_mute"]}\n\n'
|
||
)
|
||
|
||
if violators:
|
||
text += f'🔥 <b>Нарушители ({len(violators)})</b>\n\n'
|
||
for v in violators[:10]:
|
||
name = v.get('full_name') or v.get('username') or f'user{v["user_id"]}'
|
||
status = '🔇' if v.get('is_muted') else '⚠️'
|
||
text += f'{status} {name} — {v["messages_count"]} сообщ.\n'
|
||
|
||
if len(violators) > 10:
|
||
text += f'\n... и ещё {len(violators) - 10}'
|
||
else:
|
||
text += '✅ Нет активных нарушителей'
|
||
|
||
await message.answer(text)
|
||
|
||
|
||
@router.message(Command('user_rate'))
|
||
@admin_only
|
||
async def cmd_user_rate(message: Message):
|
||
"""
|
||
Проверить статистику пользователя
|
||
Использование: /user_rate <user_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/user_rate <user_id></code>',
|
||
parse_mode='HTML'
|
||
)
|
||
return
|
||
|
||
try:
|
||
user_id = int(args[1])
|
||
except ValueError:
|
||
await message.answer('❌ Неверный ID пользователя')
|
||
return
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
rate_limiter = RateLimiter(session)
|
||
stats = await rate_limiter.get_user_stats(user_id)
|
||
user = await session.get(User, user_id)
|
||
|
||
if not user:
|
||
await message.answer(f'❌ Пользователь {user_id} не найден')
|
||
return
|
||
|
||
name = user.full_name or f'user{user_id}'
|
||
apt = user.apartment or '?'
|
||
|
||
text = (
|
||
f'📊 <b>Статистика пользователя</b>\n\n'
|
||
f'Пользователь: {name}\n'
|
||
f'ID: <code>{user_id}</code>\n'
|
||
f'Квартира: {apt}\n\n'
|
||
f'📈 Активность:\n'
|
||
f'├─ Сообщений за период: {stats["messages_count"]} из {stats["max_messages"]}\n'
|
||
f'├─ Предупреждений: {stats["rate_limit_warnings"]}\n'
|
||
f'└─ Статус: {"🔇 Замучен" if stats["is_muted"] else "✅ Активен"}\n'
|
||
)
|
||
|
||
if stats["is_muted"] and stats.get("mute_until"):
|
||
text += f'└─ До: {stats["mute_until"]}'
|
||
|
||
await message.answer(text, parse_mode='HTML')
|
||
|
||
|
||
# =============================================================================
|
||
# СБРОС ПРЕДУПРЕЖДЕНИЙ
|
||
# =============================================================================
|
||
|
||
@router.message(Command('clear_rate_warnings'))
|
||
@admin_only
|
||
async def cmd_clear_rate_warnings(message: Message):
|
||
"""
|
||
Сбросить предупреждения о лимитах
|
||
Использование: /clear_rate_warnings <user_id>
|
||
"""
|
||
args = message.text.split()
|
||
if len(args) < 2:
|
||
await message.answer(
|
||
'❌ <b>Использование:</b>\n'
|
||
'<code>/clear_rate_warnings <user_id></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.rate_limit_warnings = 0
|
||
await session.commit()
|
||
await message.answer(f'✅ Предупреждения пользователя {user_id} сброшены')
|
||
else:
|
||
await message.answer(f'❌ Пользователь {user_id} не найден')
|