feat: добавлена система чата с модерацией
Some checks reported errors
continuous-integration/drone/push Build encountered an error

Реализована полнофункциональная система чата с двумя режимами работы:

## Режимы работы:
- Broadcast: рассылка сообщений всем пользователям
- Forward: пересылка сообщений в указанную группу/канал

## Функционал:
- Поддержка всех типов сообщений: text, photo, video, document, animation, sticker, voice
- Система банов: личные баны пользователей и глобальный бан чата
- Модерация: удаление сообщений с отслеживанием в БД
- История сообщений с сохранением ID пересланных сообщений

## Структура БД (миграция 005):
- chat_settings: настройки чата (режим, ID канала, глобальный бан)
- banned_users: история банов с причинами и информацией о модераторе
- chat_messages: история сообщений с типами, файлами и картой доставки (JSONB)

## Сервисы:
- ChatSettingsService: управление настройками чата
- BanService: управление банами пользователей
- ChatMessageService: работа с историей сообщений
- ChatPermissionService: проверка прав на отправку сообщений

## Обработчики:
- chat_handlers.py: обработка сообщений пользователей (7 типов контента)
- admin_chat_handlers.py: админские команды управления чатом

## Админские команды:
- /chat_mode - переключение режима (broadcast/forward)
- /set_forward <chat_id> - установка ID канала для пересылки
- /ban <user_id> [причина] - бан пользователя
- /unban <user_id> - разбан пользователя
- /banlist - список забаненных
- /global_ban - включение/выключение глобального бана
- /delete_msg - удаление сообщения (ответ на сообщение)
- /chat_stats - статистика чата

## Документация:
- docs/CHAT_SYSTEM.md: полное описание системы с примерами использования

Изменено файлов: 7 (2 modified, 5 new)
- main.py: подключены chat_router и admin_chat_router
- src/core/models.py: добавлены модели ChatSettings, BannedUser, ChatMessage
- migrations/versions/005_add_chat_system.py: миграция создания таблиц
- src/core/chat_services.py: сервисный слой для чата (267 строк)
- src/handlers/chat_handlers.py: обработчики сообщений (447 строк)
- src/handlers/admin_chat_handlers.py: админские команды (369 строк)
- docs/CHAT_SYSTEM.md: документация (390 строк)
This commit is contained in:
2025-11-16 14:25:09 +09:00
parent 505d26f0e9
commit b6c27b7b70
7 changed files with 1592 additions and 1 deletions

View File

@@ -0,0 +1,438 @@
"""Обработчики пользовательских сообщений в чате"""
from aiogram import Router, F
from aiogram.types import Message
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.chat_services import (
ChatSettingsService,
ChatPermissionService,
ChatMessageService,
BanService
)
from src.core.services import UserService
from database import get_session
from config import ADMIN_IDS
def is_admin(user_id: int) -> bool:
"""Проверка является ли пользователь админом"""
return user_id in ADMIN_IDS
router = Router(name='chat_router')
async def get_all_active_users(session: AsyncSession):
"""Получить всех активных пользователей для рассылки"""
users = await UserService.get_all_users(session)
return [u for u in users if u.is_active]
async def broadcast_message(message: Message, exclude_user_id: int = None):
"""Разослать сообщение всем пользователям"""
async for session in get_session():
users = await get_all_active_users(session)
forwarded_ids = {}
success_count = 0
fail_count = 0
for user in users:
if exclude_user_id and user.telegram_id == exclude_user_id:
continue
try:
# Копируем сообщение пользователю
sent_msg = await message.copy_to(user.telegram_id)
forwarded_ids[str(user.telegram_id)] = sent_msg.message_id
success_count += 1
except Exception as e:
fail_count += 1
print(f"Failed to send message to {user.telegram_id}: {e}")
return forwarded_ids, success_count, fail_count
async def forward_to_channel(message: Message, channel_id: str):
"""Переслать сообщение в канал/группу"""
try:
# Пересылаем сообщение в канал
sent_msg = await message.forward(channel_id)
return True, sent_msg.message_id
except Exception as e:
print(f"Failed to forward message to channel {channel_id}: {e}")
return False, None
@router.message(F.text)
async def handle_text_message(message: Message):
"""Обработчик текстовых сообщений"""
async for session in get_session():
# Проверяем права на отправку
can_send, reason = await ChatPermissionService.can_send_message(
session,
message.from_user.id,
is_admin=is_admin(message.from_user.id)
)
if not can_send:
await message.answer(f"{reason}")
return
# Получаем настройки чата
settings = await ChatSettingsService.get_or_create_settings(session)
# Получаем пользователя
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
await message.answer("❌ Пользователь не найден")
return
# Обрабатываем в зависимости от режима
if settings.mode == 'broadcast':
# Режим рассылки
forwarded_ids, success, fail = await broadcast_message(message, exclude_user_id=message.from_user.id)
# Сохраняем сообщение в историю
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='text',
text=message.text,
forwarded_ids=forwarded_ids
)
await message.answer(
f"✅ Сообщение разослано!\n"
f"📤 Доставлено: {success}\n"
f"Не доставлено: {fail}"
)
elif settings.mode == 'forward':
# Режим пересылки в канал
if not settings.forward_chat_id:
await message.answer("❌ Канал для пересылки не настроен")
return
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
if success:
# Сохраняем сообщение в историю
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='text',
text=message.text,
forwarded_ids={'channel': channel_msg_id}
)
await message.answer("✅ Сообщение переслано в канал")
else:
await message.answer("Не удалось переслать сообщение")
@router.message(F.photo)
async def handle_photo_message(message: Message):
"""Обработчик фото"""
async for session in get_session():
can_send, reason = await ChatPermissionService.can_send_message(
session,
message.from_user.id
)
if not can_send:
await message.answer(f"{reason}")
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
# Получаем file_id самого большого фото
photo = message.photo[-1]
if settings.mode == 'broadcast':
forwarded_ids, success, fail = await broadcast_message(message, exclude_user_id=message.from_user.id)
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='photo',
text=message.caption,
file_id=photo.file_id,
forwarded_ids=forwarded_ids
)
await message.answer(f"✅ Фото разослано: {success} получателей")
elif settings.mode == 'forward':
if settings.forward_chat_id:
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
if success:
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='photo',
text=message.caption,
file_id=photo.file_id,
forwarded_ids={'channel': channel_msg_id}
)
await message.answer("✅ Фото переслано в канал")
@router.message(F.video)
async def handle_video_message(message: Message):
"""Обработчик видео"""
async for session in get_session():
can_send, reason = await ChatPermissionService.can_send_message(
session,
message.from_user.id
)
if not can_send:
await message.answer(f"{reason}")
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
if settings.mode == 'broadcast':
forwarded_ids, success, fail = await broadcast_message(message, exclude_user_id=message.from_user.id)
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='video',
text=message.caption,
file_id=message.video.file_id,
forwarded_ids=forwarded_ids
)
await message.answer(f"✅ Видео разослано: {success} получателей")
elif settings.mode == 'forward':
if settings.forward_chat_id:
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
if success:
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='video',
text=message.caption,
file_id=message.video.file_id,
forwarded_ids={'channel': channel_msg_id}
)
await message.answer("✅ Видео переслано в канал")
@router.message(F.document)
async def handle_document_message(message: Message):
"""Обработчик документов"""
async for session in get_session():
can_send, reason = await ChatPermissionService.can_send_message(
session,
message.from_user.id
)
if not can_send:
await message.answer(f"{reason}")
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
if settings.mode == 'broadcast':
forwarded_ids, success, fail = await broadcast_message(message, exclude_user_id=message.from_user.id)
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='document',
text=message.caption,
file_id=message.document.file_id,
forwarded_ids=forwarded_ids
)
await message.answer(f"✅ Документ разослан: {success} получателей")
elif settings.mode == 'forward':
if settings.forward_chat_id:
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
if success:
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='document',
text=message.caption,
file_id=message.document.file_id,
forwarded_ids={'channel': channel_msg_id}
)
await message.answer("✅ Документ переслан в канал")
@router.message(F.animation)
async def handle_animation_message(message: Message):
"""Обработчик GIF анимаций"""
async for session in get_session():
can_send, reason = await ChatPermissionService.can_send_message(
session,
message.from_user.id
)
if not can_send:
await message.answer(f"{reason}")
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
if settings.mode == 'broadcast':
forwarded_ids, success, fail = await broadcast_message(message, exclude_user_id=message.from_user.id)
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='animation',
text=message.caption,
file_id=message.animation.file_id,
forwarded_ids=forwarded_ids
)
await message.answer(f"✅ Анимация разослана: {success} получателей")
elif settings.mode == 'forward':
if settings.forward_chat_id:
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
if success:
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='animation',
text=message.caption,
file_id=message.animation.file_id,
forwarded_ids={'channel': channel_msg_id}
)
await message.answer("✅ Анимация переслана в канал")
@router.message(F.sticker)
async def handle_sticker_message(message: Message):
"""Обработчик стикеров"""
async for session in get_session():
can_send, reason = await ChatPermissionService.can_send_message(
session,
message.from_user.id
)
if not can_send:
await message.answer(f"{reason}")
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
if settings.mode == 'broadcast':
forwarded_ids, success, fail = await broadcast_message(message, exclude_user_id=message.from_user.id)
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='sticker',
file_id=message.sticker.file_id,
forwarded_ids=forwarded_ids
)
await message.answer(f"✅ Стикер разослан: {success} получателей")
elif settings.mode == 'forward':
if settings.forward_chat_id:
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
if success:
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='sticker',
file_id=message.sticker.file_id,
forwarded_ids={'channel': channel_msg_id}
)
await message.answer("✅ Стикер переслан в канал")
@router.message(F.voice)
async def handle_voice_message(message: Message):
"""Обработчик голосовых сообщений"""
async for session in get_session():
can_send, reason = await ChatPermissionService.can_send_message(
session,
message.from_user.id
)
if not can_send:
await message.answer(f"{reason}")
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
if settings.mode == 'broadcast':
forwarded_ids, success, fail = await broadcast_message(message, exclude_user_id=message.from_user.id)
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='voice',
file_id=message.voice.file_id,
forwarded_ids=forwarded_ids
)
await message.answer(f"✅ Голосовое сообщение разослано: {success} получателей")
elif settings.mode == 'forward':
if settings.forward_chat_id:
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
if success:
await ChatMessageService.save_message(
session,
user_id=user.id,
telegram_message_id=message.message_id,
message_type='voice',
file_id=message.voice.file_id,
forwarded_ids={'channel': channel_msg_id}
)
await message.answer("✅ Голосовое сообщение переслано в канал")