domovoy_bot/handlers/ratelimit.py
Admin ba4676b0af v1.0 MVP - Инициализация проекта 🏠
 Регистрация и верификация жильцов
 Рейтинг активности (уровни)
 Детект шпионов (Score 0-100)
 Анти-мат система (3 предупреждения → бан)
 Админ-панель с рассылкой
 Учёт квартир (несколько жильцов)
 Телефоны экстренных служб и мастеров
 Еженедельный экспорт JSON
 Прокси (socks5) для обхода РКН

Бекап: backups/versions/v1.0_mvp_2026-02-26/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-27 10:54:16 +00:00

322 lines
12 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Анти-спам лимиты — мут/размут пользователей
Только для администраторов
"""
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 &lt;user_id&gt; [минуты]</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 &lt;user_id&gt;</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 &lt;user_id&gt;</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 &lt;user_id&gt;</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 &lt;user_id&gt;</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} не найден')