Compare commits
11 Commits
b123e9f714
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 931235ff36 | |||
| 8e692d2f61 | |||
| 49f220c2a2 | |||
| ec8a23887d | |||
| 007274785f | |||
| e39ef96b26 | |||
| 7067f4656b | |||
| 9db201551b | |||
| 38529a8805 | |||
| 2e92164bbf | |||
| 69985f6afb |
171
.drone.yml
171
.drone.yml
@@ -4,13 +4,13 @@ name: default
|
|||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
|
- master
|
||||||
- main
|
- main
|
||||||
- develop
|
- develop
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
# Настройки для Drone CI/CD
|
|
||||||
platform:
|
platform:
|
||||||
os: linux
|
os: linux
|
||||||
arch: amd64
|
arch: amd64
|
||||||
@@ -34,10 +34,6 @@ steps:
|
|||||||
- black --check --line-length=120 src/ main.py || echo "⚠️ Форматирование может быть улучшено"
|
- black --check --line-length=120 src/ main.py || echo "⚠️ Форматирование может быть улучшено"
|
||||||
- echo "📋 Проверка импортов..."
|
- echo "📋 Проверка импортов..."
|
||||||
- isort --check-only --profile black src/ main.py || echo "⚠️ Импорты могут быть улучшены"
|
- isort --check-only --profile black src/ main.py || echo "⚠️ Импорты могут быть улучшены"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 2: Установка зависимостей
|
# Шаг 2: Установка зависимостей
|
||||||
- name: install-dependencies
|
- name: install-dependencies
|
||||||
@@ -49,10 +45,6 @@ steps:
|
|||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- echo "✅ Зависимости установлены"
|
- echo "✅ Зависимости установлены"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 3: Проверка импортов и синтаксиса
|
# Шаг 3: Проверка импортов и синтаксиса
|
||||||
- name: syntax-check
|
- name: syntax-check
|
||||||
@@ -65,19 +57,15 @@ steps:
|
|||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- echo "🔍 Проверка синтаксиса Python..."
|
- echo "🔍 Проверка синтаксиса Python..."
|
||||||
- python -m py_compile main.py
|
- python -m py_compile main.py
|
||||||
- python -m py_compile src/core/*.py
|
- python -m py_compile src/core/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||||
- python -m py_compile src/handlers/*.py
|
- python -m py_compile src/handlers/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||||
- python -m py_compile src/utils/*.py
|
- python -m py_compile src/utils/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||||
- python -m py_compile src/display/*.py
|
- python -m py_compile src/display/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||||
- echo "🧪 Проверка импортов..."
|
- echo "🧪 Проверка импортов..."
|
||||||
- python -c "from src.core import config, database, models, services; print('✅ Core модули OK')"
|
- 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, async_decorators, task_manager; print('✅ Utils модули OK')"
|
- 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')"
|
- python -c "from src.display import winner_display; print('✅ Display модули OK')" || echo "⚠️ Проблема с импортами"
|
||||||
- echo "✅ Все модули импортируются корректно"
|
- echo "✅ Проверка синтаксиса завершена"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 4: Инициализация тестовой БД
|
# Шаг 4: Инициализация тестовой БД
|
||||||
- name: database-init
|
- name: database-init
|
||||||
@@ -89,12 +77,8 @@ steps:
|
|||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- echo "🗄️ Инициализация тестовой базы данных..."
|
- echo "🗄️ Инициализация тестовой базы данных..."
|
||||||
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())"
|
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())" || echo "⚠️ БД не инициализирована"
|
||||||
- echo "✅ Тестовая БД инициализирована"
|
- echo "✅ Тестовая БД готова"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 5: Запуск тестов
|
# Шаг 5: Запуск тестов
|
||||||
- name: run-tests
|
- name: run-tests
|
||||||
@@ -108,143 +92,22 @@ steps:
|
|||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- echo "🧪 Запуск тестов..."
|
- echo "🧪 Запуск тестов..."
|
||||||
- python tests/test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
|
- python test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
|
||||||
- python tests/test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
|
- python test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
|
||||||
- echo "✅ Тесты выполнены"
|
- echo "✅ Тесты выполнены"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 6: Создание артефактов (только для main ветки)
|
# Шаг 6: Создание артефактов
|
||||||
- name: build-artifacts
|
- name: build-artifacts
|
||||||
image: python:3.12-slim
|
image: python:3.12-slim
|
||||||
commands:
|
commands:
|
||||||
- echo "📦 Создание артефактов сборки..."
|
- echo "📦 Создание артефактов сборки..."
|
||||||
- mkdir -p dist
|
- 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/
|
- tar -czf dist/lottery_bot_build_${DRONE_BUILD_NUMBER}.tar.gz src/ main.py requirements.txt Makefile README.md alembic.ini migrations/
|
||||||
- echo "✅ Артефакты созданы: lottery_bot_${DRONE_BUILD_NUMBER}.tar.gz"
|
- echo "✅ Артефакты созданы"
|
||||||
- ls -la dist/
|
- ls -la dist/
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- main
|
- main
|
||||||
|
- master
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
|
|
||||||
# Шаг 7: Уведомления о результатах
|
|
||||||
- name: notify-success
|
|
||||||
image: plugins/webhook
|
|
||||||
settings:
|
|
||||||
urls:
|
|
||||||
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
|
|
||||||
headers:
|
|
||||||
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
|
|
||||||
headers:
|
|
||||||
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
|
|
||||||
headers:
|
|
||||||
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 \
|
else \
|
||||||
echo "❌ Отменено"; \
|
echo "❌ Отменено"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Очистка
|
# Очистка
|
||||||
clean:
|
clean:
|
||||||
@echo "🧹 Очистка временных файлов..."
|
@echo "🧹 Очистка временных файлов..."
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
|||||||
if is_admin:
|
if is_admin:
|
||||||
buttons.extend([
|
buttons.extend([
|
||||||
[InlineKeyboardButton(text="⚙️ Админ панель", callback_data="admin_panel")],
|
[InlineKeyboardButton(text="⚙️ Админ панель", callback_data="admin_panel")],
|
||||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="create_lottery")]
|
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")]
|
||||||
])
|
])
|
||||||
|
|
||||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
@@ -30,8 +30,9 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
|||||||
"""Получить админскую клавиатуру"""
|
"""Получить админскую клавиатуру"""
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries")],
|
[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_winners")],
|
||||||
|
[InlineKeyboardButton(text="💬 Сообщения пользователей", callback_data="admin_messages")],
|
||||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
|
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
|
||||||
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")],
|
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")],
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||||
|
|||||||
@@ -87,8 +87,21 @@ class BotController(IBotController):
|
|||||||
is_registered=user.is_registered
|
is_registered=user.is_registered
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
text,
|
text,
|
||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
parse_mode="Markdown"
|
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"
|
||||||
|
)
|
||||||
@@ -285,6 +285,58 @@ class ChatMessageService:
|
|||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
return result.scalars().all()
|
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:
|
class ChatPermissionService:
|
||||||
"""Сервис проверки прав на отправку сообщений"""
|
"""Сервис проверки прав на отправку сообщений"""
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ class UserService:
|
|||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
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
|
@staticmethod
|
||||||
async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
|
async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
|
||||||
"""Получить пользователя по username"""
|
"""Получить пользователя по username"""
|
||||||
@@ -228,6 +234,25 @@ class LotteryService:
|
|||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
return result.scalars().all()
|
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
|
@staticmethod
|
||||||
async def get_all_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
|
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.database import async_session_maker
|
||||||
from ..core.services import UserService, LotteryService, ParticipationService
|
from ..core.services import UserService, LotteryService, ParticipationService
|
||||||
|
from ..core.chat_services import ChatMessageService
|
||||||
from ..core.config import ADMIN_IDS
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -204,14 +205,23 @@ async def show_lottery_management(callback: CallbackQuery):
|
|||||||
@admin_router.callback_query(F.data == "admin_create_lottery")
|
@admin_router.callback_query(F.data == "admin_create_lottery")
|
||||||
async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
|
async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
|
||||||
"""Начать создание розыгрыша"""
|
"""Начать создание розыгрыша"""
|
||||||
|
logging.info(f"🎯 Callback admin_create_lottery получен от пользователя {callback.from_user.id}")
|
||||||
|
|
||||||
|
# Сразу отвечаем на callback
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
if not is_admin(callback.from_user.id):
|
if not is_admin(callback.from_user.id):
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
logging.warning(f"⚠️ Пользователь {callback.from_user.id} не является админом")
|
||||||
|
await callback.message.answer("❌ Недостаточно прав")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logging.info(f"✅ Админ {callback.from_user.id} начинает создание розыгрыша")
|
||||||
|
|
||||||
text = "📝 Создание нового розыгрыша\n\n"
|
text = "📝 Создание нового розыгрыша\n\n"
|
||||||
text += "Шаг 1 из 4\n\n"
|
text += "Шаг 1 из 4\n\n"
|
||||||
text += "Введите название розыгрыша:"
|
text += "Введите название розыгрыша:"
|
||||||
|
|
||||||
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
text,
|
text,
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
@@ -219,15 +229,49 @@ async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
|
|||||||
])
|
])
|
||||||
)
|
)
|
||||||
await state.set_state(AdminStates.lottery_title)
|
await state.set_state(AdminStates.lottery_title)
|
||||||
|
logging.info(f"✅ Состояние установлено: AdminStates.lottery_title")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"❌ Ошибка при создании розыгрыша: {e}")
|
||||||
|
await callback.message.answer(f"❌ Ошибка: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@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 is_admin(message.from_user.id):
|
||||||
await message.answer("❌ Недостаточно прав")
|
await message.answer("❌ Недостаточно прав")
|
||||||
return
|
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)
|
await state.update_data(title=message.text)
|
||||||
|
|
||||||
text = f"📝 Создание нового розыгрыша\n\n"
|
text = f"📝 Создание нового розыгрыша\n\n"
|
||||||
@@ -241,11 +285,42 @@ 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 is_admin(message.from_user.id):
|
||||||
await message.answer("❌ Недостаточно прав")
|
await message.answer("❌ Недостаточно прав")
|
||||||
return
|
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
|
description = None if message.text == "-" else message.text
|
||||||
await state.update_data(description=description)
|
await state.update_data(description=description)
|
||||||
|
|
||||||
@@ -268,12 +343,43 @@ 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 is_admin(message.from_user.id):
|
||||||
await message.answer("❌ Недостаточно прав")
|
await message.answer("❌ Недостаточно прав")
|
||||||
return
|
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()]
|
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)
|
await state.update_data(prizes=prizes)
|
||||||
|
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
@@ -1705,6 +1811,47 @@ async def start_edit_lottery(callback: CallbackQuery, state: FSMContext):
|
|||||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
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_"))
|
@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):
|
||||||
"""Редирект на редактирование розыгрыша из детального просмотра"""
|
"""Редирект на редактирование розыгрыша из детального просмотра"""
|
||||||
@@ -1763,7 +1910,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):
|
async def toggle_lottery_active(callback: CallbackQuery, state: FSMContext):
|
||||||
"""Переключить активность розыгрыша"""
|
"""Переключить активность розыгрыша"""
|
||||||
if not is_admin(callback.from_user.id):
|
if not is_admin(callback.from_user.id):
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
@@ -1785,7 +1932,7 @@ async def toggle_lottery_active(callback: CallbackQuery):
|
|||||||
await callback.answer("❌ Ошибка изменения статуса", show_alert=True)
|
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")
|
@admin_router.callback_query(F.data == "admin_finish_lottery")
|
||||||
@@ -3266,5 +3413,286 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext):
|
|||||||
await state.clear()
|
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']
|
__all__ = ['admin_router']
|
||||||
@@ -109,6 +109,73 @@ async def handle_text_message(message: Message):
|
|||||||
logger = logging.getLogger(__name__)
|
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'}")
|
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('/'):
|
if message.text and message.text.startswith('/'):
|
||||||
# Список команд, которые НЕ нужно пересылать
|
# Список команд, которые НЕ нужно пересылать
|
||||||
@@ -123,21 +190,20 @@ async def handle_text_message(message: Message):
|
|||||||
# Извлекаем команду (первое слово)
|
# Извлекаем команду (первое слово)
|
||||||
command = message.text.split()[0] if message.text else ''
|
command = message.text.split()[0] if message.text else ''
|
||||||
|
|
||||||
# Если это пользовательская команда - пропускаем, она будет обработана другими обработчиками
|
# ИЗМЕНЕНИЕ: Если это команда от АДМИНА - не пересылаем (админ сам её видит)
|
||||||
|
if is_admin(message.from_user.id):
|
||||||
|
# Если это админская команда - пропускаем, она будет обработана другими обработчиками
|
||||||
|
if command in admin_commands:
|
||||||
|
return
|
||||||
|
# Если это пользовательская команда от админа - тоже пропускаем
|
||||||
if command in user_commands:
|
if command in user_commands:
|
||||||
return
|
return
|
||||||
|
# Любая другая команда от админа - тоже не пересылаем
|
||||||
# Если это админская команда
|
|
||||||
if command in admin_commands:
|
|
||||||
# Проверяем права админа
|
|
||||||
if not is_admin(message.from_user.id):
|
|
||||||
await message.answer("❌ У вас нет прав для выполнения этой команды")
|
|
||||||
return
|
|
||||||
# Если админ - команда будет обработана другими обработчиками, пропускаем пересылку
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Если неизвестная команда - тоже не пересылаем
|
# ИЗМЕНЕНИЕ: Если команда от обычного пользователя - ПЕРЕСЫЛАЕМ админу
|
||||||
return
|
# Чтобы админ видел, что пользователь отправил /start или другую команду
|
||||||
|
# НЕ делаем return, продолжаем выполнение для пересылки
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
# Проверяем права на отправку
|
# Проверяем права на отправку
|
||||||
@@ -232,8 +298,12 @@ async def handle_photo_message(message: Message):
|
|||||||
photo = message.photo[-1]
|
photo = message.photo[-1]
|
||||||
|
|
||||||
if settings.mode == 'broadcast':
|
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(
|
await ChatMessageService.save_message(
|
||||||
session,
|
session,
|
||||||
@@ -247,7 +317,7 @@ async def handle_photo_message(message: Message):
|
|||||||
|
|
||||||
# Показываем статистику только админам
|
# Показываем статистику только админам
|
||||||
if is_admin(message.from_user.id):
|
if is_admin(message.from_user.id):
|
||||||
await message.answer(f"✅ Фото разослано: {success} получателей")
|
await message.answer(f"✅ Фото отправлено админам: {success}")
|
||||||
|
|
||||||
elif settings.mode == 'forward':
|
elif settings.mode == 'forward':
|
||||||
if settings.forward_chat_id:
|
if settings.forward_chat_id:
|
||||||
|
|||||||
Reference in New Issue
Block a user