chat restore
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-09 20:07:46 +09:00
parent 062b782fb7
commit 4e2c8981c2
8 changed files with 485 additions and 56 deletions

93
MIGRATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,93 @@
# 📋 Итоговый Отчет: Миграция 006 - Исправление Схемы БД
## ✅ Выполненные задачи
### 1. **Создана миграция 006_fix_missing_columns.py**
- ✅ Автоматическое добавление отсутствующих столбцов
- ✅ Идемпотентность (безопасно для повторного выполнения)
- ✅ Поддержка отката (downgrade функция)
- ✅ Проверка существования столбцов перед добавлением
### 2. **Исправленные столбцы:**
**Таблица `participations`:**
-`account_id` (INTEGER) + FK на `accounts(id)`
**Таблица `winners`:**
-`is_notified` (BOOLEAN DEFAULT FALSE)
-`is_claimed` (BOOLEAN DEFAULT FALSE)
-`claimed_at` (TIMESTAMP WITH TIME ZONE)
### 3. **Применение миграции:**
```bash
# До миграции: 005 (add_chat_system)
alembic upgrade head
# После миграции: 006 (fix_missing_columns) ← HEAD
```
### 4. **Проверка результата:**
```sql
-- participations: account_id добавлен ✅
-- winners: is_notified, is_claimed, claimed_at добавлены ✅
```
### 5. **Документация:**
- ✅ Создан `MIGRATION_006_REPORT.md` с подробным описанием
- ✅ Обновлен `README.md` с информацией о миграциях
- ✅ Добавлен список всех миграций проекта
## 🚀 Результат
### ✅ Преимущества:
1. **Автоматизация:** Все изменения БД теперь применяются через `alembic upgrade head`
2. **Безопасность:** Миграция проверяет существование столбцов
3. **Откат:** Возможность отката изменений при необходимости
4. **Документирование:** Все изменения задокументированы
5. **Production-ready:** Готово к развертыванию на production
### ✅ Проверка работоспособности:
```bash
# Бот запускается без ошибок ✅
python main.py
# 2025-11-17 05:37:26,848 - __main__ - INFO - Запуск бота...
# 2025-11-17 05:37:26,848 - __main__ - INFO - Бот запущен
# 2025-11-17 05:37:27,767 - aiogram.dispatcher - INFO - Run polling
```
## 📦 Коммиты в Git:
### 1. **Основной рефакторинг** (commit: `4a74171`)
```
feat: Полный рефакторинг с модульной архитектурой
- Исправлены критические ошибки callback обработки
- Реализована модульная архитектура с применением SOLID принципов
- Добавлена система dependency injection
```
### 2. **Миграция БД** (commit: `0623de5`)
```
feat: Добавлена миграция 006 для исправления схемы БД
- Создана миграция 006_fix_missing_columns.py
- Автоматически добавляет отсутствующие столбцы
- Миграция идемпотентна
```
---
## 🎯 Заключение
**Все изменения в базе данных вынесены в миграцию 006.**
### Для разработчиков:
При развертывании на любом сервере достаточно выполнить:
```bash
alembic upgrade head
```
### Для администраторов:
- Схема БД автоматически приводится к актуальному состоянию
- Нет необходимости в ручных SQL скриптах
- Возможность отката при проблемах
- Полная прослеживаемость изменений
**🎉 Проект полностью готов к production развертыванию!**

View File

@@ -129,12 +129,11 @@ async def main():
dp.include_router(redraw_router) # Повторные розыгрыши
dp.include_router(p2p_chat_router) # P2P чат между пользователями
# 3. Account router ПЕРЕД chat_router (обнаружение счетов для админов)
dp.include_router(account_router) # Пользовательские счета + обнаружение для админов
# 3. Chat router для broadcast (обрабатывает обычные сообщения)
dp.include_router(chat_router) # Пользовательский чат (broadcast всем) - РАНЬШЕ account_router
# 4. Chat router для broadcast (ловит все необработанные сообщения)
# chat_router пропускает сообщения со счетами от админов
dp.include_router(chat_router) # Пользовательский чат (broadcast всем)
# 4. Account router для обнаружения счетов (обрабатывает сообщения со счетами от админов)
dp.include_router(account_router) # Обнаружение счетов для админов - ПОСЛЕ chat_router
# Запускаем polling
try:

View File

@@ -0,0 +1,200 @@
"""
Revision ID: beb47ddbfc33
Revises: 008
Create Date: 2026-02-08 21:21:25.254747
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'beb47ddbfc33'
down_revision = '008'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('accounts', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True)
op.alter_column('accounts', 'is_active',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('true'))
op.drop_index('ix_accounts_owner_id', table_name='accounts')
op.drop_constraint('accounts_owner_id_fkey', 'accounts', type_='foreignkey')
op.create_foreign_key(None, 'accounts', 'users', ['owner_id'], ['id'])
op.alter_column('banned_users', 'banned_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True,
existing_server_default=sa.text('now()'))
op.alter_column('banned_users', 'is_active',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('true'))
op.drop_constraint('banned_users_user_id_fkey', 'banned_users', type_='foreignkey')
op.drop_constraint('banned_users_banned_by_fkey', 'banned_users', type_='foreignkey')
op.create_foreign_key(None, 'banned_users', 'users', ['banned_by'], ['id'])
op.create_foreign_key(None, 'banned_users', 'users', ['user_id'], ['id'])
op.alter_column('chat_messages', 'forwarded_message_ids',
existing_type=postgresql.JSONB(astext_type=sa.Text()),
type_=sa.JSON(),
existing_nullable=True)
op.alter_column('chat_messages', 'is_deleted',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('false'))
op.alter_column('chat_messages', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True,
existing_server_default=sa.text('now()'))
op.drop_index('ix_chat_messages_user_id', table_name='chat_messages')
op.drop_constraint('chat_messages_user_id_fkey', 'chat_messages', type_='foreignkey')
op.drop_constraint('chat_messages_deleted_by_fkey', 'chat_messages', type_='foreignkey')
op.create_foreign_key(None, 'chat_messages', 'users', ['user_id'], ['id'])
op.create_foreign_key(None, 'chat_messages', 'users', ['deleted_by'], ['id'])
op.alter_column('chat_settings', 'global_ban',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('false'))
op.alter_column('chat_settings', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True,
existing_server_default=sa.text('now()'))
op.alter_column('chat_settings', 'updated_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True,
existing_server_default=sa.text('now()'))
op.alter_column('p2p_messages', 'is_read',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('false'))
op.alter_column('p2p_messages', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True)
op.drop_constraint('fk_participations_account_id', 'participations', type_='foreignkey')
op.create_foreign_key(None, 'participations', 'accounts', ['account_id'], ['id'])
op.alter_column('users', 'is_registered',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('false'))
op.drop_index('ix_users_verification_code', table_name='users')
op.create_unique_constraint(None, 'users', ['verification_code'])
op.alter_column('winner_verifications', 'is_verified',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('false'))
op.alter_column('winner_verifications', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True)
op.drop_index('ix_winner_verifications_token', table_name='winner_verifications')
op.drop_index('ix_winner_verifications_winner_id', table_name='winner_verifications')
op.create_unique_constraint(None, 'winner_verifications', ['verification_token'])
op.create_unique_constraint(None, 'winner_verifications', ['winner_id'])
op.drop_constraint('winner_verifications_winner_id_fkey', 'winner_verifications', type_='foreignkey')
op.create_foreign_key(None, 'winner_verifications', 'winners', ['winner_id'], ['id'])
op.alter_column('winners', 'is_notified',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('false'))
op.alter_column('winners', 'is_claimed',
existing_type=sa.BOOLEAN(),
nullable=True,
existing_server_default=sa.text('false'))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('winners', 'is_claimed',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('false'))
op.alter_column('winners', 'is_notified',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('false'))
op.drop_constraint(None, 'winner_verifications', type_='foreignkey')
op.create_foreign_key('winner_verifications_winner_id_fkey', 'winner_verifications', 'winners', ['winner_id'], ['id'], ondelete='CASCADE')
op.drop_constraint(None, 'winner_verifications', type_='unique')
op.drop_constraint(None, 'winner_verifications', type_='unique')
op.create_index('ix_winner_verifications_winner_id', 'winner_verifications', ['winner_id'], unique=True)
op.create_index('ix_winner_verifications_token', 'winner_verifications', ['verification_token'], unique=True)
op.alter_column('winner_verifications', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False)
op.alter_column('winner_verifications', 'is_verified',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('false'))
op.drop_constraint(None, 'users', type_='unique')
op.create_index('ix_users_verification_code', 'users', ['verification_code'], unique=True)
op.alter_column('users', 'is_registered',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('false'))
op.drop_constraint(None, 'participations', type_='foreignkey')
op.create_foreign_key('fk_participations_account_id', 'participations', 'accounts', ['account_id'], ['id'], ondelete='SET NULL')
op.alter_column('p2p_messages', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False)
op.alter_column('p2p_messages', 'is_read',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('false'))
op.alter_column('chat_settings', 'updated_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False,
existing_server_default=sa.text('now()'))
op.alter_column('chat_settings', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False,
existing_server_default=sa.text('now()'))
op.alter_column('chat_settings', 'global_ban',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('false'))
op.drop_constraint(None, 'chat_messages', type_='foreignkey')
op.drop_constraint(None, 'chat_messages', type_='foreignkey')
op.create_foreign_key('chat_messages_deleted_by_fkey', 'chat_messages', 'users', ['deleted_by'], ['id'], ondelete='SET NULL')
op.create_foreign_key('chat_messages_user_id_fkey', 'chat_messages', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.create_index('ix_chat_messages_user_id', 'chat_messages', ['user_id'], unique=False)
op.alter_column('chat_messages', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False,
existing_server_default=sa.text('now()'))
op.alter_column('chat_messages', 'is_deleted',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('false'))
op.alter_column('chat_messages', 'forwarded_message_ids',
existing_type=sa.JSON(),
type_=postgresql.JSONB(astext_type=sa.Text()),
existing_nullable=True)
op.drop_constraint(None, 'banned_users', type_='foreignkey')
op.drop_constraint(None, 'banned_users', type_='foreignkey')
op.create_foreign_key('banned_users_banned_by_fkey', 'banned_users', 'users', ['banned_by'], ['id'], ondelete='SET NULL')
op.create_foreign_key('banned_users_user_id_fkey', 'banned_users', 'users', ['user_id'], ['id'], ondelete='CASCADE')
op.alter_column('banned_users', 'is_active',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('true'))
op.alter_column('banned_users', 'banned_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False,
existing_server_default=sa.text('now()'))
op.drop_constraint(None, 'accounts', type_='foreignkey')
op.create_foreign_key('accounts_owner_id_fkey', 'accounts', 'users', ['owner_id'], ['id'], ondelete='CASCADE')
op.create_index('ix_accounts_owner_id', 'accounts', ['owner_id'], unique=False)
op.alter_column('accounts', 'is_active',
existing_type=sa.BOOLEAN(),
nullable=False,
existing_server_default=sa.text('true'))
op.alter_column('accounts', 'created_at',
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False)
# ### end Alembic commands ###

View File

@@ -11,7 +11,8 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
def get_main_keyboard(self, is_admin: bool = False, is_registered: bool = False):
"""Получить главную клавиатуру"""
buttons = [
[InlineKeyboardButton(text="🎲 Активные розыгрыши", callback_data="active_lotteries")]
[InlineKeyboardButton(text="🎲 Активные розыгрыши", callback_data="active_lotteries")],
[InlineKeyboardButton(text="💬 Войти в чат", callback_data="enter_chat")]
]
# Показываем кнопку регистрации только незарегистрированным пользователям (не админам)

View File

@@ -163,7 +163,13 @@ async def cmd_ban(message: Message):
return
# Получаем админа
admin = await UserService.get_user_by_telegram_id(session, message.from_user.id)
admin = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Баним
ban = await BanService.ban_user(
@@ -271,7 +277,13 @@ async def cmd_delete_message(message: Message):
async with async_session_maker() as session:
# Получаем админа
admin = await UserService.get_user_by_telegram_id(session, message.from_user.id)
admin = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Находим сообщение в базе по telegram_message_id
from sqlalchemy import select

View File

@@ -414,7 +414,13 @@ async def confirm_create_lottery(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
user = await UserService.get_or_create_user(
session,
callback.from_user.id,
username=callback.from_user.username,
first_name=callback.from_user.first_name,
last_name=callback.from_user.last_name
)
lottery = await LotteryService.create_lottery(
session,

View File

@@ -1,6 +1,9 @@
"""Обработчики пользовательских сообщений в чате"""
from aiogram import Router, F
from aiogram.types import Message
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.filters import StateFilter, Command
from sqlalchemy.ext.asyncio import AsyncSession
import asyncio
from typing import List, Dict, Optional, Set
@@ -19,6 +22,11 @@ from src.core.config import ADMIN_IDS
from src.utils.account_utils import parse_accounts_from_message
class ChatStates(StatesGroup):
"""Состояния для работы в чате"""
in_chat = State() # Пользователь находится в режиме чата
def is_admin(user_id: int) -> bool:
"""Проверка является ли пользователь админом"""
return user_id in ADMIN_IDS
@@ -34,6 +42,69 @@ def _contains_account_numbers(text: str) -> bool:
router = Router(name='chat_router')
@router.message(Command("chat"))
async def enter_chat_command(message: Message, state: FSMContext):
"""Войти в режим чата через команду /chat"""
await enter_chat(message, state)
@router.callback_query(F.data == "enter_chat")
async def enter_chat_callback(callback: CallbackQuery, state: FSMContext):
"""Войти в режим чата через кнопку"""
await callback.answer()
await enter_chat(callback.message, state)
async def enter_chat(message: Message, state: FSMContext):
"""Общая функция входа в чат"""
await state.set_state(ChatStates.in_chat)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🚪 Выйти из чата", callback_data="exit_chat")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")]
])
await message.answer(
"💬 <b>Вы вошли в режим чата</b>\n\n"
"Теперь все ваши сообщения будут рассылаться участникам.\n"
"Вы можете отправлять текст, фото, видео, документы и стикеры.\n\n"
"Для выхода нажмите кнопку ниже или отправьте /exit",
reply_markup=keyboard,
parse_mode="HTML"
)
@router.message(Command("exit"), StateFilter(ChatStates.in_chat))
async def exit_chat_command(message: Message, state: FSMContext):
"""Выйти из режима чата через команду /exit"""
await exit_chat(message, state)
@router.callback_query(F.data == "exit_chat", StateFilter(ChatStates.in_chat))
async def exit_chat_callback(callback: CallbackQuery, state: FSMContext):
"""Выйти из режима чата через кнопку"""
await callback.answer()
await exit_chat(callback.message, state)
async def exit_chat(message: Message, state: FSMContext):
"""Общая функция выхода из чата"""
await state.clear()
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💬 Войти в чат", callback_data="enter_chat")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")]
])
await message.answer(
"✅ <b>Вы вышли из режима чата</b>\n\n"
"Ваши сообщения больше не будут рассылаться.",
reply_markup=keyboard,
parse_mode="HTML"
)
# Настройки для планировщика рассылки
BATCH_SIZE = 20 # Количество сообщений в пакете
BATCH_DELAY = 1.0 # Задержка между пакетами в секундах
@@ -224,8 +295,8 @@ async def forward_to_channel(message: Message, channel_id: str) -> tuple[bool, O
return False, None
@router.message(F.text)
async def handle_text_message(message: Message):
@router.message(F.text, StateFilter(ChatStates.in_chat))
async def handle_text_message(message: Message, state: FSMContext):
"""Обработчик текстовых сообщений"""
import logging
logger = logging.getLogger(__name__)
@@ -238,11 +309,13 @@ async def handle_text_message(message: Message):
logger.info(f"[CHAT] handle_text_message вызван: user={message.from_user.id}, text={message.text[:50] if message.text else 'None'}")
# ПРОВЕРКА СЧЕТОВ: Если админ отправил сообщение с номерами счетов - НЕ рассылаем
# Это сообщение будет обработано account_router для добавления в розыгрыш
# Пропускаем для account_router (который идет после chat_router)
if is_admin(message.from_user.id) and message.text and not message.text.startswith('/'):
if _contains_account_numbers(message.text):
logger.info(f"[CHAT] Обнаружены счета от админа, пропускаем рассылку (обработает account_router)")
return # Пропускаем - обработает account_router
logger.info(f"[CHAT] Обнаружены счета от админа, пропускаем - account_router обработает")
# Не делаем return, выбрасываем исключение для пропуска в следующий обработчик
from aiogram.handlers import SkipHandler
raise SkipHandler()
# БЫСТРОЕ УДАЛЕНИЕ: Если админ отвечает на сообщение словом "удалить"/"del"/"-"
if message.reply_to_message and is_admin(message.from_user.id):
@@ -256,9 +329,12 @@ async def handle_text_message(message: Message):
if msg_to_delete:
# Получаем админа
admin = await UserService.get_user_by_telegram_id(
admin = await UserService.get_or_create_user(
session,
message.from_user.id
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Помечаем как удаленное
@@ -355,11 +431,14 @@ async def handle_text_message(message: Message):
# Получаем настройки чата
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
# Получаем или создаем пользователя
user = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Обрабатываем в зависимости от режима
if settings.mode == 'broadcast':
@@ -421,8 +500,8 @@ async def handle_text_message(message: Message):
await message.answer("Не удалось переслать сообщение")
@router.message(F.photo)
async def handle_photo_message(message: Message):
@router.message(F.photo, StateFilter(ChatStates.in_chat))
async def handle_photo_message(message: Message, state: FSMContext):
"""Обработчик фото"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -440,10 +519,13 @@ async def handle_photo_message(message: Message):
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
user = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Получаем file_id самого большого фото
photo = message.photo[-1]
@@ -495,8 +577,8 @@ async def handle_photo_message(message: Message):
await message.answer("✅ Фото переслано в канал")
@router.message(F.video)
async def handle_video_message(message: Message):
@router.message(F.video, StateFilter(ChatStates.in_chat))
async def handle_video_message(message: Message, state: FSMContext):
"""Обработчик видео"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -514,10 +596,13 @@ async def handle_video_message(message: Message):
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
user = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ)
@@ -565,8 +650,8 @@ async def handle_video_message(message: Message):
await message.answer("✅ Видео переслано в канал")
@router.message(F.document)
async def handle_document_message(message: Message):
@router.message(F.document, StateFilter(ChatStates.in_chat))
async def handle_document_message(message: Message, state: FSMContext):
"""Обработчик документов"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -584,10 +669,13 @@ async def handle_document_message(message: Message):
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
user = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ)
@@ -635,8 +723,8 @@ async def handle_document_message(message: Message):
await message.answer("✅ Документ переслан в канал")
@router.message(F.animation)
async def handle_animation_message(message: Message):
@router.message(F.animation, StateFilter(ChatStates.in_chat))
async def handle_animation_message(message: Message, state: FSMContext):
"""Обработчик GIF анимаций"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -654,10 +742,13 @@ async def handle_animation_message(message: Message):
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
user = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ)
@@ -705,8 +796,8 @@ async def handle_animation_message(message: Message):
await message.answer("✅ Анимация переслана в канал")
@router.message(F.sticker)
async def handle_sticker_message(message: Message):
@router.message(F.sticker, StateFilter(ChatStates.in_chat))
async def handle_sticker_message(message: Message, state: FSMContext):
"""Обработчик стикеров"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -724,10 +815,13 @@ async def handle_sticker_message(message: Message):
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
user = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ)

View File

@@ -38,7 +38,13 @@ async def show_chat_menu(message: Message, state: FSMContext):
await state.clear()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
user = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if not user:
await message.answer("❌ Вы не зарегистрированы. Используйте /start")
@@ -134,7 +140,13 @@ async def start_conversation(callback: CallbackQuery, state: FSMContext):
await callback.message.edit_text("❌ Пользователь не найден")
return
sender = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
sender = await UserService.get_or_create_user(
session,
callback.from_user.id,
username=callback.from_user.username,
first_name=callback.from_user.first_name,
last_name=callback.from_user.last_name
)
# Получаем последние 10 сообщений из диалога
messages = await P2PMessageService.get_conversation(
@@ -182,7 +194,13 @@ async def show_conversations(callback: CallbackQuery):
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
sender = await UserService.get_or_create_user(
session,
callback.from_user.id,
username=callback.from_user.username,
first_name=callback.from_user.first_name,
last_name=callback.from_user.last_name
)
conversations = await P2PMessageService.get_recent_conversations(session, user.id, limit=10)
@@ -274,7 +292,13 @@ async def handle_p2p_message(message: Message, state: FSMContext):
return
async with async_session_maker() as session:
sender = await UserService.get_user_by_telegram_id(session, message.from_user.id)
sender = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
sender_name = f"@{sender.username}" if sender.username else sender.first_name
# Определяем тип сообщения