Это руководство поможет новым разработчикам понять, как создавать плагины для системы. Мы рассмотрим структуру, компоненты и лучшие практики разработки плагинов.

Начало работы

Доступные шаблоны

Перед созданием собственного плагина вы можете использовать эти бесплатные шаблоны:

  1. plugins/example_plugin — Комплексный пример, показывающий все основные функции плагина
  2. plugins/empty — Минимальный шаблон для простых плагинов

Эти шаблоны бесплатны для использования и модификации в любых целях. Скопируйте их для создания собственного плагина.

Структура плагина

Каждый плагин следует стандартной структуре директорий:

your_plugin/
├── __init__.py              # Пустой файл (обязательно)
├── plugin.py                # Основной класс плагина
├── info.json                # Метаданные плагина
├── initial_data.py          # Определения схемы и данные по умолчанию
├── config.json              # Конфигурация плагина
├── imports.json             # Внешние зависимости
├── constants.py             # Константы плагина
├── functions.py             # Вспомогательные функции
├── commands/                # Обработчики команд
│   ├── __init__.py
│   └── YourCommand.py
├── callbacks/               # Обработчики коллбэков
│   ├── __init__.py
│   └── YourCallback.py
├── states/                  # Обработчики состояний
│   ├── __init__.py
│   └── YourState.py
├── lang/                    # Языковые файлы
│   ├── messages.en.json
│   └── messages.ru.json
├── templates/               # Файлы шаблонов
│   └── your_template.tmpl
└── migrations/              # Миграции базы данных
    ├── __init__.py
    ├── template.py
    └── 1_0_1.py

1. info.json — Метаданные плагина

{
    "id": "your_plugin_id",
    "name": "Название вашего плагина",
    "author": "Ваше имя",
    "description": "Описание того, что делает ваш плагин",
    "version": "1.0.0"
}

Важные поля:

  • id: Уникальный идентификатор (строчные буквы, без пробелов)
  • name: Отображаемое имя плагина
  • version: Семантическое версионирование (например, «1.0.0»)

2. plugin.py — Основной класс плагина

from core import PluginBase
from core.CustomMenuButton import CustomMenuButton
from telebot.types import Message
from typing import List
class Plugin(PluginBase):
    # Определяем ключи области для ролевого доступа
    scope_keys = ['your_plugin_scope']

    

    def get_main_menu_buttons(self) -> List[CustomMenuButton]:
        """Определяем кнопки, которые появляются в главном меню"""
        async def button_handler(message: Message):
            from .states.YourState import YourState
            await self.state_machine.set_state(message, YourState)
        

        button = CustomMenuButton(
            'btn_your_plugin_action',  # Ключ сообщения из языковых файлов
            None,  # Иконка (опционально)
            button_handler,
           'your_plugin_scope'  # Требуемая область (опционально)
        )
        return [button]

3. initial_data.py — Схема и данные по умолчанию

# Определяем метаобъекты (пользовательские структуры данных)

METAOBJECT_SIGNATURES = [
    {
        'key': 'YOUR_CUSTOMER',
        'description': 'Информация о клиенте',
        'fields': [
            {
                'name': 'id',
                'type': 'id_int',
                'description': 'ID клиента'
            },
            {
                'name': 'name',
                'type': 'string',
                'description': 'Имя клиента'
            },
            {
                'name': 'email',
                'type': 'string',
                'description': 'Email адрес'
            }
        ]
    }
]
# Определяем группы настроек
SETTING_GROUPS = [
    {
        'title': 'Настройки вашего плагина',
        'id': 'your_plugin',
        'sort_order': 10
    }
]
# Определяем настройки
SETTINGS = [
    {
        'title': 'Включить функцию',
        'id': 'YOUR_PLUGIN_ENABLED',
        'type': 'bool',
        'value': True,
        'group': 'your_plugin',
        'sort_order': 1
    },
    {
        'title': 'API ключ',
        'id': 'YOUR_PLUGIN_API_KEY',
        'type': 'string',
        'value': '',
        'group': 'your_plugin',
        'sort_order': 2
    }
]

4. constants.py — Константы плагина

# Определяем ключи области для ролевого доступа
SCOPE_KEY_YOUR_PLUGIN = 'your_plugin'
# Другие константы
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3

5. functions.py — Вспомогательные функции

import datetime
from core.utils.config import const_config
def get_current_time() -> str:
    """Получить текущее время в настроенном часовом поясе"""
    now = datetime.datetime.now(datetime.UTC) + datetime.timedelta(hours=const_config.TIMEZONE)
    return now.strftime('%d.%m.%Y %H:%M:%S')
def validate_email(email: str) -> bool:
    """Простая валидация email"""
    return '@' in email and '.' in email

6. imports.json — Внешние зависимости

{
    "requests": "2.31.0",
    "pandas": "2.0.0"
}

7. config.json — Конфигурация плагина

{
    "DEFAULT_TIMEOUT": 30,
    "API_BASE_URL": "https://api.example.com"
}

Компоненты

Команды

Команды обрабатывают текстовые сообщения, соответствующие определенным шаблонам (регулярным выражениям).

# commands/YourCommand.py
from core.command import Command
from core.utils.messages import msg
import telebot
class YourCommand(Command):
    cmd = '/your_command'
    regex = fr'^{cmd}$'  # Regex шаблон для соответствия
    async def process(self, command_text: str, chat_id: int, 
                     original_message: telebot.types.Message|None = None):
        # Ваша логика команды здесь
        response = msg('msg_your_command_response', chat_id)
        await self.bot.send(chat_id, response)

Коллбэки

Коллбэки обрабатывают нажатия кнопок инлайн-клавиатуры. Предусмотрена обработка только строковых callback data в кодировке utf-8, прочие форматы, в том числе бинарный, не поддерживаются.

# callbacks/YourCallback.py
import telebot
from core import Callback
from core.utils.messages import msg
class YourCallback(Callback):
    prefix = 'your_callback_'
    regex = fr'^{prefix}(\d+)$'  # Regex шаблон callback data для соответствия
    async def process(self, callback: telebot.types.CallbackQuery):
        # Извлекаем данные из коллбэка
        data = callback.data[len(self.prefix):]
        chat_id = callback.message.chat.id
        # Ваша логика коллбэка здесь
        response = msg('msg_your_callback_response', chat_id)
        await self.bot.send(chat_id, response)

Состояния

Состояния управляют потоками диалогов и взаимодействиями между ботом и пользователями. Каждое состояние можно представить как «комнату», в которой находится пользователь. «Двери» ведут в другие состояния.

# states/YourState.py
from core import State
from core.security.scope_guard import scope_guard
from core.utils.messages import msg
from enum import Enum
import telebot.types
class Actions(Enum):
    BACK = 'toNav'
    SUBMIT = 'btn_your_state_submit'
class YourState(State):
    def __init__(self):
        super().__init__()
    @scope_guard('your_plugin_scope')  # Опционально: ограничить доступ
    async def enter(self, message: telebot.types.Message):
        await super().enter(message)

        

        greeting = msg('msg_your_state_greeting', message.chat.id)
        keyboard = self.create_keyboard()

        

        self.add_keyboard_button(
            keyboard, msg(Actions.SUBMIT.value, message.chat.id))
        self.add_keyboard_button(
            keyboard, msg(Actions.BACK.value, message.chat.id))

        

        await self.bot.send(message.chat.id, greeting, reply_markup=keyboard)
    async def process_text(self, message: telebot.types.Message):
        if message.text == msg(Actions.BACK.value, message.chat.id):
            from app.states.StartState import StartState
            await StartState.send_user_to_initial_state(message.chat.id)
            return

        

        if message.text == msg(Actions.SUBMIT.value, message.chat.id):
            # Обрабатываем действие отправки
            await self.bot.send(message.chat.id, "Действие выполнено!")

Поддержка языков

Языковые файлы

Создайте языковые файлы в директории lang/:

// lang/messages.en.json
{
    "msg_scope_name_your_plugin": "Your Plugin",
    "btn_your_plugin_action": "Your Action",
    "msg_your_state_greeting": "Welcome to your plugin!",
    "btn_your_state_submit": "Submit",
    "msg_your_command_response": "Command executed successfully!",
    "msg_your_callback_response": "Callback processed!"
}
// lang/messages.ru.json
{
    "msg_scope_name_your_plugin": "Ваш плагин",
    "btn_your_plugin_action": "Ваше действие",
    "msg_your_state_greeting": "Добро пожаловать в ваш плагин!",
    "btn_your_state_submit": "Отправить",
    "msg_your_command_response": "Команда выполнена успешно!",
    "msg_your_callback_response": "Обработка завершена!"
}

Хранение данных чата

ChatDataStorage доступен как storage.chat_data и предоставляет простой способ хранить и получать пары ключ-значение для каждого чата (или пользователя). Это удобно для отслеживания прогресса пользователя, временного состояния или хранения небольших данных, относящихся к конкретному чату или пользователю.

Базовое использование

Импортируйте объект хранилища:

from core.data import storage

Сохранение данных чата

Чтобы сохранить значение для определённого чата:

storage.chat_data.set(chat_id, «key», value)

  • chat_id — уникальный идентификатор чата или пользователя (обычно message.chat.id).
  • «key» — имя сохраняемого значения.
  • value — любой сериализуемый Python-объект (строка, число, словарь и т.д.).

Получение данных

Чтобы получить значение:

value = storage.chat_data.get(chat_id, «key»)

  • Возвращает None, если ключ не найден.

Удаление ключа

Чтобы удалить определённый ключ:

storage.chat_data.unset(chat_id, «key»)

Удаление ключей по префиксу

Чтобы удалить все ключи, начинающиеся с определённого префикса:

storage.chat_data.unset_by_prefix(chat_id, «prefix_»)

  • Это удобно для очистки группы связанных ключей сразу.

Пример

from core.data import storage
# Сохраняем текущий шаг мастера для пользователя
storage.chat_data.set(chat_id, "wizard_step", 2)
# Получаем шаг мастера
step = storage.chat_data.get(chat_id, "wizard_step")
# Удаляем шаг мастера
storage.chat_data.unset(chat_id, "wizard_step")
# Удаляем все ключи, начинающиеся с "wizard_"
storage.chat_data.unset_by_prefix(chat_id, "wizard_")

Когда использовать

  • Когда нужно что-то запомнить для пользователя между сообщениями (например, шаг визарда последний ввод, временный выбор).
  • Для лёгких, индивидуальных данных чата или пользователя, не требующих полноценной модели в базе данных.

Рекомендации

  • Используйте понятные и уникальные имена ключей, чтобы избежать конфликтов.
  • Не храните большие или чувствительные данные в ChatDataStorage.
  • Для постоянных, межпользовательских или больших данных используйте метаобъекты или

Безопасность и контроль доступа

Ролевой доступ

Для управления доступом к функциям вашего плагина используйте механизм скоупов (scopes). Скоупы определяют права на выполнение определённых действий или доступ к разделам плагина. Назначайте скоупы группам пользователей и проверяйте их перед выполнением защищённых операций.

1. Определите скоупы для вашего плагина

Определите все скоупы, которые будет использовать ваш плагин, как уникальные строковые константы, обычно в файле constants.py:

SCOPE_KEY_CUSTOMER_VIEW = 'crm_customer_view'
SCOPE_KEY_CUSTOMER_EDIT = 'crm_customer_edit'
SCOPE_KEY_ORDER_VIEW = 'crm_order_view'
SCOPE_KEY_ORDER_MANAGE = 'crm_order_manage'

2. Зарегистрируйте скоупы в классе плагина

Перечислите все ваши скоупы в атрибуте scope_keys класса Plugin. Это обеспечит их регистрацию и возможность назначения группам пользователей:

from . import constants
class Plugin(PluginBase):
    scope_keys = [
        constants.SCOPE_KEY_CUSTOMER_VIEW,
        constants.SCOPE_KEY_CUSTOMER_EDIT,
        constants.SCOPE_KEY_ORDER_VIEW,
        constants.SCOPE_KEY_ORDER_MANAGE
    ]
    def __init__(self, state_machine, bot):
        super().__init__(state_machine, bot)
        # ... ваша инициализация ...

Важно: Всегда определяйте все необходимые скоупы в scope_keys до их использования в декораторах или элементах интерфейса.

3. Ограничьте доступ с помощью @scope_guard

Используйте декоратор @scope_guard(SCOPE_KEY) для методов состояний или обработчиков команд, чтобы ограничить доступ только пользователям с нужным скоупом:

from core.security.scope_guard import scope_guard
from .constants import SCOPE_KEY_CUSTOMER_EDIT
class EditCustomerState(State):
    @scope_guard(SCOPE_KEY_CUSTOMER_EDIT)
    async def enter(self, message):
        # Только пользователи со скоупом 'crm_customer_edit' могут попасть в это состояние
        ...

4. Управляйте видимостью кнопок по скоупу

При создании кнопок меню указывайте, какие скоупы требуются для их отображения:

CustomMenuButton(
    caption='btn_crm_view_orders',
    fn_check_if_is_visible=None,
    action=handler_view_orders,
    visibility_scopes=constants.SCOPE_KEY_ORDER_VIEW
)

Только пользователи с указанным скоупом увидят эту кнопку.

5. Как работают скоупы

  • Скоупы регистрируются автоматически при установке плагина.
  • Каждый скоуп может быть назначен одной или нескольким группам пользователей (по умолчанию — группе администраторов).
  • При попытке доступа к защищённому ресурсу система проверяет, есть ли у пользователя нужный скоуп через его группы.
  • Декоратор scope_guard и проверки видимости кнопок используют метод services.scopes.check_if_user_has_scope(chat_id, scope_key) для проверки доступа.
  • Администраторы могут управлять скоупами и назначать их группам.

6. Лучшие практики

  • Определяйте все скоупы плагина в constants.py и перечисляйте их в scope_keys.
  • Используйте понятные, описательные имена скоупов с префиксом вашего плагина для избежания конфликтов.
  • Используйте @scope_guard для защиты важных состояний и команд.
  • Используйте скоупы для управления видимостью кнопок меню и клавиатуры, чтобы сделать интерфейс персонализированным.
  • Документируйте назначение каждого скоупа в коде для удобства поддержки.

Языковые переменные для скоупов

Для каждого скоупа определяйте человекочитаемое название в языковых файлах (например, lang/messages.ru.json, lang/messages.en.json) по шаблону ключа:

msg_scope_name_<scope_key>

Например, для скоупа crm_customer_view добавьте в языковой файл:

{
  "msg_scope_name_crm_customer_view": "Просмотр клиентов"
}

Эти переменные используются системой для отображения названий скоупов в меню, административных разделах и интерфейсах управления ролями.
Обязательно добавляйте переводы для всех поддерживаемых языков для единообразия интерфейса.

Лучшие практики

1. Соглашения об именовании

  • Используйте строчные буквы с подчеркиваниями для ID плагинов
  • Префиксируйте ключи сообщений с ID вашего плагина
  • Используйте описательные имена для функций и классов

2. Обработка ошибок

try:
    # Ваш код здесь
    result = some_operation()
except Exception as e:
    logging.error(f"Ошибка в вашем плагине: {e}")
    await self.bot.send(chat_id, "Произошла ошибка")

3. Логирование

import logging
logger = logging.getLogger(__name__)
logger.info("Операция плагина завершена")
logger.error("Произошла ошибка", exc_info=True)

4. Тестирование

Тщательно тестируйте ваш плагин:

  • Тестируйте все команды и коллбэки
  • Тестируйте переходы между состояниями
  • Тестируйте с разными ролями пользователей
  • Тестируйте условия ошибок

5. Документирование

Документируйте ваш плагин:

  • Четкое описание в info.json
  • Комментарии в коде
  • README файл для сложных плагинов

Система миграций

Для плагинов, которым нужны миграции базы данных, см. документацию Система миграций плагинов.

Развертывание

  1. Поместите ваш плагин в директорию plugins/
  2. Перезапустите бота
  3. Плагин будет автоматически загружен
  4. Проверьте логи на наличие ошибок

Устранение неполадок

Распространенные проблемы

  1. Плагин не загружается: Проверьте формат info.json и обязательные файлы
  2. Ошибки импорта: Убедитесь в правильности imports.json и установленных зависимостей
  3. Ошибки разрешений: Проверьте определения областей и роли пользователей
  4. Проблемы с языками: Убедитесь, что ключи сообщений существуют в языковых файлах

Советы по отладке

  1. Проверьте логи бота для сообщений об ошибках
  2. Используйте логирование для отладки вашего кода
  3. Тестируйте компоненты по отдельности
  4. Проверьте разрешения файлов и пути

Пример рабочего процесса

  1. Начните с шаблона: Скопируйте example_plugin или empty
  2. Измените метаданные: Обновите info.json с вашими данными
  3. Определите схему: Создайте initial_data.py с вашими структурами данных
  4. Реализуйте логику: Добавьте команды, коллбэки и состояния
  5. Добавьте языки: Создайте файлы сообщений для вашего текста
  6. Тщательно протестируйте: Протестируйте всю функциональность
  7. Разверните: Поместите в директорию плагинов и перезапустите бота

Ресурсы

  • Пример плагина: plugins/example_plugin — Комплексный пример
  • Пустой шаблон: plugins/empty — Минимальная отправная точка
  • Руководство по миграциям: Система миграций плагинов
  • Документация ядра: Проверьте документацию модуля core для продвинутых функций

Помните: Начинайте с простого и постепенно добавляйте сложность. Пример плагина демонстрирует все основные функции, которые могут вам понадобиться.