# Этот модуль - наглядный пример для интересующихся разработчиков # Здесь (TODO: планируется добавить и объяснить в принципе все функции # и возможности модулей... мета теги, инлайн и т.д.), чтобы Ты смог # начать создавать модули и стал их разработчиком. Добро пожаловать! # ⬇️ Обрати внимание! ⬇️ # В принципе, этот код не планируется адаптировать в пошаговый гайд. # Скорее, я сделал справочник по функционалу и возможностям модулей с # теорией и примерами использования. # То есть, если у тебя появился вопрос "А как сделать это?", "А как # это работает?" - этот пример поможет тебе найти ответ. # ⬆️ Обрати внимание! ⬆️ # ⬇️ Обновление (Heroku 2.1.0+) ⬇️ # С Heroku 2.1.0 инлайн-бот работает через herokutl, а не через aiogram. # aiogram оставлен только ради обратной совместимости - self.inline.bot # по-прежнему доступен для тех, кто хочет работать с ботом напрямую # ⬆️ Обновление (Heroku 2.1.0+) ⬆️ # Каждая "глава" отмечена тегами # region и # endregion. Если вы читаете # этот код в редакторе с Code View/Minimap (например, в VSCode), вы можете # ориентироваться по этим тегам на миникарте # Направление модуля: актуальные версии Heroku (с парой помарок для совместимости с Hikka-подобными) # region ВСТУПЛЕНИЕ # Итак. Перед тобой пустой файл. С чего начнём? Немного теории: # - Модули пишутся на Herokutl (форк Telethon с примочками юзербота). Инлайн-бот # тоже работает на нём с Heroku 2.1.0 (см. заметку про 2.1.0+ в начале файла) # - Общий вид и начальные требования модуля настраиваются с помощью # мета-тегов (они необязательны) #- Всё, что нужно для твоего модуля из модулей от юзербота: loader # (вся настройка модуля, команд) и utils (инструменты для облегчения # каких-либо задач) # endregion ВСТУПЛЕНИЕ # region META-ТЕГИ # 1. Версия модуля. Длина кортежа не валидируется, но до Heroku 2.0.0 # невозможно было открыть .help у модулей с версией короче трёх элементов. # Ради совместимости со старыми юзерботами лучше всегда указывать три элемента __version__ = ("beta", "test", 7) # будет отображено как "vbeta.test.7" # 2. Разработчик - имя, юзернейм или канал разработчика модуля # meta developer: @ZetGo # ^^^^^^ - может быть чем угодно. Если указан юзернейм # твоего канала, то юзербот предложит человеку # подписаться на него после установки модуля # 3. Минимальная версия юзербота - версия, с которой модуль будет работать. # Если версия юзербота старее - юзер не сможет # установить модуль, пока не обновит юзербот до нужной версии # scope: heroku_min 1.7.2 # для Hikka (допустим, если модуль использует функции, которых нет в Hikka) # scope: hikka_min 1.7.2 # 4. Отключить отображение всех команд в сообщении об успешной установке модуля # scope: disable_onload_docs # 5. Баннер - ссылка на медиа, которое будет отображено в поисковиках # модулей. Может быть GIF, фото или видео. Ссылка обязательно # должна указывать на файл, а не на страницу с ним # meta banner: https://github.com/ZetGoHack/Nullmod/raw/main/20250401_100043.jpg # 6. Инлайн-иконка - ссылка на фото, которое будет отображено в инлайн-режиме, # если у модуля есть инлайн-команды. Ссылка должна указывать # на файл, а не на страницу с ним # meta pic: https://github.com/ZetGoHack/Nullmod/raw/main/20250401_100043.jpg # 7. Langpack - ссылка на yaml-файл с переводами для модуля. Пример формата такого # файла: https://github.com/ZetGoHack/TestingModules/blob/main/chess.yml. # P.S. ⚠️ Не полагайся только на свой лангпак по ссылке. Он не всегда может нормально # загрузиться, допустим, из-за проблем с сетью. Если лангпак не загрузится, а в модуле не # будет указано внутренних strings, то вместо всех строк будет написано "Unknown strings" # до следующей перезагрузки юб # packurl: https://github.com/ZetGoHack/TestingModules/raw/main/chess.yml # endregion # region ЗАВИСИМОСТИ # Если твоему модулю нужны какие-то внешние библиотеки/системные пакеты, которых нет в юзерботе/системе, # то их можно указать в зависимостях. Юзербот при установке модуля попытается установить эти # библиотеки/пакеты. Если установка не удалась, то модуль не будет установлен # 1. Requires - установка зависимостей через pip. Список через пробел # requires: requests beautifulsoup4 git+https://github.com/ZetGoHack/TStickers.git # ^^^^ - так можно установить библиотеку прямо из репозитория # 2. Packages - если твоему модулю нужны какие-то системные пакеты, которые можно установить через менеджер # твоей системы, юзербот попытается установить их. Если установка не удалась, то модуль не будет установлен # FROM HEROKU VERSION 2.0.0+ # packages: ffmpeg # 3. FFmpeg - если модуль использует функции, для которых нужен ffmpeg. При отсутствии юзербот вернёт ошибку # пользователю с требованием установить ffmpeg. Модуль не будет установлен ##scope: ffmpeg #^ тут лишний # # ⚠️ scope-тег обрабатывается раньше установки packages. Не рекоммендую использовать этот scope для проверки наличия FFmpeg, # если ты указал его как зависимость в packages. scope попросту не пустит загрузку к установке зависимостей # 4. Inline - укажите, если модуль использует инлайн-бота для работы. Юзербот проверит, работает ли он. # Если инлайн-бот не инициализирован, будет возвращена ошибка. Модуль не будет установлен # scope: inline # endregion import asyncio from typing import TYPE_CHECKING, Literal from ..types import BotInlineCall, InlineCall, InlineQuery # ⚠️ Module и Library - специально импортируются под TYPE_CHECKING # Loader ищет класс твоего модуля как первый попавшийся в файле подкласс от Module (vars(module).values()), # а Module - подкласс самого себя. Если импортировать его ДО объявления своего класса - loader найдёт сам Module # раньше твоего класса, создаст пустой Module() и юзербот загрузит модуль как буквально нерабочую пустышку. # Аннотации, использующие эти типы, надо оборачивать в строку if TYPE_CHECKING: from ..types import Module, Library import telethon # region ИМПОРТЫ # В самой основе используется loader. Он отвечает за регистрацию команд, хендлеров и т.д. # В общем, за всё, что нужно для работы модуля from .. import loader # В utils - всякие инструменты для облегчения задач. Получение аргументов, отправка сообщений и т.д. from .. import utils # Все импорты из Telethon (или hikkatl) берутся из herokutl. Сам юзербот на уровне загрузчика патчит # импорты с "Telethon", или "hikkatl" на "herokutl". Если ты делаешь универсальный модуль под Hikka и Heroku, # то рекомендую просто использовать общий "from telethon import ...". Оба юб заменят его на свою библиотеку from herokutl.tl.custom import Message # такой импорт уже будет принят только Heroku from telethon.tl.types import UpdateUserStatus, UserStatusOnline, UserStatusOffline # для raw_handler # endregion # region СТРУКТУРА МОДУЛЯ # Сам модуль - это класс, который наследуется от loader.Module. Несёт в себе self.db, self.client и т.д. # Все доступные атрибуты можно посмотреть в dir(self) # tds - translatable docstring. Этот декоратор позволяет использовать многоязычные описания для команд и модуля. @loader.tds class TheBestExampleEverMod(loader.Module): """Это описание модуля. Оно отображается после установки и в меню команды `.help TheBestExampleEverMod`""" # region ПЕРЕВОДЫ # strings - словарь строк для модуля. Ключи - это идентификаторы строк, по которым к ним можно обратиться в коде. # Значения - это сами строки на разных языках. Язык выбирается пользователем в настройках юзербота. # Если строка не найдётся для выбранного языка, будет показано значение по умолчанию (то есть взято из strings) # Если строка не найдётся вообще, будет показано "Unknown strings" # Переводы указываются в словарях strings_{код языка}, например, strings_ru для русского. Код языка - это # стандартный код ISO 639-1 (ru, en, de и т.д.) # ⚠️ В коде всегда используйте ключи из strings - strings["ключ"]. Юзербот сам по установленному языку человека выбирает источник строк strings = { "name": "TheBestExampleEver", # имя модуля. Этот ключ обязателен. В других переводах можно не указывать, будет использоваться это значение "_cls_doc": "This is the best example module ever!", # описание самого модуля. Он заменяет указанный выше docstring. Переводимый ключ (можно использовать в других языках) "cfg_changed": "Config value changed to {}!", # строка для использования в коде "loading": "🐱 Your result is loading...", # не забываем про возможность использовать html разметку в сообщениях! "loaded": "😶 Just kidding! I haven't loaded anything, here are the arguments you entered: {}", # строка с аргументами команды. В коде можно использовать .format для подстановки аргументов "user_online": "🟢 {} is online!", "user_offline": "🔴 {} is offline!", "inl__a_message": "This is a simple message", "inl__a_message_desc": "Example of a simple message", "inl__a_message_text": "You entered in inline arguments: {}!", "inl__mrk_example": "Example of a message with buttons", "inl__a_photo": "This is an example of a message with photo", "inl__a_photo_desc": "Example of a message with photo for inline", "inl__a_photo_caption": "This is a photo with caption!", "inl__gallery": "Example gallery", "inl__gallery_desc": "Example of inline gallery from query", "inl__gallery_caption": "This photo was opened from query_gallery", "inl__button": "Button in inline", "inl__insert_query": "Insert query", "inl__a_gif": "Example GIF", "inl__a_gif_desc": "Example of a gif result", "inl__a_gif_caption": "This is a gif with caption!", "inl__a_video": "Example video", "inl__a_video_desc": "Example of a video result", "inl__a_video_caption": "This is a video with caption!", "inl__a_file": "Example file", "inl__a_file_desc": "Example of a document result", "inl__a_file_caption": "This is a PDF file!", } strings_ru = { "_cls_doc": "Это лучший пример модуля!", "cfg_changed": "Значение конфига изменено на {}!", "loading": "🐱 Ваш пример загружается...", "loaded": "😶 Шучу! Я ничего не загружал, вот аргументы, которые вы ввели: {}", "user_online": "🟢 {} вошёл в сеть!", "user_offline": "🔴 {} вышел из сети!", "inl__a_message": "Это простое сообщение", "inl__a_message_desc": "Пример простого сообщения", "inl__a_message_text": "Вы ввели в аргументы инлайна: {}!", "inl__mrk_example": "Пример сообщения с кнопками", "inl__a_photo": "Это пример сообщения с фото", "inl__a_photo_desc": "Пример сообщения с фото для инлайна", "inl__a_photo_caption": "Это фото с подписью!", "inl__gallery": "Пример галереи", "inl__gallery_desc": "Пример инлайн-галереи из запроса", "inl__gallery_caption": "Это фото открыто через query_gallery", "inl__button": "Кнопка в инлайне", "inl__insert_query": "Вставить запрос", "inl__a_gif": "Пример GIF", "inl__a_gif_desc": "Пример результата с gif", "inl__a_gif_caption": "Это gif с подписью!", "inl__a_video": "Пример видео", "inl__a_video_desc": "Пример результата с видео", "inl__a_video_caption": "Это видео с подписью!", "inl__a_file": "Пример файла", "inl__a_file_desc": "Пример результата с документом", "inl__a_file_caption": "Это PDF-файл!", } strings_jp = { # JP вместо JA - исключение! юзербот использует ключ JP для японского. "_cls_doc": "これは最高のモジュールの例です!", "cfg_changed": "構成値が{}に変更されました!", "loading": "🐱 結果を読み込んでいます...", "loaded": "😶 冗談です!何も読み込んでいません。入力した引数は次のとおりです:{}", "user_online": "🟢 {}がオンラインです!", "user_offline": "🔴 {}がオフラインです!", "inl__a_message": "これは単純なメッセージです", "inl__a_message_desc": "単純なメッセージの例", "inl__a_message_text": "インライン引数に入力した内容:{}!", "inl__mrk_example": "ボタン付きメッセージの例", "inl__a_photo": "これは写真付きメッセージの例です", "inl__a_photo_desc": "インラインの写真付きメッセージの例", "inl__a_photo_caption": "これはキャプション付きの写真です!", "inl__gallery": "ギャラリーの例", "inl__gallery_desc": "クエリから開くインラインギャラリーの例", "inl__gallery_caption": "この写真は query_gallery から開かれました", "inl__button": "インラインのボタン", "inl__insert_query": "クエリを挿入", "inl__a_gif": "GIF の例", "inl__a_gif_desc": "GIF 結果の例", "inl__a_gif_caption": "これはキャプション付きの GIF です!", "inl__a_video": "動画の例", "inl__a_video_desc": "動画結果の例", "inl__a_video_caption": "これはキャプション付きの動画です!", "inl__a_file": "ファイルの例", "inl__a_file_desc": "ドキュメント結果の例", "inl__a_file_caption": "これは PDF ファイルです!", } # endregion ПЕРЕВОДЫ # region ИНИЦИАЛИЗАЦИЯ # Инициализация. Обычно тут регистрируется конфиг self.config модуля (если он нужен) def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue( option="example_option", # имя опции. По этому имени к ней можно обратиться в коде default=42, # значение по умолчанию doc="This is an example option", # описание опции. Отображается в конфиге модуля validator=loader.validators.Integer(), # валидатор для опции. Проверяет, что значение, которое пользователь вводит в конфиге, соответствует требованиям. # ℹ️ Все валидаторы ищите в Heroku/heroku/validators.py # Список классов валидаторов: ["Boolean", "Integer", "Float", "String", "Choice", "MultiChoice", # "Series", "Link", "RegExp", "TelegramID", "Union", "NoneType", # "Hidden", "Emoji", "EntityLike", "RandomLink"] on_change=self._on_config_change, # функция, которая будет вызвана при изменении опции. Может быть асинхронной, или синхронной. # Ничего не будет передано в аргументы ), loader.ConfigValue( "hidden_value", "some_hidden_data", "Data that should not be displayed in the settings menu", validator=loader.validators.Hidden( # Hidden - буквально скрывает данные и заменяет на * в основном меню модуля. > loader.validators.String(), # При открытии меню самого значения, данные можно "подсмотреть", нажав на кнопку > ), # [Показать значение]. Значение раскрывается только в его меню. Незаменимо для > ), # конфигов API ключей и прочих чувствительных данных loader.ConfigValue( "list_example", [1, 2, 3], "This is an example list option", validator=loader.validators.Series( # валидатор для списков. Проверяет каждый элемент списка на соответствие требованиям. # В данном случае - что каждый элемент - это число от 0 до 42 включительно # ⚠️ list/dict-опции нельзя редактировать напрямую, типа - self.config["list_example"].append(4). # Оно не пройдёт через __setitem__, поэтому не сохранится в базу и не вызовет on_change. # Переприсваивай целиком: self.config["list_example"] = self.config["list_example"] + [4] loader.validators.Integer(minimum=0, maximum=42) ), on_change=self._on_config_change, ), loader.ConfigValue("nothing"), # а можно просто оставить лишь одно название значения. Это, конечно, ничего полезного не даст пользователю... loader.ConfigValue( "watcher", True, "Should wathcer be running?", validator=loader.validators.Boolean(), ), loader.ConfigValue( "raw_handler", True, "Should raw handler be runnig?", validator=loader.validators.Boolean(), ), loader.ConfigValue( "handler_targets", [], "A targets list to track online", validator=loader.validators.Series( loader.validators.Integer(), ), ), ) self._loop_iterations = 0 # переменная для loop # Действия при загрузке модуля через .dlm или .lm. Обычно используется для начальной настройки, которая не должна выполняться при каждом запуске модуля. # Вызывается ДО client_ready. В self уже готовы все атрибуты (self.client, self.db и т.д.) async def on_dlmod(self): """ Called after the module is first time loaded with .dlmod or .loadmod Possible use-cases: - Send reaction to author's channel message - Create asset folder - ... ⚠️ Note, that any error there will not interrupt module load, and will just send a message to logs with verbosity INFO and exception traceback """ # < - взято из докстринга функции pass # Клиент готов. Вызывается после __init__ и config_loaded. # Обычно тут загружаются какие-то данные из базы, или выполняются действия, которые должны быть при каждом запуске модуля (например, установка базовых # значений в self из датабазы) async def client_ready(self): pass # endregion ИНИЦИАЛИЗАЦИЯ async def _on_config_change(self): # region ДАТАБАЗА # self.set, self.get, self.pointer - установка, получение и указатель в датабазе модуля соответсвенно. Сокращения от self.db.*(self.__class__.__name__, ...) # self.db.set(owner, key, value) - установить значение ключа. Возвращает True или False в зависимости от результата установки значения. # ⚠️ Значения должны быть сериализуемыми в JSON! Вы не можете сохранить кастомные классы! # ℹ️ Учти, что числовые ключи будут автоматически конвертироваться в string-ключ. 1 > "1". Указывай в ключе только string! # self.db.get(owner, key, default) - получить значение ключа. Если такого не сущетсвует - возвращается значение из default # ℹ️ Учти, что числовые ключи будут автоматически конвертироваться в string-ключ. 1 > "1". Указывай в ключе только string! # self.db.pointer(owner, key, default) - получить указатель (в случае, если в значении стоит dict, или list). # Можно обращаться, как pointer["key"] = 42. Это изменение будет сразу же сохранено в датабазе, а указателем можно # пользоваться дальше # ℹ️ Учти, что числовые ключи будут автоматически конвертироваться в string-ключ. 1 > "1". Указывай в ключе только string! value = self.get("cfg_value") if (opt := self.config["example_option"]) != value: self.set("cfg_value", opt) else: return # endregion ДАТАБАЗА await self.client.send_message( self.client.heroku_me.id, self.strings["cfg_changed"].format(self.config["example_option"]) ) # region КОМАНДЫ # Команды. Функции, вызываемые юзерботом при вводе команды (имя команды - это имя функции). Регистрируются в юб с помощью декоратора @loader.command, # или `cmd` в конце функции (def examplecmd). При написании команды вызывается с `herokutl.tl.custom.Message` в аргументах @loader.command(ru_doc="Пример описания для команды на русском", jp_doc="コマンドの日本語の説明の例") async def example(self, message: Message): """Example description for a command in English""" # Аргументы команды. Следующие функции из utils (это лишь часть) возвращают аргументы команды, исключая её саму. ".example arg1" > "arg1" args_tuple = utils.get_args(message) # (arg1, arg2, arg3) - разделение по пробелу args_string = utils.get_args_raw(message) # arg1 arg2 arg3 - сырой текст без html оформления args_html = utils.get_args_html(message) # arg1 arg2 arg3 - сырой текст с html оформлением # У тебя есть три варианта взаимодействия с сообщением для ответа - редактирование; ответ; ответ с инлайн-формой. Вот первые два варианта: # Вариант 1 - utils.answer m = await utils.answer(message, self.strings["loading"]) # utils.answer - универсальный способ ответить на команду. Если команда # была отправлена от вашего имени (с парой помарок), то сообщение будет отредактировано. # Если от другого пользователя - будет отправлено новое сообщение с ответом. # Если указан reply_markup, то будет отправлено новое сообщение с инлайн-кнопками. # Подробнее о кнопках - в другом примере (L TODO) # Всегда возврващает объект затронутого сообщения (редакт или отправленное) await asyncio.sleep(1) # делаем задержку между отправкой "загрузка..." и результатом. # Частые запросы к API могут привести к флудвейту. Не забывайте про задержки! # Вариант 2 - использовать методы из Telethon напрямую await m.reply(self.strings["loaded"].format(args_html)) # все сообщения обрабатываются с помощью html-разметки автоматически. Рекомендуется использовать только её # endregion КОМАНДЫ async def example_callback(self, call: InlineCall, message_id: int, chat_id: int, **kwargs): # call - объект InlineCall (с Heroku 2.1.0 не связан с aiogram). Содержит в себе всю информацию о клике по кнопке, # а также методы для взаимодействия с ним (ответить на клик, отредактировать сообщение и т.д.) # message_id, chat_id - аргументы, которые мы передали в "args" кнопки. Они нужны для того, # чтобы понять, на какое сообщение была нажата кнопка и в каком чате оно находится. # kwargs - аргументы из "kwargs" кнопки. В данном случае - {"random_num": 42} await call.answer( f"You clicked the button! Here are the arguments you passed: {message_id}, {chat_id}, {kwargs['random_num']}", show_alert = True, ) async def _back(call: InlineCall): await call.edit( self.strings["inl__mrk_example"], reply_markup=self.repl_mrkp, ) await call.edit( "Ты только что нажал на кнопку! Ты можешь изменить сообщение прямо через call.edit(), как тут", reply_markup={"text": "Назад", "callback": _back}, ) async def example_input(self, call: InlineCall, data: str): # call - объект InlineCall (с Heroku 2.1.0 не связан с aiogram). Содержит в себе всю информацию о клике по кнопке, # а также методы для взаимодействия с ним (ответить на клик, отредактировать сообщение и т.д.) # data - текст, который пользователь ввёл в инлайн-режиме после клика на кнопку с "input" await call.answer(f"You entered in inline mode: {data}") async def example_gallery_photo(self): # функция должна вернуть ссылку на фотографию, или список из них return ["https://github.com/ZetGoHack/TestingModules/raw/main/ex_thumb.jpg"] @loader.callback_handler() async def example_callback_handler(self, call: BotInlineCall): if call.data != "example/hello": return await call.answer("You clicked the button with custom data! {}".format(call.data)) # region ИНЛАЙН-КОМАНДЫ @loader.command(ru_doc="Пример команды с инлайн-кнопками", jp_doc="インラインボタンを使用したコマンドの例") async def exmplmkp(self, message: Message): """Example of a command with inline buttons""" # region КНОПКИ # Инлайн-кнопки - словарь, или список словарей с определёнными ключевыми параметрами. Кнопки имеют несколько типов. # region СТРУКТУРА КНОПОК # Полная структура кнопок: self.repl_mrkp = reply_markup = [ [ # первый ряд { "text": "кнопка с обработчиком клика", "callback": self.example_callback, "args": (message.id, message.chat_id), "kwargs": {"random_num": 42}, }, # первая кнопка ], [ # второй ряд { "text": "кнопка без обработчика", "data": "example/hello", } # вторая кнопка ], [ # третий ряд { "text": "кнопка-ссылка", "url": "https://t.me/ZetGo", }, # третья кнопка ], [ # четвёртый ряд { "text": "кнопка приёма ввода", "input": "Пример приёма ввода", "handler": self.example_input, }, # четвёртая кнопка { "text": "кнопка копирования", "copy": "copy text", }, # ... { "text": "кнопка-ответ", "action": "answer", "message": "Ты клацнул по кнопке!", "disable_security": True, "show_alert": True, }, ], [ { "text": "webapp-кнопка", "url": ( # спустя 20 минут тестов и одного вопроса к одному крутому человеку выяснилось, # что web_app кнопки невозможно отправить через инлайн (только в личке)... # В любом случае, многие клиенты отображают url кнопку с webapp-подобными ссылками # как webapp кнопку "https://t.me/xgift?startapp=profile-1226061708_ref-b1e785f69fb4ac258f97898b37b72536" ), }, ], [{"text": "Закрыть форму", "action": "close"}], [{"text": "Выгрузить форму", "action": "unload"}], ] # endregion СТРУКТУРА КНОПОК # region ТИПЫ КНОПОК # ⚠️ Типы кнопок не могут использоваться вместе в одной кнопке! # Типы кнопок: # "callback" - функция-обработчик клика по кнопке. Функция должна быть # асинхронной и принимать как минимум один аргумент - CallbackQuery # "input" - заголовок для результата, выданного в инлайн-режиме. При клике на кнопку # юзербот откроет инлайн для ввода текста. После ввода вызывается хендлер из # "handler" # "handler" - функция-обработчик инлайн результата. Работает в паре с "input". Функция # должна быть асинхронной и принимать как минимум два аргумента - # CallbackQuery и текст, введённый пользователем в инлайн-режиме # "data" - callback data для кнопки. Вместо генерации рандомной callback data, которая # привязывается к обработчику, ты указываешь свою. Эта data не будет привязана # ни к чему. В таком случае, тебе нужно создать callback_handler с фильтром на # эту data. # ⚠️ Не оставляй хендлер без фильтра на data, так как юзербот при получении # callback отправляет его абсолютно всем хендлерам абсолютно всех модулей! # "url" - кнопка-ссылка. При клике открывает ссылку. Ссылка должна быть валидной, # иначе кнопка не будет создана # "web_app" - кнопка для открытия webapp. При клике открывает webapp. Может быть # строкой (url), или словарём с ключом "url". # На практике, Телеграм позволяет использовать её только в обычных, # сообщениях бота, не в инлайн-формах # "copy" - кнопка для копирования текста внутри неё "copy": "текст для копирования" # "action" - быстрые действия при нажатии кнопки: # 1. "action": "answer" - работает вместе с "show_alert" и "message". # "message" - текст, отображаемый при клике на кнопку # "show_alert" - флаг, отвечающий за вид отображения # сообщения # 2. "action": "close" - закрыть (удалить и выгрузить) инлайн-форму # 3. "action": "unload" - только выгрузить форму из памяти юзербота. Сообщение # останется, но кнопки больше не будут обрабатываться # endregion ТИПЫ КНОПОК # region ПАРАМЕТРЫ КНОПОК # Параметры кнопок: # "text" - текст кнопки. Обязательный параметр для всех типов кнопок # "args" - tuple или list аргументов для передачи в "callback", или "handler" # "kwargs" - dict аргументов (аргумент: значение) для передачи в "callback", или # "handler". О последовательности аргументов в кнопках - (L535) # "always_allow" - список ID пользователей (whitelist), которым всегда разрешено нажимать # на кнопку. Проверка безопасности пропустит этих пользователей # "disable_security" - отключить проверку безопасности для кнопки. Проверка проверяет, # является ли нажавший пользователь одним из владельцев юзербота, # или есть ли его ID в списке разрешённых пользователей # "force_me" - разрешить кнопку только владельцу юзербота # "style" - цвет кнопки: "danger" - красный, "primary" - синий, "success" - зелёный # "emoji_id" - document_id кастомного эмодзи для иконки кнопки. Не работает в Inline-сообщениях, # только в обычных сообщениях бота # endregion ПАРАМЕТРЫ КНОПОК # О последовательности аргументов # Аргументы в функции-хендлеры передаются в последовательности func(call, *args, **kwargs), # или func(call, data, *args, **kwargs) в зависимости от типа хендлера (callback/input) # Сообщение message при наличии reply_markup будет удалено, а вместо него будет открыта инлайн-форма await utils.answer(message, self.strings["inl__mrk_example"], reply_markup=reply_markup) # utils.answer с reply_markup - это почти всегда вызов self.inline.form (кроме случая, когда message # это уже call). await self.inline.form( self.strings["inl__mrk_example"], message, reply_markup, always_allow=[25, 50, 149], # Да, эти оба параметра могут быть локальными для каждой кнопки, disable_security=False, # так и глобальными для всей формы - но это OR логикв: # True с любой стороны убирает проверку. Если True указан глобально, # то локально для кнопки защиту с False уже не включить ) # endregion КНОПКИ # region БОТ # self.inline.bot - клиент бота. До Heroku 2.1.0+ (Hikka-вайбы) - Aiogram клиент, после - Telethon. # Через него можно работать напрямую от лица бота @loader.command(ru_doc="Пример отправки сообщения с кнопками от лица бота") async def exmplnotify(self, message: Message): """Example of sending message with buttons from the bot""" # region ГЕНЕРАЦИЯ КНОПОК reply_markup = self.inline.generate_markup( # generate_markup - тот же парсер кнопок, что и для инлайн-сообщений. Полезен, когда нужно # отправить сообщение от имени бота с кнопками, созданными через удобный # обработчик юзербота. (Разумеется ты можешь создавать свои кнопки через # telethon-билдер Button и ловить его через сырой callback_handler для # каких-то более сложных задач. Но удобный генератор ведь легче :3) [ [ { "text": "Да", "callback": self.exmplnotify_yes, "args": (await message.link(thread=True),), }, {"text": "Нет", "callback": self.exmplnotify_no}, ], ] ) # endregion ГЕНЕРАЦИЯ КНОПОК # вообще, self.inline.bot - ещё один прокси для обратной совместимости со старыми модулями на # Aiogram-логике. В self.inline.bot.client уже лежит реальный клиент Telethon-бота, через него # и можно инвокать методы await self.inline.bot.send_message( self.tg_id, # тут мы, разумеется, укажем айди владельца юзербота "Привет, это тестовое сообщения от TheBestExampleEver", reply_markup=reply_markup, ) await utils.answer(message, "Уведомление отправлено в личку с ботом") async def exmplnotify_yes(self, call: BotInlineCall, source_message_link: str): await call.answer("Подтверждено!") await utils.answer(call, f"Готово! (из сообщения {source_message_link})") async def exmplnotify_no(self, call: BotInlineCall): await call.answer("Отменено") await utils.answer(call, "Отменено юзером") # endregion БОТ # Инлайн-команды. Функции, вызываемые при вводе инлайн-команды в виде @ur_inline_bot inlexample query1 query2. # Регистрируются в юб с помощью декоратора @loader.inline_handler, как и обычные команды. В аргументы функции при вызове # получает объект `InlineQuery` (с Heroku 2.1.0 не связан с aiogram). @loader.inline_handler(ru_doc="Пример описания для инлайн-команды на русском", jp_doc="インラインコマンドの日本語の説明の例") async def inlexample(self, query: InlineQuery): """Example description for an inline command in English""" # Чтобы вернуть результаты инлайн-команды, нужно вернуть либо словарь, либо список словарей. Ниже все случаи использования # Для query_gallery можно ответить на query самостоятельно и ничего не возвращать: if query.args == "gallery": # query_gallery - функция для ответа на inline-запрос несколькими результатами-галереями, # например, если фото/данные для них берутся из разных источников await self.inline.query_gallery( query, [ { "title": self.strings["inl__gallery"], "description": self.strings["inl__gallery_desc"], "next_handler": self.example_gallery_photo, "caption": self.strings["inl__gallery_caption"], }, ], ) return # Ниже основные типы контента, которые ты можешь вернуть из inline_handler: results = [ # 1. Простое сообщение: { "title": self.strings["inl__a_message"], "description": self.strings["inl__a_message_desc"], "message": self.strings["inl__a_message_text"].format(query.args), "thumb": "https://github.com/ZetGoHack/TestingModules/raw/main/ex_thumb.jpg", "reply_markup": [ {"text": self.strings["inl__insert_query"], "switch_inline_query_current_chat": "inlexample "}, # switch_inline_query(_current_chat) - подставить пользователю начальный шаблон твоей команды, # например: "@ZetGoBot inlexample". Работает только в результатах inline_handler ], }, # 2. Фото: { "title": self.strings["inl__a_photo"], "description": self.strings["inl__a_photo_desc"], "photo": "https://github.com/ZetGoHack/TestingModules/raw/main/ex_thumb.jpg", "caption": self.strings["inl__a_photo_caption"], }, # 3. GIF: { "title": self.strings["inl__a_gif"], "description": self.strings["inl__a_gif_desc"], "gif": "https://github.com/ZetGoHack/TestingModules/raw/main/dooo.gif", "caption": self.strings["inl__a_gif_caption"], }, # 4. Видео { "title": self.strings["inl__a_video"], "description": self.strings["inl__a_video_desc"], "video": "https://github.com/ZetGoHack/TestingModules/raw/main/example_cat.mp4", "thumb": "https://github.com/ZetGoHack/TestingModules/raw/main/ex_thumb.jpg", "caption": self.strings["inl__a_video_caption"], }, # 5. Документ: { "title": self.strings["inl__a_file"], "description": self.strings["inl__a_file_desc"], "file": "https://sample-files.com/downloads/documents/pdf/basic-text.pdf", "mime_type": "application/pdf", # Для file обязателен параметр mime_type "caption": self.strings["inl__a_file_caption"], }, ] return results # endregion ИНЛАЙН-КОМАНДЫ # region WATCHER # watcher - функция, которая вызывается при каждом новом сообщении (❗ даже если это сервисное сообщение, # то есть без текста), полученным диспатчером юзербота, а так же подходящим под условия самого watcher. # Функция получает объект `herokutl.tl.custom.Message`. Условия watcher устанавливаются в аргументы # декоратора @loader.watcher, или @loader.tag # ℹ️ Подробнее про теги: https://dev.heroku-ub.xyz/watchers @loader.watcher(no_commands=True) # Не пропускать команды в этот watcher async def example_watcher(self, message: Message): if not self.config["watcher"]: # Включать/Выключать wathcer можно через локальные настройки return if not hasattr(message, "message"): # В нашем случае нужно исключить сервисные сообщения, return # у которых нет текста if not message.message or not message.message.startswith("testwatcher"): return prefix = self.get_prefix() await message.reply( "Watcher from TheBestExampleEverMod is working!\n" "You can turn the watcher off in {prefix}cfg TheBestExampleEverMod".format(prefix=prefix) ) # endregion WATCHER # region ЦИКЛЫ @loader.loop( interval=60, # default int 5 autostart=True, # default bool False wait_before=True, # default bool False stop_clause="IS_LOOP_RUNNING" # default NoneType None ) async def loop(self): # loop - декоратор, который помечает функцию как InfiniteLoop для запуска цикла функции каждые interval # секунд. loop по дефолту сам не запускается, если не указан autostart. Ручной запуск через # self.loop.start(), ручная остановка цикла - self.loop.stop(). NOTE: stop() до Heroku 2.2.2 # НЕ очищал поле loop._task, что мешало повторному запуску loop.start(). В Heroku 2.2.2+ ошибка # исправлена. wait_before - переносит задержку с окончания цикла в его начало. stop_clause - имя # ключа в датабазе модуля (self.get(KEY, bool)), который является знаком полной остановки цикла. # NOTE: stop_clause до Heroku 2.2.2 так же НЕ очищал поле loop._task. Для совместимых с Hikka # модулей делайте обход через очистку поля _task и status после явной остановки цикла, если # собираетесь стартовать цикл по-новой. self._loop_iterations += 1 if self._loop_iterations > 10: self.set("IS_LOOP_RUNNING", False) return # region LOOKUP tester_module: "Literal[False] | Module | Library" = self.lookup( "TestMod" ) # ^ аннотация в кавычках - см. предупреждение у импорта Module/Library в начале файла # self.lookup("module_classname_or_name") - метод, буквально возвращающий объект класса активного Модуля/Библиотеки. # с ним вы получаете доступ к self модуля и вызывать его функции, получать # его данные и прочее. можно переиспользовать одну функцию одного модуля в # других. неплохо подходит для интеграций модулей. если lookup не нашёл # модуль, будет возвращено False. if tester_module is not False: log_level = tester_module.config["tglog_level"] else: log_level = 0 # endregion LOOKUP # region INVOKE # self.invoke(command, args=None, *, peer=None, message=None, edit=False) - вызывает команду # по её имени, как будто её реально ввели в чат: либо новым сообщением в peer, либо # через существующий message (.edit, если edit=True, иначе .respond; если указан # peer - message игнорируется). Удобен, когда известно только имя команды (например, # задано пользователем/конфигом), а не конкретный модуль и метод - lookup() тут не # поможет, раз не знаешь, с какого модуля тянуть # ⚠️ команда должна быть в self.allmodules.commands (т.е. модуль с этой # командой должен быть загружен), иначе будет ValueError # Пример (закомментирован, чтобы не дёргать реальную команду каждый тик loop): # result_message = await self.invoke("ping", peer=self.tg_id) # ^ отправит ".ping" в личку с самим собой и тут же вызовет # хендлер команды ping, если она загружена # endregion INVOKE # endregion ЦИКЛЫ # region RAW_HANDLER # raw_handler - функция, которая вызывается при каждом новом событии, полученным диспатчером юзербота, если это событие # является одним из типов, указанных в аргументах декоратора @loader.raw_handler. Функция получает объект события # (например тут - UpdateUserStatus). @loader.raw_handler(UpdateUserStatus) async def raw_handler(self, event: UpdateUserStatus): if not self.config["raw_handler"]: return user_id = event.user_id if user_id in self.config["handler_targets"]: user = await self.client.get_entity(user_id) if isinstance(event.status, UserStatusOnline): status = "user_online" else: status = "user_offline" await self.client.send_message( "me", self.strings[status].format( telethon.utils.get_display_name(user) + f" ({user_id})", ), ) # endregion RAW_HANDLER # region ВЫГРУЗКА # Действия при выгрузке/обновлении модуля. Вызывается при каждом обновлении и выгрузке модуля. # Обычно используется для очистки ресурсов, остановки циклов и т.д. async def on_unload(self): pass # endregion ВЫГРУЗКА # endregion СТРУКТУРА МОДУЛЯ