snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
756
server/public/plugin/interface_generator/main.go
Normal file
756
server/public/plugin/interface_generator/main.go
Normal file
@@ -0,0 +1,756 @@
|
||||
// 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"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/tools/imports"
|
||||
)
|
||||
|
||||
var excludedPluginHooks = []string{
|
||||
"ChannelMemberWillBeAdded",
|
||||
"FileWillBeUploaded",
|
||||
"TeamMemberWillBeAdded",
|
||||
"Implemented",
|
||||
"LoadPluginConfiguration",
|
||||
"InstallPlugin",
|
||||
"LogAuditRec",
|
||||
"LogAuditRecWithLevel",
|
||||
"LogDebug",
|
||||
"LogError",
|
||||
"LogInfo",
|
||||
"LogWarn",
|
||||
"MessageWillBePosted",
|
||||
"MessageWillBeUpdated",
|
||||
"MessagesWillBeConsumed",
|
||||
"MessagesWillBeConsumedWithContext",
|
||||
"OnActivate",
|
||||
"PluginHTTP",
|
||||
"ServeHTTP",
|
||||
"UploadData",
|
||||
"ReceiveSharedChannelAttachmentSyncMsg",
|
||||
"ServeMetrics",
|
||||
}
|
||||
|
||||
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, ", ") + ")"
|
||||
}
|
||||
|
||||
// FieldListToFuncListAppendErr renders the return signature with an extra trailing
|
||||
// `error` (the RPC transport error). Used to emit *WithRPCErr companion signatures.
|
||||
func FieldListToFuncListAppendErr(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return "error"
|
||||
}
|
||||
base := FieldListToFuncList(fieldList, fileset)
|
||||
return "(" + base[1:len(base)-1] + ", error)"
|
||||
}
|
||||
|
||||
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, ", ")
|
||||
}
|
||||
|
||||
// FieldListDestructAppendErr is FieldListDestruct with an extra trailing `<prefix>RPCErr`
|
||||
// variable name appended — used to bind the transport error in *WithRPCErr templates.
|
||||
func FieldListDestructAppendErr(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
base := FieldListDestruct(structPrefix, fieldList, fileset)
|
||||
rpcErr := structPrefix + "RPCErr"
|
||||
if base == "" {
|
||||
return rpcErr
|
||||
}
|
||||
return base + ", " + rpcErr
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// FieldListToRecordSuccessWithRPCErr renders the success-flag expression for a *WithRPCErr
|
||||
// timer-layer call. It combines the RPC transport check with the base hook's app-error check
|
||||
// (if any), so the metric counts a call as successful only when both the transport and the
|
||||
// plugin's application-level return signal success.
|
||||
func FieldListToRecordSuccessWithRPCErr(structPrefix string, fieldList *ast.FieldList) string {
|
||||
rpcErr := structPrefix + "RPCErr == nil"
|
||||
base := FieldListToRecordSuccess(structPrefix, fieldList)
|
||||
if base == "true" {
|
||||
return rpcErr
|
||||
}
|
||||
return rpcErr + " && " + base
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
saml2 "github.com/mattermost/gosaml2"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
{{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 }}
|
||||
}
|
||||
|
||||
// {{.Name}}WithRPCErr returns the same values as {{.Name}}, with an additional trailing error
|
||||
// for the RPC transport — always the LAST return slot.
|
||||
func (g *hooksRPCClient) {{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}} {
|
||||
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
|
||||
_returns := &{{.Name | obscure}}Returns{}
|
||||
var _err error
|
||||
if g.implemented[{{.Name}}ID] {
|
||||
_err = g.client.Call("Plugin.{{.Name}}", _args, _returns)
|
||||
if _err != nil {
|
||||
// Reset _returns so partial gob decoding can't leak non-zero
|
||||
// values past a transport failure (HooksWithRPCErrGenerated contract).
|
||||
_returns = &{{.Name | obscure}}Returns{}
|
||||
g.log.Debug("RPC call {{.Name}} to plugin failed.", mlog.Err(_err))
|
||||
}
|
||||
}
|
||||
{{ if .Return }} return {{destruct "_returns." .Return}}, _err {{ else }} return _err {{ 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}}
|
||||
|
||||
// HooksWithRPCErrGenerated provides a WithRPCErr variant for every generated hook. The last error return
|
||||
// is always the RPC transport error — if non-nil, the plugin's other return values are zero. For
|
||||
// hooks whose base signature already returns error, the tuple is (originalReturns..., rpcErr)
|
||||
// where the final slot is always transport.
|
||||
//
|
||||
// If the plugin does not implement the hook, the companion returns zero values and a nil error —
|
||||
// indistinguishable from a successful invocation that returned zeros. Callers MUST gate on
|
||||
// supervisor.Implements(<HookID>) (or use Environment.RunMultiPluginHookWithRPCErr, which gates
|
||||
// by the iteration's hook ID — note that any *WithRPCErr method called on the closure's
|
||||
// HooksWithRPCErrGenerated is independently subject to its own implemented-gate).
|
||||
type HooksWithRPCErrGenerated interface {
|
||||
{{range .HooksMethods}}
|
||||
{{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}}
|
||||
{{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 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/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"
|
||||
|
||||
saml2 "github.com/mattermost/gosaml2"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
type hooksTimerLayer struct {
|
||||
pluginID string
|
||||
hooksImpl Hooks
|
||||
hooksWithRPCErrImpl HooksWithRPCErr
|
||||
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}}
|
||||
|
||||
{{range .HooksMethodsRPCErr}}
|
||||
|
||||
func (hooks *hooksTimerLayer) {{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}} {
|
||||
startTime := timePkg.Now()
|
||||
{{destructAppendErr "_returns" .Return}} := hooks.hooksWithRPCErrImpl.{{.Name}}WithRPCErr({{valuesOnly .Params}})
|
||||
hooks.recordTime(startTime, "{{.Name}}WithRPCErr", {{ shouldRecordSuccessWithRPCErr "_returns" .Return }})
|
||||
return {{destructAppendErr "_returns" .Return}}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
`
|
||||
|
||||
type MethodParams struct {
|
||||
Name string
|
||||
Params *ast.FieldList
|
||||
Return *ast.FieldList
|
||||
}
|
||||
|
||||
type HooksTemplateParams struct {
|
||||
HooksMethods []MethodParams
|
||||
HooksMethodsRPCErr []MethodParams
|
||||
APIMethods []MethodParams
|
||||
}
|
||||
|
||||
func generateHooksGlue(info *PluginInterfaceInfo) {
|
||||
templateFunctions := map[string]any{
|
||||
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
|
||||
"funcStyleAppendErr": func(fields *ast.FieldList) string { return FieldListToFuncListAppendErr(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{}
|
||||
err = hooksTemplate.Execute(templateResult, &templateParams)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
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 generatePluginTimerLayer(info *PluginInterfaceInfo) {
|
||||
templateFunctions := map[string]any{
|
||||
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
|
||||
"funcStyleAppendErr": func(fields *ast.FieldList) string { return FieldListToFuncListAppendErr(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)
|
||||
},
|
||||
"destructAppendErr": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListDestructAppendErr(structPrefix, fields, info.FileSet)
|
||||
},
|
||||
"shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListToRecordSuccess(structPrefix, fields)
|
||||
},
|
||||
"shouldRecordSuccessWithRPCErr": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListToRecordSuccessWithRPCErr(structPrefix, fields)
|
||||
},
|
||||
}
|
||||
|
||||
// Prepare template params. The timer layer wraps the full Hooks interface, so
|
||||
// HooksMethods includes excluded hooks too. *WithRPCErr companions only exist
|
||||
// for non-excluded hooks (see HooksWithRPCErrGenerated in client_rpc_generated.go), so the
|
||||
// excluded subset is filtered into HooksMethodsRPCErr for that loop.
|
||||
excluded := func(name string) bool { return slices.Contains(excludedPluginHooks, name) }
|
||||
templateParams := HooksTemplateParams{}
|
||||
for _, hook := range info.Hooks {
|
||||
mp := MethodParams{
|
||||
Name: hook.FuncName,
|
||||
Params: hook.Args,
|
||||
Return: hook.Results,
|
||||
}
|
||||
templateParams.HooksMethods = append(templateParams.HooksMethods, mp)
|
||||
if !excluded(hook.FuncName) {
|
||||
templateParams.HooksMethodsRPCErr = append(templateParams.HooksMethodsRPCErr, mp)
|
||||
}
|
||||
}
|
||||
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{}
|
||||
err = parsedTemplate.Execute(templateResult, &templateParams)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
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/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 {
|
||||
return slices.Contains(excluded, item)
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
Reference in New Issue
Block a user