diff --git a/docs/plugin-v1.md b/docs/plugin-v1.md new file mode 100644 index 0000000..0af0d0f --- /dev/null +++ b/docs/plugin-v1.md @@ -0,0 +1,61 @@ +--- +id: plugin-v1 +title: Intro to Gotify Plugins (v1) +--- + +> Plugins are currently only supported on Linux and MacOS due to a current limitation of golang. + +## Description + +_This documentation is generally designed for plugin developers. If you just wanted to use an existing plugin, you would need to refer to the documentation from the plugin maintainer._ + +Gotify provides built-in plugin functionality built on top of the [go plugin system](https://godoc.org/plugin). It is built for extending Gotify functionality. + +## Get Started + +First let's see a minimal example of gotify plugin, you can copy this boilerplate code to bootstrap your own plugin: + +```golang +package main + +import ( + "github.com/gotify/plugin-api" +) + +// GetGotifyPluginInfo returns gotify plugin info +func GetGotifyPluginInfo() plugin.Info { + return plugin.Info{ + Name: "minimal plugin", + ModulePath: "github.com/gotify/server/example/minimal", + } +} + +// Plugin is plugin instance +type Plugin struct{} + +// Enable implements plugin.Plugin +func (c *Plugin) Enable() error { + return nil +} + +// Disable implements plugin.Plugin +func (c *Plugin) Disable() error { + return nil +} + +// NewGotifyPluginInstance creates a plugin instance for a user context. +func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin { + return &Plugin{} +} + +func main() { + panic("this should be built as go plugin") +} +``` + +This program exports two functions: `GetGotifyPluginInfo` and `NewGotifyPluginInstance`, gotify will use these to obtain the plugin metadata and create plugin instances for each user. + +The `GetGotifyPluginInfo` must return a [`plugin.Info`](https://godoc.org/github.com/gotify/plugin-api#Info) containing descriptive info of the current plugin, all fields are optional except `ModulePath`(the module path of this plugin), which is used to distinguish different plugins. + +The `NewGotifyPluginInstance` is called with a [`plugin.UserContext`](https://godoc.org/github.com/gotify/plugin-api#UserContext) for each user at startup and every time a new user is added, the plugin must return a plugin instance that satisfies [`plugin.Plugin`](https://godoc.org/github.com/gotify/plugin-api#Plugin) interface. +More functionalities can be implemented by implementing more interfaces in the [`plugin-api`](https://godoc.org/github.com/gotify/plugin-api#Info) package. diff --git a/docs/plugin.md b/docs/plugin.md index af86008..0040207 100644 --- a/docs/plugin.md +++ b/docs/plugin.md @@ -3,13 +3,14 @@ id: plugin title: Intro to Gotify Plugins --- -> Plugins are currently only supported on Linux and MacOS due to a current limitation of golang. - ## Description _This documentation is generally designed for plugin developers. If you just wanted to use an existing plugin, you would need to refer to the documentation from the plugin maintainer._ -Gotify provides built-in plugin functionality built on top of the [go plugin system](https://godoc.org/plugin). It is built for extending Gotify functionality. +Gotify plugins are platform executables that use the gRPC architecture and contains two core behaviors: + +- Establishing a secure socket connection to the server through a certificate exchange (abbreviated as `kex` in API and protocol documentation). +- Act as a gRPC server that responds to requests from the server and provides updates to the user's state. ## Features @@ -27,51 +28,68 @@ Gotify provides built-in plugin functionality built on top of the [go plugin sys - Extending the WebUI functionality. - Delivering alarm notifications. -## Get Started +## Architecture -First let's see a minimal example of gotify plugin, you can copy this boilerplate code to bootstrap your own plugin: +- gRPC based, plugin is the "server" and gotify/server is the "client". +- each user gets a long-running server-streaming RPC. +- Backwards compatibility is provided by: + - Using optional fields in the `RunUserInstanceServer` initialization request. + - Using separate RPCs for functionally disjoint features. So the call interface can be extended without pushing breaking changes to the whole RPC schema. -```golang -package main +## Development Workflow -import ( - "github.com/gotify/plugin-api" -) +### Using the official Go Plugin Template -// GetGotifyPluginInfo returns gotify plugin info -func GetGotifyPluginInfo() plugin.Info { - return plugin.Info{ - Name: "minimal plugin", - ModulePath: "github.com/gotify/server/example/minimal", - } -} +Use the template [`gotify/plugin-template`](https://github.com/gotify/plugin-template) to scaffold your plugin. -// Plugin is plugin instance -type Plugin struct{} +The core logic happens in `PluginServer#RunUserInstance`, which creates a context that is valid (not cancelled) for the duration of the session. -// Enable implements plugin.Plugin -func (c *Plugin) Enable() error { - return nil -} - -// Disable implements plugin.Plugin -func (c *Plugin) Disable() error { - return nil -} - -// NewGotifyPluginInstance creates a plugin instance for a user context. -func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin { - return &Plugin{} -} +``` -func main() { - panic("this should be built as go plugin") +func (s *PluginServer) RunUserInstance(req *protobuf.UserInstanceRequest, stream protobuf.Plugin_RunUserInstanceServer) error { + for i := range myplugin.Capabilities { + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Capable{ + Capable: myplugin.Capabilities[i], + }, + }); err != nil { + return err + } + } + ticker := time.NewTicker(10 * time.Second) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + // your logic here + // + _ = ctx + }() + + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := stream.Send(&protobuf.InstanceUpdate{ + Update: &protobuf.InstanceUpdate_Ping{ + Ping: new(emptypb.Empty), + }, + }); err != nil { + return err + } + case <-s.shutdown: + return nil + } + } } ``` -This program exports two functions: `GetGotifyPluginInfo` and `NewGotifyPluginInstance`, gotify will use these to obtain the plugin metadata and create plugin instances for each user. -The `GetGotifyPluginInfo` must return a [`plugin.Info`](https://godoc.org/github.com/gotify/plugin-api#Info) containing descriptive info of the current plugin, all fields are optional except `ModulePath`(the module path of this plugin), which is used to distinguish different plugins. +### Manual Implementation in your favorite Language/Scaffold + +We support "duck-typed" plugins (i.e. plugins that loosely behave like one built using the Go template), namely it has to provide two functions: +- A TLS key exchange using secure file descriptors at startup. +- A long running gRPC server that instantiates a server-side stream for each user session. -The `NewGotifyPluginInstance` is called with a [`plugin.UserContext`](https://godoc.org/github.com/gotify/plugin-api#UserContext) for each user at startup and every time a new user is added, the plugin must return a plugin instance that satisfies [`plugin.Plugin`](https://godoc.org/github.com/gotify/plugin-api#Plugin) interface. -More functionalities can be implemented by implementing more interfaces in the [`plugin-api`](https://godoc.org/github.com/gotify/plugin-api#Info) package. +The protobuf files are located in [gotify/plugin-api](https://github.com/gotify/plugin-api) repository.