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(redraw_router) # Повторные розыгрыши
dp.include_router(p2p_chat_router) # P2P чат между пользователями dp.include_router(p2p_chat_router) # P2P чат между пользователями
# 3. Account router ПЕРЕД chat_router (обнаружение счетов для админов) # 3. Chat router для broadcast (обрабатывает обычные сообщения)
dp.include_router(account_router) # Пользовательские счета + обнаружение для админов dp.include_router(chat_router) # Пользовательский чат (broadcast всем) - РАНЬШЕ account_router
# 4. Chat router для broadcast (ловит все необработанные сообщения) # 4. Account router для обнаружения счетов (обрабатывает сообщения со счетами от админов)
# chat_router пропускает сообщения со счетами от админов dp.include_router(account_router) # Обнаружение счетов для админов - ПОСЛЕ chat_router
dp.include_router(chat_router) # Пользовательский чат (broadcast всем)
# Запускаем polling # Запускаем polling
try: 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): def get_main_keyboard(self, is_admin: bool = False, is_registered: bool = False):
"""Получить главную клавиатуру""" """Получить главную клавиатуру"""
buttons = [ 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 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( ban = await BanService.ban_user(
@@ -271,7 +277,13 @@ async def cmd_delete_message(message: Message):
async with async_session_maker() as session: 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 # Находим сообщение в базе по telegram_message_id
from sqlalchemy import select from sqlalchemy import select

View File

@@ -414,7 +414,13 @@ async def confirm_create_lottery(callback: CallbackQuery, state: FSMContext):
data = await state.get_data() data = await state.get_data()
async with async_session_maker() as session: 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( lottery = await LotteryService.create_lottery(
session, session,

View File

@@ -1,6 +1,9 @@
"""Обработчики пользовательских сообщений в чате""" """Обработчики пользовательских сообщений в чате"""
from aiogram import Router, F 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 from sqlalchemy.ext.asyncio import AsyncSession
import asyncio import asyncio
from typing import List, Dict, Optional, Set 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 from src.utils.account_utils import parse_accounts_from_message
class ChatStates(StatesGroup):
"""Состояния для работы в чате"""
in_chat = State() # Пользователь находится в режиме чата
def is_admin(user_id: int) -> bool: def is_admin(user_id: int) -> bool:
"""Проверка является ли пользователь админом""" """Проверка является ли пользователь админом"""
return user_id in ADMIN_IDS return user_id in ADMIN_IDS
@@ -34,6 +42,69 @@ def _contains_account_numbers(text: str) -> bool:
router = Router(name='chat_router') 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_SIZE = 20 # Количество сообщений в пакете
BATCH_DELAY = 1.0 # Задержка между пакетами в секундах BATCH_DELAY = 1.0 # Задержка между пакетами в секундах
@@ -224,8 +295,8 @@ async def forward_to_channel(message: Message, channel_id: str) -> tuple[bool, O
return False, None return False, None
@router.message(F.text) @router.message(F.text, StateFilter(ChatStates.in_chat))
async def handle_text_message(message: Message): async def handle_text_message(message: Message, state: FSMContext):
"""Обработчик текстовых сообщений""" """Обработчик текстовых сообщений"""
import logging import logging
logger = logging.getLogger(__name__) 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'}") 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 is_admin(message.from_user.id) and message.text and not message.text.startswith('/'):
if _contains_account_numbers(message.text): if _contains_account_numbers(message.text):
logger.info(f"[CHAT] Обнаружены счета от админа, пропускаем рассылку (обработает account_router)") logger.info(f"[CHAT] Обнаружены счета от админа, пропускаем - account_router обработает")
return # Пропускаем - обработает account_router # Не делаем return, выбрасываем исключение для пропуска в следующий обработчик
from aiogram.handlers import SkipHandler
raise SkipHandler()
# БЫСТРОЕ УДАЛЕНИЕ: Если админ отвечает на сообщение словом "удалить"/"del"/"-" # БЫСТРОЕ УДАЛЕНИЕ: Если админ отвечает на сообщение словом "удалить"/"del"/"-"
if message.reply_to_message and is_admin(message.from_user.id): 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: if msg_to_delete:
# Получаем админа # Получаем админа
admin = await UserService.get_user_by_telegram_id( admin = await UserService.get_or_create_user(
session, 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) settings = await ChatSettingsService.get_or_create_settings(session)
# Получаем пользователя # Получаем или создаем пользователя
user = await UserService.get_user_by_telegram_id(session, message.from_user.id) user = await UserService.get_or_create_user(
if not user: session,
await message.answer("❌ Пользователь не найден") message.from_user.id,
return username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Обрабатываем в зависимости от режима # Обрабатываем в зависимости от режима
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
@@ -421,8 +500,8 @@ async def handle_text_message(message: Message):
await message.answer("Не удалось переслать сообщение") await message.answer("Не удалось переслать сообщение")
@router.message(F.photo) @router.message(F.photo, StateFilter(ChatStates.in_chat))
async def handle_photo_message(message: Message): async def handle_photo_message(message: Message, state: FSMContext):
"""Обработчик фото""" """Обработчик фото"""
# Защита от дубликатов # Защита от дубликатов
if _is_message_processed(message.message_id): if _is_message_processed(message.message_id):
@@ -440,10 +519,13 @@ async def handle_photo_message(message: Message):
return return
settings = await ChatSettingsService.get_or_create_settings(session) settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id) user = await UserService.get_or_create_user(
session,
if not user: message.from_user.id,
return username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Получаем file_id самого большого фото # Получаем file_id самого большого фото
photo = message.photo[-1] photo = message.photo[-1]
@@ -495,8 +577,8 @@ async def handle_photo_message(message: Message):
await message.answer("✅ Фото переслано в канал") await message.answer("✅ Фото переслано в канал")
@router.message(F.video) @router.message(F.video, StateFilter(ChatStates.in_chat))
async def handle_video_message(message: Message): async def handle_video_message(message: Message, state: FSMContext):
"""Обработчик видео""" """Обработчик видео"""
# Защита от дубликатов # Защита от дубликатов
if _is_message_processed(message.message_id): if _is_message_processed(message.message_id):
@@ -514,10 +596,13 @@ async def handle_video_message(message: Message):
return return
settings = await ChatSettingsService.get_or_create_settings(session) settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id) user = await UserService.get_or_create_user(
session,
if not user: message.from_user.id,
return username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ) # Формируем информацию об отправителе для админов (если это не админ)
@@ -565,8 +650,8 @@ async def handle_video_message(message: Message):
await message.answer("✅ Видео переслано в канал") await message.answer("✅ Видео переслано в канал")
@router.message(F.document) @router.message(F.document, StateFilter(ChatStates.in_chat))
async def handle_document_message(message: Message): async def handle_document_message(message: Message, state: FSMContext):
"""Обработчик документов""" """Обработчик документов"""
# Защита от дубликатов # Защита от дубликатов
if _is_message_processed(message.message_id): if _is_message_processed(message.message_id):
@@ -584,10 +669,13 @@ async def handle_document_message(message: Message):
return return
settings = await ChatSettingsService.get_or_create_settings(session) settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id) user = await UserService.get_or_create_user(
session,
if not user: message.from_user.id,
return username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ) # Формируем информацию об отправителе для админов (если это не админ)
@@ -635,8 +723,8 @@ async def handle_document_message(message: Message):
await message.answer("✅ Документ переслан в канал") await message.answer("✅ Документ переслан в канал")
@router.message(F.animation) @router.message(F.animation, StateFilter(ChatStates.in_chat))
async def handle_animation_message(message: Message): async def handle_animation_message(message: Message, state: FSMContext):
"""Обработчик GIF анимаций""" """Обработчик GIF анимаций"""
# Защита от дубликатов # Защита от дубликатов
if _is_message_processed(message.message_id): if _is_message_processed(message.message_id):
@@ -654,10 +742,13 @@ async def handle_animation_message(message: Message):
return return
settings = await ChatSettingsService.get_or_create_settings(session) settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id) user = await UserService.get_or_create_user(
session,
if not user: message.from_user.id,
return username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ) # Формируем информацию об отправителе для админов (если это не админ)
@@ -705,8 +796,8 @@ async def handle_animation_message(message: Message):
await message.answer("✅ Анимация переслана в канал") await message.answer("✅ Анимация переслана в канал")
@router.message(F.sticker) @router.message(F.sticker, StateFilter(ChatStates.in_chat))
async def handle_sticker_message(message: Message): async def handle_sticker_message(message: Message, state: FSMContext):
"""Обработчик стикеров""" """Обработчик стикеров"""
# Защита от дубликатов # Защита от дубликатов
if _is_message_processed(message.message_id): if _is_message_processed(message.message_id):
@@ -724,10 +815,13 @@ async def handle_sticker_message(message: Message):
return return
settings = await ChatSettingsService.get_or_create_settings(session) settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id) user = await UserService.get_or_create_user(
session,
if not user: message.from_user.id,
return username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ) # Формируем информацию об отправителе для админов (если это не админ)

View File

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