Expose public/ API as submodule (#23345)
* model -> public/model * plugin -> public/plugin * public/model/utils -> public/utils * platform/shared/mlog -> public/shared/mlog * platform/shared/i18n -> public/shared/i18n * platform/shared/markdown -> public/shared/markdown * platform/services/timezones -> public/shared/timezones * channels/einterfaces -> einterfaces * expose public/ submodule * go mod tidy * .github: cache-dependency-path, setup-go-work * modules-tidy for public/ too * remove old gomodtidy
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
070ee26081
Коммит
bb02b35048
1195
server/public/plugin/api.go
Обычный файл
1195
server/public/plugin/api.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
1268
server/public/plugin/api_timer_layer_generated.go
Обычный файл
1268
server/public/plugin/api_timer_layer_generated.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
50
server/public/plugin/checker/check_api.go
Обычный файл
50
server/public/plugin/checker/check_api.go
Обычный файл
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/checker/internal/asthelpers"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/checker/internal/version"
|
||||
)
|
||||
|
||||
func checkAPIVersionComments(pkgPath string) (result, error) {
|
||||
pkg, err := asthelpers.GetPackage(pkgPath)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
|
||||
apiInterface, err := asthelpers.FindInterface("API", pkg.Syntax)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
|
||||
invalidMethods := findInvalidMethods(apiInterface.Methods.List)
|
||||
return result{Errors: renderErrors(pkg.Fset, invalidMethods)}, nil
|
||||
}
|
||||
|
||||
func findInvalidMethods(methods []*ast.Field) []*ast.Field {
|
||||
var invalid []*ast.Field
|
||||
for _, m := range methods {
|
||||
if !hasValidMinimumVersionComment(m.Doc.Text()) {
|
||||
invalid = append(invalid, m)
|
||||
}
|
||||
}
|
||||
return invalid
|
||||
}
|
||||
|
||||
func hasValidMinimumVersionComment(s string) bool {
|
||||
return version.ExtractMinimumVersionFromComment(s) != ""
|
||||
}
|
||||
|
||||
func renderErrors(fset *token.FileSet, methods []*ast.Field) []string {
|
||||
var out []string
|
||||
for _, m := range methods {
|
||||
out = append(out, renderWithFilePosition(fset, m.Pos(), fmt.Sprintf("missing a minimum server version comment on method %s", m.Names[0].Name)))
|
||||
}
|
||||
return out
|
||||
}
|
||||
60
server/public/plugin/checker/check_api_test.go
Обычный файл
60
server/public/plugin/checker/check_api_test.go
Обычный файл
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckAPIVersionComments(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name, pkgPath, err string
|
||||
expected result
|
||||
}{
|
||||
{
|
||||
name: "valid comments",
|
||||
pkgPath: "github.com/mattermost/mattermost-server/server/public/plugin/checker/internal/test/valid",
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
name: "invalid comments",
|
||||
pkgPath: "github.com/mattermost/mattermost-server/server/public/plugin/checker/internal/test/invalid",
|
||||
expected: result{
|
||||
Errors: []string{"internal/test/invalid/invalid.go:15:2: missing a minimum server version comment on method InvalidMethod"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing API interface",
|
||||
pkgPath: "github.com/mattermost/mattermost-server/server/public/plugin/checker/internal/test/missing",
|
||||
err: "could not find API interface",
|
||||
},
|
||||
{
|
||||
name: "non-existent package path",
|
||||
pkgPath: "github.com/mattermost/mattermost-server/server/public/plugin/checker/internal/test/does_not_exist",
|
||||
err: "could not find API interface",
|
||||
},
|
||||
}
|
||||
|
||||
// Enable debug flag to have packagesdriver/sizes.go print stderr of `go list` command.
|
||||
// We want to surface any error text that may exist in stderr of this command.
|
||||
prevEnvValue := os.Getenv("GOPACKAGESPRINTGOLISTERRORS")
|
||||
os.Setenv("GOPACKAGESPRINTGOLISTERRORS", "true")
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := checkAPIVersionComments(tc.pkgPath)
|
||||
assert.Equal(t, res, tc.expected)
|
||||
|
||||
if tc.err != "" {
|
||||
assert.EqualError(t, err, tc.err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
os.Setenv("GOPACKAGESPRINTGOLISTERRORS", prevEnvValue)
|
||||
}
|
||||
129
server/public/plugin/checker/internal/asthelpers/helpers.go
Обычный файл
129
server/public/plugin/checker/internal/asthelpers/helpers.go
Обычный файл
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package asthelpers
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/types"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
func GetPackage(pkgPath string) (*packages.Package, error) {
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.NeedName | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo,
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, pkgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pkgs) == 0 {
|
||||
return nil, errors.Errorf("could not find package %s", pkgPath)
|
||||
}
|
||||
return pkgs[0], nil
|
||||
}
|
||||
|
||||
func FindInterface(name string, files []*ast.File) (*ast.InterfaceType, error) {
|
||||
iface, _, err := FindInterfaceWithIdent(name, files)
|
||||
return iface, err
|
||||
}
|
||||
|
||||
func FindInterfaceWithIdent(name string, files []*ast.File) (*ast.InterfaceType, *ast.Ident, error) {
|
||||
var (
|
||||
ident *ast.Ident
|
||||
iface *ast.InterfaceType
|
||||
)
|
||||
|
||||
for _, f := range files {
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
if t, ok := n.(*ast.TypeSpec); ok {
|
||||
if iface != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if i, ok := t.Type.(*ast.InterfaceType); ok && t.Name.Name == name {
|
||||
ident = t.Name
|
||||
iface = i
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if iface != nil {
|
||||
return iface, ident, nil
|
||||
}
|
||||
}
|
||||
return nil, nil, errors.Errorf("could not find %s interface", name)
|
||||
}
|
||||
|
||||
func FindMethodsCalledOnType(info *types.Info, typ types.Type, caller *ast.FuncDecl) []string {
|
||||
var methods []string
|
||||
|
||||
ast.Inspect(caller, func(n ast.Node) bool {
|
||||
if s, ok := n.(*ast.SelectorExpr); ok {
|
||||
|
||||
var receiver *ast.Ident
|
||||
switch r := s.X.(type) {
|
||||
case *ast.Ident:
|
||||
// Left-hand side of the selector is an identifier, eg:
|
||||
//
|
||||
// a := p.API
|
||||
// a.GetTeams()
|
||||
//
|
||||
receiver = r
|
||||
case *ast.SelectorExpr:
|
||||
// Left-hand side of the selector is a selector, eg:
|
||||
//
|
||||
// p.API.GetTeams()
|
||||
//
|
||||
receiver = r.Sel
|
||||
}
|
||||
|
||||
if receiver != nil {
|
||||
obj := info.ObjectOf(receiver)
|
||||
if obj != nil && types.Identical(obj.Type(), typ) {
|
||||
methods = append(methods, s.Sel.Name)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
func FindReceiverMethods(receiverName string, files []*ast.File) []*ast.FuncDecl {
|
||||
var fns []*ast.FuncDecl
|
||||
for _, f := range files {
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
if fn, ok := n.(*ast.FuncDecl); ok {
|
||||
r := extractReceiverTypeName(fn)
|
||||
if r == receiverName {
|
||||
fns = append(fns, fn)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return fns
|
||||
}
|
||||
|
||||
func extractReceiverTypeName(fn *ast.FuncDecl) string {
|
||||
if fn.Recv != nil {
|
||||
t := fn.Recv.List[0].Type
|
||||
// Unwrap the pointer type (a star expression)
|
||||
if se, ok := t.(*ast.StarExpr); ok {
|
||||
t = se.X
|
||||
}
|
||||
if id, ok := t.(*ast.Ident); ok {
|
||||
return id.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
36
server/public/plugin/checker/internal/test/invalid/invalid.go
Обычный файл
36
server/public/plugin/checker/internal/test/invalid/invalid.go
Обычный файл
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package invalid
|
||||
|
||||
type API interface {
|
||||
// ValidMethod is a fake method for testing the
|
||||
// plugin comment checker with a valid comment.
|
||||
//
|
||||
// Minimum server version: 1.2.3
|
||||
ValidMethod()
|
||||
|
||||
// InvalidMethod is a fake method for testing the
|
||||
// plugin comment checker with an invalid comment.
|
||||
InvalidMethod()
|
||||
}
|
||||
|
||||
type Helpers interface {
|
||||
// Minimum server version: 1.1
|
||||
LowerVersionMethod()
|
||||
|
||||
// Minimum server version: 1.3
|
||||
HigherVersionMethod()
|
||||
}
|
||||
|
||||
type HelpersImpl struct {
|
||||
api API
|
||||
}
|
||||
|
||||
func (h *HelpersImpl) LowerVersionMethod() {
|
||||
h.api.ValidMethod()
|
||||
}
|
||||
|
||||
func (h *HelpersImpl) HigherVersionMethod() {
|
||||
h.api.ValidMethod()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package missing
|
||||
|
||||
// SomeType is a fake interface for testing the plugin comment checker.
|
||||
type SomeType interface {
|
||||
}
|
||||
44
server/public/plugin/checker/internal/test/valid/valid.go
Обычный файл
44
server/public/plugin/checker/internal/test/valid/valid.go
Обычный файл
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package valid
|
||||
|
||||
type API interface {
|
||||
// ValidMethod is a fake method for testing the
|
||||
// plugin comment checker with a valid comment.
|
||||
//
|
||||
// Minimum server version: 1.2.3
|
||||
ValidMethod()
|
||||
|
||||
// Minimum server version: 1.5
|
||||
NewerValidMethod()
|
||||
}
|
||||
|
||||
type Helpers interface {
|
||||
// Minimum server version: 1.2.3
|
||||
ValidHelperMethod()
|
||||
|
||||
// Minimum server version: 1.5
|
||||
NewerValidHelperMethod()
|
||||
|
||||
// Minimum server version: 1.5
|
||||
IndirectReferenceMethod()
|
||||
}
|
||||
|
||||
type HelpersImpl struct {
|
||||
api API
|
||||
}
|
||||
|
||||
func (h *HelpersImpl) ValidHelperMethod() {
|
||||
h.api.ValidMethod()
|
||||
}
|
||||
|
||||
func (h *HelpersImpl) NewerValidHelperMethod() {
|
||||
h.api.NewerValidMethod()
|
||||
h.api.ValidMethod()
|
||||
}
|
||||
|
||||
func (h *HelpersImpl) IndirectReferenceMethod() {
|
||||
a := h.api
|
||||
a.NewerValidMethod()
|
||||
}
|
||||
22
server/public/plugin/checker/internal/version/comments.go
Обычный файл
22
server/public/plugin/checker/internal/version/comments.go
Обычный файл
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var versionCommentRE = regexp.MustCompile(`^Minimum server version: (\d+\.\d+(?:\.\d+[\w-]*)?)$`)
|
||||
|
||||
func ExtractMinimumVersionFromComment(s string) string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
if len(lines) > 0 {
|
||||
lastLine := lines[len(lines)-1]
|
||||
if m := versionCommentRE.FindStringSubmatch(lastLine); len(m) >= 1 {
|
||||
return m[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
49
server/public/plugin/checker/internal/version/comments_test.go
Обычный файл
49
server/public/plugin/checker/internal/version/comments_test.go
Обычный файл
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExtractVersionFromComment(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "This is a comment.\n\nMinimum server version: 1.2.3-rc1\n",
|
||||
expected: "1.2.3-rc1",
|
||||
},
|
||||
{
|
||||
input: "This is a comment.\n\nMinimum server version: 1.2.3\n",
|
||||
expected: "1.2.3",
|
||||
},
|
||||
{
|
||||
input: "This is a comment.\n\nMinimum server version: 1.2\n",
|
||||
expected: "1.2",
|
||||
},
|
||||
{
|
||||
input: "This is a comment.\n\nMinimum server version: 1\n",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
input: "This is a comment.\n",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%+v", tc), func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, ExtractMinimumVersionFromComment(tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
80
server/public/plugin/checker/internal/version/version.go
Обычный файл
80
server/public/plugin/checker/internal/version/version.go
Обычный файл
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type V string
|
||||
|
||||
func (v V) GreaterThanOrEqualTo(other V) bool {
|
||||
return !v.LessThan(other)
|
||||
}
|
||||
|
||||
func (v V) LessThan(other V) bool {
|
||||
leftParts, leftCount := split(v)
|
||||
rightParts, rightCount := split(other)
|
||||
|
||||
var length int
|
||||
if leftCount < rightCount {
|
||||
length = rightCount
|
||||
} else {
|
||||
length = leftCount
|
||||
}
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
var left, right string
|
||||
|
||||
if i < leftCount {
|
||||
left = leftParts[i]
|
||||
}
|
||||
|
||||
if i < rightCount {
|
||||
right = rightParts[i]
|
||||
}
|
||||
|
||||
if left == right {
|
||||
continue
|
||||
}
|
||||
|
||||
leftInt := parseInt(left)
|
||||
rightInt := parseInt(right)
|
||||
|
||||
isNumericalComparison := leftInt != nil && rightInt != nil
|
||||
|
||||
if isNumericalComparison {
|
||||
return *leftInt < *rightInt
|
||||
}
|
||||
|
||||
return left < right
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func split(v V) ([]string, int) {
|
||||
var chunks []string
|
||||
|
||||
for _, part := range strings.Split(string(v), ".") {
|
||||
chunks = append(chunks, splitNumericalChunks(part)...)
|
||||
}
|
||||
|
||||
return chunks, len(chunks)
|
||||
}
|
||||
|
||||
var numericalOrAlphaRE = regexp.MustCompile(`(\d+|\D+)`)
|
||||
|
||||
func splitNumericalChunks(s string) []string {
|
||||
return numericalOrAlphaRE.FindAllString(s, -1)
|
||||
}
|
||||
|
||||
func parseInt(s string) *int64 {
|
||||
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
return &n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
58
server/public/plugin/checker/internal/version/version_test.go
Обычный файл
58
server/public/plugin/checker/internal/version/version_test.go
Обычный файл
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVersionComparison(t *testing.T) {
|
||||
testCases := []struct {
|
||||
a, b V
|
||||
}{
|
||||
{
|
||||
a: V("1.2"),
|
||||
b: V("1.10"),
|
||||
},
|
||||
{
|
||||
a: V("1.2.1"),
|
||||
b: V("1.2.3"),
|
||||
},
|
||||
{
|
||||
a: V("1.2"),
|
||||
b: V("1.2.3"),
|
||||
},
|
||||
{
|
||||
a: V("1.2.1"),
|
||||
b: V("1.2.3"),
|
||||
},
|
||||
{
|
||||
a: V("1.1"),
|
||||
b: V("1.2.3"),
|
||||
},
|
||||
{
|
||||
a: V("1.2.3"),
|
||||
b: V("1.3"),
|
||||
},
|
||||
{
|
||||
a: V("1.2.1-rc2"),
|
||||
b: V("1.2.1-rc10"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%+v", tc), func(t *testing.T) {
|
||||
assert.True(t, tc.a.LessThan(tc.b))
|
||||
assert.False(t, tc.b.LessThan(tc.a))
|
||||
|
||||
assert.True(t, tc.b.GreaterThanOrEqualTo(tc.a))
|
||||
assert.False(t, tc.a.GreaterThanOrEqualTo(tc.b))
|
||||
})
|
||||
}
|
||||
|
||||
assert.True(t, V("1.2").GreaterThanOrEqualTo("1.2"))
|
||||
}
|
||||
71
server/public/plugin/checker/main.go
Обычный файл
71
server/public/plugin/checker/main.go
Обычный файл
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const pluginPackagePath = "github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
|
||||
type result struct {
|
||||
Warnings []string
|
||||
Errors []string
|
||||
}
|
||||
|
||||
type checkFn func(pkgPath string) (result, error)
|
||||
|
||||
var checks = []checkFn{
|
||||
checkAPIVersionComments,
|
||||
}
|
||||
|
||||
func main() {
|
||||
var res result
|
||||
for _, check := range checks {
|
||||
res = runCheck(res, check)
|
||||
}
|
||||
|
||||
var msgs []string
|
||||
msgs = append(msgs, res.Errors...)
|
||||
msgs = append(msgs, res.Warnings...)
|
||||
sort.Strings(msgs)
|
||||
|
||||
if len(msgs) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "#", pluginPackagePath)
|
||||
fmt.Fprintln(os.Stderr, strings.Join(msgs, "\n"))
|
||||
}
|
||||
|
||||
if len(res.Errors) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runCheck(prev result, fn checkFn) result {
|
||||
res, err := fn(pluginPackagePath)
|
||||
if err != nil {
|
||||
prev.Errors = append(prev.Errors, err.Error())
|
||||
return prev
|
||||
}
|
||||
|
||||
if len(res.Warnings) > 0 {
|
||||
prev.Warnings = append(prev.Warnings, mapWarnings(res.Warnings)...)
|
||||
}
|
||||
|
||||
if len(res.Errors) > 0 {
|
||||
prev.Errors = append(prev.Errors, res.Errors...)
|
||||
}
|
||||
|
||||
return prev
|
||||
}
|
||||
|
||||
func mapWarnings(ss []string) []string {
|
||||
var out []string
|
||||
for _, s := range ss {
|
||||
out = append(out, "[warn] "+s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
29
server/public/plugin/checker/render.go
Обычный файл
29
server/public/plugin/checker/render.go
Обычный файл
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func renderWithFilePosition(fset *token.FileSet, pos token.Pos, msg string) string {
|
||||
var cwd string
|
||||
if d, err := os.Getwd(); err == nil {
|
||||
cwd = d
|
||||
}
|
||||
|
||||
fpos := fset.Position(pos)
|
||||
|
||||
filename, err := filepath.Rel(cwd, fpos.Filename)
|
||||
if err != nil {
|
||||
// If deriving a relative path fails for some reason,
|
||||
// we prefer to still print the absolute path to the file.
|
||||
filename = fpos.Filename
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d:%d: %s", filename, fpos.Line, fpos.Column, msg)
|
||||
}
|
||||
54
server/public/plugin/client.go
Обычный файл
54
server/public/plugin/client.go
Обычный файл
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
InternalKeyPrefix = "mmi_"
|
||||
BotUserKey = InternalKeyPrefix + "botid"
|
||||
)
|
||||
|
||||
// Starts the serving of a Mattermost plugin over net/rpc. gRPC is not yet supported.
|
||||
//
|
||||
// Call this when your plugin is ready to start.
|
||||
func ClientMain(pluginImplementation any) {
|
||||
if impl, ok := pluginImplementation.(interface {
|
||||
SetAPI(api API)
|
||||
SetDriver(driver Driver)
|
||||
}); !ok {
|
||||
panic("Plugin implementation given must embed plugin.MattermostPlugin")
|
||||
} else {
|
||||
impl.SetAPI(nil)
|
||||
impl.SetDriver(nil)
|
||||
}
|
||||
|
||||
pluginMap := map[string]plugin.Plugin{
|
||||
"hooks": &hooksPlugin{hooks: pluginImplementation},
|
||||
}
|
||||
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: handshake,
|
||||
Plugins: pluginMap,
|
||||
})
|
||||
}
|
||||
|
||||
type MattermostPlugin struct {
|
||||
// API exposes the plugin api, and becomes available just prior to the OnActive hook.
|
||||
API API
|
||||
Driver Driver
|
||||
}
|
||||
|
||||
// SetAPI persists the given API interface to the plugin. It is invoked just prior to the
|
||||
// OnActivate hook, exposing the API for use by the plugin.
|
||||
func (p *MattermostPlugin) SetAPI(api API) {
|
||||
p.API = api
|
||||
}
|
||||
|
||||
// SetDriver sets the RPC client implementation to talk with the server.
|
||||
func (p *MattermostPlugin) SetDriver(driver Driver) {
|
||||
p.Driver = driver
|
||||
}
|
||||
920
server/public/plugin/client_rpc.go
Обычный файл
920
server/public/plugin/client_rpc.go
Обычный файл
@@ -0,0 +1,920 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:generate go run interface_generator/main.go
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
var hookNameToId map[string]int = make(map[string]int)
|
||||
|
||||
type hooksRPCClient struct {
|
||||
client *rpc.Client
|
||||
log *mlog.Logger
|
||||
muxBroker *plugin.MuxBroker
|
||||
apiImpl API
|
||||
driver Driver
|
||||
implemented [TotalHooksID]bool
|
||||
doneWg sync.WaitGroup
|
||||
}
|
||||
|
||||
type hooksRPCServer struct {
|
||||
impl any
|
||||
muxBroker *plugin.MuxBroker
|
||||
apiRPCClient *apiRPCClient
|
||||
}
|
||||
|
||||
// Implements hashicorp/go-plugin/plugin.Plugin interface to connect the hooks of a plugin
|
||||
type hooksPlugin struct {
|
||||
hooks any
|
||||
apiImpl API
|
||||
driverImpl Driver
|
||||
log *mlog.Logger
|
||||
}
|
||||
|
||||
func (p *hooksPlugin) Server(b *plugin.MuxBroker) (any, error) {
|
||||
return &hooksRPCServer{impl: p.hooks, muxBroker: b}, nil
|
||||
}
|
||||
|
||||
func (p *hooksPlugin) Client(b *plugin.MuxBroker, client *rpc.Client) (any, error) {
|
||||
return &hooksRPCClient{client: client,
|
||||
log: p.log,
|
||||
muxBroker: b,
|
||||
apiImpl: p.apiImpl,
|
||||
driver: p.driverImpl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type apiRPCClient struct {
|
||||
client *rpc.Client
|
||||
muxBroker *plugin.MuxBroker
|
||||
}
|
||||
|
||||
type apiRPCServer struct {
|
||||
impl API
|
||||
muxBroker *plugin.MuxBroker
|
||||
}
|
||||
|
||||
// ErrorString is a fallback for sending unregistered implementations of the error interface across
|
||||
// rpc. For example, the errorString type from the github.com/pkg/errors package cannot be
|
||||
// registered since it is not exported, but this precludes common error handling paradigms.
|
||||
// ErrorString merely preserves the string description of the error, while satisfying the error
|
||||
// interface itself to allow other registered types (such as model.AppError) to be sent unmodified.
|
||||
type ErrorString struct {
|
||||
Code int // Code to map to various error variables
|
||||
Err string
|
||||
}
|
||||
|
||||
func (e ErrorString) Error() string {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func encodableError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := err.(*model.AppError); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := err.(*pq.Error); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := err.(*mysql.MySQLError); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
ret := &ErrorString{
|
||||
Err: err.Error(),
|
||||
}
|
||||
|
||||
switch err {
|
||||
case io.EOF:
|
||||
ret.Code = 1
|
||||
case sql.ErrNoRows:
|
||||
ret.Code = 2
|
||||
case sql.ErrConnDone:
|
||||
ret.Code = 3
|
||||
case sql.ErrTxDone:
|
||||
ret.Code = 4
|
||||
case driver.ErrSkip:
|
||||
ret.Code = 5
|
||||
case driver.ErrBadConn:
|
||||
ret.Code = 6
|
||||
case driver.ErrRemoveArgument:
|
||||
ret.Code = 7
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func decodableError(err error) error {
|
||||
if encErr, ok := err.(*ErrorString); ok {
|
||||
switch encErr.Code {
|
||||
case 1:
|
||||
return io.EOF
|
||||
case 2:
|
||||
return sql.ErrNoRows
|
||||
case 3:
|
||||
return sql.ErrConnDone
|
||||
case 4:
|
||||
return sql.ErrTxDone
|
||||
case 5:
|
||||
return driver.ErrSkip
|
||||
case 6:
|
||||
return driver.ErrBadConn
|
||||
case 7:
|
||||
return driver.ErrRemoveArgument
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Registering some types used by MM for encoding/gob used by rpc
|
||||
func init() {
|
||||
gob.Register([]*model.SlackAttachment{})
|
||||
gob.Register([]any{})
|
||||
gob.Register(map[string]any{})
|
||||
gob.Register(&model.AppError{})
|
||||
gob.Register(&pq.Error{})
|
||||
gob.Register(&mysql.MySQLError{})
|
||||
gob.Register(&ErrorString{})
|
||||
gob.Register(&model.AutocompleteDynamicListArg{})
|
||||
gob.Register(&model.AutocompleteStaticListArg{})
|
||||
gob.Register(&model.AutocompleteTextArg{})
|
||||
gob.Register(&model.PreviewPost{})
|
||||
}
|
||||
|
||||
// These enforce compile time checks to make sure types implement the interface
|
||||
// If you are getting an error here, you probably need to run `make pluginapi` to
|
||||
// autogenerate RPC glue code
|
||||
var _ plugin.Plugin = &hooksPlugin{}
|
||||
var _ Hooks = &hooksRPCClient{}
|
||||
|
||||
//
|
||||
// Below are special cases for hooks or APIs that can not be auto generated
|
||||
//
|
||||
|
||||
func (g *hooksRPCClient) Implemented() (impl []string, err error) {
|
||||
err = g.client.Call("Plugin.Implemented", struct{}{}, &impl)
|
||||
for _, hookName := range impl {
|
||||
if hookId, ok := hookNameToId[hookName]; ok {
|
||||
g.implemented[hookId] = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Implemented replies with the names of the hooks that are implemented.
|
||||
func (s *hooksRPCServer) Implemented(args struct{}, reply *[]string) error {
|
||||
ifaceType := reflect.TypeOf((*Hooks)(nil)).Elem()
|
||||
implType := reflect.TypeOf(s.impl)
|
||||
selfType := reflect.TypeOf(s)
|
||||
var methods []string
|
||||
for i := 0; i < ifaceType.NumMethod(); i++ {
|
||||
method := ifaceType.Method(i)
|
||||
if m, ok := implType.MethodByName(method.Name); !ok {
|
||||
continue
|
||||
} else if m.Type.NumIn() != method.Type.NumIn()+1 {
|
||||
continue
|
||||
} else if m.Type.NumOut() != method.Type.NumOut() {
|
||||
continue
|
||||
} else {
|
||||
match := true
|
||||
for j := 0; j < method.Type.NumIn(); j++ {
|
||||
if m.Type.In(j+1) != method.Type.In(j) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
for j := 0; j < method.Type.NumOut(); j++ {
|
||||
if m.Type.Out(j) != method.Type.Out(j) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, ok := selfType.MethodByName(method.Name); !ok {
|
||||
continue
|
||||
}
|
||||
methods = append(methods, method.Name)
|
||||
}
|
||||
*reply = methods
|
||||
return encodableError(nil)
|
||||
}
|
||||
|
||||
type Z_OnActivateArgs struct {
|
||||
APIMuxId uint32
|
||||
DriverMuxId uint32
|
||||
}
|
||||
|
||||
type Z_OnActivateReturns struct {
|
||||
A error
|
||||
}
|
||||
|
||||
func (g *hooksRPCClient) OnActivate() error {
|
||||
muxId := g.muxBroker.NextId()
|
||||
g.doneWg.Add(1)
|
||||
go func() {
|
||||
defer g.doneWg.Done()
|
||||
g.muxBroker.AcceptAndServe(muxId, &apiRPCServer{
|
||||
impl: g.apiImpl,
|
||||
muxBroker: g.muxBroker,
|
||||
})
|
||||
}()
|
||||
|
||||
nextID := g.muxBroker.NextId()
|
||||
g.doneWg.Add(1)
|
||||
go func() {
|
||||
defer g.doneWg.Done()
|
||||
g.muxBroker.AcceptAndServe(nextID, &dbRPCServer{
|
||||
dbImpl: g.driver,
|
||||
})
|
||||
}()
|
||||
|
||||
_args := &Z_OnActivateArgs{
|
||||
APIMuxId: muxId,
|
||||
DriverMuxId: nextID,
|
||||
}
|
||||
_returns := &Z_OnActivateReturns{}
|
||||
|
||||
if err := g.client.Call("Plugin.OnActivate", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call to OnActivate plugin failed.", mlog.Err(err))
|
||||
}
|
||||
return _returns.A
|
||||
}
|
||||
|
||||
func (s *hooksRPCServer) OnActivate(args *Z_OnActivateArgs, returns *Z_OnActivateReturns) error {
|
||||
connection, err := s.muxBroker.Dial(args.APIMuxId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn2, err := s.muxBroker.Dial(args.DriverMuxId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.apiRPCClient = &apiRPCClient{
|
||||
client: rpc.NewClient(connection),
|
||||
muxBroker: s.muxBroker,
|
||||
}
|
||||
|
||||
dbClient := &dbRPCClient{
|
||||
client: rpc.NewClient(conn2),
|
||||
}
|
||||
|
||||
if mmplugin, ok := s.impl.(interface {
|
||||
SetAPI(api API)
|
||||
SetDriver(driver Driver)
|
||||
}); ok {
|
||||
mmplugin.SetAPI(s.apiRPCClient)
|
||||
mmplugin.SetDriver(dbClient)
|
||||
}
|
||||
|
||||
if mmplugin, ok := s.impl.(interface {
|
||||
OnConfigurationChange() error
|
||||
}); ok {
|
||||
if err := mmplugin.OnConfigurationChange(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] call to OnConfigurationChange failed, error: %v", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Capture output of standard logger because go-plugin
|
||||
// redirects it.
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
if hook, ok := s.impl.(interface {
|
||||
OnActivate() error
|
||||
}); ok {
|
||||
returns.A = encodableError(hook.OnActivate())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_LoadPluginConfigurationArgsArgs struct {
|
||||
}
|
||||
|
||||
type Z_LoadPluginConfigurationArgsReturns struct {
|
||||
A []byte
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) LoadPluginConfiguration(dest any) error {
|
||||
_args := &Z_LoadPluginConfigurationArgsArgs{}
|
||||
_returns := &Z_LoadPluginConfigurationArgsReturns{}
|
||||
if err := g.client.Call("Plugin.LoadPluginConfiguration", _args, _returns); err != nil {
|
||||
log.Printf("RPC call to LoadPluginConfiguration API failed: %s", err.Error())
|
||||
}
|
||||
if err := json.Unmarshal(_returns.A, dest); err != nil {
|
||||
log.Printf("LoadPluginConfiguration API failed to unmarshal: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) LoadPluginConfiguration(args *Z_LoadPluginConfigurationArgsArgs, returns *Z_LoadPluginConfigurationArgsReturns) error {
|
||||
var config any
|
||||
if hook, ok := s.impl.(interface {
|
||||
LoadPluginConfiguration(dest any) error
|
||||
}); ok {
|
||||
if err := hook.LoadPluginConfiguration(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
returns.A = b
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
hookNameToId["ServeHTTP"] = ServeHTTPID
|
||||
}
|
||||
|
||||
type Z_ServeHTTPArgs struct {
|
||||
ResponseWriterStream uint32
|
||||
Request *http.Request
|
||||
Context *Context
|
||||
RequestBodyStream uint32
|
||||
}
|
||||
|
||||
func (g *hooksRPCClient) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !g.implemented[ServeHTTPID] {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
serveHTTPStreamId := g.muxBroker.NextId()
|
||||
go func() {
|
||||
connection, err := g.muxBroker.Accept(serveHTTPStreamId)
|
||||
if err != nil {
|
||||
g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't accept connection", mlog.Uint32("serve_http_stream_id", serveHTTPStreamId), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
rpcServer := rpc.NewServer()
|
||||
if err := rpcServer.RegisterName("Plugin", &httpResponseWriterRPCServer{w: w, log: g.log}); err != nil {
|
||||
g.log.Error("Plugin failed to ServeHTTP, couldn't register RPC name", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
rpcServer.ServeConn(connection)
|
||||
}()
|
||||
|
||||
requestBodyStreamId := uint32(0)
|
||||
if r.Body != nil {
|
||||
requestBodyStreamId = g.muxBroker.NextId()
|
||||
go func() {
|
||||
bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId)
|
||||
if err != nil {
|
||||
g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't Accept request body connection", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
defer bodyConnection.Close()
|
||||
serveIOReader(r.Body, bodyConnection)
|
||||
}()
|
||||
}
|
||||
|
||||
forwardedRequest := &http.Request{
|
||||
Method: r.Method,
|
||||
URL: r.URL,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
Header: r.Header,
|
||||
Host: r.Host,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
RequestURI: r.RequestURI,
|
||||
}
|
||||
|
||||
if err := g.client.Call("Plugin.ServeHTTP", Z_ServeHTTPArgs{
|
||||
Context: c,
|
||||
ResponseWriterStream: serveHTTPStreamId,
|
||||
Request: forwardedRequest,
|
||||
RequestBodyStream: requestBodyStreamId,
|
||||
}, nil); err != nil {
|
||||
g.log.Error("Plugin failed to ServeHTTP, RPC call failed", mlog.Err(err))
|
||||
http.Error(w, "500 internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *hooksRPCServer) ServeHTTP(args *Z_ServeHTTPArgs, returns *struct{}) error {
|
||||
connection, err := s.muxBroker.Dial(args.ResponseWriterStream)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote response writer stream, error: %v", err.Error())
|
||||
return err
|
||||
}
|
||||
w := connectHTTPResponseWriter(connection)
|
||||
defer w.Close()
|
||||
|
||||
r := args.Request
|
||||
if args.RequestBodyStream != 0 {
|
||||
connection, err := s.muxBroker.Dial(args.RequestBodyStream)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote request body stream, error: %v", err.Error())
|
||||
return err
|
||||
}
|
||||
r.Body = connectIOReader(connection)
|
||||
} else {
|
||||
r.Body = io.NopCloser(&bytes.Buffer{})
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if hook, ok := s.impl.(interface {
|
||||
ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request)
|
||||
}); ok {
|
||||
hook.ServeHTTP(args.Context, w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_PluginHTTPArgs struct {
|
||||
Request *http.Request
|
||||
RequestBody []byte
|
||||
}
|
||||
|
||||
type Z_PluginHTTPReturns struct {
|
||||
Response *http.Response
|
||||
ResponseBody []byte
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) PluginHTTP(request *http.Request) *http.Response {
|
||||
forwardedRequest := &http.Request{
|
||||
Method: request.Method,
|
||||
URL: request.URL,
|
||||
Proto: request.Proto,
|
||||
ProtoMajor: request.ProtoMajor,
|
||||
ProtoMinor: request.ProtoMinor,
|
||||
Header: request.Header,
|
||||
Host: request.Host,
|
||||
RemoteAddr: request.RemoteAddr,
|
||||
RequestURI: request.RequestURI,
|
||||
}
|
||||
|
||||
_args := &Z_PluginHTTPArgs{
|
||||
Request: forwardedRequest,
|
||||
}
|
||||
|
||||
if request.Body != nil {
|
||||
requestBody, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
log.Printf("RPC call to PluginHTTP API failed: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
request.Body.Close()
|
||||
request.Body = nil
|
||||
|
||||
_args.RequestBody = requestBody
|
||||
}
|
||||
|
||||
_returns := &Z_PluginHTTPReturns{}
|
||||
if err := g.client.Call("Plugin.PluginHTTP", _args, _returns); err != nil {
|
||||
log.Printf("RPC call to PluginHTTP API failed: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
_returns.Response.Body = io.NopCloser(bytes.NewBuffer(_returns.ResponseBody))
|
||||
|
||||
return _returns.Response
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) PluginHTTP(args *Z_PluginHTTPArgs, returns *Z_PluginHTTPReturns) error {
|
||||
args.Request.Body = io.NopCloser(bytes.NewBuffer(args.RequestBody))
|
||||
|
||||
if hook, ok := s.impl.(interface {
|
||||
PluginHTTP(request *http.Request) *http.Response
|
||||
}); ok {
|
||||
response := hook.PluginHTTP(args.Request)
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return encodableError(fmt.Errorf("RPC call to PluginHTTP API failed: %s", err.Error()))
|
||||
}
|
||||
response.Body.Close()
|
||||
response.Body = nil
|
||||
|
||||
returns.Response = response
|
||||
returns.ResponseBody = responseBody
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("API PluginHTTP called but not implemented"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
hookNameToId["FileWillBeUploaded"] = FileWillBeUploadedID
|
||||
}
|
||||
|
||||
type Z_FileWillBeUploadedArgs struct {
|
||||
A *Context
|
||||
B *model.FileInfo
|
||||
UploadedFileStream uint32
|
||||
ReplacementFileStream uint32
|
||||
}
|
||||
|
||||
type Z_FileWillBeUploadedReturns struct {
|
||||
A *model.FileInfo
|
||||
B string
|
||||
}
|
||||
|
||||
func (g *hooksRPCClient) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
||||
if !g.implemented[FileWillBeUploadedID] {
|
||||
return info, ""
|
||||
}
|
||||
|
||||
uploadedFileStreamId := g.muxBroker.NextId()
|
||||
go func() {
|
||||
uploadedFileConnection, err := g.muxBroker.Accept(uploadedFileStreamId)
|
||||
if err != nil {
|
||||
g.log.Error("Plugin failed to serve upload file stream. MuxBroker could not Accept connection", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
defer uploadedFileConnection.Close()
|
||||
serveIOReader(file, uploadedFileConnection)
|
||||
}()
|
||||
|
||||
replacementDone := make(chan bool)
|
||||
replacementFileStreamId := g.muxBroker.NextId()
|
||||
go func() {
|
||||
defer close(replacementDone)
|
||||
|
||||
replacementFileConnection, err := g.muxBroker.Accept(replacementFileStreamId)
|
||||
if err != nil {
|
||||
g.log.Error("Plugin failed to serve replacement file stream. MuxBroker could not Accept connection", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
defer replacementFileConnection.Close()
|
||||
if _, err := io.Copy(output, replacementFileConnection); err != nil {
|
||||
g.log.Error("Error reading replacement file.", mlog.Err(err))
|
||||
}
|
||||
}()
|
||||
|
||||
_args := &Z_FileWillBeUploadedArgs{c, info, uploadedFileStreamId, replacementFileStreamId}
|
||||
_returns := &Z_FileWillBeUploadedReturns{A: _args.B}
|
||||
if err := g.client.Call("Plugin.FileWillBeUploaded", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call FileWillBeUploaded to plugin failed.", mlog.Err(err))
|
||||
}
|
||||
|
||||
// Ensure the io.Copy from the replacementFileConnection above completes.
|
||||
<-replacementDone
|
||||
|
||||
return _returns.A, _returns.B
|
||||
}
|
||||
|
||||
func (s *hooksRPCServer) FileWillBeUploaded(args *Z_FileWillBeUploadedArgs, returns *Z_FileWillBeUploadedReturns) error {
|
||||
uploadFileConnection, err := s.muxBroker.Dial(args.UploadedFileStream)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote upload file stream, error: %v", err.Error())
|
||||
return err
|
||||
}
|
||||
defer uploadFileConnection.Close()
|
||||
fileReader := connectIOReader(uploadFileConnection)
|
||||
defer fileReader.Close()
|
||||
|
||||
replacementFileConnection, err := s.muxBroker.Dial(args.ReplacementFileStream)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote replacement file stream, error: %v", err.Error())
|
||||
return err
|
||||
}
|
||||
defer replacementFileConnection.Close()
|
||||
returnFileWriter := replacementFileConnection
|
||||
|
||||
if hook, ok := s.impl.(interface {
|
||||
FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string)
|
||||
}); ok {
|
||||
returns.A, returns.B = hook.FileWillBeUploaded(args.A, args.B, fileReader, returnFileWriter)
|
||||
} else {
|
||||
return fmt.Errorf("hook FileWillBeUploaded called but not implemented")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MessageWillBePosted is in this file because of the difficulty of identifying which fields need special behaviour.
|
||||
// The special behaviour needed is decoding the returned post into the original one to avoid the unintentional removal
|
||||
// of fields by older plugins.
|
||||
func init() {
|
||||
hookNameToId["MessageWillBePosted"] = MessageWillBePostedID
|
||||
}
|
||||
|
||||
type Z_MessageWillBePostedArgs struct {
|
||||
A *Context
|
||||
B *model.Post
|
||||
}
|
||||
|
||||
type Z_MessageWillBePostedReturns struct {
|
||||
A *model.Post
|
||||
B string
|
||||
}
|
||||
|
||||
func (g *hooksRPCClient) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) {
|
||||
_args := &Z_MessageWillBePostedArgs{c, post}
|
||||
_returns := &Z_MessageWillBePostedReturns{A: _args.B}
|
||||
if g.implemented[MessageWillBePostedID] {
|
||||
if err := g.client.Call("Plugin.MessageWillBePosted", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call MessageWillBePosted to plugin failed.", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
return _returns.A, _returns.B
|
||||
}
|
||||
|
||||
func (s *hooksRPCServer) MessageWillBePosted(args *Z_MessageWillBePostedArgs, returns *Z_MessageWillBePostedReturns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
|
||||
}); ok {
|
||||
returns.A, returns.B = hook.MessageWillBePosted(args.A, args.B)
|
||||
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("hook MessageWillBePosted called but not implemented"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MessageWillBeUpdated is in this file because of the difficulty of identifying which fields need special behaviour.
|
||||
// The special behaviour needed is decoding the returned post into the original one to avoid the unintentional removal
|
||||
// of fields by older plugins.
|
||||
func init() {
|
||||
hookNameToId["MessageWillBeUpdated"] = MessageWillBeUpdatedID
|
||||
}
|
||||
|
||||
type Z_MessageWillBeUpdatedArgs struct {
|
||||
A *Context
|
||||
B *model.Post
|
||||
C *model.Post
|
||||
}
|
||||
|
||||
type Z_MessageWillBeUpdatedReturns struct {
|
||||
A *model.Post
|
||||
B string
|
||||
}
|
||||
|
||||
func (g *hooksRPCClient) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
||||
_args := &Z_MessageWillBeUpdatedArgs{c, newPost, oldPost}
|
||||
_returns := &Z_MessageWillBeUpdatedReturns{A: _args.B}
|
||||
if g.implemented[MessageWillBeUpdatedID] {
|
||||
if err := g.client.Call("Plugin.MessageWillBeUpdated", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call MessageWillBeUpdated to plugin failed.", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
return _returns.A, _returns.B
|
||||
}
|
||||
|
||||
func (s *hooksRPCServer) MessageWillBeUpdated(args *Z_MessageWillBeUpdatedArgs, returns *Z_MessageWillBeUpdatedReturns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
|
||||
}); ok {
|
||||
returns.A, returns.B = hook.MessageWillBeUpdated(args.A, args.B, args.C)
|
||||
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("hook MessageWillBeUpdated called but not implemented"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_LogDebugArgs struct {
|
||||
A string
|
||||
B []any
|
||||
}
|
||||
|
||||
type Z_LogDebugReturns struct {
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) LogDebug(msg string, keyValuePairs ...any) {
|
||||
stringifiedPairs := stringifyToObjects(keyValuePairs)
|
||||
_args := &Z_LogDebugArgs{msg, stringifiedPairs}
|
||||
_returns := &Z_LogDebugReturns{}
|
||||
if err := g.client.Call("Plugin.LogDebug", _args, _returns); err != nil {
|
||||
log.Printf("RPC call to LogDebug API failed: %s", err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) LogDebug(args *Z_LogDebugArgs, returns *Z_LogDebugReturns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
LogDebug(msg string, keyValuePairs ...any)
|
||||
}); ok {
|
||||
hook.LogDebug(args.A, args.B...)
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("API LogDebug called but not implemented"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_LogInfoArgs struct {
|
||||
A string
|
||||
B []any
|
||||
}
|
||||
|
||||
type Z_LogInfoReturns struct {
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) LogInfo(msg string, keyValuePairs ...any) {
|
||||
stringifiedPairs := stringifyToObjects(keyValuePairs)
|
||||
_args := &Z_LogInfoArgs{msg, stringifiedPairs}
|
||||
_returns := &Z_LogInfoReturns{}
|
||||
if err := g.client.Call("Plugin.LogInfo", _args, _returns); err != nil {
|
||||
log.Printf("RPC call to LogInfo API failed: %s", err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) LogInfo(args *Z_LogInfoArgs, returns *Z_LogInfoReturns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
LogInfo(msg string, keyValuePairs ...any)
|
||||
}); ok {
|
||||
hook.LogInfo(args.A, args.B...)
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("API LogInfo called but not implemented"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_LogWarnArgs struct {
|
||||
A string
|
||||
B []any
|
||||
}
|
||||
|
||||
type Z_LogWarnReturns struct {
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) LogWarn(msg string, keyValuePairs ...any) {
|
||||
stringifiedPairs := stringifyToObjects(keyValuePairs)
|
||||
_args := &Z_LogWarnArgs{msg, stringifiedPairs}
|
||||
_returns := &Z_LogWarnReturns{}
|
||||
if err := g.client.Call("Plugin.LogWarn", _args, _returns); err != nil {
|
||||
log.Printf("RPC call to LogWarn API failed: %s", err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) LogWarn(args *Z_LogWarnArgs, returns *Z_LogWarnReturns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
LogWarn(msg string, keyValuePairs ...any)
|
||||
}); ok {
|
||||
hook.LogWarn(args.A, args.B...)
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("API LogWarn called but not implemented"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_LogErrorArgs struct {
|
||||
A string
|
||||
B []any
|
||||
}
|
||||
|
||||
type Z_LogErrorReturns struct {
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) LogError(msg string, keyValuePairs ...any) {
|
||||
stringifiedPairs := stringifyToObjects(keyValuePairs)
|
||||
_args := &Z_LogErrorArgs{msg, stringifiedPairs}
|
||||
_returns := &Z_LogErrorReturns{}
|
||||
if err := g.client.Call("Plugin.LogError", _args, _returns); err != nil {
|
||||
log.Printf("RPC call to LogError API failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) LogError(args *Z_LogErrorArgs, returns *Z_LogErrorReturns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
LogError(msg string, keyValuePairs ...any)
|
||||
}); ok {
|
||||
hook.LogError(args.A, args.B...)
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("API LogError called but not implemented"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_InstallPluginArgs struct {
|
||||
PluginStreamID uint32
|
||||
B bool
|
||||
}
|
||||
|
||||
type Z_InstallPluginReturns struct {
|
||||
A *model.Manifest
|
||||
B *model.AppError
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) {
|
||||
pluginStreamID := g.muxBroker.NextId()
|
||||
|
||||
go func() {
|
||||
uploadPluginConnection, err := g.muxBroker.Accept(pluginStreamID)
|
||||
if err != nil {
|
||||
log.Print("Plugin failed to upload plugin. MuxBroker could not Accept connection", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
defer uploadPluginConnection.Close()
|
||||
serveIOReader(file, uploadPluginConnection)
|
||||
}()
|
||||
|
||||
_args := &Z_InstallPluginArgs{pluginStreamID, replace}
|
||||
_returns := &Z_InstallPluginReturns{}
|
||||
if err := g.client.Call("Plugin.InstallPlugin", _args, _returns); err != nil {
|
||||
log.Print("RPC call InstallPlugin to plugin failed.", mlog.Err(err))
|
||||
}
|
||||
|
||||
return _returns.A, _returns.B
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) InstallPlugin(args *Z_InstallPluginArgs, returns *Z_InstallPluginReturns) error {
|
||||
hook, ok := s.impl.(interface {
|
||||
InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError)
|
||||
})
|
||||
if !ok {
|
||||
return encodableError(fmt.Errorf("API InstallPlugin called but not implemented"))
|
||||
}
|
||||
|
||||
receivePluginConnection, err := s.muxBroker.Dial(args.PluginStreamID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote plugin stream, error: %v", err.Error())
|
||||
return err
|
||||
}
|
||||
pluginReader := connectIOReader(receivePluginConnection)
|
||||
defer pluginReader.Close()
|
||||
|
||||
returns.A, returns.B = hook.InstallPlugin(pluginReader, args.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_UploadDataArgs struct {
|
||||
A *model.UploadSession
|
||||
PluginStreamID uint32
|
||||
}
|
||||
|
||||
type Z_UploadDataReturns struct {
|
||||
A *model.FileInfo
|
||||
B error
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) {
|
||||
pluginStreamID := g.muxBroker.NextId()
|
||||
|
||||
go func() {
|
||||
pluginConnection, err := g.muxBroker.Accept(pluginStreamID)
|
||||
if err != nil {
|
||||
log.Print("Failed to upload data. MuxBroker could not Accept connection", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
defer pluginConnection.Close()
|
||||
serveIOReader(rd, pluginConnection)
|
||||
}()
|
||||
|
||||
_args := &Z_UploadDataArgs{us, pluginStreamID}
|
||||
_returns := &Z_UploadDataReturns{}
|
||||
if err := g.client.Call("Plugin.UploadData", _args, _returns); err != nil {
|
||||
log.Print("RPC call UploadData to plugin failed.", mlog.Err(err))
|
||||
}
|
||||
|
||||
return _returns.A, _returns.B
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) UploadData(args *Z_UploadDataArgs, returns *Z_UploadDataReturns) error {
|
||||
hook, ok := s.impl.(interface {
|
||||
UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error)
|
||||
})
|
||||
if !ok {
|
||||
return encodableError(fmt.Errorf("API UploadData called but not implemented"))
|
||||
}
|
||||
|
||||
receivePluginConnection, err := s.muxBroker.Dial(args.PluginStreamID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote plugin stream, error: %v", err.Error())
|
||||
return err
|
||||
}
|
||||
pluginReader := connectIOReader(receivePluginConnection)
|
||||
defer pluginReader.Close()
|
||||
|
||||
returns.A, returns.B = hook.UploadData(args.A, pluginReader)
|
||||
return nil
|
||||
}
|
||||
6030
server/public/plugin/client_rpc_generated.go
Обычный файл
6030
server/public/plugin/client_rpc_generated.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
15
server/public/plugin/context.go
Обычный файл
15
server/public/plugin/context.go
Обычный файл
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// Context passes through metadata about the request or hook event.
|
||||
// For requests this is built in app/plugin_requests.go
|
||||
// For hooks, app.PluginContext() is called.
|
||||
type Context struct {
|
||||
SessionId string
|
||||
RequestId string
|
||||
IPAddress string
|
||||
AcceptLanguage string
|
||||
UserAgent string
|
||||
}
|
||||
459
server/public/plugin/db_rpc.go
Обычный файл
459
server/public/plugin/db_rpc.go
Обычный файл
@@ -0,0 +1,459 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"log"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
// dbRPCClient contains the client-side logic to handle the RPC communication
|
||||
// with the server. It's API is hand-written because we do not expect
|
||||
// new methods to be added very frequently.
|
||||
type dbRPCClient struct {
|
||||
client *rpc.Client
|
||||
}
|
||||
|
||||
// dbRPCServer is the server-side component which is responsible for calling
|
||||
// the driver methods and properly encoding the responses back to the RPC client.
|
||||
type dbRPCServer struct {
|
||||
dbImpl Driver
|
||||
}
|
||||
|
||||
var _ Driver = &dbRPCClient{}
|
||||
|
||||
type Z_DbStrErrReturn struct {
|
||||
A string
|
||||
B error
|
||||
}
|
||||
|
||||
type Z_DbErrReturn struct {
|
||||
A error
|
||||
}
|
||||
|
||||
type Z_DbInt64ErrReturn struct {
|
||||
A int64
|
||||
B error
|
||||
}
|
||||
|
||||
type Z_DbBoolReturn struct {
|
||||
A bool
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) Conn(isMaster bool) (string, error) {
|
||||
ret := &Z_DbStrErrReturn{}
|
||||
err := db.client.Call("Plugin.Conn", isMaster, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.Conn: %v", err)
|
||||
}
|
||||
ret.B = decodableError(ret.B)
|
||||
return ret.A, ret.B
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) Conn(isMaster bool, ret *Z_DbStrErrReturn) error {
|
||||
ret.A, ret.B = db.dbImpl.Conn(isMaster)
|
||||
ret.B = encodableError(ret.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) ConnPing(connID string) error {
|
||||
ret := &Z_DbErrReturn{}
|
||||
err := db.client.Call("Plugin.ConnPing", connID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.ConnPing: %v", err)
|
||||
}
|
||||
ret.A = decodableError(ret.A)
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) ConnPing(connID string, ret *Z_DbErrReturn) error {
|
||||
ret.A = db.dbImpl.ConnPing(connID)
|
||||
ret.A = encodableError(ret.A)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) ConnClose(connID string) error {
|
||||
ret := &Z_DbErrReturn{}
|
||||
err := db.client.Call("Plugin.ConnClose", connID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.ConnClose: %v", err)
|
||||
}
|
||||
ret.A = decodableError(ret.A)
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) ConnClose(connID string, ret *Z_DbErrReturn) error {
|
||||
ret.A = db.dbImpl.ConnClose(connID)
|
||||
ret.A = encodableError(ret.A)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbTxArgs struct {
|
||||
A string
|
||||
B driver.TxOptions
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) Tx(connID string, opts driver.TxOptions) (string, error) {
|
||||
args := &Z_DbTxArgs{
|
||||
A: connID,
|
||||
B: opts,
|
||||
}
|
||||
ret := &Z_DbStrErrReturn{}
|
||||
err := db.client.Call("Plugin.Tx", args, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.Tx: %v", err)
|
||||
}
|
||||
ret.B = decodableError(ret.B)
|
||||
return ret.A, ret.B
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) Tx(args *Z_DbTxArgs, ret *Z_DbStrErrReturn) error {
|
||||
ret.A, ret.B = db.dbImpl.Tx(args.A, args.B)
|
||||
ret.B = encodableError(ret.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) TxCommit(txID string) error {
|
||||
ret := &Z_DbErrReturn{}
|
||||
err := db.client.Call("Plugin.TxCommit", txID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.TxCommit: %v", err)
|
||||
}
|
||||
ret.A = decodableError(ret.A)
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) TxCommit(txID string, ret *Z_DbErrReturn) error {
|
||||
ret.A = db.dbImpl.TxCommit(txID)
|
||||
ret.A = encodableError(ret.A)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) TxRollback(txID string) error {
|
||||
ret := &Z_DbErrReturn{}
|
||||
err := db.client.Call("Plugin.TxRollback", txID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.TxRollback: %v", err)
|
||||
}
|
||||
ret.A = decodableError(ret.A)
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) TxRollback(txID string, ret *Z_DbErrReturn) error {
|
||||
ret.A = db.dbImpl.TxRollback(txID)
|
||||
ret.A = encodableError(ret.A)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbStmtArgs struct {
|
||||
A string
|
||||
B string
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) Stmt(connID, q string) (string, error) {
|
||||
args := &Z_DbStmtArgs{
|
||||
A: connID,
|
||||
B: q,
|
||||
}
|
||||
ret := &Z_DbStrErrReturn{}
|
||||
err := db.client.Call("Plugin.Stmt", args, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.Stmt: %v", err)
|
||||
}
|
||||
ret.B = decodableError(ret.B)
|
||||
return ret.A, ret.B
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) Stmt(args *Z_DbStmtArgs, ret *Z_DbStrErrReturn) error {
|
||||
ret.A, ret.B = db.dbImpl.Stmt(args.A, args.B)
|
||||
ret.B = encodableError(ret.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) StmtClose(stID string) error {
|
||||
ret := &Z_DbErrReturn{}
|
||||
err := db.client.Call("Plugin.StmtClose", stID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.StmtClose: %v", err)
|
||||
}
|
||||
ret.A = decodableError(ret.A)
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) StmtClose(stID string, ret *Z_DbErrReturn) error {
|
||||
ret.A = db.dbImpl.StmtClose(stID)
|
||||
ret.A = encodableError(ret.A)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbIntReturn struct {
|
||||
A int
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) StmtNumInput(stID string) int {
|
||||
ret := &Z_DbIntReturn{}
|
||||
err := db.client.Call("Plugin.StmtNumInput", stID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.StmtNumInput: %v", err)
|
||||
}
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) StmtNumInput(stID string, ret *Z_DbIntReturn) error {
|
||||
ret.A = db.dbImpl.StmtNumInput(stID)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbStmtQueryArgs struct {
|
||||
A string
|
||||
B []driver.NamedValue
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) StmtQuery(stID string, argVals []driver.NamedValue) (string, error) {
|
||||
args := &Z_DbStmtQueryArgs{
|
||||
A: stID,
|
||||
B: argVals,
|
||||
}
|
||||
ret := &Z_DbStrErrReturn{}
|
||||
err := db.client.Call("Plugin.StmtQuery", args, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.StmtQuery: %v", err)
|
||||
}
|
||||
ret.B = decodableError(ret.B)
|
||||
return ret.A, ret.B
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) StmtQuery(args *Z_DbStmtQueryArgs, ret *Z_DbStrErrReturn) error {
|
||||
ret.A, ret.B = db.dbImpl.StmtQuery(args.A, args.B)
|
||||
ret.B = encodableError(ret.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) StmtExec(stID string, argVals []driver.NamedValue) (ResultContainer, error) {
|
||||
args := &Z_DbStmtQueryArgs{
|
||||
A: stID,
|
||||
B: argVals,
|
||||
}
|
||||
ret := &Z_DbResultContErrReturn{}
|
||||
err := db.client.Call("Plugin.StmtExec", args, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.StmtExec: %v", err)
|
||||
}
|
||||
ret.A.LastIDError = decodableError(ret.A.LastIDError)
|
||||
ret.A.RowsAffectedError = decodableError(ret.A.RowsAffectedError)
|
||||
ret.B = decodableError(ret.B)
|
||||
return ret.A, ret.B
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) StmtExec(args *Z_DbStmtQueryArgs, ret *Z_DbResultContErrReturn) error {
|
||||
ret.A, ret.B = db.dbImpl.StmtExec(args.A, args.B)
|
||||
ret.A.LastIDError = encodableError(ret.A.LastIDError)
|
||||
ret.A.RowsAffectedError = encodableError(ret.A.RowsAffectedError)
|
||||
ret.B = encodableError(ret.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbConnArgs struct {
|
||||
A string
|
||||
B string
|
||||
C []driver.NamedValue
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) ConnQuery(connID, q string, argVals []driver.NamedValue) (string, error) {
|
||||
args := &Z_DbConnArgs{
|
||||
A: connID,
|
||||
B: q,
|
||||
C: argVals,
|
||||
}
|
||||
ret := &Z_DbStrErrReturn{}
|
||||
err := db.client.Call("Plugin.ConnQuery", args, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.ConnQuery: %v", err)
|
||||
}
|
||||
ret.B = decodableError(ret.B)
|
||||
return ret.A, ret.B
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) ConnQuery(args *Z_DbConnArgs, ret *Z_DbStrErrReturn) error {
|
||||
ret.A, ret.B = db.dbImpl.ConnQuery(args.A, args.B, args.C)
|
||||
ret.B = encodableError(ret.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbResultContErrReturn struct {
|
||||
A ResultContainer
|
||||
B error
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) ConnExec(connID, q string, argVals []driver.NamedValue) (ResultContainer, error) {
|
||||
args := &Z_DbConnArgs{
|
||||
A: connID,
|
||||
B: q,
|
||||
C: argVals,
|
||||
}
|
||||
ret := &Z_DbResultContErrReturn{}
|
||||
err := db.client.Call("Plugin.ConnExec", args, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.ConnExec: %v", err)
|
||||
}
|
||||
ret.A.LastIDError = decodableError(ret.A.LastIDError)
|
||||
ret.A.RowsAffectedError = decodableError(ret.A.RowsAffectedError)
|
||||
ret.B = decodableError(ret.B)
|
||||
return ret.A, ret.B
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) ConnExec(args *Z_DbConnArgs, ret *Z_DbResultContErrReturn) error {
|
||||
ret.A, ret.B = db.dbImpl.ConnExec(args.A, args.B, args.C)
|
||||
ret.A.LastIDError = encodableError(ret.A.LastIDError)
|
||||
ret.A.RowsAffectedError = encodableError(ret.A.RowsAffectedError)
|
||||
ret.B = encodableError(ret.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbStrSliceReturn struct {
|
||||
A []string
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) RowsColumns(rowsID string) []string {
|
||||
ret := &Z_DbStrSliceReturn{}
|
||||
err := db.client.Call("Plugin.RowsColumns", rowsID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.RowsColumns: %v", err)
|
||||
}
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) RowsColumns(rowsID string, ret *Z_DbStrSliceReturn) error {
|
||||
ret.A = db.dbImpl.RowsColumns(rowsID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) RowsClose(resID string) error {
|
||||
ret := &Z_DbErrReturn{}
|
||||
err := db.client.Call("Plugin.RowsClose", resID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.RowsClose: %v", err)
|
||||
}
|
||||
ret.A = decodableError(ret.A)
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) RowsClose(resID string, ret *Z_DbErrReturn) error {
|
||||
ret.A = db.dbImpl.RowsClose(resID)
|
||||
ret.A = encodableError(ret.A)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbRowScanReturn struct {
|
||||
A error
|
||||
B []driver.Value
|
||||
}
|
||||
|
||||
type Z_DbRowScanArg struct {
|
||||
A string
|
||||
B []driver.Value
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) RowsNext(rowsID string, dest []driver.Value) error {
|
||||
args := &Z_DbRowScanArg{
|
||||
A: rowsID,
|
||||
B: dest,
|
||||
}
|
||||
ret := &Z_DbRowScanReturn{}
|
||||
err := db.client.Call("Plugin.RowsNext", args, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.RowsNext: %v", err)
|
||||
}
|
||||
ret.A = decodableError(ret.A)
|
||||
copy(dest, ret.B)
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) RowsNext(args *Z_DbRowScanArg, ret *Z_DbRowScanReturn) error {
|
||||
ret.A = db.dbImpl.RowsNext(args.A, args.B)
|
||||
ret.A = encodableError(ret.A)
|
||||
// Trick to populate the dest slice. RPC doesn't have a semantic to populate
|
||||
// pointer type args. So the only way to pass values is via args, and only way
|
||||
// to return values is via the return struct.
|
||||
ret.B = args.B
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) RowsHasNextResultSet(rowsID string) bool {
|
||||
ret := &Z_DbBoolReturn{}
|
||||
err := db.client.Call("Plugin.RowsHasNextResultSet", rowsID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.RowsHasNextResultSet: %v", err)
|
||||
}
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) RowsHasNextResultSet(rowsID string, ret *Z_DbBoolReturn) error {
|
||||
ret.A = db.dbImpl.RowsHasNextResultSet(rowsID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) RowsNextResultSet(rowsID string) error {
|
||||
ret := &Z_DbErrReturn{}
|
||||
err := db.client.Call("Plugin.RowsNextResultSet", rowsID, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.RowsNextResultSet: %v", err)
|
||||
}
|
||||
ret.A = decodableError(ret.A)
|
||||
return ret.A
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) RowsNextResultSet(rowsID string, ret *Z_DbErrReturn) error {
|
||||
ret.A = db.dbImpl.RowsNextResultSet(rowsID)
|
||||
ret.A = encodableError(ret.A)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbRowsColumnArg struct {
|
||||
A string
|
||||
B int
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) RowsColumnTypeDatabaseTypeName(rowsID string, index int) string {
|
||||
args := &Z_DbRowsColumnArg{
|
||||
A: rowsID,
|
||||
B: index,
|
||||
}
|
||||
var ret string
|
||||
err := db.client.Call("Plugin.RowsColumnTypeDatabaseTypeName", args, &ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.RowsColumnTypeDatabaseTypeName: %v", err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) RowsColumnTypeDatabaseTypeName(args *Z_DbRowsColumnArg, ret *string) error {
|
||||
*ret = db.dbImpl.RowsColumnTypeDatabaseTypeName(args.A, args.B)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Z_DbRowsColumnTypePrecisionScaleReturn struct {
|
||||
A int64
|
||||
B int64
|
||||
C bool
|
||||
}
|
||||
|
||||
func (db *dbRPCClient) RowsColumnTypePrecisionScale(rowsID string, index int) (int64, int64, bool) {
|
||||
args := &Z_DbRowsColumnArg{
|
||||
A: rowsID,
|
||||
B: index,
|
||||
}
|
||||
ret := &Z_DbRowsColumnTypePrecisionScaleReturn{}
|
||||
err := db.client.Call("Plugin.RowsColumnTypePrecisionScale", args, ret)
|
||||
if err != nil {
|
||||
log.Printf("error during Plugin.RowsColumnTypePrecisionScale: %v", err)
|
||||
}
|
||||
return ret.A, ret.B, ret.C
|
||||
}
|
||||
|
||||
func (db *dbRPCServer) RowsColumnTypePrecisionScale(args *Z_DbRowsColumnArg, ret *Z_DbRowsColumnTypePrecisionScaleReturn) error {
|
||||
ret.A, ret.B, ret.C = db.dbImpl.RowsColumnTypePrecisionScale(args.A, args.B)
|
||||
return nil
|
||||
}
|
||||
9
server/public/plugin/doc.go
Обычный файл
9
server/public/plugin/doc.go
Обычный файл
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// The plugin package is used by Mattermost server plugins written in go. It also enables the
|
||||
// Mattermost server to manage and interact with the running plugin environment.
|
||||
//
|
||||
// Note that this package exports a large number of types prefixed with Z_. These are public only
|
||||
// to allow their use with Hashicorp's go-plugin (and net/rpc). Do not use these directly.
|
||||
package plugin
|
||||
64
server/public/plugin/driver.go
Обычный файл
64
server/public/plugin/driver.go
Обычный файл
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
)
|
||||
|
||||
// ResultContainer contains the output from the LastInsertID
|
||||
// and RowsAffected methods for a given set of rows.
|
||||
// It is used to embed another round-trip to the server,
|
||||
// and helping to avoid tracking results on the server.
|
||||
type ResultContainer struct {
|
||||
LastID int64
|
||||
LastIDError error
|
||||
RowsAffected int64
|
||||
RowsAffectedError error
|
||||
}
|
||||
|
||||
// Driver is a sql driver interface that is used by plugins to perform
|
||||
// raw SQL queries without opening DB connections by themselves. This interface
|
||||
// is not subject to backward compatibility guarantees and is only meant to be
|
||||
// used by plugins built by the Mattermost team.
|
||||
type Driver interface {
|
||||
// Connection
|
||||
Conn(isMaster bool) (string, error)
|
||||
ConnPing(connID string) error
|
||||
ConnClose(connID string) error
|
||||
ConnQuery(connID, q string, args []driver.NamedValue) (string, error) // rows
|
||||
ConnExec(connID, q string, args []driver.NamedValue) (ResultContainer, error) // result
|
||||
|
||||
// Transaction
|
||||
Tx(connID string, opts driver.TxOptions) (string, error)
|
||||
TxCommit(txID string) error
|
||||
TxRollback(txID string) error
|
||||
|
||||
// Statement
|
||||
Stmt(connID, q string) (string, error)
|
||||
StmtClose(stID string) error
|
||||
StmtNumInput(stID string) int
|
||||
StmtQuery(stID string, args []driver.NamedValue) (string, error) // rows
|
||||
StmtExec(stID string, args []driver.NamedValue) (ResultContainer, error) // result
|
||||
|
||||
// Rows
|
||||
RowsColumns(rowsID string) []string
|
||||
RowsClose(rowsID string) error
|
||||
RowsNext(rowsID string, dest []driver.Value) error
|
||||
RowsHasNextResultSet(rowsID string) bool
|
||||
RowsNextResultSet(rowsID string) error
|
||||
RowsColumnTypeDatabaseTypeName(rowsID string, index int) string
|
||||
RowsColumnTypePrecisionScale(rowsID string, index int) (int64, int64, bool)
|
||||
|
||||
// TODO: add this
|
||||
// RowsColumnScanType(rowsID string, index int) reflect.Type
|
||||
|
||||
// Note: the following cannot be implemented because either MySQL or PG
|
||||
// does not support it. So this implementation has to be a common subset
|
||||
// of both DB implementations.
|
||||
// RowsColumnTypeLength(rowsID string, index int) (int64, bool)
|
||||
// RowsColumnTypeNullable(rowsID string, index int) (bool, bool)
|
||||
// ResetSession(ctx context.Context) error
|
||||
// IsValid() bool
|
||||
}
|
||||
596
server/public/plugin/environment.go
Обычный файл
596
server/public/plugin/environment.go
Обычный файл
@@ -0,0 +1,596 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/server/public/utils"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("Item not found")
|
||||
|
||||
type apiImplCreatorFunc func(*model.Manifest) API
|
||||
|
||||
// registeredPlugin stores the state for a given plugin that has been activated
|
||||
// or attempted to be activated this server run.
|
||||
//
|
||||
// If an installed plugin is missing from the env.registeredPlugins map, then the
|
||||
// plugin is configured as disabled and has not been activated during this server run.
|
||||
type registeredPlugin struct {
|
||||
BundleInfo *model.BundleInfo
|
||||
State int
|
||||
Error string
|
||||
|
||||
supervisor *supervisor
|
||||
}
|
||||
|
||||
// PrepackagedPlugin is a plugin prepackaged with the server and found on startup.
|
||||
type PrepackagedPlugin struct {
|
||||
Path string
|
||||
IconData string
|
||||
Manifest *model.Manifest
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// Environment represents the execution environment of active plugins.
|
||||
//
|
||||
// It is meant for use by the Mattermost server to manipulate, interact with and report on the set
|
||||
// of active plugins.
|
||||
type Environment struct {
|
||||
registeredPlugins sync.Map
|
||||
pluginHealthCheckJob *PluginHealthCheckJob
|
||||
logger *mlog.Logger
|
||||
metrics metricsInterface
|
||||
newAPIImpl apiImplCreatorFunc
|
||||
dbDriver Driver
|
||||
pluginDir string
|
||||
webappPluginDir string
|
||||
prepackagedPlugins []*PrepackagedPlugin
|
||||
prepackagedPluginsLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewEnvironment(
|
||||
newAPIImpl apiImplCreatorFunc,
|
||||
dbDriver Driver,
|
||||
pluginDir string,
|
||||
webappPluginDir string,
|
||||
logger *mlog.Logger,
|
||||
metrics metricsInterface,
|
||||
) (*Environment, error) {
|
||||
return &Environment{
|
||||
logger: logger,
|
||||
metrics: metrics,
|
||||
newAPIImpl: newAPIImpl,
|
||||
dbDriver: dbDriver,
|
||||
pluginDir: pluginDir,
|
||||
webappPluginDir: webappPluginDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Performs a full scan of the given path.
|
||||
//
|
||||
// This function will return info for all subdirectories that appear to be plugins (i.e. all
|
||||
// subdirectories containing plugin manifest files, regardless of whether they could actually be
|
||||
// parsed).
|
||||
//
|
||||
// Plugins are found non-recursively and paths beginning with a dot are always ignored.
|
||||
func scanSearchPath(path string) ([]*model.BundleInfo, error) {
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ret []*model.BundleInfo
|
||||
for _, file := range files {
|
||||
if !file.IsDir() || file.Name()[0] == '.' {
|
||||
continue
|
||||
}
|
||||
info := model.BundleInfoForPath(filepath.Join(path, file.Name()))
|
||||
if info.Manifest != nil {
|
||||
ret = append(ret, info)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
var pluginIDBlocklist = map[string]bool{
|
||||
"playbooks": true,
|
||||
"com.mattermost.plugin-incident-response": true,
|
||||
"com.mattermost.plugin-incident-management": true,
|
||||
"focalboard": true,
|
||||
}
|
||||
|
||||
func PluginIDIsBlocked(id string) bool {
|
||||
_, ok := pluginIDBlocklist[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Returns a list of all plugins within the environment.
|
||||
func (env *Environment) Available() ([]*model.BundleInfo, error) {
|
||||
rawList, err := scanSearchPath(env.pluginDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter any plugins that match the blocklist
|
||||
filteredList := make([]*model.BundleInfo, 0, len(rawList))
|
||||
for _, bundleInfo := range rawList {
|
||||
if PluginIDIsBlocked(bundleInfo.Manifest.Id) {
|
||||
env.logger.Debug("Plugin ignored by blocklist", mlog.String("plugin_id", bundleInfo.Manifest.Id))
|
||||
} else {
|
||||
filteredList = append(filteredList, bundleInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredList, nil
|
||||
}
|
||||
|
||||
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder.
|
||||
// The list content is immutable and should not be modified.
|
||||
func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin {
|
||||
env.prepackagedPluginsLock.RLock()
|
||||
defer env.prepackagedPluginsLock.RUnlock()
|
||||
|
||||
return env.prepackagedPlugins
|
||||
}
|
||||
|
||||
// Returns a list of all currently active plugins within the environment.
|
||||
// The returned list should not be modified.
|
||||
func (env *Environment) Active() []*model.BundleInfo {
|
||||
activePlugins := []*model.BundleInfo{}
|
||||
env.registeredPlugins.Range(func(key, value any) bool {
|
||||
plugin := value.(registeredPlugin)
|
||||
if env.IsActive(plugin.BundleInfo.Manifest.Id) {
|
||||
activePlugins = append(activePlugins, plugin.BundleInfo)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return activePlugins
|
||||
}
|
||||
|
||||
// IsActive returns true if the plugin with the given id is active.
|
||||
func (env *Environment) IsActive(id string) bool {
|
||||
return env.GetPluginState(id) == model.PluginStateRunning
|
||||
}
|
||||
|
||||
func (env *Environment) SetPluginError(id string, err string) {
|
||||
if rp, ok := env.registeredPlugins.Load(id); ok {
|
||||
p := rp.(registeredPlugin)
|
||||
p.Error = err
|
||||
env.registeredPlugins.Store(id, p)
|
||||
}
|
||||
}
|
||||
|
||||
func (env *Environment) getPluginError(id string) string {
|
||||
if rp, ok := env.registeredPlugins.Load(id); ok {
|
||||
return rp.(registeredPlugin).Error
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetPluginState returns the current state of a plugin (disabled, running, or error)
|
||||
func (env *Environment) GetPluginState(id string) int {
|
||||
rp, ok := env.registeredPlugins.Load(id)
|
||||
if !ok {
|
||||
return model.PluginStateNotRunning
|
||||
}
|
||||
|
||||
return rp.(registeredPlugin).State
|
||||
}
|
||||
|
||||
// setPluginState sets the current state of a plugin (disabled, running, or error)
|
||||
func (env *Environment) setPluginState(id string, state int) {
|
||||
if rp, ok := env.registeredPlugins.Load(id); ok {
|
||||
p := rp.(registeredPlugin)
|
||||
p.State = state
|
||||
env.registeredPlugins.Store(id, p)
|
||||
}
|
||||
}
|
||||
|
||||
// PublicFilesPath returns a path and true if the plugin with the given id is active.
|
||||
// It returns an empty string and false if the path is not set or invalid
|
||||
func (env *Environment) PublicFilesPath(id string) (string, error) {
|
||||
if !env.IsActive(id) {
|
||||
return "", fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
return filepath.Join(env.pluginDir, id, "public"), nil
|
||||
}
|
||||
|
||||
// Statuses returns a list of plugin statuses representing the state of every plugin
|
||||
func (env *Environment) Statuses() (model.PluginStatuses, error) {
|
||||
plugins, err := env.Available()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get plugin statuses")
|
||||
}
|
||||
|
||||
pluginStatuses := make(model.PluginStatuses, 0, len(plugins))
|
||||
for _, plugin := range plugins {
|
||||
// For now we don't handle bad manifests, we should
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginState := env.GetPluginState(plugin.Manifest.Id)
|
||||
|
||||
status := &model.PluginStatus{
|
||||
PluginId: plugin.Manifest.Id,
|
||||
PluginPath: filepath.Dir(plugin.ManifestPath),
|
||||
State: pluginState,
|
||||
Error: env.getPluginError(plugin.Manifest.Id),
|
||||
Name: plugin.Manifest.Name,
|
||||
Description: plugin.Manifest.Description,
|
||||
Version: plugin.Manifest.Version,
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, status)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
// GetManifest returns a manifest for a given pluginId.
|
||||
// Returns ErrNotFound if plugin is not found.
|
||||
func (env *Environment) GetManifest(pluginId string) (*model.Manifest, error) {
|
||||
plugins, err := env.Available()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get plugin statuses")
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest != nil && plugin.Manifest.Id == pluginId {
|
||||
return plugin.Manifest, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) {
|
||||
defer func() {
|
||||
if reterr != nil {
|
||||
env.SetPluginError(id, reterr.Error())
|
||||
} else {
|
||||
env.SetPluginError(id, "")
|
||||
}
|
||||
}()
|
||||
|
||||
// Check if we are already active
|
||||
if env.IsActive(id) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
plugins, err := env.Available()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var pluginInfo *model.BundleInfo
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
if pluginInfo != nil {
|
||||
return nil, false, fmt.Errorf("multiple plugins found: %v", id)
|
||||
}
|
||||
pluginInfo = p
|
||||
}
|
||||
}
|
||||
if pluginInfo == nil {
|
||||
return nil, false, fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
|
||||
rp := newRegisteredPlugin(pluginInfo)
|
||||
env.registeredPlugins.Store(id, rp)
|
||||
|
||||
defer func() {
|
||||
if reterr == nil {
|
||||
env.setPluginState(id, model.PluginStateRunning)
|
||||
} else {
|
||||
env.setPluginState(id, model.PluginStateFailedToStart)
|
||||
}
|
||||
}()
|
||||
|
||||
if pluginInfo.Manifest.MinServerVersion != "" {
|
||||
fulfilled, err := pluginInfo.Manifest.MeetMinServerVersion(model.CurrentVersion)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("%v: %v", err.Error(), id)
|
||||
}
|
||||
if !fulfilled {
|
||||
return nil, false, fmt.Errorf("plugin requires Mattermost %v: %v", pluginInfo.Manifest.MinServerVersion, id)
|
||||
}
|
||||
}
|
||||
|
||||
componentActivated := false
|
||||
|
||||
if pluginInfo.Manifest.HasWebapp() {
|
||||
updatedManifest, err := env.UnpackWebappBundle(id)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrapf(err, "unable to generate webapp bundle: %v", id)
|
||||
}
|
||||
pluginInfo.Manifest.Webapp.BundleHash = updatedManifest.Webapp.BundleHash
|
||||
|
||||
componentActivated = true
|
||||
}
|
||||
|
||||
if pluginInfo.Manifest.HasServer() {
|
||||
sup, err := newSupervisor(pluginInfo, env.newAPIImpl(pluginInfo.Manifest), env.dbDriver, env.logger, env.metrics)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrapf(err, "unable to start plugin: %v", id)
|
||||
}
|
||||
|
||||
// We pre-emptively set the state to running to prevent re-entrancy issues.
|
||||
// The plugin's OnActivate hook can in-turn call UpdateConfiguration
|
||||
// which again calls this method. This method is guarded against multiple calls,
|
||||
// but fails if it is called recursively.
|
||||
//
|
||||
// Therefore, setting the state to running prevents this from happening,
|
||||
// and in case there is an error, the defer clause will set the proper state anyways.
|
||||
env.setPluginState(id, model.PluginStateRunning)
|
||||
|
||||
if err := sup.Hooks().OnActivate(); err != nil {
|
||||
sup.Shutdown()
|
||||
return nil, false, err
|
||||
}
|
||||
rp.supervisor = sup
|
||||
env.registeredPlugins.Store(id, rp)
|
||||
|
||||
componentActivated = true
|
||||
}
|
||||
|
||||
if !componentActivated {
|
||||
return nil, false, fmt.Errorf("unable to start plugin: must at least have a web app or server component")
|
||||
}
|
||||
|
||||
mlog.Debug("Plugin activated", mlog.String("plugin_id", pluginInfo.Manifest.Id), mlog.String("version", pluginInfo.Manifest.Version))
|
||||
|
||||
return pluginInfo.Manifest, true, nil
|
||||
}
|
||||
|
||||
func (env *Environment) RemovePlugin(id string) {
|
||||
if _, ok := env.registeredPlugins.Load(id); ok {
|
||||
env.registeredPlugins.Delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivates the plugin with the given id.
|
||||
func (env *Environment) Deactivate(id string) bool {
|
||||
p, ok := env.registeredPlugins.Load(id)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
isActive := env.IsActive(id)
|
||||
|
||||
env.setPluginState(id, model.PluginStateNotRunning)
|
||||
|
||||
if !isActive {
|
||||
return false
|
||||
}
|
||||
|
||||
rp := p.(registeredPlugin)
|
||||
if rp.supervisor != nil {
|
||||
if err := rp.supervisor.Hooks().OnDeactivate(); err != nil {
|
||||
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err))
|
||||
}
|
||||
rp.supervisor.Shutdown()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RestartPlugin deactivates, then activates the plugin with the given id.
|
||||
func (env *Environment) RestartPlugin(id string) error {
|
||||
env.Deactivate(id)
|
||||
_, _, err := env.Activate(id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutdown deactivates all plugins and gracefully shuts down the environment.
|
||||
func (env *Environment) Shutdown() {
|
||||
env.TogglePluginHealthCheckJob(false)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
env.registeredPlugins.Range(func(key, value any) bool {
|
||||
rp := value.(registeredPlugin)
|
||||
|
||||
if rp.supervisor == nil || !env.IsActive(rp.BundleInfo.Manifest.Id) {
|
||||
return true
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
defer close(done)
|
||||
if err := rp.supervisor.Hooks().OnDeactivate(); err != nil {
|
||||
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err))
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
env.logger.Warn("Plugin OnDeactivate() failed to complete in 10 seconds", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id))
|
||||
case <-done:
|
||||
}
|
||||
|
||||
rp.supervisor.Shutdown()
|
||||
}()
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
env.registeredPlugins.Range(func(key, value any) bool {
|
||||
env.registeredPlugins.Delete(key)
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// UnpackWebappBundle unpacks webapp bundle for a given plugin id on disk.
|
||||
func (env *Environment) UnpackWebappBundle(id string) (*model.Manifest, error) {
|
||||
plugins, err := env.Available()
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to get available plugins")
|
||||
}
|
||||
var manifest *model.Manifest
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
if manifest != nil {
|
||||
return nil, fmt.Errorf("multiple plugins found: %v", id)
|
||||
}
|
||||
manifest = p.Manifest
|
||||
}
|
||||
}
|
||||
if manifest == nil {
|
||||
return nil, fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
|
||||
bundlePath := filepath.Clean(manifest.Webapp.BundlePath)
|
||||
if bundlePath == "" || bundlePath[0] == '.' {
|
||||
return nil, fmt.Errorf("invalid webapp bundle path")
|
||||
}
|
||||
bundlePath = filepath.Join(env.pluginDir, id, bundlePath)
|
||||
destinationPath := filepath.Join(env.webappPluginDir, id)
|
||||
|
||||
if err = os.RemoveAll(destinationPath); err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to remove old webapp bundle directory: %v", destinationPath)
|
||||
}
|
||||
|
||||
if err = utils.CopyDir(filepath.Dir(bundlePath), destinationPath); err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to copy webapp bundle directory: %v", id)
|
||||
}
|
||||
|
||||
sourceBundleFilepath := filepath.Join(destinationPath, filepath.Base(bundlePath))
|
||||
|
||||
sourceBundleFileContents, err := os.ReadFile(sourceBundleFilepath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to read webapp bundle: %v", id)
|
||||
}
|
||||
|
||||
hash := fnv.New64a()
|
||||
if _, err = hash.Write(sourceBundleFileContents); err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to generate hash for webapp bundle: %v", id)
|
||||
}
|
||||
manifest.Webapp.BundleHash = hash.Sum([]byte{})
|
||||
|
||||
if err = os.Rename(
|
||||
sourceBundleFilepath,
|
||||
filepath.Join(destinationPath, fmt.Sprintf("%s_%x_bundle.js", id, manifest.Webapp.BundleHash)),
|
||||
); err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to rename webapp bundle: %v", id)
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// HooksForPlugin returns the hooks API for the plugin with the given id.
|
||||
//
|
||||
// Consider using RunMultiPluginHook instead.
|
||||
func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
|
||||
if p, ok := env.registeredPlugins.Load(id); ok {
|
||||
rp := p.(registeredPlugin)
|
||||
if rp.supervisor != nil && env.IsActive(id) {
|
||||
return rp.supervisor.Hooks(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
|
||||
// RunMultiPluginHook invokes hookRunnerFunc for each active plugin that implements the given hookId.
|
||||
//
|
||||
// If hookRunnerFunc returns false, iteration will not continue. The iteration order among active
|
||||
// plugins is not specified.
|
||||
func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks) bool, hookId int) {
|
||||
startTime := time.Now()
|
||||
|
||||
env.registeredPlugins.Range(func(key, value any) bool {
|
||||
rp := value.(registeredPlugin)
|
||||
|
||||
if rp.supervisor == nil || !rp.supervisor.Implements(hookId) || !env.IsActive(rp.BundleInfo.Manifest.Id) {
|
||||
return true
|
||||
}
|
||||
|
||||
hookStartTime := time.Now()
|
||||
result := hookRunnerFunc(rp.supervisor.Hooks())
|
||||
|
||||
if env.metrics != nil {
|
||||
elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second)
|
||||
env.metrics.ObservePluginMultiHookIterationDuration(rp.BundleInfo.Manifest.Id, elapsedTime)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
if env.metrics != nil {
|
||||
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
||||
env.metrics.ObservePluginMultiHookDuration(elapsedTime)
|
||||
}
|
||||
}
|
||||
|
||||
// PerformHealthCheck uses the active plugin's supervisor to verify if the plugin has crashed.
|
||||
func (env *Environment) PerformHealthCheck(id string) error {
|
||||
p, ok := env.registeredPlugins.Load(id)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
rp := p.(registeredPlugin)
|
||||
|
||||
sup := rp.supervisor
|
||||
if sup == nil {
|
||||
return nil
|
||||
}
|
||||
return sup.PerformHealthCheck()
|
||||
}
|
||||
|
||||
// SetPrepackagedPlugins saves prepackaged plugins in the environment.
|
||||
func (env *Environment) SetPrepackagedPlugins(plugins []*PrepackagedPlugin) {
|
||||
env.prepackagedPluginsLock.Lock()
|
||||
env.prepackagedPlugins = plugins
|
||||
env.prepackagedPluginsLock.Unlock()
|
||||
}
|
||||
|
||||
func newRegisteredPlugin(bundle *model.BundleInfo) registeredPlugin {
|
||||
state := model.PluginStateNotRunning
|
||||
return registeredPlugin{State: state, BundleInfo: bundle}
|
||||
}
|
||||
|
||||
// TogglePluginHealthCheckJob starts a new job if one is not running and is set to enabled, or kills an existing one if set to disabled.
|
||||
func (env *Environment) TogglePluginHealthCheckJob(enable bool) {
|
||||
// Config is set to enable. No job exists, start a new job.
|
||||
if enable && env.pluginHealthCheckJob == nil {
|
||||
mlog.Debug("Enabling plugin health check job", mlog.Duration("interval_s", HealthCheckInterval))
|
||||
|
||||
job := newPluginHealthCheckJob(env)
|
||||
env.pluginHealthCheckJob = job
|
||||
go job.run()
|
||||
}
|
||||
|
||||
// Config is set to disable. Job exists, kill existing job.
|
||||
if !enable && env.pluginHealthCheckJob != nil {
|
||||
mlog.Debug("Disabling plugin health check job")
|
||||
|
||||
env.pluginHealthCheckJob.Cancel()
|
||||
env.pluginHealthCheckJob = nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetPluginHealthCheckJob returns the configured PluginHealthCheckJob, if any.
|
||||
func (env *Environment) GetPluginHealthCheckJob() *PluginHealthCheckJob {
|
||||
return env.pluginHealthCheckJob
|
||||
}
|
||||
98
server/public/plugin/environment_test.go
Обычный файл
98
server/public/plugin/environment_test.go
Обычный файл
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func TestAvailablePlugins(t *testing.T) {
|
||||
dir, err1 := os.MkdirTemp("", "mm-plugin-test")
|
||||
require.NoError(t, err1)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(dir)
|
||||
})
|
||||
|
||||
testLogger, _ := mlog.NewLogger()
|
||||
env := Environment{
|
||||
pluginDir: dir,
|
||||
logger: testLogger,
|
||||
}
|
||||
|
||||
t.Run("Should be able to load available plugins", func(t *testing.T) {
|
||||
bundle1 := model.BundleInfo{
|
||||
Manifest: &model.Manifest{
|
||||
Id: "someid",
|
||||
Version: "1",
|
||||
},
|
||||
}
|
||||
err := os.Mkdir(filepath.Join(dir, "plugin1"), 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(filepath.Join(dir, "plugin1"))
|
||||
|
||||
path := filepath.Join(dir, "plugin1", "plugin.json")
|
||||
manifestJSON, jsonErr := json.Marshal(bundle1.Manifest)
|
||||
require.NoError(t, jsonErr)
|
||||
err = os.WriteFile(path, manifestJSON, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
bundles, err := env.Available()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, bundles, 1)
|
||||
})
|
||||
|
||||
t.Run("Should not be able to load plugins without a valid manifest file", func(t *testing.T) {
|
||||
err := os.Mkdir(filepath.Join(dir, "plugin2"), 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(filepath.Join(dir, "plugin2"))
|
||||
|
||||
path := filepath.Join(dir, "plugin2", "manifest.json")
|
||||
err = os.WriteFile(path, []byte("{}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
bundles, err := env.Available()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, bundles, 0)
|
||||
})
|
||||
|
||||
t.Run("Should not be able to load plugins without a manifest file", func(t *testing.T) {
|
||||
err := os.Mkdir(filepath.Join(dir, "plugin3"), 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(filepath.Join(dir, "plugin3"))
|
||||
|
||||
bundles, err := env.Available()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, bundles, 0)
|
||||
})
|
||||
|
||||
t.Run("Should not load bundles on blocklist", func(t *testing.T) {
|
||||
bundle := model.BundleInfo{
|
||||
Manifest: &model.Manifest{
|
||||
Id: "playbooks",
|
||||
Version: "1",
|
||||
},
|
||||
}
|
||||
err := os.Mkdir(filepath.Join(dir, "plugin4"), 0700)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(filepath.Join(dir, "plugin4"))
|
||||
|
||||
path := filepath.Join(dir, "plugin4", "plugin.json")
|
||||
manifestJSON, jsonErr := json.Marshal(bundle.Manifest)
|
||||
require.NoError(t, jsonErr)
|
||||
err = os.WriteFile(path, manifestJSON, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
bundles, err := env.Available()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, bundles, 0)
|
||||
})
|
||||
}
|
||||
28
server/public/plugin/example_hello_world_test.go
Обычный файл
28
server/public/plugin/example_hello_world_test.go
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
// HelloWorldPlugin implements the interface expected by the Mattermost server to communicate
|
||||
// between the server and plugin processes.
|
||||
type HelloWorldPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
|
||||
func (p *HelloWorldPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "Hello, world!")
|
||||
}
|
||||
|
||||
// This example demonstrates a plugin that handles HTTP requests which respond by greeting the
|
||||
// world.
|
||||
func Example_helloWorld() {
|
||||
plugin.ClientMain(&HelloWorldPlugin{})
|
||||
}
|
||||
120
server/public/plugin/example_help_test.go
Обычный файл
120
server/public/plugin/example_help_test.go
Обычный файл
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
// configuration represents the configuration for this plugin as exposed via the Mattermost
|
||||
// server configuration.
|
||||
type configuration struct {
|
||||
TeamName string
|
||||
ChannelName string
|
||||
|
||||
// channelID is resolved when the public configuration fields above change
|
||||
channelID string
|
||||
}
|
||||
|
||||
// HelpPlugin implements the interface expected by the Mattermost server to communicate
|
||||
// between the server and plugin processes.
|
||||
type HelpPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
|
||||
// configurationLock synchronizes access to the configuration.
|
||||
configurationLock sync.RWMutex
|
||||
|
||||
// configuration is the active plugin configuration. Consult getConfiguration and
|
||||
// setConfiguration for usage.
|
||||
configuration *configuration
|
||||
}
|
||||
|
||||
// getConfiguration retrieves the active configuration under lock, making it safe to use
|
||||
// concurrently. The active configuration may change underneath the client of this method, but
|
||||
// the struct returned by this API call is considered immutable.
|
||||
func (p *HelpPlugin) getConfiguration() *configuration {
|
||||
p.configurationLock.RLock()
|
||||
defer p.configurationLock.RUnlock()
|
||||
|
||||
if p.configuration == nil {
|
||||
return &configuration{}
|
||||
}
|
||||
|
||||
return p.configuration
|
||||
}
|
||||
|
||||
// setConfiguration replaces the active configuration under lock.
|
||||
//
|
||||
// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not
|
||||
// reentrant.
|
||||
func (p *HelpPlugin) setConfiguration(configuration *configuration) {
|
||||
// Replace the active configuration under lock.
|
||||
p.configurationLock.Lock()
|
||||
defer p.configurationLock.Unlock()
|
||||
p.configuration = configuration
|
||||
}
|
||||
|
||||
// OnConfigurationChange updates the active configuration for this plugin under lock.
|
||||
func (p *HelpPlugin) OnConfigurationChange() error {
|
||||
var configuration = new(configuration)
|
||||
|
||||
// Load the public configuration fields from the Mattermost server configuration.
|
||||
if err := p.API.LoadPluginConfiguration(configuration); err != nil {
|
||||
return errors.Wrap(err, "failed to load plugin configuration")
|
||||
}
|
||||
|
||||
team, err := p.API.GetTeamByName(configuration.TeamName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to find team %s", configuration.TeamName)
|
||||
}
|
||||
|
||||
channel, err := p.API.GetChannelByName(team.Id, configuration.ChannelName, false)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to find channel %s", configuration.ChannelName)
|
||||
}
|
||||
|
||||
configuration.channelID = channel.Id
|
||||
|
||||
p.setConfiguration(configuration)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MessageHasBeenPosted automatically replies to posts that plea for help.
|
||||
func (p *HelpPlugin) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
|
||||
configuration := p.getConfiguration()
|
||||
|
||||
// Ignore posts not in the configured channel
|
||||
if post.ChannelId != configuration.channelID {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore posts this plugin made.
|
||||
if sentByPlugin, _ := post.GetProp("sent_by_plugin").(bool); sentByPlugin {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore posts without a plea for help.
|
||||
if !strings.Contains(post.Message, "help") {
|
||||
return
|
||||
}
|
||||
|
||||
p.API.SendEphemeralPost(post.UserId, &model.Post{
|
||||
ChannelId: configuration.channelID,
|
||||
Message: "You asked for help? Checkout https://support.mattermost.com/hc/en-us",
|
||||
Props: map[string]any{
|
||||
"sent_by_plugin": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Example_helpPlugin() {
|
||||
plugin.ClientMain(&HelpPlugin{})
|
||||
}
|
||||
135
server/public/plugin/hclog_adapter.go
Обычный файл
135
server/public/plugin/hclog_adapter.go
Обычный файл
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type hclogAdapter struct {
|
||||
wrappedLogger *mlog.Logger
|
||||
extrasKey string
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) Log(level hclog.Level, msg string, args ...any) {
|
||||
switch level {
|
||||
case hclog.Trace:
|
||||
h.Trace(msg, args...)
|
||||
case hclog.Debug:
|
||||
h.Debug(msg, args...)
|
||||
case hclog.Info:
|
||||
h.Info(msg, args...)
|
||||
case hclog.Warn:
|
||||
h.Warn(msg, args...)
|
||||
case hclog.Error:
|
||||
h.Error(msg, args...)
|
||||
default:
|
||||
// For unknown/unexpected log level, treat it as an error so we notice and fix the code.
|
||||
h.Error(msg, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) Trace(msg string, args ...any) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Debug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) Debug(msg string, args ...any) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Debug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) Info(msg string, args ...any) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Info(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Info(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) Warn(msg string, args ...any) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Warn(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Warn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) Error(msg string, args ...any) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Error(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) IsTrace() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) IsDebug() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) IsInfo() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) IsWarn() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) IsError() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) With(args ...any) hclog.Logger {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) Named(name string) hclog.Logger {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) ResetNamed(name string) hclog.Logger {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
|
||||
return h.wrappedLogger.StdLogger(mlog.LvlInfo)
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
|
||||
return h.wrappedLogger.StdLogWriter()
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) SetLevel(hclog.Level) {}
|
||||
|
||||
func (h *hclogAdapter) GetLevel() hclog.Level { return hclog.NoLevel }
|
||||
|
||||
func (h *hclogAdapter) ImpliedArgs() []any {
|
||||
return []any{}
|
||||
}
|
||||
|
||||
func (h *hclogAdapter) Name() string {
|
||||
return "MattermostPluginLogger"
|
||||
}
|
||||
124
server/public/plugin/health_check.go
Обычный файл
124
server/public/plugin/health_check.go
Обычный файл
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
HealthCheckInterval = 30 * time.Second // How often the health check should run
|
||||
HealthCheckDeactivationWindow = 60 * time.Minute // How long we wait for num fails to occur before deactivating the plugin
|
||||
HealthCheckPingFailLimit = 3 // How many times we call RPC ping in a row before it is considered a failure
|
||||
HealthCheckNumRestartsLimit = 3 // How many times we restart a plugin before we deactivate it
|
||||
)
|
||||
|
||||
type PluginHealthCheckJob struct {
|
||||
cancel chan struct{}
|
||||
cancelled chan struct{}
|
||||
cancelOnce sync.Once
|
||||
env *Environment
|
||||
failureTimestamps sync.Map
|
||||
}
|
||||
|
||||
// run continuously performs health checks on all active plugins, on a timer.
|
||||
func (job *PluginHealthCheckJob) run() {
|
||||
mlog.Debug("Plugin health check job starting.")
|
||||
defer close(job.cancelled)
|
||||
|
||||
ticker := time.NewTicker(HealthCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
activePlugins := job.env.Active()
|
||||
for _, plugin := range activePlugins {
|
||||
job.CheckPlugin(plugin.Manifest.Id)
|
||||
}
|
||||
case <-job.cancel:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPlugin determines the plugin's health status, then handles the error or success case.
|
||||
// If the plugin passes the health check, do nothing.
|
||||
// If the plugin fails the health check, the function either restarts or deactivates the plugin, based on the quantity and frequency of its failures.
|
||||
func (job *PluginHealthCheckJob) CheckPlugin(id string) {
|
||||
err := job.env.PerformHealthCheck(id)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mlog.Warn("Health check failed for plugin", mlog.String("id", id), mlog.Err(err))
|
||||
timestamps := job.getStoredTimestamps(id)
|
||||
timestamps = append(timestamps, time.Now())
|
||||
|
||||
if shouldDeactivatePlugin(timestamps) {
|
||||
// Order matters here, must deactivate first and then set plugin state
|
||||
mlog.Debug("Deactivating plugin due to multiple crashes", mlog.String("id", id))
|
||||
job.env.Deactivate(id)
|
||||
|
||||
// Reset timestamp state for this plugin
|
||||
job.failureTimestamps.Delete(id)
|
||||
job.env.setPluginState(id, model.PluginStateFailedToStayRunning)
|
||||
} else {
|
||||
mlog.Debug("Restarting plugin due to failed health check", mlog.String("id", id))
|
||||
if err := job.env.RestartPlugin(id); err != nil {
|
||||
mlog.Error("Failed to restart plugin", mlog.String("id", id), mlog.Err(err))
|
||||
}
|
||||
|
||||
// Store this failure so we can continue to monitor the plugin
|
||||
job.failureTimestamps.Store(id, removeStaleTimestamps(timestamps))
|
||||
}
|
||||
}
|
||||
|
||||
// getStoredTimestamps returns the stored failure timestamps for a plugin.
|
||||
func (job *PluginHealthCheckJob) getStoredTimestamps(id string) []time.Time {
|
||||
timestamps, ok := job.failureTimestamps.Load(id)
|
||||
if !ok {
|
||||
timestamps = []time.Time{}
|
||||
}
|
||||
return timestamps.([]time.Time)
|
||||
}
|
||||
|
||||
func newPluginHealthCheckJob(env *Environment) *PluginHealthCheckJob {
|
||||
return &PluginHealthCheckJob{
|
||||
cancel: make(chan struct{}),
|
||||
cancelled: make(chan struct{}),
|
||||
env: env,
|
||||
}
|
||||
}
|
||||
|
||||
func (job *PluginHealthCheckJob) Cancel() {
|
||||
job.cancelOnce.Do(func() {
|
||||
close(job.cancel)
|
||||
})
|
||||
<-job.cancelled
|
||||
}
|
||||
|
||||
// shouldDeactivatePlugin determines if a plugin needs to be deactivated after the plugin has failed (HealthCheckNumRestartsLimit) times,
|
||||
// within the configured time window (HealthCheckDeactivationWindow).
|
||||
func shouldDeactivatePlugin(failedTimestamps []time.Time) bool {
|
||||
if len(failedTimestamps) < HealthCheckNumRestartsLimit {
|
||||
return false
|
||||
}
|
||||
|
||||
index := len(failedTimestamps) - HealthCheckNumRestartsLimit
|
||||
return time.Since(failedTimestamps[index]) <= HealthCheckDeactivationWindow
|
||||
}
|
||||
|
||||
// removeStaleTimestamps only keeps the last HealthCheckNumRestartsLimit items in timestamps.
|
||||
func removeStaleTimestamps(timestamps []time.Time) []time.Time {
|
||||
if len(timestamps) > HealthCheckNumRestartsLimit {
|
||||
timestamps = timestamps[len(timestamps)-HealthCheckNumRestartsLimit:]
|
||||
}
|
||||
|
||||
return timestamps
|
||||
}
|
||||
147
server/public/plugin/health_check_test.go
Обычный файл
147
server/public/plugin/health_check_test.go
Обычный файл
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/utils"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func TestPluginHealthCheck(t *testing.T) {
|
||||
for name, f := range map[string]func(*testing.T){
|
||||
"PluginHealthCheck_Success": testPluginHealthCheckSuccess,
|
||||
"PluginHealthCheck_Panic": testPluginHealthCheckPanic,
|
||||
} {
|
||||
t.Run(name, f)
|
||||
}
|
||||
}
|
||||
|
||||
func testPluginHealthCheckSuccess(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend.exe")
|
||||
utils.CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&MyPlugin{})
|
||||
}
|
||||
`, backend)
|
||||
|
||||
err = os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "server": {"executable": "backend.exe"}}`), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
log := mlog.CreateConsoleTestLogger(true, mlog.LvlError)
|
||||
defer log.Shutdown()
|
||||
|
||||
supervisor, err := newSupervisor(bundle, nil, nil, log, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, supervisor)
|
||||
defer supervisor.Shutdown()
|
||||
|
||||
err = supervisor.PerformHealthCheck()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testPluginHealthCheckPanic(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend.exe")
|
||||
utils.CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
func (p *MyPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
||||
panic("Uncaught error")
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&MyPlugin{})
|
||||
}
|
||||
`, backend)
|
||||
|
||||
err = os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "server": {"executable": "backend.exe"}}`), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
log := mlog.CreateConsoleTestLogger(true, mlog.LvlError)
|
||||
defer log.Shutdown()
|
||||
|
||||
supervisor, err := newSupervisor(bundle, nil, nil, log, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, supervisor)
|
||||
defer supervisor.Shutdown()
|
||||
|
||||
err = supervisor.PerformHealthCheck()
|
||||
require.NoError(t, err)
|
||||
|
||||
supervisor.hooks.MessageWillBePosted(&Context{}, &model.Post{})
|
||||
|
||||
err = supervisor.PerformHealthCheck()
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestShouldDeactivatePlugin(t *testing.T) {
|
||||
// No failures, don't restart
|
||||
ftime := []time.Time{}
|
||||
result := shouldDeactivatePlugin(ftime)
|
||||
require.Equal(t, false, result)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Failures are recent enough to restart
|
||||
ftime = []time.Time{}
|
||||
ftime = append(ftime, now.Add(-HealthCheckDeactivationWindow/10*2))
|
||||
ftime = append(ftime, now.Add(-HealthCheckDeactivationWindow/10))
|
||||
ftime = append(ftime, now)
|
||||
|
||||
result = shouldDeactivatePlugin(ftime)
|
||||
require.Equal(t, true, result)
|
||||
|
||||
// Failures are too spaced out to warrant a restart
|
||||
ftime = []time.Time{}
|
||||
ftime = append(ftime, now.Add(-HealthCheckDeactivationWindow*2))
|
||||
ftime = append(ftime, now.Add(-HealthCheckDeactivationWindow*1))
|
||||
ftime = append(ftime, now)
|
||||
|
||||
result = shouldDeactivatePlugin(ftime)
|
||||
require.Equal(t, false, result)
|
||||
|
||||
// Not enough failures are present to warrant a restart
|
||||
ftime = []time.Time{}
|
||||
ftime = append(ftime, now.Add(-HealthCheckDeactivationWindow/10))
|
||||
ftime = append(ftime, now)
|
||||
|
||||
result = shouldDeactivatePlugin(ftime)
|
||||
require.Equal(t, false, result)
|
||||
}
|
||||
205
server/public/plugin/hijack.go
Обычный файл
205
server/public/plugin/hijack.go
Обычный файл
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
hijackedConnReadBufSize = 4096
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotHijacked = errors.New("response is not hijacked")
|
||||
ErrAlreadyHijacked = errors.New("response was already hijacked")
|
||||
ErrCannotHijack = errors.New("response cannot be hijacked")
|
||||
)
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HjConnRWRead(b []byte, reply *[]byte) error {
|
||||
if w.hjr == nil {
|
||||
return ErrNotHijacked
|
||||
}
|
||||
data := make([]byte, len(b))
|
||||
n, err := w.hjr.bufrw.Read(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = data[:n]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HjConnRWWrite(b []byte, reply *int) error {
|
||||
if w.hjr == nil {
|
||||
return ErrNotHijacked
|
||||
}
|
||||
n, err := w.hjr.bufrw.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = n
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HjConnRead(size int, reply *[]byte) error {
|
||||
if w.hjr == nil {
|
||||
return ErrNotHijacked
|
||||
}
|
||||
if len(w.hjr.readBuf) < size {
|
||||
w.hjr.readBuf = make([]byte, size)
|
||||
}
|
||||
n, err := w.hjr.conn.Read(w.hjr.readBuf[:size])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = w.hjr.readBuf[:n]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HjConnWrite(b []byte, reply *int) error {
|
||||
if w.hjr == nil {
|
||||
return ErrNotHijacked
|
||||
}
|
||||
n, err := w.hjr.conn.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = n
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HjConnClose(args struct{}, reply *struct{}) error {
|
||||
if w.hjr == nil {
|
||||
return ErrNotHijacked
|
||||
}
|
||||
return w.hjr.conn.Close()
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HjConnSetDeadline(t time.Time, reply *struct{}) error {
|
||||
if w.hjr == nil {
|
||||
return ErrNotHijacked
|
||||
}
|
||||
return w.hjr.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HjConnSetReadDeadline(t time.Time, reply *struct{}) error {
|
||||
if w.hjr == nil {
|
||||
return ErrNotHijacked
|
||||
}
|
||||
return w.hjr.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HjConnSetWriteDeadline(t time.Time, reply *struct{}) error {
|
||||
if w.hjr == nil {
|
||||
return ErrNotHijacked
|
||||
}
|
||||
return w.hjr.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) HijackResponse(args struct{}, reply *struct{}) error {
|
||||
if w.hjr != nil {
|
||||
return ErrAlreadyHijacked
|
||||
}
|
||||
hj, ok := w.w.(http.Hijacker)
|
||||
if !ok {
|
||||
return ErrCannotHijack
|
||||
}
|
||||
conn, bufrw, err := hj.Hijack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.hjr = &hijackedResponse{
|
||||
conn: conn,
|
||||
bufrw: bufrw,
|
||||
readBuf: make([]byte, hijackedConnReadBufSize),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type hijackedConn struct {
|
||||
client *rpc.Client
|
||||
}
|
||||
|
||||
type hijackedConnRW struct {
|
||||
client *rpc.Client
|
||||
}
|
||||
|
||||
func (w *hijackedConnRW) Read(b []byte) (int, error) {
|
||||
var data []byte
|
||||
if err := w.client.Call("Plugin.HjConnRWRead", b, &data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
copy(b, data)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (w *hijackedConnRW) Write(b []byte) (int, error) {
|
||||
var n int
|
||||
if err := w.client.Call("Plugin.HjConnRWWrite", b, &n); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *hijackedConn) Read(b []byte) (int, error) {
|
||||
var data []byte
|
||||
if err := w.client.Call("Plugin.HjConnRead", len(b), &data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
copy(b, data)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (w *hijackedConn) Write(b []byte) (int, error) {
|
||||
var n int
|
||||
if err := w.client.Call("Plugin.HjConnWrite", b, &n); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *hijackedConn) Close() error {
|
||||
return w.client.Call("Plugin.HjConnClose", struct{}{}, nil)
|
||||
}
|
||||
|
||||
func (w *hijackedConn) LocalAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *hijackedConn) RemoteAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *hijackedConn) SetDeadline(t time.Time) error {
|
||||
return w.client.Call("Plugin.HjConnSetDeadline", t, nil)
|
||||
}
|
||||
|
||||
func (w *hijackedConn) SetReadDeadline(t time.Time) error {
|
||||
return w.client.Call("Plugin.HjConnSetReadDeadline", t, nil)
|
||||
}
|
||||
|
||||
func (w *hijackedConn) SetWriteDeadline(t time.Time) error {
|
||||
return w.client.Call("Plugin.HjConnSetWriteDeadline", t, nil)
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCClient) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
c := &hijackedConn{
|
||||
client: w.client,
|
||||
}
|
||||
rw := &hijackedConnRW{
|
||||
client: w.client,
|
||||
}
|
||||
|
||||
if err := w.client.Call("Plugin.HijackResponse", struct{}{}, nil); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return c, bufio.NewReadWriter(bufio.NewReader(rw), bufio.NewWriter(rw)), nil
|
||||
}
|
||||
339
server/public/plugin/hooks.go
Обычный файл
339
server/public/plugin/hooks.go
Обычный файл
@@ -0,0 +1,339 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
// These assignments are part of the wire protocol used to trigger hook events in plugins.
|
||||
//
|
||||
// Feel free to add more, but do not change existing assignments. Follow the naming convention of
|
||||
// <HookName>ID as the autogenerated glue code depends on that.
|
||||
const (
|
||||
OnActivateID = 0
|
||||
OnDeactivateID = 1
|
||||
ServeHTTPID = 2
|
||||
OnConfigurationChangeID = 3
|
||||
ExecuteCommandID = 4
|
||||
MessageWillBePostedID = 5
|
||||
MessageWillBeUpdatedID = 6
|
||||
MessageHasBeenPostedID = 7
|
||||
MessageHasBeenUpdatedID = 8
|
||||
UserHasJoinedChannelID = 9
|
||||
UserHasLeftChannelID = 10
|
||||
UserHasJoinedTeamID = 11
|
||||
UserHasLeftTeamID = 12
|
||||
ChannelHasBeenCreatedID = 13
|
||||
FileWillBeUploadedID = 14
|
||||
UserWillLogInID = 15
|
||||
UserHasLoggedInID = 16
|
||||
UserHasBeenCreatedID = 17
|
||||
ReactionHasBeenAddedID = 18
|
||||
ReactionHasBeenRemovedID = 19
|
||||
OnPluginClusterEventID = 20
|
||||
OnWebSocketConnectID = 21
|
||||
OnWebSocketDisconnectID = 22
|
||||
WebSocketMessageHasBeenPostedID = 23
|
||||
RunDataRetentionID = 24
|
||||
OnInstallID = 25
|
||||
OnSendDailyTelemetryID = 26
|
||||
OnCloudLimitsUpdatedID = 27
|
||||
UserHasPermissionToCollectionID = 28
|
||||
GetAllUserIdsForCollectionID = 29
|
||||
GetAllCollectionIDsForUserID = 30
|
||||
GetTopicRedirectID = 31
|
||||
GetCollectionMetadataByIdsID = 32
|
||||
GetTopicMetadataByIdsID = 33
|
||||
TotalHooksID = iota
|
||||
)
|
||||
|
||||
const (
|
||||
// DismissPostError dismisses a pending post when the error is returned from MessageWillBePosted.
|
||||
DismissPostError = "plugin.message_will_be_posted.dismiss_post"
|
||||
)
|
||||
|
||||
// Hooks describes the methods a plugin may implement to automatically receive the corresponding
|
||||
// event.
|
||||
//
|
||||
// A plugin only need implement the hooks it cares about. The MattermostPlugin provides some
|
||||
// default implementations for convenience but may be overridden.
|
||||
type Hooks interface {
|
||||
// OnActivate is invoked when the plugin is activated. If an error is returned, the plugin
|
||||
// will be terminated. The plugin will not receive hooks until after OnActivate returns
|
||||
// without error. OnConfigurationChange will be called once before OnActivate.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
OnActivate() error
|
||||
|
||||
// Implemented returns a list of hooks that are implemented by the plugin.
|
||||
// Plugins do not need to provide an implementation. Any given will be ignored.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
Implemented() ([]string, error)
|
||||
|
||||
// OnDeactivate is invoked when the plugin is deactivated. This is the plugin's last chance to
|
||||
// use the API, and the plugin will be terminated shortly after this invocation. The plugin
|
||||
// will stop receiving hooks just prior to this method being called.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
OnDeactivate() error
|
||||
|
||||
// OnConfigurationChange is invoked when configuration changes may have been made. Any
|
||||
// returned error is logged, but does not stop the plugin. You must be prepared to handle
|
||||
// a configuration failure gracefully. It is called once before OnActivate.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
OnConfigurationChange() error
|
||||
|
||||
// ServeHTTP allows the plugin to implement the http.Handler interface. Requests destined for
|
||||
// the /plugins/{id} path will be routed to the plugin.
|
||||
//
|
||||
// The Mattermost-User-Id header will be present if (and only if) the request is by an
|
||||
// authenticated user.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// ExecuteCommand executes a command that has been previously registered via the RegisterCommand
|
||||
// API.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
|
||||
|
||||
// UserHasBeenCreated is invoked after a user was created.
|
||||
//
|
||||
// Minimum server version: 5.10
|
||||
UserHasBeenCreated(c *Context, user *model.User)
|
||||
|
||||
// UserWillLogIn before the login of the user is returned. Returning a non empty string will reject the login event.
|
||||
// If you don't need to reject the login event, see UserHasLoggedIn
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
UserWillLogIn(c *Context, user *model.User) string
|
||||
|
||||
// UserHasLoggedIn is invoked after a user has logged in.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
UserHasLoggedIn(c *Context, user *model.User)
|
||||
|
||||
// MessageWillBePosted is invoked when a message is posted by a user before it is committed
|
||||
// to the database. If you also want to act on edited posts, see MessageWillBeUpdated.
|
||||
//
|
||||
// To reject a post, return an non-empty string describing why the post was rejected.
|
||||
// To modify the post, return the replacement, non-nil *model.Post and an empty string.
|
||||
// To allow the post without modification, return a nil *model.Post and an empty string.
|
||||
// To dismiss the post, return a nil *model.Post and the const DismissPostError string.
|
||||
//
|
||||
// If you don't need to modify or reject posts, use MessageHasBeenPosted instead.
|
||||
//
|
||||
// Note that this method will be called for posts created by plugins, including the plugin that
|
||||
// created the post.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
|
||||
|
||||
// MessageWillBeUpdated is invoked when a message is updated by a user before it is committed
|
||||
// to the database. If you also want to act on new posts, see MessageWillBePosted.
|
||||
// Return values should be the modified post or nil if rejected and an explanation for the user.
|
||||
// On rejection, the post will be kept in its previous state.
|
||||
//
|
||||
// If you don't need to modify or rejected updated posts, use MessageHasBeenUpdated instead.
|
||||
//
|
||||
// Note that this method will be called for posts updated by plugins, including the plugin that
|
||||
// updated the post.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
|
||||
|
||||
// MessageHasBeenPosted is invoked after the message has been committed to the database.
|
||||
// If you need to modify or reject the post, see MessageWillBePosted
|
||||
// Note that this method will be called for posts created by plugins, including the plugin that
|
||||
// created the post.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
MessageHasBeenPosted(c *Context, post *model.Post)
|
||||
|
||||
// MessageHasBeenUpdated is invoked after a message is updated and has been updated in the database.
|
||||
// If you need to modify or reject the post, see MessageWillBeUpdated
|
||||
// Note that this method will be called for posts created by plugins, including the plugin that
|
||||
// created the post.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post)
|
||||
|
||||
// ChannelHasBeenCreated is invoked after the channel has been committed to the database.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
ChannelHasBeenCreated(c *Context, channel *model.Channel)
|
||||
|
||||
// UserHasJoinedChannel is invoked after the membership has been committed to the database.
|
||||
// If actor is not nil, the user was invited to the channel by the actor.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
|
||||
|
||||
// UserHasLeftChannel is invoked after the membership has been removed from the database.
|
||||
// If actor is not nil, the user was removed from the channel by the actor.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
|
||||
|
||||
// UserHasJoinedTeam is invoked after the membership has been committed to the database.
|
||||
// If actor is not nil, the user was added to the team by the actor.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
|
||||
|
||||
// UserHasLeftTeam is invoked after the membership has been removed from the database.
|
||||
// If actor is not nil, the user was removed from the team by the actor.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
|
||||
|
||||
// FileWillBeUploaded is invoked when a file is uploaded, but before it is committed to backing store.
|
||||
// Read from file to retrieve the body of the uploaded file.
|
||||
//
|
||||
// To reject a file upload, return an non-empty string describing why the file was rejected.
|
||||
// To modify the file, write to the output and/or return a non-nil *model.FileInfo, as well as an empty string.
|
||||
// To allow the file without modification, do not write to the output and return a nil *model.FileInfo and an empty string.
|
||||
//
|
||||
// Note that this method will be called for files uploaded by plugins, including the plugin that uploaded the post.
|
||||
// FileInfo.Size will be automatically set properly if you modify the file.
|
||||
//
|
||||
// Minimum server version: 5.2
|
||||
FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string)
|
||||
|
||||
// ReactionHasBeenAdded is invoked after the reaction has been committed to the database.
|
||||
//
|
||||
// Note that this method will be called for reactions added by plugins, including the plugin that
|
||||
// added the reaction.
|
||||
//
|
||||
// Minimum server version: 5.30
|
||||
ReactionHasBeenAdded(c *Context, reaction *model.Reaction)
|
||||
|
||||
// ReactionHasBeenRemoved is invoked after the removal of the reaction has been committed to the database.
|
||||
//
|
||||
// Note that this method will be called for reactions removed by plugins, including the plugin that
|
||||
// removed the reaction.
|
||||
//
|
||||
// Minimum server version: 5.30
|
||||
ReactionHasBeenRemoved(c *Context, reaction *model.Reaction)
|
||||
|
||||
// OnPluginClusterEvent is invoked when an intra-cluster plugin event is received.
|
||||
//
|
||||
// This is used to allow communication between multiple instances of the same plugin
|
||||
// that are running on separate nodes of the same High-Availability cluster.
|
||||
// This hook receives events sent by a call to PublishPluginClusterEvent.
|
||||
//
|
||||
// Minimum server version: 5.36
|
||||
OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent)
|
||||
|
||||
// OnWebSocketConnect is invoked when a new websocket connection is opened.
|
||||
//
|
||||
// This is used to track which users have connections opened with the Mattermost
|
||||
// websocket.
|
||||
//
|
||||
// Minimum server version: 6.0
|
||||
OnWebSocketConnect(webConnID, userID string)
|
||||
|
||||
// OnWebSocketDisconnect is invoked when a websocket connection is closed.
|
||||
//
|
||||
// This is used to track which users have connections opened with the Mattermost
|
||||
// websocket.
|
||||
//
|
||||
// Minimum server version: 6.0
|
||||
OnWebSocketDisconnect(webConnID, userID string)
|
||||
|
||||
// WebSocketMessageHasBeenPosted is invoked when a websocket message is received.
|
||||
//
|
||||
// Minimum server version: 6.0
|
||||
WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest)
|
||||
|
||||
// RunDataRetention is invoked during a DataRetentionJob.
|
||||
//
|
||||
// Minimum server version: 6.4
|
||||
RunDataRetention(nowTime, batchSize int64) (int64, error)
|
||||
|
||||
// OnInstall is invoked after the installation of a plugin as part of the onboarding.
|
||||
// It's called on every installation, not only once.
|
||||
//
|
||||
// In the future, other plugin installation methods will trigger this hook, e.g. an installation via the Marketplace.
|
||||
//
|
||||
// Minimum server version: 6.5
|
||||
OnInstall(c *Context, event model.OnInstallEvent) error
|
||||
|
||||
// OnSendDailyTelemetry is invoked when the server send the daily telemetry data.
|
||||
//
|
||||
// Minimum server version: 6.5
|
||||
OnSendDailyTelemetry()
|
||||
|
||||
// OnCloudLimitsUpdated is invoked product limits change, for example when plan tiers change
|
||||
//
|
||||
// Minimum server version: 7.0
|
||||
OnCloudLimitsUpdated(limits *model.ProductLimits)
|
||||
|
||||
// UserHasPermissionToCollection determines if the given user has access to
|
||||
// the given collection. Plugins are only expected to handle their own
|
||||
// collections, and should return an error for unknown collections.
|
||||
//
|
||||
// For Threads Everywhere, products are expected to support at least the
|
||||
// following permissions: create_post, edit_post, delete_post,
|
||||
// edit_others_posts, and delete_others_posts.
|
||||
//
|
||||
// EXPERIMENTAL: This hook is experimental and can be changed without advance notice.
|
||||
//
|
||||
// Minimum server version: 7.6
|
||||
UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error)
|
||||
|
||||
// GetAllCollectionIDsForUser returns the set of collection ids to which
|
||||
// the given user has access. Plugins are only expected to handle their
|
||||
// own collections, and should return an error unknown types.
|
||||
//
|
||||
// EXPERIMENTAL: This hook is experimental and can be changed without advance notice.
|
||||
//
|
||||
// Minimum server version: 7.6
|
||||
GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error)
|
||||
|
||||
// GetAllCollectionIDsForUser returns the set of collection ids to which
|
||||
// the given user has access. Plugins are only expected to handle their
|
||||
// own collections, and should return an error for unknown types.
|
||||
//
|
||||
// EXPERIMENTAL: This hook is experimental and can be changed without advance notice.
|
||||
//
|
||||
// Minimum server version: 7.6
|
||||
GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error)
|
||||
|
||||
// GetTopicRedirect returns a relative URL to which to redirect a user
|
||||
// following a topic permalink. Plugins are only expected to handle their
|
||||
// own topic types, and should return an empty string and error for unknown // types.
|
||||
//
|
||||
// EXPERIMENTAL: This hook is experimental and can be changed without advance notice.
|
||||
//
|
||||
// Minimum server version: 7.6
|
||||
GetTopicRedirect(c *Context, topicType, topicID string) (string, error)
|
||||
|
||||
// GetCollectionMetadataByIds returns collection metadata for the passed ids.
|
||||
// Returned type is a map with keys of collectionId and value CollectionMetadata.
|
||||
// Plugins are only expected to handle their own collections, and should
|
||||
// return an error for unknown types.
|
||||
//
|
||||
// EXPERIMENTAL: This hook is experimental and can be changed without advance notice.
|
||||
//
|
||||
// Minimum server version: 7.6
|
||||
GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error)
|
||||
|
||||
// GetTopicMetadataByIds returns topic metadata for the passed ids.
|
||||
// Returned type is a map with keys of topicId and value CollectionMetadata.
|
||||
// Plugins are only expected to handle their own topics, and should
|
||||
// return an error for unknown types.
|
||||
//
|
||||
// EXPERIMENTAL: This hook is experimental and can be changed without advance notice.
|
||||
//
|
||||
// Minimum server version: 7.6
|
||||
GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error)
|
||||
}
|
||||
255
server/public/plugin/hooks_timer_layer_generated.go
Обычный файл
255
server/public/plugin/hooks_timer_layer_generated.go
Обычный файл
@@ -0,0 +1,255 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make pluginapi"
|
||||
// DO NOT EDIT
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
timePkg "time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
type hooksTimerLayer struct {
|
||||
pluginID string
|
||||
hooksImpl Hooks
|
||||
metrics metricsInterface
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
|
||||
if hooks.metrics != nil {
|
||||
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
|
||||
hooks.metrics.ObservePluginHookDuration(hooks.pluginID, name, success, elapsedTime)
|
||||
}
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnActivate() error {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA := hooks.hooksImpl.OnActivate()
|
||||
hooks.recordTime(startTime, "OnActivate", _returnsA == nil)
|
||||
return _returnsA
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) Implemented() ([]string, error) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.Implemented()
|
||||
hooks.recordTime(startTime, "Implemented", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnDeactivate() error {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA := hooks.hooksImpl.OnDeactivate()
|
||||
hooks.recordTime(startTime, "OnDeactivate", _returnsA == nil)
|
||||
return _returnsA
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnConfigurationChange() error {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA := hooks.hooksImpl.OnConfigurationChange()
|
||||
hooks.recordTime(startTime, "OnConfigurationChange", _returnsA == nil)
|
||||
return _returnsA
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.ServeHTTP(c, w, r)
|
||||
hooks.recordTime(startTime, "ServeHTTP", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.ExecuteCommand(c, args)
|
||||
hooks.recordTime(startTime, "ExecuteCommand", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) UserHasBeenCreated(c *Context, user *model.User) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.UserHasBeenCreated(c, user)
|
||||
hooks.recordTime(startTime, "UserHasBeenCreated", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) UserWillLogIn(c *Context, user *model.User) string {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA := hooks.hooksImpl.UserWillLogIn(c, user)
|
||||
hooks.recordTime(startTime, "UserWillLogIn", true)
|
||||
return _returnsA
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) UserHasLoggedIn(c *Context, user *model.User) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.UserHasLoggedIn(c, user)
|
||||
hooks.recordTime(startTime, "UserHasLoggedIn", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.MessageWillBePosted(c, post)
|
||||
hooks.recordTime(startTime, "MessageWillBePosted", true)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.MessageWillBeUpdated(c, newPost, oldPost)
|
||||
hooks.recordTime(startTime, "MessageWillBeUpdated", true)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) MessageHasBeenPosted(c *Context, post *model.Post) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.MessageHasBeenPosted(c, post)
|
||||
hooks.recordTime(startTime, "MessageHasBeenPosted", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.MessageHasBeenUpdated(c, newPost, oldPost)
|
||||
hooks.recordTime(startTime, "MessageHasBeenUpdated", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) ChannelHasBeenCreated(c *Context, channel *model.Channel) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.ChannelHasBeenCreated(c, channel)
|
||||
hooks.recordTime(startTime, "ChannelHasBeenCreated", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.UserHasJoinedChannel(c, channelMember, actor)
|
||||
hooks.recordTime(startTime, "UserHasJoinedChannel", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.UserHasLeftChannel(c, channelMember, actor)
|
||||
hooks.recordTime(startTime, "UserHasLeftChannel", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.UserHasJoinedTeam(c, teamMember, actor)
|
||||
hooks.recordTime(startTime, "UserHasJoinedTeam", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.UserHasLeftTeam(c, teamMember, actor)
|
||||
hooks.recordTime(startTime, "UserHasLeftTeam", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.FileWillBeUploaded(c, info, file, output)
|
||||
hooks.recordTime(startTime, "FileWillBeUploaded", true)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.ReactionHasBeenAdded(c, reaction)
|
||||
hooks.recordTime(startTime, "ReactionHasBeenAdded", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.ReactionHasBeenRemoved(c, reaction)
|
||||
hooks.recordTime(startTime, "ReactionHasBeenRemoved", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.OnPluginClusterEvent(c, ev)
|
||||
hooks.recordTime(startTime, "OnPluginClusterEvent", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnWebSocketConnect(webConnID, userID string) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.OnWebSocketConnect(webConnID, userID)
|
||||
hooks.recordTime(startTime, "OnWebSocketConnect", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnWebSocketDisconnect(webConnID, userID string) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.OnWebSocketDisconnect(webConnID, userID)
|
||||
hooks.recordTime(startTime, "OnWebSocketDisconnect", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.WebSocketMessageHasBeenPosted(webConnID, userID, req)
|
||||
hooks.recordTime(startTime, "WebSocketMessageHasBeenPosted", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) RunDataRetention(nowTime, batchSize int64) (int64, error) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.RunDataRetention(nowTime, batchSize)
|
||||
hooks.recordTime(startTime, "RunDataRetention", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnInstall(c *Context, event model.OnInstallEvent) error {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA := hooks.hooksImpl.OnInstall(c, event)
|
||||
hooks.recordTime(startTime, "OnInstall", _returnsA == nil)
|
||||
return _returnsA
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnSendDailyTelemetry() {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.OnSendDailyTelemetry()
|
||||
hooks.recordTime(startTime, "OnSendDailyTelemetry", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) OnCloudLimitsUpdated(limits *model.ProductLimits) {
|
||||
startTime := timePkg.Now()
|
||||
hooks.hooksImpl.OnCloudLimitsUpdated(limits)
|
||||
hooks.recordTime(startTime, "OnCloudLimitsUpdated", true)
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.UserHasPermissionToCollection(c, userID, collectionType, collectionId, permission)
|
||||
hooks.recordTime(startTime, "UserHasPermissionToCollection", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.GetAllCollectionIDsForUser(c, userID, collectionType)
|
||||
hooks.recordTime(startTime, "GetAllCollectionIDsForUser", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.GetAllUserIdsForCollection(c, collectionType, collectionID)
|
||||
hooks.recordTime(startTime, "GetAllUserIdsForCollection", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.GetTopicRedirect(c, topicType, topicID)
|
||||
hooks.recordTime(startTime, "GetTopicRedirect", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.GetCollectionMetadataByIds(c, collectionType, collectionIds)
|
||||
hooks.recordTime(startTime, "GetCollectionMetadataByIds", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) {
|
||||
startTime := timePkg.Now()
|
||||
_returnsA, _returnsB := hooks.hooksImpl.GetTopicMetadataByIds(c, topicType, topicIds)
|
||||
hooks.recordTime(startTime, "GetTopicMetadataByIds", _returnsB == nil)
|
||||
return _returnsA, _returnsB
|
||||
}
|
||||
103
server/public/plugin/http.go
Обычный файл
103
server/public/plugin/http.go
Обычный файл
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type hijackedResponse struct {
|
||||
conn net.Conn
|
||||
bufrw *bufio.ReadWriter
|
||||
readBuf []byte
|
||||
}
|
||||
|
||||
type httpResponseWriterRPCServer struct {
|
||||
w http.ResponseWriter
|
||||
log *mlog.Logger
|
||||
hjr *hijackedResponse
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) Header(args struct{}, reply *http.Header) error {
|
||||
*reply = w.w.Header()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) Write(args []byte, reply *struct{}) error {
|
||||
_, err := w.w.Write(args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) WriteHeader(args int, reply *struct{}) error {
|
||||
// Check if args is a valid http status code. This prevents plugins from crashing the server with a panic.
|
||||
// This is a copy of the checkWriteHeaderCode function in net/http/server.go in the go source.
|
||||
if args < 100 || args > 999 {
|
||||
w.log.Error(fmt.Sprintf("Plugin tried to write an invalid http status code: %v. Did not write the invalid header.", args))
|
||||
return errors.New("invalid http status code")
|
||||
}
|
||||
w.w.WriteHeader(args)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCServer) SyncHeader(args http.Header, reply *struct{}) error {
|
||||
dest := w.w.Header()
|
||||
for k := range dest {
|
||||
if _, ok := args[k]; !ok {
|
||||
delete(dest, k)
|
||||
}
|
||||
}
|
||||
for k, v := range args {
|
||||
dest[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type httpResponseWriterRPCClient struct {
|
||||
client *rpc.Client
|
||||
header http.Header
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = (*httpResponseWriterRPCClient)(nil)
|
||||
|
||||
func (w *httpResponseWriterRPCClient) Header() http.Header {
|
||||
if w.header == nil {
|
||||
w.client.Call("Plugin.Header", struct{}{}, &w.header)
|
||||
}
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCClient) Write(b []byte) (int, error) {
|
||||
if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := w.client.Call("Plugin.Write", b, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCClient) WriteHeader(statusCode int) {
|
||||
if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
|
||||
return
|
||||
}
|
||||
w.client.Call("Plugin.WriteHeader", statusCode, nil)
|
||||
}
|
||||
|
||||
func (w *httpResponseWriterRPCClient) Close() error {
|
||||
return w.client.Close()
|
||||
}
|
||||
|
||||
func connectHTTPResponseWriter(conn io.ReadWriteCloser) *httpResponseWriterRPCClient {
|
||||
return &httpResponseWriterRPCClient{
|
||||
client: rpc.NewClient(conn),
|
||||
}
|
||||
}
|
||||
738
server/public/plugin/interface_generator/main.go
Обычный файл
738
server/public/plugin/interface_generator/main.go
Обычный файл
@@ -0,0 +1,738 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/tools/imports"
|
||||
)
|
||||
|
||||
var excludedPluginHooks = []string{
|
||||
"FileWillBeUploaded",
|
||||
"Implemented",
|
||||
"LoadPluginConfiguration",
|
||||
"InstallPlugin",
|
||||
"LogDebug",
|
||||
"LogError",
|
||||
"LogInfo",
|
||||
"LogWarn",
|
||||
"MessageWillBePosted",
|
||||
"MessageWillBeUpdated",
|
||||
"OnActivate",
|
||||
"PluginHTTP",
|
||||
"ServeHTTP",
|
||||
"UploadData",
|
||||
}
|
||||
|
||||
var excludedProductHooks = []string{
|
||||
"Implemented",
|
||||
"OnActivate",
|
||||
"OnDeactivate",
|
||||
"ServeHTTP",
|
||||
}
|
||||
|
||||
type IHookEntry struct {
|
||||
FuncName string
|
||||
Args *ast.FieldList
|
||||
Results *ast.FieldList
|
||||
}
|
||||
|
||||
type PluginInterfaceInfo struct {
|
||||
Hooks []IHookEntry
|
||||
API []IHookEntry
|
||||
FileSet *token.FileSet
|
||||
}
|
||||
|
||||
func FieldListToFuncList(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
result := []string{}
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return "()"
|
||||
}
|
||||
for _, field := range fieldList.List {
|
||||
typeNameBuffer := &bytes.Buffer{}
|
||||
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
typeName := typeNameBuffer.String()
|
||||
names := []string{}
|
||||
for _, name := range field.Names {
|
||||
names = append(names, name.Name)
|
||||
}
|
||||
result = append(result, strings.Join(names, ", ")+" "+typeName)
|
||||
}
|
||||
|
||||
return "(" + strings.Join(result, ", ") + ")"
|
||||
}
|
||||
|
||||
func FieldListToNames(fieldList *ast.FieldList, variadicForm bool) string {
|
||||
result := []string{}
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, field := range fieldList.List {
|
||||
for _, name := range field.Names {
|
||||
paramName := name.Name
|
||||
if _, ok := field.Type.(*ast.Ellipsis); ok && variadicForm {
|
||||
paramName = fmt.Sprintf("%s...", paramName)
|
||||
}
|
||||
result = append(result, paramName)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func FieldListToEncodedErrors(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
result := []string{}
|
||||
if fieldList == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
nextLetter := 'A'
|
||||
for _, field := range fieldList.List {
|
||||
typeNameBuffer := &bytes.Buffer{}
|
||||
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if typeNameBuffer.String() != "error" {
|
||||
nextLetter++
|
||||
continue
|
||||
}
|
||||
|
||||
name := ""
|
||||
if len(field.Names) == 0 {
|
||||
name = string(nextLetter)
|
||||
nextLetter++
|
||||
} else {
|
||||
for range field.Names {
|
||||
name += string(nextLetter)
|
||||
nextLetter++
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, structPrefix+name+" = encodableError("+structPrefix+name+")")
|
||||
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
result := []string{}
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
nextLetter := 'A'
|
||||
for _, field := range fieldList.List {
|
||||
typeNameBuffer := &bytes.Buffer{}
|
||||
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
typeName := typeNameBuffer.String()
|
||||
suffix := ""
|
||||
if strings.HasPrefix(typeName, "...") {
|
||||
suffix = "..."
|
||||
}
|
||||
if len(field.Names) == 0 {
|
||||
result = append(result, structPrefix+string(nextLetter)+suffix)
|
||||
nextLetter++
|
||||
} else {
|
||||
for range field.Names {
|
||||
result = append(result, structPrefix+string(nextLetter)+suffix)
|
||||
nextLetter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func FieldListToRecordSuccess(structPrefix string, fieldList *ast.FieldList) string {
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return "true"
|
||||
}
|
||||
|
||||
result := ""
|
||||
nextLetter := 'A'
|
||||
for _, field := range fieldList.List {
|
||||
typeName := baseTypeName(field.Type)
|
||||
if typeName == "error" || typeName == "AppError" {
|
||||
result = structPrefix + string(nextLetter)
|
||||
break
|
||||
}
|
||||
nextLetter++
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
return "true"
|
||||
}
|
||||
return fmt.Sprintf("%s == nil", result)
|
||||
}
|
||||
|
||||
func FieldListToStructList(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
result := []string{}
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
nextLetter := 'A'
|
||||
for _, field := range fieldList.List {
|
||||
typeNameBuffer := &bytes.Buffer{}
|
||||
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
typeName := typeNameBuffer.String()
|
||||
if strings.HasPrefix(typeName, "...") {
|
||||
typeName = strings.Replace(typeName, "...", "[]", 1)
|
||||
}
|
||||
if len(field.Names) == 0 {
|
||||
result = append(result, string(nextLetter)+" "+typeName)
|
||||
nextLetter++
|
||||
} else {
|
||||
for range field.Names {
|
||||
result = append(result, string(nextLetter)+" "+typeName)
|
||||
nextLetter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n\t")
|
||||
}
|
||||
|
||||
func baseTypeName(x ast.Expr) string {
|
||||
switch t := x.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.SelectorExpr:
|
||||
if _, ok := t.X.(*ast.Ident); ok {
|
||||
// only possible for qualified type names;
|
||||
// assume type is imported
|
||||
return t.Sel.Name
|
||||
}
|
||||
case *ast.ParenExpr:
|
||||
return baseTypeName(t.X)
|
||||
case *ast.StarExpr:
|
||||
return baseTypeName(t.X)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func goList(dir string) ([]string, error) {
|
||||
cmd := exec.Command("go", "list", "-f", "{{.Dir}}", dir)
|
||||
bytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Can't list packages")
|
||||
}
|
||||
|
||||
return strings.Fields(string(bytes)), nil
|
||||
}
|
||||
|
||||
func (info *PluginInterfaceInfo) addHookMethod(method *ast.Field) {
|
||||
info.Hooks = append(info.Hooks, IHookEntry{
|
||||
FuncName: method.Names[0].Name,
|
||||
Args: method.Type.(*ast.FuncType).Params,
|
||||
Results: method.Type.(*ast.FuncType).Results,
|
||||
})
|
||||
}
|
||||
|
||||
func (info *PluginInterfaceInfo) addAPIMethod(method *ast.Field) {
|
||||
info.API = append(info.API, IHookEntry{
|
||||
FuncName: method.Names[0].Name,
|
||||
Args: method.Type.(*ast.FuncType).Params,
|
||||
Results: method.Type.(*ast.FuncType).Results,
|
||||
})
|
||||
}
|
||||
|
||||
func (info *PluginInterfaceInfo) makeHookInspector() func(node ast.Node) bool {
|
||||
return func(node ast.Node) bool {
|
||||
if typeSpec, ok := node.(*ast.TypeSpec); ok {
|
||||
if typeSpec.Name.Name == "Hooks" {
|
||||
for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
|
||||
info.addHookMethod(method)
|
||||
}
|
||||
return false
|
||||
} else if typeSpec.Name.Name == "API" {
|
||||
for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
|
||||
info.addAPIMethod(method)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginInfo(dir string) (*PluginInterfaceInfo, error) {
|
||||
pluginInfo := &PluginInterfaceInfo{
|
||||
Hooks: make([]IHookEntry, 0),
|
||||
FileSet: token.NewFileSet(),
|
||||
}
|
||||
|
||||
packages, err := parser.ParseDir(pluginInfo.FileSet, dir, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
log.Println("Parser error in dir "+dir+": ", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg.Name != "plugin" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range pkg.Files {
|
||||
ast.Inspect(file, pluginInfo.makeHookInspector())
|
||||
}
|
||||
}
|
||||
|
||||
return pluginInfo, nil
|
||||
}
|
||||
|
||||
var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make pluginapi"
|
||||
// DO NOT EDIT
|
||||
|
||||
package plugin
|
||||
|
||||
{{range .HooksMethods}}
|
||||
|
||||
func init() {
|
||||
hookNameToId["{{.Name}}"] = {{.Name}}ID
|
||||
}
|
||||
|
||||
type {{.Name | obscure}}Args struct {
|
||||
{{structStyle .Params}}
|
||||
}
|
||||
|
||||
type {{.Name | obscure}}Returns struct {
|
||||
{{structStyle .Return}}
|
||||
}
|
||||
|
||||
func (g *hooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
||||
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
|
||||
_returns := &{{.Name | obscure}}Returns{}
|
||||
if g.implemented[{{.Name}}ID] {
|
||||
if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
{{ if .Return }} return {{destruct "_returns." .Return}} {{ end }}
|
||||
}
|
||||
|
||||
func (s *hooksRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
|
||||
}); ok {
|
||||
{{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
|
||||
{{if .Return}}{{encodeErrors "returns." .Return}}{{end -}}
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("Hook {{.Name}} called but not implemented."))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{range .APIMethods}}
|
||||
|
||||
type {{.Name | obscure}}Args struct {
|
||||
{{structStyle .Params}}
|
||||
}
|
||||
|
||||
type {{.Name | obscure}}Returns struct {
|
||||
{{structStyle .Return}}
|
||||
}
|
||||
|
||||
func (g *apiRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
||||
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
|
||||
_returns := &{{.Name | obscure}}Returns{}
|
||||
if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
|
||||
log.Printf("RPC call to {{.Name}} API failed: %s", err.Error())
|
||||
}
|
||||
{{ if .Return }} return {{destruct "_returns." .Return}} {{ end }}
|
||||
}
|
||||
|
||||
func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}Returns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
|
||||
}); ok {
|
||||
{{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
|
||||
{{if .Return}}{{encodeErrors "returns." .Return}}{{end -}}
|
||||
} else {
|
||||
return encodableError(fmt.Errorf("API {{.Name}} called but not implemented."))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
var productHooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make pluginapi"
|
||||
// DO NOT EDIT
|
||||
|
||||
package plugin
|
||||
|
||||
{{range .HooksMethods}}
|
||||
type {{.Name}}IFace interface {
|
||||
{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
type HooksAdapter struct {
|
||||
implemented map[int]struct{}
|
||||
productHooks any
|
||||
}
|
||||
|
||||
func NewAdapter(productHooks any) (*HooksAdapter, error) {
|
||||
a := &HooksAdapter{
|
||||
implemented: make(map[int]struct{}),
|
||||
productHooks: productHooks,
|
||||
}
|
||||
var tt reflect.Type
|
||||
ft := reflect.TypeOf(productHooks)
|
||||
{{range .HooksMethods}}
|
||||
// Assessing the type of the productHooks if it individually implements {{.Name}} interface.
|
||||
tt = reflect.TypeOf((*{{.Name}}IFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[{{.Name}}ID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("{{.Name}}"); ok{
|
||||
return nil, errors.New("hook has {{.Name}} method but does not implement plugin.{{.Name}} interface")
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
{{range .HooksMethods}}
|
||||
func (a *HooksAdapter) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
||||
if _, ok := a.implemented[{{.Name}}ID]; !ok {
|
||||
panic("product hooks must implement {{.Name}}")
|
||||
}
|
||||
|
||||
{{if .Return}}return a.productHooks.({{.Name}}IFace).{{.Name}}({{valuesOnly .Params}}){{else}}a.productHooks.({{.Name}}IFace).{{.Name}}({{valuesOnly .Params}}){{end}}
|
||||
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
`
|
||||
|
||||
var apiTimerLayerTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make pluginapi"
|
||||
// DO NOT EDIT
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
timePkg "time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
type apiTimerLayer struct {
|
||||
pluginID string
|
||||
apiImpl API
|
||||
metrics metricsInterface
|
||||
}
|
||||
|
||||
func (api *apiTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
|
||||
if api.metrics != nil {
|
||||
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
|
||||
api.metrics.ObservePluginAPIDuration(api.pluginID, name, success, elapsedTime)
|
||||
}
|
||||
}
|
||||
|
||||
{{range .APIMethods}}
|
||||
|
||||
func (api *apiTimerLayer) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
||||
startTime := timePkg.Now()
|
||||
{{ if .Return }} {{destruct "_returns" .Return}} := {{ end }} api.apiImpl.{{.Name}}({{valuesOnly .Params}})
|
||||
api.recordTime(startTime, "{{.Name}}", {{ shouldRecordSuccess "_returns" .Return }})
|
||||
{{ if .Return }} return {{destruct "_returns" .Return}} {{ end -}}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
`
|
||||
|
||||
var hooksTimerLayerTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make pluginapi"
|
||||
// DO NOT EDIT
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
timePkg "time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
type hooksTimerLayer struct {
|
||||
pluginID string
|
||||
hooksImpl Hooks
|
||||
metrics metricsInterface
|
||||
}
|
||||
|
||||
func (hooks *hooksTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
|
||||
if hooks.metrics != nil {
|
||||
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
|
||||
hooks.metrics.ObservePluginHookDuration(hooks.pluginID, name, success, elapsedTime)
|
||||
}
|
||||
}
|
||||
|
||||
{{range .HooksMethods}}
|
||||
|
||||
func (hooks *hooksTimerLayer) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
||||
startTime := timePkg.Now()
|
||||
{{ if .Return }} {{destruct "_returns" .Return}} := {{ end }} hooks.hooksImpl.{{.Name}}({{valuesOnly .Params}})
|
||||
hooks.recordTime(startTime, "{{.Name}}", {{ shouldRecordSuccess "_returns" .Return }})
|
||||
{{ if .Return }} return {{destruct "_returns" .Return}} {{end -}}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
`
|
||||
|
||||
type MethodParams struct {
|
||||
Name string
|
||||
Params *ast.FieldList
|
||||
Return *ast.FieldList
|
||||
}
|
||||
|
||||
type HooksTemplateParams struct {
|
||||
HooksMethods []MethodParams
|
||||
APIMethods []MethodParams
|
||||
}
|
||||
|
||||
func generateHooksGlue(info *PluginInterfaceInfo) {
|
||||
templateFunctions := map[string]any{
|
||||
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
|
||||
"structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
|
||||
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, false) },
|
||||
"encodeErrors": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListToEncodedErrors(structPrefix, fields, info.FileSet)
|
||||
},
|
||||
"destruct": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListDestruct(structPrefix, fields, info.FileSet)
|
||||
},
|
||||
"shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListToRecordSuccess(structPrefix, fields)
|
||||
},
|
||||
"obscure": func(name string) string {
|
||||
return "Z_" + name
|
||||
},
|
||||
}
|
||||
|
||||
hooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(hooksTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templateParams := HooksTemplateParams{}
|
||||
for _, hook := range info.Hooks {
|
||||
templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{
|
||||
Name: hook.FuncName,
|
||||
Params: hook.Args,
|
||||
Return: hook.Results,
|
||||
})
|
||||
}
|
||||
for _, api := range info.API {
|
||||
templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{
|
||||
Name: api.FuncName,
|
||||
Params: api.Args,
|
||||
Return: api.Results,
|
||||
})
|
||||
}
|
||||
templateResult := &bytes.Buffer{}
|
||||
hooksTemplate.Execute(templateResult, &templateParams)
|
||||
|
||||
formatted, err := imports.Process("", templateResult.Bytes(), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), formatted, 0664); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateProductHooksInterfaces(info *PluginInterfaceInfo) {
|
||||
templateFunctions := map[string]any{
|
||||
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
|
||||
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, false) },
|
||||
}
|
||||
|
||||
templateParams := HooksTemplateParams{}
|
||||
for _, hook := range info.Hooks {
|
||||
templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{
|
||||
Name: hook.FuncName,
|
||||
Params: hook.Args,
|
||||
Return: hook.Results,
|
||||
})
|
||||
}
|
||||
|
||||
productHooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(productHooksTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templateResult := &bytes.Buffer{}
|
||||
productHooksTemplate.Execute(templateResult, &templateParams)
|
||||
|
||||
formatted, err := imports.Process("", templateResult.Bytes(), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(getPluginPackageDir(), "product_hooks_generated.go"), formatted, 0664); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func generatePluginTimerLayer(info *PluginInterfaceInfo) {
|
||||
templateFunctions := map[string]any{
|
||||
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
|
||||
"structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
|
||||
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, true) },
|
||||
"destruct": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListDestruct(structPrefix, fields, info.FileSet)
|
||||
},
|
||||
"shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListToRecordSuccess(structPrefix, fields)
|
||||
},
|
||||
}
|
||||
|
||||
// Prepare template params
|
||||
templateParams := HooksTemplateParams{}
|
||||
for _, hook := range info.Hooks {
|
||||
templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{
|
||||
Name: hook.FuncName,
|
||||
Params: hook.Args,
|
||||
Return: hook.Results,
|
||||
})
|
||||
}
|
||||
for _, api := range info.API {
|
||||
templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{
|
||||
Name: api.FuncName,
|
||||
Params: api.Args,
|
||||
Return: api.Results,
|
||||
})
|
||||
}
|
||||
|
||||
pluginTemplates := map[string]string{
|
||||
"api_timer_layer_generated.go": apiTimerLayerTemplate,
|
||||
"hooks_timer_layer_generated.go": hooksTimerLayerTemplate,
|
||||
}
|
||||
|
||||
for fileName, presetTemplate := range pluginTemplates {
|
||||
parsedTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(presetTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templateResult := &bytes.Buffer{}
|
||||
parsedTemplate.Execute(templateResult, &templateParams)
|
||||
|
||||
formatted, err := imports.Process("", templateResult.Bytes(), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(getPluginPackageDir(), fileName), formatted, 0664); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginPackageDir() string {
|
||||
dirs, err := goList("github.com/mattermost/mattermost-server/server/public/plugin")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if len(dirs) != 1 {
|
||||
panic("More than one package dir, or no dirs!")
|
||||
}
|
||||
|
||||
return dirs[0]
|
||||
}
|
||||
|
||||
func removeExcluded(info *PluginInterfaceInfo, excluded []string) *PluginInterfaceInfo {
|
||||
newIface := &PluginInterfaceInfo{
|
||||
FileSet: info.FileSet,
|
||||
}
|
||||
toBeExcluded := func(item string) bool {
|
||||
for _, exclusion := range excluded {
|
||||
if exclusion == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
hooksResult := make([]IHookEntry, 0, len(info.Hooks))
|
||||
for _, hook := range info.Hooks {
|
||||
if !toBeExcluded(hook.FuncName) {
|
||||
hooksResult = append(hooksResult, hook)
|
||||
}
|
||||
}
|
||||
newIface.Hooks = hooksResult
|
||||
|
||||
apiResult := make([]IHookEntry, 0, len(info.API))
|
||||
for _, api := range info.API {
|
||||
if !toBeExcluded(api.FuncName) {
|
||||
apiResult = append(apiResult, api)
|
||||
}
|
||||
}
|
||||
newIface.API = apiResult
|
||||
|
||||
return newIface
|
||||
}
|
||||
|
||||
func main() {
|
||||
pluginPackageDir := getPluginPackageDir()
|
||||
|
||||
forRPC, err := getPluginInfo(pluginPackageDir)
|
||||
if err != nil {
|
||||
fmt.Println("Unable to get plugin info: " + err.Error())
|
||||
}
|
||||
log.Println("Generating product hooks interfaces")
|
||||
generateProductHooksInterfaces(removeExcluded(forRPC, excludedProductHooks))
|
||||
|
||||
log.Println("Generating plugin hooks glue")
|
||||
generateHooksGlue(removeExcluded(forRPC, excludedPluginHooks))
|
||||
|
||||
// Generate plugin timer layers
|
||||
log.Println("Generating plugin timer glue")
|
||||
forPlugins, err := getPluginInfo(pluginPackageDir)
|
||||
if err != nil {
|
||||
fmt.Println("Unable to get plugin info: " + err.Error())
|
||||
}
|
||||
generatePluginTimerLayer(forPlugins)
|
||||
}
|
||||
46
server/public/plugin/io_rpc.go
Обычный файл
46
server/public/plugin/io_rpc.go
Обычный файл
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
type remoteIOReader struct {
|
||||
conn io.ReadWriteCloser
|
||||
}
|
||||
|
||||
func (r *remoteIOReader) Read(b []byte) (int, error) {
|
||||
var buf [10]byte
|
||||
n := binary.PutVarint(buf[:], int64(len(b)))
|
||||
if _, err := r.conn.Write(buf[:n]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.conn.Read(b)
|
||||
}
|
||||
|
||||
func (r *remoteIOReader) Close() error {
|
||||
return r.conn.Close()
|
||||
}
|
||||
|
||||
func connectIOReader(conn io.ReadWriteCloser) io.ReadCloser {
|
||||
return &remoteIOReader{conn}
|
||||
}
|
||||
|
||||
func serveIOReader(r io.Reader, conn io.ReadWriteCloser) {
|
||||
cr := bufio.NewReader(conn)
|
||||
defer conn.Close()
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, err := binary.ReadVarint(cr)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if written, err := io.CopyBuffer(conn, io.LimitReader(r, n), buf); err != nil || written < n {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
11
server/public/plugin/metrics.go
Обычный файл
11
server/public/plugin/metrics.go
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
type metricsInterface interface {
|
||||
ObservePluginHookDuration(pluginID, hookName string, success bool, elapsed float64)
|
||||
ObservePluginMultiHookIterationDuration(pluginID string, elapsed float64)
|
||||
ObservePluginMultiHookDuration(elapsed float64)
|
||||
ObservePluginAPIDuration(pluginID, apiName string, success bool, elapsed float64)
|
||||
}
|
||||
4181
server/public/plugin/plugintest/api.go
Обычный файл
4181
server/public/plugin/plugintest/api.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
11
server/public/plugin/plugintest/doc.go
Обычный файл
11
server/public/plugin/plugintest/doc.go
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// The plugintest package provides mocks that can be used to test plugins.
|
||||
//
|
||||
// The mocks are created using testify's mock package:
|
||||
// https://godoc.org/github.com/stretchr/testify/mock
|
||||
//
|
||||
// If you need to import the mock package, you can import it with
|
||||
// "github.com/mattermost/mattermost-server/server/public/plugin/plugintest/mock".
|
||||
package plugintest
|
||||
402
server/public/plugin/plugintest/driver.go
Обычный файл
402
server/public/plugin/plugintest/driver.go
Обычный файл
@@ -0,0 +1,402 @@
|
||||
// Code generated by mockery v2.23.2. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make plugin-mocks`.
|
||||
|
||||
package plugintest
|
||||
|
||||
import (
|
||||
driver "database/sql/driver"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
plugin "github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
// Driver is an autogenerated mock type for the Driver type
|
||||
type Driver struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Conn provides a mock function with given fields: isMaster
|
||||
func (_m *Driver) Conn(isMaster bool) (string, error) {
|
||||
ret := _m.Called(isMaster)
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(bool) (string, error)); ok {
|
||||
return rf(isMaster)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(bool) string); ok {
|
||||
r0 = rf(isMaster)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(bool) error); ok {
|
||||
r1 = rf(isMaster)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConnClose provides a mock function with given fields: connID
|
||||
func (_m *Driver) ConnClose(connID string) error {
|
||||
ret := _m.Called(connID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(connID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConnExec provides a mock function with given fields: connID, q, args
|
||||
func (_m *Driver) ConnExec(connID string, q string, args []driver.NamedValue) (plugin.ResultContainer, error) {
|
||||
ret := _m.Called(connID, q, args)
|
||||
|
||||
var r0 plugin.ResultContainer
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, []driver.NamedValue) (plugin.ResultContainer, error)); ok {
|
||||
return rf(connID, q, args)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string, []driver.NamedValue) plugin.ResultContainer); ok {
|
||||
r0 = rf(connID, q, args)
|
||||
} else {
|
||||
r0 = ret.Get(0).(plugin.ResultContainer)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string, []driver.NamedValue) error); ok {
|
||||
r1 = rf(connID, q, args)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConnPing provides a mock function with given fields: connID
|
||||
func (_m *Driver) ConnPing(connID string) error {
|
||||
ret := _m.Called(connID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(connID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConnQuery provides a mock function with given fields: connID, q, args
|
||||
func (_m *Driver) ConnQuery(connID string, q string, args []driver.NamedValue) (string, error) {
|
||||
ret := _m.Called(connID, q, args)
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, []driver.NamedValue) (string, error)); ok {
|
||||
return rf(connID, q, args)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string, []driver.NamedValue) string); ok {
|
||||
r0 = rf(connID, q, args)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string, []driver.NamedValue) error); ok {
|
||||
r1 = rf(connID, q, args)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RowsClose provides a mock function with given fields: rowsID
|
||||
func (_m *Driver) RowsClose(rowsID string) error {
|
||||
ret := _m.Called(rowsID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(rowsID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RowsColumnTypeDatabaseTypeName provides a mock function with given fields: rowsID, index
|
||||
func (_m *Driver) RowsColumnTypeDatabaseTypeName(rowsID string, index int) string {
|
||||
ret := _m.Called(rowsID, index)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(string, int) string); ok {
|
||||
r0 = rf(rowsID, index)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RowsColumnTypePrecisionScale provides a mock function with given fields: rowsID, index
|
||||
func (_m *Driver) RowsColumnTypePrecisionScale(rowsID string, index int) (int64, int64, bool) {
|
||||
ret := _m.Called(rowsID, index)
|
||||
|
||||
var r0 int64
|
||||
var r1 int64
|
||||
var r2 bool
|
||||
if rf, ok := ret.Get(0).(func(string, int) (int64, int64, bool)); ok {
|
||||
return rf(rowsID, index)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, int) int64); ok {
|
||||
r0 = rf(rowsID, index)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, int) int64); ok {
|
||||
r1 = rf(rowsID, index)
|
||||
} else {
|
||||
r1 = ret.Get(1).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(string, int) bool); ok {
|
||||
r2 = rf(rowsID, index)
|
||||
} else {
|
||||
r2 = ret.Get(2).(bool)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// RowsColumns provides a mock function with given fields: rowsID
|
||||
func (_m *Driver) RowsColumns(rowsID string) []string {
|
||||
ret := _m.Called(rowsID)
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func(string) []string); ok {
|
||||
r0 = rf(rowsID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RowsHasNextResultSet provides a mock function with given fields: rowsID
|
||||
func (_m *Driver) RowsHasNextResultSet(rowsID string) bool {
|
||||
ret := _m.Called(rowsID)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = rf(rowsID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RowsNext provides a mock function with given fields: rowsID, dest
|
||||
func (_m *Driver) RowsNext(rowsID string, dest []driver.Value) error {
|
||||
ret := _m.Called(rowsID, dest)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, []driver.Value) error); ok {
|
||||
r0 = rf(rowsID, dest)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RowsNextResultSet provides a mock function with given fields: rowsID
|
||||
func (_m *Driver) RowsNextResultSet(rowsID string) error {
|
||||
ret := _m.Called(rowsID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(rowsID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Stmt provides a mock function with given fields: connID, q
|
||||
func (_m *Driver) Stmt(connID string, q string) (string, error) {
|
||||
ret := _m.Called(connID, q)
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) (string, error)); ok {
|
||||
return rf(connID, q)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string) string); ok {
|
||||
r0 = rf(connID, q)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(connID, q)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// StmtClose provides a mock function with given fields: stID
|
||||
func (_m *Driver) StmtClose(stID string) error {
|
||||
ret := _m.Called(stID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(stID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// StmtExec provides a mock function with given fields: stID, args
|
||||
func (_m *Driver) StmtExec(stID string, args []driver.NamedValue) (plugin.ResultContainer, error) {
|
||||
ret := _m.Called(stID, args)
|
||||
|
||||
var r0 plugin.ResultContainer
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, []driver.NamedValue) (plugin.ResultContainer, error)); ok {
|
||||
return rf(stID, args)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, []driver.NamedValue) plugin.ResultContainer); ok {
|
||||
r0 = rf(stID, args)
|
||||
} else {
|
||||
r0 = ret.Get(0).(plugin.ResultContainer)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, []driver.NamedValue) error); ok {
|
||||
r1 = rf(stID, args)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// StmtNumInput provides a mock function with given fields: stID
|
||||
func (_m *Driver) StmtNumInput(stID string) int {
|
||||
ret := _m.Called(stID)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(string) int); ok {
|
||||
r0 = rf(stID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// StmtQuery provides a mock function with given fields: stID, args
|
||||
func (_m *Driver) StmtQuery(stID string, args []driver.NamedValue) (string, error) {
|
||||
ret := _m.Called(stID, args)
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, []driver.NamedValue) (string, error)); ok {
|
||||
return rf(stID, args)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, []driver.NamedValue) string); ok {
|
||||
r0 = rf(stID, args)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, []driver.NamedValue) error); ok {
|
||||
r1 = rf(stID, args)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Tx provides a mock function with given fields: connID, opts
|
||||
func (_m *Driver) Tx(connID string, opts driver.TxOptions) (string, error) {
|
||||
ret := _m.Called(connID, opts)
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, driver.TxOptions) (string, error)); ok {
|
||||
return rf(connID, opts)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, driver.TxOptions) string); ok {
|
||||
r0 = rf(connID, opts)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, driver.TxOptions) error); ok {
|
||||
r1 = rf(connID, opts)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// TxCommit provides a mock function with given fields: txID
|
||||
func (_m *Driver) TxCommit(txID string) error {
|
||||
ret := _m.Called(txID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(txID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// TxRollback provides a mock function with given fields: txID
|
||||
func (_m *Driver) TxRollback(txID string) error {
|
||||
ret := _m.Called(txID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(txID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewDriver interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewDriver creates a new instance of Driver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewDriver(t mockConstructorTestingTNewDriver) *Driver {
|
||||
mock := &Driver{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
58
server/public/plugin/plugintest/example_hello_user_test.go
Обычный файл
58
server/public/plugin/plugintest/example_hello_user_test.go
Обычный файл
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugintest_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
io "io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/plugintest"
|
||||
)
|
||||
|
||||
type HelloUserPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
func (p *HelloUserPlugin) ServeHTTP(context *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Header.Get("Mattermost-User-Id")
|
||||
user, err := p.API.GetUser(userID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
p.API.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "Welcome back, %s!", user.Username)
|
||||
}
|
||||
|
||||
func Example() {
|
||||
t := &testing.T{}
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "billybob",
|
||||
}
|
||||
|
||||
api := &plugintest.API{}
|
||||
api.On("GetUser", user.Id).Return(user, nil)
|
||||
defer api.AssertExpectations(t)
|
||||
|
||||
p := &HelloUserPlugin{}
|
||||
p.SetAPI(api)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r.Header.Add("Mattermost-User-Id", user.Id)
|
||||
p.ServeHTTP(&plugin.Context{}, w, r)
|
||||
body, err := io.ReadAll(w.Result().Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Welcome back, billybob!", string(body))
|
||||
}
|
||||
504
server/public/plugin/plugintest/hooks.go
Обычный файл
504
server/public/plugin/plugintest/hooks.go
Обычный файл
@@ -0,0 +1,504 @@
|
||||
// Code generated by mockery v2.23.2. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make plugin-mocks`.
|
||||
|
||||
package plugintest
|
||||
|
||||
import (
|
||||
io "io"
|
||||
http "net/http"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
model "github.com/mattermost/mattermost-server/server/public/model"
|
||||
|
||||
plugin "github.com/mattermost/mattermost-server/server/public/plugin"
|
||||
)
|
||||
|
||||
// Hooks is an autogenerated mock type for the Hooks type
|
||||
type Hooks struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ChannelHasBeenCreated provides a mock function with given fields: c, channel
|
||||
func (_m *Hooks) ChannelHasBeenCreated(c *plugin.Context, channel *model.Channel) {
|
||||
_m.Called(c, channel)
|
||||
}
|
||||
|
||||
// ExecuteCommand provides a mock function with given fields: c, args
|
||||
func (_m *Hooks) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
ret := _m.Called(c, args)
|
||||
|
||||
var r0 *model.CommandResponse
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.CommandArgs) (*model.CommandResponse, *model.AppError)); ok {
|
||||
return rf(c, args)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.CommandArgs) *model.CommandResponse); ok {
|
||||
r0 = rf(c, args)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.CommandResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, *model.CommandArgs) *model.AppError); ok {
|
||||
r1 = rf(c, args)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// FileWillBeUploaded provides a mock function with given fields: c, info, file, output
|
||||
func (_m *Hooks) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
||||
ret := _m.Called(c, info, file, output)
|
||||
|
||||
var r0 *model.FileInfo
|
||||
var r1 string
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.FileInfo, io.Reader, io.Writer) (*model.FileInfo, string)); ok {
|
||||
return rf(c, info, file, output)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.FileInfo, io.Reader, io.Writer) *model.FileInfo); ok {
|
||||
r0 = rf(c, info, file, output)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.FileInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, *model.FileInfo, io.Reader, io.Writer) string); ok {
|
||||
r1 = rf(c, info, file, output)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetAllCollectionIDsForUser provides a mock function with given fields: c, userID, collectionType
|
||||
func (_m *Hooks) GetAllCollectionIDsForUser(c *plugin.Context, userID string, collectionType string) ([]string, error) {
|
||||
ret := _m.Called(c, userID, collectionType)
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) ([]string, error)); ok {
|
||||
return rf(c, userID, collectionType)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) []string); ok {
|
||||
r0 = rf(c, userID, collectionType)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, string, string) error); ok {
|
||||
r1 = rf(c, userID, collectionType)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetAllUserIdsForCollection provides a mock function with given fields: c, collectionType, collectionID
|
||||
func (_m *Hooks) GetAllUserIdsForCollection(c *plugin.Context, collectionType string, collectionID string) ([]string, error) {
|
||||
ret := _m.Called(c, collectionType, collectionID)
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) ([]string, error)); ok {
|
||||
return rf(c, collectionType, collectionID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) []string); ok {
|
||||
r0 = rf(c, collectionType, collectionID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, string, string) error); ok {
|
||||
r1 = rf(c, collectionType, collectionID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetCollectionMetadataByIds provides a mock function with given fields: c, collectionType, collectionIds
|
||||
func (_m *Hooks) GetCollectionMetadataByIds(c *plugin.Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) {
|
||||
ret := _m.Called(c, collectionType, collectionIds)
|
||||
|
||||
var r0 map[string]*model.CollectionMetadata
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, []string) (map[string]*model.CollectionMetadata, error)); ok {
|
||||
return rf(c, collectionType, collectionIds)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, []string) map[string]*model.CollectionMetadata); ok {
|
||||
r0 = rf(c, collectionType, collectionIds)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]*model.CollectionMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, string, []string) error); ok {
|
||||
r1 = rf(c, collectionType, collectionIds)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTopicMetadataByIds provides a mock function with given fields: c, topicType, topicIds
|
||||
func (_m *Hooks) GetTopicMetadataByIds(c *plugin.Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) {
|
||||
ret := _m.Called(c, topicType, topicIds)
|
||||
|
||||
var r0 map[string]*model.TopicMetadata
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, []string) (map[string]*model.TopicMetadata, error)); ok {
|
||||
return rf(c, topicType, topicIds)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, []string) map[string]*model.TopicMetadata); ok {
|
||||
r0 = rf(c, topicType, topicIds)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]*model.TopicMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, string, []string) error); ok {
|
||||
r1 = rf(c, topicType, topicIds)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTopicRedirect provides a mock function with given fields: c, topicType, topicID
|
||||
func (_m *Hooks) GetTopicRedirect(c *plugin.Context, topicType string, topicID string) (string, error) {
|
||||
ret := _m.Called(c, topicType, topicID)
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) (string, error)); ok {
|
||||
return rf(c, topicType, topicID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) string); ok {
|
||||
r0 = rf(c, topicType, topicID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, string, string) error); ok {
|
||||
r1 = rf(c, topicType, topicID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Implemented provides a mock function with given fields:
|
||||
func (_m *Hooks) Implemented() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MessageHasBeenPosted provides a mock function with given fields: c, post
|
||||
func (_m *Hooks) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
|
||||
_m.Called(c, post)
|
||||
}
|
||||
|
||||
// MessageHasBeenUpdated provides a mock function with given fields: c, newPost, oldPost
|
||||
func (_m *Hooks) MessageHasBeenUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) {
|
||||
_m.Called(c, newPost, oldPost)
|
||||
}
|
||||
|
||||
// MessageWillBePosted provides a mock function with given fields: c, post
|
||||
func (_m *Hooks) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
||||
ret := _m.Called(c, post)
|
||||
|
||||
var r0 *model.Post
|
||||
var r1 string
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.Post) (*model.Post, string)); ok {
|
||||
return rf(c, post)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.Post) *model.Post); ok {
|
||||
r0 = rf(c, post)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Post)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, *model.Post) string); ok {
|
||||
r1 = rf(c, post)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MessageWillBeUpdated provides a mock function with given fields: c, newPost, oldPost
|
||||
func (_m *Hooks) MessageWillBeUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) (*model.Post, string) {
|
||||
ret := _m.Called(c, newPost, oldPost)
|
||||
|
||||
var r0 *model.Post
|
||||
var r1 string
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.Post, *model.Post) (*model.Post, string)); ok {
|
||||
return rf(c, newPost, oldPost)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.Post, *model.Post) *model.Post); ok {
|
||||
r0 = rf(c, newPost, oldPost)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Post)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, *model.Post, *model.Post) string); ok {
|
||||
r1 = rf(c, newPost, oldPost)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// OnActivate provides a mock function with given fields:
|
||||
func (_m *Hooks) OnActivate() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// OnCloudLimitsUpdated provides a mock function with given fields: limits
|
||||
func (_m *Hooks) OnCloudLimitsUpdated(limits *model.ProductLimits) {
|
||||
_m.Called(limits)
|
||||
}
|
||||
|
||||
// OnConfigurationChange provides a mock function with given fields:
|
||||
func (_m *Hooks) OnConfigurationChange() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// OnDeactivate provides a mock function with given fields:
|
||||
func (_m *Hooks) OnDeactivate() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// OnInstall provides a mock function with given fields: c, event
|
||||
func (_m *Hooks) OnInstall(c *plugin.Context, event model.OnInstallEvent) error {
|
||||
ret := _m.Called(c, event)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, model.OnInstallEvent) error); ok {
|
||||
r0 = rf(c, event)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// OnPluginClusterEvent provides a mock function with given fields: c, ev
|
||||
func (_m *Hooks) OnPluginClusterEvent(c *plugin.Context, ev model.PluginClusterEvent) {
|
||||
_m.Called(c, ev)
|
||||
}
|
||||
|
||||
// OnSendDailyTelemetry provides a mock function with given fields:
|
||||
func (_m *Hooks) OnSendDailyTelemetry() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// OnWebSocketConnect provides a mock function with given fields: webConnID, userID
|
||||
func (_m *Hooks) OnWebSocketConnect(webConnID string, userID string) {
|
||||
_m.Called(webConnID, userID)
|
||||
}
|
||||
|
||||
// OnWebSocketDisconnect provides a mock function with given fields: webConnID, userID
|
||||
func (_m *Hooks) OnWebSocketDisconnect(webConnID string, userID string) {
|
||||
_m.Called(webConnID, userID)
|
||||
}
|
||||
|
||||
// ReactionHasBeenAdded provides a mock function with given fields: c, reaction
|
||||
func (_m *Hooks) ReactionHasBeenAdded(c *plugin.Context, reaction *model.Reaction) {
|
||||
_m.Called(c, reaction)
|
||||
}
|
||||
|
||||
// ReactionHasBeenRemoved provides a mock function with given fields: c, reaction
|
||||
func (_m *Hooks) ReactionHasBeenRemoved(c *plugin.Context, reaction *model.Reaction) {
|
||||
_m.Called(c, reaction)
|
||||
}
|
||||
|
||||
// RunDataRetention provides a mock function with given fields: nowTime, batchSize
|
||||
func (_m *Hooks) RunDataRetention(nowTime int64, batchSize int64) (int64, error) {
|
||||
ret := _m.Called(nowTime, batchSize)
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(int64, int64) (int64, error)); ok {
|
||||
return rf(nowTime, batchSize)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(int64, int64) int64); ok {
|
||||
r0 = rf(nowTime, batchSize)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(int64, int64) error); ok {
|
||||
r1 = rf(nowTime, batchSize)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ServeHTTP provides a mock function with given fields: c, w, r
|
||||
func (_m *Hooks) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||
_m.Called(c, w, r)
|
||||
}
|
||||
|
||||
// UserHasBeenCreated provides a mock function with given fields: c, user
|
||||
func (_m *Hooks) UserHasBeenCreated(c *plugin.Context, user *model.User) {
|
||||
_m.Called(c, user)
|
||||
}
|
||||
|
||||
// UserHasJoinedChannel provides a mock function with given fields: c, channelMember, actor
|
||||
func (_m *Hooks) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
|
||||
_m.Called(c, channelMember, actor)
|
||||
}
|
||||
|
||||
// UserHasJoinedTeam provides a mock function with given fields: c, teamMember, actor
|
||||
func (_m *Hooks) UserHasJoinedTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) {
|
||||
_m.Called(c, teamMember, actor)
|
||||
}
|
||||
|
||||
// UserHasLeftChannel provides a mock function with given fields: c, channelMember, actor
|
||||
func (_m *Hooks) UserHasLeftChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
|
||||
_m.Called(c, channelMember, actor)
|
||||
}
|
||||
|
||||
// UserHasLeftTeam provides a mock function with given fields: c, teamMember, actor
|
||||
func (_m *Hooks) UserHasLeftTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) {
|
||||
_m.Called(c, teamMember, actor)
|
||||
}
|
||||
|
||||
// UserHasLoggedIn provides a mock function with given fields: c, user
|
||||
func (_m *Hooks) UserHasLoggedIn(c *plugin.Context, user *model.User) {
|
||||
_m.Called(c, user)
|
||||
}
|
||||
|
||||
// UserHasPermissionToCollection provides a mock function with given fields: c, userID, collectionType, collectionId, permission
|
||||
func (_m *Hooks) UserHasPermissionToCollection(c *plugin.Context, userID string, collectionType string, collectionId string, permission *model.Permission) (bool, error) {
|
||||
ret := _m.Called(c, userID, collectionType, collectionId, permission)
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, string, string, *model.Permission) (bool, error)); ok {
|
||||
return rf(c, userID, collectionType, collectionId, permission)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, string, string, string, *model.Permission) bool); ok {
|
||||
r0 = rf(c, userID, collectionType, collectionId, permission)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, string, string, string, *model.Permission) error); ok {
|
||||
r1 = rf(c, userID, collectionType, collectionId, permission)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UserWillLogIn provides a mock function with given fields: c, user
|
||||
func (_m *Hooks) UserWillLogIn(c *plugin.Context, user *model.User) string {
|
||||
ret := _m.Called(c, user)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.User) string); ok {
|
||||
r0 = rf(c, user)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WebSocketMessageHasBeenPosted provides a mock function with given fields: webConnID, userID, req
|
||||
func (_m *Hooks) WebSocketMessageHasBeenPosted(webConnID string, userID string, req *model.WebSocketRequest) {
|
||||
_m.Called(webConnID, userID, req)
|
||||
}
|
||||
|
||||
type mockConstructorTestingTNewHooks interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}
|
||||
|
||||
// NewHooks creates a new instance of Hooks. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewHooks(t mockConstructorTestingTNewHooks) *Hooks {
|
||||
mock := &Hooks{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
33
server/public/plugin/plugintest/mock/mock.go
Обычный файл
33
server/public/plugin/plugintest/mock/mock.go
Обычный файл
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// This package provides aliases for the contents of "github.com/stretchr/testify/mock". Because
|
||||
// external packages can't import our vendored dependencies, this is necessary for them to be able
|
||||
// to fully utilize the plugintest package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const (
|
||||
Anything = mock.Anything
|
||||
)
|
||||
|
||||
type Arguments = mock.Arguments
|
||||
type AnythingOfTypeArgument = mock.AnythingOfTypeArgument
|
||||
type Call = mock.Call
|
||||
type Mock = mock.Mock
|
||||
type TestingT = mock.TestingT
|
||||
|
||||
func AnythingOfType(t string) AnythingOfTypeArgument {
|
||||
return mock.AnythingOfType(t)
|
||||
}
|
||||
|
||||
func AssertExpectationsForObjects(t TestingT, testObjects ...any) bool {
|
||||
return mock.AssertExpectationsForObjects(t, testObjects...)
|
||||
}
|
||||
|
||||
func MatchedBy(fn any) any {
|
||||
return mock.MatchedBy(fn)
|
||||
}
|
||||
41
server/public/plugin/product.go
Обычный файл
41
server/public/plugin/product.go
Обычный файл
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type RegisteredProduct struct {
|
||||
ProductID string
|
||||
Adapter Hooks
|
||||
}
|
||||
|
||||
func (rp *RegisteredProduct) Implements(hookId int) bool {
|
||||
adapter, ok := rp.Adapter.(*HooksAdapter)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok = adapter.implemented[hookId]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Implemented method is overridden intentionally to prevent calling it from outside.
|
||||
func (a *HooksAdapter) Implemented() ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// OnActivate is overridden intentionally as product should not call it.
|
||||
func (a *HooksAdapter) OnActivate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnDeactivate is overridden intentionally as product should not call it.
|
||||
func (a *HooksAdapter) OnDeactivate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP is overridden intentionally as product should not call it.
|
||||
func (a *HooksAdapter) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {}
|
||||
713
server/public/plugin/product_hooks_generated.go
Обычный файл
713
server/public/plugin/product_hooks_generated.go
Обычный файл
@@ -0,0 +1,713 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make pluginapi"
|
||||
// DO NOT EDIT
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
)
|
||||
|
||||
type OnConfigurationChangeIFace interface {
|
||||
OnConfigurationChange() error
|
||||
}
|
||||
|
||||
type ExecuteCommandIFace interface {
|
||||
ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
|
||||
}
|
||||
|
||||
type UserHasBeenCreatedIFace interface {
|
||||
UserHasBeenCreated(c *Context, user *model.User)
|
||||
}
|
||||
|
||||
type UserWillLogInIFace interface {
|
||||
UserWillLogIn(c *Context, user *model.User) string
|
||||
}
|
||||
|
||||
type UserHasLoggedInIFace interface {
|
||||
UserHasLoggedIn(c *Context, user *model.User)
|
||||
}
|
||||
|
||||
type MessageWillBePostedIFace interface {
|
||||
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
|
||||
}
|
||||
|
||||
type MessageWillBeUpdatedIFace interface {
|
||||
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
|
||||
}
|
||||
|
||||
type MessageHasBeenPostedIFace interface {
|
||||
MessageHasBeenPosted(c *Context, post *model.Post)
|
||||
}
|
||||
|
||||
type MessageHasBeenUpdatedIFace interface {
|
||||
MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post)
|
||||
}
|
||||
|
||||
type ChannelHasBeenCreatedIFace interface {
|
||||
ChannelHasBeenCreated(c *Context, channel *model.Channel)
|
||||
}
|
||||
|
||||
type UserHasJoinedChannelIFace interface {
|
||||
UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
|
||||
}
|
||||
|
||||
type UserHasLeftChannelIFace interface {
|
||||
UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
|
||||
}
|
||||
|
||||
type UserHasJoinedTeamIFace interface {
|
||||
UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
|
||||
}
|
||||
|
||||
type UserHasLeftTeamIFace interface {
|
||||
UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
|
||||
}
|
||||
|
||||
type FileWillBeUploadedIFace interface {
|
||||
FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string)
|
||||
}
|
||||
|
||||
type ReactionHasBeenAddedIFace interface {
|
||||
ReactionHasBeenAdded(c *Context, reaction *model.Reaction)
|
||||
}
|
||||
|
||||
type ReactionHasBeenRemovedIFace interface {
|
||||
ReactionHasBeenRemoved(c *Context, reaction *model.Reaction)
|
||||
}
|
||||
|
||||
type OnPluginClusterEventIFace interface {
|
||||
OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent)
|
||||
}
|
||||
|
||||
type OnWebSocketConnectIFace interface {
|
||||
OnWebSocketConnect(webConnID, userID string)
|
||||
}
|
||||
|
||||
type OnWebSocketDisconnectIFace interface {
|
||||
OnWebSocketDisconnect(webConnID, userID string)
|
||||
}
|
||||
|
||||
type WebSocketMessageHasBeenPostedIFace interface {
|
||||
WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest)
|
||||
}
|
||||
|
||||
type RunDataRetentionIFace interface {
|
||||
RunDataRetention(nowTime, batchSize int64) (int64, error)
|
||||
}
|
||||
|
||||
type OnInstallIFace interface {
|
||||
OnInstall(c *Context, event model.OnInstallEvent) error
|
||||
}
|
||||
|
||||
type OnSendDailyTelemetryIFace interface {
|
||||
OnSendDailyTelemetry()
|
||||
}
|
||||
|
||||
type OnCloudLimitsUpdatedIFace interface {
|
||||
OnCloudLimitsUpdated(limits *model.ProductLimits)
|
||||
}
|
||||
|
||||
type UserHasPermissionToCollectionIFace interface {
|
||||
UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error)
|
||||
}
|
||||
|
||||
type GetAllCollectionIDsForUserIFace interface {
|
||||
GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error)
|
||||
}
|
||||
|
||||
type GetAllUserIdsForCollectionIFace interface {
|
||||
GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error)
|
||||
}
|
||||
|
||||
type GetTopicRedirectIFace interface {
|
||||
GetTopicRedirect(c *Context, topicType, topicID string) (string, error)
|
||||
}
|
||||
|
||||
type GetCollectionMetadataByIdsIFace interface {
|
||||
GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error)
|
||||
}
|
||||
|
||||
type GetTopicMetadataByIdsIFace interface {
|
||||
GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error)
|
||||
}
|
||||
|
||||
type HooksAdapter struct {
|
||||
implemented map[int]struct{}
|
||||
productHooks any
|
||||
}
|
||||
|
||||
func NewAdapter(productHooks any) (*HooksAdapter, error) {
|
||||
a := &HooksAdapter{
|
||||
implemented: make(map[int]struct{}),
|
||||
productHooks: productHooks,
|
||||
}
|
||||
var tt reflect.Type
|
||||
ft := reflect.TypeOf(productHooks)
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements OnConfigurationChange interface.
|
||||
tt = reflect.TypeOf((*OnConfigurationChangeIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[OnConfigurationChangeID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("OnConfigurationChange"); ok {
|
||||
return nil, errors.New("hook has OnConfigurationChange method but does not implement plugin.OnConfigurationChange interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements ExecuteCommand interface.
|
||||
tt = reflect.TypeOf((*ExecuteCommandIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[ExecuteCommandID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("ExecuteCommand"); ok {
|
||||
return nil, errors.New("hook has ExecuteCommand method but does not implement plugin.ExecuteCommand interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements UserHasBeenCreated interface.
|
||||
tt = reflect.TypeOf((*UserHasBeenCreatedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[UserHasBeenCreatedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("UserHasBeenCreated"); ok {
|
||||
return nil, errors.New("hook has UserHasBeenCreated method but does not implement plugin.UserHasBeenCreated interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements UserWillLogIn interface.
|
||||
tt = reflect.TypeOf((*UserWillLogInIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[UserWillLogInID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("UserWillLogIn"); ok {
|
||||
return nil, errors.New("hook has UserWillLogIn method but does not implement plugin.UserWillLogIn interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements UserHasLoggedIn interface.
|
||||
tt = reflect.TypeOf((*UserHasLoggedInIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[UserHasLoggedInID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("UserHasLoggedIn"); ok {
|
||||
return nil, errors.New("hook has UserHasLoggedIn method but does not implement plugin.UserHasLoggedIn interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements MessageWillBePosted interface.
|
||||
tt = reflect.TypeOf((*MessageWillBePostedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[MessageWillBePostedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("MessageWillBePosted"); ok {
|
||||
return nil, errors.New("hook has MessageWillBePosted method but does not implement plugin.MessageWillBePosted interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements MessageWillBeUpdated interface.
|
||||
tt = reflect.TypeOf((*MessageWillBeUpdatedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[MessageWillBeUpdatedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("MessageWillBeUpdated"); ok {
|
||||
return nil, errors.New("hook has MessageWillBeUpdated method but does not implement plugin.MessageWillBeUpdated interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements MessageHasBeenPosted interface.
|
||||
tt = reflect.TypeOf((*MessageHasBeenPostedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[MessageHasBeenPostedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("MessageHasBeenPosted"); ok {
|
||||
return nil, errors.New("hook has MessageHasBeenPosted method but does not implement plugin.MessageHasBeenPosted interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements MessageHasBeenUpdated interface.
|
||||
tt = reflect.TypeOf((*MessageHasBeenUpdatedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[MessageHasBeenUpdatedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("MessageHasBeenUpdated"); ok {
|
||||
return nil, errors.New("hook has MessageHasBeenUpdated method but does not implement plugin.MessageHasBeenUpdated interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements ChannelHasBeenCreated interface.
|
||||
tt = reflect.TypeOf((*ChannelHasBeenCreatedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[ChannelHasBeenCreatedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("ChannelHasBeenCreated"); ok {
|
||||
return nil, errors.New("hook has ChannelHasBeenCreated method but does not implement plugin.ChannelHasBeenCreated interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements UserHasJoinedChannel interface.
|
||||
tt = reflect.TypeOf((*UserHasJoinedChannelIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[UserHasJoinedChannelID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("UserHasJoinedChannel"); ok {
|
||||
return nil, errors.New("hook has UserHasJoinedChannel method but does not implement plugin.UserHasJoinedChannel interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements UserHasLeftChannel interface.
|
||||
tt = reflect.TypeOf((*UserHasLeftChannelIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[UserHasLeftChannelID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("UserHasLeftChannel"); ok {
|
||||
return nil, errors.New("hook has UserHasLeftChannel method but does not implement plugin.UserHasLeftChannel interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements UserHasJoinedTeam interface.
|
||||
tt = reflect.TypeOf((*UserHasJoinedTeamIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[UserHasJoinedTeamID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("UserHasJoinedTeam"); ok {
|
||||
return nil, errors.New("hook has UserHasJoinedTeam method but does not implement plugin.UserHasJoinedTeam interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements UserHasLeftTeam interface.
|
||||
tt = reflect.TypeOf((*UserHasLeftTeamIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[UserHasLeftTeamID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("UserHasLeftTeam"); ok {
|
||||
return nil, errors.New("hook has UserHasLeftTeam method but does not implement plugin.UserHasLeftTeam interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements FileWillBeUploaded interface.
|
||||
tt = reflect.TypeOf((*FileWillBeUploadedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[FileWillBeUploadedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("FileWillBeUploaded"); ok {
|
||||
return nil, errors.New("hook has FileWillBeUploaded method but does not implement plugin.FileWillBeUploaded interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements ReactionHasBeenAdded interface.
|
||||
tt = reflect.TypeOf((*ReactionHasBeenAddedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[ReactionHasBeenAddedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("ReactionHasBeenAdded"); ok {
|
||||
return nil, errors.New("hook has ReactionHasBeenAdded method but does not implement plugin.ReactionHasBeenAdded interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements ReactionHasBeenRemoved interface.
|
||||
tt = reflect.TypeOf((*ReactionHasBeenRemovedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[ReactionHasBeenRemovedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("ReactionHasBeenRemoved"); ok {
|
||||
return nil, errors.New("hook has ReactionHasBeenRemoved method but does not implement plugin.ReactionHasBeenRemoved interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements OnPluginClusterEvent interface.
|
||||
tt = reflect.TypeOf((*OnPluginClusterEventIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[OnPluginClusterEventID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("OnPluginClusterEvent"); ok {
|
||||
return nil, errors.New("hook has OnPluginClusterEvent method but does not implement plugin.OnPluginClusterEvent interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements OnWebSocketConnect interface.
|
||||
tt = reflect.TypeOf((*OnWebSocketConnectIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[OnWebSocketConnectID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("OnWebSocketConnect"); ok {
|
||||
return nil, errors.New("hook has OnWebSocketConnect method but does not implement plugin.OnWebSocketConnect interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements OnWebSocketDisconnect interface.
|
||||
tt = reflect.TypeOf((*OnWebSocketDisconnectIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[OnWebSocketDisconnectID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("OnWebSocketDisconnect"); ok {
|
||||
return nil, errors.New("hook has OnWebSocketDisconnect method but does not implement plugin.OnWebSocketDisconnect interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements WebSocketMessageHasBeenPosted interface.
|
||||
tt = reflect.TypeOf((*WebSocketMessageHasBeenPostedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[WebSocketMessageHasBeenPostedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("WebSocketMessageHasBeenPosted"); ok {
|
||||
return nil, errors.New("hook has WebSocketMessageHasBeenPosted method but does not implement plugin.WebSocketMessageHasBeenPosted interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements RunDataRetention interface.
|
||||
tt = reflect.TypeOf((*RunDataRetentionIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[RunDataRetentionID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("RunDataRetention"); ok {
|
||||
return nil, errors.New("hook has RunDataRetention method but does not implement plugin.RunDataRetention interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements OnInstall interface.
|
||||
tt = reflect.TypeOf((*OnInstallIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[OnInstallID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("OnInstall"); ok {
|
||||
return nil, errors.New("hook has OnInstall method but does not implement plugin.OnInstall interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements OnSendDailyTelemetry interface.
|
||||
tt = reflect.TypeOf((*OnSendDailyTelemetryIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[OnSendDailyTelemetryID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("OnSendDailyTelemetry"); ok {
|
||||
return nil, errors.New("hook has OnSendDailyTelemetry method but does not implement plugin.OnSendDailyTelemetry interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements OnCloudLimitsUpdated interface.
|
||||
tt = reflect.TypeOf((*OnCloudLimitsUpdatedIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[OnCloudLimitsUpdatedID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("OnCloudLimitsUpdated"); ok {
|
||||
return nil, errors.New("hook has OnCloudLimitsUpdated method but does not implement plugin.OnCloudLimitsUpdated interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements UserHasPermissionToCollection interface.
|
||||
tt = reflect.TypeOf((*UserHasPermissionToCollectionIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[UserHasPermissionToCollectionID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("UserHasPermissionToCollection"); ok {
|
||||
return nil, errors.New("hook has UserHasPermissionToCollection method but does not implement plugin.UserHasPermissionToCollection interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements GetAllCollectionIDsForUser interface.
|
||||
tt = reflect.TypeOf((*GetAllCollectionIDsForUserIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[GetAllCollectionIDsForUserID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("GetAllCollectionIDsForUser"); ok {
|
||||
return nil, errors.New("hook has GetAllCollectionIDsForUser method but does not implement plugin.GetAllCollectionIDsForUser interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements GetAllUserIdsForCollection interface.
|
||||
tt = reflect.TypeOf((*GetAllUserIdsForCollectionIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[GetAllUserIdsForCollectionID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("GetAllUserIdsForCollection"); ok {
|
||||
return nil, errors.New("hook has GetAllUserIdsForCollection method but does not implement plugin.GetAllUserIdsForCollection interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements GetTopicRedirect interface.
|
||||
tt = reflect.TypeOf((*GetTopicRedirectIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[GetTopicRedirectID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("GetTopicRedirect"); ok {
|
||||
return nil, errors.New("hook has GetTopicRedirect method but does not implement plugin.GetTopicRedirect interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements GetCollectionMetadataByIds interface.
|
||||
tt = reflect.TypeOf((*GetCollectionMetadataByIdsIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[GetCollectionMetadataByIdsID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("GetCollectionMetadataByIds"); ok {
|
||||
return nil, errors.New("hook has GetCollectionMetadataByIds method but does not implement plugin.GetCollectionMetadataByIds interface")
|
||||
}
|
||||
|
||||
// Assessing the type of the productHooks if it individually implements GetTopicMetadataByIds interface.
|
||||
tt = reflect.TypeOf((*GetTopicMetadataByIdsIFace)(nil)).Elem()
|
||||
|
||||
if ft.Implements(tt) {
|
||||
a.implemented[GetTopicMetadataByIdsID] = struct{}{}
|
||||
} else if _, ok := ft.MethodByName("GetTopicMetadataByIds"); ok {
|
||||
return nil, errors.New("hook has GetTopicMetadataByIds method but does not implement plugin.GetTopicMetadataByIds interface")
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) OnConfigurationChange() error {
|
||||
if _, ok := a.implemented[OnConfigurationChangeID]; !ok {
|
||||
panic("product hooks must implement OnConfigurationChange")
|
||||
}
|
||||
|
||||
return a.productHooks.(OnConfigurationChangeIFace).OnConfigurationChange()
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
if _, ok := a.implemented[ExecuteCommandID]; !ok {
|
||||
panic("product hooks must implement ExecuteCommand")
|
||||
}
|
||||
|
||||
return a.productHooks.(ExecuteCommandIFace).ExecuteCommand(c, args)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) UserHasBeenCreated(c *Context, user *model.User) {
|
||||
if _, ok := a.implemented[UserHasBeenCreatedID]; !ok {
|
||||
panic("product hooks must implement UserHasBeenCreated")
|
||||
}
|
||||
|
||||
a.productHooks.(UserHasBeenCreatedIFace).UserHasBeenCreated(c, user)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) UserWillLogIn(c *Context, user *model.User) string {
|
||||
if _, ok := a.implemented[UserWillLogInID]; !ok {
|
||||
panic("product hooks must implement UserWillLogIn")
|
||||
}
|
||||
|
||||
return a.productHooks.(UserWillLogInIFace).UserWillLogIn(c, user)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) UserHasLoggedIn(c *Context, user *model.User) {
|
||||
if _, ok := a.implemented[UserHasLoggedInID]; !ok {
|
||||
panic("product hooks must implement UserHasLoggedIn")
|
||||
}
|
||||
|
||||
a.productHooks.(UserHasLoggedInIFace).UserHasLoggedIn(c, user)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) {
|
||||
if _, ok := a.implemented[MessageWillBePostedID]; !ok {
|
||||
panic("product hooks must implement MessageWillBePosted")
|
||||
}
|
||||
|
||||
return a.productHooks.(MessageWillBePostedIFace).MessageWillBePosted(c, post)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) {
|
||||
if _, ok := a.implemented[MessageWillBeUpdatedID]; !ok {
|
||||
panic("product hooks must implement MessageWillBeUpdated")
|
||||
}
|
||||
|
||||
return a.productHooks.(MessageWillBeUpdatedIFace).MessageWillBeUpdated(c, newPost, oldPost)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) MessageHasBeenPosted(c *Context, post *model.Post) {
|
||||
if _, ok := a.implemented[MessageHasBeenPostedID]; !ok {
|
||||
panic("product hooks must implement MessageHasBeenPosted")
|
||||
}
|
||||
|
||||
a.productHooks.(MessageHasBeenPostedIFace).MessageHasBeenPosted(c, post)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) {
|
||||
if _, ok := a.implemented[MessageHasBeenUpdatedID]; !ok {
|
||||
panic("product hooks must implement MessageHasBeenUpdated")
|
||||
}
|
||||
|
||||
a.productHooks.(MessageHasBeenUpdatedIFace).MessageHasBeenUpdated(c, newPost, oldPost)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) {
|
||||
if _, ok := a.implemented[ChannelHasBeenCreatedID]; !ok {
|
||||
panic("product hooks must implement ChannelHasBeenCreated")
|
||||
}
|
||||
|
||||
a.productHooks.(ChannelHasBeenCreatedIFace).ChannelHasBeenCreated(c, channel)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
|
||||
if _, ok := a.implemented[UserHasJoinedChannelID]; !ok {
|
||||
panic("product hooks must implement UserHasJoinedChannel")
|
||||
}
|
||||
|
||||
a.productHooks.(UserHasJoinedChannelIFace).UserHasJoinedChannel(c, channelMember, actor)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
|
||||
if _, ok := a.implemented[UserHasLeftChannelID]; !ok {
|
||||
panic("product hooks must implement UserHasLeftChannel")
|
||||
}
|
||||
|
||||
a.productHooks.(UserHasLeftChannelIFace).UserHasLeftChannel(c, channelMember, actor)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
|
||||
if _, ok := a.implemented[UserHasJoinedTeamID]; !ok {
|
||||
panic("product hooks must implement UserHasJoinedTeam")
|
||||
}
|
||||
|
||||
a.productHooks.(UserHasJoinedTeamIFace).UserHasJoinedTeam(c, teamMember, actor)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
|
||||
if _, ok := a.implemented[UserHasLeftTeamID]; !ok {
|
||||
panic("product hooks must implement UserHasLeftTeam")
|
||||
}
|
||||
|
||||
a.productHooks.(UserHasLeftTeamIFace).UserHasLeftTeam(c, teamMember, actor)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
|
||||
if _, ok := a.implemented[FileWillBeUploadedID]; !ok {
|
||||
panic("product hooks must implement FileWillBeUploaded")
|
||||
}
|
||||
|
||||
return a.productHooks.(FileWillBeUploadedIFace).FileWillBeUploaded(c, info, file, output)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) {
|
||||
if _, ok := a.implemented[ReactionHasBeenAddedID]; !ok {
|
||||
panic("product hooks must implement ReactionHasBeenAdded")
|
||||
}
|
||||
|
||||
a.productHooks.(ReactionHasBeenAddedIFace).ReactionHasBeenAdded(c, reaction)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) {
|
||||
if _, ok := a.implemented[ReactionHasBeenRemovedID]; !ok {
|
||||
panic("product hooks must implement ReactionHasBeenRemoved")
|
||||
}
|
||||
|
||||
a.productHooks.(ReactionHasBeenRemovedIFace).ReactionHasBeenRemoved(c, reaction)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) {
|
||||
if _, ok := a.implemented[OnPluginClusterEventID]; !ok {
|
||||
panic("product hooks must implement OnPluginClusterEvent")
|
||||
}
|
||||
|
||||
a.productHooks.(OnPluginClusterEventIFace).OnPluginClusterEvent(c, ev)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) OnWebSocketConnect(webConnID, userID string) {
|
||||
if _, ok := a.implemented[OnWebSocketConnectID]; !ok {
|
||||
panic("product hooks must implement OnWebSocketConnect")
|
||||
}
|
||||
|
||||
a.productHooks.(OnWebSocketConnectIFace).OnWebSocketConnect(webConnID, userID)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) OnWebSocketDisconnect(webConnID, userID string) {
|
||||
if _, ok := a.implemented[OnWebSocketDisconnectID]; !ok {
|
||||
panic("product hooks must implement OnWebSocketDisconnect")
|
||||
}
|
||||
|
||||
a.productHooks.(OnWebSocketDisconnectIFace).OnWebSocketDisconnect(webConnID, userID)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) {
|
||||
if _, ok := a.implemented[WebSocketMessageHasBeenPostedID]; !ok {
|
||||
panic("product hooks must implement WebSocketMessageHasBeenPosted")
|
||||
}
|
||||
|
||||
a.productHooks.(WebSocketMessageHasBeenPostedIFace).WebSocketMessageHasBeenPosted(webConnID, userID, req)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) RunDataRetention(nowTime, batchSize int64) (int64, error) {
|
||||
if _, ok := a.implemented[RunDataRetentionID]; !ok {
|
||||
panic("product hooks must implement RunDataRetention")
|
||||
}
|
||||
|
||||
return a.productHooks.(RunDataRetentionIFace).RunDataRetention(nowTime, batchSize)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) OnInstall(c *Context, event model.OnInstallEvent) error {
|
||||
if _, ok := a.implemented[OnInstallID]; !ok {
|
||||
panic("product hooks must implement OnInstall")
|
||||
}
|
||||
|
||||
return a.productHooks.(OnInstallIFace).OnInstall(c, event)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) OnSendDailyTelemetry() {
|
||||
if _, ok := a.implemented[OnSendDailyTelemetryID]; !ok {
|
||||
panic("product hooks must implement OnSendDailyTelemetry")
|
||||
}
|
||||
|
||||
a.productHooks.(OnSendDailyTelemetryIFace).OnSendDailyTelemetry()
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) {
|
||||
if _, ok := a.implemented[OnCloudLimitsUpdatedID]; !ok {
|
||||
panic("product hooks must implement OnCloudLimitsUpdated")
|
||||
}
|
||||
|
||||
a.productHooks.(OnCloudLimitsUpdatedIFace).OnCloudLimitsUpdated(limits)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) {
|
||||
if _, ok := a.implemented[UserHasPermissionToCollectionID]; !ok {
|
||||
panic("product hooks must implement UserHasPermissionToCollection")
|
||||
}
|
||||
|
||||
return a.productHooks.(UserHasPermissionToCollectionIFace).UserHasPermissionToCollection(c, userID, collectionType, collectionId, permission)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) {
|
||||
if _, ok := a.implemented[GetAllCollectionIDsForUserID]; !ok {
|
||||
panic("product hooks must implement GetAllCollectionIDsForUser")
|
||||
}
|
||||
|
||||
return a.productHooks.(GetAllCollectionIDsForUserIFace).GetAllCollectionIDsForUser(c, userID, collectionType)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) {
|
||||
if _, ok := a.implemented[GetAllUserIdsForCollectionID]; !ok {
|
||||
panic("product hooks must implement GetAllUserIdsForCollection")
|
||||
}
|
||||
|
||||
return a.productHooks.(GetAllUserIdsForCollectionIFace).GetAllUserIdsForCollection(c, collectionType, collectionID)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) {
|
||||
if _, ok := a.implemented[GetTopicRedirectID]; !ok {
|
||||
panic("product hooks must implement GetTopicRedirect")
|
||||
}
|
||||
|
||||
return a.productHooks.(GetTopicRedirectIFace).GetTopicRedirect(c, topicType, topicID)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) {
|
||||
if _, ok := a.implemented[GetCollectionMetadataByIdsID]; !ok {
|
||||
panic("product hooks must implement GetCollectionMetadataByIds")
|
||||
}
|
||||
|
||||
return a.productHooks.(GetCollectionMetadataByIdsIFace).GetCollectionMetadataByIds(c, collectionType, collectionIds)
|
||||
|
||||
}
|
||||
|
||||
func (a *HooksAdapter) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) {
|
||||
if _, ok := a.implemented[GetTopicMetadataByIdsID]; !ok {
|
||||
panic("product hooks must implement GetTopicMetadataByIds")
|
||||
}
|
||||
|
||||
return a.productHooks.(GetTopicMetadataByIdsIFace).GetTopicMetadataByIds(c, topicType, topicIds)
|
||||
|
||||
}
|
||||
31
server/public/plugin/stringifier.go
Обычный файл
31
server/public/plugin/stringifier.go
Обычный файл
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func stringify(objects []any) []string {
|
||||
stringified := make([]string, len(objects))
|
||||
for i, object := range objects {
|
||||
stringified[i] = fmt.Sprintf("%+v", object)
|
||||
}
|
||||
return stringified
|
||||
}
|
||||
|
||||
func toObjects(strings []string) []any {
|
||||
if strings == nil {
|
||||
return nil
|
||||
}
|
||||
objects := make([]any, len(strings))
|
||||
for i, string := range strings {
|
||||
objects[i] = string
|
||||
}
|
||||
return objects
|
||||
}
|
||||
|
||||
func stringifyToObjects(objects []any) []any {
|
||||
return toObjects(stringify(objects))
|
||||
}
|
||||
94
server/public/plugin/stringifier_test.go
Обычный файл
94
server/public/plugin/stringifier_test.go
Обычный файл
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStringify(t *testing.T) {
|
||||
t.Run("NilShouldReturnEmpty", func(t *testing.T) {
|
||||
strings := stringify(nil)
|
||||
assert.Empty(t, strings)
|
||||
})
|
||||
t.Run("EmptyShouldReturnEmpty", func(t *testing.T) {
|
||||
strings := stringify(make([]any, 0))
|
||||
assert.Empty(t, strings)
|
||||
})
|
||||
t.Run("PrimitivesAndCompositesShouldReturnCorrectValues", func(t *testing.T) {
|
||||
strings := stringify([]any{
|
||||
1234,
|
||||
3.14159265358979323846264338327950288419716939937510,
|
||||
true,
|
||||
"foo",
|
||||
nil,
|
||||
[]string{"foo", "bar"},
|
||||
map[string]int{"one": 1, "two": 2},
|
||||
&WithString{},
|
||||
&WithoutString{},
|
||||
&WithStringAndError{},
|
||||
})
|
||||
assert.Equal(t, []string{
|
||||
"1234",
|
||||
"3.141592653589793",
|
||||
"true",
|
||||
"foo",
|
||||
"<nil>",
|
||||
"[foo bar]",
|
||||
"map[one:1 two:2]",
|
||||
"string",
|
||||
"&{}",
|
||||
"error",
|
||||
}, strings)
|
||||
})
|
||||
t.Run("ErrorShouldReturnFormattedStack", func(t *testing.T) {
|
||||
strings := stringify([]any{
|
||||
errors.New("error"),
|
||||
errors.WithStack(errors.New("error")),
|
||||
})
|
||||
stackRegexp := "error\n.*plugin.TestStringify.func\\d+\n\t.*plugin/stringifier_test.go:\\d+\ntesting.tRunner\n\t.*testing.go:\\d+.*"
|
||||
assert.Len(t, strings, 2)
|
||||
assert.Regexp(t, stackRegexp, strings[0])
|
||||
assert.Regexp(t, stackRegexp, strings[1])
|
||||
})
|
||||
}
|
||||
|
||||
type WithString struct {
|
||||
}
|
||||
|
||||
func (*WithString) String() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
type WithoutString struct {
|
||||
}
|
||||
|
||||
type WithStringAndError struct {
|
||||
}
|
||||
|
||||
func (*WithStringAndError) String() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func (*WithStringAndError) Error() string {
|
||||
return "error"
|
||||
}
|
||||
|
||||
func TestToObjects(t *testing.T) {
|
||||
t.Run("NilShouldReturnNil", func(t *testing.T) {
|
||||
objects := toObjects(nil)
|
||||
assert.Nil(t, objects)
|
||||
})
|
||||
t.Run("EmptyShouldReturnEmpty", func(t *testing.T) {
|
||||
objects := toObjects(make([]string, 0))
|
||||
assert.Empty(t, objects)
|
||||
})
|
||||
t.Run("ShouldReturnSliceOfObjects", func(t *testing.T) {
|
||||
objects := toObjects([]string{"foo", "bar"})
|
||||
assert.Equal(t, []any{"foo", "bar"}, objects)
|
||||
})
|
||||
}
|
||||
197
server/public/plugin/supervisor.go
Обычный файл
197
server/public/plugin/supervisor.go
Обычный файл
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
plugin "github.com/hashicorp/go-plugin"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
type supervisor struct {
|
||||
lock sync.RWMutex
|
||||
client *plugin.Client
|
||||
hooks Hooks
|
||||
implemented [TotalHooksID]bool
|
||||
pid int
|
||||
hooksClient *hooksRPCClient
|
||||
}
|
||||
|
||||
func newSupervisor(pluginInfo *model.BundleInfo, apiImpl API, driver Driver, parentLogger *mlog.Logger, metrics metricsInterface) (retSupervisor *supervisor, retErr error) {
|
||||
sup := supervisor{}
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
sup.Shutdown()
|
||||
}
|
||||
}()
|
||||
|
||||
wrappedLogger := pluginInfo.WrapLogger(parentLogger)
|
||||
|
||||
hclogAdaptedLogger := &hclogAdapter{
|
||||
wrappedLogger: wrappedLogger,
|
||||
extrasKey: "wrapped_extras",
|
||||
}
|
||||
|
||||
pluginMap := map[string]plugin.Plugin{
|
||||
"hooks": &hooksPlugin{
|
||||
log: wrappedLogger,
|
||||
driverImpl: driver,
|
||||
apiImpl: &apiTimerLayer{pluginInfo.Manifest.Id, apiImpl, metrics},
|
||||
},
|
||||
}
|
||||
|
||||
executable := filepath.Clean(filepath.Join(
|
||||
".",
|
||||
pluginInfo.Manifest.GetExecutableForRuntime(runtime.GOOS, runtime.GOARCH),
|
||||
))
|
||||
if executable == "" {
|
||||
return nil, fmt.Errorf("backend executable not found for environment %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
if strings.HasPrefix(executable, "..") {
|
||||
return nil, fmt.Errorf("invalid backend executable")
|
||||
}
|
||||
|
||||
executable = filepath.Join(pluginInfo.Path, executable)
|
||||
|
||||
cmd := exec.Command(executable)
|
||||
|
||||
// This doesn't add more security than before
|
||||
// but removes the SecureConfig is nil warning.
|
||||
// https://mattermost.atlassian.net/browse/MM-49167
|
||||
pluginChecksum, err := getPluginExecutableChecksum(executable)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to generate plugin checksum")
|
||||
}
|
||||
|
||||
sup.client = plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: handshake,
|
||||
Plugins: pluginMap,
|
||||
Cmd: cmd,
|
||||
SyncStdout: wrappedLogger.With(mlog.String("source", "plugin_stdout")).StdLogWriter(),
|
||||
SyncStderr: wrappedLogger.With(mlog.String("source", "plugin_stderr")).StdLogWriter(),
|
||||
Logger: hclogAdaptedLogger,
|
||||
StartTimeout: time.Second * 3,
|
||||
SecureConfig: &plugin.SecureConfig{
|
||||
Checksum: pluginChecksum,
|
||||
Hash: sha256.New(),
|
||||
},
|
||||
})
|
||||
|
||||
rpcClient, err := sup.client.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sup.pid = cmd.Process.Pid
|
||||
|
||||
raw, err := rpcClient.Dispense("hooks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, ok := raw.(*hooksRPCClient)
|
||||
if ok {
|
||||
sup.hooksClient = c
|
||||
}
|
||||
|
||||
sup.hooks = &hooksTimerLayer{pluginInfo.Manifest.Id, raw.(Hooks), metrics}
|
||||
|
||||
impl, err := sup.hooks.Implemented()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, hookName := range impl {
|
||||
if hookId, ok := hookNameToId[hookName]; ok {
|
||||
sup.implemented[hookId] = true
|
||||
}
|
||||
}
|
||||
|
||||
return &sup, nil
|
||||
}
|
||||
|
||||
func (sup *supervisor) Shutdown() {
|
||||
sup.lock.RLock()
|
||||
defer sup.lock.RUnlock()
|
||||
if sup.client != nil {
|
||||
sup.client.Kill()
|
||||
}
|
||||
|
||||
// Wait for API RPC server and DB RPC server to exit.
|
||||
if sup.hooksClient != nil {
|
||||
sup.hooksClient.doneWg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (sup *supervisor) Hooks() Hooks {
|
||||
sup.lock.RLock()
|
||||
defer sup.lock.RUnlock()
|
||||
return sup.hooks
|
||||
}
|
||||
|
||||
// PerformHealthCheck checks the plugin through an an RPC ping.
|
||||
func (sup *supervisor) PerformHealthCheck() error {
|
||||
// No need for a lock here because Ping is read-locked.
|
||||
if pingErr := sup.Ping(); pingErr != nil {
|
||||
for pingFails := 1; pingFails < HealthCheckPingFailLimit; pingFails++ {
|
||||
pingErr = sup.Ping()
|
||||
if pingErr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if pingErr != nil {
|
||||
return fmt.Errorf("plugin RPC connection is not responding")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping checks that the RPC connection with the plugin is alive and healthy.
|
||||
func (sup *supervisor) Ping() error {
|
||||
sup.lock.RLock()
|
||||
defer sup.lock.RUnlock()
|
||||
client, err := sup.client.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.Ping()
|
||||
}
|
||||
|
||||
func (sup *supervisor) Implements(hookId int) bool {
|
||||
sup.lock.RLock()
|
||||
defer sup.lock.RUnlock()
|
||||
return sup.implemented[hookId]
|
||||
}
|
||||
|
||||
func getPluginExecutableChecksum(executablePath string) ([]byte, error) {
|
||||
pathHash := sha256.New()
|
||||
file, err := os.Open(executablePath)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(pathHash, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pathHash.Sum(nil), nil
|
||||
}
|
||||
83
server/public/plugin/supervisor_test.go
Обычный файл
83
server/public/plugin/supervisor_test.go
Обычный файл
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/server/public/model"
|
||||
"github.com/mattermost/mattermost-server/server/public/plugin/utils"
|
||||
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func TestSupervisor(t *testing.T) {
|
||||
for name, f := range map[string]func(*testing.T){
|
||||
"Supervisor_InvalidExecutablePath": testSupervisorInvalidExecutablePath,
|
||||
"Supervisor_NonExistentExecutablePath": testSupervisorNonExistentExecutablePath,
|
||||
"Supervisor_StartTimeout": testSupervisorStartTimeout,
|
||||
} {
|
||||
t.Run(name, f)
|
||||
}
|
||||
}
|
||||
|
||||
func testSupervisorInvalidExecutablePath(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "server": {"executable": "/foo/../../backend.exe"}}`), 0600)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
log := mlog.CreateConsoleTestLogger(true, mlog.LvlError)
|
||||
defer log.Shutdown()
|
||||
supervisor, err := newSupervisor(bundle, nil, nil, log, nil)
|
||||
assert.Nil(t, supervisor)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func testSupervisorNonExistentExecutablePath(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "server": {"executable": "thisfileshouldnotexist"}}`), 0600)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
log := mlog.CreateConsoleTestLogger(true, mlog.LvlError)
|
||||
defer log.Shutdown()
|
||||
supervisor, err := newSupervisor(bundle, nil, nil, log, nil)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, supervisor)
|
||||
}
|
||||
|
||||
// If plugin development goes really wrong, let's make sure plugin activation won't block forever.
|
||||
func testSupervisorStartTimeout(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend.exe")
|
||||
utils.CompileGo(t, `
|
||||
package main
|
||||
|
||||
func main() {
|
||||
for {
|
||||
}
|
||||
}
|
||||
`, backend)
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "server": {"executable": "backend.exe"}}`), 0600)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
log := mlog.CreateConsoleTestLogger(true, mlog.LvlError)
|
||||
defer log.Shutdown()
|
||||
supervisor, err := newSupervisor(bundle, nil, nil, log, nil)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, supervisor)
|
||||
}
|
||||
73
server/public/plugin/utils/test_files_compiler.go
Обычный файл
73
server/public/plugin/utils/test_files_compiler.go
Обычный файл
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func CompileGo(t *testing.T, sourceCode, outputPath string) {
|
||||
dir, err := os.MkdirTemp(".", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
dir, err = filepath.Abs(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write out main.go given the source code.
|
||||
main := filepath.Join(dir, "main.go")
|
||||
err = os.WriteFile(main, []byte(sourceCode), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, sourceFile, _, ok := runtime.Caller(0)
|
||||
require.True(t, ok)
|
||||
serverPath := filepath.Dir(filepath.Dir(sourceFile))
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
cmd := exec.Command("go", "build", "-o", outputPath, main)
|
||||
cmd.Dir = serverPath
|
||||
cmd.Stdout = out
|
||||
cmd.Stderr = out
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
t.Log("Go compile errors:\n", out.String())
|
||||
}
|
||||
require.NoError(t, err, "failed to compile go")
|
||||
}
|
||||
|
||||
func CompileGoTest(t *testing.T, sourceCode, outputPath string) {
|
||||
dir, err := os.MkdirTemp(".", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
dir, err = filepath.Abs(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write out main.go given the source code.
|
||||
main := filepath.Join(dir, "main_test.go")
|
||||
err = os.WriteFile(main, []byte(sourceCode), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, sourceFile, _, ok := runtime.Caller(0)
|
||||
require.True(t, ok)
|
||||
serverPath := filepath.Dir(filepath.Dir(sourceFile))
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
cmd := exec.Command("go", "test", "-c", "-o", outputPath, main)
|
||||
cmd.Dir = serverPath
|
||||
cmd.Stdout = out
|
||||
cmd.Stderr = out
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
t.Log("Go compile errors:\n", out.String())
|
||||
}
|
||||
require.NoError(t, err, "failed to compile go")
|
||||
}
|
||||
Ссылка в новой задаче
Block a user