Initial commit from mattermost-plugin-starter-template
This commit is contained in:
commit
acbc69f7eb
57 changed files with 27772 additions and 0 deletions
2
server/.gitignore
vendored
Normal file
2
server/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
coverage.txt
|
||||
dist
|
42
server/api.go
Normal file
42
server/api.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
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.
|
||||
// The root URL is currently <siteUrl>/plugins/com.mattermost.bridge-xmpp/api/v1/. Replace com.mattermost.bridge-xmpp with the plugin ID.
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
64
server/command/command.go
Normal file
64
server/command/command.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
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 {
|
||||
err := client.SlashCommand.Register(&model.Command{
|
||||
Trigger: helloCommandTrigger,
|
||||
AutoComplete: true,
|
||||
AutoCompleteDesc: "Say hello to someone",
|
||||
AutoCompleteHint: "[@username]",
|
||||
AutocompleteData: model.NewAutocompleteData(helloCommandTrigger, "[@username]", "Username to say hello to"),
|
||||
})
|
||||
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,
|
||||
}
|
||||
}
|
47
server/command/command_test.go
Normal file
47
server/command/command_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
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]",
|
||||
AutocompleteData: model.NewAutocompleteData("hello", "[@username]", "Username to say hello to"),
|
||||
}).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
server/command/mocks/mock_commands.go
Normal file
64
server/command/mocks/mock_commands.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/mattermost/mattermost-plugin-bridge-xmpp/server/command (interfaces: Command)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
model "github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// MockCommand is a mock of Command interface.
|
||||
type MockCommand struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockCommandMockRecorder
|
||||
}
|
||||
|
||||
// MockCommandMockRecorder is the mock recorder for MockCommand.
|
||||
type MockCommandMockRecorder struct {
|
||||
mock *MockCommand
|
||||
}
|
||||
|
||||
// NewMockCommand creates a new mock instance.
|
||||
func NewMockCommand(ctrl *gomock.Controller) *MockCommand {
|
||||
mock := &MockCommand{ctrl: ctrl}
|
||||
mock.recorder = &MockCommandMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockCommand) EXPECT() *MockCommandMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Handle mocks base method.
|
||||
func (m *MockCommand) Handle(arg0 *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Handle", arg0)
|
||||
ret0, _ := ret[0].(*model.CommandResponse)
|
||||
ret1, _ := ret[1].(*model.AppError)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Handle indicates an expected call of Handle.
|
||||
func (mr *MockCommandMockRecorder) Handle(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Handle", reflect.TypeOf((*MockCommand)(nil).Handle), arg0)
|
||||
}
|
||||
|
||||
// executeHelloCommand mocks base method.
|
||||
func (m *MockCommand) executeHelloCommand(arg0 *model.CommandArgs) *model.CommandResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "executeHelloCommand", arg0)
|
||||
ret0, _ := ret[0].(*model.CommandResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// executeHelloCommand indicates an expected call of executeHelloCommand.
|
||||
func (mr *MockCommandMockRecorder) executeHelloCommand(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "executeHelloCommand", reflect.TypeOf((*MockCommand)(nil).executeHelloCommand), arg0)
|
||||
}
|
83
server/configuration.go
Normal file
83
server/configuration.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// configuration captures the plugin's external configuration as exposed in the Mattermost server
|
||||
// configuration, as well as values computed from the configuration. Any public fields will be
|
||||
// deserialized from the Mattermost server configuration in OnConfigurationChange.
|
||||
//
|
||||
// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin
|
||||
// configuration can change at any time, access to the configuration must be synchronized. The
|
||||
// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire
|
||||
// struct whenever it changes. You may replace this with whatever strategy you choose.
|
||||
//
|
||||
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
|
||||
// copy appropriate for your types.
|
||||
type configuration struct {
|
||||
}
|
||||
|
||||
// Clone shallow copies the configuration. Your implementation may require a deep copy if
|
||||
// your configuration has reference types.
|
||||
func (c *configuration) Clone() *configuration {
|
||||
var clone = *c
|
||||
return &clone
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
if p.configuration == nil {
|
||||
return &configuration{}
|
||||
}
|
||||
|
||||
return p.configuration
|
||||
}
|
||||
|
||||
// setConfiguration replaces the active configuration under lock.
|
||||
//
|
||||
// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not
|
||||
// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a
|
||||
// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur.
|
||||
//
|
||||
// This method panics if setConfiguration is called with the existing configuration. This almost
|
||||
// certainly means that the configuration was modified without being cloned and may result in
|
||||
// an unsafe access.
|
||||
func (p *Plugin) setConfiguration(configuration *configuration) {
|
||||
p.configurationLock.Lock()
|
||||
defer p.configurationLock.Unlock()
|
||||
|
||||
if configuration != nil && p.configuration == configuration {
|
||||
// Ignore assignment if the configuration struct is empty. Go will optimize the
|
||||
// allocation for same to point at the same memory address, breaking the check
|
||||
// above.
|
||||
if reflect.ValueOf(*configuration).NumField() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
panic("setConfiguration called with the existing configuration")
|
||||
}
|
||||
|
||||
p.configuration = configuration
|
||||
}
|
||||
|
||||
// OnConfigurationChange is invoked when configuration changes may have been made.
|
||||
func (p *Plugin) OnConfigurationChange() error {
|
||||
var configuration = new(configuration)
|
||||
|
||||
// Load the public configuration fields from the Mattermost server configuration.
|
||||
if err := p.API.LoadPluginConfiguration(configuration); err != nil {
|
||||
return errors.Wrap(err, "failed to load plugin configuration")
|
||||
}
|
||||
|
||||
p.setConfiguration(configuration)
|
||||
|
||||
return nil
|
||||
}
|
6
server/job.go
Normal file
6
server/job.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package main
|
||||
|
||||
func (p *Plugin) runJob() {
|
||||
// Include job logic here
|
||||
p.API.LogInfo("Job is currently running")
|
||||
}
|
9
server/main.go
Normal file
9
server/main.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&Plugin{})
|
||||
}
|
82
server/plugin.go
Normal file
82
server/plugin.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/command"
|
||||
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/store/kvstore"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
"github.com/mattermost/mattermost/server/public/pluginapi"
|
||||
"github.com/mattermost/mattermost/server/public/pluginapi/cluster"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
|
||||
type Plugin struct {
|
||||
plugin.MattermostPlugin
|
||||
|
||||
// kvstore is the client used to read/write KV records for this plugin.
|
||||
kvstore kvstore.KVStore
|
||||
|
||||
// client is the Mattermost server API client.
|
||||
client *pluginapi.Client
|
||||
|
||||
// commandClient is the client used to register and execute slash commands.
|
||||
commandClient command.Command
|
||||
|
||||
backgroundJob *cluster.Job
|
||||
|
||||
// configurationLock synchronizes access to the configuration.
|
||||
configurationLock sync.RWMutex
|
||||
|
||||
// configuration is the active plugin configuration. Consult getConfiguration and
|
||||
// setConfiguration for usage.
|
||||
configuration *configuration
|
||||
}
|
||||
|
||||
// OnActivate is invoked when the plugin is activated. If an error is returned, the plugin will be deactivated.
|
||||
func (p *Plugin) OnActivate() error {
|
||||
p.client = pluginapi.NewClient(p.API, p.Driver)
|
||||
|
||||
p.kvstore = kvstore.NewKVStore(p.client)
|
||||
|
||||
p.commandClient = command.NewCommandHandler(p.client)
|
||||
|
||||
job, err := cluster.Schedule(
|
||||
p.API,
|
||||
"BackgroundJob",
|
||||
cluster.MakeWaitForRoundedInterval(1*time.Hour),
|
||||
p.runJob,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to schedule background job")
|
||||
}
|
||||
|
||||
p.backgroundJob = job
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnDeactivate is invoked when the plugin is deactivated.
|
||||
func (p *Plugin) OnDeactivate() error {
|
||||
if p.backgroundJob != nil {
|
||||
if err := p.backgroundJob.Close(); err != nil {
|
||||
p.API.LogError("Failed to close background job", "err", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This will execute the commands that were registered in the NewCommandHandler function.
|
||||
func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
response, err := p.commandClient.Handle(args)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ExecuteCommand", "plugin.command.execute_command.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// See https://developers.mattermost.com/extend/plugins/server/reference/
|
29
server/plugin_test.go
Normal file
29
server/plugin_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
plugin := Plugin{}
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/v1/hello", nil)
|
||||
r.Header.Set("Mattermost-User-ID", "test-user-id")
|
||||
|
||||
plugin.ServeHTTP(nil, w, r)
|
||||
|
||||
result := w.Result()
|
||||
assert.NotNil(result)
|
||||
defer result.Body.Close()
|
||||
bodyBytes, err := io.ReadAll(result.Body)
|
||||
assert.Nil(err)
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
assert.Equal("Hello, world!", bodyString)
|
||||
}
|
6
server/store/kvstore/kvstore.go
Normal file
6
server/store/kvstore/kvstore.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package kvstore
|
||||
|
||||
type KVStore interface {
|
||||
// Define your methods here. This package is used to access the KVStore pluginapi methods.
|
||||
GetTemplateData(userID string) (string, error)
|
||||
}
|
29
server/store/kvstore/startertemplate.go
Normal file
29
server/store/kvstore/startertemplate.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package kvstore
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/pluginapi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// We expose our calls to the KVStore pluginapi methods through this interface for testability and stability.
|
||||
// This allows us to better control which values are stored with which keys.
|
||||
|
||||
type Client struct {
|
||||
client *pluginapi.Client
|
||||
}
|
||||
|
||||
func NewKVStore(client *pluginapi.Client) KVStore {
|
||||
return Client{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// Sample method to get a key-value pair in the KV store
|
||||
func (kv Client) GetTemplateData(userID string) (string, error) {
|
||||
var templateData string
|
||||
err := kv.client.KV.Get("template_key-"+userID, &templateData)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get template data")
|
||||
}
|
||||
return templateData, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue