from __future__ import annotations import asyncio import logging from typing import Any from aiogram import Bot, Dispatcher, F, Router from aiogram.exceptions import TelegramBadRequest from aiogram.filters import CommandStart from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import CallbackQuery, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from session_bot.config import load_actor_config, load_settings from session_bot.render import build_channel_text from session_bot.storage import JsonStateStorage router = Router() STATUS_CHOICES = { "open": "Открыть", "backstage": "Закулисье", "delay": "Задержка", "rest": "Антракт", } class SessionForm(StatesGroup): waiting_for_phrase = State() def build_actor_lookup(config: dict) -> dict[int, dict[str, Any]]: lookup: dict[int, dict[str, Any]] = {} for actor in config["actors"]: lookup[int(actor["operator_user_id"])] = actor return lookup def is_allowed(user_id: int, actor_lookup: dict[int, dict[str, Any]], admin_ids: set[int]) -> bool: return user_id in actor_lookup or user_id in admin_ids def build_actor_keyboard(config: dict, user_id: int, admin_ids: set[int]) -> InlineKeyboardBuilder: keyboard = InlineKeyboardBuilder() for actor in config["actors"]: if user_id in admin_ids or int(actor["operator_user_id"]) == user_id: keyboard.button(text=actor["button_text"], callback_data=f"actor:{actor['key']}") keyboard.adjust(2) return keyboard def build_status_keyboard(actor_key: str) -> InlineKeyboardBuilder: keyboard = InlineKeyboardBuilder() for status_key, title in STATUS_CHOICES.items(): keyboard.button(text=title, callback_data=f"status:{actor_key}:{status_key}") keyboard.adjust(2) return keyboard async def update_channel_post(bot: Bot, app_config: dict, state_storage: JsonStateStorage, settings) -> None: state = state_storage.load() text = build_channel_text(app_config, state) await bot.edit_message_text( chat_id=settings.channel_id, message_id=settings.channel_message_id, text=text, disable_web_page_preview=True, ) @router.message(CommandStart()) async def start_handler(message: Message, state: FSMContext, app_config: dict, actor_lookup: dict, settings) -> None: await state.clear() user_id = message.from_user.id if not is_allowed(user_id, actor_lookup, settings.admin_ids): await message.answer("У вас нет доступа к этой панели.") return keyboard = build_actor_keyboard(app_config, user_id, settings.admin_ids) await message.answer("Выберите персонажа для смены статуса.", reply_markup=keyboard.as_markup()) @router.callback_query(F.data.startswith("actor:")) async def actor_handler(callback: CallbackQuery, settings, actor_lookup: dict, app_config: dict) -> None: user_id = callback.from_user.id if not is_allowed(user_id, actor_lookup, settings.admin_ids): await callback.answer("Нет доступа.", show_alert=True) return actor_key = callback.data.split(":", maxsplit=1)[1] actor = next((item for item in app_config["actors"] if item["key"] == actor_key), None) if actor is None: await callback.answer("Персонаж не найден.", show_alert=True) return if user_id not in settings.admin_ids and int(actor["operator_user_id"]) != user_id: await callback.answer("Можно менять только свой статус.", show_alert=True) return keyboard = build_status_keyboard(actor_key) await callback.message.edit_text( f"Выбран {actor['display_name']}. Какой статус поставить?", reply_markup=keyboard.as_markup(), ) await callback.answer() @router.callback_query(F.data.startswith("status:")) async def status_handler( callback: CallbackQuery, state: FSMContext, settings, actor_lookup: dict, app_config: dict, ) -> None: user_id = callback.from_user.id if not is_allowed(user_id, actor_lookup, settings.admin_ids): await callback.answer("Нет доступа.", show_alert=True) return _, actor_key, status_key = callback.data.split(":") actor = next((item for item in app_config["actors"] if item["key"] == actor_key), None) if actor is None: await callback.answer("Персонаж не найден.", show_alert=True) return if user_id not in settings.admin_ids and int(actor["operator_user_id"]) != user_id: await callback.answer("Можно менять только свой статус.", show_alert=True) return default_phrase = actor.get("phrases", {}).get(status_key, "") await state.set_state(SessionForm.waiting_for_phrase) await state.update_data(actor_key=actor_key, status_key=status_key) prompt = ( f"Статус для {actor['display_name']}: {STATUS_CHOICES[status_key]}.\n" "Отправьте фразу для публикации.\n" "Если хотите использовать шаблон, отправьте точку: .\n" f"Шаблон сейчас: {default_phrase or 'не задан'}" ) await callback.message.edit_text(prompt) await callback.answer() @router.message(SessionForm.waiting_for_phrase) async def phrase_handler( message: Message, state: FSMContext, bot: Bot, app_config: dict, state_storage: JsonStateStorage, settings, ) -> None: if message.text is None: await message.answer("Нужен текст сообщения.") return data = await state.get_data() actor_key = data["actor_key"] status_key = data["status_key"] actor = next(item for item in app_config["actors"] if item["key"] == actor_key) phrase = message.text.strip() if phrase == ".": phrase = actor.get("phrases", {}).get(status_key, "") payload = state_storage.load() payload.setdefault("actors", {}) payload["actors"][actor_key] = { "status": status_key, "phrase": phrase, "updated_by": message.from_user.id, } state_storage.save(payload) try: await update_channel_post(bot, app_config, state_storage, settings) except TelegramBadRequest as exc: logging.exception("Failed to edit channel message") await message.answer(f"Статус сохранен, но сообщение канала не обновилось: {exc}") await state.clear() return await state.clear() await message.answer(f"Обновлено: {actor['display_name']} -> {STATUS_CHOICES[status_key].lower()}.") def build_dispatcher(app_config: dict, settings, state_storage: JsonStateStorage) -> Dispatcher: dispatcher = Dispatcher(storage=MemoryStorage()) dispatcher["app_config"] = app_config dispatcher["actor_lookup"] = build_actor_lookup(app_config) dispatcher["settings"] = settings dispatcher["state_storage"] = state_storage dispatcher.include_router(router) return dispatcher async def main() -> None: logging.basicConfig(level=logging.INFO) settings = load_settings() app_config = load_actor_config(settings.config_path) state_storage = JsonStateStorage(settings.state_path) bot = Bot(token=settings.bot_token) dispatcher = build_dispatcher(app_config, settings, state_storage) await dispatcher.start_polling(bot) def run() -> None: asyncio.run(main())