[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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
bc11b29807
Коммит
3ee5432664
53
server/public/pluginapi/experimental/bot/bot.go
Обычный файл
53
server/public/pluginapi/experimental/bot/bot.go
Обычный файл
@@ -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)...)
|
||||
}
|
||||
78
server/public/pluginapi/experimental/bot/logger/logger.go
Обычный файл
78
server/public/pluginapi/experimental/bot/logger/logger.go
Обычный файл
@@ -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
|
||||
}
|
||||
17
server/public/pluginapi/experimental/bot/logger/nil_logger.go
Обычный файл
17
server/public/pluginapi/experimental/bot/logger/nil_logger.go
Обычный файл
@@ -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)
|
||||
}
|
||||
61
server/public/pluginapi/experimental/bot/logger/test_logger.go
Обычный файл
61
server/public/pluginapi/experimental/bot/logger/test_logger.go
Обычный файл
@@ -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...) }
|
||||
77
server/public/pluginapi/experimental/bot/mocks/mock_bot.go
Обычный файл
77
server/public/pluginapi/experimental/bot/mocks/mock_bot.go
Обычный файл
@@ -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))
|
||||
}
|
||||
159
server/public/pluginapi/experimental/bot/mocks/mock_logger.go
Обычный файл
159
server/public/pluginapi/experimental/bot/mocks/mock_logger.go
Обычный файл
@@ -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)
|
||||
}
|
||||
151
server/public/pluginapi/experimental/bot/mocks/mock_poster.go
Обычный файл
151
server/public/pluginapi/experimental/bot/mocks/mock_poster.go
Обычный файл
@@ -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...)
|
||||
})
|
||||
}
|
||||
14
server/public/pluginapi/experimental/bot/poster/import.go
Обычный файл
14
server/public/pluginapi/experimental/bot/poster/import.go
Обычный файл
@@ -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)
|
||||
}
|
||||
38
server/public/pluginapi/experimental/bot/poster/poster.go
Обычный файл
38
server/public/pluginapi/experimental/bot/poster/poster.go
Обычный файл
@@ -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)
|
||||
}
|
||||
31
server/public/pluginapi/experimental/command/command.go
Обычный файл
31
server/public/pluginapi/experimental/command/command.go
Обычный файл
@@ -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
|
||||
}
|
||||
74
server/public/pluginapi/experimental/command/info.go
Обычный файл
74
server/public/pluginapi/experimental/command/info.go
Обычный файл
@@ -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
|
||||
}
|
||||
21
server/public/pluginapi/experimental/common/kvstore.go
Обычный файл
21
server/public/pluginapi/experimental/common/kvstore.go
Обычный файл
@@ -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)
|
||||
}
|
||||
8
server/public/pluginapi/experimental/common/logapi.go
Обычный файл
8
server/public/pluginapi/experimental/common/logapi.go
Обычный файл
@@ -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{})
|
||||
}
|
||||
22
server/public/pluginapi/experimental/common/markdown.go
Обычный файл
22
server/public/pluginapi/experimental/common/markdown.go
Обычный файл
@@ -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))
|
||||
}
|
||||
24
server/public/pluginapi/experimental/common/slack_attachments.go
Обычный файл
24
server/public/pluginapi/experimental/common/slack_attachments.go
Обычный файл
@@ -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)
|
||||
}
|
||||
28
server/public/pluginapi/experimental/common/url.go
Обычный файл
28
server/public/pluginapi/experimental/common/url.go
Обычный файл
@@ -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
|
||||
}
|
||||
254
server/public/pluginapi/experimental/flow/flow.go
Обычный файл
254
server/public/pluginapi/experimental/flow/flow.go
Обычный файл
@@ -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
|
||||
}
|
||||
}
|
||||
210
server/public/pluginapi/experimental/flow/handler.go
Обычный файл
210
server/public/pluginapi/experimental/flow/handler.go
Обычный файл
@@ -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"
|
||||
}
|
||||
}
|
||||
146
server/public/pluginapi/experimental/flow/state.go
Обычный файл
146
server/public/pluginapi/experimental/flow/state.go
Обычный файл
@@ -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()
|
||||
}
|
||||
269
server/public/pluginapi/experimental/flow/step.go
Обычный файл
269
server/public/pluginapi/experimental/flow/step.go
Обычный файл
@@ -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))
|
||||
}
|
||||
105
server/public/pluginapi/experimental/oauther/mocks/mock_oauther.go
Обычный файл
105
server/public/pluginapi/experimental/oauther/mocks/mock_oauther.go
Обычный файл
@@ -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)
|
||||
}
|
||||
191
server/public/pluginapi/experimental/oauther/oauth2.go
Обычный файл
191
server/public/pluginapi/experimental/oauther/oauth2.go
Обычный файл
@@ -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
|
||||
}
|
||||
105
server/public/pluginapi/experimental/oauther/oauth2_complete.go
Обычный файл
105
server/public/pluginapi/experimental/oauther/oauth2_complete.go
Обычный файл
@@ -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)
|
||||
}
|
||||
}
|
||||
38
server/public/pluginapi/experimental/oauther/oauth2_connect.go
Обычный файл
38
server/public/pluginapi/experimental/oauther/oauth2_connect.go
Обычный файл
@@ -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)
|
||||
}
|
||||
47
server/public/pluginapi/experimental/oauther/options.go
Обычный файл
47
server/public/pluginapi/experimental/oauther/options.go
Обычный файл
@@ -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
|
||||
}
|
||||
}
|
||||
70
server/public/pluginapi/experimental/panel/handler.go
Обычный файл
70
server/public/pluginapi/experimental/panel/handler.go
Обычный файл
@@ -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)
|
||||
}
|
||||
118
server/public/pluginapi/experimental/panel/mocks/mock_panel.go
Обычный файл
118
server/public/pluginapi/experimental/panel/mocks/mock_panel.go
Обычный файл
@@ -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)
|
||||
}
|
||||
149
server/public/pluginapi/experimental/panel/mocks/mock_setting.go
Обычный файл
149
server/public/pluginapi/experimental/panel/mocks/mock_setting.go
Обычный файл
@@ -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)
|
||||
}
|
||||
171
server/public/pluginapi/experimental/panel/panel.go
Обычный файл
171
server/public/pluginapi/experimental/panel/panel.go
Обычный файл
@@ -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
|
||||
}
|
||||
114
server/public/pluginapi/experimental/panel/settings/bool_setting.go
Обычный файл
114
server/public/pluginapi/experimental/panel/settings/bool_setting.go
Обычный файл
@@ -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
|
||||
}
|
||||
33
server/public/pluginapi/experimental/panel/settings/setting.go
Обычный файл
33
server/public/pluginapi/experimental/panel/settings/setting.go
Обычный файл
@@ -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)
|
||||
}
|
||||
7
server/public/pluginapi/experimental/panel/settings/store.go
Обычный файл
7
server/public/pluginapi/experimental/panel/settings/store.go
Обычный файл
@@ -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)
|
||||
}
|
||||
16
server/public/pluginapi/experimental/panel/settings/utils.go
Обычный файл
16
server/public/pluginapi/experimental/panel/settings/utils.go
Обычный файл
@@ -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
|
||||
}
|
||||
53
server/public/pluginapi/experimental/panel/store.go
Обычный файл
53
server/public/pluginapi/experimental/panel/store.go
Обычный файл
@@ -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
|
||||
}
|
||||
76
server/public/pluginapi/experimental/telemetry/doc.go
Обычный файл
76
server/public/pluginapi/experimental/telemetry/doc.go
Обычный файл
@@ -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
|
||||
49
server/public/pluginapi/experimental/telemetry/rudder.go
Обычный файл
49
server/public/pluginapi/experimental/telemetry/rudder.go
Обычный файл
@@ -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()
|
||||
}
|
||||
179
server/public/pluginapi/experimental/telemetry/tracker.go
Обычный файл
179
server/public/pluginapi/experimental/telemetry/tracker.go
Обычный файл
@@ -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)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user