import uuid
from threading import Lock
from aiogram import Router, F
from aiogram.types import (
Message, CallbackQuery,
InlineKeyboardButton, InlineKeyboardMarkup
)
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
from BotCode.core.storage import storage
from BotCode.utils import textmd2
router = Router()
class PostState(StatesGroup):
waiting_for_text = State()
waiting_for_privacy = State()
waiting_for_id = State()
waiting_for_image = State()
waiting_for_buttons = State()
post_id_lock = Lock()
# --- Utility functions ---
def make_inline_markup(rows: list[list[InlineKeyboardButton]]) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=rows)
def cancel_button() -> InlineKeyboardMarkup:
return make_inline_markup([[InlineKeyboardButton(text="Отмена", callback_data="cancel_creation")]])
def privacy_markup(is_private: bool) -> InlineKeyboardMarkup:
toggle = InlineKeyboardButton(
text="🔒 Приватный" if is_private else "🔓 Публичный",
callback_data="toggle_privacy"
)
cont = InlineKeyboardButton(text="Продолжить ➡️", callback_data="continue_creation")
return make_inline_markup([[toggle], [cont]])
def parse_buttons(text: str) -> list[list[dict]]:
rows: list[list[dict]] = []
current: list[dict] = []
for raw in text.splitlines():
line = raw.strip()
if not line:
if current:
rows.append(current)
current = []
continue
if '|' not in line:
raise ValueError(f"Неверный формат кнопки: '{line}'")
label, action = map(str.strip, line.split('|', 1))
btn: dict = {"text": label}
if action.startswith('notification:'):
btn['notification'] = action.split(':', 1)[1]
btn['show_alert'] = True
elif action.startswith('copy:'):
btn['callback_data'] = f"copy_{uuid.uuid4().hex}"
btn['copy_text'] = action.split(':', 1)[1]
elif action.startswith('switch_inline:'):
btn['switch_inline_query'] = action.split(':', 1)[1]
elif action.startswith('switch_inline_current:'):
btn['switch_inline_query_current_chat'] = action.split(':', 1)[1]
elif action.startswith('switch_inline_chosen:'):
btn['switch_inline_query_chosen_chat'] = action.split(':', 1)[1]
elif action.startswith(('http://', 'https://')):
btn['url'] = action
else:
btn['callback_data'] = action
current.append(btn)
if current:
rows.append(current)
return rows
# --- Handlers ---
@router.message(F.text == "Создать пост📔")
async def start_creation(message: Message, state: FSMContext):
await state.set_state(PostState.waiting_for_text)
await state.update_data(private=False, buttons=[])
await message.reply(
textmd2(
"""Отправьте текст вашего поста:
Тест для проверки @userbotname
Жирный
Курсив
Подчёркнутый
Зачёркнутый
Моноширинный
Предварительно отформатированныйСсылка """ ), reply_markup=cancel_button(), parse_mode=None ) @router.message(PostState.waiting_for_text) async def got_text(message: Message, state: FSMContext): await state.update_data(text=message.text or message.caption or "") await state.set_state(PostState.waiting_for_privacy) data = await state.get_data() await message.reply( "Выберите приватность поста:", reply_markup=privacy_markup(data.get('private', False)) ) @router.callback_query(lambda c: c.data == "toggle_privacy") async def toggle_privacy(cq: CallbackQuery, state: FSMContext): data = await state.get_data() is_priv = not data.get('private', False) await state.update_data(private=is_priv) await cq.message.edit_reply_markup( reply_markup=privacy_markup(is_priv) ) await cq.answer() @router.callback_query(lambda c: c.data == "continue_creation") async def continue_to_id(cq: CallbackQuery, state: FSMContext): await state.set_state(PostState.waiting_for_id) await cq.message.edit_text("Введите уникальный ID поста (латиница, цифры, подчёрки):") await cq.answer() @router.message(PostState.waiting_for_id) async def got_id(message: Message, state: FSMContext): pid = message.text.strip() if not pid.replace('_', '').isalnum(): await message.reply( "ID должен содержать только латиницу, цифры и подчёркивания.", reply_markup=cancel_button() ) return with post_id_lock: if not storage.is_post_available(pid): await message.reply( text="Этот ID уже занят, введите другой:", reply_markup=cancel_button() ) return await state.update_data(post_id=pid) await state.set_state(PostState.waiting_for_image) await message.reply( text="Отправьте ссылку на изображение или 'нет':\n" "Пример: https://img4.teletype.in/files/f2/47/...", reply_markup=cancel_button() ) @router.message(PostState.waiting_for_image) async def got_image(message: Message, state: FSMContext): img = message.text.strip() if img.lower() in ('нет', 'no', 'none'): img = '' await state.update_data(image=img) await state.set_state(PostState.waiting_for_buttons) await message.reply( textmd2( """Отправьте кнопки по шаблону: Кнопка заглушка | void Уведомление | notification:Для вас! Кнопка ссылка | https://google.com Копирование | copy:Копирование текста! Для одного | callback_data | allowed_ids=123 | unauthorized_message=Нет доступа Пустая строка — новый ряд. /done — закончить.""" ), reply_markup=cancel_button(), parse_mode=None ) @router.message(PostState.waiting_for_buttons) async def got_buttons(message: Message, state: FSMContext): text = message.text.strip() data = await state.get_data() uid = message.from_user.id pid = data['post_id'] try: if text.lower() in ('/done', 'none'): btns = data.get('buttons', []) if text == '/done' else [] posts = storage.load_user_posts(uid) posts[pid] = { 'user_id': uid, 'text': data['text'], 'image': data['image'], 'buttons': btns, 'private': data.get('private', False) } storage.save_user_posts(uid, posts) await message.reply( f"✅ Пост создан! ID: {pid}\n" f"{'🔒 Приватный' if data.get('private') else '🔓 Публичный'}\n" f"Используйте:
@{(await message.bot.me()).username} {pid}"
)
await state.clear()
return
rows = parse_buttons(text)
existing = data.get('buttons', [])
await state.update_data(buttons=existing + rows)
await message.reply(
text="✅ Кнопки добавлены. Добавьте ещё или /done для окончания.",
reply_markup=cancel_button()
)
except ValueError as err:
await message.reply(f"❌ {err}")
@router.callback_query(lambda c: c.data == "cancel_creation")
async def cancel(cq: CallbackQuery, state: FSMContext):
await state.clear()
await cq.message.reply(textmd2("Процесс создания поста отменён."))
await cq.answer()