Версия 1.0
This commit is contained in:
10
BotCode/handlers/post/__init__.py
Normal file
10
BotCode/handlers/post/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from aiogram import Router
|
||||
from .create_posts import router as posts_router
|
||||
from .post_list import router as post_list_router
|
||||
|
||||
router = Router(name="post_router")
|
||||
|
||||
router.include_routers(
|
||||
posts_router,
|
||||
post_list_router,
|
||||
)
|
||||
210
BotCode/handlers/post/create_posts.py
Normal file
210
BotCode/handlers/post/create_posts.py
Normal file
@@ -0,0 +1,210 @@
|
||||
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
|
||||
<b>Жирный</b>
|
||||
<i>Курсив</i>
|
||||
<u>Подчёркнутый</u>
|
||||
<s>Зачёркнутый</s>
|
||||
<code>Моноширинный</code>
|
||||
<pre>Предварительно отформатированный</pre>
|
||||
<a href=\"https://example.com\">Ссылка</a>
|
||||
"""
|
||||
),
|
||||
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"Используйте: <code>@{(await message.bot.me()).username} {pid}</code>"
|
||||
)
|
||||
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()
|
||||
200
BotCode/handlers/post/post_list.py
Normal file
200
BotCode/handlers/post/post_list.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from math import ceil
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import (
|
||||
Message, CallbackQuery,
|
||||
InlineKeyboardButton, InlineKeyboardMarkup,
|
||||
SwitchInlineQueryChosenChat, CopyTextButton
|
||||
)
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.utils.markdown import hide_link
|
||||
|
||||
from BotCode.core.storage import storage
|
||||
from BotCode.utils.pagination import create_pagination_buttons
|
||||
from BotCode.utils import textmd2
|
||||
from BotCode.config import PARSE_MODE
|
||||
|
||||
router = Router(name="posts_manager")
|
||||
|
||||
PAGE_SIZE = 5
|
||||
|
||||
async def send_posts_list(
|
||||
message: Message = None,
|
||||
callback_query: CallbackQuery = None,
|
||||
page: int = 0
|
||||
) -> None:
|
||||
"""Отправляет список постов пользователя с пагинацией."""
|
||||
user_id = message.from_user.id if message else callback_query.from_user.id
|
||||
posts = storage.load_user_posts(user_id)
|
||||
|
||||
if not posts:
|
||||
msg = "Нет сохранённых постов."
|
||||
if message:
|
||||
await message.answer(msg)
|
||||
else:
|
||||
await callback_query.answer(msg, show_alert=True)
|
||||
return
|
||||
|
||||
post_ids = list(posts.keys())
|
||||
total = len(post_ids)
|
||||
pages = ceil(total / PAGE_SIZE)
|
||||
page = max(0, min(page, pages - 1))
|
||||
|
||||
start = page * PAGE_SIZE
|
||||
end = start + PAGE_SIZE
|
||||
current_ids = post_ids[start:end]
|
||||
|
||||
rows: list[list[InlineKeyboardButton]] = []
|
||||
for pid in current_ids:
|
||||
post = posts[pid]
|
||||
priv = "🔒" if post.get("private") else "🔓"
|
||||
btn = InlineKeyboardButton(
|
||||
text=f"{priv} Пост {pid}",
|
||||
callback_data=f"view_post_{pid}"
|
||||
)
|
||||
rows.append([btn])
|
||||
|
||||
# Пагинация
|
||||
nav_buttons = create_pagination_buttons(
|
||||
action="open_post_list",
|
||||
page=page,
|
||||
total_posts=total,
|
||||
bt_page=PAGE_SIZE
|
||||
)
|
||||
if nav_buttons:
|
||||
rows.append(nav_buttons)
|
||||
|
||||
# Кнопка закрытия
|
||||
rows.append([InlineKeyboardButton(text="Закрыть❌", callback_data="cancel_list")])
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
header = "Список ваших постов:"
|
||||
|
||||
try:
|
||||
if callback_query:
|
||||
await callback_query.message.edit_text(header, reply_markup=keyboard)
|
||||
else:
|
||||
await message.answer(header, reply_markup=keyboard)
|
||||
except TelegramBadRequest:
|
||||
if callback_query:
|
||||
await callback_query.message.delete()
|
||||
await callback_query.message.answer(header, reply_markup=keyboard)
|
||||
else:
|
||||
await message.answer(header, reply_markup=keyboard)
|
||||
|
||||
# --- Хендлеры списка ---
|
||||
@router.message(F.text.lower() == "посмотреть список📋")
|
||||
async def cmd_list(message: Message):
|
||||
await send_posts_list(message=message)
|
||||
|
||||
@router.callback_query(F.data == "open_post_list")
|
||||
async def cb_open_list(cq: CallbackQuery):
|
||||
await send_posts_list(callback_query=cq)
|
||||
await cq.answer()
|
||||
|
||||
@router.callback_query(lambda c: c.data and c.data.startswith("open_post_list_page_"))
|
||||
async def cb_paginate(cq: CallbackQuery):
|
||||
try:
|
||||
page = int(cq.data.rsplit("_", 1)[-1])
|
||||
except ValueError:
|
||||
await cq.answer("Некорректная страница", show_alert=True)
|
||||
return
|
||||
await send_posts_list(callback_query=cq, page=page)
|
||||
await cq.answer()
|
||||
|
||||
@router.callback_query(F.data == "cancel_list")
|
||||
async def cb_cancel(cq: CallbackQuery):
|
||||
await cq.message.delete()
|
||||
await cq.answer()
|
||||
|
||||
# --- Просмотр отдельного поста ---
|
||||
@router.callback_query(lambda c: c.data and c.data.startswith("view_post_"))
|
||||
async def view_post_callback(cq: CallbackQuery):
|
||||
pid = cq.data.replace("view_post_", "")
|
||||
uid = cq.from_user.id
|
||||
posts = storage.load_user_posts(uid)
|
||||
if pid not in posts:
|
||||
await cq.answer("Пост не найден", show_alert=True)
|
||||
return
|
||||
|
||||
post = posts[pid]
|
||||
text = textmd2(post.get("text", ""))
|
||||
img = post.get("image", "")
|
||||
if img.startswith("http"):
|
||||
text = f"{hide_link(img)}{text}"
|
||||
|
||||
rows: list[list[InlineKeyboardButton]] = []
|
||||
for row in post.get("buttons", []):
|
||||
btns: list[InlineKeyboardButton] = []
|
||||
for b in row:
|
||||
if "copy_text" in b:
|
||||
btns.append(
|
||||
InlineKeyboardButton(
|
||||
text=b["text"],
|
||||
copy_text=CopyTextButton(text=b["copy_text"])
|
||||
)
|
||||
)
|
||||
elif "switch_inline_query" in b:
|
||||
btns.append(
|
||||
InlineKeyboardButton(
|
||||
text=b["text"],
|
||||
switch_inline_query=b["switch_inline_query"]
|
||||
)
|
||||
)
|
||||
elif "switch_inline_query_current_chat" in b:
|
||||
btns.append(
|
||||
InlineKeyboardButton(
|
||||
text=b["text"],
|
||||
switch_inline_query_current_chat=b["switch_inline_query_current_chat"]
|
||||
)
|
||||
)
|
||||
elif "switch_inline_query_chosen_chat" in b:
|
||||
raw = b["switch_inline_query_chosen_chat"]
|
||||
cfg = raw if isinstance(raw, dict) else {
|
||||
"query": raw,
|
||||
"allow_user_chats": True
|
||||
}
|
||||
btns.append(
|
||||
InlineKeyboardButton(
|
||||
text=b["text"],
|
||||
switch_inline_query_chosen_chat=SwitchInlineQueryChosenChat(**cfg)
|
||||
)
|
||||
)
|
||||
elif "url" in b:
|
||||
url = b["url"]
|
||||
if url.lower().endswith("void"):
|
||||
btns.append(
|
||||
InlineKeyboardButton(text=b["text"], callback_data="void")
|
||||
)
|
||||
else:
|
||||
btns.append(
|
||||
InlineKeyboardButton(text=b["text"], url=url)
|
||||
)
|
||||
elif "callback_data" in b:
|
||||
btns.append(
|
||||
InlineKeyboardButton(text=b["text"], callback_data=b["callback_data"])
|
||||
)
|
||||
if btns:
|
||||
rows.append(btns)
|
||||
|
||||
# Удалить / назад
|
||||
rows.append([
|
||||
InlineKeyboardButton(text="Удалить❌", callback_data=f"delete_post_{pid}"),
|
||||
InlineKeyboardButton(text="Назад◀️", callback_data="open_post_list")
|
||||
])
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
|
||||
await cq.message.answer(text=text, reply_markup=keyboard, parse_mode=PARSE_MODE)
|
||||
await cq.message.delete()
|
||||
await cq.answer()
|
||||
|
||||
# --- Удаление поста ---
|
||||
@router.callback_query(lambda c: c.data and c.data.startswith("delete_post_"))
|
||||
async def delete_post_callback(cq: CallbackQuery):
|
||||
pid = cq.data.replace("delete_post_", "")
|
||||
uid = cq.from_user.id
|
||||
if storage.delete_user_post(uid, pid):
|
||||
await cq.answer(f"Пост {pid} удалён")
|
||||
await send_posts_list(callback_query=cq)
|
||||
else:
|
||||
await cq.answer("Не удалось удалить пост", show_alert=True)
|
||||
Reference in New Issue
Block a user