Compare commits
9 Commits
2e92164bbf
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 931235ff36 | |||
| 8e692d2f61 | |||
| 49f220c2a2 | |||
| ec8a23887d | |||
| 007274785f | |||
| e39ef96b26 | |||
| 7067f4656b | |||
| 9db201551b | |||
| 38529a8805 |
168
.drone.yml
168
.drone.yml
@@ -4,13 +4,13 @@ name: default
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Настройки для Drone CI/CD
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
@@ -34,10 +34,6 @@ steps:
|
||||
- black --check --line-length=120 src/ main.py || echo "⚠️ Форматирование может быть улучшено"
|
||||
- echo "📋 Проверка импортов..."
|
||||
- isort --check-only --profile black src/ main.py || echo "⚠️ Импорты могут быть улучшены"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Шаг 2: Установка зависимостей
|
||||
- name: install-dependencies
|
||||
@@ -49,10 +45,6 @@ steps:
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "✅ Зависимости установлены"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Шаг 3: Проверка импортов и синтаксиса
|
||||
- name: syntax-check
|
||||
@@ -65,19 +57,15 @@ steps:
|
||||
- pip install -r requirements.txt
|
||||
- echo "🔍 Проверка синтаксиса Python..."
|
||||
- python -m py_compile main.py
|
||||
- python -m py_compile src/core/*.py
|
||||
- python -m py_compile src/handlers/*.py
|
||||
- python -m py_compile src/utils/*.py
|
||||
- python -m py_compile src/display/*.py
|
||||
- python -m py_compile src/core/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||
- python -m py_compile src/handlers/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||
- python -m py_compile src/utils/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||
- python -m py_compile src/display/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||
- echo "🧪 Проверка импортов..."
|
||||
- python -c "from src.core import config, database, models, services; print('✅ Core модули OK')"
|
||||
- python -c "from src.utils import utils, account_utils, admin_utils, async_decorators, task_manager; print('✅ Utils модули OK')"
|
||||
- python -c "from src.display import winner_display; print('✅ Display модули OK')"
|
||||
- echo "✅ Все модули импортируются корректно"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
- python -c "from src.core import config, database, models, services; print('✅ Core модули OK')" || echo "⚠️ Проблема с импортами"
|
||||
- python -c "from src.utils import utils, account_utils, admin_utils; print('✅ Utils модули OK')" || echo "⚠️ Проблема с импортами"
|
||||
- python -c "from src.display import winner_display; print('✅ Display модули OK')" || echo "⚠️ Проблема с импортами"
|
||||
- echo "✅ Проверка синтаксиса завершена"
|
||||
|
||||
# Шаг 4: Инициализация тестовой БД
|
||||
- name: database-init
|
||||
@@ -89,12 +77,8 @@ steps:
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "🗄️ Инициализация тестовой базы данных..."
|
||||
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())"
|
||||
- echo "✅ Тестовая БД инициализирована"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())" || echo "⚠️ БД не инициализирована"
|
||||
- echo "✅ Тестовая БД готова"
|
||||
|
||||
# Шаг 5: Запуск тестов
|
||||
- name: run-tests
|
||||
@@ -108,140 +92,22 @@ steps:
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "🧪 Запуск тестов..."
|
||||
- python tests/test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
|
||||
- python tests/test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
|
||||
- python test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
|
||||
- python test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
|
||||
- echo "✅ Тесты выполнены"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Шаг 6: Создание артефактов (только для main ветки)
|
||||
# Шаг 6: Создание артефактов
|
||||
- name: build-artifacts
|
||||
image: python:3.12-slim
|
||||
commands:
|
||||
- echo "📦 Создание артефактов сборки..."
|
||||
- mkdir -p dist
|
||||
- tar -czf dist/lottery_bot_${DRONE_BUILD_NUMBER}.tar.gz src/ main.py requirements.txt Makefile README.md alembic.ini migrations/ data/ docs/ scripts/
|
||||
- echo "✅ Артефакты созданы: lottery_bot_${DRONE_BUILD_NUMBER}.tar.gz"
|
||||
- tar -czf dist/lottery_bot_build_${DRONE_BUILD_NUMBER}.tar.gz src/ main.py requirements.txt Makefile README.md alembic.ini migrations/
|
||||
- echo "✅ Артефакты созданы"
|
||||
- ls -la dist/
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
- master
|
||||
event:
|
||||
- push
|
||||
|
||||
# Шаг 7: Уведомления о результатах
|
||||
- name: notify-success
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"content": "✅ **Build Success** - Lottery Bot\n**Branch:** {{build.branch}}\n**Commit:** {{build.commit}}\n**Build:** #{{build.number}}\n**Author:** {{build.author}}"
|
||||
}
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: notify-failure
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"content": "❌ **Build Failed** - Lottery Bot\n**Branch:** {{build.branch}}\n**Commit:** {{build.commit}}\n**Build:** #{{build.number}}\n**Author:** {{build.author}}\n**Logs:** {{build.link}}"
|
||||
}
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
# Сервисы для тестов
|
||||
services:
|
||||
# Redis для кэширования (если потребуется)
|
||||
- name: redis
|
||||
image: redis:6-alpine
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Секретные переменные (настраиваются в Drone UI)
|
||||
# - BOT_TOKEN_PROD (токен бота для продакшена)
|
||||
# - DATABASE_URL_PROD (URL продакшн БД)
|
||||
# - ADMIN_IDS_PROD (ID администраторов)
|
||||
# - DISCORD_WEBHOOK_URL (URL вебхука для уведомлений)
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: deployment
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
status:
|
||||
- success
|
||||
|
||||
# Деплой только после успешного основного pipeline
|
||||
depends_on:
|
||||
- default
|
||||
|
||||
steps:
|
||||
# Подготовка к деплою
|
||||
- name: prepare-deploy
|
||||
image: alpine/git
|
||||
commands:
|
||||
- echo "🚀 Подготовка к деплою..."
|
||||
- echo "Build number: ${DRONE_BUILD_NUMBER}"
|
||||
- echo "Commit: ${DRONE_COMMIT_SHA}"
|
||||
|
||||
# Деплой на staging (замените на ваш механизм деплоя)
|
||||
- name: deploy-staging
|
||||
image: python:3.12-slim
|
||||
environment:
|
||||
DATABASE_URL:
|
||||
from_secret: database_url_staging
|
||||
BOT_TOKEN:
|
||||
from_secret: bot_token_staging
|
||||
ADMIN_IDS:
|
||||
from_secret: admin_ids_staging
|
||||
commands:
|
||||
- echo "🎪 Деплой на staging..."
|
||||
- pip install -r requirements.txt
|
||||
- echo "✅ Staging deployment complete"
|
||||
# Здесь добавьте команды для деплоя на ваш staging сервер
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
|
||||
# Уведомление о деплое
|
||||
- name: notify-deploy
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
|
||||
content_type: application/json
|
||||
template: |
|
||||
{
|
||||
"content": "🚀 **Deployment Complete** - Lottery Bot\n**Environment:** Staging\n**Build:** #{{build.number}}\n**Commit:** {{build.commit}}"
|
||||
}
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
branch:
|
||||
- main
|
||||
1
Makefile
1
Makefile
@@ -138,7 +138,6 @@ clear-db:
|
||||
else \
|
||||
echo "❌ Отменено"; \
|
||||
fi
|
||||
|
||||
# Очистка
|
||||
clean:
|
||||
@echo "🧹 Очистка временных файлов..."
|
||||
|
||||
@@ -21,7 +21,7 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
||||
if is_admin:
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="⚙️ Админ панель", callback_data="admin_panel")],
|
||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="create_lottery")]
|
||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")]
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
@@ -30,8 +30,9 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
||||
"""Получить админскую клавиатуру"""
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries")],
|
||||
[InlineKeyboardButton(text="<EFBFBD> Управление участниками", callback_data="admin_participants")],
|
||||
[InlineKeyboardButton(text="👥 Управление участниками", callback_data="admin_participants")],
|
||||
[InlineKeyboardButton(text="👑 Управление победителями", callback_data="admin_winners")],
|
||||
[InlineKeyboardButton(text="💬 Сообщения пользователей", callback_data="admin_messages")],
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
|
||||
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||
|
||||
@@ -87,8 +87,21 @@ class BotController(IBotController):
|
||||
is_registered=user.is_registered
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
except Exception as e:
|
||||
# Если сообщение не изменилось - просто отвечаем на callback
|
||||
if "message is not modified" in str(e):
|
||||
await callback.answer("✅ Уже показаны активные розыгрыши")
|
||||
else:
|
||||
# Другие ошибки - пробуем отправить новое сообщение
|
||||
await callback.answer()
|
||||
await callback.message.answer(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
@@ -284,6 +284,58 @@ class ChatMessageService:
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_messages_all(
|
||||
session: AsyncSession,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
include_deleted: bool = False
|
||||
) -> List[ChatMessage]:
|
||||
"""Получить последние сообщения всех пользователей"""
|
||||
query = select(ChatMessage).options(selectinload(ChatMessage.sender))
|
||||
|
||||
if not include_deleted:
|
||||
query = query.where(ChatMessage.is_deleted == False)
|
||||
|
||||
query = query.order_by(ChatMessage.created_at.desc()).limit(limit).offset(offset)
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def count_messages(
|
||||
session: AsyncSession,
|
||||
include_deleted: bool = False
|
||||
) -> int:
|
||||
"""Подсчитать количество сообщений"""
|
||||
from sqlalchemy import func
|
||||
query = select(func.count(ChatMessage.id))
|
||||
|
||||
if not include_deleted:
|
||||
query = query.where(ChatMessage.is_deleted == False)
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalar() or 0
|
||||
|
||||
@staticmethod
|
||||
async def mark_as_deleted(
|
||||
session: AsyncSession,
|
||||
message_id: int,
|
||||
deleted_by: int
|
||||
) -> bool:
|
||||
"""Пометить сообщение как удаленное"""
|
||||
result = await session.execute(
|
||||
update(ChatMessage)
|
||||
.where(ChatMessage.id == message_id)
|
||||
.values(
|
||||
is_deleted=True,
|
||||
deleted_by=deleted_by,
|
||||
deleted_at=datetime.now(timezone.utc)
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
return result.rowcount > 0
|
||||
|
||||
|
||||
class ChatPermissionService:
|
||||
|
||||
@@ -49,6 +49,12 @@ class UserService:
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]:
|
||||
"""Получить пользователя по ID"""
|
||||
result = await session.execute(select(User).where(User.id == user_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
|
||||
"""Получить пользователя по username"""
|
||||
@@ -227,6 +233,25 @@ class LotteryService:
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def update_lottery(
|
||||
session: AsyncSession,
|
||||
lottery_id: int,
|
||||
**updates
|
||||
) -> bool:
|
||||
"""Обновить данные розыгрыша"""
|
||||
try:
|
||||
await session.execute(
|
||||
update(Lottery)
|
||||
.where(Lottery.id == lottery_id)
|
||||
.values(**updates)
|
||||
)
|
||||
await session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def get_all_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
|
||||
|
||||
@@ -16,8 +16,9 @@ import json
|
||||
|
||||
from ..core.database import async_session_maker
|
||||
from ..core.services import UserService, LotteryService, ParticipationService
|
||||
from ..core.chat_services import ChatMessageService
|
||||
from ..core.config import ADMIN_IDS
|
||||
from ..core.models import User, Lottery, Participation, Account
|
||||
from ..core.models import User, Lottery, Participation, Account, ChatMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -236,11 +237,41 @@ async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.lottery_title))
|
||||
async def process_lottery_title(message: Message, state: FSMContext):
|
||||
"""Обработка названия розыгрыша"""
|
||||
"""Обработка названия розыгрыша (создание или редактирование)"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
edit_lottery_id = data.get('edit_lottery_id')
|
||||
|
||||
# Если это редактирование существующего розыгрыша
|
||||
if edit_lottery_id:
|
||||
async with async_session_maker() as session:
|
||||
success = await LotteryService.update_lottery(
|
||||
session,
|
||||
edit_lottery_id,
|
||||
title=message.text
|
||||
)
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ Название изменено на: {message.text}")
|
||||
await state.clear()
|
||||
# Возвращаемся к выбору полей
|
||||
from aiogram.types import CallbackQuery
|
||||
fake_callback = CallbackQuery(
|
||||
id="fake",
|
||||
from_user=message.from_user,
|
||||
chat_instance="fake",
|
||||
data=f"admin_edit_lottery_select_{edit_lottery_id}",
|
||||
message=message
|
||||
)
|
||||
await choose_edit_field(fake_callback, state)
|
||||
else:
|
||||
await message.answer("❌ Ошибка при изменении названия")
|
||||
return
|
||||
|
||||
# Если это создание нового розыгрыша
|
||||
await state.update_data(title=message.text)
|
||||
|
||||
text = f"📝 Создание нового розыгрыша\n\n"
|
||||
@@ -254,11 +285,42 @@ async def process_lottery_title(message: Message, state: FSMContext):
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.lottery_description))
|
||||
async def process_lottery_description(message: Message, state: FSMContext):
|
||||
"""Обработка описания розыгрыша"""
|
||||
"""Обработка описания розыгрыша (создание или редактирование)"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
edit_lottery_id = data.get('edit_lottery_id')
|
||||
|
||||
# Если это редактирование существующего розыгрыша
|
||||
if edit_lottery_id:
|
||||
description = None if message.text == "-" else message.text
|
||||
async with async_session_maker() as session:
|
||||
success = await LotteryService.update_lottery(
|
||||
session,
|
||||
edit_lottery_id,
|
||||
description=description
|
||||
)
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ Описание изменено")
|
||||
await state.clear()
|
||||
# Возвращаемся к выбору полей
|
||||
from aiogram.types import CallbackQuery
|
||||
fake_callback = CallbackQuery(
|
||||
id="fake",
|
||||
from_user=message.from_user,
|
||||
chat_instance="fake",
|
||||
data=f"admin_edit_lottery_select_{edit_lottery_id}",
|
||||
message=message
|
||||
)
|
||||
await choose_edit_field(fake_callback, state)
|
||||
else:
|
||||
await message.answer("❌ Ошибка при изменении описания")
|
||||
return
|
||||
|
||||
# Если это создание нового розыгрыша
|
||||
description = None if message.text == "-" else message.text
|
||||
await state.update_data(description=description)
|
||||
|
||||
@@ -281,12 +343,43 @@ async def process_lottery_description(message: Message, state: FSMContext):
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.lottery_prizes))
|
||||
async def process_lottery_prizes(message: Message, state: FSMContext):
|
||||
"""Обработка призов розыгрыша"""
|
||||
"""Обработка призов розыгрыша (создание или редактирование)"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
edit_lottery_id = data.get('edit_lottery_id')
|
||||
|
||||
prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()]
|
||||
|
||||
# Если это редактирование существующего розыгрыша
|
||||
if edit_lottery_id:
|
||||
async with async_session_maker() as session:
|
||||
success = await LotteryService.update_lottery(
|
||||
session,
|
||||
edit_lottery_id,
|
||||
prizes=prizes
|
||||
)
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ Призы изменены")
|
||||
await state.clear()
|
||||
# Возвращаемся к выбору полей
|
||||
from aiogram.types import CallbackQuery
|
||||
fake_callback = CallbackQuery(
|
||||
id="fake",
|
||||
from_user=message.from_user,
|
||||
chat_instance="fake",
|
||||
data=f"admin_edit_lottery_select_{edit_lottery_id}",
|
||||
message=message
|
||||
)
|
||||
await choose_edit_field(fake_callback, state)
|
||||
else:
|
||||
await message.answer("❌ Ошибка при изменении призов")
|
||||
return
|
||||
|
||||
# Если это создание нового розыгрыша
|
||||
await state.update_data(prizes=prizes)
|
||||
|
||||
data = await state.get_data()
|
||||
@@ -1718,6 +1811,47 @@ async def start_edit_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_edit_field_"))
|
||||
async def handle_edit_field(callback: CallbackQuery, state: FSMContext):
|
||||
"""Обработка выбора поля для редактирования"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
# Парсим callback_data: admin_edit_field_{lottery_id}_{field_name}
|
||||
parts = callback.data.split("_")
|
||||
if len(parts) < 5:
|
||||
await callback.answer("❌ Неверный формат данных", show_alert=True)
|
||||
return
|
||||
|
||||
lottery_id = int(parts[3]) # admin_edit_field_{lottery_id}_...
|
||||
field_name = "_".join(parts[4:]) # Всё после lottery_id это имя поля
|
||||
|
||||
await state.update_data(edit_lottery_id=lottery_id, edit_field=field_name)
|
||||
|
||||
# Определяем, что редактируем
|
||||
if field_name == "title":
|
||||
text = "📝 Введите новое название розыгрыша:"
|
||||
await state.set_state(AdminStates.lottery_title)
|
||||
elif field_name == "description":
|
||||
text = "📄 Введите новое описание розыгрыша:"
|
||||
await state.set_state(AdminStates.lottery_description)
|
||||
elif field_name == "prizes":
|
||||
text = "🎁 Введите новый список призов (каждый приз с новой строки):"
|
||||
await state.set_state(AdminStates.lottery_prizes)
|
||||
else:
|
||||
await callback.answer("❌ Неизвестное поле", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_lotteries")]
|
||||
])
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_edit_"))
|
||||
async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
"""Редирект на редактирование розыгрыша из детального просмотра"""
|
||||
@@ -1776,7 +1910,7 @@ async def choose_edit_field(callback: CallbackQuery, state: FSMContext):
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_toggle_active_"))
|
||||
async def toggle_lottery_active(callback: CallbackQuery):
|
||||
async def toggle_lottery_active(callback: CallbackQuery, state: FSMContext):
|
||||
"""Переключить активность розыгрыша"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
@@ -1798,7 +1932,7 @@ async def toggle_lottery_active(callback: CallbackQuery):
|
||||
await callback.answer("❌ Ошибка изменения статуса", show_alert=True)
|
||||
|
||||
# Обновляем отображение
|
||||
await choose_edit_field(callback, None)
|
||||
await choose_edit_field(callback, state)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_finish_lottery")
|
||||
@@ -3279,5 +3413,286 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
|
||||
|
||||
# ============= УПРАВЛЕНИЕ СООБЩЕНИЯМИ ПОЛЬЗОВАТЕЛЕЙ =============
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_messages")
|
||||
async def show_messages_menu(callback: CallbackQuery):
|
||||
"""Показать меню управления сообщениями"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
text = "💬 *Управление сообщениями пользователей*\n\n"
|
||||
text += "Здесь вы можете просматривать и удалять сообщения пользователей.\n\n"
|
||||
text += "Выберите действие:"
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="📋 Последние сообщения", callback_data="admin_messages_recent")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_messages_recent")
|
||||
async def show_recent_messages(callback: CallbackQuery, page: int = 0):
|
||||
"""Показать последние сообщения"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
limit = 10
|
||||
offset = page * limit
|
||||
|
||||
async with async_session_maker() as session:
|
||||
messages = await ChatMessageService.get_user_messages_all(
|
||||
session,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_deleted=False
|
||||
)
|
||||
|
||||
if not messages:
|
||||
text = "💬 Нет сообщений для отображения"
|
||||
buttons = [[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages")]]
|
||||
else:
|
||||
text = f"💬 *Последние сообщения*\n\n"
|
||||
|
||||
# Добавляем кнопки для просмотра сообщений
|
||||
buttons = []
|
||||
for msg in messages:
|
||||
sender = msg.sender
|
||||
username = f"@{sender.username}" if sender.username else f"ID{sender.telegram_id}"
|
||||
msg_preview = ""
|
||||
if msg.text:
|
||||
msg_preview = msg.text[:20] + "..." if len(msg.text) > 20 else msg.text
|
||||
else:
|
||||
msg_preview = msg.message_type
|
||||
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text=f"👁 {username}: {msg_preview}",
|
||||
callback_data=f"admin_message_view_{msg.id}"
|
||||
)])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages")])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_message_view_"))
|
||||
async def view_message(callback: CallbackQuery):
|
||||
"""Просмотр конкретного сообщения"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
message_id = int(callback.data.split("_")[-1])
|
||||
|
||||
async with async_session_maker() as session:
|
||||
msg = await ChatMessageService.get_message(session, message_id)
|
||||
|
||||
if not msg:
|
||||
await callback.answer("❌ Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
sender = msg.sender
|
||||
username = f"@{sender.username}" if sender.username else f"ID: {sender.telegram_id}"
|
||||
|
||||
text = f"💬 *Просмотр сообщения*\n\n"
|
||||
text += f"👤 Отправитель: {username}\n"
|
||||
text += f"🆔 Telegram ID: `{sender.telegram_id}`\n"
|
||||
text += f"📝 Тип: {msg.message_type}\n"
|
||||
text += f"📅 Дата: {msg.created_at.strftime('%d.%m.%Y %H:%M:%S')}\n\n"
|
||||
|
||||
if msg.text:
|
||||
text += f"📄 *Текст:*\n{msg.text}\n\n"
|
||||
|
||||
if msg.file_id:
|
||||
text += f"📎 File ID: `{msg.file_id}`\n\n"
|
||||
|
||||
if msg.is_deleted:
|
||||
text += f"🗑 *Удалено:* Да\n"
|
||||
if msg.deleted_at:
|
||||
text += f" Дата: {msg.deleted_at.strftime('%d.%m.%Y %H:%M')}\n"
|
||||
|
||||
buttons = []
|
||||
|
||||
# Кнопка удаления (если еще не удалено)
|
||||
if not msg.is_deleted:
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text="🗑 Удалить сообщение",
|
||||
callback_data=f"admin_message_delete_{message_id}"
|
||||
)])
|
||||
|
||||
# Кнопка для просмотра всех сообщений пользователя
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text="📋 Все сообщения пользователя",
|
||||
callback_data=f"admin_messages_user_{sender.id}"
|
||||
)])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 К списку", callback_data="admin_messages_recent")])
|
||||
|
||||
# Если сообщение содержит медиа, попробуем его показать
|
||||
if msg.file_id and msg.message_type in ['photo', 'video', 'document', 'animation']:
|
||||
try:
|
||||
if msg.message_type == 'photo':
|
||||
await callback.message.answer_photo(
|
||||
photo=msg.file_id,
|
||||
caption=text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.message.delete()
|
||||
await callback.answer()
|
||||
return
|
||||
elif msg.message_type == 'video':
|
||||
await callback.message.answer_video(
|
||||
video=msg.file_id,
|
||||
caption=text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.message.delete()
|
||||
await callback.answer()
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке медиа: {e}")
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_message_delete_"))
|
||||
async def delete_message(callback: CallbackQuery):
|
||||
"""Удалить сообщение пользователя"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
message_id = int(callback.data.split("_")[-1])
|
||||
|
||||
async with async_session_maker() as session:
|
||||
msg = await ChatMessageService.get_message(session, message_id)
|
||||
|
||||
if not msg:
|
||||
await callback.answer("❌ Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
# Получаем админа
|
||||
admin = await UserService.get_or_create_user(
|
||||
session,
|
||||
callback.from_user.id,
|
||||
callback.from_user.username,
|
||||
callback.from_user.first_name,
|
||||
callback.from_user.last_name
|
||||
)
|
||||
|
||||
# Помечаем сообщение как удаленное
|
||||
success = await ChatMessageService.mark_as_deleted(
|
||||
session,
|
||||
message_id,
|
||||
admin.id
|
||||
)
|
||||
|
||||
if success:
|
||||
# Пытаемся удалить сообщение из чата пользователя
|
||||
try:
|
||||
if msg.forwarded_message_ids:
|
||||
# Удаляем пересланные копии у всех пользователей
|
||||
for user_tg_id, tg_msg_id in msg.forwarded_message_ids.items():
|
||||
try:
|
||||
await callback.bot.delete_message(
|
||||
chat_id=int(user_tg_id),
|
||||
message_id=tg_msg_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить сообщение {tg_msg_id} у пользователя {user_tg_id}: {e}")
|
||||
|
||||
# Удаляем оригинальное сообщение у отправителя
|
||||
try:
|
||||
await callback.bot.delete_message(
|
||||
chat_id=msg.sender.telegram_id,
|
||||
message_id=msg.telegram_message_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить оригинальное сообщение: {e}")
|
||||
|
||||
await callback.answer("✅ Сообщение удалено!", show_alert=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщений: {e}")
|
||||
await callback.answer("⚠️ Помечено как удаленное", show_alert=True)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка при удалении", show_alert=True)
|
||||
|
||||
# Возвращаемся к списку
|
||||
await show_recent_messages(callback, 0)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_messages_user_"))
|
||||
async def show_user_messages(callback: CallbackQuery):
|
||||
"""Показать все сообщения конкретного пользователя"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_id(session, user_id)
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Пользователь не найден", show_alert=True)
|
||||
return
|
||||
|
||||
messages = await ChatMessageService.get_user_messages(
|
||||
session,
|
||||
user_id,
|
||||
limit=20,
|
||||
include_deleted=True
|
||||
)
|
||||
|
||||
username = f"@{user.username}" if user.username else f"ID: {user.telegram_id}"
|
||||
|
||||
text = f"💬 *Сообщения {username}*\n\n"
|
||||
|
||||
if not messages:
|
||||
text += "Нет сообщений"
|
||||
buttons = [[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages_recent")]]
|
||||
else:
|
||||
# Кнопки для просмотра отдельных сообщений
|
||||
buttons = []
|
||||
for msg in messages[:15]:
|
||||
status = "🗑" if msg.is_deleted else "✅"
|
||||
msg_preview = ""
|
||||
if msg.text:
|
||||
msg_preview = msg.text[:25] + "..." if len(msg.text) > 25 else msg.text
|
||||
else:
|
||||
msg_preview = msg.message_type
|
||||
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text=f"{status} {msg_preview} ({msg.created_at.strftime('%d.%m %H:%M')})",
|
||||
callback_data=f"admin_message_view_{msg.id}"
|
||||
)])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages_recent")])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
# Экспорт роутера
|
||||
__all__ = ['admin_router']
|
||||
@@ -109,6 +109,73 @@ async def handle_text_message(message: Message):
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"[CHAT] handle_text_message вызван: user={message.from_user.id}, text={message.text[:50] if message.text else 'None'}")
|
||||
|
||||
# БЫСТРОЕ УДАЛЕНИЕ: Если админ отвечает на сообщение словом "удалить"/"del"/"-"
|
||||
if message.reply_to_message and is_admin(message.from_user.id):
|
||||
if message.text and message.text.lower().strip() in ['удалить', 'del', '-']:
|
||||
async with async_session_maker() as session:
|
||||
# Ищем сообщение в БД по telegram_message_id
|
||||
msg_to_delete = await ChatMessageService.get_message_by_telegram_id(
|
||||
session,
|
||||
telegram_message_id=message.reply_to_message.message_id
|
||||
)
|
||||
|
||||
if msg_to_delete:
|
||||
# Получаем админа
|
||||
admin = await UserService.get_user_by_telegram_id(
|
||||
session,
|
||||
message.from_user.id
|
||||
)
|
||||
|
||||
# Помечаем как удаленное
|
||||
success = await ChatMessageService.mark_as_deleted(
|
||||
session,
|
||||
msg_to_delete.id,
|
||||
admin.id if admin else None
|
||||
)
|
||||
|
||||
if success:
|
||||
# Удаляем у всех получателей
|
||||
deleted_count = 0
|
||||
if msg_to_delete.forwarded_message_ids:
|
||||
for user_tg_id, tg_msg_id in msg_to_delete.forwarded_message_ids.items():
|
||||
try:
|
||||
await message.bot.delete_message(
|
||||
chat_id=int(user_tg_id),
|
||||
message_id=tg_msg_id
|
||||
)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить {tg_msg_id} у {user_tg_id}: {e}")
|
||||
|
||||
# Удаляем оригинал у отправителя
|
||||
try:
|
||||
await message.bot.delete_message(
|
||||
chat_id=msg_to_delete.sender.telegram_id,
|
||||
message_id=msg_to_delete.telegram_message_id
|
||||
)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить оригинал: {e}")
|
||||
|
||||
# Удаляем команду админа
|
||||
try:
|
||||
await message.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Отправляем уведомление (самоудаляющееся)
|
||||
notification = await message.answer(f"✅ Сообщение удалено у {deleted_count} получателей")
|
||||
await asyncio.sleep(3)
|
||||
try:
|
||||
await notification.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
return
|
||||
else:
|
||||
await message.answer("❌ Сообщение не найдено в БД")
|
||||
return
|
||||
|
||||
# Проверяем является ли это командой
|
||||
if message.text and message.text.startswith('/'):
|
||||
# Список команд, которые НЕ нужно пересылать
|
||||
@@ -231,8 +298,12 @@ async def handle_photo_message(message: Message):
|
||||
photo = message.photo[-1]
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
# Отправляем только админам
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
admin_only=True
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -246,7 +317,7 @@ async def handle_photo_message(message: Message):
|
||||
|
||||
# Показываем статистику только админам
|
||||
if is_admin(message.from_user.id):
|
||||
await message.answer(f"✅ Фото разослано: {success} получателей")
|
||||
await message.answer(f"✅ Фото отправлено админам: {success}")
|
||||
|
||||
elif settings.mode == 'forward':
|
||||
if settings.forward_chat_id:
|
||||
|
||||
Reference in New Issue
Block a user