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

270
src/core/chat_services.py Normal file
View File

@@ -0,0 +1,270 @@
"""Сервисы для системы чата"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, or_, update, delete
from sqlalchemy.orm import selectinload
from typing import Optional, List, Dict, Any
from datetime import datetime, timezone
from .models import ChatSettings, BannedUser, ChatMessage, User
class ChatSettingsService:
"""Сервис управления настройками чата"""
@staticmethod
async def get_settings(session: AsyncSession) -> Optional[ChatSettings]:
"""Получить текущие настройки чата"""
result = await session.execute(
select(ChatSettings).where(ChatSettings.id == 1)
)
return result.scalar_one_or_none()
@staticmethod
async def get_or_create_settings(session: AsyncSession) -> ChatSettings:
"""Получить или создать настройки чата"""
settings = await ChatSettingsService.get_settings(session)
if not settings:
settings = ChatSettings(id=1, mode='broadcast', global_ban=False)
session.add(settings)
await session.commit()
await session.refresh(settings)
return settings
@staticmethod
async def set_mode(session: AsyncSession, mode: str) -> ChatSettings:
"""Установить режим работы чата (broadcast/forward)"""
settings = await ChatSettingsService.get_or_create_settings(session)
settings.mode = mode
settings.updated_at = datetime.now(timezone.utc)
await session.commit()
await session.refresh(settings)
return settings
@staticmethod
async def set_forward_chat(session: AsyncSession, chat_id: str) -> ChatSettings:
"""Установить ID группы/канала для пересылки"""
settings = await ChatSettingsService.get_or_create_settings(session)
settings.forward_chat_id = chat_id
settings.updated_at = datetime.now(timezone.utc)
await session.commit()
await session.refresh(settings)
return settings
@staticmethod
async def set_global_ban(session: AsyncSession, enabled: bool) -> ChatSettings:
"""Включить/выключить глобальный бан чата"""
settings = await ChatSettingsService.get_or_create_settings(session)
settings.global_ban = enabled
settings.updated_at = datetime.now(timezone.utc)
await session.commit()
await session.refresh(settings)
return settings
class BanService:
"""Сервис управления банами пользователей"""
@staticmethod
async def is_banned(session: AsyncSession, telegram_id: int) -> bool:
"""Проверить забанен ли пользователь"""
result = await session.execute(
select(BannedUser).where(
and_(
BannedUser.telegram_id == telegram_id,
BannedUser.is_active == True
)
)
)
return result.scalar_one_or_none() is not None
@staticmethod
async def ban_user(
session: AsyncSession,
user_id: int,
telegram_id: int,
banned_by: int,
reason: Optional[str] = None
) -> BannedUser:
"""Забанить пользователя"""
# Проверяем есть ли уже активный бан
existing_ban = await session.execute(
select(BannedUser).where(
and_(
BannedUser.telegram_id == telegram_id,
BannedUser.is_active == True
)
)
)
existing = existing_ban.scalar_one_or_none()
if existing:
# Обновляем причину
existing.reason = reason
existing.banned_at = datetime.now(timezone.utc)
await session.commit()
await session.refresh(existing)
return existing
# Создаем новый бан
ban = BannedUser(
user_id=user_id,
telegram_id=telegram_id,
banned_by=banned_by,
reason=reason
)
session.add(ban)
await session.commit()
await session.refresh(ban)
return ban
@staticmethod
async def unban_user(session: AsyncSession, telegram_id: int) -> bool:
"""Разбанить пользователя"""
result = await session.execute(
update(BannedUser)
.where(
and_(
BannedUser.telegram_id == telegram_id,
BannedUser.is_active == True
)
)
.values(is_active=False)
)
await session.commit()
return result.rowcount > 0
@staticmethod
async def get_banned_users(session: AsyncSession, active_only: bool = True) -> List[BannedUser]:
"""Получить список забаненных пользователей"""
query = select(BannedUser).options(
selectinload(BannedUser.user),
selectinload(BannedUser.admin)
)
if active_only:
query = query.where(BannedUser.is_active == True)
result = await session.execute(query.order_by(BannedUser.banned_at.desc()))
return result.scalars().all()
class ChatMessageService:
"""Сервис работы с сообщениями чата"""
@staticmethod
async def save_message(
session: AsyncSession,
user_id: int,
telegram_message_id: int,
message_type: str,
text: Optional[str] = None,
file_id: Optional[str] = None,
forwarded_ids: Optional[Dict[str, int]] = None
) -> ChatMessage:
"""Сохранить сообщение в историю"""
message = ChatMessage(
user_id=user_id,
telegram_message_id=telegram_message_id,
message_type=message_type,
text=text,
file_id=file_id,
forwarded_message_ids=forwarded_ids
)
session.add(message)
await session.commit()
await session.refresh(message)
return message
@staticmethod
async def get_message(session: AsyncSession, message_id: int) -> Optional[ChatMessage]:
"""Получить сообщение по ID"""
result = await session.execute(
select(ChatMessage)
.options(selectinload(ChatMessage.sender))
.where(ChatMessage.id == message_id)
)
return result.scalar_one_or_none()
@staticmethod
async def get_user_messages(
session: AsyncSession,
user_id: int,
limit: int = 50,
include_deleted: bool = False
) -> List[ChatMessage]:
"""Получить сообщения пользователя"""
query = select(ChatMessage).where(ChatMessage.user_id == user_id)
if not include_deleted:
query = query.where(ChatMessage.is_deleted == False)
query = query.order_by(ChatMessage.created_at.desc()).limit(limit)
result = await session.execute(query)
return result.scalars().all()
@staticmethod
async def delete_message(
session: AsyncSession,
message_id: int,
deleted_by: int
) -> bool:
"""Пометить сообщение как удаленное"""
result = await session.execute(
update(ChatMessage)
.where(ChatMessage.id == message_id)
.values(
is_deleted=True,
deleted_by=deleted_by,
deleted_at=datetime.now(timezone.utc)
)
)
await session.commit()
return result.rowcount > 0
@staticmethod
async def get_recent_messages(
session: AsyncSession,
limit: int = 100,
include_deleted: bool = False
) -> List[ChatMessage]:
"""Получить последние сообщения чата"""
query = select(ChatMessage).options(selectinload(ChatMessage.sender))
if not include_deleted:
query = query.where(ChatMessage.is_deleted == False)
query = query.order_by(ChatMessage.created_at.desc()).limit(limit)
result = await session.execute(query)
return result.scalars().all()
class ChatPermissionService:
"""Сервис проверки прав на отправку сообщений"""
@staticmethod
async def can_send_message(
session: AsyncSession,
telegram_id: int,
is_admin: bool = False
) -> tuple[bool, Optional[str]]:
"""
Проверить может ли пользователь отправлять сообщения
Возвращает (разрешено, причина_отказа)
"""
# Админы всегда могут отправлять
if is_admin:
return True, None
# Проверяем глобальный бан
settings = await ChatSettingsService.get_settings(session)
if settings and settings.global_ban:
return False, "Чат временно закрыт администратором"
# Проверяем личный бан
is_banned = await BanService.is_banned(session, telegram_id)
if is_banned:
return False, "Вы заблокированы и не можете отправлять сообщения"
return True, None

View File

@@ -156,4 +156,63 @@ class Winner(Base):
def __repr__(self):
if self.account_number:
return f"<Winner(lottery_id={self.lottery_id}, account={self.account_number}, place={self.place})>"
return f"<Winner(lottery_id={self.lottery_id}, user_id={self.user_id}, place={self.place})>"
return f"<Winner(lottery_id={self.lottery_id}, user_id={self.user_id}, place={self.place})>"
class ChatSettings(Base):
"""Настройки системы чата"""
__tablename__ = "chat_settings"
id = Column(Integer, primary_key=True)
mode = Column(String(20), nullable=False, default='broadcast') # broadcast или forward
forward_chat_id = Column(String(50), nullable=True) # ID группы/канала для пересылки
global_ban = Column(Boolean, default=False) # Глобальный бан чата
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
def __repr__(self):
return f"<ChatSettings(mode={self.mode}, global_ban={self.global_ban})>"
class BannedUser(Base):
"""Забаненные пользователи (не могут отправлять сообщения)"""
__tablename__ = "banned_users"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
telegram_id = Column(Integer, nullable=False, index=True)
banned_by = Column(Integer, ForeignKey("users.id"), nullable=False)
reason = Column(Text, nullable=True)
banned_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
is_active = Column(Boolean, default=True, index=True) # Активен ли бан
# Связи
user = relationship("User", foreign_keys=[user_id])
admin = relationship("User", foreign_keys=[banned_by])
def __repr__(self):
return f"<BannedUser(telegram_id={self.telegram_id}, is_active={self.is_active})>"
class ChatMessage(Base):
"""История сообщений чата (для модерации)"""
__tablename__ = "chat_messages"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
telegram_message_id = Column(Integer, nullable=False)
message_type = Column(String(20), nullable=False) # text, photo, video, document, animation, sticker, voice, etc.
text = Column(Text, nullable=True) # Текст сообщения
file_id = Column(String(255), nullable=True) # ID файла в Telegram
forwarded_message_ids = Column(JSON, nullable=True) # Список telegram_message_id пересланных сообщений {"user_telegram_id": message_id}
is_deleted = Column(Boolean, default=False, index=True)
deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True)
deleted_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True)
# Связи
sender = relationship("User", foreign_keys=[user_id])
moderator = relationship("User", foreign_keys=[deleted_by])
def __repr__(self):
return f"<ChatMessage(id={self.id}, user_id={self.user_id}, type={self.message_type})>"

View File

@@ -0,0 +1,374 @@
"""Админские обработчики для управления чатом"""
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.filters import Command
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.chat_services import (
ChatSettingsService,
BanService,
ChatMessageService
)
from src.core.services import UserService
from database import get_session
from config import ADMIN_IDS
router = Router(name='admin_chat_router')
def is_admin(user_id: int) -> bool:
"""Проверка является ли пользователь админом"""
return user_id in ADMIN_IDS
def get_chat_mode_keyboard() -> InlineKeyboardMarkup:
"""Клавиатура выбора режима чата"""
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="📢 Рассылка всем", callback_data="chat_mode:broadcast"),
InlineKeyboardButton(text="➡️ Пересылка в канал", callback_data="chat_mode:forward")
],
[InlineKeyboardButton(text="❌ Закрыть", callback_data="close_menu")]
])
@router.message(Command("chat_mode"))
async def cmd_chat_mode(message: Message):
"""Команда управления режимом чата"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
async for session in get_session():
settings = await ChatSettingsService.get_or_create_settings(session)
mode_text = "📢 Рассылка всем пользователям" if settings.mode == 'broadcast' else "➡️ Пересылка в канал"
await message.answer(
f"🎛 <b>Управление режимом чата</b>\n\n"
f"Текущий режим: {mode_text}\n\n"
f"Выберите режим работы:",
reply_markup=get_chat_mode_keyboard(),
parse_mode="HTML"
)
@router.callback_query(F.data.startswith("chat_mode:"))
async def process_chat_mode(callback: CallbackQuery):
"""Обработка выбора режима чата"""
if not is_admin(callback.from_user.id):
await callback.answer("У вас нет прав", show_alert=True)
return
mode = callback.data.split(":")[1]
async for session in get_session():
settings = await ChatSettingsService.set_mode(session, mode)
mode_text = "📢 Рассылка всем пользователям" if mode == 'broadcast' else "➡️ Пересылка в канал"
await callback.message.edit_text(
f"✅ Режим чата изменен!\n\n"
f"Новый режим: {mode_text}",
reply_markup=None
)
await callback.answer("✅ Режим изменен")
@router.message(Command("set_forward"))
async def cmd_set_forward(message: Message):
"""Установить ID канала для пересылки"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
args = message.text.split(maxsplit=1)
if len(args) < 2:
await message.answer(
"📝 <b>Использование:</b>\n"
"/set_forward <chat_id>\n\n"
"Пример: /set_forward -1001234567890\n\n"
"💡 Чтобы узнать ID канала/группы:\n"
"1. Добавьте бота в канал/группу\n"
"2. Напишите любое сообщение\n"
"3. Перешлите его боту @userinfobot",
parse_mode="HTML"
)
return
chat_id = args[1].strip()
async for session in get_session():
settings = await ChatSettingsService.set_forward_chat(session, chat_id)
await message.answer(
f"✅ ID канала для пересылки установлен!\n\n"
f"Chat ID: <code>{chat_id}</code>\n\n"
f"Теперь переключитесь в режим пересылки командой /chat_mode",
parse_mode="HTML"
)
@router.message(Command("global_ban"))
async def cmd_global_ban(message: Message):
"""Включить/выключить глобальный бан чата"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
async for session in get_session():
settings = await ChatSettingsService.get_or_create_settings(session)
# Переключаем состояние
new_state = not settings.global_ban
settings = await ChatSettingsService.set_global_ban(session, new_state)
if new_state:
await message.answer(
"🔇 <b>Глобальный бан включен</b>\n\n"
"Теперь только администраторы могут отправлять сообщения в чат",
parse_mode="HTML"
)
else:
await message.answer(
"🔊 <b>Глобальный бан выключен</b>\n\n"
"Все пользователи снова могут отправлять сообщения",
parse_mode="HTML"
)
@router.message(Command("ban"))
async def cmd_ban(message: Message):
"""Забанить пользователя"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
# Проверяем является ли это ответом на сообщение
if message.reply_to_message:
target_user_id = message.reply_to_message.from_user.id
reason = message.text.split(maxsplit=1)[1] if len(message.text.split(maxsplit=1)) > 1 else None
else:
args = message.text.split(maxsplit=2)
if len(args) < 2:
await message.answer(
"📝 <b>Использование:</b>\n\n"
"1. Ответьте на сообщение пользователя: /ban [причина]\n"
"2. Укажите ID: /ban <user_id> [причина]\n\n"
"Пример: /ban 123456789 Спам",
parse_mode="HTML"
)
return
try:
target_user_id = int(args[1])
reason = args[2] if len(args) > 2 else None
except ValueError:
await message.answer("❌ Неверный ID пользователя")
return
async for session in get_session():
# Получаем пользователя
user = await UserService.get_user_by_telegram_id(session, target_user_id)
if not user:
await message.answer("❌ Пользователь не найден в базе")
return
# Получаем админа
admin = await UserService.get_user_by_telegram_id(session, message.from_user.id)
# Баним
ban = await BanService.ban_user(
session,
user_id=user.id,
telegram_id=target_user_id,
banned_by=admin.id,
reason=reason
)
reason_text = f"\n📝 Причина: {reason}" if reason else ""
await message.answer(
f"🚫 <b>Пользователь забанен</b>\n\n"
f"👤 Пользователь: {user.name or 'Неизвестен'}\n"
f"🆔 ID: <code>{target_user_id}</code>"
f"{reason_text}",
parse_mode="HTML"
)
@router.message(Command("unban"))
async def cmd_unban(message: Message):
"""Разбанить пользователя"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
# Проверяем является ли это ответом на сообщение
if message.reply_to_message:
target_user_id = message.reply_to_message.from_user.id
else:
args = message.text.split()
if len(args) < 2:
await message.answer(
"📝 <b>Использование:</b>\n\n"
"1. Ответьте на сообщение пользователя: /unban\n"
"2. Укажите ID: /unban <user_id>\n\n"
"Пример: /unban 123456789",
parse_mode="HTML"
)
return
try:
target_user_id = int(args[1])
except ValueError:
await message.answer("❌ Неверный ID пользователя")
return
async for session in get_session():
# Разбаниваем
success = await BanService.unban_user(session, target_user_id)
if success:
await message.answer(
f"✅ <b>Пользователь разбанен</b>\n\n"
f"🆔 ID: <code>{target_user_id}</code>\n\n"
f"Теперь пользователь может отправлять сообщения",
parse_mode="HTML"
)
else:
await message.answer("❌ Пользователь не был забанен")
@router.message(Command("banlist"))
async def cmd_banlist(message: Message):
"""Показать список забаненных пользователей"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
async for session in get_session():
banned_users = await BanService.get_banned_users(session, active_only=True)
if not banned_users:
await message.answer("📋 Список банов пуст")
return
text = "🚫 <b>Забаненные пользователи</b>\n\n"
for ban in banned_users:
user = ban.user
admin = ban.admin
text += f"👤 {user.name or 'Неизвестен'} (<code>{ban.telegram_id}</code>)\n"
text += f"🔨 Забанил: {admin.name if admin else 'Неизвестен'}\n"
if ban.reason:
text += f"📝 Причина: {ban.reason}\n"
text += f"📅 Дата: {ban.banned_at.strftime('%d.%m.%Y %H:%M')}\n"
text += "\n"
await message.answer(text, parse_mode="HTML")
@router.message(Command("delete_msg"))
async def cmd_delete_message(message: Message):
"""Удалить сообщение из чата (пометить как удаленное)"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
if not message.reply_to_message:
await message.answer(
"📝 <b>Использование:</b>\n\n"
"Ответьте на сообщение которое хотите удалить командой /delete_msg",
parse_mode="HTML"
)
return
async for session in get_session():
# Получаем админа
admin = await UserService.get_user_by_telegram_id(session, message.from_user.id)
# Находим сообщение в базе по telegram_message_id
from sqlalchemy import select
from src.core.models import ChatMessage
result = await session.execute(
select(ChatMessage).where(
ChatMessage.telegram_message_id == message.reply_to_message.message_id
)
)
chat_message = result.scalar_one_or_none()
if not chat_message:
await message.answer("❌ Сообщение не найдено в базе данных")
return
# Помечаем как удаленное
success = await ChatMessageService.delete_message(
session,
message_id=chat_message.id,
deleted_by=admin.id
)
if success:
# Пытаемся удалить сообщение у всех пользователей
if chat_message.forwarded_message_ids:
deleted_count = 0
for user_telegram_id, msg_id in chat_message.forwarded_message_ids.items():
try:
await message.bot.delete_message(int(user_telegram_id), msg_id)
deleted_count += 1
except Exception as e:
print(f"Failed to delete message {msg_id} for user {user_telegram_id}: {e}")
await message.answer(
f"✅ <b>Сообщение удалено</b>\n\n"
f"🗑 Удалено у {deleted_count} пользователей",
parse_mode="HTML"
)
else:
await message.answer("✅ Сообщение помечено как удаленное")
else:
await message.answer("Не удалось удалить сообщение")
@router.message(Command("chat_stats"))
async def cmd_chat_stats(message: Message):
"""Статистика чата"""
if not is_admin(message.from_user.id):
await message.answer("У вас нет прав для выполнения этой команды")
return
async for session in get_session():
settings = await ChatSettingsService.get_or_create_settings(session)
banned_users = await BanService.get_banned_users(session, active_only=True)
recent_messages = await ChatMessageService.get_recent_messages(session, limit=100)
mode_text = "📢 Рассылка всем" if settings.mode == 'broadcast' else "➡️ Пересылка в канал"
global_ban_text = "🔇 Включен" if settings.global_ban else "🔊 Выключен"
text = (
f"📊 <b>Статистика чата</b>\n\n"
f"🎛 Режим: {mode_text}\n"
f"🚫 Глобальный бан: {global_ban_text}\n"
f"👥 Забанено пользователей: {len(banned_users)}\n"
f"💬 Сообщений за последнее время: {len(recent_messages)}\n"
)
if settings.mode == 'forward' and settings.forward_chat_id:
text += f"\n➡️ ID канала: <code>{settings.forward_chat_id}</code>"
await message.answer(text, parse_mode="HTML")
@router.callback_query(F.data == "close_menu")
async def close_menu(callback: CallbackQuery):
"""Закрыть меню"""
await callback.message.delete()
await callback.answer()

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("✅ Голосовое сообщение переслано в канал")