From 0b3b957c0af054b8c18f70229699afc88b3f55a7 Mon Sep 17 00:00:00 2001 From: Whyverum Date: Tue, 20 May 2025 09:12:05 +0700 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B5=D1=80=D1=81=D0=B8=D1=8F=201.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 36 +++ .env | 10 + .gitattributes | 75 +++++ .gitignore | 67 ++++ .idea/PRIMOSTORYFINAL.iml | 14 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .idea/workspace.xml | 142 +++++++++ BotCode/__init__.py | 4 + BotCode/config.py | 19 ++ BotCode/core/__init__.py | 0 BotCode/core/storage.py | 234 ++++++++++++++ BotCode/handlers/__init__.py | 16 + BotCode/handlers/callback.py | 48 +++ BotCode/handlers/commands/__init__.py | 7 + BotCode/handlers/commands/start_cmd.py | 36 +++ BotCode/handlers/inline.py | 179 +++++++++++ BotCode/handlers/post/__init__.py | 10 + BotCode/handlers/post/create_posts.py | 210 +++++++++++++ BotCode/handlers/post/post_list.py | 200 ++++++++++++ BotCode/loggers/__init__.py | 5 + BotCode/loggers/logs.py | 147 +++++++++ BotCode/utils/__init__.py | 3 + BotCode/utils/md2_escape.py | 36 +++ BotCode/utils/pagination.py | 22 ++ BotCode/utils/usernames.py | 22 ++ Dockerfile | 10 + LICENSE | 21 ++ README.md | 31 ++ assets/start.jpg | Bin 0 -> 72016 bytes main.py | 30 ++ posts/posts_6751720805.json | 291 ++++++++++++++++++ pyproject.toml | 19 ++ 34 files changed, 1964 insertions(+) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .idea/PRIMOSTORYFINAL.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 BotCode/__init__.py create mode 100644 BotCode/config.py create mode 100644 BotCode/core/__init__.py create mode 100644 BotCode/core/storage.py create mode 100644 BotCode/handlers/__init__.py create mode 100644 BotCode/handlers/callback.py create mode 100644 BotCode/handlers/commands/__init__.py create mode 100644 BotCode/handlers/commands/start_cmd.py create mode 100644 BotCode/handlers/inline.py create mode 100644 BotCode/handlers/post/__init__.py create mode 100644 BotCode/handlers/post/create_posts.py create mode 100644 BotCode/handlers/post/post_list.py create mode 100644 BotCode/loggers/__init__.py create mode 100644 BotCode/loggers/logs.py create mode 100644 BotCode/utils/__init__.py create mode 100644 BotCode/utils/md2_escape.py create mode 100644 BotCode/utils/pagination.py create mode 100644 BotCode/utils/usernames.py create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/start.jpg create mode 100644 main.py create mode 100644 posts/posts_6751720805.json create mode 100644 pyproject.toml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f90d979 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# .dockerignore: Исключения для Docker сборки +# Игнорировать всё, кроме необходимого для production + +**/.git +**/.gitignore +**/.dockerignore +**/Dockerfile +**/README.md + +# Директории +**/__pycache__ +**/.mypy_cache +**/.pytest_cache +**/.idea +**/.vscode +**/test +**/tests +**/docs +**/examples + +# Файлы +**/*.pyc +**/*.pyo +**/*.pyd +**/*.egg-info +**/*.log +**/*.logs +**/*.sqlite +**/*.db +config/.env +**/docker-compose* + +# Артефакты сборки +**/build +**/dist +**/node_modules diff --git a/.env b/.env new file mode 100644 index 0000000..e22aefa --- /dev/null +++ b/.env @@ -0,0 +1,10 @@ +BOT_TOKEN=7694271285:AAEp9AbA72NRPNIJShDfvcL34awHD67Uvug +BOT_DEBUG_TOKEN=7403842222:AAGUFZEQiICZhsvRHSzjHhQp8YXqKb8jL6I + +ADMIN_ID=[6751720805,1686743480,1979597550,1191474440] + +PARSE_MODE=HTML +POST_DIR=posts + +LOGGING_TO_CONSOLE=False +DEBUG_MODE=False diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f96c285 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,75 @@ +# .gitattributes +# === Git LFS (для больших бинарных файлов, ИИ-моделей, архивов и т.п.) === +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text + +# === Конец строки и автоопределение текста === +* text=auto eol=lf + +# === Текстовые файлы (принудительно) === +*.html text +*.css text +*.js text +*.json text +*.md text +*.yml text +*.yaml text +*.xml text +*.txt text + +# === Изображения === +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.bmp binary +*.webp binary +*.ico binary +*.svg text # SVG можно диффить + +# === Шрифты === +*.eot binary +*.ttf binary +*.woff binary +*.woff2 binary +*.otf binary + +# === GitHub Linguist (для языка проекта на GitHub) === +*.html linguist-language=HTML +*.css linguist-language=CSS +*.js linguist-language=JavaScript +*.json linguist-language=JSON +*.md linguist-language=Markdown diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c7d07d --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# .gitignore: Игнорируемые файлы для Python проектов +# Подробнее: https://github.com/github/gitignore/blob/main/Python.gitignore + +### Python ### +# Виртуальные окружения и настройки +config/.env +../../../../Desktop/PostBot/.venv +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# Кэш интерпретатора +__pycache__/ +*.py[cod] +*$py.class + +# Пакеты и сборки +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.eggs + +# Poetry +poetry.lock +.pypoetry/ + +### Логи и БД ### +*.log +*.logs +*.log.* +*.logs.* +log/ +logs/ +*.sqlite +*.db + +### IDE ### +.idea/ +.vscode/ +*.swp +*.sublime-* + +### OS ### +.DS_Store +Thumbs.db + +### Тестирование ### +.coverage +htmlcov/ +.tox/ +.nox/ +.pytest_cache/ +.mypy_cache/ diff --git a/.idea/PRIMOSTORYFINAL.iml b/.idea/PRIMOSTORYFINAL.iml new file mode 100644 index 0000000..0685815 --- /dev/null +++ b/.idea/PRIMOSTORYFINAL.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2a39176 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..a8f81ca --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1747702717100 + + + + + + + + + \ No newline at end of file diff --git a/BotCode/__init__.py b/BotCode/__init__.py new file mode 100644 index 0000000..8e4a6d3 --- /dev/null +++ b/BotCode/__init__.py @@ -0,0 +1,4 @@ +from .config import * +from .handlers import * +from .utils import * +from .config import * diff --git a/BotCode/config.py b/BotCode/config.py new file mode 100644 index 0000000..152f4b8 --- /dev/null +++ b/BotCode/config.py @@ -0,0 +1,19 @@ +# BotCode/config.py +from os import getenv +from ast import literal_eval +from dotenv import load_dotenv + +# Загружаем переменные из файла .env +load_dotenv() + +BOT_TOKEN: str|None = getenv('BOT_TOKEN', None) +BOT_DEBUG_TOKEN: str|None = getenv('BOT_DEBUG_TOKEN', None) + +ADMIN_ID: tuple[int] = literal_eval(getenv('ADMIN_ID', '[6751720805]')) + +PARSE_MODE: str = getenv('PARSE_MODE', "HTML") + +LOGGING_TO_CONSOLE: bool = getenv('LOGGING_TO_CONSOLE', "False").lower() == 'true' +DEBUG_MODE: bool = getenv('DEBUG_MODE', "False").lower() == 'true' + +POSTS_DIR: str = getenv('POSTS_DIR', "posts") diff --git a/BotCode/core/__init__.py b/BotCode/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BotCode/core/storage.py b/BotCode/core/storage.py new file mode 100644 index 0000000..771169e --- /dev/null +++ b/BotCode/core/storage.py @@ -0,0 +1,234 @@ +import json +from os import path, makedirs, listdir +from typing import Any, Dict, List, Optional +from BotCode.config import POSTS_DIR +from BotCode.loggers import logs + + +class PostStorage: + """Класс для управления хранением постов и связанных уведомлений.""" + + def __init__(self, posts_dir: str = POSTS_DIR): + self.posts_dir = posts_dir + self.global_posts: Dict[str, Dict[str, Any]] = {} + self.notifications: Dict[str, Dict[str, Any]] = {} + self.alert_texts: Dict[str, Dict[str, Any]] = {} + + self._ensure_posts_dir() + self.load_all_posts() + + def _ensure_posts_dir(self, directory: Optional[str] = None) -> None: + """Создаёт директорию для хранения постов, если она не существует.""" + dir_path = directory or self.posts_dir + if not path.isdir(dir_path): + makedirs(dir_path, exist_ok=True) + logs.info( + f"Created posts directory: {dir_path}", + log_type="STORAGE", + ) + + def _get_user_posts_file(self, user_id: int) -> str: + """Возвращает путь к файлу с постами пользователя.""" + return path.join(self.posts_dir, f"posts_{user_id}.json") + + def _update_button_notifications(self, callback_data: str, notification_data: Dict[str, Any]) -> None: + """Регистрирует данные уведомления кнопки во внутренних хранилищах.""" + if not callback_data: + return + self.alert_texts[callback_data] = notification_data + self.notifications[callback_data] = notification_data + + def _process_buttons(self, post_id: str, buttons: List[Any]) -> None: + """ + Обрабатывает кнопки поста, нормализует callback_data и регистрирует уведомления. + Поддерживает различные типы кнопок: callback, url, copy, inline. + """ + if not buttons: + return + + for row_idx, row in enumerate(buttons): + btns = row if isinstance(row, list) else [row] + for col_idx, button in enumerate(btns): + if not isinstance(button, dict): + continue + + if 'callback_data' in button: + cb_data = button['callback_data'] + if not cb_data or not (cb_data.startswith('bt_') or cb_data.startswith('show_alert_')): + prefix = 'show_alert_' if button.get('show_alert') else 'bt_' + button['callback_data'] = f"{prefix}{post_id}_{row_idx}_{col_idx}" + cb_data = button['callback_data'] + + if 'notification' in button: + notification = { + 'text': button['notification'], + 'show_alert': button.get('show_alert', False), + 'allowed_ids': button.get('allowed_ids'), + 'unauthorized_message': button.get('unauthorized_message') + } + self._update_button_notifications(cb_data, notification) + logs.debug( + f"Registered notification for {cb_data}", + log_type="STORAGE", + ) + + def load_user_posts(self, user_id: int) -> Dict[str, Any]: + """Загружает посты пользователя из файла.""" + file_path = self._get_user_posts_file(user_id) + try: + if path.isfile(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + posts = json.load(f) + if isinstance(posts, dict): + return posts + logs.warning( + f"Invalid posts format in {file_path}", + log_type="STORAGE", + ) + except json.JSONDecodeError as e: + logs.error( + f"JSON decode error in {file_path}: {str(e)}", + log_type="STORAGE", + ) + except Exception as e: + logs.error( + f"Error loading posts from {file_path}: {str(e)}", + log_type="STORAGE", + ) + return {} + + def save_user_posts(self, user_id: int, posts: Dict[str, Any]) -> None: + """ + Сохраняет посты пользователя в файл и обновляет внутренние хранилища. + Обрабатывает кнопки и уведомления перед сохранением. + """ + if not isinstance(posts, dict): + logs.error( + "Invalid posts format, expected dict", + log_type="STORAGE", + ) + return + + for post_id, post in posts.items(): + if isinstance(post, dict) and 'buttons' in post: + self._process_buttons(post_id, post['buttons']) + + file_path = self._get_user_posts_file(user_id) + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(posts, f, ensure_ascii=False, indent=4) + logs.info( + f"Saved posts for user {user_id}", + log_type="STORAGE", + ) + except Exception as e: + logs.error( + f"Error saving posts to {file_path}: {str(e)}", + log_type="STORAGE", + ) + return + + # Обновление кэша: перезагружаем записи этого пользователя + # Удаляем старые записи + for pid in list(self.global_posts): + if pid in posts: + self.global_posts.pop(pid, None) + # Загружаем свежие + fresh = self.load_user_posts(user_id) + for pid, post in fresh.items(): + if isinstance(post, dict) and 'buttons' in post: + self._process_buttons(pid, post['buttons']) + self.global_posts[pid] = post + + def delete_user_post(self, user_id: int, post_id: str) -> bool: + """Удаляет пост пользователя и связанные уведомления. Возвращает статус операции.""" + user_posts = self.load_user_posts(user_id) + if post_id not in user_posts: + logs.warning( + f"Post {post_id} not found for user {user_id}", + log_type="STORAGE", + ) + return False + + post = user_posts.pop(post_id) + notification_count = 0 + if isinstance(post.get('buttons'), list): + for row in post['buttons']: + btns = row if isinstance(row, list) else [row] + for button in btns: + if isinstance(button, dict): + cb = button.get('callback_data') + if cb and cb in self.alert_texts: + self.alert_texts.pop(cb) + self.notifications.pop(cb, None) + notification_count += 1 + logs.debug( + f"Removed {notification_count} notifications for post {post_id}", + log_type="STORAGE", + ) + + # Сохраняем и обновляем кэш + self.save_user_posts(user_id, user_posts) + self.global_posts.pop(post_id, None) + logs.info( + f"Deleted post {post_id} for user {user_id}", + log_type="STORAGE", + ) + return True + + def is_post_available(self, post_id: str) -> bool: + """Проверяет доступность идентификатора поста.""" + return post_id not in self.global_posts + + def load_all_posts(self) -> None: + """Загружает все посты из файлов в рабочей директории.""" + self.global_posts.clear() + self.alert_texts.clear() + self.notifications.clear() + + self._ensure_posts_dir() + loaded_files = 0 + loaded_posts = 0 + + try: + for filename in listdir(self.posts_dir): + if filename.endswith('.json'): + user_id_str = filename[len('posts_'):-len('.json')] + try: + user_id = int(user_id_str) + except ValueError: + logs.warning( + f"Invalid filename format: {filename}", + log_type="STORAGE", + ) + continue + + posts = self.load_user_posts(user_id) + for pid, post in posts.items(): + if isinstance(post, dict) and 'buttons' in post: + self._process_buttons(pid, post['buttons']) + self.global_posts[pid] = post + loaded_posts += 1 + loaded_files += 1 + except Exception as e: + logs.error( + f"Error loading all posts: {str(e)}", + log_type="STORAGE", + ) + + logs.info( + f"Loaded {loaded_posts} posts from {loaded_files} files", + log_type="STORAGE", + ) + + def get_post(self, post_id: str) -> Optional[Dict[str, Any]]: + """Возвращает пост по идентификатору или None если не найден.""" + return self.global_posts.get(post_id) + + def get_notification(self, callback_data: str) -> Optional[Dict[str, Any]]: + """Возвращает данные уведомления для указанного callback.""" + return self.notifications.get(callback_data) + + +# Инициализация хранилища при импорте модуля +storage = PostStorage() diff --git a/BotCode/handlers/__init__.py b/BotCode/handlers/__init__.py new file mode 100644 index 0000000..87ad758 --- /dev/null +++ b/BotCode/handlers/__init__.py @@ -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 +) diff --git a/BotCode/handlers/callback.py b/BotCode/handlers/callback.py new file mode 100644 index 0000000..ae5f596 --- /dev/null +++ b/BotCode/handlers/callback.py @@ -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 diff --git a/BotCode/handlers/commands/__init__.py b/BotCode/handlers/commands/__init__.py new file mode 100644 index 0000000..76bfde9 --- /dev/null +++ b/BotCode/handlers/commands/__init__.py @@ -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,) diff --git a/BotCode/handlers/commands/start_cmd.py b/BotCode/handlers/commands/start_cmd.py new file mode 100644 index 0000000..825a4ea --- /dev/null +++ b/BotCode/handlers/commands/start_cmd.py @@ -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!")) diff --git a/BotCode/handlers/inline.py b/BotCode/handlers/inline.py new file mode 100644 index 0000000..83686a4 --- /dev/null +++ b/BotCode/handlers/inline.py @@ -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' +] \ No newline at end of file diff --git a/BotCode/handlers/post/__init__.py b/BotCode/handlers/post/__init__.py new file mode 100644 index 0000000..f111393 --- /dev/null +++ b/BotCode/handlers/post/__init__.py @@ -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, +) diff --git a/BotCode/handlers/post/create_posts.py b/BotCode/handlers/post/create_posts.py new file mode 100644 index 0000000..727495f --- /dev/null +++ b/BotCode/handlers/post/create_posts.py @@ -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 +Жирный +Курсив +Подчёркнутый +Зачёркнутый +Моноширинный +
Предварительно отформатированный
+Ссылка +""" + ), + 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() diff --git a/BotCode/handlers/post/post_list.py b/BotCode/handlers/post/post_list.py new file mode 100644 index 0000000..6e0ab77 --- /dev/null +++ b/BotCode/handlers/post/post_list.py @@ -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) diff --git a/BotCode/loggers/__init__.py b/BotCode/loggers/__init__.py new file mode 100644 index 0000000..2e432b8 --- /dev/null +++ b/BotCode/loggers/__init__.py @@ -0,0 +1,5 @@ +# BotLibrary/loggers/__init__.py +# Инициализация модуля loggers, для настройки логеров + +# Экспортирование модулей во внешние слои проекта +from .logs import * diff --git a/BotCode/loggers/logs.py b/BotCode/loggers/logs.py new file mode 100644 index 0000000..14d4fc8 --- /dev/null +++ b/BotCode/loggers/logs.py @@ -0,0 +1,147 @@ +""" +Модуль логирования для Telegram-бота. + +Особенности: +* Вывод логов в консоль и/или файл +* Автоматическая ротация и удержание +* Форматирование с информацией о системе, типе события и пользователе +* Удобные методы для разных уровней логирования +""" + +from sys import stderr +from pathlib import Path +from typing import Optional, Final, Union + +from loguru import logger +from aiogram.types import Message, User + +try: + from config import LogConfig +except ImportError: + class LogConfig: + """Запасные настройки логирования, если config недоступен.""" + CONSOLE: Final[bool] = True + FILE: Final[bool] = True + DIR: Final[Path] = Path('Logs') + ROTATION: Final[str] = '100 MB' + RETENTION: Final[str] = '7 days' + +# Настройка экспорта в модули +__all__ = ['Logs', 'logs'] + + +class Logs: + """ + Класс для работы с логированием через loguru. + """ + _SYSTEM_NAME: Final[str] = 'PRIMO' # Исправлено: убран обратный слэш + _LOG_FORMAT: Final[str] = ( + '{time:YYYY-MM-DD HH:mm:ss.SSS} | ' # Исправлено форматирование времени + '{extra[system]}-{extra[log_type]} | ' + '{extra[user]} | {message}' + ) + + @staticmethod + def _format_user(message: Optional[Message]) -> str: + """ + Форматирует информацию о пользователе из сообщения. + """ + if not message or not message.from_user: + return '@System' + user: User = message.from_user + return f"@{user.username}" if user.username else f"id{user.id}" + + @classmethod + def _log(cls, + level: Union[str, int], + text: str, + log_type: str, + message: Optional[Message] = None) -> None: + """Внутренний метод логирования.""" + user_ctx = cls._format_user(message) + logger.bind( + system=cls._SYSTEM_NAME, + user=user_ctx, + log_type=log_type, + ).log(level, text) + + @classmethod + def setup(cls, start: bool = True) -> None: + """Инициализация логирования: консоль и/или файл.""" + logger.remove() + + # Консольный вывод + if getattr(LogConfig, 'CONSOLE', False): + logger.add( + stderr, + format=cls._LOG_FORMAT, + colorize=True, + level='DEBUG', + filter=lambda rec: rec['extra'].get('log_type') != 'DEBUG' + ) + + # Файловый вывод с ротацией + if getattr(LogConfig, 'FILE', False): + log_dir = getattr(LogConfig, 'DIR', Path('logs')) + log_dir.mkdir(parents=True, exist_ok=True) + logger.add( + log_dir / 'bot.log', + rotation=getattr(LogConfig, 'ROTATION', '100 MB'), + retention=getattr(LogConfig, 'RETENTION', '7 days'), + format=cls._LOG_FORMAT, + level='DEBUG', + enqueue=True, + backtrace=True, + diagnose=True + ) + + # Добавляем вызов start() если нужно + if start: + cls.start() + + @classmethod + def start(cls, text: str = 'Запуск бота...', log_type: str = 'START') -> None: + """Логирование старта приложения.""" + cls._log(level='INFO', text=text, log_type=log_type) + + @classmethod + def debug(cls, + text: str, + log_type: str = 'DEBUG', + message: Optional[Message] = None) -> None: + cls._log(level='DEBUG', text=text, log_type=log_type, message=message) + + @classmethod + def info(cls, + text: str, + log_type: str = 'INFO', + message: Optional[Message] = None) -> None: + cls._log(level='INFO', text=text, log_type=log_type, message=message) + + @classmethod + def warning(cls, + text: str, + log_type: str = 'WARNING', + message: Optional[Message] = None) -> None: + cls._log(level='WARNING', text=text, log_type=log_type, message=message) + + @classmethod + def error(cls, + text: str, + log_type: str = 'ERROR', + message: Optional[Message] = None) -> None: + cls._log(level='ERROR', text=text, log_type=log_type, message=message) + + @classmethod + def exception(cls, + text: str, + exception: Exception, + log_type: str = 'EXCEPTION', + message: Optional[Message] = None) -> None: + full_text = f"{text}\nException: {exception!r}" + cls._log(level='ERROR', text=full_text, log_type=log_type, message=message) + + +# Инициализация экземпляра логгера +logs = Logs() +logs.setup() diff --git a/BotCode/utils/__init__.py b/BotCode/utils/__init__.py new file mode 100644 index 0000000..c74a0a3 --- /dev/null +++ b/BotCode/utils/__init__.py @@ -0,0 +1,3 @@ +from .md2_escape import * +from .usernames import * +from .pagination import * diff --git a/BotCode/utils/md2_escape.py b/BotCode/utils/md2_escape.py new file mode 100644 index 0000000..65e8ac2 --- /dev/null +++ b/BotCode/utils/md2_escape.py @@ -0,0 +1,36 @@ +# BotCode/utils/md2_escape.py +from BotCode.config import PARSE_MODE + +# Настройка экспорта в модули +__all__ = ("textmd2",) + +def textmd2(msg: str, + parse_mode: str = PARSE_MODE, + special_chars: str = r"_*[]()~`>#+-=|{}.!") -> str: + """ + Экранирует специальные символы MarkdownV2 в переданном тексте. + + :param msg: Входной текст в виде строки. + :param parse_mode: Формат форматирования ('MarkdownV2' или 'HTML'). + :param special_chars: Символы, которые необходимо экранировать. + + :return: Экранированный текст или исходный текст, если формат HTML. + :raises TypeError: Если передан не строковый тип данных. + :raises ValueError: Если parse_mode задан некорректно. + """ + from re import sub, escape + + if not isinstance(msg, str): + raise TypeError(f"Ожидается строка, но получено {type(msg).__name__}") + + if not isinstance(parse_mode, str): + raise TypeError(f"parse_mode должен быть строкой, но получено {type(parse_mode).__name__}") + + if parse_mode.strip().lower() == "html": + return msg + + elif parse_mode in {"markdownv2", "markdown"}: + return sub(rf"([{escape(special_chars)}])", r"\\\1", msg) + + else: + raise ValueError(f"Недопустимое значение parse_mode: '{parse_mode}'. Ожидалось 'HTML' или 'MarkdownV2'") diff --git a/BotCode/utils/pagination.py b/BotCode/utils/pagination.py new file mode 100644 index 0000000..c139cb0 --- /dev/null +++ b/BotCode/utils/pagination.py @@ -0,0 +1,22 @@ +# BotCode/utils/pagination.py +from typing import List +from aiogram.types import InlineKeyboardButton + +# Настройка экспорта в модули +__all__ = ('create_pagination_buttons',) + +def create_pagination_buttons(action: str, + page: int = 0, + total_posts: int = 0, + bt_page: int = 5) -> List[InlineKeyboardButton]: + """Создает кнопки для пагинации.""" + navigation_buttons = [] + if page > 0: + navigation_buttons.append(InlineKeyboardButton( + text="←", callback_data=f"{action}_page_{page - 1}" + )) + if (page + 1) * bt_page < total_posts: + navigation_buttons.append(InlineKeyboardButton( + text="→", callback_data=f"{action}_page_{page + 1}" + )) + return navigation_buttons diff --git a/BotCode/utils/usernames.py b/BotCode/utils/usernames.py new file mode 100644 index 0000000..bfba6ae --- /dev/null +++ b/BotCode/utils/usernames.py @@ -0,0 +1,22 @@ +# BotCode/utils/username.py +from aiogram.types import Message + +# Настройка экспорта в модули +__all__ = ('username', ) + +# Функция получения юзера или ID пользователя +def username(message: Message) -> str: + """ + Возвращает юзернейм пользователя из сообщения, или ID, если юзернейм не указан. + + :param message: Объект сообщения из aiogram. + :return: Строка с юзернеймом пользователя или его ID. + :raises ValueError: Если в сообщении отсутствует информация о пользователе. + """ + try: + if message.from_user: + return f"@{message.from_user.username}" if message.from_user.username else f"@{message.from_user.id}" + raise ValueError("Информация о пользователе отсутствует в сообщении.") + + except ValueError as e: + raise e # Перебрасываем ошибку выше для дальнейшей обработки diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc7d007 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM mwalbeck/python-poetry:2.1-3.11 + +WORKDIR /PostBot + +COPY pyproject.toml poetry.lock ./ +RUN poetry install --no-interaction --no-root --only main + +COPY ../../../../Desktop/PostBot . + +CMD ["poetry", "run", "python", "-m", "main"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..46329e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2025] [Лейн] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7da29b9 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Создание поста +new_post = { + "id": "cat_post", + "author_id": 123, + "mod": "HTML", + "type": "photo", + "text": "Мой котик!", + "media": "cat.jpg", + "private": True, + "allowed_users": [456, 789], + "buttons": [[{ + "type": "share", + "name": "Поделиться", + "params": {"message": "Посмотрите этого котика!"} + }]] +} + +post_id = storage.create_post(new_post) + +# Получение поста +post = storage.get_post(post_id, user_id=456) # Доступ разрешен +post = storage.get_post(post_id, user_id=000) # Доступ запрещен + +# Поиск постов +results = storage.search_posts("котик", user_id=456) + +# Обновление поста +storage.update_post(post_id, updater_id=123, updates={"text": "Новый текст"}) + +# Удаление поста +storage.delete_post(post_id, deleter_id=123) \ No newline at end of file diff --git a/assets/start.jpg b/assets/start.jpg new file mode 100644 index 0000000000000000000000000000000000000000..41fea9fb6190a82c7d91a972581be10689c05276 GIT binary patch literal 72016 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�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 None: + from aiogram.types import User + from BotCode.loggers import logs + from BotCode.handlers import router as main_router + + bot_info: User = await bot.get_me() + logs.start(text=f"Бот @{bot_info.username} запущен!") + + dp.include_router(main_router) + + await dp.start_polling(bot) + +if __name__ == "__main__": + from asyncio import run + run(main()) diff --git a/posts/posts_6751720805.json b/posts/posts_6751720805.json new file mode 100644 index 0000000..653a358 --- /dev/null +++ b/posts/posts_6751720805.json @@ -0,0 +1,291 @@ +{ + "anketa_dottore_butterfly": { + "user_id": 6751720805, + "text": "┏━━━━━━━━━━━━━━━━━━━━┓\n Вы сидите в кабинете управляющего и ждёте его возвращения. Время тянется бесконечно долго. Вам всё больше кажется, что ожидание длится целую вечность. \nРешив немного развеяться, вы начинаете прохаживаться по кабинету и замечаете, что персональный компьютер управляющего включён. Подойдя ближе, вы понимаете, что он забыл его выключить. \nПовиляв мышкой, вы видите, как на рабочем столе появляется изображение — картина с портретом владельца этого устройства.\n※─────────────【₪】─────────────※\n Добро пожаловать, Дотторе.\n — ..\n — Досье №67517...\n — teminаl\n — experiment_X01.tar\n — wallpaper.png\n※─────────────【₪】─────────────※ \n Кажется, вы проверили все файлы... Нужно поскорее прибраться, пока не вернулся управляющий. Вряд ли он обрадуется такому \"подарку\".\n┗━━━━━━━━━━━━━━━━━━━━┛", + "image": "https://img4.teletype.in/files/3f/b9/3fb9c695-28cb-417c-9577-6fe8fb7e6fe1.jpeg", + "private": true, + "buttons": [ + [ + { + "text": "Прочитать досье📋", + "url": "https://teletype.in/@whyverum/DottoreMagicButterflies" + } + ], + [ + { + "text": "Рабочий стол🖼", + "callback_data": "show_alert_0", + "show_alert": true, + "notification": "Подпись внутри файла: \"Я помню кем ты был. Не изменись..\"" + } + ], + [ + { + "text": "Эксперимент👀", + "callback_data": "show_alert_1", + "show_alert": false, + "notification": "Никто. Не должен. Знать об этом." + } + ] + ] + }, + "dottore_butterfly_post1": { + "user_id": 6751720805, + "text": "╔══════════════════════╗\n \n ◯ ⃝ꦿДотторе: Творец бытия 𖧷۪۪‌⃟ꦽ⃟\n\n╰─────────────────────╮ \n╚══════════════════════╝\n〇 ° ੦ ੦ ੦ ° ੦ ᅠᅠᅠᅠ\n♾️ Дᴧя ʍᴇня ᴄущᴇᴄᴛʙуᴇᴛ дʙᴇ ʙᴏɜʍᴏжнᴏᴄᴛи: ᴧибᴏ дᴏбиᴛьᴄя ᴨᴏᴧнᴏᴦᴏ ᴏᴄущᴇᴄᴛʙᴧᴇния ᴄʙᴏих ᴨᴧᴀнᴏʙ, ᴧибᴏ ᴨᴏᴛᴇᴩᴨᴇᴛь нᴇудᴀчу. Дᴏбьюᴄь — ᴄᴛᴀну ᴏдниʍ иɜ ʙᴇᴧичᴀйɯих ʙ иᴄᴛᴏᴩии, ᴨᴏᴛᴇᴩᴨᴧю нᴇудᴀчу — буду ᴏᴄуждᴇн, ᴏᴛʙᴇᴩᴦнуᴛ и ᴨᴩᴏᴋᴧяᴛ.\n\n 〇 Ѵαɗε αʈ Ѵεɾʝʈʈαʈεϻ! 〇 ᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠ \n ┏━━━━━━━━━━━━━━━━━━━┓\n || Дотторе; Фурина; Наблюдатель; ???;\n┊□࿔°⊹—– Новые встречи. Эксперимент №1.\n \"Живые воды\".\n\n ❉ ...Дотторе, резко оглянулся. Теперь вместо уютного кабинета, он видел пустоту, что поглощала остатки его кабинета. Всё, что оставалось, — это стол и старое офисное кресло, на котором тот сидел. Атмосфера всепоглощающей бездны начинала давить на него, но все равно управляющий старался держать себя под контролем. Он медленно встал с кресла и оперся о стол. Смотря вдаль, он начал замечать странный силуэт, чем-то напоминающий ██████████.❉\n\n ⥤ Ха! Значит, это ты, тот кто перенес меня сюда, верно?!\n\n ➢ ...\n\n ⥤ Аномалия? Зараженный? Нет. Ты нечто иное, верно?\n\n ➢ Д̶ͬо̍͢к̱ͪт0̺͞рͤ͡, я̸́ у́͂в̮́_̙̉рͧ͝е̝͒н̖ͤ в̰͘ в̨̂а̅ͯшИ̃̕х͎̋ с̵̙п2̴ͣо̛̥сͯ͡0̦͢б̙ͦн̛̼о̩̐с̦̑т̃Я̼̇х̀̐.\n\n [...] \n\n ❉ Сущность перед Дотторе, будто улыбнулась и рассмеялась после новой партии в шахматы. Оно смотрело прямо на управляющего, будто тот загорелся новой идеей.❉\n\n ⥤ Значит, вот оно что.. Хахах! Как же я сам не догадался об этом! Эфир! Это..!\n\n ➢ П̠͞р̤͘о̗͌щ̷͋а̺͒й͚̚т͎ͥе̛ͨ, Д͚ͬоͭ̚к͂̋т̾͟о̗ͤр̥̇.\n\n\n ❉ Силуэт ██████████ начал растворяться в бесконечной пустоте. Спустя часы или, быть может, дни — собеседник управляющего исчез бесследно. Дотторе с улыбкой, поглаживал свой подбородок, захлестываясь новыми идеями. Но одна мысль не давала ему покоя.. ❉ \n\n ⥤ Как же мне выбраться отсюда? Кхм, а если...\n ┗━━━━━━━━━━━━━━━━━━━┛\n . ·.° ✤╮•.✦╯•╰─✣.·\n • ·.°│.•. . ·°\n ❉°.", + "image": "https://img3.teletype.in/files/6a/53/6a53563b-6f39-4b11-841c-376de0d0cf36.jpeg", + "private": true, + "buttons": [ + [ + { + "text": "Сфокусироваться♾️", + "url": "https://teletype.in/@whyverum/dottore_butterfly_post1" + } + ], + [ + { + "text": "Отчет📋", + "callback_data": "bt_dottore_butterfly_post1_0", + "show_alert": true, + "notification": "Леди Фурина обладает способностями управления водой и создания водяных существ. На данный момент ей присвоен класс D — из-за довольно лёгких условий содержания и быстрого подавления.", + "allowed_ids": [ + 7940956521, + 1987289929, + 1781218883, + 1004666697, + 6751720805, + 5539791027, + 1848629094, + 7670414891, + 1102904738, + 1723370206, + 6718320347 + ], + "unauthorized_message": "🔒 Вы не являетесь официальным сотрудником по приказу S1-2025! Запросите повышение доступа!" + }, + { + "text": "Зов💠", + "callback_data": "bt_dottore_butterfly_post1_1", + "show_alert": true, + "notification": "✡️⚐🕆 👍✌️☠️ 💧❄️✋☹️☹️ 👎✌️☠️👍☜.", + "allowed_ids": [ + 6714603161, + 7483863010, + 1993133001, + 8162774433, + 1010196821, + 785169037, + 481787136, + 2040384869, + 6356873908, + 2046536572, + 6960477141, + 1398573474 + ], + "unauthorized_message": "В̺͓̅ӹ̻̘́ н͉͋̈́е̷̶̥ п̮͘͞о̩̄͡д̫͊͘о̨̱̍й̝ͥ͠д̷̛͆е̵̸͙т̴ͫ̆е̼̌͢." + } + ], + [ + { + "text": "Сдаться⛔️", + "callback_data": "bt_dottore_butterfly_post1_2", + "show_alert": false, + "notification": "Нет, эта история должна, идти по другому пути." + } + ], + [ + { + "text": "Леди Фурине🔖", + "callback_data": "bt_dottore_butterfly_post1_3", + "show_alert": true, + "notification": "И как же вам пост, милая Леди?", + "allowed_ids": [ + 6714603161 + ], + "unauthorized_message": "📶 Загрузка: ■■■■■■■□□□ 70%" + } + ] + ] + }, + "dottore_butterfly_post2": { + "user_id": 6751720805, + "text": "╔══════════════════════╗\n \n ◯ ⃝ꦿДотторе: Творец бытия 𖧷۪۪‌⃟ꦽ⃟\n\n╰─────────────────────╮ \n╚══════════════════════╝\n〇 ° ੦ ੦ ੦ ° ੦ ᅠᅠᅠᅠ\n♾️ Дᴧя ʍᴇня ᴄущᴇᴄᴛʙуᴇᴛ дʙᴇ ʙᴏɜʍᴏжнᴏᴄᴛи: ᴧибᴏ дᴏбиᴛьᴄя ᴨᴏᴧнᴏᴦᴏ ᴏᴄущᴇᴄᴛʙᴧᴇния ᴄʙᴏих ᴨᴧᴀнᴏʙ, ᴧибᴏ ᴨᴏᴛᴇᴩᴨᴇᴛь нᴇудᴀчу. Дᴏбьюᴄь — ᴄᴛᴀну ᴏдниʍ иɜ ʙᴇᴧичᴀйɯих ʙ иᴄᴛᴏᴩии, ᴨᴏᴛᴇᴩᴨᴧю нᴇудᴀчу — буду ᴏᴄуждᴇн, ᴏᴛʙᴇᴩᴦнуᴛ и ᴨᴩᴏᴋᴧяᴛ.\n\n 〇 Ѵαɗε αʈ Ѵεɾʝʈʈαʈεϻ! 〇 ᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠᅠ \n ┏━━━━━━━━━━━━━━━━━━━┓\n || Дотторе; Фурина; Наблюдатель; ???;\n┊□࿔°⊹—– Эксперимент №1.\n \"Живые воды\". Истинное лицо..\n\n ❉ ...То, что вы всё же увидели, начало не просто пугать, а скорее шокировать. Тьма, что всего пару минут назад казалась бесконечно пустой, окрасилась в ярко-кровавый оттенок. Но страннее всего был запах — плотный, всепроникающий, с отчётливыми нотами гари.\nПод ногами вдруг начал вырисовываться горный выступ, с которого тьма стекала вниз, словно по трубе. Нет — скорее, по \"тьмапроводу\". Бессмысленные ассоциации лезли в голову, пытаясь хоть как-то защитить рассудок — безуспешно.\nИ вот, вдалеке, стал виден источник запаха: охваченный огнём город. Оттуда поднимались целые столбы дыма. Со скалы хорошо просматривались пылающие улицы, горящие пятиэтажки и центр города — с полуразрушенной церковью. С неохотой вы решаете спуститься вниз. В Бездне было лишь одно различимое место — город пламени.❉ \n\n ➢ Что здесь произошло?..\n\n ❉ Всё вокруг было объято огнём. Каждый шаг сопровождался треском горящей древесины и далекими, резкими криками. Всё сложнее становилось воспринимать происходящее.\nЧем ближе вы подходили к центру, тем сильнее нарастал внутренний дискомфорт. Единственным кажущимся безопасным местом была церковь — пугающе тихая, но почему-то манящая.\nИ тогда вы начали замечать их. Манекены. Они были повсюду: сидели за столами, стояли за прилавками, их части валялись на дороге, никому не нужные. Всё это напоминало сцены из старых фильмов ужасов...\nЗвон колокола прервал мысли — одинокий, чистый звук, будто разгонявший атмосферу апокалипсиса. И затем...\nМанекены ожили.\nК их телам спустились с неба кровавые нити — теперь они были марионетками. Армия неживых, безликих \"деревянных солдатиков\" медленно, но уверенно начала приближаться.\nИ среди них вы увидели его. Фигура мужчины — белый халат, напоминающий медицинский, маска в стиле чумных докторов XVII века и волосы неестественного лазурного цвета. Он сразу выделялся на фоне остальных.\nОн посмотрел вам прямо в лицо... и рассмеялся. Смех был безумен — словно психопат наслаждался происходящим.❉ \n\n ⥤ Значит, это ты? Ха-ха-ха-ха!\n\n ➢ ...\n\n ⥤ Человек, которому дана возможность переписывать миры? Бог, стоящий передо мной?!\n\n ➢ Ч-что...?\n\n ❉ Доктор взмахнул рукой, указывая пальцем вниз. Манекены за спиной тут же сдавили ваши плечи, опуская на колени. Они не причиняли вреда — просто подчинялись приказу своего полководца.\nДоктор продолжал смеяться, наслаждаясь моментом. Он подошёл ближе, вальяжной походкой, и, наконец, схватил вас за подбородок, подняв голову вверх, заставляя смотреть в глаза.❉ \n\n ⥤ Наблюдатель... Смотри на меня. Я — одно из ваших творений. Так почему на коленях стою не я?\n\n ⥤ Так что ты скажешь теперь?)\n ┗━━━━━━━━━━━━━━━━━━━┛\n . ·.° ✤╮•.✦╯•╰─✣.·\n • ·.°│.•. . ·°\n ❉°", + "image": "https://img3.teletype.in/files/68/61/6861ff76-b7ec-418a-999f-d3522356c97d.jpeg", + "private": true, + "buttons": [ + [ + { + "text": "Si vis pacem, para bellum!⚜️", + "url": "https://teletype.in/@whyverum/dottore_butterfly_post2" + } + ], + [ + { + "text": "Панталоне💳", + "callback_data": "bt_dottore_butterfly_post2_0", + "show_alert": true, + "notification": "11010000 10111101 11010000 10110101", + "allowed_ids": [ + 5539791027, + 7051557370 + ], + "unauthorized_message": "...Уважаемый Панталоне, в связи с тем, что наши исследования берут новый оборот, нам нужно пересчитать бюджет и отправить отчетность о нем в вышестоящие..." + }, + { + "text": "Аль-Хайтам🧰", + "callback_data": "bt_dottore_butterfly_post2_1", + "show_alert": true, + "notification": "11010000 10110010 11010000 10110101 11010001 10000000 11010001 10001100", + "allowed_ids": [ + 1723370206, + 7051557370 + ], + "unauthorized_message": "...смотрителю Аль-Хайтаму, следует явиться как можно скорее в кабинет управляющего Дотторе, для того чтобы провести беседу о его прикрепленных подопытных..." + } + ], + [ + { + "text": "Яэ Мико✉️", + "callback_data": "bt_dottore_butterfly_post2_2", + "show_alert": true, + "notification": "11010001 10000010 11010000 10111110 11010000 10111100 11010001 10000011", + "allowed_ids": [ + 7940956521, + 7051557370 + ], + "unauthorized_message": "...я назначаю вас, заведующим эксперимента \"Вопль-11\", мы должны узнать истину о нашем \"знакомом\"." + }, + { + "text": "Сяо🎭", + "callback_data": "bt_dottore_butterfly_post2_3", + "show_alert": true, + "notification": "11010000 10111010 11010001 10000010 11010000 10111110", + "allowed_ids": [ + 8018592486, + 7051557370 + ], + "unauthorized_message": "Ⲧⲁⲕ ⲧы ⲧⲟⲯⲉ ⲙⲟⲏⲥⲧⲣ?..." + } + ], + [ + { + "text": "Сянь Юнь📝", + "callback_data": "bt_dottore_butterfly_post2_4", + "show_alert": true, + "notification": "11010000 10110011 11010000 10111110 11010000 10110010 11010000 10111110 11010001 10000000 11010000 10111000 11010001 10000010", + "allowed_ids": [ + 7511347907, + 7051557370 + ], + "unauthorized_message": "...недавние медицинские анализы образцов смутили меня. Проведите повторную полную диагностику, особенно над леди Фуриной, у нее замечены определенные проблемы..." + }, + { + "text": "Мидзуки🔑", + "callback_data": "bt_dottore_butterfly_post2_5", + "show_alert": true, + "notification": "11010001 10000000 11010001 10000011 11010000 10111010 11010000 10110000 11010000 10111100 11010000 10111000", + "allowed_ids": [ + 1497624978, + 7051557370 + ], + "unauthorized_message": "...в заключении, передай остальным, что скоро я проверю работу каждого сотрудника. Пусть все будут готовы и исправят свои недочеты..." + } + ], + [ + { + "text": "Я все еще могу танцевать.💠", + "callback_data": "bt_dottore_butterfly_post2_6", + "show_alert": true, + "notification": "Я ⲣⲁⲇ, ⳡⲧⲟ ⲃы ⲡⲟⲏяⲗυ ⲙⲉⲏя. Ⲧⲟⲅⲇⲁ ⲡⲩⲧь ⲏⲁⳡⲏёⲧⲥя ⲥ ⲥⲟⲕⲣыⲧυя. Ⲡⲣυⲕⲣⲟύⲧⲉ ⲡⲁⲗьцⲉⲙ ⳝⲉⲥⲕⲟⲏⲉⳡⲏⲟⲥⲧь — υ ⲃы ⲃⲥё ⲡⲟύⲙёⲧⲉ.", + "allowed_ids": [ + 7483863010, + 1993133001, + 7051557370, + 6714603161, + 8162774433, + 7846830127, + 2051969619, + 2040384869, + 2046536572, + 6960477141, + 6721342628, + 1686805799, + 5991527415 + ], + "unauthorized_message": "Ты дуʍᴀᴇɯь я буду ʙᴇᴩиᴛь,Тᴏʍу, ᴋᴛᴏ ᴏᴛняᴧ их ᴄʙᴏбᴏду?Тᴏʍу, ᴋᴛᴏ ᴄᴛᴏᴧьᴋᴏ ʙᴩᴇʍᴇниВᴇᴩиᴧ ʙ ᴧжиʙыᴇ нᴀʍᴇᴩᴇнья?" + } + ] + ] + }, + "TestButton": { + "user_id": 6751720805, + "text": "Тест для проверки\nЖирный\nКурсив\nПодчёркнутый\nЗачёркнутый\ninline-код\n
блок кода
\nссылка\nСкрытый текст\n
текст
", + "image": "https://img4.teletype.in/files/f2/47/f247b03d-6197-419a-86c6-10a20c12b2f7.png", + "private": false, + "buttons": [ + [ + { + "text": "Кнопка заглушка", + "url": "http://void" + } + ], + [ + { + "text": "Уведомление", + "callback_data": "bt_TestButton_0", + "show_alert": true, + "notification": "Для вас!" + }, + { + "text": "Увед", + "callback_data": "bt_TestButton_1", + "show_alert": false, + "notification": "Нет! Не для вас!" + } + ], + [ + { + "text": "Кнопка ссылка", + "url": "http://google.com" + } + ], + [ + { + "text": "Копирование", + "copy_text": "Копирование текста!" + } + ], + [ + { + "text": "Для только одного!", + "callback_data": "bt_TestButton_2", + "show_alert": true, + "notification": "только для босса)", + "allowed_ids": [ + 6751720805 + ], + "unauthorized_message": "Вы не босс!" + } + ], + [ + { + "text": "Инлайн 0", + "switch_inline_query": "" + } + ], + [ + { + "text": "инлайн1", + "switch_inline_query": "ЗАПРОСИЩЕ" + }, + { + "text": "инлайн 2", + "switch_inline_query_current_chat": "ЧАТОВАЯ" + }, + { + "text": "инлайн 3", + "switch_inline_query_chosen_chat": "чего?" + } + ] + ] + } +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3d044a1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "primostorybot" +version = "1.4" +description = "Бот для отправки постов с кнопками и разметкой сообщений" +authors = [ + {name = "Verum",email = "sergeyzavalin@outlook.com"} +] +license = {text = "None"} +readme = "README.md" +requires-python = ">=3.10,<4.0" +dependencies = [ + "aiogram (>=3.20.0.post0,<4.0.0)", + "loguru (>=0.7.3,<0.8.0)", + "dotenv (>=0.9.9,<0.10.0)" +] + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api"