This commit is contained in:
93
MIGRATION_SUMMARY.md
Normal file
93
MIGRATION_SUMMARY.md
Normal 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 развертыванию!**
|
||||
9
main.py
9
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:
|
||||
|
||||
200
migrations/versions/20260208_2121_25_beb47ddbfc33_.py
Normal file
200
migrations/versions/20260208_2121_25_beb47ddbfc33_.py
Normal 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 ###
|
||||
@@ -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")]
|
||||
]
|
||||
|
||||
# Показываем кнопку регистрации только незарегистрированным пользователям (не админам)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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':
|
||||
# Формируем информацию об отправителе для админов (если это не админ)
|
||||
|
||||
@@ -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
|
||||
|
||||
# Определяем тип сообщения
|
||||
|
||||
Reference in New Issue
Block a user