Skip to content

Writing plugins

Let’s take a closer look at the minimal plugin instance we created in the introduction:

// Plugin — plugin instance
type Plugin struct{}
// Enable — implements plugin.Plugin
// Called immediately after initialization if the plugin is already enabled.
// Called every time the plugin switches to the enabled state.
func (c *Plugin) Enable() error {
return nil
}
// Disable — implements plugin.Plugin
// Called every time the plugin switches to the disabled state.
func (c *Plugin) Disable() error {
return nil
}

Now only the basic Plugin interface is implemented. To provide it with more functionality, more interfaces can be implemented.

APIs are exposed as interfaces and are invoked during plugin initialization and/or when the remote API is called to fetch information or provide callbacks.

These are all the API interfaces provided by notifly. Some interface implementations are omitted in the examples.

Displayer is the simplest form of plugin API, used to provide instructions on the plugin page in the WebUI. Plugins can dynamically generate information based on current state. It receives a location parameter containing the server hostname, port, and scheme reconstructed from the original request to the Displayer API. Markdown is supported.

The REST API for this is provided at /plugin/:id/display.

// Plugin — plugin instance
type Plugin struct {
userCtx plugin.UserContext
}
// GetDisplay — implements plugin.Displayer
// Called when the user views plugin settings. Plugins don't need to be enabled to handle GetDisplay calls.
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 — creates a plugin instance for the user context.
func NewNotiflyPluginInstance(ctx plugin.UserContext) plugin.Plugin {
return &Plugin{ctx}
}

Messenger is used for sending messages. It is invoked with a callback that plugin instances can call at any time to send messages to the user.

// Plugin — plugin instance
type Plugin struct {
msgHandler plugin.MessageHandler
}
// SetMessageHandler — implements plugin.Messenger
// Called during initialization
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 is used to persist information into the notifly database at the user level. Data serialization is handled by the plugin itself.

// Plugin — plugin instance
type Plugin struct {
storageHandler plugin.StorageHandler
}
// SetStorageHandler — implements plugin.Storager
// Called during initialization
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 is used to register custom gin handlers. The base path is the base path of the RouterGroup, which remains consistent across reloads. Plugins can construct the absolute webhook URL by combining the basePath and the location parameter from the Displayer call. Useful for registering webhook handlers. Theoretically you can even register a full custom UI here.

// Plugin — plugin instance
type Plugin struct {
basePath string
}
// RegisterWebhook — implements plugin.Webhooker
// Called during initialization.
// Webhooks are unavailable when plugins are disabled.
func (c *Plugin) RegisterWebhook(basePath string, mux *gin.RouterGroup) {
c.basePath = basePath
mux.POST("/hook", func(c *gin.Context) {
// Handles webhook and performs actions (sending messages, etc.)
})
}
// GetDisplay — implements plugin.Displayer
func (c *Plugin) GetDisplay(location *url.URL) string {
baseLocation := &url.URL{
Path: c.basePath,
}
if location != nil {
// If the server location can be determined, make the URL absolute
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 is used to provide configuration interfaces to the user. Marshalling and unmarshalling are handled by the main notifly program.

The REST API for this is provided at /plugin/:id/config.

// Plugin — plugin instance
type Plugin struct {
config *Config
}
type Config struct {
GitHubUserName string
}
// DefaultConfig — implements plugin.Configurer
// Default configuration will be provided to the user for editing. Also used for unmarshalling.
// Called every time unmarshalling is required.
func (c *Plugin) DefaultConfig() interface{} {
return &Config{
GitHubUserName: "jmattheis",
}
}
// ValidateAndSetConfig — called every time the plugin is initialized or the user changes the config.
// Plugins should validate the config and optionally return an error.
// The parameter is guaranteed to have the same type as the return type of DefaultConfig(), so it is safe to do a hard type assertion here.
//
// "Validation" in this context means checking for conflicting or impossible values, such as a non-URL in a field that should only contain URLs.
// To ensure the plugin instance always operates in a valid state, this method should always accept the result of DefaultConfig()
//
// Called during initialization to provide the initial config. Return nil to accept or return an error to indicate the config is outdated.
// When the config is marked as outdated due to an unmarshalling error or rejection by the plugin, the plugin is automatically disabled and the user is prompted to resolve the config conflict.
// Called every time the config update API is called. Validate the config and return nil to accept or return an error to indicate the config is invalid.
// Return a short and concise error here; if you have detailed guidance for resolving the issue, use the Displayer to provide additional information to the user,
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
}

Although we covered how to implement plugin functionality in the previous chapter, it’s important to follow these practices to ensure the plugin can be loaded successfully and operates efficiently.

  • Use go modules to manage dependencies and use gomod-cap to prevent incompatible dependencies.
  • Handle all errors. A panic in a goroutine started by the plugin can crash the entire notifly program.
  • Provide detailed information about the plugin and use the Displayer to display instructions to users. Detailed plugin information will be shown in the WebUI, making it easier to identify and use.

You can clone the official plugin template and browse community contributions to see plugins in action and/or upload your project.