diff --git a/server/channels/app/plugin_hooks_test.go b/server/channels/app/plugin_hooks_test.go index 1e96a45e5f..02e8bf2e01 100644 --- a/server/channels/app/plugin_hooks_test.go +++ b/server/channels/app/plugin_hooks_test.go @@ -429,6 +429,50 @@ func TestHookMessageHasBeenUpdated(t *testing.T) { require.Nil(t, err) } +func TestHookMessageHasBeenDeleted(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + var mockAPI plugintest.API + mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil) + mockAPI.On("LogDebug", "message").Return(nil).Times(1) + + tearDown, _, _ := SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) MessageHasBeenDeleted(c *plugin.Context, post *model.Post) { + p.API.LogDebug(post.Message) + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, func(*model.Manifest) plugin.API { return &mockAPI }) + defer tearDown() + + post := &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + Message: "message", + CreateAt: model.GetMillis() - 10000, + } + _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, false, true) + require.Nil(t, err) + _, err = th.App.DeletePost(th.Context, post.Id, th.BasicUser.Id) + require.Nil(t, err) +} + func TestHookFileWillBeUploaded(t *testing.T) { t.Run("rejected", func(t *testing.T) { th := Setup(t).InitBasic() diff --git a/server/channels/app/post.go b/server/channels/app/post.go index 340ff28ee9..546702dab8 100644 --- a/server/channels/app/post.go +++ b/server/channels/app/post.go @@ -1339,6 +1339,15 @@ func (a *App) DeletePost(c request.CTX, postID, deleteByID string) (*model.Post, a.deleteFlaggedPosts(post.Id) }) + pluginPost := post.ForPlugin() + pluginContext := pluginContext(c) + a.Srv().Go(func() { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.MessageHasBeenDeleted(pluginContext, pluginPost) + return true + }, plugin.MessageHasBeenDeletedID) + }) + a.Srv().Go(func() { if err = a.RemoveNotifications(c, post, channel); err != nil { a.Log().Error("DeletePost failed to delete notification", mlog.Err(err)) diff --git a/server/public/plugin/client_rpc_generated.go b/server/public/plugin/client_rpc_generated.go index 8e589f49f9..c91e2df258 100644 --- a/server/public/plugin/client_rpc_generated.go +++ b/server/public/plugin/client_rpc_generated.go @@ -290,6 +290,40 @@ func (s *hooksRPCServer) MessageHasBeenUpdated(args *Z_MessageHasBeenUpdatedArgs return nil } +func init() { + hookNameToId["MessageHasBeenDeleted"] = MessageHasBeenDeletedID +} + +type Z_MessageHasBeenDeletedArgs struct { + A *Context + B *model.Post +} + +type Z_MessageHasBeenDeletedReturns struct { +} + +func (g *hooksRPCClient) MessageHasBeenDeleted(c *Context, post *model.Post) { + _args := &Z_MessageHasBeenDeletedArgs{c, post} + _returns := &Z_MessageHasBeenDeletedReturns{} + if g.implemented[MessageHasBeenDeletedID] { + if err := g.client.Call("Plugin.MessageHasBeenDeleted", _args, _returns); err != nil { + g.log.Error("RPC call MessageHasBeenDeleted to plugin failed.", mlog.Err(err)) + } + } + +} + +func (s *hooksRPCServer) MessageHasBeenDeleted(args *Z_MessageHasBeenDeletedArgs, returns *Z_MessageHasBeenDeletedReturns) error { + if hook, ok := s.impl.(interface { + MessageHasBeenDeleted(c *Context, post *model.Post) + }); ok { + hook.MessageHasBeenDeleted(args.A, args.B) + } else { + return encodableError(fmt.Errorf("Hook MessageHasBeenDeleted called but not implemented.")) + } + return nil +} + func init() { hookNameToId["ChannelHasBeenCreated"] = ChannelHasBeenCreatedID } diff --git a/server/public/plugin/hooks.go b/server/public/plugin/hooks.go index 153ca4fae3..138f466774 100644 --- a/server/public/plugin/hooks.go +++ b/server/public/plugin/hooks.go @@ -52,6 +52,7 @@ const ( ConfigurationWillBeSavedID = 34 NotificationWillBePushedID = 35 UserHasBeenDeactivatedID = 36 + MessageHasBeenDeletedID = 37 TotalHooksID = iota ) @@ -169,6 +170,13 @@ type Hooks interface { // Minimum server version: 5.2 MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) + // MessageHasBeenDeleted is invoked after the message has been deleted from the database. + // Note that this method will be called for posts deleted by plugins, including the plugin that + // deleted the post. + // + // Minimum server version: 9.1 + MessageHasBeenDeleted(c *Context, post *model.Post) + // ChannelHasBeenCreated is invoked after the channel has been committed to the database. // // Minimum server version: 5.2 diff --git a/server/public/plugin/hooks_timer_layer_generated.go b/server/public/plugin/hooks_timer_layer_generated.go index fd561cc9df..373386dc7e 100644 --- a/server/public/plugin/hooks_timer_layer_generated.go +++ b/server/public/plugin/hooks_timer_layer_generated.go @@ -113,6 +113,12 @@ func (hooks *hooksTimerLayer) MessageHasBeenUpdated(c *Context, newPost, oldPost hooks.recordTime(startTime, "MessageHasBeenUpdated", true) } +func (hooks *hooksTimerLayer) MessageHasBeenDeleted(c *Context, post *model.Post) { + startTime := timePkg.Now() + hooks.hooksImpl.MessageHasBeenDeleted(c, post) + hooks.recordTime(startTime, "MessageHasBeenDeleted", true) +} + func (hooks *hooksTimerLayer) ChannelHasBeenCreated(c *Context, channel *model.Channel) { startTime := timePkg.Now() hooks.hooksImpl.ChannelHasBeenCreated(c, channel) diff --git a/server/public/plugin/plugintest/hooks.go b/server/public/plugin/plugintest/hooks.go index d4d9f37435..6af55e7b80 100644 --- a/server/public/plugin/plugintest/hooks.go +++ b/server/public/plugin/plugintest/hooks.go @@ -131,6 +131,11 @@ func (_m *Hooks) Implemented() ([]string, error) { return r0, r1 } +// MessageHasBeenDeleted provides a mock function with given fields: c, post +func (_m *Hooks) MessageHasBeenDeleted(c *plugin.Context, post *model.Post) { + _m.Called(c, post) +} + // MessageHasBeenPosted provides a mock function with given fields: c, post func (_m *Hooks) MessageHasBeenPosted(c *plugin.Context, post *model.Post) { _m.Called(c, post) diff --git a/server/public/plugin/product_hooks_generated.go b/server/public/plugin/product_hooks_generated.go index df2a40ba9c..ab359a6454 100644 --- a/server/public/plugin/product_hooks_generated.go +++ b/server/public/plugin/product_hooks_generated.go @@ -50,6 +50,10 @@ type MessageHasBeenUpdatedIFace interface { MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) } +type MessageHasBeenDeletedIFace interface { + MessageHasBeenDeleted(c *Context, post *model.Post) +} + type ChannelHasBeenCreatedIFace interface { ChannelHasBeenCreated(c *Context, channel *model.Channel) } @@ -220,6 +224,15 @@ func NewAdapter(productHooks any) (*HooksAdapter, error) { 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 MessageHasBeenDeleted interface. + tt = reflect.TypeOf((*MessageHasBeenDeletedIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[MessageHasBeenDeletedID] = struct{}{} + } else if _, ok := ft.MethodByName("MessageHasBeenDeleted"); ok { + return nil, errors.New("hook has MessageHasBeenDeleted method but does not implement plugin.MessageHasBeenDeleted interface") + } + // Assessing the type of the productHooks if it individually implements ChannelHasBeenCreated interface. tt = reflect.TypeOf((*ChannelHasBeenCreatedIFace)(nil)).Elem() @@ -475,6 +488,15 @@ func (a *HooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model } +func (a *HooksAdapter) MessageHasBeenDeleted(c *Context, post *model.Post) { + if _, ok := a.implemented[MessageHasBeenDeletedID]; !ok { + panic("product hooks must implement MessageHasBeenDeleted") + } + + a.productHooks.(MessageHasBeenDeletedIFace).MessageHasBeenDeleted(c, post) + +} + func (a *HooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) { if _, ok := a.implemented[ChannelHasBeenCreatedID]; !ok { panic("product hooks must implement ChannelHasBeenCreated")