From 9e79ca71609fe94a441983ffd8ab5bcd9917dabe Mon Sep 17 00:00:00 2001 From: emmyni <44761757+emmyni@users.noreply.github.com> Date: Mon, 5 Dec 2022 12:10:00 -0500 Subject: [PATCH 1/2] [MM-48472]: Update the Upgrade Confirmation Email for yearly subscriptions (#21766) --- api4/cloud.go | 30 ++++++++++++++++++++-- app/app_iface.go | 2 +- app/cloud.go | 4 +-- app/email/email.go | 10 ++++++-- app/email/email_test.go | 38 ++++++++++++++++++++++++++-- app/email/mocks/ServiceInterface.go | 10 ++++---- app/email/service.go | 2 +- app/opentracing/opentracing_layer.go | 4 +-- i18n/en.json | 14 +++++++--- 9 files changed, 94 insertions(+), 20 deletions(-) diff --git a/api4/cloud.go b/api4/cloud.go index 173d975279..3f9106710c 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -136,9 +136,16 @@ func changeSubscription(c *Context, w http.ResponseWriter, r *http.Request) { return } + product, err := c.App.Cloud().GetCloudProduct(c.AppContext.Session().UserId, subscriptionChange.ProductID) + if err != nil || product == nil { + c.Logger.Error("Error finding the new cloud product", mlog.Err(err)) + } + + isYearly := product.IsYearly() + // Log failures for purchase confirmation email, but don't show an error to the user so as not to confuse them // At this point, the upgrade is complete. - if appErr := c.App.SendUpgradeConfirmationEmail(); appErr != nil { + if appErr := c.App.SendUpgradeConfirmationEmail(isYearly); appErr != nil { c.Logger.Error("Error sending purchase confirmation email", mlog.Err(appErr)) } @@ -636,7 +643,26 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { return } case model.EventTypeSendUpgradeConfirmationEmail: - if nErr := c.App.SendUpgradeConfirmationEmail(); nErr != nil { + + // isYearly determines whether to send the yearly or monthly Upgrade email + isYearly := false + if event.Subscription != nil && event.CloudWorkspaceOwner != nil { + user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName) + if appErr != nil { + c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode) + return + } + + // Get the current cloud product to determine whether it's a monthly or yearly product + product, err := c.App.Cloud().GetCloudProduct(user.Id, event.Subscription.ProductID) + if err != nil { + c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError) + return + } + isYearly = product.IsYearly() + } + + if nErr := c.App.SendUpgradeConfirmationEmail(isYearly); nErr != nil { c.Err = nErr return } diff --git a/app/app_iface.go b/app/app_iface.go index 948060d0f6..781f12b7e1 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -1038,7 +1038,7 @@ type AppIface interface { SendPasswordReset(email string, siteURL string) (bool, *model.AppError) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError SendTestPushNotification(deviceID string) string - SendUpgradeConfirmationEmail() *model.AppError + SendUpgradeConfirmationEmail(isYearly bool) *model.AppError ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) SessionHasPermissionTo(session model.Session, permission *model.Permission) bool SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool diff --git a/app/cloud.go b/app/cloud.go index fd2d4fb23e..f045c57311 100644 --- a/app/cloud.go +++ b/app/cloud.go @@ -174,7 +174,7 @@ func getNextBillingDateString() string { return fmt.Sprintf("%s %d, %d", t.Month(), t.Day(), t.Year()) } -func (a *App) SendUpgradeConfirmationEmail() *model.AppError { +func (a *App) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError { sysAdmins, e := a.getSysAdminsEmailRecipients() if e != nil { return e @@ -200,7 +200,7 @@ func (a *App) SendUpgradeConfirmationEmail() *model.AppError { name = admin.Username } - err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS()) + err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS(), isYearly) if err != nil { a.Log().Error("Error sending trial ended email to", mlog.String("email", admin.Email), mlog.Err(err)) countNotOks++ diff --git a/app/email/email.go b/app/email/email.go index b0cd66e7dd..d434d94935 100644 --- a/app/email/email.go +++ b/app/email/email.go @@ -233,13 +233,13 @@ func (es *Service) SendWelcomeEmail(userID string, email string, verified bool, return nil } -func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, locale, siteURL, workspaceName string) error { +func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, locale, siteURL, workspaceName string, isYearly bool) error { T := i18n.GetUserTranslations(locale) subject := T("api.templates.cloud_upgrade_confirmation.subject") data := es.NewEmailTemplateData(locale) data.Props["Title"] = T("api.templates.cloud_upgrade_confirmation.title") - data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation.subtitle", map[string]any{"WorkspaceName": workspaceName, "Date": date}) + data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_monthly.subtitle", map[string]any{"WorkspaceName": workspaceName, "Date": date}) data.Props["SiteURL"] = siteURL data.Props["ButtonURL"] = siteURL data.Props["Button"] = T("api.templates.cloud_welcome_email.button") @@ -247,6 +247,12 @@ func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, loca data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail + if isYearly { + data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_yearly.subtitle", map[string]any{"WorkspaceName": workspaceName}) + data.Props["ButtonURL"] = siteURL + "/admin_console/billing/billing_history" + data.Props["Button"] = T("api.templates.cloud_welcome_email.yearly_plan_button") + } + body, err := es.templatesContainer.RenderToString("cloud_upgrade_confirmation", data) if err != nil { return err diff --git a/app/email/email_test.go b/app/email/email_test.go index 0fe3b237e0..2ad00123c3 100644 --- a/app/email/email_test.go +++ b/app/email/email_test.go @@ -258,7 +258,7 @@ func TestSendCloudUpgradedEmail(t *testing.T) { emailTo := "testclouduser@example.com" emailToUsername := strings.Split(emailTo, "@")[0] - t.Run("SendCloudUpgradedEmail", func(t *testing.T) { + t.Run("SendCloudMonthlyUpgradedEmail", func(t *testing.T) { verifyMailbox := func(t *testing.T) { t.Helper() @@ -278,10 +278,44 @@ func TestSendCloudUpgradedEmail(t *testing.T) { require.NoError(t, err, "Could not get message from mailbox") require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text) require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "You'll be billed from", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "Open Mattermost", "Wrong received message %s", resultsEmail.Body.Text) } mail.DeleteMailBox(emailTo) - err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName") + // Send Update to Monthly Plan email + err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", false) + require.NoError(t, err) + + verifyMailbox(t) + }) + + t.Run("SendCloudYearlyUpgradedEmail", func(t *testing.T) { + verifyMailbox := func(t *testing.T) { + t.Helper() + + var resultsMailbox mail.JSONMessageHeaderInbucket + err2 := mail.RetryInbucket(5, func() error { + var err error + resultsMailbox, err = mail.GetMailBox(emailTo) + return err + }) + if err2 != nil { + t.Skipf("No email was received, maybe due load on the server: %v", err2) + } + + require.Len(t, resultsMailbox, 1) + require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient") + resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID) + require.NoError(t, err, "Could not get message from mailbox") + require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text) + require.Contains(t, resultsEmail.Body.Text, "View your invoice", "Wrong received message %s", resultsEmail.Body.Text) + } + mail.DeleteMailBox(emailTo) + + // Send Update to Monthly Plan email + err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", true) require.NoError(t, err) verifyMailbox(t) diff --git a/app/email/mocks/ServiceInterface.go b/app/email/mocks/ServiceInterface.go index 08cddbe4ad..98b39f90fe 100644 --- a/app/email/mocks/ServiceInterface.go +++ b/app/email/mocks/ServiceInterface.go @@ -125,13 +125,13 @@ func (_m *ServiceInterface) SendChangeUsernameEmail(newUsername string, _a1 stri return r0 } -// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName -func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string) error { - ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName) +// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly +func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string, isYearly bool) error { + ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly) var r0 error - if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) error); ok { - r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName) + if rf, ok := ret.Get(0).(func(string, string, string, string, string, string, bool) error); ok { + r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly) } else { r0 = ret.Error(0) } diff --git a/app/email/service.go b/app/email/service.go index 0f9158e343..d8745a1735 100644 --- a/app/email/service.go +++ b/app/email/service.go @@ -129,7 +129,7 @@ type ServiceInterface interface { SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error SendSignInChangeEmail(email, method, locale, siteURL string) error SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error - SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string) error + SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string, isYearly bool) error SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error SendPasswordChangeEmail(email, method, locale, siteURL string) error SendUserAccessTokenAddedEmail(email, locale, siteURL string) error diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 498451174b..1e25574b70 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -15546,7 +15546,7 @@ func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string { return resultVar0 } -func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail() *model.AppError { +func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendUpgradeConfirmationEmail") @@ -15558,7 +15558,7 @@ func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail() *model.AppError { }() defer span.Finish() - resultVar0 := a.app.SendUpgradeConfirmationEmail() + resultVar0 := a.app.SendUpgradeConfirmationEmail(isYearly) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) diff --git a/i18n/en.json b/i18n/en.json index 91c3351aaf..43b65baa82 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3164,12 +3164,16 @@ "translation": "Mattermost Upgrade Confirmation" }, { - "id": "api.templates.cloud_upgrade_confirmation.subtitle", + "id": "api.templates.cloud_upgrade_confirmation.title", + "translation": "You are now upgraded!" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", "translation": "Your {{.WorkspaceName}} workspace has now been upgraded. You'll be billed from {{.Date}}" }, { - "id": "api.templates.cloud_upgrade_confirmation.title", - "translation": "You are now upgraded!" + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "Your {{.WorkspaceName}} workspace has now been upgraded." }, { "id": "api.templates.cloud_welcome_email.add_apps_info", @@ -3239,6 +3243,10 @@ "id": "api.templates.cloud_welcome_email.title", "translation": "Your workspace is ready to go!" }, + { + "id": "api.templates.cloud_welcome_email.yearly_plan_button", + "translation": "View your invoice" + }, { "id": "api.templates.copyright", "translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301" From 43e26ccda2db693b10aa98cd5cf5d93c480abf1c Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Mon, 5 Dec 2022 22:16:35 +0300 Subject: [PATCH 2/2] MPA: move product hooks out of plugins environment (#21772) * product: add new hooks manager for porducts * move product hooks out of plugins environment * add hooks for plugin --- api4/websocket.go | 2 +- app/channel.go | 10 ++-- app/channels.go | 30 +++++++++++- app/cluster_handlers.go | 11 +++-- app/file.go | 2 +- app/login.go | 4 +- app/onboarding.go | 2 +- app/platform/service.go | 15 ++++-- app/platform/web_conn.go | 60 ++++++++++------------- app/platform/web_conn_test.go | 23 +++++++-- app/platform/web_hub_test.go | 3 +- app/plugin.go | 6 +-- app/plugin_hooks_test.go | 6 +-- app/post.go | 8 +-- app/reaction.go | 4 +- app/server.go | 4 ++ app/team.go | 4 +- app/upload.go | 2 +- app/user.go | 2 +- app/web_conn.go | 2 +- plugin/environment.go | 45 ----------------- plugin/interface_generator/main.go | 8 +-- plugin/product.go | 18 +++---- plugin/product_hooks_generated.go | 68 ++++++++++++------------- product/hooks.go | 79 ++++++++++++++++++++++++++++++ 25 files changed, 249 insertions(+), 169 deletions(-) create mode 100644 product/hooks.go diff --git a/api4/websocket.go b/api4/websocket.go index 5f1cb2cdd3..d2c1c10f44 100644 --- a/api4/websocket.go +++ b/api4/websocket.go @@ -61,7 +61,7 @@ func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) { } } - wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv().Channels().GetPluginsEnvironment) + wc := c.App.Srv().Platform().NewWebConn(cfg, c.App, c.App.Srv().Channels()) if c.AppContext.Session().UserId != "" { c.App.Srv().Platform().HubRegister(wc) } diff --git a/app/channel.go b/app/channel.go index 0fe00de4e6..273555f6b8 100644 --- a/app/channel.go +++ b/app/channel.go @@ -346,7 +346,7 @@ func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember boo if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ChannelHasBeenCreated(pluginContext, sc) return true }, plugin.ChannelHasBeenCreatedID) @@ -432,7 +432,7 @@ func (a *App) handleCreationEvent(c request.CTX, userID, otherUserID string, cha if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ChannelHasBeenCreated(pluginContext, channel) return true }, plugin.ChannelHasBeenCreatedID) @@ -1602,7 +1602,7 @@ func (a *App) AddChannelMember(c request.CTX, userID string, channel *model.Chan if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor) return true }, plugin.UserHasJoinedChannelID) @@ -2180,7 +2180,7 @@ func (a *App) JoinChannel(c request.CTX, channel *model.Channel, userID string) if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedChannel(pluginContext, cm, nil) return true }, plugin.UserHasJoinedChannelID) @@ -2492,7 +2492,7 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLeftChannel(pluginContext, cm, actorUser) return true }, plugin.UserHasLeftChannelID) diff --git a/app/channels.go b/app/channels.go index 2c9f1cee51..31ef2d429c 100644 --- a/app/channels.go +++ b/app/channels.go @@ -322,5 +322,33 @@ func (s *hooksService) RegisterHooks(productID string, hooks any) error { return errors.New("could not find plugins environment") } - return s.ch.pluginsEnvironment.AddProduct(productID, hooks) + return s.ch.srv.hooksManager.AddProduct(productID, hooks) +} + +func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) { + if env := ch.pluginsEnvironment; env != nil { + env.RunMultiPluginHook(hookRunnerFunc, hookId) + } + + // run hook for the products + ch.srv.hooksManager.RunMultiHook(hookRunnerFunc, hookId) +} + +func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) { + var hooks plugin.Hooks + if env := ch.pluginsEnvironment; env != nil { + // we intentionally ignore the error here, because the id can be a product id + // we are going to check if we have the hooks or not + hooks, _ = env.HooksForPlugin(id) + if hooks != nil { + return hooks, nil + } + } + + hooks = ch.srv.hooksManager.HooksForProduct(id) + if hooks != nil { + return hooks, nil + } + + return nil, fmt.Errorf("could not find hooks for id %s", id) } diff --git a/app/cluster_handlers.go b/app/cluster_handlers.go index 182e6ede01..3fa90abf1e 100644 --- a/app/cluster_handlers.go +++ b/app/cluster_handlers.go @@ -28,10 +28,6 @@ func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) { } func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) { - env := s.Channels().GetPluginsEnvironment() - if env == nil { - return - } if msg.Props == nil { mlog.Warn("ClusterMessage.Props for plugin event should not be nil") return @@ -48,7 +44,12 @@ func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) { return } - hooks, err := env.HooksForPlugin(pluginID) + channels, ok := s.products["channels"].(*Channels) + if !ok { + return + } + + hooks, err := channels.HooksForPluginOrProduct(pluginID) if err != nil { mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err)) return diff --git a/app/file.go b/app/file.go index 2f0050dff6..67a227cab0 100644 --- a/app/file.go +++ b/app/file.go @@ -898,7 +898,7 @@ func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTe if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { var rejectionError *model.AppError pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { var newBytes bytes.Buffer replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes) if rejectionReason != "" { diff --git a/app/login.go b/app/login.go index bd63787546..98279d7f2f 100644 --- a/app/login.go +++ b/app/login.go @@ -160,7 +160,7 @@ func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { var rejectionReason string pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { rejectionReason = hooks.UserWillLogIn(pluginContext, user) return rejectionReason == "" }, plugin.UserWillLogInID) @@ -229,7 +229,7 @@ func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLoggedIn(pluginContext, user) return true }, plugin.UserHasLoggedInID) diff --git a/app/onboarding.go b/app/onboarding.go index 9a9e25739c..d76525f017 100644 --- a/app/onboarding.go +++ b/app/onboarding.go @@ -53,7 +53,7 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo return } - hooks, err := pluginsEnvironment.HooksForPlugin(id) + hooks, err := a.ch.HooksForPluginOrProduct(id) if err != nil { mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", id), mlog.Err(err)) return diff --git a/app/platform/service.go b/app/platform/service.go index 0fa635d5d8..be3f4ed805 100644 --- a/app/platform/service.go +++ b/app/platform/service.go @@ -95,7 +95,12 @@ type PlatformService struct { additionalClusterHandlers map[model.ClusterEvent]einterfaces.ClusterMessageHandler sharedChannelService SharedChannelServiceIFace - pluginEnv *plugin.Environment + pluginEnv HookRunner +} + +type HookRunner interface { + RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) + GetPluginsEnvironment() *plugin.Environment } // New creates a new PlatformService. @@ -426,17 +431,17 @@ func (ps *PlatformService) SetSharedChannelService(s SharedChannelServiceIFace) ps.sharedChannelService = s } -func (ps *PlatformService) SetPluginsEnvironment(env *plugin.Environment) { - ps.pluginEnv = env +func (ps *PlatformService) SetPluginsEnvironment(runner HookRunner) { + ps.pluginEnv = runner } // GetPluginStatuses meant to be used by cluster implementation func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { - if ps.pluginEnv == nil { + if ps.pluginEnv == nil || ps.pluginEnv.GetPluginsEnvironment() == nil { return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - pluginStatuses, err := ps.pluginEnv.Statuses() + pluginStatuses, err := ps.pluginEnv.GetPluginsEnvironment().Statuses() if err != nil { return nil, model.NewAppError("GetPluginStatuses", "app.plugin.get_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } diff --git a/app/platform/web_conn.go b/app/platform/web_conn.go index d9fa60cd5d..4956d49a73 100644 --- a/app/platform/web_conn.go +++ b/app/platform/web_conn.go @@ -72,15 +72,15 @@ type WebConnConfig struct { // It contains all the necessary state to manage sending/receiving data to/from // a websocket. type WebConn struct { - sessionExpiresAt int64 // This should stay at the top for 64-bit alignment of 64-bit words accessed atomically - Platform *PlatformService - Suite SuiteIFace - PluginsEnvironment func() *plugin.Environment - WebSocket *websocket.Conn - T i18n.TranslateFunc - Locale string - Sequence int64 - UserId string + sessionExpiresAt int64 // This should stay at the top for 64-bit alignment of 64-bit words accessed atomically + Platform *PlatformService + Suite SuiteIFace + HookRunner HookRunner + WebSocket *websocket.Conn + T i18n.TranslateFunc + Locale string + Sequence int64 + UserId string allChannelMembers map[string]string lastAllChannelMembersTime int64 @@ -162,7 +162,7 @@ func (ps *PlatformService) PopulateWebConnConfig(s *model.Session, cfg *WebConnC } // NewWebConn returns a new WebConn instance. -func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, envFn func() *plugin.Environment) *WebConn { +func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runner HookRunner) *WebConn { if cfg.Session.UserId != "" { ps.Go(func() { suite.SetStatusOnline(cfg.Session.UserId, false) @@ -200,7 +200,7 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, envF wc := &WebConn{ Platform: ps, Suite: suite, - PluginsEnvironment: envFn, + HookRunner: runner, send: cfg.activeQueue, deadQueue: cfg.deadQueue, deadQueuePointer: cfg.deadQueuePointer, @@ -222,14 +222,12 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, envF wc.SetSessionExpiresAt(cfg.Session.ExpiresAt) wc.SetConnectionID(cfg.ConnectionID) - if pluginsEnvironment := wc.PluginsEnvironment(); pluginsEnvironment != nil { - wc.Platform.Go(func() { - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { - hooks.OnWebSocketConnect(wc.GetConnectionID(), wc.UserId) - return true - }, plugin.OnWebSocketConnectID) - }) - } + wc.Platform.Go(func() { + wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.OnWebSocketConnect(wc.GetConnectionID(), wc.UserId) + return true + }, plugin.OnWebSocketConnectID) + }) return wc } @@ -238,12 +236,10 @@ func (wc *WebConn) pluginPostedConsumer(wg *sync.WaitGroup) { defer wg.Done() for msg := range wc.pluginPosted { - if pluginsEnvironment := wc.PluginsEnvironment(); pluginsEnvironment != nil { - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { - hooks.WebSocketMessageHasBeenPosted(msg.connectionID, msg.userID, msg.req) - return true - }, plugin.WebSocketMessageHasBeenPostedID) - } + wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.WebSocketMessageHasBeenPosted(msg.connectionID, msg.userID, msg.req) + return true + }, plugin.WebSocketMessageHasBeenPostedID) } } @@ -328,14 +324,12 @@ func (wc *WebConn) Pump() { wc.Platform.HubUnregister(wc) close(wc.pumpFinished) - if pluginsEnvironment := wc.PluginsEnvironment(); pluginsEnvironment != nil { - wc.Platform.Go(func() { - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { - hooks.OnWebSocketDisconnect(wc.GetConnectionID(), wc.UserId) - return true - }, plugin.OnWebSocketDisconnectID) - }) - } + wc.Platform.Go(func() { + wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.OnWebSocketDisconnect(wc.GetConnectionID(), wc.UserId) + return true + }, plugin.OnWebSocketDisconnectID) + }) } func (wc *WebConn) readPump() { diff --git a/app/platform/web_conn_test.go b/app/platform/web_conn_test.go index b12232bd61..149f7d019d 100644 --- a/app/platform/web_conn_test.go +++ b/app/platform/web_conn_test.go @@ -5,6 +5,7 @@ package platform import ( "bytes" + "errors" "net" "net/http" "net/http/httptest" @@ -18,13 +19,27 @@ import ( "github.com/mattermost/mattermost-server/v6/plugin" ) +type hookRunner struct { +} + +func (h *hookRunner) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) { + +} +func (h *hookRunner) HooksForPlugin(id string) (plugin.Hooks, error) { + return nil, errors.New("not implemented") +} + +func (h *hookRunner) GetPluginsEnvironment() *plugin.Environment { + return nil +} + func TestWebConnAddDeadQueue(t *testing.T) { th := Setup(t) defer th.TearDown() wc := th.Service.NewWebConn(&WebConnConfig{ WebSocket: &websocket.Conn{}, - }, th.Suite, func() *plugin.Environment { return nil }) + }, th.Suite, &hookRunner{}) for i := 0; i < 2; i++ { msg := &model.WebSocketEvent{} @@ -53,7 +68,7 @@ func TestWebConnIsInDeadQueue(t *testing.T) { wc := th.Service.NewWebConn(&WebConnConfig{ WebSocket: &websocket.Conn{}, - }, th.Suite, func() *plugin.Environment { return nil }) + }, th.Suite, &hookRunner{}) var i int for ; i < 2; i++ { @@ -114,7 +129,7 @@ func TestWebConnClearDeadQueue(t *testing.T) { wc := th.Service.NewWebConn(&WebConnConfig{ WebSocket: &websocket.Conn{}, - }, th.Suite, func() *plugin.Environment { return nil }) + }, th.Suite, &hookRunner{}) var i int for ; i < 2; i++ { @@ -140,7 +155,7 @@ func TestWebConnDrainDeadQueue(t *testing.T) { cfg := &WebConnConfig{ WebSocket: c, } - return th.Service.NewWebConn(cfg, th.Suite, func() *plugin.Environment { return nil }) + return th.Service.NewWebConn(cfg, th.Suite, &hookRunner{}) } t.Run("Empty Queue", func(t *testing.T) { diff --git a/app/platform/web_hub_test.go b/app/platform/web_hub_test.go index f73836e1ad..e3f6e4ddbe 100644 --- a/app/platform/web_hub_test.go +++ b/app/platform/web_hub_test.go @@ -17,7 +17,6 @@ import ( platform_mocks "github.com/mattermost/mattermost-server/v6/app/platform/mocks" "github.com/mattermost/mattermost-server/v6/model" - "github.com/mattermost/mattermost-server/v6/plugin" "github.com/mattermost/mattermost-server/v6/shared/i18n" "github.com/mattermost/mattermost-server/v6/store/storetest/mocks" "github.com/mattermost/mattermost-server/v6/testlib" @@ -50,7 +49,7 @@ func registerDummyWebConn(t *testing.T, th *TestHelper, addr net.Addr, session * TFunc: i18n.IdentityTfunc(), Locale: "en", } - wc := th.Service.NewWebConn(cfg, th.Suite, func() *plugin.Environment { return nil }) + wc := th.Service.NewWebConn(cfg, th.Suite, &hookRunner{}) th.Service.HubRegister(wc) go wc.Pump() return wc diff --git a/app/plugin.go b/app/plugin.go index b48503de5d..93d47fd5b6 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -93,7 +93,7 @@ func (ch *Channels) SetPluginsEnvironment(pluginsEnvironment *plugin.Environment defer ch.pluginsLock.Unlock() ch.pluginsEnvironment = pluginsEnvironment - ch.srv.Platform().SetPluginsEnvironment(pluginsEnvironment) + ch.srv.Platform().SetPluginsEnvironment(ch) } func (ch *Channels) syncPluginsActiveState() { @@ -213,7 +213,7 @@ func (a *App) InitPlugins(c *request.Context, pluginDir, webappPluginDir string) func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir string) { // Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment. defer func() { - ch.srv.Platform().SetPluginsEnvironment(ch.pluginsEnvironment) + ch.srv.Platform().SetPluginsEnvironment(ch) }() ch.pluginsLock.RLock() @@ -279,7 +279,7 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s ch.syncPluginsActiveState() } if pluginsEnvironment := ch.GetPluginsEnvironment(); pluginsEnvironment != nil { - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + ch.RunMultiHook(func(hooks plugin.Hooks) bool { if err := hooks.OnConfigurationChange(); err != nil { ch.srv.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err)) } diff --git a/app/plugin_hooks_test.go b/app/plugin_hooks_test.go index 1b1673be93..7cd81f8fae 100644 --- a/app/plugin_hooks_test.go +++ b/app/plugin_hooks_test.go @@ -1234,7 +1234,7 @@ func TestHookRunDataRetention(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.GetPluginsEnvironment().RunMultiPluginHook(func(hooks plugin.Hooks) bool { + th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { n, _ := hooks.RunDataRetention(0, 0) // Ensure return it correct assert.Equal(t, int64(100), n) @@ -1278,7 +1278,7 @@ func TestHookOnSendDailyTelemetry(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.GetPluginsEnvironment().RunMultiPluginHook(func(hooks plugin.Hooks) bool { + th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.OnSendDailyTelemetry() hookCalled = true @@ -1322,7 +1322,7 @@ func TestHookOnCloudLimitsUpdated(t *testing.T) { require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) hookCalled := false - th.App.GetPluginsEnvironment().RunMultiPluginHook(func(hooks plugin.Hooks) bool { + th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool { hooks.OnCloudLimitsUpdated(nil) hookCalled = true diff --git a/app/post.go b/app/post.go index 9fafd61f9b..ace950239e 100644 --- a/app/post.go +++ b/app/post.go @@ -270,7 +270,7 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel } var rejectionError *model.AppError pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { replacementPost, rejectionReason := hooks.MessageWillBePosted(pluginContext, post.ForPlugin()) if rejectionReason != "" { id := "Post rejected by plugin. " + rejectionReason @@ -332,7 +332,7 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel pluginPost := rpost.ForPlugin() a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.MessageHasBeenPosted(pluginContext, pluginPost) return true }, plugin.MessageHasBeenPostedID) @@ -661,7 +661,7 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { var rejectionReason string pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost.ForPlugin(), oldPost.ForPlugin()) return post != nil }, plugin.MessageWillBeUpdatedID) @@ -689,7 +689,7 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool) pluginNewPost := newPost.ForPlugin() a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.MessageHasBeenUpdated(pluginContext, pluginNewPost, pluginOldPost) return true }, plugin.MessageHasBeenUpdatedID) diff --git a/app/reaction.go b/app/reaction.go index 0163c98519..80bc24b4b4 100644 --- a/app/reaction.go +++ b/app/reaction.go @@ -46,7 +46,7 @@ func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction) if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ReactionHasBeenAdded(pluginContext, reaction) return true }, plugin.ReactionHasBeenAddedID) @@ -145,7 +145,7 @@ func (a *App) DeleteReactionForPost(c *request.Context, reaction *model.Reaction if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.ReactionHasBeenRemoved(pluginContext, reaction) return true }, plugin.ReactionHasBeenRemovedID) diff --git a/app/server.go b/app/server.go index 8699a013c6..a5e01db9b7 100644 --- a/app/server.go +++ b/app/server.go @@ -161,6 +161,8 @@ type Server struct { tracer *tracing.Tracer products map[string]Product + + hooksManager *product.HooksManager } func (s *Server) Store() store.Store { @@ -255,6 +257,8 @@ func NewServer(options ...Option) (*Server, error) { return nil, errors.Wrapf(err, "unable to create teams service") } + s.hooksManager = product.NewHooksManager(s.GetMetrics()) + // ensure app implements `product.UserService` var _ product.UserService = (*App)(nil) diff --git a/app/team.go b/app/team.go index 9c1d99ee6b..4376e2d77a 100644 --- a/app/team.go +++ b/app/team.go @@ -854,7 +854,7 @@ func (a *App) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User, a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasJoinedTeam(pluginContext, teamMember, actor) return true }, plugin.UserHasJoinedTeamID) @@ -1228,7 +1228,7 @@ func (a *App) postProcessTeamMemberLeave(c request.CTX, teamMember *model.TeamMe a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasLeftTeam(pluginContext, teamMember, actor) return true }, plugin.UserHasLeftTeamID) diff --git a/app/upload.go b/app/upload.go index df5b329117..b63725b135 100644 --- a/app/upload.go +++ b/app/upload.go @@ -67,7 +67,7 @@ func (a *App) runPluginsHook(c *request.Context, info *model.FileInfo, file io.R var rejErr *model.AppError var once sync.Once pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { once.Do(func() { hookHasRunCh <- struct{}{} }) diff --git a/app/user.go b/app/user.go index ddc329ddc3..953446071d 100644 --- a/app/user.go +++ b/app/user.go @@ -311,7 +311,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { a.Srv().Go(func() { pluginContext := pluginContext(c) - pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { hooks.UserHasBeenCreated(pluginContext, ruser) return true }, plugin.UserHasBeenCreatedID) diff --git a/app/web_conn.go b/app/web_conn.go index 0edc3504ae..cdf59eb31e 100644 --- a/app/web_conn.go +++ b/app/web_conn.go @@ -16,5 +16,5 @@ func (a *App) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfi // NewWebConn returns a new WebConn instance. func (a *App) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn { - return a.Srv().Platform().NewWebConn(cfg, a, a.ch.GetPluginsEnvironment) + return a.Srv().Platform().NewWebConn(cfg, a, a.ch) } diff --git a/plugin/environment.go b/plugin/environment.go index 0b73b9c435..db07f051bd 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -50,7 +50,6 @@ type PrepackagedPlugin struct { // of active plugins. type Environment struct { registeredPlugins sync.Map - registeredProducts sync.Map pluginHealthCheckJob *PluginHealthCheckJob logger *mlog.Logger metrics einterfaces.MetricsInterface @@ -326,26 +325,6 @@ func (env *Environment) Activate(id string) (manifest *model.Manifest, activated return pluginInfo.Manifest, true, nil } -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) { - env.registeredProducts.Delete(productID) -} - func (env *Environment) RemovePlugin(id string) { if _, ok := env.registeredPlugins.Load(id); ok { env.registeredPlugins.Delete(id) @@ -499,12 +478,6 @@ func (env *Environment) HooksForPlugin(id string) (Hooks, error) { } } - if p, ok := env.registeredProducts.Load(id); ok { - rp := p.(*registeredProduct) - - return rp.adapter, nil - } - return nil, fmt.Errorf("plugin not found: %v", id) } @@ -533,24 +506,6 @@ func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks) bool return result }) - env.registeredProducts.Range(func(key, value any) bool { - rp := value.(*registeredProduct) - - if !rp.Implements(hookId) { - return true - } - - hookStartTime := time.Now() - result := hookRunnerFunc(rp.adapter) - - if env.metrics != nil { - elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second) - env.metrics.ObservePluginMultiHookIterationDuration(rp.productID, elapsedTime) - } - - return result - }) - if env.metrics != nil { elapsedTime := float64(time.Since(startTime)) / float64(time.Second) env.metrics.ObservePluginMultiHookDuration(elapsedTime) diff --git a/plugin/interface_generator/main.go b/plugin/interface_generator/main.go index 0dd367f9cd..3afcb4d559 100644 --- a/plugin/interface_generator/main.go +++ b/plugin/interface_generator/main.go @@ -399,13 +399,13 @@ type {{.Name}}IFace interface { {{end}} -type hooksAdapter struct { +type HooksAdapter struct { implemented map[int]struct{} productHooks any } -func newAdapter(productHooks any) (*hooksAdapter, error) { - a := &hooksAdapter{ +func NewAdapter(productHooks any) (*HooksAdapter, error) { + a := &HooksAdapter{ implemented: make(map[int]struct{}), productHooks: productHooks, } @@ -427,7 +427,7 @@ func newAdapter(productHooks any) (*hooksAdapter, error) { } {{range .HooksMethods}} -func (a *hooksAdapter) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { +func (a *HooksAdapter) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { if _, ok := a.implemented[{{.Name}}ID]; !ok { panic("product hooks must implement {{.Name}}") } diff --git a/plugin/product.go b/plugin/product.go index cd56dec569..557b7546af 100644 --- a/plugin/product.go +++ b/plugin/product.go @@ -7,13 +7,13 @@ import ( "net/http" ) -type registeredProduct struct { - productID string - adapter Hooks +type RegisteredProduct struct { + ProductID string + Adapter Hooks } -func (rp *registeredProduct) Implements(hookId int) bool { - adapter, ok := rp.adapter.(*hooksAdapter) +func (rp *RegisteredProduct) Implements(hookId int) bool { + adapter, ok := rp.Adapter.(*HooksAdapter) if !ok { return false } @@ -23,19 +23,19 @@ func (rp *registeredProduct) Implements(hookId int) bool { } // Implemented method is overridden intentionally to prevent calling it from outside. -func (a *hooksAdapter) Implemented() ([]string, error) { +func (a *HooksAdapter) Implemented() ([]string, error) { return nil, nil } // OnActivate is overridden intentionally as product should not call it. -func (a *hooksAdapter) OnActivate() error { +func (a *HooksAdapter) OnActivate() error { return nil } // OnDeactivate is overridden intentionally as product should not call it. -func (a *hooksAdapter) OnDeactivate() error { +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) {} +func (a *HooksAdapter) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {} diff --git a/plugin/product_hooks_generated.go b/plugin/product_hooks_generated.go index 7f00e543aa..d3d51a930f 100644 --- a/plugin/product_hooks_generated.go +++ b/plugin/product_hooks_generated.go @@ -138,13 +138,13 @@ type GetTopicMetadataByIdsIFace interface { GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) } -type hooksAdapter struct { +type HooksAdapter struct { implemented map[int]struct{} productHooks any } -func newAdapter(productHooks any) (*hooksAdapter, error) { - a := &hooksAdapter{ +func NewAdapter(productHooks any) (*HooksAdapter, error) { + a := &HooksAdapter{ implemented: make(map[int]struct{}), productHooks: productHooks, } @@ -433,7 +433,7 @@ func newAdapter(productHooks any) (*hooksAdapter, error) { return a, nil } -func (a *hooksAdapter) OnConfigurationChange() error { +func (a *HooksAdapter) OnConfigurationChange() error { if _, ok := a.implemented[OnConfigurationChangeID]; !ok { panic("product hooks must implement OnConfigurationChange") } @@ -442,7 +442,7 @@ func (a *hooksAdapter) OnConfigurationChange() error { } -func (a *hooksAdapter) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { +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") } @@ -451,7 +451,7 @@ func (a *hooksAdapter) ExecuteCommand(c *Context, args *model.CommandArgs) (*mod } -func (a *hooksAdapter) UserHasBeenCreated(c *Context, user *model.User) { +func (a *HooksAdapter) UserHasBeenCreated(c *Context, user *model.User) { if _, ok := a.implemented[UserHasBeenCreatedID]; !ok { panic("product hooks must implement UserHasBeenCreated") } @@ -460,7 +460,7 @@ func (a *hooksAdapter) UserHasBeenCreated(c *Context, user *model.User) { } -func (a *hooksAdapter) UserWillLogIn(c *Context, user *model.User) string { +func (a *HooksAdapter) UserWillLogIn(c *Context, user *model.User) string { if _, ok := a.implemented[UserWillLogInID]; !ok { panic("product hooks must implement UserWillLogIn") } @@ -469,7 +469,7 @@ func (a *hooksAdapter) UserWillLogIn(c *Context, user *model.User) string { } -func (a *hooksAdapter) UserHasLoggedIn(c *Context, user *model.User) { +func (a *HooksAdapter) UserHasLoggedIn(c *Context, user *model.User) { if _, ok := a.implemented[UserHasLoggedInID]; !ok { panic("product hooks must implement UserHasLoggedIn") } @@ -478,7 +478,7 @@ func (a *hooksAdapter) UserHasLoggedIn(c *Context, user *model.User) { } -func (a *hooksAdapter) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) { +func (a *HooksAdapter) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) { if _, ok := a.implemented[MessageWillBePostedID]; !ok { panic("product hooks must implement MessageWillBePosted") } @@ -487,7 +487,7 @@ func (a *hooksAdapter) MessageWillBePosted(c *Context, post *model.Post) (*model } -func (a *hooksAdapter) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) { +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") } @@ -496,7 +496,7 @@ func (a *hooksAdapter) MessageWillBeUpdated(c *Context, newPost, oldPost *model. } -func (a *hooksAdapter) MessageHasBeenPosted(c *Context, post *model.Post) { +func (a *HooksAdapter) MessageHasBeenPosted(c *Context, post *model.Post) { if _, ok := a.implemented[MessageHasBeenPostedID]; !ok { panic("product hooks must implement MessageHasBeenPosted") } @@ -505,7 +505,7 @@ func (a *hooksAdapter) MessageHasBeenPosted(c *Context, post *model.Post) { } -func (a *hooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) { +func (a *HooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) { if _, ok := a.implemented[MessageHasBeenUpdatedID]; !ok { panic("product hooks must implement MessageHasBeenUpdated") } @@ -514,7 +514,7 @@ func (a *hooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model } -func (a *hooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) { +func (a *HooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) { if _, ok := a.implemented[ChannelHasBeenCreatedID]; !ok { panic("product hooks must implement ChannelHasBeenCreated") } @@ -523,7 +523,7 @@ func (a *hooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) } -func (a *hooksAdapter) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) { +func (a *HooksAdapter) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) { if _, ok := a.implemented[UserHasJoinedChannelID]; !ok { panic("product hooks must implement UserHasJoinedChannel") } @@ -532,7 +532,7 @@ func (a *hooksAdapter) UserHasJoinedChannel(c *Context, channelMember *model.Cha } -func (a *hooksAdapter) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) { +func (a *HooksAdapter) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) { if _, ok := a.implemented[UserHasLeftChannelID]; !ok { panic("product hooks must implement UserHasLeftChannel") } @@ -541,7 +541,7 @@ func (a *hooksAdapter) UserHasLeftChannel(c *Context, channelMember *model.Chann } -func (a *hooksAdapter) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) { +func (a *HooksAdapter) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) { if _, ok := a.implemented[UserHasJoinedTeamID]; !ok { panic("product hooks must implement UserHasJoinedTeam") } @@ -550,7 +550,7 @@ func (a *hooksAdapter) UserHasJoinedTeam(c *Context, teamMember *model.TeamMembe } -func (a *hooksAdapter) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) { +func (a *HooksAdapter) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) { if _, ok := a.implemented[UserHasLeftTeamID]; !ok { panic("product hooks must implement UserHasLeftTeam") } @@ -559,7 +559,7 @@ func (a *hooksAdapter) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, } -func (a *hooksAdapter) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) { +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") } @@ -568,7 +568,7 @@ func (a *hooksAdapter) FileWillBeUploaded(c *Context, info *model.FileInfo, file } -func (a *hooksAdapter) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) { +func (a *HooksAdapter) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) { if _, ok := a.implemented[ReactionHasBeenAddedID]; !ok { panic("product hooks must implement ReactionHasBeenAdded") } @@ -577,7 +577,7 @@ func (a *hooksAdapter) ReactionHasBeenAdded(c *Context, reaction *model.Reaction } -func (a *hooksAdapter) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) { +func (a *HooksAdapter) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) { if _, ok := a.implemented[ReactionHasBeenRemovedID]; !ok { panic("product hooks must implement ReactionHasBeenRemoved") } @@ -586,7 +586,7 @@ func (a *hooksAdapter) ReactionHasBeenRemoved(c *Context, reaction *model.Reacti } -func (a *hooksAdapter) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) { +func (a *HooksAdapter) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) { if _, ok := a.implemented[OnPluginClusterEventID]; !ok { panic("product hooks must implement OnPluginClusterEvent") } @@ -595,7 +595,7 @@ func (a *hooksAdapter) OnPluginClusterEvent(c *Context, ev model.PluginClusterEv } -func (a *hooksAdapter) OnWebSocketConnect(webConnID, userID string) { +func (a *HooksAdapter) OnWebSocketConnect(webConnID, userID string) { if _, ok := a.implemented[OnWebSocketConnectID]; !ok { panic("product hooks must implement OnWebSocketConnect") } @@ -604,7 +604,7 @@ func (a *hooksAdapter) OnWebSocketConnect(webConnID, userID string) { } -func (a *hooksAdapter) OnWebSocketDisconnect(webConnID, userID string) { +func (a *HooksAdapter) OnWebSocketDisconnect(webConnID, userID string) { if _, ok := a.implemented[OnWebSocketDisconnectID]; !ok { panic("product hooks must implement OnWebSocketDisconnect") } @@ -613,7 +613,7 @@ func (a *hooksAdapter) OnWebSocketDisconnect(webConnID, userID string) { } -func (a *hooksAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) { +func (a *HooksAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) { if _, ok := a.implemented[WebSocketMessageHasBeenPostedID]; !ok { panic("product hooks must implement WebSocketMessageHasBeenPosted") } @@ -622,7 +622,7 @@ func (a *hooksAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, r } -func (a *hooksAdapter) RunDataRetention(nowTime, batchSize int64) (int64, error) { +func (a *HooksAdapter) RunDataRetention(nowTime, batchSize int64) (int64, error) { if _, ok := a.implemented[RunDataRetentionID]; !ok { panic("product hooks must implement RunDataRetention") } @@ -631,7 +631,7 @@ func (a *hooksAdapter) RunDataRetention(nowTime, batchSize int64) (int64, error) } -func (a *hooksAdapter) OnInstall(c *Context, event model.OnInstallEvent) error { +func (a *HooksAdapter) OnInstall(c *Context, event model.OnInstallEvent) error { if _, ok := a.implemented[OnInstallID]; !ok { panic("product hooks must implement OnInstall") } @@ -640,7 +640,7 @@ func (a *hooksAdapter) OnInstall(c *Context, event model.OnInstallEvent) error { } -func (a *hooksAdapter) OnSendDailyTelemetry() { +func (a *HooksAdapter) OnSendDailyTelemetry() { if _, ok := a.implemented[OnSendDailyTelemetryID]; !ok { panic("product hooks must implement OnSendDailyTelemetry") } @@ -649,7 +649,7 @@ func (a *hooksAdapter) OnSendDailyTelemetry() { } -func (a *hooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) { +func (a *HooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) { if _, ok := a.implemented[OnCloudLimitsUpdatedID]; !ok { panic("product hooks must implement OnCloudLimitsUpdated") } @@ -658,7 +658,7 @@ func (a *hooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) { } -func (a *hooksAdapter) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) { +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") } @@ -667,7 +667,7 @@ func (a *hooksAdapter) UserHasPermissionToCollection(c *Context, userID string, } -func (a *hooksAdapter) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) { +func (a *HooksAdapter) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) { if _, ok := a.implemented[GetAllCollectionIDsForUserID]; !ok { panic("product hooks must implement GetAllCollectionIDsForUser") } @@ -676,7 +676,7 @@ func (a *hooksAdapter) GetAllCollectionIDsForUser(c *Context, userID, collection } -func (a *hooksAdapter) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) { +func (a *HooksAdapter) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) { if _, ok := a.implemented[GetAllUserIdsForCollectionID]; !ok { panic("product hooks must implement GetAllUserIdsForCollection") } @@ -685,7 +685,7 @@ func (a *hooksAdapter) GetAllUserIdsForCollection(c *Context, collectionType, co } -func (a *hooksAdapter) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) { +func (a *HooksAdapter) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) { if _, ok := a.implemented[GetTopicRedirectID]; !ok { panic("product hooks must implement GetTopicRedirect") } @@ -694,7 +694,7 @@ func (a *hooksAdapter) GetTopicRedirect(c *Context, topicType, topicID string) ( } -func (a *hooksAdapter) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) { +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") } @@ -703,7 +703,7 @@ func (a *hooksAdapter) GetCollectionMetadataByIds(c *Context, collectionType str } -func (a *hooksAdapter) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) { +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") } diff --git a/product/hooks.go b/product/hooks.go new file mode 100644 index 0000000000..a2515c9a7e --- /dev/null +++ b/product/hooks.go @@ -0,0 +1,79 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package product + +import ( + "sync" + "time" + + "github.com/mattermost/mattermost-server/v6/einterfaces" + "github.com/mattermost/mattermost-server/v6/plugin" +) + +type HooksManager struct { + registeredProducts sync.Map + metrics einterfaces.MetricsInterface +} + +func NewHooksManager(metrics einterfaces.MetricsInterface) *HooksManager { + return &HooksManager{ + metrics: metrics, + } +} + +func (m *HooksManager) AddProduct(productID string, hooks any) error { + prod, err := plugin.NewAdapter(hooks) + if err != nil { + return err + } + + rp := &plugin.RegisteredProduct{ + ProductID: productID, + Adapter: prod, + } + + m.registeredProducts.Store(productID, rp) + + return nil +} + +func (m *HooksManager) RemoveProduct(productID string) { + m.registeredProducts.Delete(productID) +} + +func (m *HooksManager) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) { + startTime := time.Now() + + m.registeredProducts.Range(func(key, value any) bool { + rp := value.(*plugin.RegisteredProduct) + + if !rp.Implements(hookId) { + return true + } + + hookStartTime := time.Now() + result := hookRunnerFunc(rp.Adapter) + + if m.metrics != nil { + elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second) + m.metrics.ObservePluginMultiHookIterationDuration(rp.ProductID, elapsedTime) + } + + return result + }) + + if m.metrics != nil { + elapsedTime := float64(time.Since(startTime)) / float64(time.Second) + m.metrics.ObservePluginMultiHookDuration(elapsedTime) + } +} + +func (m *HooksManager) HooksForProduct(id string) plugin.Hooks { + if value, ok := m.registeredProducts.Load(id); ok { + rp := value.(*plugin.RegisteredProduct) + return rp.Adapter + } + + return nil +}