From a46fa320bde5eae966ddcffdf26d361eadb4ab53 Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Sat, 12 Jan 2019 22:10:09 +0800 Subject: [PATCH] init v1 api --- .travis.yml | 18 +++++++ Makefile | 45 ++++++++++++++++ README.md | 38 ++++++++++++- cmd/gomod-cap/merge.go | 104 ++++++++++++++++++++++++++++++++++++ config.go | 14 +++++ displayer.go | 9 ++++ export_ref_do_not_edit.json | 1 + go.mod | 19 +++++++ go.sum | 37 +++++++++++++ messenger.go | 22 ++++++++ plugin.go | 29 ++++++++++ storage.go | 13 +++++ webhook.go | 11 ++++ 13 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 .travis.yml create mode 100644 Makefile create mode 100644 cmd/gomod-cap/merge.go create mode 100644 config.go create mode 100644 displayer.go create mode 100644 export_ref_do_not_edit.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 messenger.go create mode 100644 plugin.go create mode 100644 storage.go create mode 100644 webhook.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..65a0ff9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: go +go: + - "1.11" + +notifications: + email: false + +env: + - GO111MODULE=on + +before_install: + - make download-tools + +install: + - go get + +script: + - make check diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..134e232 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +PKG=./ + +cap-version: + wget -O /tmp/gotify.go.mod https://github.com/gotify/server/raw/master/go.mod; \ + for P in ${PKG}; do \ + go run github.com/gotify/plugin-api/cmd/gomod-cap -to `echo $$P` -from /tmp/gotify.go.mod; \ + done + +check: check-go check-symbol check-mod check-lint + +check-go: + for P in ${PKG}; do \ + go test `echo $$P`; \ + done + +check-lint: + for P in ${PKG}; do \ + golint -set_exit_status `echo $$P`; \ + done + +check-mod: + wget -O /tmp/gotify.go.mod https://github.com/gotify/server/raw/master/go.mod; \ + for P in ${PKG}; do \ + go run github.com/gotify/plugin-api/cmd/gomod-cap -to `echo $$P` -from /tmp/gotify.go.mod -check=true; \ + done + +check-symbol: + for P in ${PKG}; do \ + go-exports -d `echo $$P` -c `echo $$P/export_ref_do_not_edit.json`; \ + done + +release: generate-symbol + +generate-symbol: + for P in ${PKG}; do \ + if [ ! -f `echo $$P/export_ref_do_not_edit.json` ]; then \ + go-exports -d `echo $$P` > `echo $$P/export_ref_do_not_edit.json`; \ + fi; \ + done + +download-tools: + go get -u golang.org/x/lint/golint + go get -u github.com/eternal-flame-AD/go-exports + +.PHONY: check-go check-symbol check-lint check-mod generate-symbol cap-version download-tools \ No newline at end of file diff --git a/README.md b/README.md index c2dac8e..2d807b2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ # plugin-api -tbd +[![Build Status](https://travis-ci.org/gotify/plugin-api.svg?branch=master)](https://travis-ci.org/gotify/plugin-api) +[![GoDoc](https://godoc.org/github.com/gotify/plugin-api?status.svg)](https://godoc.org/github.com/gotify/plugin-api) + +This repository consisted of APIs used in gotify plugins. + +## Usage + +### Plugin API + +https://gotify.net/docs/plugin + +### CLI tools + +#### `github.com/gotify/cmd/gomod-cap` + +Since `go.mod` follows a [minimal version selection](https://github.com/golang/proposal/blob/master/design/24301-versioned-go.md), packages are built with the lowest common version requirement defined in `go.mod`. This poses a problem when developing plugins: + +If the gotify server is built with the following go.mod: +``` +require some/package v0.1.0 +``` +But when the plugin is built, it used a newer version of this package: +``` +require some/package v0.1.1 +``` +Since the server is built with `v0.1.0` and the plugin is built with `v0.1.1` of `some/package`, the built plugin could not be loaded due to different import package versions. + +`gomod-cap` is a simple util to ensure that plugin `go.mod` files does not have higher version requirements than the main gotify `go.mod` file. + +To resolve all incompatible requirements: +```bash +$ go run github.com/gotify/plugin-api/cmd/gomod-cap -from /path/to/gotify/server/go.mod -to /path/to/plugin/go.mod +``` +To only check for incompatible requirements(useful in CI): +```bash +$ go run github.com/gotify/plugin-api/cmd/gomod-cap -from /path/to/gotify/server/go.mod -to /path/to/plugin/go.mod -check=true +``` diff --git a/cmd/gomod-cap/merge.go b/cmd/gomod-cap/merge.go new file mode 100644 index 0000000..c6d1324 --- /dev/null +++ b/cmd/gomod-cap/merge.go @@ -0,0 +1,104 @@ +/*gomod-cap + +This command line util is used to help plugins keep their go.mod file requirements lower than the gotify-server itself in order to make sure that plugins are built with the same version of dependencies as the main gotify program does. + +Since go.mod follow a minimal version selection(https://github.com/golang/proposal/blob/master/design/24301-versioned-go.md). Common import paths described in the plugin go.mod must have a lower or equal version than the main program itself. + +Sample usage: + +$ go run github.com/gotify/plugin-api/cmd/gomod-cap -from /path/to/gotify/server/go.mod -to /path/to/your/plugin/go.mod +*/ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "log" + "os" + "os/exec" + "path" +) + +// Copied from go help mod edit + +type Module struct { + Path string + Version string +} + +type GoMod struct { + Module Module + Require []Require + Exclude []Module + Replace []Replace +} + +type Require struct { + Path string + Version string + Indirect bool +} + +type Replace struct { + Old Module + New Module +} + +func goCmd(args ...string) ([]byte, error) { + cmd := exec.Command("go", args...) + out := bytes.NewBuffer([]byte{}) + cmd.Stdout = out + cmd.Stderr = os.Stderr + err := cmd.Run() + return out.Bytes(), err +} + +func getModuleRequireFromGoModFile(path string) []Require { + var res GoMod + goModJSON, err := goCmd("mod", "edit", "-json", path) + if err != nil { + panic(err) + } + if err := json.Unmarshal(goModJSON, &res); err != nil { + panic(err) + } + return res.Require +} + +func main() { + fromFlag := flag.String("from", "./go.mod", "go.mod file or dir to cap go.mod version from") + toFlag := flag.String("to", "./go.mod", "go.mod file or dir to cap go.mod version to") + checkFlag := flag.Bool("check", false, "check for incompatibility only") + flag.Parse() + + from := *fromFlag + if info, err := os.Stat(from); err == nil && info.IsDir() { + from = path.Join(from, "./", "go.mod") + } + cur := *toFlag + if info, err := os.Stat(cur); err == nil && info.IsDir() { + cur = path.Join(cur, "./", "go.mod") + } + curMods := getModuleRequireFromGoModFile(cur) + fromMods := getModuleRequireFromGoModFile(from) + + modConstraint := make(map[string]string) + for _, req := range fromMods { + modConstraint[req.Path] = req.Version + } + + for _, req := range curMods { + if constraintVersion, ok := modConstraint[req.Path]; ok { + log.Printf("Found common import path %s", req.Path) + if req.Version != constraintVersion { + if *checkFlag { + log.Printf("Found incompatible go.mod constraint: path %s has version %s, higher than %s", req.Path, req.Version, constraintVersion) + os.Exit(1) + } + log.Printf("Changing require version to %s", constraintVersion) + goCmd("mod", "edit", "-require="+req.Path+"@"+constraintVersion, cur) + } + } + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..f3ece91 --- /dev/null +++ b/config.go @@ -0,0 +1,14 @@ +package plugin + +// Configurer is the interface plugins should implement in order to provide configuration interface to the user +type Configurer interface { + Plugin + // DefaultConfig will be called on plugin first run. The default configuration will be provided to the user for future editing. + DefaultConfig() interface{} + // EmptyConfig returns an instance of an empty configuration. Used for generating schemes. + EmptyConfig() interface{} + // ValidateAndSetConfig will be called every time the plugin is initialized or the configuration has been changed by the user. + // Plugins should check whether the configuration is valid and optionally return an error. + // Parameter is guaranteed to be the same type as the return type of EmptyConfig() + ValidateAndSetConfig(c interface{}) error +} diff --git a/displayer.go b/displayer.go new file mode 100644 index 0000000..82383cb --- /dev/null +++ b/displayer.go @@ -0,0 +1,9 @@ +package plugin + +// Displayer is the interface plugins should implement to show a text to the user. +// The text will appear on the plugin details page and can be multi-line. +// Markdown syntax is allowed. Good for providing dynamically generated instructions to the user. +type Displayer interface { + Plugin + GetDisplay() string +} diff --git a/export_ref_do_not_edit.json b/export_ref_do_not_edit.json new file mode 100644 index 0000000..ab550d7 --- /dev/null +++ b/export_ref_do_not_edit.json @@ -0,0 +1 @@ +[{"label":"Displayer","type":"interface","fileName":"displayer.go","pos":276,"members":[{"label":"Plugin","type":"embed"},{"label":"GetDisplay","type":"method","funcSpec":{"returns":[{"type":"type","underlyingType":"string"}]}}]},{"label":"Message","type":"struct","fileName":"messenger.go","pos":93,"members":[{"label":"Message","type":"member"},{"label":"Title","type":"member"},{"label":"Priority","type":"member"}]},{"label":"MessageHandler","type":"interface","fileName":"messenger.go","pos":237,"members":[{"label":"SendMessage","type":"method","funcSpec":{"params":[{"type":"type","underlyingType":"Message"}],"returns":[{"type":"type","underlyingType":"error"}]}}]},{"label":"Messenger","type":"interface","fileName":"messenger.go","pos":452,"members":[{"label":"Plugin","type":"embed"},{"label":"SetMessageHandler","type":"method","funcSpec":{"params":[{"type":"type","underlyingType":"MessageHandler"}]}}]},{"label":"Plugin","type":"interface","fileName":"plugin.go","pos":79,"members":[{"label":"Enable","type":"method","funcSpec":{"returns":[{"type":"type","underlyingType":"error"}]}},{"label":"Disable","type":"method","funcSpec":{"returns":[{"type":"type","underlyingType":"error"}]}}]},{"label":"UserContext","type":"struct","fileName":"plugin.go","pos":473,"members":[{"label":"ID","type":"member"},{"label":"Name","type":"member"},{"label":"Admin","type":"member"}]},{"label":"Info","type":"struct","fileName":"plugin.go","pos":731,"members":[{"label":"Version","type":"member"},{"label":"Author","type":"member"},{"label":"Name","type":"member"},{"label":"Website","type":"member"},{"label":"Description","type":"member"},{"label":"License","type":"member"},{"label":"ModulePath","type":"member"}]},{"label":"Configurer","type":"interface","fileName":"config.go","pos":133,"members":[{"label":"Plugin","type":"embed"},{"label":"DefaultConfig","type":"method","funcSpec":{"returns":[{"type":"interface"}]}},{"label":"EmptyConfig","type":"method","funcSpec":{"returns":[{"type":"interface"}]}},{"label":"ValidateAndSetConfig","type":"method","funcSpec":{"params":[{"type":"interface"}],"returns":[{"type":"type","underlyingType":"error"}]}}]},{"label":"Webhooker","type":"interface","fileName":"webhook.go","pos":139,"members":[{"label":"Plugin","type":"embed"},{"label":"RegisterWebhook","type":"method","funcSpec":{"params":[{"label":"gin.RouterGroup","type":"selector"}]}}]},{"label":"StorageHandler","type":"interface","fileName":"storage.go","pos":125,"members":[{"label":"Save","type":"method","funcSpec":{"params":[{"label":"[]byte","type":"array"}],"returns":[{"type":"type","underlyingType":"error"}]}},{"label":"Load","type":"method","funcSpec":{"returns":[{"label":"[]byte","type":"array"},{"type":"type","underlyingType":"error"}]}}]},{"label":"Storager","type":"interface","fileName":"storage.go","pos":287,"members":[{"label":"Plugin","type":"embed"},{"label":"SetStorageHandler","type":"method","funcSpec":{"params":[{"type":"type","underlyingType":"StorageHandler"}]}}]}] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7f4b6c2 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/gotify/plugin-api + +require ( + github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect + github.com/gin-gonic/gin v1.3.0 + github.com/golang/protobuf v1.2.0 // indirect + github.com/json-iterator/go v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/stretchr/testify v1.3.0 // indirect + github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect + golang.org/x/net v0.0.0-20190110200230-915654e7eabc // indirect + golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect + golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fde3a63 --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM= +golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb h1:1w588/yEchbPNpa9sEvOcMZYbWHedwJjg4VOAdDHWHk= +golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/messenger.go b/messenger.go new file mode 100644 index 0000000..134c305 --- /dev/null +++ b/messenger.go @@ -0,0 +1,22 @@ +package plugin + +// Message describes a message to be send by MessageHandler#SendMessage +type Message struct { + Message string + Title string + Priority int +} + +// MessageHandler consists of message callbacks to be used by plugins. +type MessageHandler interface { + // SendMessage sends a message with the given information in the request. + SendMessage(msg Message) error +} + +// Messenger is the interface plugins should implement to send messages. +type Messenger interface { + Plugin + // SetMessageHandler is called every time the plugin is initialized. + // Plugins should record the handler and use the callbacks provided in the handler to send messages. + SetMessageHandler(h MessageHandler) +} diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..9b5238b --- /dev/null +++ b/plugin.go @@ -0,0 +1,29 @@ +package plugin + +// Plugin is the interface every plugin need to implement +type Plugin interface { + // Enable is called every time a plugin is started. Spawn custom goroutines here for polling, etc. + // It is always called after ^Set.*Handler$ + Enable() error + // Disable is called every time a plugin is disabled. Plugins should stop all custom goroutines here. + Disable() error +} + +// UserContext is provided when calling New to create a plugin instance for each user +type UserContext struct { + ID uint + Name string + Admin bool +} + +// Info is returned by the exported plugin function GetPluginInfo() for identification +// plugins are identified by their ModulePath, gotify will refuse to load plugins with empty ModulePath +type Info struct { + Version string + Author string + Name string + Website string + Description string + License string + ModulePath string +} diff --git a/storage.go b/storage.go new file mode 100644 index 0000000..0fbeeef --- /dev/null +++ b/storage.go @@ -0,0 +1,13 @@ +package plugin + +// StorageHandler consists callbacks used to perform read/writes to the persistent storage for plugins. +type StorageHandler interface { + Save(b []byte) error + Load() ([]byte, error) +} + +// Storager is the interface plugins should implement to use persistent storage. +type Storager interface { + Plugin + SetStorageHandler(handler StorageHandler) +} diff --git a/webhook.go b/webhook.go new file mode 100644 index 0000000..296e304 --- /dev/null +++ b/webhook.go @@ -0,0 +1,11 @@ +package plugin + +import "github.com/gin-gonic/gin" + +// Webhooker is the interface plugin should implement to register custom handlers. +type Webhooker interface { + Plugin + // RegisterWebhook is called for plugins to create their own handler. + // Plugins can call mux.BasePath() to acquire their custom handler base path. + RegisterWebhook(mux gin.RouterGroup) +}