Версия 1.0

This commit is contained in:
Whyverum
2025-05-20 09:12:05 +07:00
commit 0b3b957c0a
34 changed files with 1964 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
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
router = Router(name=__name__)
# Include routers with different priorities
router.include_routers(
cmd_routers,
callback_router,
post_routers,
inline_router
)

View File

@@ -0,0 +1,48 @@
# BotCode/handlers/callback.py
from aiogram import Router, F
from aiogram.types import CallbackQuery
from BotCode.core.storage import storage
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 = callback_query.data
user_id = 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 as e:
try:
await callback_query.answer(text="Произошла ошибка при отображении уведомления.", show_alert=True)
except:
pass
@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 as e:
return

View File

@@ -0,0 +1,7 @@
from aiogram import Router
from .start_cmd import router as start_cmd_router
__all__ = ('router',)
router = Router(name="post_router")
router.include_routers(start_cmd_router,)

View File

@@ -0,0 +1,36 @@
# BotCode/handlers/commands/start_cmd.py
from aiogram import Router, types
from aiogram.filters import CommandStart
router = Router(name=__name__)
__all__ = ("router",)
@router.message(CommandStart())
async def start_cmd(message: types.Message) -> None:
"""
Обработчик команды /start.
:param message: Объект сообщения и информации о нем.
:return: Вывод сообщения для администратора, о выборе режимов работы.
"""
from BotCode.loggers import logs
from BotCode.utils import textmd2
logs.info(text="использовал(а) команду /start", log_type="Start", message=message)
if message.from_user.id:
# Создаем клавиатурный билдер
from aiogram.utils.keyboard import ReplyKeyboardBuilder
rkb: ReplyKeyboardBuilder = ReplyKeyboardBuilder()
rkb.row(types.KeyboardButton(text="Создать пост📔"))
rkb.row(types.KeyboardButton(text="Посмотреть список📋"))
# Отправка фотографии с текстом и клавиатурой
from aiogram.types.input_file import FSInputFile
await message.reply_photo(
photo=FSInputFile('assets/start.jpg'),
caption=textmd2("Добро пожаловать в систему, Босс!"),
reply_markup=rkb.as_markup(resize_keyboard=True)
)
else:
await message.reply(text=textmd2("Простите, вы не мой Босс!❌\nОбратитесь к @verdise!"))

179
BotCode/handlers/inline.py Normal file
View File

@@ -0,0 +1,179 @@
# 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 BotCode.core.storage import storage
from BotCode.utils import textmd2
from BotCode.config import PARSE_MODE
from BotCode.loggers import logs
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 = textmd2(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,
parse_mode=PARSE_MODE
),
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'
]

View 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,
)

View 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()

View 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)