Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add boilerplate for api, commands, kvstore access and job management #212

Merged
merged 10 commits into from
Jan 7, 2025
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,9 @@ logs-watch:
# Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort

mock:
ifneq ($(HAS_SERVER),)
go install github.com/golang/mock/[email protected]
mockgen -destination=server/command/mocks/mock_commands.go -package=mocks github.com/mattermost/mattermost-plugin-starter-template/server/command Command
endif
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ To avoid having to manually install your plugin, build and deploy your plugin us
}
```

### Modifying the server boilerplate

The server code comes with some boilerplate for creating an api, using slash commands, accessing the kvstore and using the cluster package for jobs.

#### Api

api.go implements the ServeHTTP hook which allows the plugin to implement the http.Handler interface. Requests destined for the `/plugins/{id}` path will be routed to the plugin. This file also contains a sample `HelloWorld` endpoint that is tested in plugin_test.go.

#### Command package

This package contains the boilerplate for adding a slash command and an instance of it is created in the `OnActivate` hook in plugin.go. If you don't need it you can delete the package and remove any reference to `commandClient` in plugin.go. The package also contains an example of how to create a mock for testing.

#### KVStore package

This is a central place for you to access the KVStore methods that are available in the `pluginapi.Client`. The package contains an interface for you to define your methods that will wrap the KVStore methods. An instance of the KVStore is created in the `OnActivate` hook.

#### Jobs package
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a package anymore.


The cluster package in the pluginapi contains methods to run cluster aware jobs, there is an example of it's usage in plugin.go.

### Deploying with Local Mode

If your Mattermost server is running locally, you can enable [local mode](https://docs.mattermost.com/administration/mmctl-cli-tool.html#local-mode) to streamline deploying your plugin. Edit your server configuration as follows:
Expand Down
25 changes: 17 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ module github.com/mattermost/mattermost-plugin-starter-template
go 1.21

require (
github.com/mattermost/mattermost/server/public v0.0.14
github.com/golang/mock v1.6.0
github.com/mattermost/mattermost/server/public v0.1.4
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.4
)

require (
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/objx v0.5.1 // indirect
)

require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
Expand All @@ -17,7 +25,8 @@ require (
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/go-hclog v1.6.2 // indirect
github.com/hashicorp/go-plugin v1.6.0 // indirect
Expand All @@ -40,13 +49,13 @@ require (
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/crypto v0.20.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
google.golang.org/grpc v1.60.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/grpc v1.62.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
74 changes: 56 additions & 18 deletions go.sum

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions server/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"net/http"

"github.com/gorilla/mux"
"github.com/mattermost/mattermost/server/public/plugin"
)

// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
BenCookie95 marked this conversation as resolved.
Show resolved Hide resolved
func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
router := mux.NewRouter()

// Middleware to require that the user is logged in
router.Use(p.MattermostAuthorizationRequired)

apiRouter := router.PathPrefix("/api/v1").Subrouter()

apiRouter.HandleFunc("/hello", p.HelloWorld).Methods(http.MethodGet)

router.ServeHTTP(w, r)
}

func (p *Plugin) MattermostAuthorizationRequired(next http.Handler) http.Handler {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be awesome to hoist this into the public package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

next.ServeHTTP(w, r)
})
}

func (p *Plugin) HelloWorld(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write([]byte("Hello, world!")); err != nil {
p.API.LogError("Failed to write response", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
63 changes: 63 additions & 0 deletions server/command/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package command

import (
"fmt"
"strings"

"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi"
)

type Handler struct {
client *pluginapi.Client
}

type Command interface {
Handle(args *model.CommandArgs) (*model.CommandResponse, error)
executeHelloCommand(args *model.CommandArgs) *model.CommandResponse
}

const helloCommandTrigger = "hello"

// Register all your slash commands in the NewCommandHandler function.
func NewCommandHandler(client *pluginapi.Client) Command {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One day, it would be awesome to hoist this into the public package and just register callbacks for the slash commands to be handled.

In that future, I wonder if we would need a discrete package at all and could just do this all in command.go?

err := client.SlashCommand.Register(&model.Command{
Trigger: helloCommandTrigger,
AutoComplete: true,
AutoCompleteDesc: "Say hello to someone",
AutoCompleteHint: "<username>",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should illustrate the advanced autocomplete functionality?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is that advanced functionality? I'm not aware, is there something I can read?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

})
if err != nil {
client.Log.Error("Failed to register command", "error", err)
}
return &Handler{
client: client,
}
}

// ExecuteCommand hook calls this method to execute the commands that were registered in the NewCommandHandler function.
func (c *Handler) Handle(args *model.CommandArgs) (*model.CommandResponse, error) {
trigger := strings.TrimPrefix(strings.Fields(args.Command)[0], "/")
switch trigger {
case helloCommandTrigger:
return c.executeHelloCommand(args), nil
default:
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: fmt.Sprintf("Unknown command: %s", args.Command),
}, nil
}
}

func (c *Handler) executeHelloCommand(args *model.CommandArgs) *model.CommandResponse {
if len(strings.Fields(args.Command)) < 2 {
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: "Please specify a username",
}
}
username := strings.Fields(args.Command)[1]
return &model.CommandResponse{
Text: "Hello, " + username,
}
}
46 changes: 46 additions & 0 deletions server/command/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package command

import (
"testing"

"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
"github.com/mattermost/mattermost/server/public/pluginapi"
"github.com/stretchr/testify/assert"
)

type env struct {
client *pluginapi.Client
api *plugintest.API
}

func setupTest() *env {
api := &plugintest.API{}
driver := &plugintest.Driver{}
client := pluginapi.NewClient(api, driver)

return &env{
client: client,
api: api,
}
}

func TestHelloCommand(t *testing.T) {
assert := assert.New(t)
env := setupTest()

env.api.On("RegisterCommand", &model.Command{
Trigger: helloCommandTrigger,
AutoComplete: true,
AutoCompleteDesc: "Say hello to someone",
AutoCompleteHint: "<username>",
}).Return(nil)
cmdHandler := NewCommandHandler(env.client)

args := &model.CommandArgs{
Command: "/hello world",
}
response, err := cmdHandler.Handle(args)
assert.Nil(err)
assert.Equal("Hello, world", response.Text)
}
64 changes: 64 additions & 0 deletions server/command/mocks/mock_commands.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ func (c *configuration) Clone() *configuration {
// getConfiguration retrieves the active configuration under lock, making it safe to use
// concurrently. The active configuration may change underneath the client of this method, but
// the struct returned by this API call is considered immutable.
func (p *Plugin) getConfiguration() *configuration {
p.configurationLock.RLock()
defer p.configurationLock.RUnlock()
// func (p *Plugin) getConfiguration() *configuration {
// p.configurationLock.RLock()
// defer p.configurationLock.RUnlock()

if p.configuration == nil {
return &configuration{}
}
// if p.configuration == nil {
// return &configuration{}
// }

return p.configuration
}
// return p.configuration
// }
BenCookie95 marked this conversation as resolved.
Show resolved Hide resolved

// setConfiguration replaces the active configuration under lock.
//
Expand Down
5 changes: 5 additions & 0 deletions server/job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package main

func (p *Plugin) runJob() {
// Include job logic here
BenCookie95 marked this conversation as resolved.
Show resolved Hide resolved
}
Loading