v1.2.1
This commit is contained in:
17
bot/handlers/__init__.py
Normal file
17
bot/handlers/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from aiogram import Router
|
||||
from .post import router as post_routers
|
||||
from .commands import router as cmd_routers
|
||||
from .callback import router as callback_router
|
||||
from .inline import router as inline_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name="handlers_router")
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
cmd_routers,
|
||||
callback_router,
|
||||
post_routers,
|
||||
inline_router
|
||||
)
|
||||
46
bot/handlers/callback.py
Normal file
46
bot/handlers/callback.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import CallbackQuery
|
||||
from bot.core import storage
|
||||
|
||||
router: Router = Router(name="callback_router")
|
||||
|
||||
@router.callback_query(F.data.startswith("bt_"))
|
||||
@router.callback_query(F.data.startswith("show_alert_"))
|
||||
async def handle_button_alert(callback_query: CallbackQuery) -> None:
|
||||
key: Optional[str] = callback_query.data
|
||||
user_id: int = callback_query.from_user.id
|
||||
|
||||
# Получаем уведомление через хранилище
|
||||
notif = storage.get_notification(key)
|
||||
if not notif:
|
||||
await callback_query.answer()
|
||||
return
|
||||
|
||||
# Проверяем права доступа
|
||||
allowed = notif.get("allowed_ids")
|
||||
if allowed and user_id not in allowed:
|
||||
msg = notif.get("unauthorized_message", "У вас нет доступа к этому уведомлению.")
|
||||
await callback_query.answer(text=msg, show_alert=True)
|
||||
return
|
||||
|
||||
text = notif.get("text", "")
|
||||
show_alert = notif.get("show_alert", False)
|
||||
|
||||
try:
|
||||
await callback_query.answer(text=text, show_alert=show_alert)
|
||||
except Exception:
|
||||
await callback_query.answer(text="Произошла ошибка при отображении уведомления.", show_alert=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "void")
|
||||
async def handle_void_callback(callback_query: CallbackQuery) -> None:
|
||||
"""
|
||||
Обработка пустых callback-запросов (void).
|
||||
Просто отвечает на callback без уведомления.
|
||||
"""
|
||||
try:
|
||||
await callback_query.answer()
|
||||
except Exception:
|
||||
return
|
||||
12
bot/handlers/commands/__init__.py
Normal file
12
bot/handlers/commands/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from aiogram import Router
|
||||
from .start import router as start_cmd_router
|
||||
from .help import router as help_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ('router',)
|
||||
router: Router = Router(name="cmd_router")
|
||||
|
||||
# Подготовка роутера команд
|
||||
router.include_routers(start_cmd_router,
|
||||
help_cmd_router,
|
||||
)
|
||||
47
bot/handlers/commands/help.py
Normal file
47
bot/handlers/commands/help.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery, KeyboardButton
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.templates import msg_photo
|
||||
from bot.utils.interesting_facts import interesting_fact
|
||||
from middleware.loggers import log
|
||||
from bot.bots import BotInfo
|
||||
from configs import COMMANDS, BotEdit
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
CMD: str = "help".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
|
||||
async def help_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /help
|
||||
|
||||
Args:
|
||||
message (Message | CallbackQuery): Сообщение или callback-запрос от пользователя.
|
||||
state (FSMContext): Состояние пользователя бота.
|
||||
"""
|
||||
await state.clear()
|
||||
|
||||
# Создаем клавиатуру с кнопками
|
||||
rkb: ReplyKeyboardBuilder = ReplyKeyboardBuilder()
|
||||
rkb.row(KeyboardButton(text=_("Создать пост📔")))
|
||||
rkb.row(KeyboardButton(text=_("Посмотреть список📋")))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _(
|
||||
"""Добро пожаловать, <a href="{url}">{name}</a>!"""
|
||||
).format(
|
||||
url=message.from_user.url if message.from_user else "",
|
||||
name=message.from_user.first_name if message.from_user else "пользователь",
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(message=message, text=text, file='assets/start.jpg', markup=rkb)
|
||||
57
bot/handlers/commands/start.py
Normal file
57
bot/handlers/commands/start.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery, KeyboardButton
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.templates import msg_photo
|
||||
from bot.utils.interesting_facts import interesting_fact
|
||||
from middleware.loggers import log
|
||||
from bot.bots import BotInfo
|
||||
from configs import COMMANDS, BotEdit
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
CMD: str = "start".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
|
||||
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /start
|
||||
|
||||
Args:
|
||||
message (Message | CallbackQuery): Сообщение или callback-запрос от пользователя.
|
||||
state (FSMContext): Состояние пользователя бота.
|
||||
"""
|
||||
await state.clear()
|
||||
|
||||
# Создаем клавиатуру с кнопками
|
||||
rkb: ReplyKeyboardBuilder = ReplyKeyboardBuilder()
|
||||
rkb.row(KeyboardButton(text=_("Создать пост📔")))
|
||||
rkb.row(KeyboardButton(text=_("Посмотреть список📋")))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _(
|
||||
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||
|
||||
Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших историй!
|
||||
Моя цель — помочь вам сориентироваться и сделать ваши истории куда интереснее!
|
||||
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
|
||||
|
||||
Интересный факт:
|
||||
<blockquote>{fact}</blockquote>
|
||||
"""
|
||||
).format(
|
||||
url=message.from_user.url if message.from_user else "",
|
||||
name=message.from_user.first_name if message.from_user else "пользователь",
|
||||
bot_name=BotEdit.PROJECT_NAME,
|
||||
fact=interesting_fact(),
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(message=message, text=text, file='assets/start.jpg', markup=rkb)
|
||||
174
bot/handlers/inline.py
Normal file
174
bot/handlers/inline.py
Normal file
@@ -0,0 +1,174 @@
|
||||
# BotCode/handlers/inline.py
|
||||
from aiogram import Router
|
||||
from aiogram.types import (
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
InlineQuery,
|
||||
InputTextMessageContent,
|
||||
InlineQueryResultArticle,
|
||||
SwitchInlineQueryChosenChat,
|
||||
CopyTextButton,
|
||||
)
|
||||
from aiogram.utils.markdown import hide_link
|
||||
|
||||
from bot.core import storage
|
||||
from bot.loggers import logs
|
||||
|
||||
router: Router = Router(name="inline_send")
|
||||
|
||||
|
||||
|
||||
def build_markup(buttons_def: list[list[dict]]) -> InlineKeyboardMarkup | None:
|
||||
"""
|
||||
Создаёт InlineKeyboardMarkup из списка описаний кнопок.
|
||||
Поддерживает URL, callback, inline-моды.
|
||||
Обрабатывает "void"-кнопки как callback_data="void".
|
||||
Для switch_inline_query_chosen_chat устанавливает хотя бы один allow_* True.
|
||||
"""
|
||||
if not buttons_def:
|
||||
return None
|
||||
|
||||
rows: list[list[InlineKeyboardButton]] = []
|
||||
for row_idx, row in enumerate(buttons_def):
|
||||
if not isinstance(row, list):
|
||||
logs.warning(f"Некорректный формат ряда кнопок: {row}")
|
||||
continue
|
||||
|
||||
kb_row: list[InlineKeyboardButton] = []
|
||||
for col_idx, b in enumerate(row):
|
||||
if not isinstance(b, dict):
|
||||
logs.warning(f"Некорректный формат кнопки в ряду {row_idx}: {b}")
|
||||
continue
|
||||
|
||||
text = b.get("text", "")
|
||||
if not text:
|
||||
logs.warning(f"Пустой текст кнопки в ряду {row_idx}, колонке {col_idx}")
|
||||
continue
|
||||
|
||||
btn = None
|
||||
try:
|
||||
if "url" in b:
|
||||
url = b["url"]
|
||||
if url.lower().endswith("void"):
|
||||
btn = InlineKeyboardButton(text=text, callback_data="void")
|
||||
else:
|
||||
btn = InlineKeyboardButton(text=text, url=url)
|
||||
elif "switch_inline_query" in b:
|
||||
btn = InlineKeyboardButton(
|
||||
text=text,
|
||||
switch_inline_query=b["switch_inline_query"]
|
||||
)
|
||||
elif "switch_inline_query_current_chat" in b:
|
||||
btn = InlineKeyboardButton(
|
||||
text=text,
|
||||
switch_inline_query_current_chat=b["switch_inline_query_current_chat"]
|
||||
)
|
||||
elif "switch_inline_query_chosen_chat" in b:
|
||||
query = b["switch_inline_query_chosen_chat"]
|
||||
if isinstance(query, dict):
|
||||
siqcc = SwitchInlineQueryChosenChat(
|
||||
query=query.get("query", ""),
|
||||
allow_user_chats=query.get("allow_user_chats", True),
|
||||
allow_group_chats=query.get("allow_group_chats", True),
|
||||
allow_channel_chats=query.get("allow_channel_chats", True),
|
||||
allow_bot_chats=query.get("allow_bot_chats", False),
|
||||
)
|
||||
else:
|
||||
siqcc = SwitchInlineQueryChosenChat(
|
||||
query=query,
|
||||
allow_user_chats=True,
|
||||
allow_group_chats=True,
|
||||
allow_channel_chats=True,
|
||||
allow_bot_chats=False,
|
||||
)
|
||||
btn = InlineKeyboardButton(
|
||||
text=text,
|
||||
switch_inline_query_chosen_chat=siqcc
|
||||
)
|
||||
elif "copy_text" in b:
|
||||
btn = InlineKeyboardButton(
|
||||
text=text,
|
||||
copy_text=CopyTextButton(text=b["copy_text"])
|
||||
)
|
||||
elif "callback_data" in b:
|
||||
btn = InlineKeyboardButton(
|
||||
text=text,
|
||||
callback_data=b["callback_data"]
|
||||
)
|
||||
except Exception as e:
|
||||
logs.error(f"Ошибка при создании кнопки в ряду {row_idx}, колонке {col_idx}: {e}")
|
||||
continue
|
||||
|
||||
if btn:
|
||||
kb_row.append(btn)
|
||||
|
||||
if kb_row:
|
||||
rows.append(kb_row)
|
||||
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
|
||||
|
||||
@router.inline_query()
|
||||
async def inline_query_handler(inline_query: InlineQuery):
|
||||
"""
|
||||
Обрабатывает инлайн-запросы для поиска и отправки постов.
|
||||
Фильтрует посты по приватности и поисковому запросу.
|
||||
"""
|
||||
# Перезагружаем все посты из файлов на случай изменений
|
||||
storage.load_all_posts()
|
||||
|
||||
query = inline_query.query or ""
|
||||
user_id = inline_query.from_user.id
|
||||
username = inline_query.from_user.username or f"user_{user_id}"
|
||||
|
||||
logs.debug(f"Получен инлайн-запрос от {username} (ID: {user_id}): {query}")
|
||||
|
||||
results = []
|
||||
for post_id, post in storage.global_posts.items():
|
||||
try:
|
||||
# Проверка приватности
|
||||
if post.get("private") and post.get("user_id") != user_id:
|
||||
continue
|
||||
|
||||
# Проверка поискового запроса
|
||||
if query and query.lower() not in post_id.lower():
|
||||
continue
|
||||
|
||||
# Тело сообщения
|
||||
text = post.get("text", "")
|
||||
image = post.get("image", "")
|
||||
if image and image.startswith("http"):
|
||||
text = f"{hide_link(image)}{text}"
|
||||
|
||||
# Клавиатура
|
||||
markup = build_markup(post.get("buttons", []))
|
||||
|
||||
results.append(
|
||||
InlineQueryResultArticle(
|
||||
id=post_id,
|
||||
title=f"Пост {post_id}",
|
||||
description=(post.get("text", "")[:100] + "...") if len(post.get("text", "")) > 100 else post.get(
|
||||
"text", ""),
|
||||
input_message_content=InputTextMessageContent(message_text=text),
|
||||
reply_markup=markup
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logs.error(f"Ошибка при обработке поста {post_id}: {e}")
|
||||
continue
|
||||
|
||||
logs.info(f"Отправлено {len(results)} результатов для запроса '{query}' от {username} (ID: {user_id})")
|
||||
|
||||
try:
|
||||
await inline_query.answer(results, cache_time=0, is_personal=True)
|
||||
except Exception as e:
|
||||
logs.error(f"Ошибка при отправке результатов инлайн-запроса: {e}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
'router',
|
||||
'inline_query_handler'
|
||||
]
|
||||
13
bot/handlers/post/__init__.py
Normal file
13
bot/handlers/post/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from aiogram import Router
|
||||
from .create_posts import router as posts_router
|
||||
from .post_list import router as post_list_router
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router", )
|
||||
router: Router = Router(name="post_router")
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
post_list_router,
|
||||
posts_router,
|
||||
)
|
||||
420
bot/handlers/post/create_posts.py
Normal file
420
bot/handlers/post/create_posts.py
Normal file
@@ -0,0 +1,420 @@
|
||||
# bot/modules/create_post.py
|
||||
import re
|
||||
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 bot.core import storage
|
||||
|
||||
router: Router = Router(name="create_post_router")
|
||||
|
||||
|
||||
class PostState(StatesGroup):
|
||||
waiting_for_text = State()
|
||||
waiting_for_privacy = State()
|
||||
waiting_for_id = State()
|
||||
waiting_for_image = State()
|
||||
waiting_for_buttons = State()
|
||||
preview = State()
|
||||
editing_choice = State()
|
||||
|
||||
|
||||
post_id_lock: 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, post_id: str) -> list[list[dict]]:
|
||||
"""
|
||||
Поддерживается синтаксис:
|
||||
Текст | msg:Только для боссов | 123,456 | msg:Для всех остальных
|
||||
Текст | ntf:Без алерта | 789 | msg:Нет доступа
|
||||
"""
|
||||
rows: list[list[dict]] = []
|
||||
button_index = 0
|
||||
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# каждая строка может содержать несколько кнопок через ';'
|
||||
btn_texts = [b.strip() for b in line.split(';') if b.strip()]
|
||||
row: list[dict] = []
|
||||
|
||||
for raw in btn_texts:
|
||||
parts = [p.strip() for p in raw.split('|')]
|
||||
if len(parts) < 2:
|
||||
raise ValueError(f"Неверный формат кнопки: '{raw}'")
|
||||
|
||||
btn = {"text": parts[0]}
|
||||
primary_notification = None
|
||||
primary_alert = False
|
||||
allowed_ids = None
|
||||
unauthorized_message = None
|
||||
|
||||
# обрабатываем параметры слева направо
|
||||
for part in parts[1:]:
|
||||
# URL / void
|
||||
if part == "void":
|
||||
btn["url"] = "http://void"
|
||||
elif part.startswith("http") or part.startswith("tg://"):
|
||||
btn["url"] = part
|
||||
|
||||
# первое уведомление (msg: — с алертом)
|
||||
elif part.startswith("msg:") and primary_notification is None:
|
||||
primary_notification = part.split(":", 1)[1]
|
||||
primary_alert = True
|
||||
|
||||
# первое уведомление без алерта
|
||||
elif part.startswith(("ntf:", "notification:")) and primary_notification is None:
|
||||
primary_notification = part.split(":", 1)[1]
|
||||
primary_alert = False
|
||||
|
||||
# список разрешённых ID
|
||||
elif re.fullmatch(r'\d+(?:\s*,\s*\d+)*', part):
|
||||
allowed_ids = [int(x.strip()) for x in part.split(",")]
|
||||
|
||||
# второе сообщение — для неавторизованных
|
||||
elif part.startswith("msg:") and primary_notification is not None and allowed_ids is not None:
|
||||
unauthorized_message = part.split(":", 1)[1]
|
||||
|
||||
# копирование текста
|
||||
elif part.startswith("copy:"):
|
||||
btn["callback_data"] = f"copy_{uuid.uuid4().hex}"
|
||||
btn["copy_text"] = part.split(":", 1)[1]
|
||||
|
||||
# inline-параметры
|
||||
elif part.startswith("inline:"):
|
||||
btn["switch_inline_query"] = part.split(":", 1)[1]
|
||||
elif part.startswith("inline_current:"):
|
||||
btn["switch_inline_query_current_chat"] = part.split(":", 1)[1]
|
||||
elif part.startswith("inline_chosen:"):
|
||||
btn["switch_inline_query_chosen_chat"] = part.split(":", 1)[1]
|
||||
|
||||
# произвольный callback_data (если ещё не задан)
|
||||
else:
|
||||
if "callback_data" not in btn and "url" not in btn:
|
||||
btn["callback_data"] = part
|
||||
|
||||
# если было уведомление — добавляем поля
|
||||
if primary_notification is not None:
|
||||
btn["callback_data"] = f"bt_{post_id}_{button_index}"
|
||||
button_index += 1
|
||||
btn["notification"] = primary_notification
|
||||
btn["show_alert"] = primary_alert
|
||||
|
||||
if allowed_ids is not None:
|
||||
btn["allowed_ids"] = allowed_ids
|
||||
if unauthorized_message is not None:
|
||||
btn["unauthorized_message"] = unauthorized_message
|
||||
|
||||
# финализируем кнопку
|
||||
row.append(btn)
|
||||
|
||||
if row:
|
||||
rows.append(row)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
# --- Handlers ---
|
||||
@router.message(F.text == "Создать пост📔")
|
||||
async def start_creation(message: Message, state: FSMContext) -> None:
|
||||
await state.set_state(PostState.waiting_for_text)
|
||||
await state.update_data(private=False, buttons=[])
|
||||
await message.reply(
|
||||
text="Отправьте текст вашего поста:\n<i>Вы также можете использовать разметку</i>(<b>жирный</b>, <i>курсив</i> и <u>прочие</u>)!",
|
||||
reply_markup=cancel_button()
|
||||
)
|
||||
|
||||
|
||||
@router.message(PostState.waiting_for_text)
|
||||
async def got_text(message: Message, state: FSMContext) -> None:
|
||||
html_text = message.html_text or message.text or message.caption or ""
|
||||
await state.update_data(text=html_text)
|
||||
await show_preview(message, state)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "toggle_privacy")
|
||||
async def toggle_privacy(cq: CallbackQuery, state: FSMContext) -> None:
|
||||
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(F.data == "continue_creation")
|
||||
async def continue_to_id(cq: CallbackQuery, state: FSMContext) -> None:
|
||||
await state.set_state(PostState.waiting_for_id)
|
||||
await cq.message.edit_text(
|
||||
"Введите уникальный ID поста (латиница, цифры, подчёрки):\n<i>Совет: инициалыРП_роль_тип_номер</i>\nПример: sgrp_dottore_post_4")
|
||||
await cq.answer()
|
||||
|
||||
|
||||
@router.message(PostState.waiting_for_id)
|
||||
async def got_id(message: Message, state: FSMContext) -> None:
|
||||
pid = message.text.strip()
|
||||
if not pid.replace('_', '').isalnum():
|
||||
await message.reply(text="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
|
||||
|
||||
# Создаем клавиатуру с кнопкой "Без изображения"
|
||||
image_markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🚫 Без изображения", callback_data="no_image")],
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_creation")]
|
||||
])
|
||||
|
||||
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/...\n\nСовет! Сохраняйте фотографии в teletype, а после копируйте ссылку на фотографию!\n\nИли нажмите 'Без изображения'.",
|
||||
reply_markup=image_markup
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "no_image", PostState.waiting_for_image)
|
||||
async def no_image_callback(cq: CallbackQuery, state: FSMContext):
|
||||
await state.update_data(image='')
|
||||
await state.set_state(PostState.waiting_for_buttons)
|
||||
await cq.message.delete()
|
||||
|
||||
buttons_markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🚫 Без кнопок", callback_data="no_buttons")],
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_creation")]
|
||||
])
|
||||
|
||||
await cq.message.answer(
|
||||
text="""Отправьте кнопки по шаблону:
|
||||
Кнопка заглушка | void
|
||||
Уведомление | msg:Для вас!
|
||||
Уведомление в закрепе | ntf:Сообщение
|
||||
Кнопка ссылка | https://google.com
|
||||
Копирование | copy:Текст для копирования
|
||||
|
||||
Для уведомлений с ограничением:
|
||||
Уведомление | msg:Для вас! | 123,456 | msg:Для всех остальных!
|
||||
Уведомление без алерта | ntf:Сообщение | 789 | msg:Нет доступа
|
||||
|
||||
Разделять кнопки через ;
|
||||
Кнопка1 | void ; Кнопка2 | void ; ....
|
||||
|
||||
Или нажмите "Без кнопок".""",
|
||||
reply_markup=buttons_markup,
|
||||
parse_mode=None
|
||||
)
|
||||
await cq.answer()
|
||||
|
||||
|
||||
@router.message(PostState.waiting_for_image)
|
||||
async def got_image(message: Message, state: FSMContext) -> None:
|
||||
img: str = message.text.strip()
|
||||
if img.lower() in ('нет', 'no', 'none', 'без изображения'):
|
||||
img: str = ''
|
||||
|
||||
await state.update_data(image=img)
|
||||
await show_preview(message, state)
|
||||
|
||||
|
||||
@router.callback_query(PostState.waiting_for_buttons, F.data == "no_buttons")
|
||||
async def no_buttons_handler(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
await state.update_data(buttons=[])
|
||||
await show_preview(callback.message, state)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(PostState.waiting_for_buttons, F.data == "finish_buttons")
|
||||
async def finish_buttons_handler(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
data = await state.get_data()
|
||||
|
||||
# Формируем финальные кнопки
|
||||
final = []
|
||||
for row in data.get('buttons', []):
|
||||
final_row = []
|
||||
for b in row:
|
||||
btn = {"text": b["text"]}
|
||||
if "url" in b:
|
||||
btn["url"] = b["url"]
|
||||
if "switch_inline_query" in b:
|
||||
btn["switch_inline_query"] = b["switch_inline_query"]
|
||||
if "callback_data" in b:
|
||||
btn["callback_data"] = b["callback_data"]
|
||||
if "notification" in b:
|
||||
btn["notification"] = b["notification"]
|
||||
btn["show_alert"] = b.get("show_alert", False)
|
||||
final_row.append(btn)
|
||||
final.append(final_row)
|
||||
|
||||
await state.update_data(buttons=final)
|
||||
await show_preview(callback.message, state)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "cancel_creation")
|
||||
async def cancel_handler(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
await callback.message.edit_text("❌ Создание поста отменено")
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# --- Preview and Edit Handlers ---
|
||||
async def show_preview(message: Message, state: FSMContext) -> None:
|
||||
data = await state.get_data()
|
||||
text = data.get('text', '')
|
||||
image = data.get('image', '')
|
||||
buttons = data.get('buttons', [])
|
||||
private = data.get('private', False)
|
||||
post_id = data.get('post_id', '')
|
||||
|
||||
# Формируем текст предпросмотра
|
||||
preview_text = f"<b>ПРЕДПРОСМОТР ПОСТА</b>\n\n{text}\n\n"
|
||||
preview_text += f"🆔 ID: <code>{post_id}</code>\n"
|
||||
preview_text += f"🔒 Приватность: {'Приватный' if private else 'Публичный'}\n"
|
||||
|
||||
if image:
|
||||
preview_text += f"🖼 Изображение: {image}\n"
|
||||
else:
|
||||
preview_text += f"🖼 Изображение: отсутствует\n"
|
||||
|
||||
if buttons:
|
||||
preview_text += "\n🔘 Кнопки:\n"
|
||||
for row in buttons:
|
||||
preview_text += " | ".join([btn['text'] for btn in row]) + "\n"
|
||||
else:
|
||||
preview_text += "\n🔘 Кнопки: отсутствуют\n"
|
||||
|
||||
# Клавиатура предпросмотра
|
||||
preview_markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Изменить", callback_data="edit_post"),
|
||||
InlineKeyboardButton(text="Подтвердить", callback_data="confirm_post")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Отменить создание", callback_data="cancel_creation")
|
||||
]
|
||||
])
|
||||
|
||||
await state.set_state(PostState.preview)
|
||||
await message.answer(preview_text, reply_markup=preview_markup, disable_web_page_preview=True)
|
||||
|
||||
|
||||
@router.callback_query(PostState.preview, F.data == "edit_post")
|
||||
async def edit_post_handler(cq: CallbackQuery, state: FSMContext) -> None:
|
||||
# Клавиатура выбора поля для редактирования
|
||||
edit_markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Текст", callback_data="edit_field:text"),
|
||||
InlineKeyboardButton(text="Изображение", callback_data="edit_field:image"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Кнопки", callback_data="edit_field:buttons"),
|
||||
InlineKeyboardButton(text="ID", callback_data="edit_field:id"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Приватность", callback_data="edit_field:privacy"),
|
||||
InlineKeyboardButton(text="Назад", callback_data="back_to_preview"),
|
||||
]
|
||||
])
|
||||
|
||||
await cq.message.edit_text("Выберите что изменить:", reply_markup=edit_markup)
|
||||
await state.set_state(PostState.editing_choice)
|
||||
await cq.answer()
|
||||
|
||||
|
||||
@router.callback_query(PostState.editing_choice, F.data == "back_to_preview")
|
||||
async def back_to_preview(cq: CallbackQuery, state: FSMContext) -> None:
|
||||
await show_preview(cq.message, state)
|
||||
await cq.answer()
|
||||
|
||||
|
||||
@router.callback_query(PostState.editing_choice, F.data.startswith("edit_field:"))
|
||||
async def handle_field_edit(cq: CallbackQuery, state: FSMContext) -> None:
|
||||
field = cq.data.split(":")[1]
|
||||
|
||||
if field == "text":
|
||||
await state.set_state(PostState.waiting_for_text)
|
||||
await cq.message.edit_text("Введите новый текст поста:", reply_markup=cancel_button())
|
||||
|
||||
elif field == "image":
|
||||
await state.set_state(PostState.waiting_for_image)
|
||||
markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🚫 Без изображения", callback_data="no_image")],
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_creation")]
|
||||
])
|
||||
await cq.message.edit_text(
|
||||
"Отправьте новую ссылку на изображение или нажмите 'Без изображения':",
|
||||
reply_markup=markup
|
||||
)
|
||||
|
||||
elif field == "buttons":
|
||||
await state.set_state(PostState.waiting_for_buttons)
|
||||
markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🚫 Без кнопок", callback_data="no_buttons")],
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_creation")]
|
||||
])
|
||||
await cq.message.edit_text(
|
||||
"Отправьте новые кнопки по шаблону или нажмите 'Без кнопок':",
|
||||
reply_markup=markup
|
||||
)
|
||||
|
||||
elif field == "id":
|
||||
await state.set_state(PostState.waiting_for_id)
|
||||
await cq.message.edit_text("Введите новый ID поста:", reply_markup=cancel_button())
|
||||
|
||||
elif field == "privacy":
|
||||
data = await state.get_data()
|
||||
await state.set_state(PostState.waiting_for_privacy)
|
||||
await cq.message.edit_text(
|
||||
"Измените приватность поста:",
|
||||
reply_markup=privacy_markup(data.get('private', False))
|
||||
)
|
||||
|
||||
await cq.answer()
|
||||
|
||||
|
||||
@router.callback_query(PostState.preview, F.data == "confirm_post")
|
||||
async def confirm_post_handler(cq: CallbackQuery, state: FSMContext) -> None:
|
||||
data = await state.get_data()
|
||||
post_id = data['post_id']
|
||||
|
||||
# Сохранение поста в хранилище
|
||||
storage.save_post(post_id, {
|
||||
'text': data['text'],
|
||||
'image': data.get('image', ''),
|
||||
'buttons': data.get('buttons', []),
|
||||
'private': data['private'],
|
||||
'post_id': post_id
|
||||
})
|
||||
|
||||
await cq.message.edit_text(f"✅ Пост успешно создан с ID: <code>{post_id}</code>")
|
||||
await state.clear()
|
||||
await cq.answer()
|
||||
209
bot/handlers/post/post_list.py
Normal file
209
bot/handlers/post/post_list.py
Normal file
@@ -0,0 +1,209 @@
|
||||
from math import ceil
|
||||
from typing import Final
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import (
|
||||
Message, CallbackQuery,
|
||||
InlineKeyboardButton, InlineKeyboardMarkup,
|
||||
SwitchInlineQueryChosenChat, CopyTextButton
|
||||
)
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.utils.markdown import hide_link
|
||||
|
||||
from bot.core import storage
|
||||
from bot.utils import pagination_btn
|
||||
|
||||
router: Router = Router(name="posts_manager_router")
|
||||
|
||||
PAGE_SIZE: Final[int] = 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 = pagination_btn(
|
||||
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: str = "Список ваших постов:"
|
||||
|
||||
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, state: FSMContext):
|
||||
# Сбрасываем состояние перед показом списка
|
||||
await state.clear()
|
||||
await send_posts_list(message=message)
|
||||
|
||||
@router.callback_query(F.data == "open_post_list")
|
||||
async def cb_open_list(cq: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
await send_posts_list(callback_query=cq)
|
||||
await cq.answer()
|
||||
|
||||
@router.callback_query(F.data.startswith("open_post_list_page_"))
|
||||
async def cb_paginate(cq: CallbackQuery, state: FSMContext):
|
||||
try:
|
||||
page = int(cq.data.rsplit("_", 1)[-1])
|
||||
except ValueError:
|
||||
await cq.answer("Некорректная страница", show_alert=True)
|
||||
return
|
||||
await state.clear()
|
||||
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(F.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 = 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")
|
||||
])
|
||||
rows.append(
|
||||
[InlineKeyboardButton(text="Отправить↪️", switch_inline_query=f"{pid}")])
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
|
||||
await cq.message.answer(text=text, reply_markup=keyboard)
|
||||
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, state: FSMContext):
|
||||
"""Удаление поста."""
|
||||
pid = cq.data.replace("delete_post_", "")
|
||||
uid = cq.from_user.id
|
||||
if storage.delete_user_post(uid, pid):
|
||||
await cq.answer(f"Пост {pid} удалён")
|
||||
await state.clear()
|
||||
await send_posts_list(callback_query=cq)
|
||||
else:
|
||||
await cq.answer(text="Не удалось удалить пост", show_alert=True)
|
||||
Reference in New Issue
Block a user