Это руководство поможет новым разработчикам понять, как создавать плагины для системы. Мы рассмотрим структуру, компоненты и лучшие практики разработки плагинов.
Начало работы
Доступные шаблоны
Перед созданием собственного плагина вы можете использовать эти бесплатные шаблоны:
- plugins/example_plugin — Комплексный пример, показывающий все основные функции плагина
- 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 файл для сложных плагинов
Система миграций
Для плагинов, которым нужны миграции базы данных, см. документацию Система миграций плагинов.
Развертывание
- Поместите ваш плагин в директорию plugins/
- Перезапустите бота
- Плагин будет автоматически загружен
- Проверьте логи на наличие ошибок
Устранение неполадок
Распространенные проблемы
- Плагин не загружается: Проверьте формат info.json и обязательные файлы
- Ошибки импорта: Убедитесь в правильности imports.json и установленных зависимостей
- Ошибки разрешений: Проверьте определения областей и роли пользователей
- Проблемы с языками: Убедитесь, что ключи сообщений существуют в языковых файлах
Советы по отладке
- Проверьте логи бота для сообщений об ошибках
- Используйте логирование для отладки вашего кода
- Тестируйте компоненты по отдельности
- Проверьте разрешения файлов и пути
Пример рабочего процесса
- Начните с шаблона: Скопируйте example_plugin или empty
- Измените метаданные: Обновите info.json с вашими данными
- Определите схему: Создайте initial_data.py с вашими структурами данных
- Реализуйте логику: Добавьте команды, коллбэки и состояния
- Добавьте языки: Создайте файлы сообщений для вашего текста
- Тщательно протестируйте: Протестируйте всю функциональность
- Разверните: Поместите в директорию плагинов и перезапустите бота
Ресурсы
- Пример плагина: plugins/example_plugin — Комплексный пример
- Пустой шаблон: plugins/empty — Минимальная отправная точка
- Руководство по миграциям: Система миграций плагинов
- Документация ядра: Проверьте документацию модуля core для продвинутых функций
Помните: Начинайте с простого и постепенно добавляйте сложность. Пример плагина демонстрирует все основные функции, которые могут вам понадобиться.