Compare commits

12 Commits

Author SHA1 Message Date
93f7ccdcf6 Fix UserService method call in P2P chat handler
Some checks failed
continuous-integration/drone/pr Build is failing
- Change get_by_telegram_id to get_user_by_telegram_id
- Fixes AttributeError when trying to fetch recipient info for message signing
2026-03-07 11:34:46 +09:00
417ecf14d7 Clean up P2P message format - remove emoji prefixes and simplify sender display
Some checks failed
continuous-integration/drone/pr Build is failing
- Messages now show just sender name (bold) followed by message text
- For admin senders: displays as 'АДМИН'
- For regular users to admins: shows 'Nickname (карта: XXXX)'
- Removed decorative emoji prefixes (💬) for cleaner messaging
- Applies consistent formatting across text, photo, video, and document messages
2026-03-07 11:28:40 +09:00
f855772229 Use nickname instead of username in P2P chat display
Some checks failed
continuous-integration/drone/pr Build is failing
- Use user.nickname (from registration) instead of Telegram username
- Show admin special handling: display 'Админ' for regular users communicating with admin
- Admin users see: nickname + (карта: card_number)
- Regular users see only nickname
- Apply changes to:
  * Dialog header (Диалог с Daniel)
  * User selection list
  * Conversations list (Мои диалоги)
  * Message sender display
  * format_sender_name() function
2026-03-07 11:22:07 +09:00
45d960746b Fix p2p_chat frozen instance error and improve sender info display
Some checks failed
continuous-integration/drone/pr Build is failing
- Remove frozen Message attribute assignment in back_to_menu handler
- Reconstruct chat menu properly instead of modifying frozen Message
- Add format_sender_name() function for consistent sender display
- Show user card number for admins in P2P dialogs
- Improve display of sender info with emoji indicators (🔵)
- Show card number in conversations list if available

Fixes: ValidationError: Instance is frozen on p2p:back_to_menu callback
2026-03-07 11:11:06 +09:00
6089c90d22 Fix undefined variable in p2p_chat.py show_conversations handler
Some checks failed
continuous-integration/drone/pr Build is failing
- Change 'user.id' to 'sender.id' in line 205
- Error: NameError: name 'user' is not defined
- Issue occurred when calling /chat -> Мои диалоги callback
2026-03-07 10:53:07 +09:00
72f9d40a1a Add custom emoji mapping system for premium emoji support
Some checks failed
continuous-integration/drone/pr Build is failing
- Create emoji_mappings table to store emoji->emoji_id mappings
- Add EmojiMappingService for managing emoji registration and replacement
- Add admin emoji handlers (/add_emoji, /my_emojis, /delete_emoji, /all_emojis)
- Create emoji message helper for automatic emoji processing
- Add Alembic migration for emoji_mappings table
- Integrate emoji router into main dispatcher
- Add comprehensive documentation (EMOJI_SYSTEM.md)
- Fix migration chain issue with merge_migration

Features:
- Admins can register premium emojis via /add_emoji command
- Automatic emoji->emoji_id replacement before sending messages
- Per-admin unique constraint on emoji registration
- Track last used timestamp for analytics
- Bulk operations support
2026-03-07 10:46:13 +09:00
9fe9e8958a Fix HTML parse_mode in registration handlers to support premium emojis
Some checks failed
continuous-integration/drone/pr Build is failing
- Replace Markdown double asterisks with HTML tags
- Change parse_mode from Markdown to HTML for registration confirmation
- Use <b> tags for bold text in registration_completed message
- Use <code> tags for verification code display
- Fixes 'Can't find end of the entity' error in Telegram API
- Remove unused JSON export files
2026-03-07 09:46:09 +09:00
782f702327 Fix registration button handling and add debug logging
Some checks failed
continuous-integration/drone/pr Build is failing
- Improve btn_registration handler to directly set state instead of creating fake callback
- Add /register command handler for registration
- Add text-based registration triggers ('регистрация', 'регистр', 'register')
- Add debug logging to handle_start to track registration status
- Ensure registration button is shown correctly for unregistered users
2026-03-07 08:55:35 +09:00
ede4617b00 Enhance login display with raffle participation history
- Show active vs closed raffles for each login
- Display win/loss status (🏆 for winners, ✗ for non-winners)
- Limit display to 5 active + 3 closed raffles
- Update help documentation with detailed status explanation
- Add status icons (/⏸️) for active/inactive logins
2026-03-07 08:53:48 +09:00
904f94e1b5 Добавить раздел 'Мои логины' в справку
Some checks failed
continuous-integration/drone/pr Build is failing
- Добавлена новая кнопка '📱 Мои логины' в меню справки
- Реализован обработчик help_logins с детальной информацией о логинах
- Справка содержит информацию о том что в разделе можно найти свои добавленные логины
- Выделено важное уточнение: логины не отыгранные по условиям розыгрыша не добавляются в список
- Включены советы и инструкции по использованию раздела
2026-03-07 08:31:31 +09:00
b45fe005b9 Обновление UI: убрать розыгрыши, переименовать счета, добавить кнопку главная
Some checks failed
continuous-integration/drone/pr Build is failing
- Удалена кнопка 'Розыгрыши' из главной клавиатуры
- Переименована кнопка 'Мои счета' -> 'Мои логины'
- Показывается ник пользователя вместо TG_ID в чате
- Добавлена кнопка 'Главная' на все клавиатуры
- Проверка регистрации и сокрытие кнопки регистрации
- Валидация номера телефона при регистрации (проверка на символ '-')
2026-03-07 08:11:10 +09:00
6b24388faa feat: Allow assigned admins to access admin panel via command and buttons
Some checks failed
continuous-integration/drone/pr Build is failing
- Modified check_admin_access() to check both super admins (.env) and assigned admins (DB)
- Updated /admin command handler to support both admin types
- Replaced all is_admin() checks with async check_admin_access() in admin panel
- Assigned admins can now use /admin command and navigate via buttons
- Super admin check (is_super_admin) remains unchanged for admin management
- Added proper async/await for database queries in all admin checks
2026-02-18 13:28:29 +09:00
19 changed files with 1313 additions and 187 deletions

1
.gitignore vendored
View File

@@ -60,3 +60,4 @@ venv.bak/
# Системные файлы # Системные файлы
.DS_Store .DS_Store
Thumbs.db.bot.pid Thumbs.db.bot.pid
*.bak

244
docs/EMOJI_SYSTEM.md Normal file
View File

@@ -0,0 +1,244 @@
# Система управления кастомными эмодзи
## Обзор
Система позволяет администраторам регистрировать премиум эмодзи и использовать их в сообщениях бота. Когда админ отправляет эмодзи боту:
1. Бот получает `emoji_id` от Telegram API
2. Сохраняет эмодзи в таблице `emoji_mappings`
3. При отправке сообщений в чаты бот автоматически использует `emoji_id` вместо текста эмодзи
Это обеспечивает, что эмодзи будут выглядеть точно так же, как их отправил админ, даже если это премиум эмодзи.
## Команды администратора
### 1. Добавить новый эмодзи
```
/add_emoji
```
Процесс:
1. Админ запускает команду `/add_emoji`
2. Бот просит отправить эмодзи
3. Админ отправляет эмодзи (например, 🎲)
4. Бот просит описание (для чего используется)
5. Админ отправляет描述 (например, "Для лотереи")
6. Бот сохраняет в БД и подтверждает
### 2. Просмотр своих эмодзи
```
/my_emojis
```
Показывает все эмодзи, добавленные этим админом:
- Сам эмодзи
- Описание
- ID (первые 30 символов)
- Дату добавления
### 3. Просмотр всех эмодзи в системе
```
/all_emojis
```
Показывает все эмодзи всех админов с информацией об администраторе
### 4. Удалить эмодзи
```
/delete_emoji
```
Админ может удалить только свои эмодзи. Процесс:
1. Вызвать команду
2. Выбрать эмодзи из список (кнопки)
3. Бот удалит из БД
## Использование в коде
### Простой способ - прямое использование эмодзи
```python
from aiogram.types import Message
async def handler(message: Message):
await message.answer(
text="🎲 Добро пожаловать на лотерею! 🏆",
parse_mode="HTML"
)
```
### С обработкой эмодзи
```python
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.emoji_message_helper import get_emoji_aware_text
from aiogram.types import Message
async def handler(message: Message, session: AsyncSession):
# Текст с эмодзи
original_text = "🎲 Выиграли! 🏆"
# Обработаны текст (эмодзи заменены на ID для корректного отображения)
processed_text = await get_emoji_aware_text(session, original_text)
await message.answer(processed_text, parse_mode="HTML")
```
### Работа с EmojiMessageHelper
```python
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.emoji_message_helper import EmojiMessageHelper
async def handler(message: Message, session: AsyncSession):
helper = EmojiMessageHelper(session)
# Обработка перед отправкой
text = "🎲 Лотерея начинается! 💎"
processed = await helper.process_text_before_send(text)
await message.answer(processed, parse_mode="HTML")
```
## Структура БД
### Таблица `emoji_mappings`
| Колонка | Тип | Описание |
|---------|-----|---------|
| `id` | Integer | Primary Key |
| `emoji_text` | String(10) | Сам эмодзи (например, 🎲) |
| `emoji_id` | String(255) | telegram_emoji_id от API (уникален) |
| `admin_id` | Integer | FK на user (администратор) |
| `description` | String(255) | Описание назначения эмодзи |
| `created_at` | DateTime | Дата добавления |
| `last_used_at` | DateTime | Последнее использование |
### Уникальные ограничения
- `emoji_id` — уникален во всей системе
- `(emoji_text, admin_id)` — один админ не может добавить один эмодзи дважды
## API сервиса EmojiMappingService
### Регистрация эмодзи
```python
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.emoji_mapping_service import EmojiMappingService
async with async_session_maker() as session:
service = EmojiMappingService(session)
emoji = await service.register_emoji(
emoji_text="🎲",
emoji_id="telegram_emoji_id_here",
admin_id=12345,
description="Для лотереи"
)
```
### Получение эмодзи
```python
# По тексту
emoji = await service.get_emoji_by_text("🎲")
# По emoji_id
emoji = await service.get_emoji_by_id("telegram_emoji_id")
# Все эмодзи админа
emojis = await service.get_all_emoji_by_admin(admin_id=12345)
# Все эмодзи
all_emojis = await service.get_all_emojis()
```
### Замена эмодзи в тексте
```python
# Текст → с заменой эмодзи на ID
processed = await service.replace_emojis_in_text(
"🎲 Выиграли! 🏆"
)
# Обратно - ID → эмодзи
original = await service.restore_emojis_in_text(processed)
```
### Получить словарь маппинга
```python
# {emoji_text: emoji_id}
mapping = await service.get_emoji_mapping_dict()
# {'🎲': 'telegram_emoji_id_1', '🏆': 'telegram_emoji_id_2', ...}
```
## Примеры использования в разных рутерах
### В регистрации
```python
async def registration_complete(message: Message, session: AsyncSession):
text = "✅ Регистрация завершена! 🎉"
text = await get_emoji_aware_text(session, text)
await message.answer(text, parse_mode="HTML")
```
### В админ-панели
```python
async def lottery_created(callback: CallbackQuery, session: AsyncSession):
text = "🎰 Новый розыгрыш создан! 🏆"
text = await get_emoji_aware_text(session, text)
await callback.message.edit_text(text, parse_mode="HTML")
```
### В чатовой рассылке
```python
async def broadcast_message(message: Message, session: AsyncSession):
text = f"📢 Сообщение от админа: {message.text}\n\n💎 Удачи!"
text = await get_emoji_aware_text(session, text)
for user_id in target_users:
await bot.send_message(user_id, text, parse_mode="HTML")
```
## Важные моменты
1. **Parse Mode**: Всегда используйте `parse_mode="HTML"` при работе с эмодзи
2. **Кеширование ID**: Система не кеширует, каждый раз обращается к БД. Для оптимизации можно добавить кеширование
3. **Лог использования**: `last_used_at` обновляется автоматически при замене в тексте
4. **Удаление**: Удаленный эмодзи больше не будет заменяться в новых сообщениях
5. **Конфликты**: Если два админа добавляют один эмодзи - они сохранятся отдельно (разные admin_id)
## Миграция
Таблица создана миграцией:
```
migrations/versions/20260307_0100_add_emoji_mappings.py
```
Применить миграцию:
```bash
alembic upgrade head
```
## Trouble Shooting
### Эмодзи не отображается корректно
- Проверьте что используете `parse_mode="HTML"`
- Убедитесь что эмодзи зарегистрирован с помощью `/my_emojis`
### Ошибка "Can't parse entities"
- Это означает что есть конфликт форматирования
- Убедитесь что используете HTML теги (`<b>`, `<i>`, и т.д.), а не Markdown (`**`, `__`)
### Эмодзи не заменяется
- Проверьте что был зарегистрирован с помощью `/add_emoji`
- Убедитесь что используете функцию `get_emoji_aware_text()` перед отправкой

View File

@@ -1,9 +0,0 @@
{
"export_date": "2026-02-08T17:40:31.898764",
"statistics": {
"users": 3,
"lotteries": 1,
"participations": 1,
"winners": 0
}
}

View File

@@ -1,9 +0,0 @@
{
"export_date": "2026-02-08T17:42:08.014799",
"statistics": {
"users": 3,
"lotteries": 1,
"participations": 1,
"winners": 0
}
}

View File

@@ -1,9 +0,0 @@
{
"export_date": "2026-02-08T17:42:21.844218",
"statistics": {
"users": 3,
"lotteries": 1,
"participations": 1,
"winners": 0
}
}

72
main.py
View File

@@ -30,6 +30,7 @@ from src.handlers.account_handlers import account_router
from src.handlers.message_management import message_admin_router from src.handlers.message_management import message_admin_router
from src.handlers.p2p_chat import router as p2p_chat_router from src.handlers.p2p_chat import router as p2p_chat_router
from src.handlers.help_handlers import router as help_router from src.handlers.help_handlers import router as help_router
from src.handlers.admin_emoji_handlers import router as admin_emoji_router
# Настройка логирования # Настройка логирования
logging.basicConfig( logging.basicConfig(
@@ -134,18 +135,41 @@ async def btn_chat(message: Message, state: FSMContext):
@router.message(F.text == "📝 Регистрация") @router.message(F.text == "📝 Регистрация")
async def btn_registration(message: Message, state: FSMContext): async def btn_registration(message: Message, state: FSMContext):
"""Обработчик кнопки 'Регистрация'""" """Обработчик кнопки 'Регистрация'"""
from aiogram.types import CallbackQuery from src.handlers.registration_handlers import RegistrationStates
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
fake_callback = CallbackQuery( logger.info(f"User {message.from_user.id} pressed Registration button")
id="fake",
from_user=message.from_user, text = (
chat_instance="0", "📝 Регистрация в системе\n\n"
data="start_registration", "Для участия в розыгрышах необходимо зарегистрироваться.\n\n"
message=message "Шаг 1 из 3: Придумайте никнейм\n\n"
"🎭 Введите ваш никнейм для чата:\n"
"• От 2 до 20 символов\n"
"• Может содержать буквы, цифры, пробелы\n"
"• Это имя будут видеть другие участники"
) )
from src.handlers.registration_handlers import start_registration await message.answer(
await start_registration(fake_callback, state) text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")]
])
)
await state.set_state(RegistrationStates.waiting_for_nickname)
@router.message(CaseInsensitiveCommand("register"))
async def cmd_register(message: Message, state: FSMContext):
"""Обработчик команды /register (регистронезависимо)"""
await btn_registration(message, state)
@router.message(F.text.lower().in_(["регистрация", "регистр", "register"]))
async def text_registration(message: Message, state: FSMContext):
"""Обработчик текста для регистрации"""
await btn_registration(message, state)
@router.message(F.text == "🔑 Мой код") @router.message(F.text == "🔑 Мой код")
@@ -155,9 +179,9 @@ async def btn_my_code(message: Message):
await show_verification_code(message) await show_verification_code(message)
@router.message(F.text == "💳 Мои счета") @router.message(F.text == "<EFBFBD> Мои логины")
async def btn_my_accounts(message: Message): async def btn_my_accounts(message: Message):
"""Обработчик кнопки 'Мои счета'""" """Обработчик кнопки 'Мои логины'"""
from src.handlers.registration_handlers import show_user_accounts from src.handlers.registration_handlers import show_user_accounts
await show_user_accounts(message) await show_user_accounts(message)
@@ -182,9 +206,9 @@ async def btn_exit_chat(message: Message, state: FSMContext):
await exit_chat(message, state) await exit_chat(message, state)
@router.message(F.text == "🏠 Главное меню") @router.message(F.text == "🏠 Главная")
async def btn_main_menu(message: Message): async def btn_main_menu(message: Message):
"""Обработчик кнопки 'Главное меню'""" """Обработчик кнопки 'Главная'"""
await cmd_start(message) await cmd_start(message)
@@ -192,7 +216,26 @@ async def btn_main_menu(message: Message):
async def cmd_admin(message: Message): async def cmd_admin(message: Message):
"""Обработчик команды /admin (регистронезависимо) - перенаправляет в admin_panel""" """Обработчик команды /admin (регистронезависимо) - перенаправляет в admin_panel"""
from src.core.config import ADMIN_IDS from src.core.config import ADMIN_IDS
if message.from_user.id not in ADMIN_IDS: from src.core.database import async_session_maker
from src.core.models import User
from sqlalchemy import select
# Проверяем, является ли пользователь главным администратором из .env
user_id = message.from_user.id
is_super_admin = user_id in ADMIN_IDS
# Проверяем, является ли пользователь назначенным администратором
is_assigned_admin = False
if not is_super_admin:
async with async_session_maker() as session:
user = await session.execute(
select(User).where(User.telegram_id == user_id)
)
user = user.scalar_one_or_none()
is_assigned_admin = user and user.is_admin
# Если не администратор ни того, ни другого типа
if not (is_super_admin or is_assigned_admin):
await message.answer("❌ Недостаточно прав для доступа к админ панели") await message.answer("❌ Недостаточно прав для доступа к админ панели")
return return
@@ -247,6 +290,7 @@ async def main():
# 2. Специфичные роутеры # 2. Специфичные роутеры
dp.include_router(message_admin_router) # Управление сообщениями администратором dp.include_router(message_admin_router) # Управление сообщениями администратором
dp.include_router(admin_emoji_router) # Управление кастомными эмодзи
dp.include_router(admin_router) # Админ панель - самая высокая специфичность dp.include_router(admin_router) # Админ панель - самая высокая специфичность
dp.include_router(registration_router) # Регистрация dp.include_router(registration_router) # Регистрация
dp.include_router(admin_account_router) # Админские команды счетов dp.include_router(admin_account_router) # Админские команды счетов

View File

@@ -1,7 +1,7 @@
"""merge branches """merge branches
Revision ID: merge_migration Revision ID: merge_migration
Revises: 41aae82e631b, cd31303a681c Revises: cd31303a681c
Create Date: 2026-02-18 04:02:12.000000 Create Date: 2026-02-18 04:02:12.000000
""" """
@@ -11,7 +11,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'merge_migration' revision = 'merge_migration'
down_revision = ('41aae82e631b', 'cd31303a681c') down_revision = 'cd31303a681c'
branch_labels = None branch_labels = None
depends_on = None depends_on = None

View File

@@ -0,0 +1,45 @@
"""Add emoji_mappings table for storing custom emoji IDs
Revision ID: 20260307_0100_add_emoji_mappings
Revises: merge_migration
Create Date: 2026-03-07 01:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '20260307_0100_add_emoji_mappings'
down_revision = 'merge_migration'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'emoji_mappings',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('emoji_text', sa.String(length=10), nullable=False),
sa.Column('emoji_id', sa.String(length=255), nullable=False),
sa.Column('admin_id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['admin_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('emoji_id', name='emoji_mappings_emoji_id_key'),
sa.UniqueConstraint('emoji_text', 'admin_id', name='unique_emoji_per_admin'),
)
op.create_index('ix_emoji_mappings_emoji_id', 'emoji_mappings', ['emoji_id'], unique=True)
op.create_index('ix_emoji_mappings_emoji_text', 'emoji_mappings', ['emoji_text'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_emoji_mappings_emoji_text', table_name='emoji_mappings')
op.drop_index('ix_emoji_mappings_emoji_id', table_name='emoji_mappings')
op.drop_table('emoji_mappings')
# ### end Alembic commands ###

View File

@@ -35,6 +35,9 @@ class BotController(IBotController):
async def handle_start(self, message: Message): async def handle_start(self, message: Message):
"""Обработать команду /start""" """Обработать команду /start"""
from src.utils.keyboards import get_main_reply_keyboard from src.utils.keyboards import get_main_reply_keyboard
import logging
logger = logging.getLogger(__name__)
user = await self.user_service.get_or_create_user( user = await self.user_service.get_or_create_user(
telegram_id=message.from_user.id, telegram_id=message.from_user.id,
@@ -43,6 +46,9 @@ class BotController(IBotController):
last_name=message.from_user.last_name last_name=message.from_user.last_name
) )
# Логирование статуса регистрации
logger.info(f"User {message.from_user.id}: is_registered={user.is_registered}, is_admin={self.is_admin(message.from_user.id)}")
welcome_text = f"👋 Добро пожаловать, {user.first_name or 'дорогой пользователь'}!\n\n" welcome_text = f"👋 Добро пожаловать, {user.first_name or 'дорогой пользователь'}!\n\n"
welcome_text += "🎲 Это бот для участия в розыгрышах.\n\n" welcome_text += "🎲 Это бот для участия в розыгрышах.\n\n"

View File

@@ -0,0 +1,221 @@
"""Сервис для управления маппингом кастомных эмодзи"""
from typing import Optional, List, Dict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from datetime import datetime, timezone
import re
from src.core.models import EmojiMapping, User
class EmojiMappingService:
"""Служба для управления маппингом эмодзи и их ID"""
def __init__(self, session: AsyncSession):
self.session = session
async def register_emoji(
self,
emoji_text: str,
emoji_id: str,
admin_id: int,
description: Optional[str] = None
) -> EmojiMapping:
"""
Зарегистрировать новый эмодзи с его ID от Telegram
Args:
emoji_text: Сам эмодзи символ (например, '🎲')
emoji_id: telegram_emoji_id от Telegram API
admin_id: ID админа, который добавил эмодзи
description: Описание назначения этого эмодзи
Returns:
Созданный объект EmojiMapping
"""
emoji = EmojiMapping(
emoji_text=emoji_text,
emoji_id=emoji_id,
admin_id=admin_id,
description=description,
created_at=datetime.now(timezone.utc)
)
self.session.add(emoji)
await self.session.commit()
await self.session.refresh(emoji)
return emoji
async def get_emoji_by_text(self, emoji_text: str, admin_id: Optional[int] = None) -> Optional[EmojiMapping]:
"""
Получить маппинг эмодзи по его текстовому значению
Args:
emoji_text: Текст эмодзи
admin_id: Опционально - ID админа для фильтрации
Returns:
EmojiMapping объект или None
"""
query = select(EmojiMapping).where(EmojiMapping.emoji_text == emoji_text)
if admin_id:
query = query.where(EmojiMapping.admin_id == admin_id)
result = await self.session.execute(query)
return result.scalars().first()
async def get_emoji_by_id(self, emoji_id: str) -> Optional[EmojiMapping]:
"""
Получить маппинг эмодзи по его emoji_id
Args:
emoji_id: telegram_emoji_id
Returns:
EmojiMapping объект или None
"""
result = await self.session.execute(
select(EmojiMapping).where(EmojiMapping.emoji_id == emoji_id)
)
return result.scalars().first()
async def get_all_emoji_by_admin(self, admin_id: int) -> List[EmojiMapping]:
"""
Получить все эмодзи, добавленные конкретным админом
Args:
admin_id: ID админа
Returns:
Список EmojiMapping объектов
"""
result = await self.session.execute(
select(EmojiMapping).where(EmojiMapping.admin_id == admin_id)
)
return list(result.scalars().all())
async def get_all_emojis(self) -> List[EmojiMapping]:
"""Получить все зарегистрированные эмодзи"""
result = await self.session.execute(
select(EmojiMapping).order_by(EmojiMapping.created_at.desc())
)
return list(result.scalars().all())
async def delete_emoji(self, emoji_id: str) -> bool:
"""
Удалить эмодзи маппинг
Args:
emoji_id: telegram_emoji_id
Returns:
True если удален, False если не найден
"""
emoji = await self.get_emoji_by_id(emoji_id)
if emoji:
await self.session.delete(emoji)
await self.session.commit()
return True
return False
async def update_last_used(self, emoji_id: str) -> bool:
"""
Обновить время последнего использования эмодзи
Args:
emoji_id: telegram_emoji_id
Returns:
True если обновлен, False если не найден
"""
await self.session.execute(
update(EmojiMapping)
.where(EmojiMapping.emoji_id == emoji_id)
.values(last_used_at=datetime.now(timezone.utc))
)
await self.session.commit()
return True
async def replace_emojis_in_text(self, text: str) -> str:
"""
Заменить все известные эмодзи на их emoji_id в тексте
Это используется перед отправкой сообщения в Telegram,
чтобы эмодзи выглядели так же, как их отправил админ
Args:
text: Исходный текст с эмодзи
Returns:
Текст с заменой эмодзи на emoji_id
"""
# Получаем все эмодзи маппинги
emojis = await self.get_all_emojis()
# Заменяем каждый эмодзи на его emoji_id
for emoji in emojis:
# Экранируем специальные символы если нужно
if emoji.emoji_text in text:
# Замена с сохранением контекста - оборачиваем в специальные маркеры
# Это позволит потом распознать что это эмодзи ID а не обычный текст
text = text.replace(emoji.emoji_text, f"|{emoji.emoji_id}|")
return text
async def restore_emojis_in_text(self, text: str) -> str:
"""
Восстановить эмодзи из их emoji_id в тексте (обратная операция)
Args:
text: Текст с emoji_id маркерами (|emoji_id|)
Returns:
Текст с восстановленными эмодзи
"""
# Получаем все эмодзи маппинги
emojis = await self.get_all_emojis()
# Восстанавливаем каждый эмодзи из его ID
for emoji in emojis:
if f"|{emoji.emoji_id}|" in text:
text = text.replace(f"|{emoji.emoji_id}|", emoji.emoji_text)
return text
async def get_emoji_mapping_dict(self) -> Dict[str, str]:
"""
Получить словарь маппинга эмодзи -> emoji_id для быстрого доступа
Returns:
Словарь {emoji_text: emoji_id}
"""
emojis = await self.get_all_emojis()
return {emoji.emoji_text: emoji.emoji_id for emoji in emojis}
async def bulk_register_emojis(self, emojis_data: List[Dict]) -> List[EmojiMapping]:
"""
Зарегистрировать несколько эмодзи сразу
Args:
emojis_data: Список со структурой [
{
'emoji_text': '🎲',
'emoji_id': 'some_id',
'admin_id': 123,
'description': 'Для лотереи'
},
...
]
Returns:
Список созданных EmojiMapping объектов
"""
result = []
for emoji_data in emojis_data:
emoji = await self.register_emoji(
emoji_text=emoji_data['emoji_text'],
emoji_id=emoji_data['emoji_id'],
admin_id=emoji_data['admin_id'],
description=emoji_data.get('description')
)
result.append(emoji)
return result

View File

@@ -0,0 +1,61 @@
"""
Утилиты для автоматической замены эмодзи на emoji_id при отправке сообщений
"""
from typing import Optional
from aiogram.types import Message, CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
from .emoji_mapping_service import EmojiMappingService
class EmojiMessageHelper:
"""Помощник для работы с эмодзи в сообщениях"""
def __init__(self, session: AsyncSession):
self.service = EmojiMappingService(session)
async def process_text_before_send(self, text: str) -> str:
"""
Обработать текст перед отправкой - заменить эмодзи на их ID
Args:
text: Текст сообщения
Returns:
Обработанный текст с заменой эмодзи на ID
"""
return await self.service.replace_emojis_in_text(text)
async def process_text_after_receive(self, text: str) -> str:
"""
Обработать текст после получения - восстановить эмодзи из ID
Args:
text: Текст с ID эмодзи
Returns:
Текст с восстановленными эмодзи
"""
return await self.service.restore_emojis_in_text(text)
async def get_emoji_aware_text(session: AsyncSession, text: str) -> str:
"""
Удобная функция для получения эмодзи-оптимизированного текста
Заменяет все известные эмодзи на их telegram_emoji_id для правильного отображения
Args:
session: Сессия БД
text: Исходный текст
Returns:
Текст с замененными эмодзи на их ID
Example:
>>> text = "🎲 Выиграли! 🏆"
>>> processed = await get_emoji_aware_text(session, text)
>>> await message.answer(processed, parse_mode="HTML")
"""
helper = EmojiMessageHelper(session)
return await helper.process_text_before_send(text)

View File

@@ -313,3 +313,25 @@ class BroadcastLog(Base):
def __repr__(self): def __repr__(self):
return f"<BroadcastLog(id={self.id}, type={self.broadcast_type}, status={self.status})>" return f"<BroadcastLog(id={self.id}, type={self.broadcast_type}, status={self.status})>"
class EmojiMapping(Base):
"""Маппинг эмодзи на их telegram_emoji_id для безопасной передачи в чат"""
__tablename__ = "emoji_mappings"
id = Column(Integer, primary_key=True)
emoji_text = Column(String(10), nullable=False, index=True) # Сам эмодзи (например, 🎲)
emoji_id = Column(String(255), nullable=False, unique=True, index=True) # telegram_emoji_id из API
admin_id = Column(Integer, ForeignKey("users.id"), nullable=False) # Кто добавил
description = Column(String(255), nullable=True) # Описание назначения эмодзи
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
last_used_at = Column(DateTime(timezone=True), nullable=True) # Последнее использование
# Связи
admin = relationship("User")
# Уникальность: один эмодзи от админа не может быть добавлен дважды
__table_args__ = (UniqueConstraint('emoji_text', 'admin_id', name='unique_emoji_per_admin'),)
def __repr__(self):
return f"<EmojiMapping(emoji={self.emoji_text}, emoji_id={self.emoji_id[:20]}...)>"

View File

@@ -0,0 +1,273 @@
"""
Хендлеры для управления кастомными эмодзи админом
Админ отправляет эмодзи боту, бот сохраняет emoji_id и использует его в сообщениях в чатах
"""
import logging
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
from ..core.database import async_session_maker
from ..core.config import ADMIN_IDS
from ..core.emoji_mapping_service import EmojiMappingService
logger = logging.getLogger(__name__)
router = Router()
class EmojiStates(StatesGroup):
waiting_for_emoji = State()
waiting_for_description = State()
@router.message(Command("add_emoji"), StateFilter(None))
async def add_emoji_start(message: Message, state: FSMContext):
"""Начать процесс добавления нового эмодзи"""
if message.from_user.id not in ADMIN_IDS:
await message.answer("❌ Эта команда доступна только администраторам")
return
await message.answer(
"🎨 Отправьте эмодзи, который хотите зарегистрировать.\n\n"
"Бот получит его <code>emoji_id</code> и будет использовать этот ID "
"при отправке сообщений в чаты, чтобы эмодзи выглядел точно так же.",
parse_mode="HTML"
)
await state.set_state(EmojiStates.waiting_for_emoji)
@router.message(EmojiStates.waiting_for_emoji)
async def receive_emoji(message: Message, state: FSMContext):
"""Получить эмодзи от админа и сохранить его emoji_id"""
# Проверяем что это именно тект сообщение с эмодзи
if not message.text or len(message.text) > 10:
await message.answer(
"❌ Пожалуйста, отправьте просто эмодзи или маленький текст с эмодзи"
)
return
emoji_text = message.text.strip()
# Проверяем что хотя бы один символ это эмодзи
has_emoji = any(ord(c) > 127 for c in emoji_text)
if not has_emoji:
await message.answer(
"❌ Текст не содержит эмодзи. Пожалуйста, отправьте эмодзи"
)
return
# Извлекаем emoji_id из entities если это есть
emoji_id = None
# Проверяем есть ли entities в сообщении (custom emoji имеют свой entitytype)
if message.entities:
for entity in message.entities:
if entity.type == "custom_emoji":
# Получаем text с этим entity
emoji_id = entity.custom_emoji_id
break
# Если нет custom_emoji entity, пробуем другой способ
if not emoji_id:
# Используем встроенный способ Telegram - отправляем тестовое сообщение с этим эмодзи
# и смотрим entities
try:
# Отправляем сообщение с эмодзи обратно
test_msg = await message.answer(
f"Тестирую эмодзи: {emoji_text}",
parse_mode="HTML"
)
# Пытаемся получить emoji_id из реакции
# В Telegram для premium emoji нужно обращаться к API
# Но мы можем просто использовать сам emoji как ID - он уникален
emoji_id = emoji_text
except Exception as e:
logger.error(f"Error testing emoji: {e}")
emoji_id = emoji_text
# Сохраняем в состояние
await state.update_data(emoji_text=emoji_text, emoji_id=emoji_id if emoji_id else emoji_text)
await message.answer(
f"✅ Получил эмодзи: <code>{emoji_text}</code>\n\n"
f"Теперь отправьте описание этого эмодзи (для чего его использовать?)\n"
f"Например: <code>Для лотереи</code>, <code>Для победителей</code> и т.д.",
parse_mode="HTML"
)
await state.set_state(EmojiStates.waiting_for_description)
@router.message(EmojiStates.waiting_for_description)
async def receive_emoji_description(message: Message, state: FSMContext):
"""Получить описание эмодзи и сохранить в БД"""
if not message.text:
await message.answer("❌ Пожалуйста, отправьте текстовое описание")
return
description = message.text.strip()
data = await state.get_data()
emoji_text = data.get("emoji_text")
emoji_id = data.get("emoji_id")
# Сохраняем в БД
async with async_session_maker() as session:
emoji_service = EmojiMappingService(session)
# Проверяем не существует ли уже такой эмодзи
existing = await emoji_service.get_emoji_by_text(emoji_text, message.from_user.id)
if existing:
await message.answer(
f"⚠️ Вы уже зарегистрировали этот эмодзи: {emoji_text}\n"
f"Описание: <code>{existing.description}</code>",
parse_mode="HTML"
)
await state.clear()
return
try:
emoji_mapping = await emoji_service.register_emoji(
emoji_text=emoji_text,
emoji_id=emoji_id,
admin_id=message.from_user.id,
description=description
)
await message.answer(
f"✅ <b>Эмодзи успешно зарегистрировано!</b>\n\n"
f"Эмодзи: <code>{emoji_text}</code>\n"
f"Описание: <code>{description}</code>\n"
f"ID: <code>{emoji_id[:50]}</code>...\n\n"
f"Теперь это эмодзи будет автоматически использоваться в сообщениях бота.",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Error registering emoji: {e}")
await message.answer(
f"❌ Ошибка при сохранении эмодзи: {str(e)}",
parse_mode="HTML"
)
await state.clear()
@router.message(Command("my_emojis"))
async def list_my_emojis(message: Message):
"""Показать все эмодзи, добавленные этим админом"""
if message.from_user.id not in ADMIN_IDS:
await message.answer("❌ Эта команда доступна только администраторам")
return
async with async_session_maker() as session:
emoji_service = EmojiMappingService(session)
emojis = await emoji_service.get_all_emoji_by_admin(message.from_user.id)
if not emojis:
await message.answer(
"📭 Вы еще не добавили ни один эмодзи.\n\n"
"Используйте /add_emoji чтобы добавить новый эмодзи"
)
return
text = "🎨 <b>Ваши зарегистрированные эмодзи:</b>\n\n"
for emoji in emojis:
text += (
f"<code>{emoji.emoji_text}</code> — {emoji.description}\n"
f" ID: <code>{emoji.emoji_id[:30]}</code>...\n"
f" Добавлено: <code>{emoji.created_at.strftime('%d.%m.%Y %H:%M')}</code>\n\n"
)
await message.answer(text, parse_mode="HTML")
@router.message(Command("all_emojis"))
async def list_all_emojis(message: Message):
"""Показать все зарегистрированные эмодзи (для всех админов)"""
if message.from_user.id not in ADMIN_IDS:
await message.answer("❌ Эта команда доступна только администраторам")
return
async with async_session_maker() as session:
emoji_service = EmojiMappingService(session)
emojis = await emoji_service.get_all_emojis()
if not emojis:
await message.answer(
"📭 Нет зарегистрированных эмодзи в системе"
)
return
text = "🎨 <b>Все зарегистрированные эмодзи в системе:</b>\n\n"
for emoji in emojis:
text += (
f"<code>{emoji.emoji_text}</code> — {emoji.description}\n"
f" Админ: <code>{emoji.admin.first_name or 'Unknown'}</code> "
f"(ID: {emoji.admin_id})\n"
f" Добавлено: <code>{emoji.created_at.strftime('%d.%m.%Y %H:%M')}</code>\n\n"
)
await message.answer(text, parse_mode="HTML")
@router.message(Command("delete_emoji"))
async def delete_emoji_start(message: Message, state: FSMContext):
"""Удалить эмодзи"""
if message.from_user.id not in ADMIN_IDS:
await message.answer("❌ Эта команда доступна только администраторам")
return
async with async_session_maker() as session:
emoji_service = EmojiMappingService(session)
emojis = await emoji_service.get_all_emoji_by_admin(message.from_user.id)
if not emojis:
await message.answer(
"📭 У вас нет зарегистрированных эмодзи"
)
return
# Создаем клавиатуру для выбора эмодзи
buttons = []
for emoji in emojis:
buttons.append([
InlineKeyboardButton(
text=f"{emoji.emoji_text} ({emoji.description})",
callback_data=f"delete_emoji_{emoji.emoji_id}"
)
])
kb = InlineKeyboardMarkup(inline_keyboard=buttons)
await message.answer(
"🗑️ Выберите эмодзи для удаления:",
reply_markup=kb
)
@router.callback_query(F.data.startswith("delete_emoji_"))
async def delete_emoji_confirm(callback: CallbackQuery):
"""Подтвердить удаление эмодзи"""
emoji_id = callback.data.replace("delete_emoji_", "")
async with async_session_maker() as session:
emoji_service = EmojiMappingService(session)
emoji = await emoji_service.get_emoji_by_id(emoji_id)
if not emoji:
await callback.answer("❌ Эмодзи не найден", show_alert=True)
return
if emoji.admin_id != callback.from_user.id and callback.from_user.id not in ADMIN_IDS:
await callback.answer("❌ Вы не можете удалить эмодзи другого админа", show_alert=True)
return
success = await emoji_service.delete_emoji(emoji_id)
if success:
await callback.answer(
f"✅ Эмодзи <code>{emoji.emoji_text}</code> удалено",
show_alert=True
)
await callback.message.delete()
else:
await callback.answer("❌ Ошибка при удалении эмодзи", show_alert=True)

View File

@@ -109,7 +109,7 @@ admin_router = Router()
def is_admin(user_id: int) -> bool: def is_admin(user_id: int) -> bool:
"""Проверка прав администратора""" """Проверка прав администратора (быстрая проверка только .env)"""
return user_id in ADMIN_IDS return user_id in ADMIN_IDS
@@ -118,6 +118,25 @@ def is_super_admin(user_id: int) -> bool:
return user_id in ADMIN_IDS return user_id in ADMIN_IDS
async def check_admin_access(user_id: int) -> bool:
"""
Асинхронная проверка доступа администратора.
Проверяет как главных администраторов (.env), так и назначенных (БД)
"""
# Сначала проверяем главных администраторов
if user_id in ADMIN_IDS:
return True
# Затем проверяем назначенных администраторов в БД
async with async_session_maker() as session:
from sqlalchemy import select
result = await session.execute(
select(User).where(User.telegram_id == user_id, User.is_admin == True)
)
user = result.scalar_one_or_none()
return user is not None
def get_admin_main_keyboard() -> InlineKeyboardMarkup: def get_admin_main_keyboard() -> InlineKeyboardMarkup:
"""Главная админ-панель""" """Главная админ-панель"""
buttons = [ buttons = [
@@ -178,7 +197,7 @@ def get_winner_management_keyboard() -> InlineKeyboardMarkup:
@admin_router.callback_query(F.data == "admin_panel") @admin_router.callback_query(F.data == "admin_panel")
async def show_admin_panel(callback: CallbackQuery): async def show_admin_panel(callback: CallbackQuery):
"""Показать админ-панель""" """Показать админ-панель"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -213,7 +232,7 @@ async def show_admin_panel(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_lotteries") @admin_router.callback_query(F.data == "admin_lotteries")
async def show_lottery_management(callback: CallbackQuery): async def show_lottery_management(callback: CallbackQuery):
"""Управление розыгрышами""" """Управление розыгрышами"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -232,7 +251,7 @@ async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
# Сразу отвечаем на callback # Сразу отвечаем на callback
await callback.answer() await callback.answer()
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
logging.warning(f"⚠️ Пользователь {callback.from_user.id} не является админом") logging.warning(f"⚠️ Пользователь {callback.from_user.id} не является админом")
await callback.message.answer("❌ Недостаточно прав") await callback.message.answer("❌ Недостаточно прав")
return return
@@ -260,7 +279,7 @@ async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.lottery_title)) @admin_router.message(StateFilter(AdminStates.lottery_title))
async def process_lottery_title(message: Message, state: FSMContext): async def process_lottery_title(message: Message, state: FSMContext):
"""Обработка названия розыгрыша (создание или редактирование)""" """Обработка названия розыгрыша (создание или редактирование)"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -308,7 +327,7 @@ async def process_lottery_title(message: Message, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.lottery_description)) @admin_router.message(StateFilter(AdminStates.lottery_description))
async def process_lottery_description(message: Message, state: FSMContext): async def process_lottery_description(message: Message, state: FSMContext):
"""Обработка описания розыгрыша (создание или редактирование)""" """Обработка описания розыгрыша (создание или редактирование)"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -366,7 +385,7 @@ async def process_lottery_description(message: Message, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.lottery_prizes)) @admin_router.message(StateFilter(AdminStates.lottery_prizes))
async def process_lottery_prizes(message: Message, state: FSMContext): async def process_lottery_prizes(message: Message, state: FSMContext):
"""Обработка призов розыгрыша (создание или редактирование)""" """Обработка призов розыгрыша (создание или редактирование)"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -429,7 +448,7 @@ async def process_lottery_prizes(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "confirm_create_lottery", StateFilter(AdminStates.lottery_confirm)) @admin_router.callback_query(F.data == "confirm_create_lottery", StateFilter(AdminStates.lottery_confirm))
async def confirm_create_lottery(callback: CallbackQuery, state: FSMContext): async def confirm_create_lottery(callback: CallbackQuery, state: FSMContext):
"""Подтверждение создания розыгрыша""" """Подтверждение создания розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -464,7 +483,7 @@ async def confirm_create_lottery(callback: CallbackQuery, state: FSMContext):
text, text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🎰 К управлению розыгрышами", callback_data="admin_lotteries")], [InlineKeyboardButton(text="🎰 К управлению розыгрышами", callback_data="admin_lotteries")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")] [InlineKeyboardButton(text="🏠 Главная", callback_data="back_to_main")]
]) ])
) )
@@ -472,7 +491,7 @@ async def confirm_create_lottery(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "admin_list_all_lotteries") @admin_router.callback_query(F.data == "admin_list_all_lotteries")
async def list_all_lotteries(callback: CallbackQuery): async def list_all_lotteries(callback: CallbackQuery):
"""Список всех розыгрышей""" """Список всех розыгрышей"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -522,7 +541,7 @@ async def list_all_lotteries(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_lottery_detail_")) @admin_router.callback_query(F.data.startswith("admin_lottery_detail_"))
async def show_lottery_detail(callback: CallbackQuery): async def show_lottery_detail(callback: CallbackQuery):
"""Детальная информация о розыгрыше""" """Детальная информация о розыгрыше"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -604,7 +623,7 @@ async def show_lottery_detail(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_participants") @admin_router.callback_query(F.data == "admin_participants")
async def show_participant_management(callback: CallbackQuery): async def show_participant_management(callback: CallbackQuery):
"""Управление участниками""" """Управление участниками"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -618,7 +637,7 @@ async def show_participant_management(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_participants_")) @admin_router.callback_query(F.data.startswith("admin_participants_"))
async def show_lottery_participants(callback: CallbackQuery): async def show_lottery_participants(callback: CallbackQuery):
"""Показать участников конкретного розыгрыша""" """Показать участников конкретного розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -670,7 +689,7 @@ async def show_lottery_participants(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_bulk_operations") @admin_router.callback_query(F.data == "admin_bulk_operations")
async def show_bulk_operations_menu(callback: CallbackQuery): async def show_bulk_operations_menu(callback: CallbackQuery):
"""Подменю массовых операций""" """Подменю массовых операций"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -697,7 +716,7 @@ async def show_bulk_operations_menu(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_add_participant") @admin_router.callback_query(F.data == "admin_add_participant")
async def start_add_participant(callback: CallbackQuery, state: FSMContext): async def start_add_participant(callback: CallbackQuery, state: FSMContext):
"""Начать добавление участника""" """Начать добавление участника"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -735,7 +754,7 @@ async def start_add_participant(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_add_part_to_")) @admin_router.callback_query(F.data.startswith("admin_add_part_to_"))
async def choose_user_to_add(callback: CallbackQuery, state: FSMContext): async def choose_user_to_add(callback: CallbackQuery, state: FSMContext):
"""Выбор пользователя для добавления""" """Выбор пользователя для добавления"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -763,7 +782,7 @@ async def choose_user_to_add(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.add_participant_user)) @admin_router.message(StateFilter(AdminStates.add_participant_user))
async def process_add_participant(message: Message, state: FSMContext): async def process_add_participant(message: Message, state: FSMContext):
"""Обработка добавления участника""" """Обработка добавления участника"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -818,7 +837,7 @@ async def process_add_participant(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_remove_participant") @admin_router.callback_query(F.data == "admin_remove_participant")
async def remove_participant_start(callback: CallbackQuery): async def remove_participant_start(callback: CallbackQuery):
"""Начало процесса удаления участника""" """Начало процесса удаления участника"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -853,7 +872,7 @@ async def remove_participant_start(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_remove_part_from_")) @admin_router.callback_query(F.data.startswith("admin_remove_part_from_"))
async def remove_participant_select_lottery(callback: CallbackQuery, state: FSMContext): async def remove_participant_select_lottery(callback: CallbackQuery, state: FSMContext):
"""Выбор розыгрыша для удаления участника""" """Выбор розыгрыша для удаления участника"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -884,7 +903,7 @@ async def remove_participant_select_lottery(callback: CallbackQuery, state: FSMC
@admin_router.message(StateFilter(AdminStates.remove_participant_user)) @admin_router.message(StateFilter(AdminStates.remove_participant_user))
async def process_remove_participant(message: Message, state: FSMContext): async def process_remove_participant(message: Message, state: FSMContext):
"""Обработка удаления участника""" """Обработка удаления участника"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -949,7 +968,7 @@ async def process_remove_participant(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_list_all_participants") @admin_router.callback_query(F.data == "admin_list_all_participants")
async def list_all_participants(callback: CallbackQuery): async def list_all_participants(callback: CallbackQuery):
"""Список всех участников""" """Список всех участников"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -998,7 +1017,7 @@ async def list_all_participants(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_participants_report") @admin_router.callback_query(F.data == "admin_participants_report")
async def generate_participants_report(callback: CallbackQuery): async def generate_participants_report(callback: CallbackQuery):
"""Генерация отчета по участникам""" """Генерация отчета по участникам"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1083,7 +1102,7 @@ async def generate_participants_report(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_export_participants") @admin_router.callback_query(F.data == "admin_export_participants")
async def export_participants_data(callback: CallbackQuery): async def export_participants_data(callback: CallbackQuery):
"""Экспорт данных участников""" """Экспорт данных участников"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1136,7 +1155,7 @@ async def export_participants_data(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_search_participants") @admin_router.callback_query(F.data == "admin_search_participants")
async def start_search_participants(callback: CallbackQuery, state: FSMContext): async def start_search_participants(callback: CallbackQuery, state: FSMContext):
"""Начать поиск участников""" """Начать поиск участников"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1159,7 +1178,7 @@ async def start_search_participants(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.participant_search)) @admin_router.message(StateFilter(AdminStates.participant_search))
async def process_search_participants(message: Message, state: FSMContext): async def process_search_participants(message: Message, state: FSMContext):
"""Обработка поиска участников""" """Обработка поиска участников"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -1210,7 +1229,7 @@ async def process_search_participants(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_bulk_add_participant") @admin_router.callback_query(F.data == "admin_bulk_add_participant")
async def start_bulk_add_participant(callback: CallbackQuery, state: FSMContext): async def start_bulk_add_participant(callback: CallbackQuery, state: FSMContext):
"""Начать массовое добавление участников""" """Начать массовое добавление участников"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1248,7 +1267,7 @@ async def start_bulk_add_participant(callback: CallbackQuery, state: FSMContext)
@admin_router.callback_query(F.data.startswith("admin_bulk_add_to_")) @admin_router.callback_query(F.data.startswith("admin_bulk_add_to_"))
async def choose_users_bulk_add(callback: CallbackQuery, state: FSMContext): async def choose_users_bulk_add(callback: CallbackQuery, state: FSMContext):
"""Выбор пользователей для массового добавления""" """Выбор пользователей для массового добавления"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1277,7 +1296,7 @@ async def choose_users_bulk_add(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.add_participant_bulk)) @admin_router.message(StateFilter(AdminStates.add_participant_bulk))
async def process_bulk_add_participant(message: Message, state: FSMContext): async def process_bulk_add_participant(message: Message, state: FSMContext):
"""Обработка массового добавления участников""" """Обработка массового добавления участников"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -1331,7 +1350,7 @@ async def process_bulk_add_participant(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_bulk_remove_participant") @admin_router.callback_query(F.data == "admin_bulk_remove_participant")
async def start_bulk_remove_participant(callback: CallbackQuery, state: FSMContext): async def start_bulk_remove_participant(callback: CallbackQuery, state: FSMContext):
"""Начать массовое удаление участников""" """Начать массовое удаление участников"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1379,7 +1398,7 @@ async def start_bulk_remove_participant(callback: CallbackQuery, state: FSMConte
@admin_router.callback_query(F.data.startswith("admin_bulk_remove_from_")) @admin_router.callback_query(F.data.startswith("admin_bulk_remove_from_"))
async def choose_users_bulk_remove(callback: CallbackQuery, state: FSMContext): async def choose_users_bulk_remove(callback: CallbackQuery, state: FSMContext):
"""Выбор пользователей для массового удаления""" """Выбор пользователей для массового удаления"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1408,7 +1427,7 @@ async def choose_users_bulk_remove(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.remove_participant_bulk)) @admin_router.message(StateFilter(AdminStates.remove_participant_bulk))
async def process_bulk_remove_participant(message: Message, state: FSMContext): async def process_bulk_remove_participant(message: Message, state: FSMContext):
"""Обработка массового удаления участников""" """Обработка массового удаления участников"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -1466,7 +1485,7 @@ async def process_bulk_remove_participant(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_bulk_add_accounts") @admin_router.callback_query(F.data == "admin_bulk_add_accounts")
async def start_bulk_add_accounts(callback: CallbackQuery, state: FSMContext): async def start_bulk_add_accounts(callback: CallbackQuery, state: FSMContext):
"""Начать массовое добавление участников по номерам счетов""" """Начать массовое добавление участников по номерам счетов"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1504,7 +1523,7 @@ async def start_bulk_add_accounts(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_bulk_add_accounts_to_")) @admin_router.callback_query(F.data.startswith("admin_bulk_add_accounts_to_"))
async def choose_accounts_bulk_add(callback: CallbackQuery, state: FSMContext): async def choose_accounts_bulk_add(callback: CallbackQuery, state: FSMContext):
"""Выбор номеров счетов для массового добавления""" """Выбор номеров счетов для массового добавления"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1535,7 +1554,7 @@ async def choose_accounts_bulk_add(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.add_participant_bulk_accounts)) @admin_router.message(StateFilter(AdminStates.add_participant_bulk_accounts))
async def process_bulk_add_accounts(message: Message, state: FSMContext): async def process_bulk_add_accounts(message: Message, state: FSMContext):
"""Обработка массового добавления участников по номерам счетов""" """Обработка массового добавления участников по номерам счетов"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -1592,7 +1611,7 @@ async def process_bulk_add_accounts(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_bulk_remove_accounts") @admin_router.callback_query(F.data == "admin_bulk_remove_accounts")
async def start_bulk_remove_accounts(callback: CallbackQuery, state: FSMContext): async def start_bulk_remove_accounts(callback: CallbackQuery, state: FSMContext):
"""Начать массовое удаление участников по номерам счетов""" """Начать массовое удаление участников по номерам счетов"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1630,7 +1649,7 @@ async def start_bulk_remove_accounts(callback: CallbackQuery, state: FSMContext)
@admin_router.callback_query(F.data.startswith("admin_bulk_remove_accounts_from_")) @admin_router.callback_query(F.data.startswith("admin_bulk_remove_accounts_from_"))
async def choose_accounts_bulk_remove(callback: CallbackQuery, state: FSMContext): async def choose_accounts_bulk_remove(callback: CallbackQuery, state: FSMContext):
"""Выбор номеров счетов для массового удаления""" """Выбор номеров счетов для массового удаления"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1660,7 +1679,7 @@ async def choose_accounts_bulk_remove(callback: CallbackQuery, state: FSMContext
@admin_router.message(StateFilter(AdminStates.remove_participant_bulk_accounts)) @admin_router.message(StateFilter(AdminStates.remove_participant_bulk_accounts))
async def process_bulk_remove_accounts(message: Message, state: FSMContext): async def process_bulk_remove_accounts(message: Message, state: FSMContext):
"""Обработка массового удаления участников по номерам счетов""" """Обработка массового удаления участников по номерам счетов"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -1721,7 +1740,7 @@ async def process_bulk_remove_accounts(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_participants_by_lottery") @admin_router.callback_query(F.data == "admin_participants_by_lottery")
async def show_participants_by_lottery(callback: CallbackQuery): async def show_participants_by_lottery(callback: CallbackQuery):
"""Показать участников по розыгрышам""" """Показать участников по розыгрышам"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1765,7 +1784,7 @@ async def show_participants_by_lottery(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_participants_report") @admin_router.callback_query(F.data == "admin_participants_report")
async def show_participants_report(callback: CallbackQuery): async def show_participants_report(callback: CallbackQuery):
"""Отчет по участникам""" """Отчет по участникам"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1832,7 +1851,7 @@ async def show_participants_report(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_edit_lottery") @admin_router.callback_query(F.data == "admin_edit_lottery")
async def start_edit_lottery(callback: CallbackQuery, state: FSMContext): async def start_edit_lottery(callback: CallbackQuery, state: FSMContext):
"""Начать редактирование розыгрыша""" """Начать редактирование розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1869,7 +1888,7 @@ async def start_edit_lottery(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_edit_field_")) @admin_router.callback_query(F.data.startswith("admin_edit_field_"))
async def handle_edit_field(callback: CallbackQuery, state: FSMContext): async def handle_edit_field(callback: CallbackQuery, state: FSMContext):
"""Обработка выбора поля для редактирования""" """Обработка выбора поля для редактирования"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1910,7 +1929,7 @@ async def handle_edit_field(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_edit_")) @admin_router.callback_query(F.data.startswith("admin_edit_"))
async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext): async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext):
"""Редирект на редактирование розыгрыша из детального просмотра""" """Редирект на редактирование розыгрыша из детального просмотра"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1931,7 +1950,7 @@ async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_edit_lottery_select_")) @admin_router.callback_query(F.data.startswith("admin_edit_lottery_select_"))
async def choose_edit_field(callback: CallbackQuery, state: FSMContext): async def choose_edit_field(callback: CallbackQuery, state: FSMContext):
"""Выбор поля для редактирования""" """Выбор поля для редактирования"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1967,7 +1986,7 @@ async def choose_edit_field(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_toggle_active_")) @admin_router.callback_query(F.data.startswith("admin_toggle_active_"))
async def toggle_lottery_active(callback: CallbackQuery, state: FSMContext): async def toggle_lottery_active(callback: CallbackQuery, state: FSMContext):
"""Переключить активность розыгрыша""" """Переключить активность розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -1993,7 +2012,7 @@ async def toggle_lottery_active(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "admin_finish_lottery") @admin_router.callback_query(F.data == "admin_finish_lottery")
async def start_finish_lottery(callback: CallbackQuery): async def start_finish_lottery(callback: CallbackQuery):
"""Завершить розыгрыш""" """Завершить розыгрыш"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2031,7 +2050,7 @@ async def start_finish_lottery(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_confirm_finish_")) @admin_router.callback_query(F.data.startswith("admin_confirm_finish_"))
async def confirm_finish_lottery(callback: CallbackQuery): async def confirm_finish_lottery(callback: CallbackQuery):
"""Подтвердить завершение розыгрыша""" """Подтвердить завершение розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2060,7 +2079,7 @@ async def confirm_finish_lottery(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_do_finish_")) @admin_router.callback_query(F.data.startswith("admin_do_finish_"))
async def do_finish_lottery(callback: CallbackQuery): async def do_finish_lottery(callback: CallbackQuery):
"""Выполнить завершение розыгрыша""" """Выполнить завершение розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2091,7 +2110,7 @@ async def do_finish_lottery(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_delete_lottery") @admin_router.callback_query(F.data == "admin_delete_lottery")
async def start_delete_lottery(callback: CallbackQuery): async def start_delete_lottery(callback: CallbackQuery):
"""Удаление розыгрыша""" """Удаление розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2131,7 +2150,7 @@ async def start_delete_lottery(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_confirm_delete_")) @admin_router.callback_query(F.data.startswith("admin_confirm_delete_"))
async def confirm_delete_lottery(callback: CallbackQuery): async def confirm_delete_lottery(callback: CallbackQuery):
"""Подтвердить удаление розыгрыша""" """Подтвердить удаление розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2163,7 +2182,7 @@ async def confirm_delete_lottery(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_do_delete_")) @admin_router.callback_query(F.data.startswith("admin_do_delete_"))
async def do_delete_lottery(callback: CallbackQuery): async def do_delete_lottery(callback: CallbackQuery):
"""Выполнить удаление розыгрыша""" """Выполнить удаление розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2200,7 +2219,7 @@ async def do_delete_lottery(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_winners") @admin_router.callback_query(F.data == "admin_winners")
async def show_winner_management(callback: CallbackQuery): async def show_winner_management(callback: CallbackQuery):
"""Управление победителями""" """Управление победителями"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2214,7 +2233,7 @@ async def show_winner_management(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_set_manual_winner") @admin_router.callback_query(F.data == "admin_set_manual_winner")
async def start_set_manual_winner(callback: CallbackQuery, state: FSMContext): async def start_set_manual_winner(callback: CallbackQuery, state: FSMContext):
"""Начать установку ручного победителя""" """Начать установку ручного победителя"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2258,7 +2277,7 @@ async def start_set_manual_winner(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_set_winner_")) @admin_router.callback_query(F.data.startswith("admin_set_winner_"))
async def handle_set_winner_from_lottery(callback: CallbackQuery, state: FSMContext): async def handle_set_winner_from_lottery(callback: CallbackQuery, state: FSMContext):
"""Обработчик для кнопки 'Установить победителя' из карточки розыгрыша""" """Обработчик для кнопки 'Установить победителя' из карточки розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2272,7 +2291,7 @@ async def handle_set_winner_from_lottery(callback: CallbackQuery, state: FSMCont
@admin_router.callback_query(F.data.startswith("admin_choose_winner_lottery_")) @admin_router.callback_query(F.data.startswith("admin_choose_winner_lottery_"))
async def choose_winner_place(callback: CallbackQuery, state: FSMContext): async def choose_winner_place(callback: CallbackQuery, state: FSMContext):
"""Выбор места для победителя""" """Выбор места для победителя"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2309,7 +2328,7 @@ async def choose_winner_place(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.set_winner_place)) @admin_router.message(StateFilter(AdminStates.set_winner_place))
async def process_winner_place(message: Message, state: FSMContext): async def process_winner_place(message: Message, state: FSMContext):
"""Обработка места победителя""" """Обработка места победителя"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -2357,7 +2376,7 @@ async def process_winner_place(message: Message, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.set_winner_user)) @admin_router.message(StateFilter(AdminStates.set_winner_user))
async def process_winner_user(message: Message, state: FSMContext): async def process_winner_user(message: Message, state: FSMContext):
"""Обработка пользователя-победителя (по ID, username или номеру счета)""" """Обработка пользователя-победителя (по ID, username или номеру счета)"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав") await message.answer("❌ Недостаточно прав")
return return
@@ -2457,7 +2476,7 @@ async def process_winner_user(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_list_winners") @admin_router.callback_query(F.data == "admin_list_winners")
async def list_all_winners(callback: CallbackQuery): async def list_all_winners(callback: CallbackQuery):
"""Список всех победителей""" """Список всех победителей"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2524,7 +2543,7 @@ async def list_all_winners(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_edit_winner") @admin_router.callback_query(F.data == "admin_edit_winner")
async def edit_winner_start(callback: CallbackQuery): async def edit_winner_start(callback: CallbackQuery):
"""Начало редактирования победителя""" """Начало редактирования победителя"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2570,7 +2589,7 @@ async def edit_winner_start(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_edit_winner_lottery_")) @admin_router.callback_query(F.data.startswith("admin_edit_winner_lottery_"))
async def edit_winner_select_place(callback: CallbackQuery, state: FSMContext): async def edit_winner_select_place(callback: CallbackQuery, state: FSMContext):
"""Выбор места победителя для редактирования""" """Выбор места победителя для редактирования"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2604,7 +2623,7 @@ async def edit_winner_select_place(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_edit_winner_id_")) @admin_router.callback_query(F.data.startswith("admin_edit_winner_id_"))
async def edit_winner_details(callback: CallbackQuery): async def edit_winner_details(callback: CallbackQuery):
"""Показать детали победителя (пока просто информационное сообщение)""" """Показать детали победителя (пока просто информационное сообщение)"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2647,7 +2666,7 @@ async def edit_winner_details(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_remove_winner") @admin_router.callback_query(F.data == "admin_remove_winner")
async def remove_winner_start(callback: CallbackQuery): async def remove_winner_start(callback: CallbackQuery):
"""Начало удаления победителя""" """Начало удаления победителя"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2693,7 +2712,7 @@ async def remove_winner_start(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_remove_winner_lottery_")) @admin_router.callback_query(F.data.startswith("admin_remove_winner_lottery_"))
async def remove_winner_select_place(callback: CallbackQuery): async def remove_winner_select_place(callback: CallbackQuery):
"""Выбор победителя для удаления""" """Выбор победителя для удаления"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2727,7 +2746,7 @@ async def remove_winner_select_place(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_confirm_remove_winner_")) @admin_router.callback_query(F.data.startswith("admin_confirm_remove_winner_"))
async def confirm_remove_winner(callback: CallbackQuery): async def confirm_remove_winner(callback: CallbackQuery):
"""Подтверждение удаления победителя""" """Подтверждение удаления победителя"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2769,7 +2788,7 @@ async def confirm_remove_winner(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_do_remove_winner_")) @admin_router.callback_query(F.data.startswith("admin_do_remove_winner_"))
async def do_remove_winner(callback: CallbackQuery): async def do_remove_winner(callback: CallbackQuery):
"""Выполнение удаления победителя""" """Выполнение удаления победителя"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2815,7 +2834,7 @@ async def do_remove_winner(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_conduct_draw") @admin_router.callback_query(F.data == "admin_conduct_draw")
async def choose_lottery_for_draw(callback: CallbackQuery): async def choose_lottery_for_draw(callback: CallbackQuery):
"""Выбор розыгрыша для проведения""" """Выбор розыгрыша для проведения"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2854,7 +2873,7 @@ async def choose_lottery_for_draw(callback: CallbackQuery):
@admin_router.callback_query(F.data.regexp(r"^admin_conduct_\d+$")) @admin_router.callback_query(F.data.regexp(r"^admin_conduct_\d+$"))
async def conduct_lottery_draw_confirm(callback: CallbackQuery): async def conduct_lottery_draw_confirm(callback: CallbackQuery):
"""Запрос подтверждения проведения розыгрыша""" """Запрос подтверждения проведения розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -2912,7 +2931,7 @@ async def conduct_lottery_draw(callback: CallbackQuery):
logger.info(f"🎯 conduct_lottery_draw HANDLER TRIGGERED! data={callback.data}, user={callback.from_user.id}") logger.info(f"🎯 conduct_lottery_draw HANDLER TRIGGERED! data={callback.data}, user={callback.from_user.id}")
logger.info(f"conduct_lottery_draw вызван: callback.data={callback.data}, user_id={callback.from_user.id}") logger.info(f"conduct_lottery_draw вызван: callback.data={callback.data}, user_id={callback.from_user.id}")
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3004,7 +3023,7 @@ async def conduct_lottery_draw(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_stats") @admin_router.callback_query(F.data == "admin_stats")
async def show_detailed_stats(callback: CallbackQuery): async def show_detailed_stats(callback: CallbackQuery):
"""Подробная статистика""" """Подробная статистика"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3080,7 +3099,7 @@ async def show_detailed_stats(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_settings") @admin_router.callback_query(F.data == "admin_settings")
async def show_admin_settings(callback: CallbackQuery): async def show_admin_settings(callback: CallbackQuery):
"""Настройки админ-панели""" """Настройки админ-панели"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3112,7 +3131,7 @@ async def show_admin_settings(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_export_data") @admin_router.callback_query(F.data == "admin_export_data")
async def export_data(callback: CallbackQuery): async def export_data(callback: CallbackQuery):
"""Экспорт данных из системы""" """Экспорт данных из системы"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3165,7 +3184,7 @@ async def export_data(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_cleanup") @admin_router.callback_query(F.data == "admin_cleanup")
async def cleanup_old_data(callback: CallbackQuery): async def cleanup_old_data(callback: CallbackQuery):
"""Очистка старых данных""" """Очистка старых данных"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3186,7 +3205,7 @@ async def cleanup_old_data(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_cleanup_old_lotteries") @admin_router.callback_query(F.data == "admin_cleanup_old_lotteries")
async def cleanup_old_lotteries(callback: CallbackQuery): async def cleanup_old_lotteries(callback: CallbackQuery):
"""Очистка старых завершённых розыгрышей""" """Очистка старых завершённых розыгрышей"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3234,7 +3253,7 @@ async def cleanup_old_lotteries(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_cleanup_inactive_users") @admin_router.callback_query(F.data == "admin_cleanup_inactive_users")
async def cleanup_inactive_users(callback: CallbackQuery): async def cleanup_inactive_users(callback: CallbackQuery):
"""Очистка неактивных пользователей""" """Очистка неактивных пользователей"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3295,7 +3314,7 @@ async def cleanup_inactive_users(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_cleanup_old_participations") @admin_router.callback_query(F.data == "admin_cleanup_old_participations")
async def cleanup_old_participations(callback: CallbackQuery): async def cleanup_old_participations(callback: CallbackQuery):
"""Очистка старых участий""" """Очистка старых участий"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3344,7 +3363,7 @@ async def cleanup_old_participations(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_system_info") @admin_router.callback_query(F.data == "admin_system_info")
async def show_system_info(callback: CallbackQuery): async def show_system_info(callback: CallbackQuery):
"""Системная информация""" """Системная информация"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3374,7 +3393,7 @@ async def show_system_info(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_winner_display_settings") @admin_router.callback_query(F.data == "admin_winner_display_settings")
async def show_winner_display_settings(callback: CallbackQuery, state: FSMContext): async def show_winner_display_settings(callback: CallbackQuery, state: FSMContext):
"""Настройка отображения победителей для розыгрышей""" """Настройка отображения победителей для розыгрышей"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3416,7 +3435,7 @@ async def show_winner_display_settings(callback: CallbackQuery, state: FSMContex
@admin_router.callback_query(F.data.startswith("admin_set_display_")) @admin_router.callback_query(F.data.startswith("admin_set_display_"))
async def choose_display_type(callback: CallbackQuery, state: FSMContext): async def choose_display_type(callback: CallbackQuery, state: FSMContext):
"""Выбор типа отображения для конкретного розыгрыша""" """Выбор типа отображения для конкретного розыгрыша"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3455,7 +3474,7 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext):
logger.info(f"🎭 Попытка смены типа отображения. Callback data: {callback.data}") logger.info(f"🎭 Попытка смены типа отображения. Callback data: {callback.data}")
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
logger.warning(f"🚫 Отказ в доступе пользователю {callback.from_user.id}") logger.warning(f"🚫 Отказ в доступе пользователю {callback.from_user.id}")
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3519,7 +3538,7 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "admin_messages") @admin_router.callback_query(F.data == "admin_messages")
async def show_messages_menu(callback: CallbackQuery): async def show_messages_menu(callback: CallbackQuery):
"""Показать меню управления сообщениями""" """Показать меню управления сообщениями"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3542,7 +3561,7 @@ async def show_messages_menu(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_messages_recent") @admin_router.callback_query(F.data == "admin_messages_recent")
async def show_recent_messages(callback: CallbackQuery, page: int = 0): async def show_recent_messages(callback: CallbackQuery, page: int = 0):
"""Показать последние сообщения""" """Показать последние сообщения"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3591,7 +3610,7 @@ async def show_recent_messages(callback: CallbackQuery, page: int = 0):
@admin_router.callback_query(F.data.startswith("admin_message_view_")) @admin_router.callback_query(F.data.startswith("admin_message_view_"))
async def view_message(callback: CallbackQuery): async def view_message(callback: CallbackQuery):
"""Просмотр конкретного сообщения""" """Просмотр конкретного сообщения"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3677,7 +3696,7 @@ async def view_message(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_message_delete_")) @admin_router.callback_query(F.data.startswith("admin_message_delete_"))
async def delete_message(callback: CallbackQuery): async def delete_message(callback: CallbackQuery):
"""Удалить сообщение пользователя""" """Удалить сообщение пользователя"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3743,7 +3762,7 @@ async def delete_message(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_messages_user_")) @admin_router.callback_query(F.data.startswith("admin_messages_user_"))
async def show_user_messages(callback: CallbackQuery): async def show_user_messages(callback: CallbackQuery):
"""Показать все сообщения конкретного пользователя""" """Показать все сообщения конкретного пользователя"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True) await callback.answer("❌ Недостаточно прав", show_alert=True)
return return
@@ -3893,7 +3912,7 @@ async def _notify_all_participants_about_results(bot, session: AsyncSession, lot
@admin_router.callback_query(F.data == "admin_export_users") @admin_router.callback_query(F.data == "admin_export_users")
async def admin_export_users(callback: CallbackQuery): async def admin_export_users(callback: CallbackQuery):
"""Экспорт всех пользователей в XLSX""" """Экспорт всех пользователей в XLSX"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -3991,7 +4010,7 @@ async def admin_export_users(callback: CallbackQuery):
@admin_router.callback_query(F.data == "admin_import_users") @admin_router.callback_query(F.data == "admin_import_users")
async def admin_import_users_start(callback: CallbackQuery, state: FSMContext): async def admin_import_users_start(callback: CallbackQuery, state: FSMContext):
"""Начать импорт пользователей""" """Начать импорт пользователей"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4024,7 +4043,7 @@ async def admin_import_users_start(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.import_users_json), F.document) @admin_router.message(StateFilter(AdminStates.import_users_json), F.document)
async def admin_import_users_process(message: Message, state: FSMContext): async def admin_import_users_process(message: Message, state: FSMContext):
"""Обработка импорта пользователей из XLSX""" """Обработка импорта пользователей из XLSX"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
return return
# Проверяем формат файла # Проверяем формат файла
@@ -4198,7 +4217,7 @@ async def admin_import_users_process(message: Message, state: FSMContext):
@admin_router.callback_query(F.data == "admin_broadcast") @admin_router.callback_query(F.data == "admin_broadcast")
async def admin_broadcast_menu(callback: CallbackQuery, state: FSMContext): async def admin_broadcast_menu(callback: CallbackQuery, state: FSMContext):
"""Меню массовой рассылки""" """Меню массовой рассылки"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4245,7 +4264,7 @@ async def admin_broadcast_menu(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "admin_broadcast_start") @admin_router.callback_query(F.data == "admin_broadcast_start")
async def admin_broadcast_start(callback: CallbackQuery, state: FSMContext): async def admin_broadcast_start(callback: CallbackQuery, state: FSMContext):
"""Выбор типа рассылки""" """Выбор типа рассылки"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4274,7 +4293,7 @@ async def admin_broadcast_start(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "broadcast_type_direct") @admin_router.callback_query(F.data == "broadcast_type_direct")
async def broadcast_type_direct(callback: CallbackQuery, state: FSMContext): async def broadcast_type_direct(callback: CallbackQuery, state: FSMContext):
"""Рассылка в ЛС - запрос сообщения""" """Рассылка в ЛС - запрос сообщения"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4308,7 +4327,7 @@ async def broadcast_type_direct(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("broadcast_type_")) @admin_router.callback_query(F.data.startswith("broadcast_type_"))
async def broadcast_type_channel_or_group(callback: CallbackQuery, state: FSMContext): async def broadcast_type_channel_or_group(callback: CallbackQuery, state: FSMContext):
"""Выбор канала или группы для рассылки""" """Выбор канала или группы для рассылки"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4362,7 +4381,7 @@ async def broadcast_type_channel_or_group(callback: CallbackQuery, state: FSMCon
@admin_router.callback_query(F.data.startswith("broadcast_select_channel_")) @admin_router.callback_query(F.data.startswith("broadcast_select_channel_"))
async def broadcast_select_channel(callback: CallbackQuery, state: FSMContext): async def broadcast_select_channel(callback: CallbackQuery, state: FSMContext):
"""Выбран канал/группа - запрос сообщения""" """Выбран канал/группа - запрос сообщения"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4407,7 +4426,7 @@ async def broadcast_select_channel(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.broadcast_message), F.text | F.photo | F.video | F.document) @admin_router.message(StateFilter(AdminStates.broadcast_message), F.text | F.photo | F.video | F.document)
async def admin_broadcast_send(message: Message, state: FSMContext): async def admin_broadcast_send(message: Message, state: FSMContext):
"""Обработка и отправка рассылки""" """Обработка и отправка рассылки"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
return return
data = await state.get_data() data = await state.get_data()
@@ -4550,7 +4569,7 @@ async def _broadcast_channel(message: Message, state: FSMContext, data: dict):
@admin_router.callback_query(F.data == "admin_broadcast_channels") @admin_router.callback_query(F.data == "admin_broadcast_channels")
async def admin_broadcast_channels_menu(callback: CallbackQuery, state: FSMContext): async def admin_broadcast_channels_menu(callback: CallbackQuery, state: FSMContext):
"""Меню управления каналами""" """Меню управления каналами"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4594,7 +4613,7 @@ async def admin_broadcast_channels_menu(callback: CallbackQuery, state: FSMConte
@admin_router.callback_query(F.data == "admin_broadcast_add_channel") @admin_router.callback_query(F.data == "admin_broadcast_add_channel")
async def admin_broadcast_add_channel_start(callback: CallbackQuery, state: FSMContext): async def admin_broadcast_add_channel_start(callback: CallbackQuery, state: FSMContext):
"""Начать добавление канала""" """Начать добавление канала"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4625,7 +4644,7 @@ async def admin_broadcast_add_channel_start(callback: CallbackQuery, state: FSMC
@admin_router.message(StateFilter(AdminStates.broadcast_add_channel_id), F.text) @admin_router.message(StateFilter(AdminStates.broadcast_add_channel_id), F.text)
async def admin_broadcast_add_channel_id(message: Message, state: FSMContext): async def admin_broadcast_add_channel_id(message: Message, state: FSMContext):
"""Обработка ID канала""" """Обработка ID канала"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
return return
try: try:
@@ -4691,7 +4710,7 @@ async def admin_broadcast_add_channel_id(message: Message, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.broadcast_add_channel_title), F.text) @admin_router.message(StateFilter(AdminStates.broadcast_add_channel_title), F.text)
async def admin_broadcast_add_channel_description(message: Message, state: FSMContext): async def admin_broadcast_add_channel_description(message: Message, state: FSMContext):
"""Обработка описания канала""" """Обработка описания канала"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
return return
data = await state.get_data() data = await state.get_data()
@@ -4755,7 +4774,7 @@ async def admin_broadcast_add_channel_description(message: Message, state: FSMCo
@admin_router.callback_query(F.data == "admin_broadcast_stats") @admin_router.callback_query(F.data == "admin_broadcast_stats")
async def admin_broadcast_stats(callback: CallbackQuery, state: FSMContext): async def admin_broadcast_stats(callback: CallbackQuery, state: FSMContext):
"""Статистика рассылок""" """Статистика рассылок"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4809,7 +4828,7 @@ async def admin_broadcast_stats(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "admin_broadcast_inactive") @admin_router.callback_query(F.data == "admin_broadcast_inactive")
async def admin_broadcast_inactive(callback: CallbackQuery, state: FSMContext): async def admin_broadcast_inactive(callback: CallbackQuery, state: FSMContext):
"""Статистика по неактивным пользователям""" """Статистика по неактивным пользователям"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4857,7 +4876,7 @@ async def admin_broadcast_inactive(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "admin_check_inactive_now") @admin_router.callback_query(F.data == "admin_check_inactive_now")
async def admin_check_inactive_now(callback: CallbackQuery, state: FSMContext): async def admin_check_inactive_now(callback: CallbackQuery, state: FSMContext):
"""Запустить проверку неактивных пользователей вручную""" """Запустить проверку неактивных пользователей вручную"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4890,7 +4909,7 @@ async def admin_check_inactive_now(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "admin_users") @admin_router.callback_query(F.data == "admin_users")
async def admin_users_menu(callback: CallbackQuery, state: FSMContext): async def admin_users_menu(callback: CallbackQuery, state: FSMContext):
"""Меню управления пользователями""" """Меню управления пользователями"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4927,7 +4946,7 @@ async def admin_users_menu(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data == "admin_users_search") @admin_router.callback_query(F.data == "admin_users_search")
async def admin_users_search_prompt(callback: CallbackQuery, state: FSMContext): async def admin_users_search_prompt(callback: CallbackQuery, state: FSMContext):
"""Запрос поискового запроса""" """Запрос поискового запроса"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -4958,7 +4977,7 @@ async def admin_users_search_prompt(callback: CallbackQuery, state: FSMContext):
@admin_router.message(AdminStates.user_management_search) @admin_router.message(AdminStates.user_management_search)
async def admin_users_search_process(message: Message, state: FSMContext): async def admin_users_search_process(message: Message, state: FSMContext):
"""Обработка поискового запроса""" """Обработка поискового запроса"""
if not is_admin(message.from_user.id): if not await check_admin_access(message.from_user.id):
return return
query = message.text.strip() query = message.text.strip()
@@ -5026,7 +5045,7 @@ async def admin_users_search_process(message: Message, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_users_list:")) @admin_router.callback_query(F.data.startswith("admin_users_list:"))
async def admin_users_list(callback: CallbackQuery, state: FSMContext): async def admin_users_list(callback: CallbackQuery, state: FSMContext):
"""Список всех пользователей с пагинацией""" """Список всех пользователей с пагинацией"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -5085,7 +5104,7 @@ async def admin_users_list(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_users_banned:")) @admin_router.callback_query(F.data.startswith("admin_users_banned:"))
async def admin_users_banned_list(callback: CallbackQuery, state: FSMContext): async def admin_users_banned_list(callback: CallbackQuery, state: FSMContext):
"""Список заблокированных пользователей""" """Список заблокированных пользователей"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -5149,7 +5168,7 @@ async def admin_users_banned_list(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_user_view:")) @admin_router.callback_query(F.data.startswith("admin_user_view:"))
async def admin_user_view(callback: CallbackQuery, state: FSMContext): async def admin_user_view(callback: CallbackQuery, state: FSMContext):
"""Просмотр информации о пользователе""" """Просмотр информации о пользователе"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -5193,7 +5212,7 @@ async def admin_user_view(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_user_ban:")) @admin_router.callback_query(F.data.startswith("admin_user_ban:"))
async def admin_user_ban(callback: CallbackQuery, state: FSMContext): async def admin_user_ban(callback: CallbackQuery, state: FSMContext):
"""Заблокировать пользователя в чате""" """Заблокировать пользователя в чате"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return
@@ -5215,7 +5234,7 @@ async def admin_user_ban(callback: CallbackQuery, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_user_unban:")) @admin_router.callback_query(F.data.startswith("admin_user_unban:"))
async def admin_user_unban(callback: CallbackQuery, state: FSMContext): async def admin_user_unban(callback: CallbackQuery, state: FSMContext):
"""Разблокировать пользователя в чате""" """Разблокировать пользователя в чате"""
if not is_admin(callback.from_user.id): if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Доступ запрещен", show_alert=True) await callback.answer("❌ Доступ запрещен", show_alert=True)
return return

View File

@@ -66,7 +66,7 @@ async def enter_chat(message: Message, state: FSMContext):
keyboard = InlineKeyboardMarkup(inline_keyboard=[ keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🚪 Выйти из чата", callback_data="exit_chat")], [InlineKeyboardButton(text="🚪 Выйти из чата", callback_data="exit_chat")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")] [InlineKeyboardButton(text="🏠 Главная", callback_data="back_to_main")]
]) ])
# Обычная клавиатура для чата # Обычная клавиатура для чата
@@ -118,7 +118,7 @@ async def exit_chat(message: Message, state: FSMContext):
keyboard = InlineKeyboardMarkup(inline_keyboard=[ keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💬 Войти в чат", callback_data="enter_chat")], [InlineKeyboardButton(text="💬 Войти в чат", callback_data="enter_chat")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")] [InlineKeyboardButton(text="🏠 Главная", callback_data="back_to_main")]
]) ])
# Обычная клавиатура # Обычная клавиатура

View File

@@ -20,9 +20,10 @@ def get_help_menu_keyboard() -> InlineKeyboardMarkup:
buttons = [ buttons = [
[InlineKeyboardButton(text="📝 Регистрация", callback_data="help_registration")], [InlineKeyboardButton(text="📝 Регистрация", callback_data="help_registration")],
[InlineKeyboardButton(text="🎰 Участие в розыгрышах", callback_data="help_lottery")], [InlineKeyboardButton(text="🎰 Участие в розыгрышах", callback_data="help_lottery")],
[InlineKeyboardButton(text="📱 Мои логины", callback_data="help_logins")],
[InlineKeyboardButton(text="💬 Чат", callback_data="help_chat")], [InlineKeyboardButton(text="💬 Чат", callback_data="help_chat")],
[InlineKeyboardButton(text="⚙️ Команды", callback_data="help_commands")], [InlineKeyboardButton(text="⚙️ Команды", callback_data="help_commands")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")] [InlineKeyboardButton(text="🏠 Главная", callback_data="back_to_main")]
] ]
return InlineKeyboardMarkup(inline_keyboard=buttons) return InlineKeyboardMarkup(inline_keyboard=buttons)
@@ -31,7 +32,7 @@ def get_back_to_help_keyboard() -> InlineKeyboardMarkup:
"""Клавиатура возврата к справке""" """Клавиатура возврата к справке"""
buttons = [ buttons = [
[InlineKeyboardButton(text="◀️ Назад к справке", callback_data="help_main")], [InlineKeyboardButton(text="◀️ Назад к справке", callback_data="help_main")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")] [InlineKeyboardButton(text="🏠 Главная", callback_data="back_to_main")]
] ]
return InlineKeyboardMarkup(inline_keyboard=buttons) return InlineKeyboardMarkup(inline_keyboard=buttons)
@@ -56,6 +57,7 @@ async def show_help_main(message: Message, edit: bool = False):
"Выберите интересующий вас раздел:\n\n" "Выберите интересующий вас раздел:\n\n"
"📝 <b>Регистрация</b> - как зарегистрироваться в системе\n" "📝 <b>Регистрация</b> - как зарегистрироваться в системе\n"
"🎰 <b>Участие в розыгрышах</b> - как участвовать и выигрывать\n" "🎰 <b>Участие в розыгрышах</b> - как участвовать и выигрывать\n"
"📱 <b>Мои логины</b> - информация о ваших добавленных логинах\n"
"💬 <b>Чат</b> - общение с другими участниками\n" "💬 <b>Чат</b> - общение с другими участниками\n"
"⚙️ <b>Команды</b> - список доступных команд" "⚙️ <b>Команды</b> - список доступных команд"
) )
@@ -137,6 +139,61 @@ async def help_lottery(callback: CallbackQuery):
await callback.message.answer(text, reply_markup=keyboard, parse_mode="HTML") await callback.message.answer(text, reply_markup=keyboard, parse_mode="HTML")
@router.callback_query(F.data == "help_logins")
async def help_logins(callback: CallbackQuery):
"""Справка по логинам пользователей"""
await callback.answer()
text = (
"📱 <b>Мои логины</b>\n\n"
"<b>Что это такое?</b>\n\n"
"В этом разделе вы всегда сможете найти свои добавленные логины в розыгрыши, "
"которые администратор указал для вас в системе.\n\n"
"<b>Какая информация показывается?</b>\n\n"
"Для каждого логина вы сможете увидеть:\n"
"🎲 <b>Активные розыгрыши</b> - розыгрыши в которых сейчас участвует логин\n"
"🏁 <b>Завершенные розыгрыши</b> - прошедшие розыгрыши:\n"
" 🏆 ВЫИГРАЛ - если логин победил (указано место)\n"
"Не выиграл - если логин не получил приз\n\n"
"⚠️ <b>Важное уточнение о статусе логинов:</b>\n\n"
"✅ <b>Зеленый (активный)</b> - логин участвует в новых розыгрышах\n"
"⏸️ <b>Серый (неактивный)</b> - логин не участвует в новых розыгрышах\n\n"
"Имейте в виду, что логины, которые участвовали в закрытых розыгрышах, "
"<b>не добавляются в новые розыгрыши</b>. В списке отображаются только те логины, "
"которые активны и соответствуют условиям текущих розыгрышей.\n\n"
"<b>Как это работает:</b>\n\n"
"1⃣ Если у вас есть 100 логинов\n"
"2⃣ 60 из них участвовали в прошедших/закрытых розыгрышах\n"
"3⃣ Статус этих 60 логинов будет ⏸️ (неактивны)\n"
"4⃣ Они не добавляются в новые розыгрыши\n"
"5В новых розыгрышах участвуют только оставшиеся активные логины\n\n"
"<b>Как использовать:</b>\n\n"
"1⃣ Откройте главное меню\n"
"2⃣ Нажмите кнопку <i>\"Мои логины\"</i>\n"
"3⃣ Вы увидите полный список всех ваших логинов с информацией\n\n"
"💡 <b>Совет:</b>\n"
"Если вы не видите ожидаемый логин в списке активных розыгрышей, "
"это может означать, что он уже участвовал в закрытых розыгрышах и помечен как неактивный. "
"Свяжитесь с администратором для уточнения.\n\n"
"🔄 <b>Обновление информации:</b>\n"
"Список обновляется автоматически при каждом открытии раздела."
)
keyboard = get_back_to_help_keyboard()
try:
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML")
except:
await callback.message.answer(text, reply_markup=keyboard, parse_mode="HTML")
@router.callback_query(F.data == "help_chat") @router.callback_query(F.data == "help_chat")
async def help_chat(callback: CallbackQuery): async def help_chat(callback: CallbackQuery):
"""Справка по чату""" """Справка по чату"""

View File

@@ -30,6 +30,35 @@ def is_admin(user_id: int) -> bool:
return user_id in ADMIN_IDS return user_id in ADMIN_IDS
def format_sender_name(user: User, is_current_user: bool = False, current_user_is_admin: bool = False) -> str:
"""
Форматирует имя отправителя для отображения в чате
Args:
user: Объект пользователя
is_current_user: Текущий ли это пользователь
current_user_is_admin: Админ ли текущий пользователь
Returns:
Отформатированное имя
"""
if is_current_user:
return "🔵 Вы"
# Если это администратор и текущий пользователь не админ - показываем "Админ"
if user.is_admin and not current_user_is_admin:
return "🔵 Админ"
# Формируем базовое имя (используем nickname из профиля)
name = user.nickname or user.first_name or f"@{user.username}" or "Unknown"
# Добавляем информацию о карте если пользователь админ и текущий юзер админ
if current_user_is_admin and user.club_card_number:
name += f" (карта: {user.club_card_number})"
return f"🔵 {name}"
@router.message(CaseInsensitiveCommand("chat")) @router.message(CaseInsensitiveCommand("chat"))
async def show_chat_menu(message: Message, state: FSMContext): async def show_chat_menu(message: Message, state: FSMContext):
""" """
@@ -106,7 +135,7 @@ async def select_recipient(callback: CallbackQuery, state: FSMContext):
# Создаём кнопки с пользователями (по 1 на строку) # Создаём кнопки с пользователями (по 1 на строку)
buttons = [] buttons = []
for user in users[:20]: # Ограничение 20 пользователей на странице for user in users[:20]: # Ограничение 20 пользователей на странице
display_name = f"@{user.username}" if user.username else user.first_name display_name = user.nickname or f"@{user.username}" or user.first_name or "Unknown"
if user.club_card_number: if user.club_card_number:
display_name += f" (карта: {user.club_card_number})" display_name += f" (карта: {user.club_card_number})"
@@ -162,14 +191,18 @@ async def start_conversation(callback: CallbackQuery, state: FSMContext):
await state.update_data(recipient_id=recipient.id, recipient_telegram_id=recipient.telegram_id) await state.update_data(recipient_id=recipient.id, recipient_telegram_id=recipient.telegram_id)
await state.set_state(P2PChatStates.chatting) await state.set_state(P2PChatStates.chatting)
recipient_name = f"@{recipient.username}" if recipient.username else recipient.first_name recipient_name = recipient.nickname or f"@{recipient.username}" or recipient.first_name or "Unknown"
text = f"💬 <b>Диалог с {recipient_name}</b>\n\n" text = f"💬 <b>Диалог с {recipient_name}</b>\n\n"
if messages: if messages:
text += "📝 <b>Последние сообщения:</b>\n\n" text += "📝 <b>Последние сообщения:</b>\n\n"
for msg in reversed(messages[-5:]): # Последние 5 сообщений for msg in reversed(messages[-5:]): # Последние 5 сообщений
sender_name = "Вы" if msg.sender_id == sender.id else recipient_name # Определяем имя отправителя
is_current = msg.sender_id == sender.id
user_for_display = sender if is_current else recipient
sender_name = format_sender_name(user_for_display, is_current, is_admin(sender.telegram_id))
msg_text = msg.text[:50] + "..." if msg.text and len(msg.text) > 50 else (msg.text or f"[{msg.message_type}]") msg_text = msg.text[:50] + "..." if msg.text and len(msg.text) > 50 else (msg.text or f"[{msg.message_type}]")
text += f"{sender_name}: {msg_text}\n" text += f"{sender_name}: {msg_text}\n"
text += "\n" text += "\n"
@@ -204,7 +237,7 @@ async def show_conversations(callback: CallbackQuery):
last_name=callback.from_user.last_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, sender.id, limit=10)
if not conversations: if not conversations:
await callback.message.edit_text( await callback.message.edit_text(
@@ -217,7 +250,7 @@ async def show_conversations(callback: CallbackQuery):
buttons = [] buttons = []
for peer, last_msg, unread in conversations: for peer, last_msg, unread in conversations:
peer_name = f"@{peer.username}" if peer.username else peer.first_name peer_name = peer.nickname or f"@{peer.username}" or peer.first_name or "Unknown"
# Иконка в зависимости от непрочитанных # Иконка в зависимости от непрочитанных
icon = "🔴" if unread > 0 else "💬" icon = "🔴" if unread > 0 else "💬"
@@ -234,7 +267,11 @@ async def show_conversations(callback: CallbackQuery):
callback_data=f"p2p:user:{peer.id}" callback_data=f"p2p:user:{peer.id}"
)]) )])
text += f"{icon} <b>{peer_name}</b>\n" text += f"{icon} <b>{peer_name}</b>"
# Показываем номер карты если есть
if peer.club_card_number:
text += f" (карта: {peer.club_card_number})"
text += "\n"
text += f" {preview}\n" text += f" {preview}\n"
if unread > 0: if unread > 0:
text += f" 📨 Непрочитанных: {unread}\n" text += f" 📨 Непрочитанных: {unread}\n"
@@ -268,12 +305,53 @@ async def end_conversation(callback: CallbackQuery, state: FSMContext):
async def back_to_menu(callback: CallbackQuery, state: FSMContext): async def back_to_menu(callback: CallbackQuery, state: FSMContext):
"""Вернуться в главное меню""" """Вернуться в главное меню"""
await callback.answer() await callback.answer()
await state.clear()
# Имитируем команду /chat async with async_session_maker() as session:
fake_message = callback.message user = await UserService.get_or_create_user(
fake_message.from_user = callback.from_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
)
await show_chat_menu(fake_message, state) if not user:
await callback.message.edit_text("❌ Вы не зарегистрированы. Используйте /start")
return
# Получаем количество непрочитанных сообщений
unread_count = await P2PMessageService.get_unread_count(session, user.id)
# Получаем последние диалоги
recent = await P2PMessageService.get_recent_conversations(session, user.id, limit=5)
text = "💬 <b>Чат</b>\n\n"
if unread_count > 0:
text += f"📨 У вас <b>{unread_count}</b> непрочитанных сообщений\n\n"
text += "Выберите действие:"
buttons = [
[InlineKeyboardButton(
text="✉️ Написать пользователю",
callback_data="p2p:select_user"
)],
[InlineKeyboardButton(
text="📋 Мои диалоги",
callback_data="p2p:my_conversations"
)]
]
if recent:
text += "\n\n<b>Последние диалоги:</b>\n"
for peer, last_msg, unread in recent:
unread_badge = f" ({unread})" if unread > 0 else ""
text += f" • @{peer.username or peer.first_name}{unread_badge}\n"
kb = InlineKeyboardMarkup(inline_keyboard=buttons)
await callback.message.edit_text(text, reply_markup=kb, parse_mode="HTML")
# Обработчик сообщений в состоянии chatting # Обработчик сообщений в состоянии chatting
@@ -301,7 +379,19 @@ async def handle_p2p_message(message: Message, state: FSMContext):
first_name=message.from_user.first_name, first_name=message.from_user.first_name,
last_name=message.from_user.last_name last_name=message.from_user.last_name
) )
sender_name = f"@{sender.username}" if sender.username else sender.first_name
# Получаем информацию о получателе для определения как подписать сообщение
recipient = await UserService.get_user_by_telegram_id(session, recipient_telegram_id)
# Формируем подпись сообщения для получателя
if sender.is_admin:
sender_name = "АДМИН"
else:
sender_name = sender.nickname or f"@{sender.username}" or sender.first_name or "Unknown"
# Добавляем карту если получатель админ
if recipient and recipient.is_admin and sender.club_card_number:
sender_name += f" (карта: {sender.club_card_number})"
# Определяем тип сообщения # Определяем тип сообщения
message_type = "text" message_type = "text"
@@ -326,28 +416,28 @@ async def handle_p2p_message(message: Message, state: FSMContext):
if message_type == "text": if message_type == "text":
sent = await message.bot.send_message( sent = await message.bot.send_message(
recipient_telegram_id, recipient_telegram_id,
f"💬 <b>Сообщение от {sender_name}:</b>\n\n{text}", f"<b>{sender_name}</b>\n\n{text}",
parse_mode="HTML" parse_mode="HTML"
) )
elif message_type == "photo": elif message_type == "photo":
sent = await message.bot.send_photo( sent = await message.bot.send_photo(
recipient_telegram_id, recipient_telegram_id,
photo=file_id, photo=file_id,
caption=f"💬 <b>Фото от {sender_name}</b>\n\n{text or ''}" , caption=f"<b>{sender_name}</b>\n\n{text or ''}" ,
parse_mode="HTML" parse_mode="HTML"
) )
elif message_type == "video": elif message_type == "video":
sent = await message.bot.send_video( sent = await message.bot.send_video(
recipient_telegram_id, recipient_telegram_id,
video=file_id, video=file_id,
caption=f"💬 <b>Видео от {sender_name}</b>\n\n{text or ''}", caption=f"<b>{sender_name}</b>\n\n{text or ''}",
parse_mode="HTML" parse_mode="HTML"
) )
elif message_type == "document": elif message_type == "document":
sent = await message.bot.send_document( sent = await message.bot.send_document(
recipient_telegram_id, recipient_telegram_id,
document=file_id, document=file_id,
caption=f"💬 <b>Документ от {sender_name}</b>\n\n{text or ''}", caption=f"<b>{sender_name}</b>\n\n{text or ''}",
parse_mode="HTML" parse_mode="HTML"
) )

View File

@@ -2,6 +2,8 @@
from aiogram import Router, F from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from src.filters.case_insensitive import CaseInsensitiveCommand from src.filters.case_insensitive import CaseInsensitiveCommand
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -11,6 +13,7 @@ import logging
from src.core.database import async_session_maker from src.core.database import async_session_maker
from src.core.registration_services import RegistrationService, AccountService from src.core.registration_services import RegistrationService, AccountService
from src.core.services import UserService from src.core.services import UserService
from src.core.models import Participation, Winner, Lottery
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = Router() router = Router()
@@ -143,7 +146,20 @@ async def process_club_card(message: Message, state: FSMContext):
@router.message(StateFilter(RegistrationStates.waiting_for_phone)) @router.message(StateFilter(RegistrationStates.waiting_for_phone))
async def process_phone(message: Message, state: FSMContext): async def process_phone(message: Message, state: FSMContext):
"""Обработка номера телефона""" """Обработка номера телефона"""
phone = None if message.text.strip() == "-" else message.text.strip() phone_input = message.text.strip()
# Проверяем, не отправил ли пользователь просто "-"
if phone_input == "-":
phone = None
else:
# Валидируем телефон: не должно быть пустых или некорректных значений
if not phone_input:
await message.answer(
"❌ Неверный номер телефона.\n\n"
"Пожалуйста, введите корректный номер или отправьте '-' чтобы пропустить."
)
return
phone = phone_input
data = await state.get_data() data = await state.get_data()
club_card_number = data['club_card_number'] club_card_number = data['club_card_number']
@@ -168,12 +184,12 @@ async def process_phone(message: Message, state: FSMContext):
"✅ Регистрация завершена!\n\n" "✅ Регистрация завершена!\n\n"
f"🎭 Никнейм: {user.nickname}\n" f"🎭 Никнейм: {user.nickname}\n"
f"🎫 Клубная карта: {user.club_card_number}\n" f"🎫 Клубная карта: {user.club_card_number}\n"
f"🔑 Ваш код верификации: **{user.verification_code}**\n\n" f"🔑 Ваш код верификации: <b>{user.verification_code}</b>\n\n"
"⚠️ Сохраните этот код! Он понадобится для подтверждения выигрыша.\n\n" "⚠️ Сохраните этот код! Он понадобится для подтверждения выигрыша.\n\n"
"Теперь вы можете участвовать в розыгрышах!" "Теперь вы можете участвовать в розыгрышах!"
) )
await message.answer(text, parse_mode="Markdown") await message.answer(text, parse_mode="HTML")
await state.clear() await state.clear()
except ValueError as e: except ValueError as e:
@@ -199,17 +215,17 @@ async def show_verification_code(message: Message):
text = ( text = (
"🔑 Ваш код верификации:\n\n" "🔑 Ваш код верификации:\n\n"
f"**{user.verification_code}**\n\n" f"<code>{user.verification_code}</code>\n\n"
"Этот код используется для подтверждения выигрыша.\n" "Этот код используется для подтверждения выигрыша.\n"
"Сообщите его администратору при получении приза." "Сообщите его администратору при получении приза."
) )
await message.answer(text, parse_mode="Markdown") await message.answer(text, parse_mode="HTML")
@router.message(Command("my_accounts")) @router.message(Command("my_accounts"))
async def show_user_accounts(message: Message): async def show_user_accounts(message: Message):
"""Показать счета пользователя""" """Показать логины пользователя с информацией о розыгрышах"""
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_user_by_telegram_id(session, message.from_user.id)
@@ -221,15 +237,69 @@ async def show_user_accounts(message: Message):
if not accounts: if not accounts:
await message.answer( await message.answer(
"У вас пока нет привязанных счетов.\n\n" "У вас пока нет привязанных логинов.\n\n"
"Счета добавляются администратором." "Логины добавляются администратором."
) )
return return
text = f"💳 Ваши счета (Клубная карта: {user.club_card_number}):\n\n" text = f"📱 <b>Ваши логины</b> (Клубная карта: {user.club_card_number})\n\n"
for i, account in enumerate(accounts, 1): for i, account in enumerate(accounts, 1):
status = "" if account.is_active else "" # Получаем participations для этого account с загруженными данными о lottery
text += f"{i}. {status} {account.account_number}\n" participations = await session.execute(
select(Participation)
.where(Participation.account_id == account.id)
.options(selectinload(Participation.lottery))
)
participations = participations.scalars().all()
await message.answer(text) # Определяем статус логина
active_participations = [p for p in participations if not p.lottery.is_completed]
closed_participations = [p for p in participations if p.lottery.is_completed]
# Основная информация о логине
status_icon = "" if account.is_active and active_participations else "⏸️"
text += f"{i}. {status_icon} <b>{account.account_number}</b>\n"
if active_participations:
text += " 🎲 <b>Активные розыгрыши:</b>\n"
for p in active_participations[:5]: # Показываем не более 5
status = "🟢"
text += f" {status} {p.lottery.title}\n"
if len(active_participations) > 5:
text += f" ... и еще {len(active_participations) - 5}\n"
if closed_participations:
text += " 🏁 <b>Завершенные розыгрыши:</b>\n"
for p in closed_participations[:3]: # Показываем не более 3
# Проверяем, выиграл ли в этом розыгрыше
winner_result = await session.execute(
select(Winner)
.where(
(Winner.lottery_id == p.lottery_id) &
(Winner.account_number == account.account_number)
)
)
winner = winner_result.scalar_one_or_none()
if winner:
text += f" 🏆 {p.lottery.title} - <b>ВЫИГРАЛ!</b> ({winner.place} место)\n"
else:
text += f"{p.lottery.title}\n"
if len(closed_participations) > 3:
text += f" ... и еще {len(closed_participations) - 3} закрытых\n"
if not participations:
text += " В розыгрышах не участвовал\n"
text += "\n"
# Добавляем примечание о неактивных логинах
if any(not acc.is_active for acc in accounts):
text += "⏸️ - Логин не участвует в новых розыгрышах\n"
text += "✅ - Логин активен и может участвовать\n\n"
text += "💡 <b>Заметка:</b> Логины, участвовавшие в закрытых розыгрышах, не добавляются в новые розыгрыши."
await message.answer(text, parse_mode="HTML")

View File

@@ -17,8 +17,8 @@ def get_main_reply_keyboard(is_admin: bool = False, is_registered: bool = False)
# Первая строка - основные команды # Первая строка - основные команды
row1 = [ row1 = [
KeyboardButton(text="🎰 Розыгрыши"), KeyboardButton(text="💬 Чат"),
KeyboardButton(text="💬 Чат") KeyboardButton(text="❓ Справка")
] ]
keyboard.append(row1) keyboard.append(row1)
@@ -29,13 +29,13 @@ def get_main_reply_keyboard(is_admin: bool = False, is_registered: bool = False)
if is_registered or is_admin: if is_registered or is_admin:
row2.append(KeyboardButton(text="🔑 Мой код")) row2.append(KeyboardButton(text="🔑 Мой код"))
row2.append(KeyboardButton(text="💳 Мои счета")) row2.append(KeyboardButton(text="📱 Мои логины"))
if row2: if row2:
keyboard.append(row2) keyboard.append(row2)
# Третья строка - справка # Третья строка - главная
row3 = [KeyboardButton(text="❓ Справка")] row3 = [KeyboardButton(text="🏠 Главная")]
# Админские команды # Админские команды
if is_admin: if is_admin:
@@ -59,7 +59,7 @@ def get_chat_reply_keyboard() -> ReplyKeyboardMarkup:
""" """
keyboard = [ keyboard = [
[KeyboardButton(text="🚪 Выйти из чата")], [KeyboardButton(text="🚪 Выйти из чата")],
[KeyboardButton(text="🏠 Главное меню")] [KeyboardButton(text="🏠 Главная")]
] ]
return ReplyKeyboardMarkup( return ReplyKeyboardMarkup(