Перейти к содержимому

Написание плагинов

Давайте внимательнее посмотрим на минимальный экземпляр плагина, который мы создали в части введения:

// Plugin — экземпляр плагина
type Plugin struct{}
// Enable — реализует plugin.Plugin
// Вызывается сразу после инициализации, если плагин уже включён.
// Вызывается каждый раз, когда плагин переключается на включённое состояние.
func (c *Plugin) Enable() error {
return nil
}
// Disable — реализует plugin.Plugin
// Вызывается каждый раз, когда плагин переключается на отключённое состояние.
func (c *Plugin) Disable() error {
return nil
}

Теперь реализован только базовый интерфейс Plugin. Чтобы предоставить ему больше функциональностей, можно реализовать больше интерфейсов.

API предоставляются как интерфейсы и вызываются во время инициализации плагина и/или вызова удалённого API для получения информации или предоставления обратных вызовов.

Это все интерфейсы API, предоставляемые notifly. Некоторые реализации интерфейсов опущены в примерах.

Displayer — это самая простая форма API плагина, используется для предоставления инструкций на странице плагина в WebUI. Плагины могут динамически генерировать информацию на основе текущего состояния. Он получает параметр location, содержащий имя хоста сервера, порт и схему, восстановленные из исходного запроса к API Displayer. Поддерживается markdown.

REST API для этого предоставлен в /plugin/:id/display.

// Plugin — экземпляр плагина
type Plugin struct {
userCtx plugin.UserContext
}
// GetDisplay — реализует plugin.Displayer
// Вызывается, когда пользователь просматривает параметры плагина. Плагины не должны быть включены для обработки вызовов GetDisplay.
func (c *Plugin) GetDisplay(location *url.URL) string {
if (c.userCtx.Admin) {
return "You are an admin! You have super cow powers."
} else {
return "You are **NOT** an admin! You can do nothing:("
}
}
// NewNotiflyPluginInstance — создаёт экземпляр плагина для контекста пользователя.
func NewNotiflyPluginInstance(ctx plugin.UserContext) plugin.Plugin {
return &Plugin{ctx}
}

Messenger используется для отправки сообщений. Он вызывается с обратным вызовом, который экземпляры плагина могут вызвать в любой момент для отправки сообщений пользователю.

// Plugin — экземпляр плагина
type Plugin struct {
msgHandler plugin.MessageHandler
}
// SetMessageHandler — реализует plugin.Messenger
// Вызывается при инициализации
func (c *Plugin) SetMessageHandler(h plugin.MessageHandler) {
c.msgHandler = h
}
func (c *Plugin) Enable() error {
go func() {
time.Sleep(5 * time.Second)
c.msgHandler.SendMessage(plugin.Message{
Message: "The plugin has been enabled for 5 seconds.",
})
}()
return nil
}

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

// Plugin — экземпляр плагина
type Plugin struct {
storageHandler plugin.StorageHandler
}
// SetStorageHandler — реализует plugin.Storager
// Вызывается при инициализации
func (c *Plugin) SetStorageHandler(h plugin.StorageHandler) {
c.storageHandler = h
}
type Storage struct {
EnabledTimes uint `json:"enabled_times"`
}
func (c *Plugin) Enable() error {
storage := new(Storage)
storageBytes, err := c.storageHandler.Load()
if err != nil {
return err
}
if len(storageBytes) == 0 {
storage.EnabledTimes = 1
storageBytes, _ = json.Marshal(storage)
c.storageHandler.Save(storageBytes)
} else {
json.Unmarshal(storageBytes, storage)
}
log.Printf("This plugin has been enabled %d times.", storage.EnabledTimes)
return nil
}

Webhooker используется для регистрации пользовательских обработчиков gin. Базовый путь — это базовый путь RouterGroup, который остаётся согласованным между перезагрузками. Плагины могут собрать абсолютный URL webhook, объединив basePath и параметр location в вызове Displayer. Полезно для регистрации обработчиков webhook. Теоретически вы можете даже зарегистрировать полный пользовательский интерфейс здесь.

// Plugin — экземпляр плагина
type Plugin struct {
basePath string
}
// RegisterWebhook — реализует plugin.Webhooker
// Вызывается при инициализации.
// Webhook недоступны, когда плагины отключены.
func (c *Plugin) RegisterWebhook(basePath string, mux *gin.RouterGroup) {
c.basePath = basePath
mux.POST("/hook", func(c *gin.Context) {
// Обрабатывает webhook и выполняет действия (отправка сообщений и т.д.)
})
}
// GetDisplay — реализует plugin.Displayer
func (c *Plugin) GetDisplay(location *url.URL) string {
baseLocation := &url.URL{
Path: c.basePath,
}
if location != nil {
// Если местоположение сервера можно определить, сделать URL абсолютным
loc.Scheme = location.Scheme
loc.Host = location.Host
}
loc = loc.ResolveReference(&url.URL{
Path: "hook",
})
return fmt.Sprintf("Set your webhook URL to %s and you are all set", loc)
}

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

REST API для этого предоставлен в /plugin/:id/config.

// Plugin — экземпляр плагина
type Plugin struct {
config *Config
}
type Config struct {
GitHubUserName string
}
// DefaultConfig — реализует plugin.Configurer
// Конфигурация по умолчанию будет предоставлена пользователю для редактирования. Также используется для анмаршаллинга.
// Вызывается каждый раз, когда требуется анмаршаллинг.
func (c *Plugin) DefaultConfig() interface{} {
return &Config{
GitHubUserName: "jmattheis",
}
}
// ValidateAndSetConfig — будет вызван каждый раз при инициализации плагина или изменении конфигурации пользователем.
// Плагины должны проверить конфигурацию и опционально вернуть ошибку.
// Параметр гарантированно имеет тот же тип, что и тип возврата DefaultConfig(), поэтому безопасно выполнить жёсткое приведение типов здесь.
//
// "Проверка" в этом контексте означает проверку конфликтующих или невозможных значений, таких как не-URL в поле, которое должно содержать только URL.
// Чтобы убедиться, что экземпляр плагина всегда работает в допустимом состоянии, этот метод должен всегда принимать результат DefaultConfig()
//
// Вызывается при инициализации для предоставления начальной конфигурации. Вернуть nil для принятия или вернуть ошибку, чтобы указать, что конфиг устарел.
// Когда конфигурация помечена устаревшей из-за ошибки анмаршаллинга или отклонения на стороне плагина, плагин автоматически отключается и пользователю предлагается разрешить конфликт конфигурации.
// Вызывается каждый раз, когда вызывается API обновления конфигурации. Проверьте конфигурацию и вернуть nil для принятия или вернуть ошибку, чтобы указать, что конфиг недействительный.
// Верните короткую и краткую ошибку здесь и, если у вас есть подробные рекомендации по решению проблемы, используйте Displayer для предоставления дополнительной информации пользователю,
func (c *Plugin) ValidateAndSetConfig(c interface{}) error {
config = c.(*Config)
if !userNameIsValid(config.GitHubUserName) {
return errors.New("the user name is not valid")
}
c.config = config
return nil
}

Хотя мы охватили способ реализации функциональностей плагина в предыдущей главе, очень важно следовать этим практикам, чтобы убедиться, что плагин может быть загружен успешно и работает эффективно.

  • Используйте go modules для управления зависимостями и используйте gomod-cap для предотвращения несовместимых зависимостей.
  • Обрабатывайте все ошибки. Паника в goroutine, запущенной в плагине, может привести к краху всей программы notifly.
  • Предоставьте подробную информацию о плагине и используйте Displayer для отображения инструкций пользователям. Подробная информация о плагине будет отображаться в WebUI, что облегчает его идентификацию и использование.

Вы можете клонировать официальный шаблон плагина и посмотреть вклады сообщества, чтобы увидеть плагины в действии и/или загрузить свой проект.