diff --git a/app/channels.go b/app/channels.go index 9114533502..bc4416f000 100644 --- a/app/channels.go +++ b/app/channels.go @@ -316,11 +316,10 @@ type hooksService struct { ch *Channels } -func (s *hooksService) RegisterHooks(productID string, hooks product.Hooks) error { +func (s *hooksService) RegisterHooks(productID string, hooks any) error { if s.ch.pluginsEnvironment == nil { return errors.New("could not find plugins environment") } - s.ch.pluginsEnvironment.AddProduct(productID, hooks) - return nil + return s.ch.pluginsEnvironment.AddProduct(productID, hooks) } diff --git a/plugin/environment.go b/plugin/environment.go index c5477b8612..51684ac34c 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -301,8 +301,20 @@ func (env *Environment) Activate(id string) (manifest *model.Manifest, activated return pluginInfo.Manifest, true, nil } -func (env *Environment) AddProduct(productID string, hooks ProductHooks) { - env.registeredProducts.Store(productID, newRegisteredProduct(productID, hooks)) +func (env *Environment) AddProduct(productID string, hooks any) error { + prod, err := newAdapter(hooks) + if err != nil { + return err + } + + rp := ®isteredProduct{ + productID: productID, + adapter: prod, + } + + env.registeredProducts.Store(productID, rp) + + return nil } func (env *Environment) RemoveProduct(productID string) { diff --git a/plugin/interface_generator/main.go b/plugin/interface_generator/main.go index a6217d20b4..6f3d6d9802 100644 --- a/plugin/interface_generator/main.go +++ b/plugin/interface_generator/main.go @@ -21,6 +21,29 @@ import ( "golang.org/x/tools/imports" ) +var excludedPluginHooks = []string{ + "FileWillBeUploaded", + "Implemented", + "LoadPluginConfiguration", + "InstallPlugin", + "LogDebug", + "LogError", + "LogInfo", + "LogWarn", + "MessageWillBePosted", + "MessageWillBeUpdated", + "OnActivate", + "PluginHTTP", + "ServeHTTP", +} + +var excludedProductHooks = []string{ + "Implemented", + "OnActivate", + "OnDeactivate", + "ServeHTTP", +} + type IHookEntry struct { FuncName string Args *ast.FieldList @@ -360,6 +383,61 @@ func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name {{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{}), + } + 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. @@ -507,6 +585,39 @@ func generateHooksGlue(info *PluginInterfaceInfo) { } } +func generateProductHooksInterfaces(info *PluginInterfaceInfo) { + templateFunctions := map[string]interface{}{ + "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 := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), "product_hooks_generated.go"), formatted, 0664); err != nil { + panic(err) + } +} + func generatePluginTimerLayer(info *PluginInterfaceInfo) { templateFunctions := map[string]interface{}{ "funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) }, @@ -573,23 +684,11 @@ func getPluginPackageDir() string { return dirs[0] } -func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo { +func removeExcluded(info *PluginInterfaceInfo, excluded []string) *PluginInterfaceInfo { + newIface := &PluginInterfaceInfo{ + FileSet: info.FileSet, + } toBeExcluded := func(item string) bool { - excluded := []string{ - "FileWillBeUploaded", - "Implemented", - "LoadPluginConfiguration", - "InstallPlugin", - "LogDebug", - "LogError", - "LogInfo", - "LogWarn", - "MessageWillBePosted", - "MessageWillBeUpdated", - "OnActivate", - "PluginHTTP", - "ServeHTTP", - } for _, exclusion := range excluded { if exclusion == item { return true @@ -603,7 +702,7 @@ func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo { hooksResult = append(hooksResult, hook) } } - info.Hooks = hooksResult + newIface.Hooks = hooksResult apiResult := make([]IHookEntry, 0, len(info.API)) for _, api := range info.API { @@ -611,20 +710,23 @@ func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo { apiResult = append(apiResult, api) } } - info.API = apiResult + newIface.API = apiResult - return info + return newIface } func main() { pluginPackageDir := getPluginPackageDir() - log.Println("Generating plugin hooks glue") forRPC, err := getPluginInfo(pluginPackageDir) if err != nil { fmt.Println("Unable to get plugin info: " + err.Error()) } - generateHooksGlue(removeExcluded(forRPC)) + 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") diff --git a/plugin/product.go b/plugin/product.go index 18f5678b37..cd56dec569 100644 --- a/plugin/product.go +++ b/plugin/product.go @@ -4,144 +4,38 @@ package plugin import ( - "errors" - "io" "net/http" - - "github.com/mattermost/mattermost-server/v6/model" ) -// ProductHooks is a subset of Hooks -type ProductHooks interface { - OnConfigurationChange() error - MessageWillBePosted(ctx *Context, post *model.Post) (*model.Post, string) - MessageWillBeUpdated(ctx *Context, newPost, oldPost *model.Post) (*model.Post, string) - OnPluginClusterEvent(ctx *Context, ev model.PluginClusterEvent) - OnWebSocketDisconnect(webConnID, userID string) - OnWebSocketConnect(webConnID, userID string) - WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) -} - type registeredProduct struct { - productID string - implemented map[int]struct{} - adapter Hooks + productID string + adapter Hooks } func (rp *registeredProduct) Implements(hookId int) bool { - _, ok := rp.implemented[hookId] + adapter, ok := rp.adapter.(*hooksAdapter) + if !ok { + return false + } + + _, ok = adapter.implemented[hookId] return ok } -type hooksAdapter struct { - productHooks ProductHooks -} - -func newRegisteredProduct(pluginID string, productHooks ProductHooks) *registeredProduct { - return ®isteredProduct{ - productID: pluginID, - implemented: map[int]struct{}{ - OnConfigurationChangeID: {}, - MessageWillBePostedID: {}, - MessageWillBeUpdatedID: {}, - OnPluginClusterEventID: {}, - OnWebSocketConnectID: {}, - OnWebSocketDisconnectID: {}, - WebSocketMessageHasBeenPostedID: {}, - }, - adapter: &hooksAdapter{ - productHooks: productHooks, - }, - } -} - -func (a *hooksAdapter) OnActivate() error { - return errors.New("not implemented") -} - +// Implemented method is overridden intentionally to prevent calling it from outside. func (a *hooksAdapter) Implemented() ([]string, error) { - return nil, errors.New("not implemented") + 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 errors.New("not implemented") -} - -func (a *hooksAdapter) OnConfigurationChange() error { - return a.productHooks.OnConfigurationChange() + return nil } +// ServeHTTP is overridden intentionally as product should not call it. func (a *hooksAdapter) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {} - -func (a *hooksAdapter) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - return nil, model.NewAppError("ExecuteCommand", "api.command.execute_command.start.app_error", nil, "not implemented", http.StatusNotImplemented) -} - -func (a *hooksAdapter) UserHasBeenCreated(c *Context, user *model.User) {} - -func (a *hooksAdapter) UserWillLogIn(c *Context, user *model.User) string { - return "" -} - -func (a *hooksAdapter) UserHasLoggedIn(c *Context, user *model.User) {} - -func (a *hooksAdapter) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) { - return a.productHooks.MessageWillBePosted(c, post) -} - -func (a *hooksAdapter) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) { - return a.productHooks.MessageWillBeUpdated(c, newPost, oldPost) -} - -func (a *hooksAdapter) MessageHasBeenPosted(c *Context, post *model.Post) {} - -func (a *hooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) {} - -func (a *hooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) {} - -func (a *hooksAdapter) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) { -} - -func (a *hooksAdapter) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) { -} - -func (a *hooksAdapter) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) { -} - -func (a *hooksAdapter) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {} - -func (a *hooksAdapter) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) { - return nil, "" -} - -func (a *hooksAdapter) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) {} - -func (a *hooksAdapter) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) {} - -func (a *hooksAdapter) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) { - a.productHooks.OnPluginClusterEvent(c, ev) -} - -func (a *hooksAdapter) OnWebSocketConnect(webConnID, userID string) { - a.productHooks.OnWebSocketConnect(webConnID, userID) -} - -func (a *hooksAdapter) OnWebSocketDisconnect(webConnID, userID string) { - a.productHooks.OnWebSocketDisconnect(webConnID, userID) -} - -func (a *hooksAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) { - a.productHooks.WebSocketMessageHasBeenPosted(webConnID, userID, req) -} - -func (a *hooksAdapter) RunDataRetention(nowTime, batchSize int64) (int64, error) { - return -1, errors.New("not implemented") -} - -func (a *hooksAdapter) OnInstall(c *Context, event model.OnInstallEvent) error { - return errors.New("not implemented") -} - -func (a *hooksAdapter) OnSendDailyTelemetry() {} - -func (a *hooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) {} diff --git a/plugin/product_hooks_generated.go b/plugin/product_hooks_generated.go new file mode 100644 index 0000000000..2431113115 --- /dev/null +++ b/plugin/product_hooks_generated.go @@ -0,0 +1,580 @@ +// 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/v6/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 hooksAdapter struct { + implemented map[int]struct{} + productHooks any +} + +func newAdapter(productHooks any) (*hooksAdapter, error) { + a := &hooksAdapter{ + implemented: make(map[int]struct{}), + } + 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") + } + + 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) + +} diff --git a/product/api.go b/product/api.go index e142f57998..ab711edbae 100644 --- a/product/api.go +++ b/product/api.go @@ -10,7 +10,6 @@ import ( "github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/model" - "github.com/mattermost/mattermost-server/v6/plugin" "github.com/mattermost/mattermost-server/v6/shared/filestore" ) @@ -108,19 +107,25 @@ type ConfigService interface { SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError) } -// Hooks is an interim solution for enabling plugin hooks on the multi-product architecture. After the -// focalboard migration is completed, this API should replaced with something else that would enable a -// product to register any hook. Currently this is added to unblock the migration. -type Hooks interface { - plugin.ProductHooks -} - // HooksService is the API for adding exiting plugin hooks to the server so that they can be called as // they were. This Service is required to be used after the products start. Otherwise it will return an error. // // The service shall be registered via app.HooksKey service key. type HooksService interface { - RegisterHooks(productID string, hooks Hooks) error + // RegisterHook checks whether if the 'hooks' implements any method of plugin.Hooks methods. Rather than + // using the whole plugin.Hooks interface with its 20+ methods, a product can implement any exiting method + // of plugin.Hooks w/o requiring to declare which method they implemented or not. This is going to be + // checked on runtime. We have individual interfaces for each method declared in plugin.Hooks interface. + // Hence, while registering a product, the service will check if the product implements any of these individual + // interfaces. If so, a map of hook IDs that are implemented will be used to call the hooks. The method will + // return an error in case if there is an incorrect implementation of the any of the individual interface in runtime. + // Consider checking plugin.Hooks for the reference. + // Following methods are not allowed to be implemented in the product: + // - plugin.Hooks.OnActivate + // - plugin.Hooks.OnDeactivate + // - plugin.Hooks.Implemented + // - plugin.Hooks.ServeHTTP + RegisterHooks(productID string, hooks any) error } // FilestoreService is the API for accessing the file store.