[MM-53968] Includes mattermost-plugin-api into the mono repo (#24235)

Include https://github.com/mattermost/mattermost-plugin-api into the mono repo

Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com>
Co-authored-by: Michael Kochell <mjkochell@gmail.com>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Co-authored-by: Alex Dovenmuehle <alex.dovenmuehle@mattermost.com>
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
Co-authored-by: Christopher Poile <cpoile@gmail.com>
Co-authored-by: İlker Göktuğ Öztürk <ilkergoktugozturk@gmail.com>
Co-authored-by: Shota Gvinepadze <wineson@gmail.com>
Co-authored-by: Ali Farooq <ali.farooq0@pm.me>
Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com>
Co-authored-by: Daniel Espino García <larkox@gmail.com>
Co-authored-by: Christopher Speller <crspeller@gmail.com>
Co-authored-by: Alex Dovenmuehle <adovenmuehle@gmail.com>
Co-authored-by: Szymon Gibała <szymongib@gmail.com>
Co-authored-by: Lev <1187448+levb@users.noreply.github.com>
Co-authored-by: Jason Frerich <jason.frerich@mattermost.com>
Co-authored-by: Agniva De Sarker <agnivade@yahoo.co.in>
Co-authored-by: Artur M. Wolff <artur.m.wolff@gmail.com>
Co-authored-by: Madhav Hugar <16546715+madhavhugar@users.noreply.github.com>
Co-authored-by: Joe <security.joe@pm.me>
Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Co-authored-by: José Peso <trilopin@users.noreply.github.com>
Этот коммит содержится в:
Ben Schumacher
2023-08-21 09:50:30 +02:00
коммит произвёл GitHub
родитель bc11b29807
Коммит 3ee5432664
117 изменённых файлов: 14912 добавлений и 5 удалений

Просмотреть файл

@@ -0,0 +1,53 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.
package bot
import (
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi"
)
type Bot interface {
Ensure(stored *model.Bot, iconPath string) error
MattermostUserID() string
String() string
}
type bot struct {
botService pluginapi.BotService
mattermostUserID string
displayName string
}
func New(botService pluginapi.BotService) Bot {
newBot := &bot{
botService: botService,
}
return newBot
}
func (bot *bot) Ensure(stored *model.Bot, iconPath string) error {
if bot.mattermostUserID != "" {
// Already done
return nil
}
botUserID, err := bot.botService.EnsureBot(stored, pluginapi.ProfileImagePath(iconPath))
if err != nil {
return errors.Wrap(err, "failed to ensure bot account")
}
bot.mattermostUserID = botUserID
bot.displayName = stored.DisplayName
return nil
}
func (bot *bot) MattermostUserID() string {
return bot.mattermostUserID
}
func (bot *bot) String() string {
return bot.displayName
}

Просмотреть файл

@@ -0,0 +1,97 @@
package admincclogger
import (
"fmt"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/common"
)
type adminCCLogger struct {
logger.Logger
dmer poster.DMer
logLevel logger.LogLevel
includeContext bool
userIDs []string
}
/*
New promotes the provided logger into a admin cc logger, sending direct messages to all the admin
ids provided through the dmer provided, about all events below the logLevel. If logVerbose is set,
it will also send the context.
- l Logger: A logger to promote.
- dmer DMer: A DMer to send the messages to the admins.
- logLevel: The highest type of message to be stored in telemetry.
- includeContext: Whether the log context should be messaged to the admins.
- userIDs: The user IDs of the admins.
*/
func New(l logger.Logger, dmer poster.DMer, logLevel logger.LogLevel, includeContext bool, userIDs ...string) logger.Logger {
return &adminCCLogger{
Logger: l,
dmer: dmer,
logLevel: logLevel,
includeContext: includeContext,
userIDs: userIDs,
}
}
// NewFromAPI creates a adminCCLogger directly from a LogAPI instead of passing a logger.
func NewFromAPI(api common.LogAPI, dmer poster.DMer, logLevel logger.LogLevel, includeContext bool, userIDs ...string) logger.Logger {
return New(logger.New(api), dmer, logLevel, includeContext, userIDs...)
}
func (l *adminCCLogger) Debugf(format string, args ...interface{}) {
l.Logger.Debugf(format, args...)
message := fmt.Sprintf(format, args...)
if logger.Level(l.logLevel) >= 4 {
l.logToAdmins("DEBUG", message)
}
}
func (l *adminCCLogger) Errorf(format string, args ...interface{}) {
l.Logger.Errorf(format, args...)
message := fmt.Sprintf(format, args...)
if logger.Level(l.logLevel) >= 1 {
l.logToAdmins("ERROR", message)
}
}
func (l *adminCCLogger) Infof(format string, args ...interface{}) {
l.Logger.Infof(format, args...)
message := fmt.Sprintf(format, args...)
if logger.Level(l.logLevel) >= 3 {
l.logToAdmins("INFO", message)
}
}
func (l *adminCCLogger) Warnf(format string, args ...interface{}) {
l.Logger.Warnf(format, args...)
message := fmt.Sprintf(format, args...)
if logger.Level(l.logLevel) >= 2 {
l.logToAdmins("WARN", message)
}
}
func (l *adminCCLogger) logToAdmins(level, message string) {
context := l.Context()
if l.includeContext && len(context) > 0 {
message += "\n" + common.JSONBlock(context)
}
_ = l.dmAdmins("(log " + level + ") " + message)
}
func (l *adminCCLogger) dmAdmins(format string, args ...interface{}) error {
for _, id := range l.userIDs {
_, err := l.dmer.DM(id, format, args)
if err != nil {
return err
}
}
return nil
}

Просмотреть файл

@@ -0,0 +1,82 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.
package logger
import (
"fmt"
"time"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/common"
)
type defaultLogger struct {
logContext LogContext
logAPI common.LogAPI
}
/*
New creates a new logger.
- api: LogAPI implementation
*/
func New(api common.LogAPI) Logger {
l := &defaultLogger{
logAPI: api,
}
return l
}
func (l *defaultLogger) With(logContext LogContext) Logger {
newLogger := *l
if len(newLogger.logContext) == 0 {
newLogger.logContext = map[string]interface{}{}
}
for k, v := range logContext {
newLogger.logContext[k] = v
}
return &newLogger
}
func (l *defaultLogger) WithError(err error) Logger {
newLogger := *l
if len(newLogger.logContext) == 0 {
newLogger.logContext = map[string]interface{}{}
}
newLogger.logContext[ErrorKey] = err.Error()
return &newLogger
}
func (l *defaultLogger) Context() LogContext {
return l.logContext
}
func (l *defaultLogger) Timed() Logger {
return l.With(LogContext{
timed: time.Now(),
})
}
func (l *defaultLogger) Debugf(format string, args ...interface{}) {
measure(l.logContext)
message := fmt.Sprintf(format, args...)
l.logAPI.LogDebug(message, toKeyValuePairs(l.logContext)...)
}
func (l *defaultLogger) Errorf(format string, args ...interface{}) {
measure(l.logContext)
message := fmt.Sprintf(format, args...)
l.logAPI.LogError(message, toKeyValuePairs(l.logContext)...)
}
func (l *defaultLogger) Infof(format string, args ...interface{}) {
measure(l.logContext)
message := fmt.Sprintf(format, args...)
l.logAPI.LogInfo(message, toKeyValuePairs(l.logContext)...)
}
func (l *defaultLogger) Warnf(format string, args ...interface{}) {
measure(l.logContext)
message := fmt.Sprintf(format, args...)
l.logAPI.LogWarn(message, toKeyValuePairs(l.logContext)...)
}

Просмотреть файл

@@ -0,0 +1,78 @@
package logger
import "time"
const (
timed = "__since"
elapsed = "Elapsed"
ErrorKey = "error"
)
// LogLevel defines the level of log messages
type LogLevel string
const (
// LogLevelDebug denotes debug messages
LogLevelDebug = "debug"
// LogLevelInfo denotes info messages
LogLevelInfo = "info"
// LogLevelWarn denotes warn messages
LogLevelWarn = "warn"
// LogLevelError denotes error messages
LogLevelError = "error"
)
// LogContext defines the context for the logs.
type LogContext map[string]interface{}
// Logger defines an object able to log messages.
type Logger interface {
// With adds a logContext to the logger.
With(LogContext) Logger
// WithError adds an Error to the logger.
WithError(error) Logger
// Context returns the current context
Context() LogContext
// Timed add a timed log context.
Timed() Logger
// Debugf logs a formatted string as a debug message.
Debugf(format string, args ...interface{})
// Errorf logs a formatted string as an error message.
Errorf(format string, args ...interface{})
// Infof logs a formatted string as an info message.
Infof(format string, args ...interface{})
// Warnf logs a formatted string as an warning message.
Warnf(format string, args ...interface{})
}
func measure(lc LogContext) {
if lc[timed] == nil {
return
}
started := lc[timed].(time.Time)
lc[elapsed] = time.Since(started).String()
delete(lc, timed)
}
// Level assigns an integer to the LogLevel string
func Level(l LogLevel) int {
switch l {
case LogLevelDebug:
return 4
case LogLevelInfo:
return 3
case LogLevelWarn:
return 2
case LogLevelError:
return 1
}
return 0
}
func toKeyValuePairs(in map[string]interface{}) (out []interface{}) {
for k, v := range in {
out = append(out, k, v)
}
return out
}

Просмотреть файл

@@ -0,0 +1,17 @@
package logger
type nilLogger struct{}
// NewNilLogger returns a logger that performs no action.
func NewNilLogger() Logger {
return &nilLogger{}
}
func (l *nilLogger) With(LogContext) Logger { return l }
func (l *nilLogger) WithError(error) Logger { return l }
func (l *nilLogger) Context() LogContext { return nil }
func (l *nilLogger) Timed() Logger { return l }
func (l *nilLogger) Debugf(string, ...interface{}) {}
func (l *nilLogger) Errorf(string, ...interface{}) {}
func (l *nilLogger) Infof(string, ...interface{}) {}
func (l *nilLogger) Warnf(string, ...interface{}) {}

Просмотреть файл

@@ -0,0 +1,80 @@
package telemetrylogger
import (
"fmt"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/common"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/telemetry"
)
type telemetryLogger struct {
logger.Logger
logLevel logger.LogLevel
tracker telemetry.Tracker
}
/*
New promotes the provided logger into a telemetry logger, storing all events below the logLevel
through the tracker.
- l Logger: A logger to promote.
- logLevel: The highest type of message to be stored in telemetry.
- tracker: The telemetry tracker to store the messages.
*/
func New(l logger.Logger, logLevel logger.LogLevel, tracker telemetry.Tracker) logger.Logger {
return &telemetryLogger{
Logger: l,
logLevel: logLevel,
tracker: tracker,
}
}
// NewFromAPI creates a telemetryLogger directly from a LogAPI instead of passing a logger.
func NewFromAPI(api common.LogAPI, logLevel logger.LogLevel, tracker telemetry.Tracker) logger.Logger {
return New(logger.New(api), logLevel, tracker)
}
func (l *telemetryLogger) Debugf(format string, args ...interface{}) {
l.Logger.Debugf(format, args...)
message := fmt.Sprintf(format, args...)
if logger.Level(l.logLevel) >= 4 {
l.logToTelemetry("DEBUG", message)
}
}
func (l *telemetryLogger) Errorf(format string, args ...interface{}) {
l.Logger.Errorf(format, args...)
message := fmt.Sprintf(format, args...)
if logger.Level(l.logLevel) >= 1 {
l.logToTelemetry("ERROR", message)
}
}
func (l *telemetryLogger) Infof(format string, args ...interface{}) {
l.Logger.Infof(format, args...)
message := fmt.Sprintf(format, args...)
if logger.Level(l.logLevel) >= 3 {
l.logToTelemetry("INFO", message)
}
}
func (l *telemetryLogger) Warnf(format string, args ...interface{}) {
l.Logger.Warnf(format, args...)
message := fmt.Sprintf(format, args...)
if logger.Level(l.logLevel) >= 2 {
l.logToTelemetry("WARN", message)
}
}
func (l *telemetryLogger) logToTelemetry(level, message string) {
properties := map[string]interface{}{}
properties["message"] = message
for k, v := range l.Context() {
properties["context_"+k] = fmt.Sprintf("%v", v)
}
_ = l.tracker.TrackEvent("logger_"+level, properties)
}

Просмотреть файл

@@ -0,0 +1,61 @@
package logger
import (
"fmt"
"testing"
"time"
)
type testLogger struct {
testing.TB
logContext LogContext
}
// NewTestLogger creates a logger for testing purposes.
func NewTestLogger() Logger {
return &testLogger{}
}
func (l *testLogger) With(logContext LogContext) Logger {
newl := *l
if len(newl.logContext) == 0 {
newl.logContext = map[string]interface{}{}
}
for k, v := range logContext {
newl.logContext[k] = v
}
return &newl
}
func (l *testLogger) WithError(err error) Logger {
newl := *l
if len(newl.logContext) == 0 {
newl.logContext = map[string]interface{}{}
}
newl.logContext[ErrorKey] = err.Error()
return &newl
}
func (l *testLogger) Context() LogContext {
return l.logContext
}
func (l *testLogger) Timed() Logger {
return l.With(LogContext{
timed: time.Now(),
})
}
func (l *testLogger) logf(prefix, format string, args ...interface{}) {
out := fmt.Sprintf(prefix+": "+format, args...)
if len(l.logContext) > 0 {
measure(l.logContext)
out += fmt.Sprintf(" -- %+v", l.logContext)
}
l.TB.Logf(out)
}
func (l *testLogger) Debugf(format string, args ...interface{}) { l.logf("DEBUG", format, args...) }
func (l *testLogger) Errorf(format string, args ...interface{}) { l.logf("ERROR", format, args...) }
func (l *testLogger) Infof(format string, args ...interface{}) { l.logf("INFO", format, args...) }
func (l *testLogger) Warnf(format string, args ...interface{}) { l.logf("WARN", format, args...) }

Просмотреть файл

@@ -0,0 +1,77 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot (interfaces: Bot)
// Package mock_bot is a generated GoMock package.
package mock_bot
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
model "github.com/mattermost/mattermost/server/public/model"
)
// MockBot is a mock of Bot interface.
type MockBot struct {
ctrl *gomock.Controller
recorder *MockBotMockRecorder
}
// MockBotMockRecorder is the mock recorder for MockBot.
type MockBotMockRecorder struct {
mock *MockBot
}
// NewMockBot creates a new mock instance.
func NewMockBot(ctrl *gomock.Controller) *MockBot {
mock := &MockBot{ctrl: ctrl}
mock.recorder = &MockBotMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockBot) EXPECT() *MockBotMockRecorder {
return m.recorder
}
// Ensure mocks base method.
func (m *MockBot) Ensure(arg0 *model.Bot, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ensure", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Ensure indicates an expected call of Ensure.
func (mr *MockBotMockRecorder) Ensure(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ensure", reflect.TypeOf((*MockBot)(nil).Ensure), arg0, arg1)
}
// MattermostUserID mocks base method.
func (m *MockBot) MattermostUserID() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MattermostUserID")
ret0, _ := ret[0].(string)
return ret0
}
// MattermostUserID indicates an expected call of MattermostUserID.
func (mr *MockBotMockRecorder) MattermostUserID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MattermostUserID", reflect.TypeOf((*MockBot)(nil).MattermostUserID))
}
// String mocks base method.
func (m *MockBot) String() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "String")
ret0, _ := ret[0].(string)
return ret0
}
// String indicates an expected call of String.
func (mr *MockBotMockRecorder) String() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockBot)(nil).String))
}

Просмотреть файл

@@ -0,0 +1,159 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger (interfaces: Logger)
// Package mock_bot is a generated GoMock package.
package mock_bot
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
logger "github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Context mocks base method.
func (m *MockLogger) Context() logger.LogContext {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Context")
ret0, _ := ret[0].(logger.LogContext)
return ret0
}
// Context indicates an expected call of Context.
func (mr *MockLoggerMockRecorder) Context() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockLogger)(nil).Context))
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(arg0 string, arg1 ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(arg0 string, arg1 ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(arg0 string, arg1 ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
// Timed mocks base method.
func (m *MockLogger) Timed() logger.Logger {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Timed")
ret0, _ := ret[0].(logger.Logger)
return ret0
}
// Timed indicates an expected call of Timed.
func (mr *MockLoggerMockRecorder) Timed() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Timed", reflect.TypeOf((*MockLogger)(nil).Timed))
}
// Warnf mocks base method.
func (m *MockLogger) Warnf(arg0 string, arg1 ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warnf", varargs...)
}
// Warnf indicates an expected call of Warnf.
func (mr *MockLoggerMockRecorder) Warnf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...)
}
// With mocks base method.
func (m *MockLogger) With(arg0 logger.LogContext) logger.Logger {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "With", arg0)
ret0, _ := ret[0].(logger.Logger)
return ret0
}
// With indicates an expected call of With.
func (mr *MockLoggerMockRecorder) With(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockLogger)(nil).With), arg0)
}
// WithError mocks base method.
func (m *MockLogger) WithError(arg0 error) logger.Logger {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WithError", arg0)
ret0, _ := ret[0].(logger.Logger)
return ret0
}
// WithError indicates an expected call of WithError.
func (mr *MockLoggerMockRecorder) WithError(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithError", reflect.TypeOf((*MockLogger)(nil).WithError), arg0)
}

Просмотреть файл

@@ -0,0 +1,151 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster (interfaces: Poster)
// Package mock_bot is a generated GoMock package.
package mock_bot
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
model "github.com/mattermost/mattermost/server/public/model"
)
// MockPoster is a mock of Poster interface.
type MockPoster struct {
ctrl *gomock.Controller
recorder *MockPosterMockRecorder
}
// MockPosterMockRecorder is the mock recorder for MockPoster.
type MockPosterMockRecorder struct {
mock *MockPoster
}
// NewMockPoster creates a new mock instance.
func NewMockPoster(ctrl *gomock.Controller) *MockPoster {
mock := &MockPoster{ctrl: ctrl}
mock.recorder = &MockPosterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPoster) EXPECT() *MockPosterMockRecorder {
return m.recorder
}
// DM mocks base method.
func (m *MockPoster) DM(arg0, arg1 string, arg2 ...interface{}) (string, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DM", varargs...)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DM indicates an expected call of DM.
func (mr *MockPosterMockRecorder) DM(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DM", reflect.TypeOf((*MockPoster)(nil).DM), varargs...)
}
// DMWithAttachments mocks base method.
func (m *MockPoster) DMWithAttachments(arg0 string, arg1 ...*model.SlackAttachment) (string, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DMWithAttachments", varargs...)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DMWithAttachments indicates an expected call of DMWithAttachments.
func (mr *MockPosterMockRecorder) DMWithAttachments(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DMWithAttachments", reflect.TypeOf((*MockPoster)(nil).DMWithAttachments), varargs...)
}
// DeletePost mocks base method.
func (m *MockPoster) DeletePost(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeletePost", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeletePost indicates an expected call of DeletePost.
func (mr *MockPosterMockRecorder) DeletePost(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePost", reflect.TypeOf((*MockPoster)(nil).DeletePost), arg0)
}
// Ephemeral mocks base method.
func (m *MockPoster) Ephemeral(arg0, arg1, arg2 string, arg3 ...interface{}) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Ephemeral", varargs...)
}
// Ephemeral indicates an expected call of Ephemeral.
func (mr *MockPosterMockRecorder) Ephemeral(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ephemeral", reflect.TypeOf((*MockPoster)(nil).Ephemeral), varargs...)
}
// UpdatePost mocks base method.
func (m *MockPoster) UpdatePost(arg0 *model.Post) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePost", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdatePost indicates an expected call of UpdatePost.
func (mr *MockPosterMockRecorder) UpdatePost(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePost", reflect.TypeOf((*MockPoster)(nil).UpdatePost), arg0)
}
// UpdatePostByID mocks base method.
func (m *MockPoster) UpdatePostByID(arg0, arg1 string, arg2 ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UpdatePostByID", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// UpdatePostByID indicates an expected call of UpdatePostByID.
func (mr *MockPosterMockRecorder) UpdatePostByID(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePostByID", reflect.TypeOf((*MockPoster)(nil).UpdatePostByID), varargs...)
}
// UpdatePosterID mocks base method.
func (m *MockPoster) UpdatePosterID(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UpdatePosterID", arg0)
}
// UpdatePosterID indicates an expected call of UpdatePosterID.
func (mr *MockPosterMockRecorder) UpdatePosterID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePosterID", reflect.TypeOf((*MockPoster)(nil).UpdatePosterID), arg0)
}

Просмотреть файл

@@ -0,0 +1,76 @@
package poster
import (
"fmt"
"github.com/mattermost/mattermost/server/public/model"
)
type defaultPoster struct {
postAPI PostAPI
id string
}
// NewPoster creates a new default poster
func NewPoster(postAPI PostAPI, id string) Poster {
return &defaultPoster{
postAPI: postAPI,
id: id,
}
}
// DM posts a simple Direct Message to the specified user
func (p *defaultPoster) DM(mattermostUserID, format string, args ...interface{}) (string, error) {
post := &model.Post{
Message: fmt.Sprintf(format, args...),
}
err := p.postAPI.DM(p.id, mattermostUserID, post)
if err != nil {
return "", err
}
return post.Id, nil
}
// DMWithAttachments posts a Direct Message that contains Slack attachments.
// Often used to include post actions.
func (p *defaultPoster) DMWithAttachments(mattermostUserID string, attachments ...*model.SlackAttachment) (string, error) {
post := model.Post{}
model.ParseSlackAttachment(&post, attachments)
err := p.postAPI.DM(p.id, mattermostUserID, &post)
if err != nil {
return "", err
}
return post.Id, nil
}
// Ephemeral sends an ephemeral message to a user
func (p *defaultPoster) Ephemeral(userID, channelID, format string, args ...interface{}) {
post := &model.Post{
UserId: p.id,
ChannelId: channelID,
Message: fmt.Sprintf(format, args...),
}
p.postAPI.SendEphemeralPost(userID, post)
}
func (p *defaultPoster) UpdatePostByID(postID, format string, args ...interface{}) error {
post, err := p.postAPI.GetPost(postID)
if err != nil {
return err
}
post.Message = fmt.Sprintf(format, args...)
return p.UpdatePost(post)
}
func (p *defaultPoster) DeletePost(postID string) error {
return p.postAPI.DeletePost(postID)
}
func (p *defaultPoster) UpdatePost(post *model.Post) error {
return p.postAPI.UpdatePost(post)
}
func (p *defaultPoster) UpdatePosterID(id string) {
p.id = id
}

Просмотреть файл

@@ -0,0 +1,414 @@
package poster
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
"github.com/mattermost/mattermost/server/public/pluginapi"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster/mock_import"
)
const (
botID = "test-bot-user"
userID = "test-user-1"
dmChannelID = "dm-channel-id"
)
func TestInterface(t *testing.T) {
t.Run("Plugin API satisfy the interface", func(t *testing.T) {
api := &plugintest.API{}
driver := &plugintest.Driver{}
client := pluginapi.NewClient(api, driver)
_ = NewPoster(&client.Post, botID)
})
}
func TestDM(t *testing.T) {
format := "test format, string: %s int: %d value: %v"
args := []interface{}{"some string", 5, 8.423}
expectedMessage := "test format, string: some string int: 5 value: 8.423"
expectedPostID := "expected-post-id"
post := &model.Post{
Message: expectedMessage,
}
postWithID := model.Post{
Id: expectedPostID,
UserId: botID,
ChannelId: dmChannelID,
Message: expectedMessage,
}
mockError := errors.New("mock error")
t.Run("DM Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
//nolint:govet //copy lock, but only used in tests
postAPI.
EXPECT().
DM(botID, userID, post).
SetArg(2, postWithID).
Return(nil).
Times(1)
postID, err := poster.DM(userID, format, args...)
assert.Equal(t, expectedPostID, postID)
assert.NoError(t, err)
})
t.Run("DM error", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
DM(botID, userID, post).
Return(mockError).
Times(1)
_, err := poster.DM(userID, format, args...)
assert.Error(t, err)
})
}
func TestDMWithAttachments(t *testing.T) {
expectedPostID := "expected-post-id"
attachments := []*model.SlackAttachment{
{},
{},
}
post := &model.Post{}
model.ParseSlackAttachment(post, attachments)
postWithID := model.Post{
Id: expectedPostID,
UserId: botID,
ChannelId: dmChannelID,
Type: model.PostTypeSlackAttachment,
Props: model.StringInterface{
"attachments": attachments,
},
}
mockError := errors.New("mock error")
t.Run("DM Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
//nolint:govet //copy lock, but only used in tests
postAPI.
EXPECT().
DM(botID, userID, post).
SetArg(2, postWithID).
Return(nil).
Times(1)
postID, err := poster.DMWithAttachments(userID, attachments...)
assert.Equal(t, expectedPostID, postID)
assert.NoError(t, err)
})
t.Run("DM error", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
DM(botID, userID, post).
Return(mockError).
Times(1)
_, err := poster.DMWithAttachments(userID, attachments...)
assert.Error(t, err)
})
}
func TestEphemeral(t *testing.T) {
format := "test format, string: %s int: %d value: %v"
args := []interface{}{"some string", 5, 8.423}
expectedMessage := "test format, string: some string int: 5 value: 8.423"
channelID := "some-channel"
post := &model.Post{
UserId: botID,
ChannelId: channelID,
Message: expectedMessage,
}
expectedPostID := "some-post-ID"
postWithID := model.Post{
Id: expectedPostID,
UserId: botID,
ChannelId: channelID,
Message: expectedMessage,
}
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
//nolint:govet //copy lock, but only used in tests
postAPI.
EXPECT().
SendEphemeralPost(userID, post).
SetArg(1, postWithID).
Times(1)
poster.Ephemeral(userID, channelID, format, args...)
})
}
func TestUpdatePostByID(t *testing.T) {
format := "test format, string: %s int: %d value: %v"
args := []interface{}{"some string", 5, 8.423}
expectedMessage := "test format, string: some string int: 5 value: 8.423"
postID := "some-post-id"
originalPost := &model.Post{
Id: postID,
Message: "some message",
}
updatedPost := &model.Post{
Id: postID,
Message: expectedMessage,
}
mockError := errors.New("mock error")
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
GetPost(postID).
Return(originalPost, nil).
Times(1)
postAPI.
EXPECT().
UpdatePost(updatedPost).
Return(nil).
Times(1)
err := poster.UpdatePostByID(postID, format, args...)
assert.NoError(t, err)
})
t.Run("Error fetching", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
GetPost(postID).
Return(nil, mockError).
Times(1)
err := poster.UpdatePostByID(postID, format, args...)
assert.Error(t, err)
})
t.Run("Error updating", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
GetPost(postID).
Return(originalPost, nil).
Times(1)
postAPI.
EXPECT().
UpdatePost(updatedPost).
Return(mockError).
Times(1)
err := poster.UpdatePostByID(postID, format, args...)
assert.Error(t, err)
})
}
func TestDeletePost(t *testing.T) {
postID := "some-post-id"
mockError := errors.New("mock channel error")
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
DeletePost(postID).
Return(nil).
Times(1)
err := poster.DeletePost(postID)
assert.NoError(t, err)
})
t.Run("Error", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
DeletePost(postID).
Return(mockError).
Times(1)
err := poster.DeletePost(postID)
assert.Error(t, err)
})
}
func TestUpdatePost(t *testing.T) {
post := &model.Post{
Id: "some-post-id",
Message: "some message",
}
mockError := errors.New("mock channel error")
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
UpdatePost(post).
Return(nil).
Times(1)
err := poster.UpdatePost(post)
assert.NoError(t, err)
})
t.Run("Error", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
postAPI.
EXPECT().
UpdatePost(post).
Return(mockError).
Times(1)
err := poster.UpdatePost(post)
assert.Error(t, err)
})
}
func TestUpdatePosterID(t *testing.T) {
format := "test format, string: %s int: %d value: %v"
args := []interface{}{"some string", 5, 8.423}
expectedMessage := "test format, string: some string int: 5 value: 8.423"
expectedPostID := "expected-post-id"
post := &model.Post{
Message: expectedMessage,
}
postWithID := model.Post{
Id: expectedPostID,
UserId: botID,
ChannelId: dmChannelID,
Message: expectedMessage,
}
newBotID := "new-bot-id"
t.Run("Success", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
postAPI := mock_import.NewMockPostAPI(ctrl)
poster := NewPoster(postAPI, botID)
//nolint:govet //copy lock, but only used in tests
postAPI.
EXPECT().
DM(botID, userID, post).
SetArg(2, postWithID).
Return(nil).
Times(1)
_, _ = poster.DM(userID, format, args...)
poster.UpdatePosterID(newBotID)
//nolint:govet //copy lock, but only used in tests
postAPI.
EXPECT().
DM(newBotID, userID, post).
SetArg(2, postWithID).
Return(nil).
Times(1)
_, _ = poster.DM(userID, format, args...)
})
}

Просмотреть файл

@@ -0,0 +1,14 @@
package poster
import (
"github.com/mattermost/mattermost/server/public/model"
)
// PostAPI defines the portion of the Post Service used by the poster
type PostAPI interface {
DM(senderUserID, receiverUserID string, post *model.Post) error
GetPost(postID string) (*model.Post, error)
UpdatePost(post *model.Post) error
DeletePost(postID string) error
SendEphemeralPost(userID string, post *model.Post)
}

Просмотреть файл

@@ -0,0 +1,104 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster (interfaces: PostAPI)
// Package mock_import is a generated GoMock package.
package mock_import
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
model "github.com/mattermost/mattermost/server/public/model"
)
// MockPostAPI is a mock of PostAPI interface.
type MockPostAPI struct {
ctrl *gomock.Controller
recorder *MockPostAPIMockRecorder
}
// MockPostAPIMockRecorder is the mock recorder for MockPostAPI.
type MockPostAPIMockRecorder struct {
mock *MockPostAPI
}
// NewMockPostAPI creates a new mock instance.
func NewMockPostAPI(ctrl *gomock.Controller) *MockPostAPI {
mock := &MockPostAPI{ctrl: ctrl}
mock.recorder = &MockPostAPIMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPostAPI) EXPECT() *MockPostAPIMockRecorder {
return m.recorder
}
// DM mocks base method.
func (m *MockPostAPI) DM(arg0, arg1 string, arg2 *model.Post) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DM", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// DM indicates an expected call of DM.
func (mr *MockPostAPIMockRecorder) DM(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DM", reflect.TypeOf((*MockPostAPI)(nil).DM), arg0, arg1, arg2)
}
// DeletePost mocks base method.
func (m *MockPostAPI) DeletePost(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeletePost", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeletePost indicates an expected call of DeletePost.
func (mr *MockPostAPIMockRecorder) DeletePost(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePost", reflect.TypeOf((*MockPostAPI)(nil).DeletePost), arg0)
}
// GetPost mocks base method.
func (m *MockPostAPI) GetPost(arg0 string) (*model.Post, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPost", arg0)
ret0, _ := ret[0].(*model.Post)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPost indicates an expected call of GetPost.
func (mr *MockPostAPIMockRecorder) GetPost(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPost", reflect.TypeOf((*MockPostAPI)(nil).GetPost), arg0)
}
// SendEphemeralPost mocks base method.
func (m *MockPostAPI) SendEphemeralPost(arg0 string, arg1 *model.Post) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SendEphemeralPost", arg0, arg1)
}
// SendEphemeralPost indicates an expected call of SendEphemeralPost.
func (mr *MockPostAPIMockRecorder) SendEphemeralPost(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendEphemeralPost", reflect.TypeOf((*MockPostAPI)(nil).SendEphemeralPost), arg0, arg1)
}
// UpdatePost mocks base method.
func (m *MockPostAPI) UpdatePost(arg0 *model.Post) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePost", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdatePost indicates an expected call of UpdatePost.
func (mr *MockPostAPIMockRecorder) UpdatePost(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePost", reflect.TypeOf((*MockPostAPI)(nil).UpdatePost), arg0)
}

Просмотреть файл

@@ -0,0 +1,38 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.
package poster
import (
"github.com/mattermost/mattermost/server/public/model"
)
// Poster defines an entity that can post DMs and Ephemerals and update and delete those posts
type Poster interface {
DMer
// DMWithAttachments posts a Direct Message that contains Slack attachments.
// Often used to include post actions.
DMWithAttachments(mattermostUserID string, attachments ...*model.SlackAttachment) (string, error)
// Ephemeral sends an ephemeral message to a user
Ephemeral(mattermostUserID, channelID, format string, args ...interface{})
// UpdatePostByID updates the post with postID with the formatted message
UpdatePostByID(postID, format string, args ...interface{}) error
// DeletePost deletes a single post
DeletePost(postID string) error
// DMUpdatePost substitute one post with another
UpdatePost(post *model.Post) error
// UpdatePosterID updates the Mattermost User ID of the poster
UpdatePosterID(id string)
}
// DMer defines an entity that can send Direct Messages
type DMer interface {
// DM posts a simple Direct Message to the specified user
DM(mattermostUserID, format string, args ...interface{}) (string, error)
}

Просмотреть файл

@@ -0,0 +1,31 @@
package command
import (
"encoding/base64"
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
)
// PluginAPI is the plugin API interface required to manage slash commands.
type PluginAPI interface {
GetBundlePath() (string, error)
}
// GetIconData returns the base64 encoding of a icon for a given path.
// The data returned may be used for slash command autocomplete.
func GetIconData(api PluginAPI, iconPath string) (string, error) {
bundlePath, err := api.GetBundlePath()
if err != nil {
return "", errors.Wrap(err, "couldn't get bundle path")
}
icon, err := os.ReadFile(filepath.Join(bundlePath, iconPath))
if err != nil {
return "", errors.Wrap(err, "failed to open icon")
}
return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil
}

Просмотреть файл

@@ -0,0 +1,74 @@
package command
import (
"fmt"
"regexp"
"runtime/debug"
"strings"
"time"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
)
var versionRegexp = regexp.MustCompile(`/v\d$`)
func BuildInfoAutocomplete(cmd string) *model.AutocompleteData {
return model.NewAutocompleteData(cmd, "", "Display build info")
}
func BuildInfo(manifest model.Manifest) (string, error) {
info, ok := debug.ReadBuildInfo()
if !ok {
return "", errors.New("failed to read build info")
}
var (
revision string
revisionShort string
buildTime time.Time
dirty bool
)
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
revision = s.Value
revisionShort = revision[0:7]
case "vcs.time":
var err error
buildTime, err = time.Parse(time.RFC3339, s.Value)
if err != nil {
return "", err
}
case "vcs.modified":
if s.Value == "true" {
dirty = true
}
}
}
path := info.Main.Path
matches := versionRegexp.FindAllString(path, -1)
if len(matches) > 0 {
path = strings.TrimSuffix(path, matches[len(matches)-1])
}
dirtyText := ""
if dirty {
dirtyText = " (dirty)"
}
commit := fmt.Sprintf("[%s](https://%s/commit/%s)", revisionShort, path, revision)
return fmt.Sprintf("%s version: %s, %s%s, built %s with %s\n",
manifest.Name,
manifest.Version,
commit,
dirtyText,
buildTime.Format(time.RFC1123),
info.GoVersion),
nil
}

Просмотреть файл

@@ -0,0 +1,21 @@
package common
import (
"errors"
"time"
"github.com/mattermost/mattermost/server/public/pluginapi"
)
var ErrNotFound = errors.New("not found")
type KVStore interface {
Set(key string, value interface{}, options ...pluginapi.KVSetOption) (bool, error)
SetWithExpiry(key string, value interface{}, ttl time.Duration) error
CompareAndSet(key string, oldValue, value interface{}) (bool, error)
CompareAndDelete(key string, oldValue interface{}) (bool, error)
Get(key string, o interface{}) error
Delete(key string) error
DeleteAll() error
ListKeys(page, count int, options ...pluginapi.ListKeysOption) ([]string, error)
}

Просмотреть файл

@@ -0,0 +1,8 @@
package common
type LogAPI interface {
LogError(message string, keyValuePairs ...interface{})
LogWarn(message string, keyValuePairs ...interface{})
LogInfo(message string, keyValuePairs ...interface{})
LogDebug(message string, keyValuePairs ...interface{})
}

Просмотреть файл

@@ -0,0 +1,22 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.
package common
import (
"encoding/json"
"fmt"
)
func JSON(ref interface{}) string {
bb, _ := json.MarshalIndent(ref, "", " ")
return string(bb)
}
func CodeBlock(in string) string {
return fmt.Sprintf("\n```\n%s\n```\n", in)
}
func JSONBlock(ref interface{}) string {
return fmt.Sprintf("\n```json\n%s\n```\n", JSON(ref))
}

Просмотреть файл

@@ -0,0 +1,24 @@
package common
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost/server/public/model"
)
func SlackAttachmentError(w http.ResponseWriter, err error) {
response := model.PostActionIntegrationResponse{
EphemeralText: "Error:" + err.Error(),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response)
}
func DialogError(w http.ResponseWriter, err error) {
response := model.SubmitDialogResponse{
Error: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response)
}

Просмотреть файл

@@ -0,0 +1,28 @@
package common
import (
"net/url"
"strings"
"github.com/mattermost/mattermost/server/public/pluginapi"
)
// GetPluginURL returns a url like siteURL/plugins/pluginID based on the information from the client.
// If any error happens in the process, a empty string is returned.
func GetPluginURL(client *pluginapi.Client) string {
mattermostSiteURL := client.Configuration.GetConfig().ServiceSettings.SiteURL
if mattermostSiteURL == nil {
return ""
}
_, err := url.Parse(*mattermostSiteURL)
if err != nil {
return ""
}
manifest, err := client.System.GetManifest()
if err != nil {
return ""
}
pluginURLPath := "/plugins/" + manifest.Id
return strings.TrimRight(*mattermostSiteURL, "/") + pluginURLPath
}

Просмотреть файл

@@ -0,0 +1,254 @@
package flow
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi"
)
type Name string
const (
contextStepKey = "step"
contextButtonKey = "button"
)
type Flow struct {
UserID string
state *flowState
name Name
api *pluginapi.Client
pluginURL string
botUserID string
steps map[Name]Step
index []Name
done func(userID string, state State) error
debugLogState bool
}
// NewFlow creates a new flow using direct messages with the user.
//
// name must be a unique identifier for the flow within the plugin.
func NewFlow(name Name, api *pluginapi.Client, pluginURL, botUserID string) *Flow {
return &Flow{
name: name,
api: api,
pluginURL: pluginURL,
botUserID: botUserID,
steps: map[Name]Step{},
}
}
func (f *Flow) WithSteps(orderedSteps ...Step) *Flow {
if f.steps == nil {
f.steps = map[Name]Step{}
}
for _, step := range orderedSteps {
stepName := step.name
if _, ok := f.steps[stepName]; ok {
f.api.Log.Warn("ignored duplicate step name", "name", stepName, "flow", f.name)
continue
}
f.steps[stepName] = step
f.index = append(f.index, stepName)
}
return f
}
func (f *Flow) OnDone(done func(string, State) error) *Flow {
f.done = done
return f
}
func (f *Flow) InitHTTP(r *mux.Router) *Flow {
flowRouter := r.PathPrefix("/").Subrouter()
flowRouter.HandleFunc(namePath(f.name)+"/button", f.handleButtonHTTP).Methods(http.MethodPost)
flowRouter.HandleFunc(namePath(f.name)+"/dialog", f.handleDialogHTTP).Methods(http.MethodPost)
return f
}
func (f *Flow) WithDebugLog() *Flow {
f.debugLogState = true
return f
}
// ForUser creates a new flow using direct messages with the user.
func (f *Flow) ForUser(userID string) *Flow {
clone := *f
clone.UserID = userID
clone.state = nil
return &clone
}
func (f *Flow) GetCurrentStep() (Name, error) {
state, err := f.getState()
if err != nil {
// Don't return an error if no flow is running
if errors.Is(err, errStateNotFound) {
return "", nil
}
return "", err
}
return state.StepName, err
}
func (f *Flow) GetState() State {
state, _ := f.getState()
return state.AppState
}
func (f *Flow) Start(appState State) error {
if len(f.index) == 0 {
return errors.New("no steps")
}
err := f.storeState(flowState{
AppState: appState,
})
if err != nil {
return err
}
return f.Go(f.index[0])
}
func (f *Flow) Finish() error {
state, err := f.getState()
if err != nil {
return err
}
_ = f.removeState()
if f.done != nil {
err = f.done(f.UserID, state.AppState)
}
return err
}
func (f *Flow) Go(toName Name) error {
state, err := f.getState()
if err != nil {
return err
}
if toName == state.StepName {
// Stay at the current step, nothing to do
return nil
}
// Moving onto a different step, mark the current step as "Done"
if state.StepName != "" && !state.Done {
from, ok := f.steps[state.StepName]
if !ok {
return errors.Errorf("%s: step not found", toName)
}
var donePost *model.Post
donePost, err = from.done(f, 0)
if err != nil {
return err
}
if donePost != nil {
donePost.Id = state.PostID
err = f.api.Post.UpdatePost(donePost)
if err != nil {
return err
}
}
}
if toName == "" {
return f.Finish()
}
to, ok := f.steps[toName]
if !ok {
return errors.Errorf("%s: step not found", toName)
}
post, terminal, err := to.do(f)
if err != nil {
return err
}
f.processButtonPostActions(post)
if f.debugLogState {
data, _ := json.MarshalIndent(state, "", " ")
post.Message = fmt.Sprintf("State:\n```\n%s\n```\n", string(data))
}
err = f.api.Post.DM(f.botUserID, f.UserID, post)
if err != nil {
return err
}
if terminal {
return f.Finish()
}
state.StepName = toName
state.Done = false
state.PostID = post.Id
err = f.storeState(state)
if err != nil {
return err
}
if to.autoForward {
var nextName Name
if to.forwardTo != "" {
nextName = to.forwardTo
} else {
nextName = f.next(toName)
}
if nextName != "" {
return f.Go(nextName)
}
}
return nil
}
func (f Flow) next(fromName Name) Name {
for i, n := range f.index {
if fromName == n {
if i+1 < len(f.index) {
return f.index[i+1]
}
return ""
}
}
return ""
}
func namePath(name Name) string {
return "/" + url.PathEscape(strings.Trim(string(name), "/"))
}
func Goto(toName Name) func(*Flow) (Name, State, error) {
return func(_ *Flow) (Name, State, error) {
return toName, nil, nil
}
}
func DialogGoto(toName Name) func(*Flow, map[string]interface{}) (Name, State, map[string]string, error) {
return func(_ *Flow, submitted map[string]interface{}) (Name, State, map[string]string, error) {
stateUpdate := State{}
for k, v := range submitted {
stateUpdate[k] = fmt.Sprintf("%v", v)
}
return toName, stateUpdate, nil, nil
}
}

Просмотреть файл

@@ -0,0 +1,210 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.
package flow
import (
"encoding/json"
"fmt"
"net/http"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/common"
)
func (f *Flow) handleButtonHTTP(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
common.SlackAttachmentError(w, errors.New("Not authorized"))
return
}
f = f.ForUser(userID)
var request model.PostActionIntegrationRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
common.SlackAttachmentError(w, errors.New("invalid request"))
return
}
// selectedButton is 1-based
fromName, selectedButton, err := buttonContext(&request)
if err != nil {
common.SlackAttachmentError(w, err)
return
}
donePost, err := f.handleButton(fromName, selectedButton, request.TriggerId)
if err != nil {
common.SlackAttachmentError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(model.PostActionIntegrationResponse{
Update: donePost,
})
}
func (f *Flow) handleDialogHTTP(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
common.DialogError(w, errors.New("not authorized"))
return
}
f = f.ForUser(userID)
var request model.SubmitDialogRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
common.DialogError(w, errors.New("invalid request"))
return
}
fromName, selectedButton, err := dialogContext(&request)
if err != nil {
common.DialogError(w, errors.Wrap(err, "invalid request"))
return
}
// handleDialog updates the post
donePost, fieldErrors, err := f.handleDialog(fromName, selectedButton, request.Submission)
if err != nil || len(fieldErrors) != 0 {
w.Header().Set("Content-Type", "application/json")
resp := model.SubmitDialogResponse{
Errors: fieldErrors,
}
if err != nil {
resp.Error = err.Error()
}
_ = json.NewEncoder(w).Encode(resp)
return
}
err = f.api.Post.UpdatePost(donePost)
if err != nil {
common.DialogError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(model.SubmitDialogResponse{})
}
func (f *Flow) handleButton(fromName Name, selectedButton int, triggerID string) (*model.Post, error) {
post, _, err := f.handle(fromName, selectedButton, nil, triggerID, true)
return post, err
}
func (f *Flow) handleDialog(
fromName Name, selectedButton int, submission map[string]interface{},
) (
*model.Post, map[string]string, error,
) {
return f.handle(fromName, selectedButton, submission, "", false)
}
func (f *Flow) handle(
fromName Name, selectedButton int, submission map[string]interface{}, triggerID string, asButton bool,
) (
*model.Post, map[string]string, error,
) {
state, err := f.getState()
if err != nil {
return nil, nil, err
}
if state.StepName != fromName {
return nil, nil, errors.Errorf("click from an inactive step: %v", fromName)
}
from, ok := f.steps[fromName]
if !ok {
return nil, nil, errors.Errorf("step %q not found", fromName)
}
if selectedButton == 0 || selectedButton > len(from.buttons) {
return nil, nil, errors.Errorf("button number %v to high or too low, only %v buttons", selectedButton, len(from.buttons))
}
b := from.buttons[selectedButton-1]
var updated State
toName := fromName
var fieldErrors map[string]string
if asButton {
if b.OnClick != nil {
toName, updated, err = b.OnClick(f)
}
} else {
if b.OnDialogSubmit != nil {
toName, updated, fieldErrors, err = b.OnDialogSubmit(f, submission)
}
}
if err != nil || len(fieldErrors) > 0 {
return nil, fieldErrors, err
}
state.AppState = state.AppState.MergeWith(updated)
state.Done = true
err = f.storeState(state)
if err != nil {
return nil, nil, err
}
// Empty next step name in the response indicates advancing to the next step
// in the flow. To stay on the same step the handlers should return the step
// name.
if toName == "" {
toName = f.next(fromName)
}
if asButton && b.Dialog != nil {
if b.OnDialogSubmit == nil {
return nil, nil, errors.Errorf("no submit function for dialog, step: %s", fromName)
}
dialogRequest := model.OpenDialogRequest{
TriggerId: triggerID,
URL: f.pluginURL + namePath(f.name) + "/dialog",
Dialog: processDialog(b.Dialog, state.AppState),
}
dialogRequest.Dialog.State = fmt.Sprintf("%v,%v", fromName, selectedButton)
err = f.api.Frontend.OpenInteractiveDialog(dialogRequest)
if err != nil {
return nil, nil, err
}
}
if toName == fromName {
// Nothing else to do
return nil, nil, nil
}
donePost, err := from.done(f, selectedButton)
if err != nil {
return nil, nil, err
}
donePost.Id = state.PostID
f.processButtonPostActions(donePost)
err = f.Go(toName)
if err != nil {
f.api.Log.Warn("failed to advance flow to next step", "flow_name", f.name, "from", fromName, "to", toName, "error", err.Error())
}
// return the "done" post for the from step - leave updating up to the
// API-specific caller.
return donePost, nil, nil
}
func (f *Flow) processButtonPostActions(post *model.Post) {
attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment)
if !ok || len(attachments) == 0 {
return
}
sa := attachments[0]
for _, a := range sa.Actions {
if a.Integration == nil {
a.Integration = &model.PostActionIntegration{}
}
a.Integration.URL = f.pluginURL + namePath(f.name) + "/button"
}
}

Просмотреть файл

@@ -0,0 +1,146 @@
package flow
import (
"bytes"
"errors"
"text/template"
)
var errStateNotFound = errors.New("flow state not found")
// State is the "app"'s state
type State map[string]interface{}
func (s State) MergeWith(update State) State {
n := State{}
for k, v := range s {
n[k] = v
}
for k, v := range update {
n[k] = v
}
return n
}
// GetString return the value to a given key as a string.
// If the key is not found or isn't a string, an empty string is returned.
func (s State) GetString(key string) string {
vRaw, ok := s[key]
if ok {
v, ok := vRaw.(string)
if ok {
return v
}
}
return ""
}
// GetInt return the value to a given key as a int.
// If the key is not found or isn't an int, zero is returned.
func (s State) GetInt(key string) int {
vRaw, ok := s[key]
if ok {
v, ok := vRaw.(int)
if ok {
return v
}
}
return 0
}
// GetBool return the value to a given key as a bool.
// If the key is not found or isn't a bool, false is returned.
func (s State) GetBool(key string) bool {
vRaw, ok := s[key]
if ok {
v, ok := vRaw.(bool)
if ok {
return v
}
}
return false
}
// JSON-serializable flow state.
type flowState struct {
// The name of the step.
StepName Name
Done bool
// ID of the post produced by the step.
PostID string
// Application-level state.
AppState State
}
func (f *Flow) storeState(state flowState) error {
if f.UserID == "" {
return errors.New("no user specified")
}
// Set AppState to differentiate an existing flow
if state.AppState == nil {
state.AppState = State{}
}
ok, err := f.api.KV.Set(kvKey(f.UserID, f.name), state)
if err != nil {
return err
}
if !ok {
return errors.New("value not set without errors")
}
f.state = &state
return nil
}
func (f *Flow) getState() (flowState, error) {
if f.UserID == "" {
return flowState{}, errors.New("no user specified")
}
if f.state != nil {
return *f.state, nil
}
state := flowState{}
err := f.api.KV.Get(kvKey(f.UserID, f.name), &state)
if err != nil {
return flowState{}, err
}
if state.AppState == nil {
return flowState{}, errStateNotFound
}
f.state = &state
return state, err
}
func (f *Flow) removeState() error {
if f.UserID == "" {
return errors.New("no user specified")
}
f.state = nil
return f.api.KV.Delete(kvKey(f.UserID, f.name))
}
func kvKey(userID string, flowName Name) string {
return "_flow-" + userID + "-" + string(flowName)
}
func formatState(source string, state State) string {
t, err := template.New("message").Parse(source)
if err != nil {
return source + " ###ERROR: " + err.Error()
}
buf := bytes.NewBuffer(nil)
err = t.Execute(buf, state)
if err != nil {
return source + " ###ERROR: " + err.Error()
}
return buf.String()
}

Просмотреть файл

@@ -0,0 +1,269 @@
package flow
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
)
type Color string
const (
ColorDefault Color = "default"
ColorPrimary Color = "primary"
ColorSuccess Color = "success"
ColorGood Color = "good"
ColorWarning Color = "warning"
ColorDanger Color = "danger"
)
type Step struct {
name Name
template *model.SlackAttachment
forwardTo Name
autoForward bool
terminal bool
onRender func(f *Flow)
buttons []Button
}
type Button struct {
Name string
Disabled bool
Color Color
// OnClick is called when the button is clicked. It returns the next step's
// name and the state updates to apply.
//
// If Dialog is also specified, OnClick is executed first.
OnClick func(f *Flow) (Name, State, error)
// Dialog is the interactive dialog to display if the button is clicked
// (OnClick is executed first). OnDialogSubmit must be provided.
Dialog *model.Dialog
// Function that is called when the dialog box is submitted. It can return a
// general error, or field-specific errors. On success it returns the name
// of the next step, and the state updates to apply.
OnDialogSubmit func(f *Flow, submitted map[string]interface{}) (Name, State, map[string]string, error)
}
func NewStep(name Name) Step {
return Step{
name: name,
template: &model.SlackAttachment{},
}
}
func (s Step) WithButton(buttons ...Button) Step {
s.buttons = append(s.buttons, buttons...)
return s
}
func (s Step) Terminal() Step {
s.terminal = true
return s
}
func (s Step) OnRender(f func(*Flow)) Step {
s.onRender = f
return s
}
func (s Step) Next(name Name) Step {
s.forwardTo = name
s.autoForward = true
return s
}
func (s Step) WithImage(imageURL string) Step {
if u, err := url.Parse(imageURL); err == nil {
if u.Host != "" && (u.Scheme == "http" || u.Scheme == "https") {
s.template.ImageURL = imageURL
} else {
s.template.ImageURL = u.Path
}
}
return s
}
func (s Step) WithColor(color Color) Step {
s.template.Color = string(color)
return s
}
func (s Step) WithPretext(text string) Step {
s.template.Pretext = text
return s
}
func (s Step) WithField(title, value string) Step {
s.template.Fields = append(s.template.Fields, &model.SlackAttachmentField{
Title: title,
Value: value,
})
return s
}
func (s Step) WithTitle(text string) Step {
s.template.Title = text
return s
}
func (s Step) WithText(text string) Step {
s.template.Text = text
return s
}
func (s Step) do(f *Flow) (*model.Post, bool, error) {
if s.onRender != nil {
s.onRender(f)
}
return s.render(f, false, 0)
}
func (s Step) done(f *Flow, selectedButton int) (*model.Post, error) {
post, _, err := s.render(f, true, selectedButton)
return post, err
}
func (s Step) render(f *Flow, done bool, selectedButton int) (*model.Post, bool, error) {
sa := f.processAttachment(s.template)
post := model.Post{}
model.ParseSlackAttachment(&post, []*model.SlackAttachment{sa})
if s.terminal {
// Nothing else to do, do not display buttons on terminal posts.
return &post, true, nil
}
buttons := processButtons(s.buttons, f.state.AppState)
attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment)
if !ok || len(attachments) != 1 {
return nil, false, errors.New("expected 1 slack attachment")
}
var actions []*model.PostAction
if done {
if selectedButton > 0 {
action := renderButton(buttons[selectedButton-1], s.name, selectedButton, f.state.AppState)
action.Disabled = true
actions = append(actions, action)
}
} else {
for i, b := range buttons {
actions = append(actions, renderButton(b, s.name, i+1, f.state.AppState))
}
}
attachments[0].Actions = actions
return &post, false, nil
}
func (f *Flow) processAttachment(attachment *model.SlackAttachment) *model.SlackAttachment {
if attachment == nil {
return &model.SlackAttachment{Text: "ERROR"}
}
a := *attachment
a.Pretext = formatState(attachment.Pretext, f.state.AppState)
a.Title = formatState(attachment.Title, f.state.AppState)
a.Text = formatState(attachment.Text, f.state.AppState)
for _, field := range a.Fields {
field.Title = formatState(field.Title, f.state.AppState)
v := field.Value.(string)
if v != "" {
field.Value = formatState(v, f.state.AppState)
}
}
a.Fallback = fmt.Sprintf("%s: %s", a.Title, a.Text)
if attachment.ImageURL != "" {
if u, err := url.Parse(attachment.ImageURL); err == nil {
if u.Host != "" && (u.Scheme == "http" || u.Scheme == "https") {
a.ImageURL = attachment.ImageURL
} else {
a.ImageURL = f.pluginURL + "/" + strings.TrimPrefix(attachment.ImageURL, "/")
}
}
}
return &a
}
func processButtons(in []Button, state State) []Button {
var out []Button
for _, b := range in {
button := b
button.Name = formatState(b.Name, state)
out = append(out, button)
}
return out
}
func processDialog(in *model.Dialog, state State) model.Dialog {
d := *in
d.Title = formatState(d.Title, state)
d.IntroductionText = formatState(d.IntroductionText, state)
d.SubmitLabel = formatState(d.SubmitLabel, state)
for i := range d.Elements {
d.Elements[i].DisplayName = formatState(d.Elements[i].DisplayName, state)
d.Elements[i].Name = formatState(d.Elements[i].Name, state)
d.Elements[i].Default = formatState(d.Elements[i].Default, state)
d.Elements[i].Placeholder = formatState(d.Elements[i].Placeholder, state)
d.Elements[i].HelpText = formatState(d.Elements[i].HelpText, state)
}
return d
}
func renderButton(b Button, stepName Name, i int, state State) *model.PostAction {
return &model.PostAction{
Name: formatState(b.Name, state),
Disabled: b.Disabled,
Style: string(b.Color),
Integration: &model.PostActionIntegration{
Context: map[string]interface{}{
contextStepKey: string(stepName),
contextButtonKey: strconv.Itoa(i),
},
},
}
}
func buttonContext(request *model.PostActionIntegrationRequest) (Name, int, error) {
fromString, ok := request.Context[contextStepKey].(string)
if !ok {
return "", 0, errors.New("missing step name")
}
fromName := Name(fromString)
buttonStr, ok := request.Context[contextButtonKey].(string)
if !ok {
return "", 0, errors.New("missing button id")
}
buttonIndex, err := strconv.Atoi(buttonStr)
if err != nil {
return "", 0, errors.Wrap(err, "invalid button number")
}
return fromName, buttonIndex, nil
}
func dialogContext(request *model.SubmitDialogRequest) (Name, int, error) {
data := strings.Split(request.State, ",")
if len(data) != 2 {
return "", 0, errors.New("invalid request")
}
fromName := Name(data[0])
buttonIndex, err := strconv.Atoi(data[1])
if err != nil {
return "", 0, errors.Wrap(err, "malformed button number")
}
return fromName, buttonIndex, nil
}

Просмотреть файл

@@ -0,0 +1,77 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost-plugin-mscalendar/server/utils/oauther (interfaces: OAuther)
// Package mock_oauther is a generated GoMock package.
package mock_oauther
import (
gomock "github.com/golang/mock/gomock"
oauth2 "golang.org/x/oauth2"
reflect "reflect"
)
// MockOAuther is a mock of OAuther interface
type MockOAuther struct {
ctrl *gomock.Controller
recorder *MockOAutherMockRecorder
}
// MockOAutherMockRecorder is the mock recorder for MockOAuther
type MockOAutherMockRecorder struct {
mock *MockOAuther
}
// NewMockOAuther creates a new mock instance
func NewMockOAuther(ctrl *gomock.Controller) *MockOAuther {
mock := &MockOAuther{ctrl: ctrl}
mock.recorder = &MockOAutherMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOAuther) EXPECT() *MockOAutherMockRecorder {
return m.recorder
}
// Deauth mocks base method
func (m *MockOAuther) Deauth(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Deauth", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Deauth indicates an expected call of Deauth
func (mr *MockOAutherMockRecorder) Deauth(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deauth", reflect.TypeOf((*MockOAuther)(nil).Deauth), arg0)
}
// GetToken mocks base method
func (m *MockOAuther) GetToken(arg0 string) (*oauth2.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetToken", arg0)
ret0, _ := ret[0].(*oauth2.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetToken indicates an expected call of GetToken
func (mr *MockOAutherMockRecorder) GetToken(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToken", reflect.TypeOf((*MockOAuther)(nil).GetToken), arg0)
}
// GetURL mocks base method
func (m *MockOAuther) GetURL() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetURL")
ret0, _ := ret[0].(string)
return ret0
}
// GetURL indicates an expected call of GetURL
func (mr *MockOAutherMockRecorder) GetURL() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetURL", reflect.TypeOf((*MockOAuther)(nil).GetURL))
}

Просмотреть файл

@@ -0,0 +1,105 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/oauther (interfaces: OAuther)
// Package mock_oauther is a generated GoMock package.
package mock_oauther
import (
http "net/http"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
oauth2 "golang.org/x/oauth2"
)
// MockOAuther is a mock of OAuther interface.
type MockOAuther struct {
ctrl *gomock.Controller
recorder *MockOAutherMockRecorder
}
// MockOAutherMockRecorder is the mock recorder for MockOAuther.
type MockOAutherMockRecorder struct {
mock *MockOAuther
}
// NewMockOAuther creates a new mock instance.
func NewMockOAuther(ctrl *gomock.Controller) *MockOAuther {
mock := &MockOAuther{ctrl: ctrl}
mock.recorder = &MockOAutherMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOAuther) EXPECT() *MockOAutherMockRecorder {
return m.recorder
}
// AddPayload mocks base method.
func (m *MockOAuther) AddPayload(arg0 string, arg1 []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddPayload", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// AddPayload indicates an expected call of AddPayload.
func (mr *MockOAutherMockRecorder) AddPayload(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPayload", reflect.TypeOf((*MockOAuther)(nil).AddPayload), arg0, arg1)
}
// Deauthorize mocks base method.
func (m *MockOAuther) Deauthorize(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Deauthorize", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Deauthorize indicates an expected call of Deauthorize.
func (mr *MockOAutherMockRecorder) Deauthorize(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deauthorize", reflect.TypeOf((*MockOAuther)(nil).Deauthorize), arg0)
}
// GetConnectURL mocks base method.
func (m *MockOAuther) GetConnectURL() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetConnectURL")
ret0, _ := ret[0].(string)
return ret0
}
// GetConnectURL indicates an expected call of GetConnectURL.
func (mr *MockOAutherMockRecorder) GetConnectURL() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConnectURL", reflect.TypeOf((*MockOAuther)(nil).GetConnectURL))
}
// GetToken mocks base method.
func (m *MockOAuther) GetToken(arg0 string) (*oauth2.Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetToken", arg0)
ret0, _ := ret[0].(*oauth2.Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetToken indicates an expected call of GetToken.
func (mr *MockOAutherMockRecorder) GetToken(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToken", reflect.TypeOf((*MockOAuther)(nil).GetToken), arg0)
}
// ServeHTTP mocks base method.
func (m *MockOAuther) ServeHTTP(arg0 http.ResponseWriter, arg1 *http.Request) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "ServeHTTP", arg0, arg1)
}
// ServeHTTP indicates an expected call of ServeHTTP.
func (mr *MockOAutherMockRecorder) ServeHTTP(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServeHTTP", reflect.TypeOf((*MockOAuther)(nil).ServeHTTP), arg0, arg1)
}

Просмотреть файл

@@ -0,0 +1,191 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.
package oauther
import (
"net/http"
"time"
"golang.org/x/oauth2"
"github.com/mattermost/mattermost/server/public/pluginapi"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/common"
)
const (
// DefaultStorePrefix is the prefix used when storing information in the KVStore by default.
DefaultStorePrefix = "oauth_"
// DefaultOAuthURL is the URL the OAuther will use to register its endpoints by default.
DefaultOAuthURL = "/oauth2"
// DefaultConnectedString is the string shown to the user when the oauth flow is completed by default.
DefaultConnectedString = "Successfully connected. Please close this window."
// DefaultOAuth2StateTimeToLive is the duration the states from the OAuth flow will live in the KVStore by default.
DefaultOAuth2StateTimeToLive = 5 * time.Minute
// DefaultPayloadTimeToLive is the duration the user payload will live in the KVStore by default.
DefaultPayloadTimeToLive = 10 * time.Minute
)
const (
connectURL = "/connect"
completeURL = "/complete"
)
// OAuther defines an object able to perform the OAuth flow.
type OAuther interface {
// GetToken returns the oauth token for userID, or error if it does not exist or there is any store error.
GetToken(userID string) (*oauth2.Token, error)
// GetConnectURL returns the URL to reach in order to start the OAuth flow.
GetConnectURL() string
// Deauthorize removes the token for userID. Return error if there is any store error.
Deauthorize(userID string) error
// ServeHTTP implements http.Handler
ServeHTTP(w http.ResponseWriter, r *http.Request)
// AddPayload stores some information to be returned after the flow is over
AddPayload(userID string, payload []byte) error
}
type oAuther struct {
pluginURL string
config oauth2.Config
onConnect func(userID string, token oauth2.Token, payload []byte)
store common.KVStore
logger logger.Logger
storePrefix string
oAuthURL string
connectedString string
oAuth2StateTimeToLive time.Duration
payloadTimeToLive time.Duration
}
/*
New creates a new OAuther.
- pluginURL: The base URL for the plugin (e.g. https://www.instance.com/plugins/pluginid).
- oAuthConfig: The configuration of the Authorization flow to perform.
- onConnect: What to do when the Authorization process is complete.
- store: A KVStore to store the data of the OAuther.
- l Logger: A logger to log errors during authorization.
- options: Optional options for the OAuther. Available options are StorePrefix, OAuthURL, ConnectedString and OAuth2StateTimeToLive.
*/
func New(
pluginURL string,
oAuthConfig oauth2.Config,
onConnect func(userID string, token oauth2.Token, payload []byte),
store common.KVStore,
l logger.Logger,
options ...Option,
) OAuther {
o := &oAuther{
pluginURL: pluginURL,
config: oAuthConfig,
onConnect: onConnect,
store: store,
logger: l,
storePrefix: DefaultStorePrefix,
oAuthURL: DefaultOAuthURL,
connectedString: DefaultConnectedString,
oAuth2StateTimeToLive: DefaultOAuth2StateTimeToLive,
payloadTimeToLive: DefaultPayloadTimeToLive,
}
for _, option := range options {
option(o)
}
o.config.RedirectURL = o.pluginURL + o.oAuthURL + "/complete"
return o
}
/*
NewFromClient creates a new OAuther from the plugin api client.
- pluginapi: A plugin api client.
- pluginID: The plugin ID.
- oAuthConfig: The configuration of the Authorization flow to perform.
- onConnect: What to do when the Authorization process is complete.
- l Logger: A logger to log errors during authorization.
- options: Optional options for the OAuther. Available options are StorePrefix, OAuthURL, ConnectedString and OAuth2StateTimeToLive.
*/
func NewFromClient(
client *pluginapi.Client,
oAuthConfig oauth2.Config,
onConnect func(userID string, token oauth2.Token, payload []byte),
l logger.Logger,
options ...Option,
) OAuther {
return New(
common.GetPluginURL(client),
oAuthConfig,
onConnect,
&client.KV,
l,
options...,
)
}
func (o *oAuther) GetConnectURL() string {
return o.pluginURL + o.oAuthURL + "/connect"
}
func (o *oAuther) GetToken(userID string) (*oauth2.Token, error) {
var token *oauth2.Token
err := o.store.Get(o.getTokenKey(userID), &token)
if err != nil {
return nil, err
}
return token, nil
}
func (o *oAuther) getTokenKey(userID string) string {
return o.storePrefix + "token_" + userID
}
func (o *oAuther) getStateKey(userID string) string {
return o.storePrefix + "state_" + userID
}
func (o *oAuther) getPayloadKey(userID string) string {
return o.storePrefix + "payload_" + userID
}
func (o *oAuther) Deauthorize(userID string) error {
err := o.store.Delete(o.getTokenKey(userID))
if err != nil {
return err
}
return nil
}
func (o *oAuther) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case o.oAuthURL + connectURL:
o.oauth2Connect(w, r)
case o.oAuthURL + completeURL:
o.oauth2Complete(w, r)
default:
http.NotFound(w, r)
}
}
func (o *oAuther) AddPayload(userID string, payload []byte) error {
_, err := o.store.Set(o.getPayloadKey(userID), payload, pluginapi.SetExpiry(o.payloadTimeToLive))
if err != nil {
return err
}
return nil
}

Просмотреть файл

@@ -0,0 +1,105 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.
package oauther
import (
"context"
"fmt"
"net/http"
"strings"
)
func (o *oAuther) oauth2Complete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
authedUserID := r.Header.Get("Mattermost-User-ID")
if authedUserID == "" {
o.logger.Debugf("oauth2Complete: reached by non authed user")
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
code := r.URL.Query().Get("code")
if code == "" {
o.logger.Debugf("oauth2Complete: reached with no code")
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
state := r.URL.Query().Get("state")
var storedState string
err := o.store.Get(o.getStateKey(authedUserID), &storedState)
if err != nil {
o.logger.Warnf("oauth2Complete: cannot get state, err=%s", err.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if storedState != state {
o.logger.Debugf("oauth2Complete: state mismatch")
o.logger.Debugf("received state '%s'; expected state '%s%", state, storedState)
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
userID := strings.Split(state, "_")[1]
if userID != authedUserID {
o.logger.Debugf("oauth2Complete: authed user mismatch")
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
ctx := context.Background()
token, err := o.config.Exchange(ctx, code)
if err != nil {
o.logger.Warnf("oauth2Complete: could not generate token, err=%s", err.Error())
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
var payload []byte
err = o.store.Get(o.getPayloadKey(userID), &payload)
if err != nil {
o.logger.Errorf("oauth2Complete: could not fetch payload, err=&s", err.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
ok, err := o.store.Set(o.getTokenKey(userID), token)
if err != nil {
o.logger.Errorf("oauth2Complete: cannot store the token, err=%s", err.Error())
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if !ok {
o.logger.Errorf("oauth2Complete: cannot store token without error")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<script>
window.close();
</script>
</head>
<body>
<p>%s</p>
</body>
</html>
`, o.connectedString)
w.Header().Set("Content-Type", "text/html")
_, err = w.Write([]byte(html))
if err != nil {
o.logger.Errorf("oauth2Complete: error writing response, err=%s", err.Error())
}
if o.onConnect != nil {
o.onConnect(userID, *token, payload)
}
}

Просмотреть файл

@@ -0,0 +1,38 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.
package oauther
import (
"fmt"
"net/http"
"golang.org/x/oauth2"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi"
)
func (o *oAuther) oauth2Connect(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
o.logger.Debugf("oauth2Connect: reached by non authed user")
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
state := fmt.Sprintf("%v_%v", model.NewId()[0:15], userID)
_, err := o.store.Set(o.getStateKey(userID), state, pluginapi.SetExpiry(o.oAuth2StateTimeToLive))
if err != nil {
o.logger.Errorf("oauth2Connect: failed to store state, err=%s", err.Error())
http.Error(w, "failed to store token state", http.StatusInternalServerError)
return
}
redirectURL := o.config.AuthCodeURL(state, oauth2.AccessTypeOffline)
http.Redirect(w, r, redirectURL, http.StatusFound)
}

Просмотреть файл

@@ -0,0 +1,47 @@
package oauther
import "time"
// Option defines each option that can be passed in the creation of the OAuther.
// Options functions available are OAuthURL, StorePrefix, ConnectedString and OAuth2StateTimeToLive and PayloadTimeToLive.
type Option func(*oAuther)
// OAuthURL defines the URL the OAuther will use to register its endpoints.
// Defaults to "/oauth2".
func OAuthURL(url string) Option {
return func(o *oAuther) {
o.oAuthURL = url
}
}
// StorePrefix defines the prefix the OAuther will use to store information in the KVStore.
// Defaults to "oauth_".
func StorePrefix(prefix string) Option {
return func(o *oAuther) {
o.storePrefix = prefix
}
}
// ConnectedString defines the string shown to the user when the oauth flow is completed.
// Defaults to "Successfully connected. Please close this window.".
func ConnectedString(text string) Option {
return func(o *oAuther) {
o.connectedString = text
}
}
// OAuth2StateTimeToLive is the duration the states from the OAuth flow will live in the KVStore.
// Defaults to 5 minutes.
func OAuth2StateTimeToLive(ttl time.Duration) Option {
return func(o *oAuther) {
o.oAuth2StateTimeToLive = ttl
}
}
// PayloadTimeToLive is the duration the payload from the OAuth flow will live in the KVStore.
// Defaults to 10 minutes.
func PayloadTimeToLive(ttl time.Duration) Option {
return func(o *oAuther) {
o.payloadTimeToLive = ttl
}
}

Просмотреть файл

@@ -0,0 +1,70 @@
package panel
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/common"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel/settings"
)
type handler struct {
panel Panel
}
func Init(r *mux.Router, panel Panel) {
sh := &handler{
panel: panel,
}
panelRouter := r.PathPrefix("/").Subrouter()
panelRouter.HandleFunc(panel.URL(), sh.handleAction).Methods(http.MethodPost)
}
func (sh *handler) handleAction(w http.ResponseWriter, r *http.Request) {
mattermostUserID := r.Header.Get("Mattermost-User-ID")
if mattermostUserID == "" {
common.SlackAttachmentError(w, errors.New("Not authorized"))
return
}
var request model.PostActionIntegrationRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
common.SlackAttachmentError(w, errors.New("invalid request"))
return
}
id, ok := request.Context[settings.ContextIDKey]
if !ok {
common.SlackAttachmentError(w, errors.New("missing setting id"))
return
}
value, ok := request.Context[settings.ContextButtonValueKey]
if !ok {
value, ok = request.Context[settings.ContextOptionValueKey]
if !ok {
common.SlackAttachmentError(w, errors.New("valid key not found"))
return
}
}
idString := id.(string)
err := sh.panel.Set(mattermostUserID, idString, value)
if err != nil {
common.SlackAttachmentError(w, errors.Wrap(err, "cannot save setting"))
return
}
response := model.PostActionIntegrationResponse{}
post, err := sh.panel.ToPost(mattermostUserID)
if err == nil {
response.Update = post
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response)
}

Просмотреть файл

@@ -0,0 +1,118 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel (interfaces: Panel)
// Package mock_panel is a generated GoMock package.
package mock_panel
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
model "github.com/mattermost/mattermost/server/public/model"
)
// MockPanel is a mock of Panel interface.
type MockPanel struct {
ctrl *gomock.Controller
recorder *MockPanelMockRecorder
}
// MockPanelMockRecorder is the mock recorder for MockPanel.
type MockPanelMockRecorder struct {
mock *MockPanel
}
// NewMockPanel creates a new mock instance.
func NewMockPanel(ctrl *gomock.Controller) *MockPanel {
mock := &MockPanel{ctrl: ctrl}
mock.recorder = &MockPanelMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPanel) EXPECT() *MockPanelMockRecorder {
return m.recorder
}
// Clear mocks base method.
func (m *MockPanel) Clear(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Clear", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Clear indicates an expected call of Clear.
func (mr *MockPanelMockRecorder) Clear(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clear", reflect.TypeOf((*MockPanel)(nil).Clear), arg0)
}
// GetSettingIDs mocks base method.
func (m *MockPanel) GetSettingIDs() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSettingIDs")
ret0, _ := ret[0].([]string)
return ret0
}
// GetSettingIDs indicates an expected call of GetSettingIDs.
func (mr *MockPanelMockRecorder) GetSettingIDs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSettingIDs", reflect.TypeOf((*MockPanel)(nil).GetSettingIDs))
}
// Print mocks base method.
func (m *MockPanel) Print(arg0 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Print", arg0)
}
// Print indicates an expected call of Print.
func (mr *MockPanelMockRecorder) Print(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Print", reflect.TypeOf((*MockPanel)(nil).Print), arg0)
}
// Set mocks base method.
func (m *MockPanel) Set(arg0, arg1 string, arg2 interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockPanelMockRecorder) Set(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockPanel)(nil).Set), arg0, arg1, arg2)
}
// ToPost mocks base method.
func (m *MockPanel) ToPost(arg0 string) (*model.Post, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToPost", arg0)
ret0, _ := ret[0].(*model.Post)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ToPost indicates an expected call of ToPost.
func (mr *MockPanelMockRecorder) ToPost(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToPost", reflect.TypeOf((*MockPanel)(nil).ToPost), arg0)
}
// URL mocks base method.
func (m *MockPanel) URL() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "URL")
ret0, _ := ret[0].(string)
return ret0
}
// URL indicates an expected call of URL.
func (mr *MockPanelMockRecorder) URL() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "URL", reflect.TypeOf((*MockPanel)(nil).URL))
}

Просмотреть файл

@@ -0,0 +1,77 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel (interfaces: Store)
// Package mock_panel is a generated GoMock package.
package mock_panel
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockStore is a mock of Store interface.
type MockStore struct {
ctrl *gomock.Controller
recorder *MockStoreMockRecorder
}
// MockStoreMockRecorder is the mock recorder for MockStore.
type MockStoreMockRecorder struct {
mock *MockStore
}
// NewMockStore creates a new mock instance.
func NewMockStore(ctrl *gomock.Controller) *MockStore {
mock := &MockStore{ctrl: ctrl}
mock.recorder = &MockStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStore) EXPECT() *MockStoreMockRecorder {
return m.recorder
}
// DeletePanelPostID mocks base method.
func (m *MockStore) DeletePanelPostID(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeletePanelPostID", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DeletePanelPostID indicates an expected call of DeletePanelPostID.
func (mr *MockStoreMockRecorder) DeletePanelPostID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePanelPostID", reflect.TypeOf((*MockStore)(nil).DeletePanelPostID), arg0)
}
// GetPanelPostID mocks base method.
func (m *MockStore) GetPanelPostID(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPanelPostID", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPanelPostID indicates an expected call of GetPanelPostID.
func (mr *MockStoreMockRecorder) GetPanelPostID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPanelPostID", reflect.TypeOf((*MockStore)(nil).GetPanelPostID), arg0)
}
// SetPanelPostID mocks base method.
func (m *MockStore) SetPanelPostID(arg0, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetPanelPostID", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SetPanelPostID indicates an expected call of SetPanelPostID.
func (mr *MockStoreMockRecorder) SetPanelPostID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPanelPostID", reflect.TypeOf((*MockStore)(nil).SetPanelPostID), arg0, arg1)
}

Просмотреть файл

@@ -0,0 +1,149 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel/settings (interfaces: Setting)
// Package mock_panel is a generated GoMock package.
package mock_panel
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
model "github.com/mattermost/mattermost/server/public/model"
)
// MockSetting is a mock of Setting interface.
type MockSetting struct {
ctrl *gomock.Controller
recorder *MockSettingMockRecorder
}
// MockSettingMockRecorder is the mock recorder for MockSetting.
type MockSettingMockRecorder struct {
mock *MockSetting
}
// NewMockSetting creates a new mock instance.
func NewMockSetting(ctrl *gomock.Controller) *MockSetting {
mock := &MockSetting{ctrl: ctrl}
mock.recorder = &MockSettingMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSetting) EXPECT() *MockSettingMockRecorder {
return m.recorder
}
// Get mocks base method.
func (m *MockSetting) Get(arg0 string) (interface{}, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(interface{})
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockSettingMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSetting)(nil).Get), arg0)
}
// GetDependency mocks base method.
func (m *MockSetting) GetDependency() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDependency")
ret0, _ := ret[0].(string)
return ret0
}
// GetDependency indicates an expected call of GetDependency.
func (mr *MockSettingMockRecorder) GetDependency() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDependency", reflect.TypeOf((*MockSetting)(nil).GetDependency))
}
// GetDescription mocks base method.
func (m *MockSetting) GetDescription() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDescription")
ret0, _ := ret[0].(string)
return ret0
}
// GetDescription indicates an expected call of GetDescription.
func (mr *MockSettingMockRecorder) GetDescription() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDescription", reflect.TypeOf((*MockSetting)(nil).GetDescription))
}
// GetID mocks base method.
func (m *MockSetting) GetID() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetID")
ret0, _ := ret[0].(string)
return ret0
}
// GetID indicates an expected call of GetID.
func (mr *MockSettingMockRecorder) GetID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetID", reflect.TypeOf((*MockSetting)(nil).GetID))
}
// GetSlackAttachments mocks base method.
func (m *MockSetting) GetSlackAttachments(arg0, arg1 string, arg2 bool) (*model.SlackAttachment, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSlackAttachments", arg0, arg1, arg2)
ret0, _ := ret[0].(*model.SlackAttachment)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSlackAttachments indicates an expected call of GetSlackAttachments.
func (mr *MockSettingMockRecorder) GetSlackAttachments(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSlackAttachments", reflect.TypeOf((*MockSetting)(nil).GetSlackAttachments), arg0, arg1, arg2)
}
// GetTitle mocks base method.
func (m *MockSetting) GetTitle() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTitle")
ret0, _ := ret[0].(string)
return ret0
}
// GetTitle indicates an expected call of GetTitle.
func (mr *MockSettingMockRecorder) GetTitle() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTitle", reflect.TypeOf((*MockSetting)(nil).GetTitle))
}
// IsDisabled mocks base method.
func (m *MockSetting) IsDisabled(arg0 interface{}) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsDisabled", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsDisabled indicates an expected call of IsDisabled.
func (mr *MockSettingMockRecorder) IsDisabled(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDisabled", reflect.TypeOf((*MockSetting)(nil).IsDisabled), arg0)
}
// Set mocks base method.
func (m *MockSetting) Set(arg0 string, arg1 interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockSettingMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockSetting)(nil).Set), arg0, arg1)
}

Просмотреть файл

@@ -0,0 +1,171 @@
package panel
import (
"errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/poster"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/common"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/panel/settings"
)
type Panel interface {
Set(userID, settingID string, value interface{}) error
Print(userID string)
ToPost(userID string) (*model.Post, error)
Clear(userID string) error
URL() string
GetSettingIDs() []string
}
type panel struct {
settings map[string]settings.Setting
settingKeys []string
poster poster.Poster
logger logger.Logger
store Store
settingHandler string
pluginURL string
}
func NewSettingsPanel(
settingList []settings.Setting,
p poster.Poster,
l logger.Logger,
store Store,
settingHandler,
pluginURL string,
) Panel {
settingsMap := make(map[string]settings.Setting)
settingKeys := []string{}
for _, s := range settingList {
settingsMap[s.GetID()] = s
settingKeys = append(settingKeys, s.GetID())
}
panel := &panel{
settings: settingsMap,
settingKeys: settingKeys,
poster: p,
logger: l,
store: store,
settingHandler: settingHandler,
pluginURL: pluginURL,
}
return panel
}
func (p *panel) Set(userID, settingID string, value interface{}) error {
s, ok := p.settings[settingID]
if !ok {
return errors.New("cannot find setting " + settingID)
}
err := s.Set(userID, value)
if err != nil {
return err
}
return nil
}
func (p *panel) GetSettingIDs() []string {
return p.settingKeys
}
func (p *panel) URL() string {
return p.settingHandler
}
func (p *panel) Print(userID string) {
err := p.cleanPreviousSettingsPosts(userID)
if err != nil {
p.logger.Errorf("could not clean previous setting post, " + err.Error())
}
sas := []*model.SlackAttachment{}
for _, key := range p.settingKeys {
s := p.settings[key]
sa, loopErr := s.GetSlackAttachments(userID, p.pluginURL+p.settingHandler, p.isSettingDisabled(userID, s))
if loopErr != nil {
p.logger.Errorf("error creating the slack attachment, err=" + loopErr.Error())
continue
}
sas = append(sas, sa)
}
postID, err := p.poster.DMWithAttachments(userID, sas...)
if err != nil {
p.logger.Errorf("error creating the message, err=", err.Error())
return
}
err = p.store.SetPanelPostID(userID, postID)
if err != nil {
p.logger.Errorf("could not set the post IDs, err=", err.Error())
}
}
func (p *panel) ToPost(userID string) (*model.Post, error) {
post := &model.Post{}
sas := []*model.SlackAttachment{}
for _, key := range p.settingKeys {
s := p.settings[key]
sa, err := s.GetSlackAttachments(userID, p.pluginURL+p.settingHandler, p.isSettingDisabled(userID, s))
if err != nil {
p.logger.Errorf("error creating the slack attachment for setting %s, err=%s", s.GetID(), err.Error())
continue
}
sas = append(sas, sa)
}
model.ParseSlackAttachment(post, sas)
return post, nil
}
func (p *panel) cleanPreviousSettingsPosts(userID string) error {
postID, err := p.store.GetPanelPostID(userID)
if err == common.ErrNotFound {
return nil
}
if err != nil {
return err
}
err = p.poster.DeletePost(postID)
if err != nil {
p.logger.Errorf("could not delete setting post, %s", err)
}
err = p.store.DeletePanelPostID(userID)
if err != nil {
return err
}
return nil
}
func (p *panel) Clear(userID string) error {
return p.cleanPreviousSettingsPosts(userID)
}
func (p *panel) isSettingDisabled(userID string, s settings.Setting) bool {
dependencyID := s.GetDependency()
if dependencyID == "" {
return false
}
dependency, ok := p.settings[dependencyID]
if !ok {
p.logger.Errorf("settings dependency %s not found", dependencyID)
return false
}
value, err := dependency.Get(userID)
if err != nil {
p.logger.Errorf("cannot get dependency %s value", dependencyID)
return false
}
return s.IsDisabled(value)
}

Просмотреть файл

@@ -0,0 +1,28 @@
package settings
type baseSetting struct {
title string
description string
id string
dependsOn string
}
func (s *baseSetting) GetID() string {
return s.id
}
func (s *baseSetting) GetTitle() string {
return s.title
}
func (s *baseSetting) GetDescription() string {
return s.description
}
func (s *baseSetting) GetDependency() string {
return s.dependsOn
}
func (s *baseSetting) IsDisabled(foreignValue interface{}) bool {
return false
}

Просмотреть файл

@@ -0,0 +1,114 @@
package settings
import (
"errors"
"fmt"
"github.com/mattermost/mattermost/server/public/model"
)
type boolSetting struct {
baseSetting
store SettingStore
}
// NewBoolSetting creates a new setting input for boolean values
func NewBoolSetting(id, title, description, dependsOn string, store SettingStore) Setting {
return &boolSetting{
baseSetting: baseSetting{
title: title,
description: description,
id: id,
dependsOn: dependsOn,
},
store: store,
}
}
func (s *boolSetting) Set(userID string, value interface{}) error {
boolValue := false
if value == TrueString {
boolValue = true
}
err := s.store.SetSetting(userID, s.id, boolValue)
if err != nil {
return err
}
return nil
}
func (s *boolSetting) Get(userID string) (interface{}, error) {
value, err := s.store.GetSetting(userID, s.id)
if err != nil {
return "", err
}
boolValue, ok := value.(bool)
if !ok {
return "", errors.New("current value is not a bool")
}
stringValue := FalseString
if boolValue {
stringValue = TrueString
}
return stringValue, nil
}
func (s *boolSetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) {
title := fmt.Sprintf("Setting: %s", s.title)
currentValueMessage := DisabledString
actions := []*model.PostAction{}
if !disabled {
currentValue, err := s.Get(userID)
if err != nil {
return nil, err
}
currentTextValue := "No"
if currentValue == TrueString {
currentTextValue = "Yes"
}
currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue)
actionTrue := model.PostAction{
Name: "Yes",
Integration: &model.PostActionIntegration{
URL: settingHandler,
Context: map[string]interface{}{
ContextIDKey: s.id,
ContextButtonValueKey: TrueString,
},
},
}
actionFalse := model.PostAction{
Name: "No",
Integration: &model.PostActionIntegration{
URL: settingHandler,
Context: map[string]interface{}{
ContextIDKey: s.id,
ContextButtonValueKey: FalseString,
},
},
}
actions = []*model.PostAction{&actionTrue, &actionFalse}
}
text := fmt.Sprintf("%s\n%s", s.description, currentValueMessage)
sa := model.SlackAttachment{
Title: title,
Text: text,
Fallback: fmt.Sprintf("%s: %s", title, text),
Actions: actions,
}
return &sa, nil
}
func (s *boolSetting) IsDisabled(foreignValue interface{}) bool {
return foreignValue == FalseString
}

Просмотреть файл

@@ -0,0 +1,41 @@
package settings
import (
"fmt"
"github.com/mattermost/mattermost/server/public/model"
)
type emptySetting struct {
baseSetting
}
// NewEmptySetting creates a new panel value with no setting attached
func NewEmptySetting(id, title, description string) Setting {
return &emptySetting{
baseSetting: baseSetting{
id: id,
title: title,
description: description,
},
}
}
func (s *emptySetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) {
title := fmt.Sprintf("Setting: %s", s.title)
sa := model.SlackAttachment{
Title: title,
Text: s.description,
Fallback: fmt.Sprintf("%s: %s", title, s.description),
}
return &sa, nil
}
func (s *emptySetting) Get(userID string) (interface{}, error) {
return nil, nil
}
func (s *emptySetting) Set(userID string, value interface{}) error {
return nil
}

Просмотреть файл

@@ -0,0 +1,91 @@
package settings
import (
"errors"
"fmt"
"github.com/mattermost/mattermost/server/public/model"
)
type optionSetting struct {
baseSetting
options []string
store SettingStore
}
// NewOptionSetting creates a new setting input to select from a dropdown
func NewOptionSetting(id, title, description, dependsOn string, options []string, store SettingStore) Setting {
return &optionSetting{
baseSetting: baseSetting{
title: title,
description: description,
id: id,
dependsOn: dependsOn,
},
options: options,
store: store,
}
}
func (s *optionSetting) Set(userID string, value interface{}) error {
err := s.store.SetSetting(userID, s.id, value)
if err != nil {
return err
}
return nil
}
func (s *optionSetting) Get(userID string) (interface{}, error) {
value, err := s.store.GetSetting(userID, s.id)
if err != nil {
return "", err
}
valueString, ok := value.(string)
if !ok {
return "", errors.New("current value is not a string")
}
return valueString, nil
}
func (s *optionSetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) {
title := fmt.Sprintf("Setting: %s", s.title)
currentValueMessage := DisabledString
actions := []*model.PostAction{}
if !disabled {
currentTextValue, err := s.Get(userID)
if err != nil {
return nil, err
}
currentValueMessage = fmt.Sprintf("Current value: %s", currentTextValue)
actionOptions := model.PostAction{
Name: "Select an option:",
Integration: &model.PostActionIntegration{
URL: settingHandler + "?" + s.id + "=true",
Context: map[string]interface{}{
ContextIDKey: s.id,
},
},
Type: "select",
Options: stringsToOptions(s.options),
}
actions = []*model.PostAction{&actionOptions}
}
text := fmt.Sprintf("%s\n%s", s.description, currentValueMessage)
sa := model.SlackAttachment{
Title: title,
Text: text,
Fallback: fmt.Sprintf("%s: %s", title, text),
Actions: actions,
}
return &sa, nil
}
func (s *optionSetting) IsDisabled(foreignValue interface{}) bool {
return foreignValue == FalseString
}

Просмотреть файл

@@ -0,0 +1,69 @@
package settings
import (
"errors"
"fmt"
"github.com/mattermost/mattermost/server/public/model"
)
type readOnlySetting struct {
baseSetting
store SettingStore
}
// NewReadOnlySetting creates a new panel value that only read from the setting
func NewReadOnlySetting(id, title, description, dependsOn string, store SettingStore) Setting {
return &readOnlySetting{
baseSetting: baseSetting{
title: title,
description: description,
id: id,
dependsOn: dependsOn,
},
store: store,
}
}
func (s *readOnlySetting) Get(userID string) (interface{}, error) {
value, err := s.store.GetSetting(userID, s.id)
if err != nil {
return "", err
}
stringValue, ok := value.(string)
if !ok {
return "", errors.New("current value is not a string")
}
return stringValue, nil
}
func (s *readOnlySetting) Set(userID string, value interface{}) error {
return nil
}
func (s *readOnlySetting) GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error) {
title := fmt.Sprintf("Setting: %s", s.title)
currentValueMessage := DisabledString
if !disabled {
currentValue, err := s.Get(userID)
if err != nil {
return nil, err
}
currentValueMessage = fmt.Sprintf("Current value: %s", currentValue)
}
text := fmt.Sprintf("%s\n%s", s.description, currentValueMessage)
sa := model.SlackAttachment{
Title: title,
Text: text,
Fallback: fmt.Sprintf("%s: %s", title, text),
}
return &sa, nil
}
func (s *readOnlySetting) IsDisabled(foreignValue interface{}) bool {
return foreignValue == FalseString
}

Просмотреть файл

@@ -0,0 +1,33 @@
package settings
import (
"github.com/mattermost/mattermost/server/public/model"
)
const (
// ContextIDKey defines the key used in the context to store the ID
ContextIDKey = "setting_id"
// ContextButtonValueKey defines the key used in the context to store a button value
ContextButtonValueKey = "button_value"
// ContextOptionValueKey defines the key used in the context to store a selected option value
ContextOptionValueKey = "selected_option"
// DisabledString defines the string used to show that a setting is disabled
DisabledString = "Disabled"
// TrueString codify the boolean true into a string
TrueString = "true"
// FalseString codify the boolean false into a string
FalseString = "false"
)
// Setting defines the behavior of each element a the panel
type Setting interface {
Set(userID string, value interface{}) error
Get(userID string) (interface{}, error)
GetID() string
GetDependency() string
IsDisabled(foreignValue interface{}) bool
GetTitle() string
GetDescription() string
GetSlackAttachments(userID, settingHandler string, disabled bool) (*model.SlackAttachment, error)
}

Просмотреть файл

@@ -0,0 +1,7 @@
package settings
// SettingStore defines the behavior needed to set and get settings
type SettingStore interface {
SetSetting(userID, settingID string, value interface{}) error
GetSetting(userID, settingID string) (interface{}, error)
}

Просмотреть файл

@@ -0,0 +1,16 @@
package settings
import (
"github.com/mattermost/mattermost/server/public/model"
)
func stringsToOptions(in []string) []*model.PostActionOptions {
out := make([]*model.PostActionOptions, len(in))
for i, o := range in {
out[i] = &model.PostActionOptions{
Text: o,
Value: o,
}
}
return out
}

Просмотреть файл

@@ -0,0 +1,53 @@
package panel
import (
"errors"
"github.com/mattermost/mattermost/server/public/pluginapi"
)
type Store interface {
SetPanelPostID(userID string, postID string) error
GetPanelPostID(userID string) (string, error)
DeletePanelPostID(userID string) error
}
type panelStore struct {
kv *pluginapi.KVService
keyPrefix string
}
func NewPanelStore(kv *pluginapi.KVService, keyPrefix string) Store {
return &panelStore{
kv: kv,
keyPrefix: keyPrefix,
}
}
func (ps *panelStore) SetPanelPostID(userID, postID string) error {
ok, err := ps.kv.Set(ps.getKey(userID), postID)
if err != nil {
return err
}
if !ok {
return errors.New("value not set without errors")
}
return nil
}
func (ps *panelStore) GetPanelPostID(userID string) (string, error) {
var postID string
err := ps.kv.Get(ps.getKey(userID), &postID)
if err != nil {
return "", err
}
return postID, nil
}
func (ps *panelStore) DeletePanelPostID(userID string) error {
return ps.kv.Delete(ps.getKey(userID))
}
func (ps *panelStore) getKey(userID string) string {
return ps.keyPrefix + "-" + userID
}

Просмотреть файл

@@ -0,0 +1,76 @@
// Package telemetry allows you to add telemetry to your plugins.
// For Rudder, you can set the data plane URL and the write key on build time,
// to allow having different keys for production and development.
// If you are working on a Mattermost project, the data plane URL is already set.
// In order to default to the development key we have to set an environment variable during build time.
// Copy the following lines in build/custom.mk to setup that variable.
//
// ifndef MM_RUDDER_WRITE_KEY
// MM_RUDDER_WRITE_KEY = 1d5bMvdrfWClLxgK1FvV3s4U1tg
// endif
//
// To use this environment variable to set the key in the plugin,
// you have to add this line after the previous ones.
//
// LDFLAGS += -X "github.com/mattermost/mattermost/server/public/pluginapi/experimental/telemetry.rudderWriteKey=$(MM_RUDDER_WRITE_KEY)"
//
// MM_RUDDER_WRITE_KEY environment variable must be set also during CI
// to the production write key ("1dP7Oi78p0PK1brYLsfslgnbD1I").
// If you want to use your own data plane URL, add also this line and
// make sure the MM_RUDDER_DATAPLANE_URL environment variable is set.
//
// LDFLAGS += -X "github.com/mattermost/mattermost/server/public/pluginapi/experimental/telemetry.rudderDataPlaneURL=$(MM_RUDDER_DATAPLANE_URL)"
//
// In order to use telemetry you should:
//
// 1. Add the new fields to the plugin
//
// type Plugin struct {
// plugin.MattermostPlugin
// ...
// telemetryClient telemetry.Client
// tracker telemetry.Tracker
// }
//
// 2. Start the telemetry client and tracker on plugin activate
//
// func (p *Plugin) OnActivate() error {
// p.telemetryClient, err = telemetry.NewRudderClient()
// if err != nil {
// p.API.LogWarn("telemetry client not started", "error", err.Error())
// }
// ...
// p.tracker = telemetry.NewTracker(
// p.telemetryClient,
// p.API.GetDiagnosticId(),
// p.API.GetServerVersion(),
// Manifest.Id,
// Manifest.Version,
// "plugin_short_namame",
// telemetry.NewTrackerConfig(p.API.GetConfig()),
// logger.New(p.API)
// )
// }
//
// 3. Trigger tracker changes when configuration changes
//
// func (p *Plugin) OnConfigurationChange() error {
// ...
// if p.tracker != nil {
// p.tracker.ReloadConfig(telemetry.NewTrackerConfig(p.API.GetConfig()))
// }
// return nil
// }
//
// 4. Close the client on plugin deactivate
//
// func (p *Plugin) OnDeactivate() error {
// if p.telemetryClient != nil {
// err := p.telemetryClient.Close()
// if err != nil {
// p.API.LogWarn("OnDeactivate: failed to close telemetryClient", "error", err.Error())
// }
// }
// return nil
// }
package telemetry

Просмотреть файл

@@ -0,0 +1,49 @@
package telemetry
import (
rudder "github.com/rudderlabs/analytics-go"
)
// rudderDataPlaneURL is set to the common Data Plane URL for all Mattermost Projects.
// It can be set during build time. More info in the package documentation.
var rudderDataPlaneURL = "https://pdat.matterlytics.com"
// rudderWriteKey is set during build time. More info in the package documentation.
var rudderWriteKey string
// NewRudderClient creates a new telemetry client with Rudder using the default configuration.
func NewRudderClient() (Client, error) {
return NewRudderClientWithCredentials(rudderWriteKey, rudderDataPlaneURL)
}
// NewRudderClientWithCredentials lets you create a Rudder client with your own credentials.
func NewRudderClientWithCredentials(writeKey, dataPlaneURL string) (Client, error) {
client, err := rudder.NewWithConfig(writeKey, dataPlaneURL, rudder.Config{})
if err != nil {
return nil, err
}
return &rudderWrapper{client: client}, nil
}
type rudderWrapper struct {
client rudder.Client
}
func (r *rudderWrapper) Enqueue(t Track) error {
var context *rudder.Context
if t.InstallationID != "" {
context = &rudder.Context{Traits: map[string]any{"installationId": t.InstallationID}}
}
return r.client.Enqueue(rudder.Track{
UserId: t.UserID,
Event: t.Event,
Context: context,
Properties: t.Properties,
})
}
func (r *rudderWrapper) Close() error {
return r.client.Close()
}

Просмотреть файл

@@ -0,0 +1,179 @@
package telemetry
import (
"os"
"sync"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger"
)
type TrackerConfig struct {
EnabledTracking bool
EnabledLogging bool
}
// NewTrackerConfig returns a new trackerConfig from the current values of the model.Config.
func NewTrackerConfig(config *model.Config) TrackerConfig {
var enabledTracking, enabledLogging bool
if config == nil {
return TrackerConfig{}
}
if enableDiagnostics := config.LogSettings.EnableDiagnostics; enableDiagnostics != nil {
enabledTracking = *enableDiagnostics
}
if enableDeveloper := config.ServiceSettings.EnableDeveloper; enableDeveloper != nil {
enabledLogging = *enableDeveloper
}
return TrackerConfig{
EnabledTracking: enabledTracking,
EnabledLogging: enabledLogging,
}
}
// Tracker defines a telemetry tracker
type Tracker interface {
// TrackEvent registers an event through the configured telemetry client
TrackEvent(event string, properties map[string]interface{}) error
// TrackUserEvent registers an event through the configured telemetry client associated to a user
TrackUserEvent(event string, userID string, properties map[string]interface{}) error
// Reload Config re-evaluates tracker config to determine if tracking behavior should change
ReloadConfig(config TrackerConfig)
}
// Client defines a telemetry client
type Client interface {
// Enqueue adds a tracker event (Track) to be registered
Enqueue(t Track) error
// Close closes the client connection, flushing any event left on the queue
Close() error
}
// Track defines an event ready for the client to process
type Track struct {
UserID string
Event string
Properties map[string]interface{}
InstallationID string
}
type tracker struct {
client Client
diagnosticID string
serverVersion string
pluginID string
pluginVersion string
telemetryShortName string
configLock sync.RWMutex
config TrackerConfig
logger logger.Logger
}
// NewTracker creates a default Tracker
// - c Client: A telemetry client. If nil, the tracker will not track any event.
// - diagnosticID: Server unique ID used for telemetry.
// - severVersion: Mattermost server version.
// - pluginID: The plugin ID.
// - pluginVersion: The plugin version.
// - telemetryShortName: Short name for the plugin to use in telemetry. Used to avoid dot separated names like `com.company.pluginName`.
// If a empty string is provided, it will use the pluginID.
// - config: Whether the system has enabled sending telemetry data. If false, the tracker will not track any event.
// - l Logger: A logger to debug event tracking and some important changes (it won't log if nil is passed as logger).
func NewTracker(
c Client,
diagnosticID,
serverVersion,
pluginID,
pluginVersion,
telemetryShortName string,
config TrackerConfig,
l logger.Logger,
) Tracker {
if telemetryShortName == "" {
telemetryShortName = pluginID
}
return &tracker{
telemetryShortName: telemetryShortName,
client: c,
diagnosticID: diagnosticID,
serverVersion: serverVersion,
pluginID: pluginID,
pluginVersion: pluginVersion,
logger: l,
config: config,
}
}
func (t *tracker) ReloadConfig(config TrackerConfig) {
t.configLock.Lock()
defer t.configLock.Unlock()
if config.EnabledTracking != t.config.EnabledTracking {
if config.EnabledTracking {
t.debugf("Enabling plugin telemetry")
} else {
t.debugf("Disabling plugin telemetry")
}
}
t.config.EnabledTracking = config.EnabledTracking
t.config.EnabledLogging = config.EnabledLogging
}
// Note that config lock is handled by the caller.
func (t *tracker) debugf(message string, args ...interface{}) {
if t.logger == nil || !t.config.EnabledLogging {
return
}
t.logger.Debugf(message, args...)
}
func (t *tracker) TrackEvent(event string, properties map[string]interface{}) error {
t.configLock.RLock()
defer t.configLock.RUnlock()
event = t.telemetryShortName + "_" + event
if !t.config.EnabledTracking || t.client == nil {
t.debugf("Plugin telemetry event `%s` tracked, but not sent due to configuration", event)
return nil
}
if properties == nil {
properties = map[string]interface{}{}
}
properties["PluginID"] = t.pluginID
properties["PluginVersion"] = t.pluginVersion
properties["ServerVersion"] = t.serverVersion
// if we are part of a cloud installation, add it's ID to the tracked event's context.
installationID := os.Getenv("MM_CLOUD_INSTALLATION_ID")
err := t.client.Enqueue(Track{
// We consider the server the "user" on the telemetry system. Any reference to the actual user is passed by properties.
UserID: t.diagnosticID,
Event: event,
Properties: properties,
InstallationID: installationID,
})
if err != nil {
return errors.Wrap(err, "cannot enqueue the track")
}
t.debugf("Tracked plugin telemetry event `%s`", event)
return nil
}
func (t *tracker) TrackUserEvent(event, userID string, properties map[string]interface{}) error {
if properties == nil {
properties = map[string]interface{}{}
}
properties["UserActualID"] = userID
return t.TrackEvent(event, properties)
}