diff --git a/MIGRATION_SUMMARY.md b/MIGRATION_SUMMARY.md
new file mode 100644
index 0000000..43f2148
--- /dev/null
+++ b/MIGRATION_SUMMARY.md
@@ -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 развертыванию!**
\ No newline at end of file
diff --git a/main.py b/main.py
index 4b141e7..a135dbf 100644
--- a/main.py
+++ b/main.py
@@ -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:
diff --git a/migrations/versions/20260208_2121_25_beb47ddbfc33_.py b/migrations/versions/20260208_2121_25_beb47ddbfc33_.py
new file mode 100644
index 0000000..2017e93
--- /dev/null
+++ b/migrations/versions/20260208_2121_25_beb47ddbfc33_.py
@@ -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 ###
\ No newline at end of file
diff --git a/src/components/ui.py b/src/components/ui.py
index 784a5d3..e4f715d 100644
--- a/src/components/ui.py
+++ b/src/components/ui.py
@@ -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")]
]
# Показываем кнопку регистрации только незарегистрированным пользователям (не админам)
diff --git a/src/handlers/admin_chat_handlers.py b/src/handlers/admin_chat_handlers.py
index 35cac33..83baed7 100644
--- a/src/handlers/admin_chat_handlers.py
+++ b/src/handlers/admin_chat_handlers.py
@@ -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
diff --git a/src/handlers/admin_panel.py b/src/handlers/admin_panel.py
index fb6058b..60a4558 100644
--- a/src/handlers/admin_panel.py
+++ b/src/handlers/admin_panel.py
@@ -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,
diff --git a/src/handlers/chat_handlers.py b/src/handlers/chat_handlers.py
index 74997a2..bd06b72 100644
--- a/src/handlers/chat_handlers.py
+++ b/src/handlers/chat_handlers.py
@@ -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(
+ "💬 Вы вошли в режим чата\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(
+ "✅ Вы вышли из режима чата\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':
# Формируем информацию об отправителе для админов (если это не админ)
diff --git a/src/handlers/p2p_chat.py b/src/handlers/p2p_chat.py
index c00c343..c337c61 100644
--- a/src/handlers/p2p_chat.py
+++ b/src/handlers/p2p_chat.py
@@ -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
# Определяем тип сообщения