Remove obsolete manage command and related modules
- Deleted `manage.py` and related classes - Removed unused `PendingManageAction` and repository calls - Updated references to obsolete manage features in tests: - Deleted `test_statistics.py` - Deleted `test_cron.py` - Deleted `test_kurigram_compat.py` - Cleaned up outdated functionality for managing questions, schedules, and statistics via `/manage`.
This commit is contained in:
4
bot/manage/__init__.py
Normal file
4
bot/manage/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .controller import ManageController
|
||||
from .state import PendingManageAction
|
||||
|
||||
__all__ = ["ManageController", "PendingManageAction"]
|
||||
138
bot/manage/api.py
Normal file
138
bot/manage/api.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from game import SQLiteWouldYouRatherRepository
|
||||
from game.api_service import QuestionApiService, parse_wyr_app_payload
|
||||
|
||||
from .shared import edit_message, menu_keyboard
|
||||
from .state import PendingManageAction
|
||||
|
||||
|
||||
class ApiMenu:
|
||||
def __init__(
|
||||
self,
|
||||
repository: SQLiteWouldYouRatherRepository,
|
||||
pending_actions: dict[int, PendingManageAction],
|
||||
) -> None:
|
||||
self.repository = repository
|
||||
self.pending_actions = pending_actions
|
||||
|
||||
async def handle_callback(self, client: Client, callback: CallbackQuery, data: str, user_id: int) -> bool:
|
||||
"""Handle API-related callbacks. Returns True if handled."""
|
||||
|
||||
if data == "mg:api:load":
|
||||
self.pending_actions.pop(user_id, None)
|
||||
await self._handle_api_load(callback, user_id)
|
||||
return True
|
||||
|
||||
if data == "mg:api:next":
|
||||
self.pending_actions.pop(user_id, None)
|
||||
offset = int(self.repository.get_setting("api_offset", "0"))
|
||||
self.repository.set_setting("api_offset", str(offset + 5))
|
||||
await self._handle_api_load(callback, user_id)
|
||||
return True
|
||||
|
||||
if data.startswith("mg:api:add:"):
|
||||
index = int(data.split(":")[-1])
|
||||
await self._handle_api_add(callback, user_id, index)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data == "mg:api:done":
|
||||
offset = int(self.repository.get_setting("api_offset", "0"))
|
||||
self.repository.set_setting("api_offset", str(offset + 5))
|
||||
self.pending_actions.pop(user_id, None)
|
||||
await edit_message(callback, "✅ Загрузка завершена.", menu_keyboard())
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _handle_api_load(self, callback: CallbackQuery, user_id: int) -> None:
|
||||
offset_str = self.repository.get_setting("api_offset", "0")
|
||||
offset = int(offset_str)
|
||||
page_size = 5
|
||||
|
||||
api_url = f"https://io.wyr.app/api/v1/statements/ru/daily?pageOffset={offset}&pageSize={page_size}"
|
||||
service = QuestionApiService(url=api_url, handler=parse_wyr_app_payload)
|
||||
|
||||
try:
|
||||
questions = service.fetch_questions()
|
||||
if not questions:
|
||||
await callback.answer("API не вернуло вопросов.", show_alert=True)
|
||||
return
|
||||
|
||||
self.pending_actions[user_id] = PendingManageAction(
|
||||
mode="api_select",
|
||||
api_questions=questions,
|
||||
added_indices=set()
|
||||
)
|
||||
|
||||
await self._render_api_selection(callback, questions)
|
||||
await callback.answer()
|
||||
|
||||
except Exception as e:
|
||||
await callback.answer(f"Ошибка загрузки: {str(e)}", show_alert=True)
|
||||
|
||||
async def _render_api_selection(self, callback: CallbackQuery, questions: list) -> None:
|
||||
user_id = callback.from_user.id if callback.from_user else None
|
||||
action = self.pending_actions.get(user_id) if user_id else None
|
||||
added_indices = action.added_indices if action else set()
|
||||
|
||||
text = "🌐 **Выберите вопросы для добавления в базу:**\n\n"
|
||||
keyboard_rows = []
|
||||
|
||||
for i, q in enumerate(questions):
|
||||
is_added = i in added_indices
|
||||
status = "✅ [ДОБАВЛЕНО] " if is_added else ""
|
||||
answers_text = "\n ".join([f"🔹 {a}" for a in q.answers])
|
||||
text += f"{i+1}. {status}{q.text}\n {answers_text}\n\n"
|
||||
|
||||
if is_added:
|
||||
keyboard_rows.append([InlineKeyboardButton(f"➖ Удалить #{i+1}", callback_data=f"mg:api:add:{i}")])
|
||||
else:
|
||||
keyboard_rows.append([InlineKeyboardButton(f"➕ Добавить #{i+1}", callback_data=f"mg:api:add:{i}")])
|
||||
|
||||
keyboard_rows.append([InlineKeyboardButton("➡️ Загрузить еще (пропустить эти)", callback_data="mg:api:next")])
|
||||
keyboard_rows.append([InlineKeyboardButton("✅ Готово", callback_data="mg:api:done")])
|
||||
|
||||
await edit_message(callback, text, InlineKeyboardMarkup(keyboard_rows))
|
||||
|
||||
async def _handle_api_add(self, callback: CallbackQuery, user_id: int, index: int) -> None:
|
||||
action = self.pending_actions.get(user_id)
|
||||
if not action or action.mode != "api_select" or not action.api_questions:
|
||||
await callback.answer("Сессия истекла.", show_alert=True)
|
||||
return
|
||||
|
||||
if index < 0 or index >= len(action.api_questions):
|
||||
await callback.answer("Некорректный индекс.", show_alert=True)
|
||||
return
|
||||
|
||||
if index in action.added_indices:
|
||||
question = action.api_questions[index]
|
||||
try:
|
||||
if self.repository.delete_question_by_text(question.text):
|
||||
action.added_indices.remove(index)
|
||||
await callback.answer(f"Вопрос #{index+1} удалён!")
|
||||
await self._render_api_selection(callback, action.api_questions)
|
||||
else:
|
||||
await callback.answer("Не удалось найти вопрос для удаления.", show_alert=True)
|
||||
except Exception as e:
|
||||
await callback.answer(f"Ошибка удаления: {str(e)}", show_alert=True)
|
||||
return
|
||||
|
||||
question = action.api_questions[index]
|
||||
try:
|
||||
self.repository.add_question(question.text, question.answers)
|
||||
action.added_indices.add(index)
|
||||
await callback.answer(f"Вопрос #{index+1} добавлен!")
|
||||
await self._render_api_selection(callback, action.api_questions)
|
||||
except Exception as e:
|
||||
if "UNIQUE constraint failed" in str(e):
|
||||
action.added_indices.add(index)
|
||||
await callback.answer("Этот вопрос уже есть в базе.", show_alert=True)
|
||||
await self._render_api_selection(callback, action.api_questions)
|
||||
else:
|
||||
await callback.answer(f"Ошибка: {str(e)}", show_alert=True)
|
||||
99
bot/manage/controller.py
Normal file
99
bot/manage/controller.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.handlers import CallbackQueryHandler, MessageHandler
|
||||
from pyrogram.types import Message
|
||||
|
||||
from game import SQLiteWouldYouRatherRepository
|
||||
|
||||
from bot.command_system import CommandContext
|
||||
|
||||
from .api import ApiMenu
|
||||
from .questions import QuestionMenu
|
||||
from .schedules import ScheduleMenu
|
||||
from .shared import edit_message, menu_keyboard
|
||||
from .state import PendingManageAction
|
||||
from .statistics import StatisticsMenu
|
||||
|
||||
|
||||
class ManageController:
|
||||
def __init__(
|
||||
self,
|
||||
context: CommandContext,
|
||||
page_size: int = 5,
|
||||
repository: SQLiteWouldYouRatherRepository | None = None,
|
||||
) -> None:
|
||||
self.context = context
|
||||
self.page_size = page_size
|
||||
self.repository = repository or SQLiteWouldYouRatherRepository()
|
||||
self.pending_actions: dict[int, PendingManageAction] = {}
|
||||
|
||||
# Initialize menu handlers
|
||||
self.question_menu = QuestionMenu(self.repository, self.pending_actions, page_size)
|
||||
self.statistics_menu = StatisticsMenu(self.repository, self.pending_actions, page_size)
|
||||
self.schedule_menu = ScheduleMenu(self.repository, self.pending_actions)
|
||||
self.api_menu = ApiMenu(self.repository, self.pending_actions)
|
||||
|
||||
def register(self, client: Client) -> None:
|
||||
client.add_handler(
|
||||
MessageHandler(self._on_manage_command, filters.command(["manage"]))
|
||||
)
|
||||
client.add_handler(
|
||||
CallbackQueryHandler(self._on_callback_query, filters.regex(r"^mg:"))
|
||||
)
|
||||
client.add_handler(
|
||||
MessageHandler(self._on_text_message, filters.text & ~filters.regex(r"^/"))
|
||||
)
|
||||
|
||||
async def _on_manage_command(self, _: Client, message: Message) -> None:
|
||||
user_id = message.from_user.id if message.from_user else None
|
||||
if user_id is None or user_id not in self.context.admin_ids:
|
||||
await message.reply_text("Команда доступна только администраторам.")
|
||||
return
|
||||
|
||||
await message.reply_text(
|
||||
"Панель управления вопросами:",
|
||||
reply_markup=menu_keyboard(),
|
||||
)
|
||||
|
||||
async def _on_callback_query(self, client: Client, callback) -> None:
|
||||
user_id = callback.from_user.id if callback.from_user else None
|
||||
if user_id is None or user_id not in self.context.admin_ids:
|
||||
await callback.answer("Только для администраторов.", show_alert=True)
|
||||
return
|
||||
|
||||
data = callback.data or ""
|
||||
|
||||
# Main menu
|
||||
if data == "mg:menu":
|
||||
self.pending_actions.pop(user_id, None)
|
||||
await edit_message(callback, "Панель управления вопросами:", menu_keyboard())
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
# Delegate to menu handlers
|
||||
if await self.question_menu.handle_callback(client, callback, data, user_id):
|
||||
return
|
||||
|
||||
if await self.statistics_menu.handle_callback(client, callback, data, user_id):
|
||||
return
|
||||
|
||||
if await self.schedule_menu.handle_callback(client, callback, data, user_id):
|
||||
return
|
||||
|
||||
if await self.api_menu.handle_callback(client, callback, data, user_id):
|
||||
return
|
||||
|
||||
await callback.answer("Неизвестное действие.")
|
||||
|
||||
async def _on_text_message(self, client: Client, message: Message) -> None:
|
||||
user_id = message.from_user.id if message.from_user else None
|
||||
if user_id is None or user_id not in self.context.admin_ids:
|
||||
return
|
||||
|
||||
# Delegate to menu handlers
|
||||
if await self.question_menu.handle_text_message(message, user_id):
|
||||
return
|
||||
|
||||
if await self.schedule_menu.handle_text_message(message, user_id):
|
||||
return
|
||||
175
bot/manage/questions.py
Normal file
175
bot/manage/questions.py
Normal file
@@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
|
||||
|
||||
from game import SQLiteWouldYouRatherRepository
|
||||
|
||||
from .shared import (
|
||||
back_to_menu_keyboard,
|
||||
edit_message,
|
||||
extract_id,
|
||||
extract_page,
|
||||
menu_keyboard,
|
||||
pagination_keyboard,
|
||||
parse_question_input,
|
||||
)
|
||||
from .state import PendingManageAction
|
||||
|
||||
|
||||
class QuestionMenu:
|
||||
def __init__(
|
||||
self,
|
||||
repository: SQLiteWouldYouRatherRepository,
|
||||
pending_actions: dict[int, PendingManageAction],
|
||||
page_size: int = 5,
|
||||
) -> None:
|
||||
self.repository = repository
|
||||
self.pending_actions = pending_actions
|
||||
self.page_size = page_size
|
||||
|
||||
async def handle_callback(self, client: Client, callback: CallbackQuery, data: str, user_id: int) -> bool:
|
||||
"""Handle question-related callbacks. Returns True if handled."""
|
||||
|
||||
if data.startswith("mg:view:"):
|
||||
page = extract_page(data)
|
||||
await self._render_questions_page(callback, page)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data == "mg:add":
|
||||
self.pending_actions.pop(user_id, None)
|
||||
self.pending_actions[user_id] = PendingManageAction(mode="add")
|
||||
await edit_message(
|
||||
callback,
|
||||
"Отправьте новый вопрос в формате:\n"
|
||||
"<вопрос> | <вариант 1> | <вариант 2> [| <вариант 3> ...]",
|
||||
back_to_menu_keyboard(),
|
||||
)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data.startswith("mg:edit:"):
|
||||
page = extract_page(data)
|
||||
await self._render_edit_page(callback, page)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data.startswith("mg:editq:"):
|
||||
try:
|
||||
question_id = extract_id(data)
|
||||
except ValueError:
|
||||
await callback.answer("Некорректный идентификатор.", show_alert=True)
|
||||
return True
|
||||
self.pending_actions[user_id] = PendingManageAction(mode="edit", question_id=question_id)
|
||||
await edit_message(
|
||||
callback,
|
||||
"Отправьте обновлённый вопрос в формате:\n"
|
||||
"<вопрос> | <вариант 1> | <вариант 2> [| <вариант 3> ...]",
|
||||
back_to_menu_keyboard(),
|
||||
)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def handle_text_message(self, message: Message, user_id: int) -> bool:
|
||||
"""Handle text messages for question actions. Returns True if handled."""
|
||||
|
||||
action = self.pending_actions.get(user_id)
|
||||
if action is None:
|
||||
return False
|
||||
|
||||
text = message.text or ""
|
||||
|
||||
if action.mode == "add":
|
||||
try:
|
||||
question_text, answers = parse_question_input(text)
|
||||
question_id = self.repository.add_question(question_text, answers)
|
||||
self.pending_actions.pop(user_id, None)
|
||||
await message.reply_text(
|
||||
f"Вопрос добавлен (ID: {question_id}).",
|
||||
reply_markup=menu_keyboard(),
|
||||
)
|
||||
except ValueError as error:
|
||||
await message.reply_text(
|
||||
f"{error}\n\nИспользуйте формат:\n"
|
||||
"<вопрос> | <вариант 1> | <вариант 2> [| <вариант 3> ...]"
|
||||
)
|
||||
return True
|
||||
|
||||
if action.mode == "edit":
|
||||
if action.question_id is None:
|
||||
await message.reply_text("Ошибка: ID вопроса не найден.")
|
||||
return True
|
||||
try:
|
||||
question_text, answers = parse_question_input(text)
|
||||
self.repository.update_question(action.question_id, question_text, answers)
|
||||
self.pending_actions.pop(user_id, None)
|
||||
await message.reply_text(
|
||||
f"Вопрос #{action.question_id} обновлён.",
|
||||
reply_markup=menu_keyboard(),
|
||||
)
|
||||
except ValueError as error:
|
||||
await message.reply_text(
|
||||
f"{error}\n\nИспользуйте формат:\n"
|
||||
"<вопрос> | <вариант 1> | <вариант 2> [| <вариант 3> ...]"
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _render_questions_page(self, callback: CallbackQuery, page: int) -> None:
|
||||
total = self.repository.count_questions(unused_only=True)
|
||||
total_pages = max(1, math.ceil(total / self.page_size))
|
||||
safe_page = min(max(page, 1), total_pages)
|
||||
offset = (safe_page - 1) * self.page_size
|
||||
rows = self.repository.list_questions(
|
||||
limit=self.page_size, offset=offset, unused_only=True
|
||||
)
|
||||
|
||||
if not rows:
|
||||
text = "В очереди пока нет вопросов."
|
||||
else:
|
||||
lines = [
|
||||
f"В очереди: {total} вопросов",
|
||||
f"Список (страница {safe_page}/{total_pages}):",
|
||||
"",
|
||||
]
|
||||
for item in rows:
|
||||
short_text = (
|
||||
item.text if len(item.text) <= 100 else f"{item.text[:97]}..."
|
||||
)
|
||||
full_question = self.repository.get_question(item.id)
|
||||
answers_text = ", ".join([a.text for a in full_question.answers]) if full_question else "нет вариантов"
|
||||
lines.append(f"#{item.id}: {short_text}\n 🔹 {answers_text}\n")
|
||||
text = "\n".join(lines)
|
||||
|
||||
await edit_message(callback, text, pagination_keyboard("mg:view", safe_page, total_pages))
|
||||
|
||||
async def _render_edit_page(self, callback: CallbackQuery, page: int) -> None:
|
||||
total = self.repository.count_questions()
|
||||
total_pages = max(1, math.ceil(total / self.page_size))
|
||||
safe_page = min(max(page, 1), total_pages)
|
||||
offset = (safe_page - 1) * self.page_size
|
||||
rows = self.repository.list_questions(limit=self.page_size, offset=offset)
|
||||
|
||||
if not rows:
|
||||
await edit_message(callback, "Нет вопросов для изменения.", back_to_menu_keyboard())
|
||||
return
|
||||
|
||||
lines = [f"Выберите вопрос для изменения ({safe_page}/{total_pages}):"]
|
||||
keyboard_rows: list[list[InlineKeyboardButton]] = []
|
||||
for item in rows:
|
||||
short_text = item.text if len(item.text) <= 50 else f"{item.text[:47]}..."
|
||||
lines.append(f"#{item.id}: {short_text}")
|
||||
keyboard_rows.append(
|
||||
[InlineKeyboardButton(f"✏️ #{item.id}", callback_data=f"mg:editq:{item.id}")]
|
||||
)
|
||||
|
||||
from .shared import pagination_rows
|
||||
keyboard_rows.extend(pagination_rows("mg:edit", safe_page, total_pages))
|
||||
keyboard_rows.append([InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")])
|
||||
await edit_message(callback, "\n".join(lines), InlineKeyboardMarkup(keyboard_rows))
|
||||
269
bot/manage/schedules.py
Normal file
269
bot/manage/schedules.py
Normal file
@@ -0,0 +1,269 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
|
||||
|
||||
from game import SQLiteWouldYouRatherRepository
|
||||
|
||||
from .shared import (
|
||||
back_to_menu_keyboard,
|
||||
edit_message,
|
||||
extract_id,
|
||||
menu_keyboard,
|
||||
parse_schedule_input,
|
||||
)
|
||||
from .state import PendingManageAction
|
||||
|
||||
|
||||
class ScheduleMenu:
|
||||
def __init__(
|
||||
self,
|
||||
repository: SQLiteWouldYouRatherRepository,
|
||||
pending_actions: dict[int, PendingManageAction],
|
||||
) -> None:
|
||||
self.repository = repository
|
||||
self.pending_actions = pending_actions
|
||||
|
||||
async def handle_callback(self, client: Client, callback: CallbackQuery, data: str, user_id: int) -> bool:
|
||||
"""Handle schedule-related callbacks. Returns True if handled."""
|
||||
|
||||
if data == "mg:schedule:help":
|
||||
await self._render_help(callback)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data == "mg:schedule:list":
|
||||
await self._render_schedules(callback)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data == "mg:schedule:add":
|
||||
self.pending_actions.pop(user_id, None)
|
||||
self.pending_actions[user_id] = PendingManageAction(mode="schedule_add")
|
||||
await edit_message(
|
||||
callback,
|
||||
"Отправьте расписание в формате:\n"
|
||||
"<название> | <cron 5 полей> | <active: 1 или 0>\n"
|
||||
"Пример: Утро | 0 9 * * 1-5 | 1",
|
||||
back_to_menu_keyboard(),
|
||||
)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data == "mg:schedule:edit:list":
|
||||
await self._render_schedule_edit_list(callback)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data.startswith("mg:schedule:edit:"):
|
||||
try:
|
||||
schedule_id = extract_id(data)
|
||||
except ValueError:
|
||||
await callback.answer("Некорректный идентификатор.", show_alert=True)
|
||||
return True
|
||||
|
||||
schedule = self.repository.get_schedule(schedule_id)
|
||||
if schedule is None:
|
||||
await callback.answer("Расписание не найдено.", show_alert=True)
|
||||
return True
|
||||
|
||||
self.pending_actions[user_id] = PendingManageAction(
|
||||
mode="schedule_edit",
|
||||
schedule_id=schedule_id,
|
||||
)
|
||||
await edit_message(
|
||||
callback,
|
||||
"Отправьте обновлённое расписание в формате:\n"
|
||||
"<название> | <cron 5 полей> | <active: 1 или 0>\n"
|
||||
f"Текущее: {schedule.name} | {schedule.cron} | {1 if schedule.is_active else 0}",
|
||||
back_to_menu_keyboard(),
|
||||
)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data == "mg:schedule:delete:list":
|
||||
await self._render_schedule_delete_list(callback)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data.startswith("mg:schedule:delete:"):
|
||||
try:
|
||||
schedule_id = extract_id(data)
|
||||
except ValueError:
|
||||
await callback.answer("Некорректный идентификатор.", show_alert=True)
|
||||
return True
|
||||
|
||||
try:
|
||||
schedule = self.repository.get_schedule(schedule_id)
|
||||
if schedule is None:
|
||||
await callback.answer("Расписание не найдено.", show_alert=True)
|
||||
return True
|
||||
self.repository.delete_schedule(schedule_id)
|
||||
await callback.answer(f"Расписание '{schedule.name}' удалено.", show_alert=True)
|
||||
await self._render_schedule_delete_list(callback)
|
||||
except ValueError as error:
|
||||
await callback.answer(str(error), show_alert=True)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def handle_text_message(self, message: Message, user_id: int) -> bool:
|
||||
"""Handle text messages for schedule actions. Returns True if handled."""
|
||||
|
||||
action = self.pending_actions.get(user_id)
|
||||
if action is None:
|
||||
return False
|
||||
|
||||
text = message.text or ""
|
||||
|
||||
if action.mode == "schedule_add":
|
||||
try:
|
||||
name, cron, is_active = parse_schedule_input(text)
|
||||
self.repository.add_schedule(name=name, cron=cron, is_active=is_active)
|
||||
self.pending_actions.pop(user_id, None)
|
||||
await message.reply_text(
|
||||
"Расписание добавлено.",
|
||||
reply_markup=menu_keyboard(),
|
||||
)
|
||||
except ValueError as error:
|
||||
await message.reply_text(
|
||||
f"{error}\n\nИспользуйте формат:\n"
|
||||
"<название> | <cron 5 полей> | <active: 1 или 0>"
|
||||
)
|
||||
return True
|
||||
|
||||
if action.mode == "schedule_edit":
|
||||
if action.schedule_id is None:
|
||||
await message.reply_text("Ошибка: ID расписания не найден.")
|
||||
return True
|
||||
try:
|
||||
name, cron, is_active = parse_schedule_input(text)
|
||||
self.repository.update_schedule(
|
||||
schedule_id=action.schedule_id,
|
||||
name=name,
|
||||
cron=cron,
|
||||
is_active=is_active,
|
||||
)
|
||||
self.pending_actions.pop(user_id, None)
|
||||
await message.reply_text(
|
||||
f"Расписание #{action.schedule_id} обновлено.",
|
||||
reply_markup=menu_keyboard(),
|
||||
)
|
||||
except ValueError as error:
|
||||
await message.reply_text(
|
||||
f"{error}\n\nИспользуйте формат:\n"
|
||||
"<название> | <cron 5 полей> | <active: 1 или 0>"
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _render_schedules(self, callback: CallbackQuery) -> None:
|
||||
schedules = self.repository.list_schedules()
|
||||
if not schedules:
|
||||
text = "Расписаний пока нет."
|
||||
else:
|
||||
lines = ["Текущие расписания:"]
|
||||
for schedule in schedules:
|
||||
status = "🟢 active" if schedule.is_active else "⚪️ inactive"
|
||||
lines.append(
|
||||
f"#{schedule.id} [{status}] {schedule.name}: `{schedule.cron}`"
|
||||
)
|
||||
text = "\n".join(lines)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[
|
||||
[InlineKeyboardButton("➕ Добавить расписание", callback_data="mg:schedule:add")],
|
||||
[InlineKeyboardButton("✏️ Изменить расписание", callback_data="mg:schedule:edit:list")],
|
||||
[InlineKeyboardButton("🗑️ Удалить расписание", callback_data="mg:schedule:delete:list")],
|
||||
[InlineKeyboardButton("❓ Помощь", callback_data="mg:schedule:help")],
|
||||
[InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")],
|
||||
]
|
||||
)
|
||||
await edit_message(callback, text, keyboard)
|
||||
|
||||
async def _render_schedule_edit_list(self, callback: CallbackQuery) -> None:
|
||||
schedules = self.repository.list_schedules()
|
||||
if not schedules:
|
||||
await edit_message(
|
||||
callback,
|
||||
"Нет расписаний для изменения.",
|
||||
InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("⬅️ К расписаниям", callback_data="mg:schedule:list")]]
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
lines = ["Выберите расписание для изменения:"]
|
||||
keyboard_rows: list[list[InlineKeyboardButton]] = []
|
||||
for schedule in schedules:
|
||||
status = "🟢" if schedule.is_active else "⚪️"
|
||||
lines.append(f"#{schedule.id} {schedule.name}: {schedule.cron} ({status})")
|
||||
keyboard_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
f"✏️ #{schedule.id} {schedule.name}",
|
||||
callback_data=f"mg:schedule:edit:{schedule.id}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
keyboard_rows.append([InlineKeyboardButton("⬅️ К расписаниям", callback_data="mg:schedule:list")])
|
||||
keyboard_rows.append([InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")])
|
||||
await edit_message(callback, "\n".join(lines), InlineKeyboardMarkup(keyboard_rows))
|
||||
|
||||
async def _render_schedule_delete_list(self, callback: CallbackQuery) -> None:
|
||||
schedules = self.repository.list_schedules()
|
||||
if not schedules:
|
||||
await edit_message(
|
||||
callback,
|
||||
"Нет расписаний для удаления.",
|
||||
InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("⬅️ К расписаниям", callback_data="mg:schedule:list")]]
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
lines = ["⚠️ Выберите расписание для удаления:"]
|
||||
keyboard_rows: list[list[InlineKeyboardButton]] = []
|
||||
for schedule in schedules:
|
||||
status = "🟢" if schedule.is_active else "⚪️"
|
||||
lines.append(f"#{schedule.id} {schedule.name}: {schedule.cron} ({status})")
|
||||
keyboard_rows.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
f"🗑️ #{schedule.id} {schedule.name}",
|
||||
callback_data=f"mg:schedule:delete:{schedule.id}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
keyboard_rows.append([InlineKeyboardButton("⬅️ К расписаниям", callback_data="mg:schedule:list")])
|
||||
keyboard_rows.append([InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")])
|
||||
await edit_message(callback, "\n".join(lines), InlineKeyboardMarkup(keyboard_rows))
|
||||
|
||||
async def _render_help(self, callback: CallbackQuery) -> None:
|
||||
help_text = (
|
||||
"📖 <b>Помощь по расписаниям</b>\n\n"
|
||||
"<b>Формат ввода:</b>\n"
|
||||
"название | cron | active\n\n"
|
||||
"<b>Cron-формат (5 полей, UTC):</b>\n"
|
||||
"минута час день месяц день_недели\n\n"
|
||||
"<b>Примеры:</b>\n"
|
||||
"• <code>0 9 * * 1-5</code> — каждый рабочий день в 09:00 UTC\n"
|
||||
"• <code>30 12 * * *</code> — каждый день в 12:30 UTC\n"
|
||||
"• <code>0 18 * * 0</code> — каждое воскресенье в 18:00 UTC\n"
|
||||
"• <code>*/15 * * * *</code> — каждые 15 минут\n"
|
||||
"• <code>0 9,18 * * *</code> — дважды в день: 09:00 и 18:00\n\n"
|
||||
"<b>День недели:</b> 0 и 7 = воскресенье, 1 = понедельник\n\n"
|
||||
"<b>Active-флаг:</b>\n"
|
||||
"• 1 — расписание активно (вопросы публикуются)\n"
|
||||
"• 0 — расписание выключено"
|
||||
)
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[
|
||||
[InlineKeyboardButton("⬅️ К расписаниям", callback_data="mg:schedule:list")],
|
||||
[InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")],
|
||||
]
|
||||
)
|
||||
await edit_message(callback, help_text, keyboard)
|
||||
94
bot/manage/shared.py
Normal file
94
bot/manage/shared.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pyrogram.errors import MessageNotModified
|
||||
from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from bot.scheduler import validate_cron_expression
|
||||
|
||||
|
||||
def menu_keyboard() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
[
|
||||
[InlineKeyboardButton("📋 Просмотреть вопросы", callback_data="mg:view:1")],
|
||||
[InlineKeyboardButton("➕ Добавить вопрос", callback_data="mg:add")],
|
||||
[InlineKeyboardButton("🌐 Загрузить из API", callback_data="mg:api:load")],
|
||||
[InlineKeyboardButton("✏️ Изменить вопрос", callback_data="mg:edit:1")],
|
||||
[InlineKeyboardButton("📊 Смотреть статистику", callback_data="mg:stats:1")],
|
||||
[InlineKeyboardButton("⏰ Расписания", callback_data="mg:schedule:list")],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def back_to_menu_keyboard() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")]])
|
||||
|
||||
|
||||
def pagination_keyboard(prefix: str, page: int, total_pages: int) -> InlineKeyboardMarkup:
|
||||
rows = pagination_rows(prefix, page, total_pages)
|
||||
rows.append([InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")])
|
||||
return InlineKeyboardMarkup(rows)
|
||||
|
||||
|
||||
def pagination_rows(prefix: str, page: int, total_pages: int) -> list[list[InlineKeyboardButton]]:
|
||||
buttons: list[InlineKeyboardButton] = []
|
||||
if page > 1:
|
||||
buttons.append(InlineKeyboardButton("⬅️", callback_data=f"{prefix}:{page - 1}"))
|
||||
if page < total_pages:
|
||||
buttons.append(InlineKeyboardButton("➡️", callback_data=f"{prefix}:{page + 1}"))
|
||||
if not buttons:
|
||||
return []
|
||||
return [buttons]
|
||||
|
||||
|
||||
async def edit_message(
|
||||
callback: CallbackQuery,
|
||||
text: str,
|
||||
reply_markup: InlineKeyboardMarkup,
|
||||
) -> None:
|
||||
if callback.message:
|
||||
try:
|
||||
await callback.message.edit_text(text=text, reply_markup=reply_markup)
|
||||
except MessageNotModified:
|
||||
return
|
||||
|
||||
|
||||
def parse_question_input(text: str) -> tuple[str, list[str]]:
|
||||
parts = [part.strip() for part in text.split("|")]
|
||||
if len(parts) < 3:
|
||||
raise ValueError("Нужно указать вопрос и минимум 2 варианта ответа.")
|
||||
question_text = parts[0]
|
||||
answers = [part for part in parts[1:] if part]
|
||||
if not question_text:
|
||||
raise ValueError("Текст вопроса не может быть пустым.")
|
||||
if len(answers) < 2:
|
||||
raise ValueError("Нужно указать минимум 2 непустых варианта.")
|
||||
return question_text, answers
|
||||
|
||||
|
||||
def parse_schedule_input(text: str) -> tuple[str, str, bool]:
|
||||
parts = [part.strip() for part in text.split("|")]
|
||||
if len(parts) != 3:
|
||||
raise ValueError("Нужно указать имя, cron и active-флаг (1 или 0).")
|
||||
|
||||
name, cron, active_token = parts
|
||||
if not name:
|
||||
raise ValueError("Имя расписания не может быть пустым.")
|
||||
validate_cron_expression(cron)
|
||||
if active_token not in ("0", "1"):
|
||||
raise ValueError("active-флаг должен быть 1 (активно) или 0 (выключено).")
|
||||
return name, cron, active_token == "1"
|
||||
|
||||
|
||||
def extract_page(data: str) -> int:
|
||||
try:
|
||||
return max(1, int(data.rsplit(":", 1)[1]))
|
||||
except (IndexError, ValueError):
|
||||
return 1
|
||||
|
||||
|
||||
def extract_id(data: str) -> int:
|
||||
try:
|
||||
value = int(data.rsplit(":", 1)[1])
|
||||
except (IndexError, ValueError) as error:
|
||||
raise ValueError("Invalid callback payload.") from error
|
||||
return value
|
||||
14
bot/manage/state.py
Normal file
14
bot/manage/state.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from game.api_service import ApiQuestion
|
||||
|
||||
|
||||
@dataclass
|
||||
class PendingManageAction:
|
||||
mode: str
|
||||
question_id: int | None = None
|
||||
schedule_id: int | None = None
|
||||
api_questions: list[ApiQuestion] | None = None
|
||||
added_indices: set[int] = field(default_factory=set)
|
||||
104
bot/manage/statistics.py
Normal file
104
bot/manage/statistics.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from game import SQLiteWouldYouRatherRepository
|
||||
|
||||
from .shared import (
|
||||
back_to_menu_keyboard,
|
||||
edit_message,
|
||||
extract_id,
|
||||
extract_page,
|
||||
pagination_rows,
|
||||
)
|
||||
from .state import PendingManageAction
|
||||
|
||||
|
||||
class StatisticsMenu:
|
||||
def __init__(
|
||||
self,
|
||||
repository: SQLiteWouldYouRatherRepository,
|
||||
pending_actions: dict[int, PendingManageAction],
|
||||
page_size: int = 5,
|
||||
) -> None:
|
||||
self.repository = repository
|
||||
self.pending_actions = pending_actions
|
||||
self.page_size = page_size
|
||||
|
||||
async def handle_callback(self, client: Client, callback: CallbackQuery, data: str, user_id: int) -> bool:
|
||||
"""Handle statistics-related callbacks. Returns True if handled."""
|
||||
|
||||
if data.startswith("mg:stats:"):
|
||||
page = extract_page(data)
|
||||
await self._render_stats_page(callback, page)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
if data.startswith("mg:statq:"):
|
||||
try:
|
||||
question_id = extract_id(data)
|
||||
except ValueError:
|
||||
await callback.answer("Некорректный идентификатор.", show_alert=True)
|
||||
return True
|
||||
await self._render_question_stats(callback, question_id)
|
||||
await callback.answer()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _render_stats_page(self, callback: CallbackQuery, page: int) -> None:
|
||||
total = self.repository.count_questions()
|
||||
total_pages = max(1, math.ceil(total / self.page_size))
|
||||
safe_page = min(max(page, 1), total_pages)
|
||||
offset = (safe_page - 1) * self.page_size
|
||||
rows = self.repository.list_questions(limit=self.page_size, offset=offset)
|
||||
|
||||
if not rows:
|
||||
await edit_message(callback, "Нет вопросов для статистики.", back_to_menu_keyboard())
|
||||
return
|
||||
|
||||
lines = [f"Выберите вопрос для статистики ({safe_page}/{total_pages}):"]
|
||||
keyboard_rows: list[list[InlineKeyboardButton]] = []
|
||||
for item in rows:
|
||||
short_text = item.text if len(item.text) <= 50 else f"{item.text[:47]}..."
|
||||
lines.append(f"#{item.id}: {short_text}")
|
||||
keyboard_rows.append(
|
||||
[InlineKeyboardButton(f"📊 #{item.id}", callback_data=f"mg:statq:{item.id}")]
|
||||
)
|
||||
|
||||
keyboard_rows.extend(pagination_rows("mg:stats", safe_page, total_pages))
|
||||
keyboard_rows.append([InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")])
|
||||
await edit_message(callback, "\n".join(lines), InlineKeyboardMarkup(keyboard_rows))
|
||||
|
||||
async def _render_question_stats(self, callback: CallbackQuery, question_id: int) -> None:
|
||||
question = self.repository.get_question(question_id)
|
||||
if question is None:
|
||||
await edit_message(
|
||||
callback,
|
||||
"Вопрос не найден.",
|
||||
InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ В статистику", callback_data="mg:stats:1")]]),
|
||||
)
|
||||
return
|
||||
|
||||
stats = self.repository.get_question_statistics(question_id)
|
||||
total_votes = sum(item.count for item in stats)
|
||||
lines = [f"Статистика для вопроса #{question_id}:", question.question.text, ""]
|
||||
if not stats:
|
||||
lines.append("Нет вариантов ответа.")
|
||||
else:
|
||||
for item in stats:
|
||||
percent = (item.count / total_votes * 100) if total_votes else 0.0
|
||||
lines.append(f"{item.answer_text}: {item.count} ({percent:.1f}%)")
|
||||
lines.append("")
|
||||
lines.append(f"Всего голосов: {total_votes}")
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[
|
||||
[InlineKeyboardButton("⬅️ К вопросам статистики", callback_data="mg:stats:1")],
|
||||
[InlineKeyboardButton("⬅️ В меню", callback_data="mg:menu")],
|
||||
]
|
||||
)
|
||||
await edit_message(callback, "\n".join(lines), keyboard)
|
||||
Reference in New Issue
Block a user