From 502cd6ef7df47fcbf307bffe4970f6fa79f28e85 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Wed, 3 Jan 2024 12:25:53 -0500 Subject: [PATCH] MM-56082 Add PreferencesHaveChanged plugin hook (#25659) * Add interface for PreferencesHaveChanged hook * Add context to preference-related methods of App * Implement PreferencesHaveChanged * Re-add missing "fmt" import * Update minimum server version for the new hook * Remove pointers to be consistent with other preference APIs --- server/channels/api4/preference.go | 10 +-- server/channels/api4/status.go | 2 +- server/channels/api4/user_test.go | 4 +- server/channels/app/app_iface.go | 12 +-- server/channels/app/export.go | 12 +-- server/channels/app/export_test.go | 2 +- .../app/opentracing/opentracing_layer.go | 24 +++--- server/channels/app/plugin_api.go | 6 +- server/channels/app/plugin_hooks_test.go | 77 +++++++++++++++++++ server/channels/app/preference.go | 32 +++++--- server/channels/app/status.go | 14 ++-- server/channels/app/user_test.go | 12 +-- server/channels/product/api.go | 6 +- server/channels/web/oauth.go | 2 +- server/public/plugin/client_rpc_generated.go | 34 ++++++++ server/public/plugin/hooks.go | 8 ++ .../plugin/hooks_timer_layer_generated.go | 6 ++ server/public/plugin/plugintest/hooks.go | 5 ++ .../public/plugin/product_hooks_generated.go | 22 ++++++ 19 files changed, 226 insertions(+), 64 deletions(-) diff --git a/server/channels/api4/preference.go b/server/channels/api4/preference.go index c9e61b50c07..14f3ee96b95 100644 --- a/server/channels/api4/preference.go +++ b/server/channels/api4/preference.go @@ -31,7 +31,7 @@ func getPreferences(c *Context, w http.ResponseWriter, r *http.Request) { return } - preferences, err := c.App.GetPreferencesForUser(c.Params.UserId) + preferences, err := c.App.GetPreferencesForUser(c.AppContext, c.Params.UserId) if err != nil { c.Err = err return @@ -53,7 +53,7 @@ func getPreferencesByCategory(c *Context, w http.ResponseWriter, r *http.Request return } - preferences, err := c.App.GetPreferenceByCategoryForUser(c.Params.UserId, c.Params.Category) + preferences, err := c.App.GetPreferenceByCategoryForUser(c.AppContext, c.Params.UserId, c.Params.Category) if err != nil { c.Err = err return @@ -75,7 +75,7 @@ func getPreferenceByCategoryAndName(c *Context, w http.ResponseWriter, r *http.R return } - preferences, err := c.App.GetPreferenceByCategoryAndNameForUser(c.Params.UserId, c.Params.Category, c.Params.PreferenceName) + preferences, err := c.App.GetPreferenceByCategoryAndNameForUser(c.AppContext, c.Params.UserId, c.Params.Category, c.Params.PreferenceName) if err != nil { c.Err = err return @@ -125,7 +125,7 @@ func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) { sanitizedPreferences = append(sanitizedPreferences, pref) } - if err := c.App.UpdatePreferences(c.Params.UserId, sanitizedPreferences); err != nil { + if err := c.App.UpdatePreferences(c.AppContext, c.Params.UserId, sanitizedPreferences); err != nil { c.Err = err return } @@ -154,7 +154,7 @@ func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) { return } - if err := c.App.DeletePreferences(c.Params.UserId, preferences); err != nil { + if err := c.App.DeletePreferences(c.AppContext, c.Params.UserId, preferences); err != nil { c.Err = err return } diff --git a/server/channels/api4/status.go b/server/channels/api4/status.go index 83c262071c5..124ab3d3b86 100644 --- a/server/channels/api4/status.go +++ b/server/channels/api4/status.go @@ -203,7 +203,7 @@ func removeUserRecentCustomStatus(c *Context, w http.ResponseWriter, r *http.Req return } - if err := c.App.RemoveRecentCustomStatus(c.Params.UserId, &recentCustomStatus); err != nil { + if err := c.App.RemoveRecentCustomStatus(c.AppContext, c.Params.UserId, &recentCustomStatus); err != nil { c.Err = err return } diff --git a/server/channels/api4/user_test.go b/server/channels/api4/user_test.go index dad0e69f588..75ad046205f 100644 --- a/server/channels/api4/user_test.go +++ b/server/channels/api4/user_test.go @@ -3089,7 +3089,7 @@ func TestGetUsersInGroupByDisplayName(t *testing.T) { Value: model.ShowUsername, } - err = th.App.UpdatePreferences(th.SystemAdminUser.Id, model.Preferences{preference}) + err = th.App.UpdatePreferences(th.Context, th.SystemAdminUser.Id, model.Preferences{preference}) assert.Nil(t, err) t.Run("Returns users in group in right order for username", func(t *testing.T) { @@ -3099,7 +3099,7 @@ func TestGetUsersInGroupByDisplayName(t *testing.T) { }) preference.Value = model.ShowNicknameFullName - err = th.App.UpdatePreferences(th.SystemAdminUser.Id, model.Preferences{preference}) + err = th.App.UpdatePreferences(th.Context, th.SystemAdminUser.Id, model.Preferences{preference}) assert.Nil(t, err) t.Run("Returns users in group in right order for nickname", func(t *testing.T) { diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index 8ae77a41179..3eb69228ce2 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -555,7 +555,7 @@ type AppIface interface { DeleteOutgoingWebhook(hookID string) *model.AppError DeletePluginKey(pluginID string, key string) *model.AppError DeletePost(c request.CTX, postID, deleteByID string) (*model.Post, *model.AppError) - DeletePreferences(userID string, preferences model.Preferences) *model.AppError + DeletePreferences(c request.CTX, userID string, preferences model.Preferences) *model.AppError DeleteReactionForPost(c request.CTX, reaction *model.Reaction) *model.AppError DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError) DeleteRetentionPolicy(policyID string) *model.AppError @@ -750,9 +750,9 @@ type AppIface interface { GetPostsForChannelAroundLastUnread(c request.CTX, channelID, userID string, limitBefore, limitAfter int, skipFetchThreads bool, collapsedThreads, collapsedThreadsExtended bool) (*model.PostList, *model.AppError) GetPostsPage(options model.GetPostsOptions) (*model.PostList, *model.AppError) GetPostsSince(options model.GetPostsSinceOptions) (*model.PostList, *model.AppError) - GetPreferenceByCategoryAndNameForUser(userID string, category string, preferenceName string) (*model.Preference, *model.AppError) - GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError) - GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) + GetPreferenceByCategoryAndNameForUser(c request.CTX, userID string, category string, preferenceName string) (*model.Preference, *model.AppError) + GetPreferenceByCategoryForUser(c request.CTX, userID string, category string) (model.Preferences, *model.AppError) + GetPreferencesForUser(c request.CTX, userID string) (model.Preferences, *model.AppError) GetPrevPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) @@ -994,7 +994,7 @@ type AppIface interface { RemoveLdapPrivateCertificate() *model.AppError RemoveLdapPublicCertificate() *model.AppError RemoveNotifications(c request.CTX, post *model.Post, channel *model.Channel) error - RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError + RemoveRecentCustomStatus(c request.CTX, userID string, status *model.CustomStatus) *model.AppError RemoveSamlIdpCertificate() *model.AppError RemoveSamlPrivateCertificate() *model.AppError RemoveSamlPublicCertificate() *model.AppError @@ -1157,7 +1157,7 @@ type AppIface interface { UpdatePasswordByUserIdSendEmail(c request.CTX, userID, newPassword, method string) *model.AppError UpdatePasswordSendEmail(c request.CTX, user *model.User, newPassword, method string) *model.AppError UpdatePost(c request.CTX, receivedUpdatedPost *model.Post, safeUpdate bool) (*model.Post, *model.AppError) - UpdatePreferences(userID string, preferences model.Preferences) *model.AppError + UpdatePreferences(c request.CTX, userID string, preferences model.Preferences) *model.AppError UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError) UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError) UpdateRole(role *model.Role) (*model.Role, *model.AppError) diff --git a/server/channels/app/export.go b/server/channels/app/export.go index 3dbc3b0f469..e216a76ae78 100644 --- a/server/channels/app/export.go +++ b/server/channels/app/export.go @@ -278,7 +278,7 @@ func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer, // Gathering here the exportable preferences to pass them on to ImportLineFromUser exportedPrefs := make(map[string]*string) - allPrefs, err := a.GetPreferencesForUser(user.Id) + allPrefs, err := a.GetPreferencesForUser(ctx, user.Id) if err != nil { return err } @@ -319,7 +319,7 @@ func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer, userLine.User.NotifyProps = a.buildUserNotifyProps(user.NotifyProps) // Do the Team Memberships. - members, err := a.buildUserTeamAndChannelMemberships(user.Id, includeArchivedChannels) + members, err := a.buildUserTeamAndChannelMemberships(ctx, user.Id, includeArchivedChannels) if err != nil { return err } @@ -335,7 +335,7 @@ func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer, return nil } -func (a *App) buildUserTeamAndChannelMemberships(userID string, includeArchivedChannels bool) (*[]imports.UserTeamImportData, *model.AppError) { +func (a *App) buildUserTeamAndChannelMemberships(c request.CTX, userID string, includeArchivedChannels bool) (*[]imports.UserTeamImportData, *model.AppError) { var memberships []imports.UserTeamImportData members, err := a.Srv().Store().Team().GetTeamMembersForExport(userID) @@ -353,7 +353,7 @@ func (a *App) buildUserTeamAndChannelMemberships(userID string, includeArchivedC memberData := ImportUserTeamDataFromTeamMember(member) // Do the Channel Memberships. - channelMembers, err := a.buildUserChannelMemberships(userID, member.TeamId, includeArchivedChannels) + channelMembers, err := a.buildUserChannelMemberships(c, userID, member.TeamId, includeArchivedChannels) if err != nil { return nil, err } @@ -372,14 +372,14 @@ func (a *App) buildUserTeamAndChannelMemberships(userID string, includeArchivedC return &memberships, nil } -func (a *App) buildUserChannelMemberships(userID string, teamID string, includeArchivedChannels bool) (*[]imports.UserChannelImportData, *model.AppError) { +func (a *App) buildUserChannelMemberships(c request.CTX, userID string, teamID string, includeArchivedChannels bool) (*[]imports.UserChannelImportData, *model.AppError) { members, nErr := a.Srv().Store().Channel().GetChannelMembersForExport(userID, teamID, includeArchivedChannels) if nErr != nil { return nil, model.NewAppError("buildUserChannelMemberships", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } category := model.PreferenceCategoryFavoriteChannel - preferences, err := a.GetPreferenceByCategoryForUser(userID, category) + preferences, err := a.GetPreferenceByCategoryForUser(c, userID, category) if err != nil && err.StatusCode != http.StatusNotFound { return nil, err } diff --git a/server/channels/app/export_test.go b/server/channels/app/export_test.go index f8d621dfba1..4242a47eac8 100644 --- a/server/channels/app/export_test.go +++ b/server/channels/app/export_test.go @@ -104,7 +104,7 @@ func TestExportUserChannels(t *testing.T) { require.NoError(t, err) th.App.UpdateChannelMemberNotifyProps(th.Context, notifyProps, channel.Id, user.Id) - exportData, appErr := th.App.buildUserChannelMemberships(user.Id, team.Id, false) + exportData, appErr := th.App.buildUserChannelMemberships(th.Context, user.Id, team.Id, false) require.Nil(t, appErr) assert.Equal(t, len(*exportData), 3) for _, data := range *exportData { diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index c2fdaf157b7..dce7cfd9b23 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -3394,7 +3394,7 @@ func (a *OpenTracingAppLayer) DeletePost(c request.CTX, postID string, deleteByI return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) DeletePreferences(userID string, preferences model.Preferences) *model.AppError { +func (a *OpenTracingAppLayer) DeletePreferences(c request.CTX, userID string, preferences model.Preferences) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePreferences") @@ -3406,7 +3406,7 @@ func (a *OpenTracingAppLayer) DeletePreferences(userID string, preferences model }() defer span.Finish() - resultVar0 := a.app.DeletePreferences(userID, preferences) + resultVar0 := a.app.DeletePreferences(c, userID, preferences) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) @@ -8395,7 +8395,7 @@ func (a *OpenTracingAppLayer) GetPostsUsage() (int64, *model.AppError) { return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetPreferenceByCategoryAndNameForUser(userID string, category string, preferenceName string) (*model.Preference, *model.AppError) { +func (a *OpenTracingAppLayer) GetPreferenceByCategoryAndNameForUser(c request.CTX, userID string, category string, preferenceName string) (*model.Preference, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferenceByCategoryAndNameForUser") @@ -8407,7 +8407,7 @@ func (a *OpenTracingAppLayer) GetPreferenceByCategoryAndNameForUser(userID strin }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetPreferenceByCategoryAndNameForUser(userID, category, preferenceName) + resultVar0, resultVar1 := a.app.GetPreferenceByCategoryAndNameForUser(c, userID, category, preferenceName) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -8417,7 +8417,7 @@ func (a *OpenTracingAppLayer) GetPreferenceByCategoryAndNameForUser(userID strin return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError) { +func (a *OpenTracingAppLayer) GetPreferenceByCategoryForUser(c request.CTX, userID string, category string) (model.Preferences, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferenceByCategoryForUser") @@ -8429,7 +8429,7 @@ func (a *OpenTracingAppLayer) GetPreferenceByCategoryForUser(userID string, cate }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetPreferenceByCategoryForUser(userID, category) + resultVar0, resultVar1 := a.app.GetPreferenceByCategoryForUser(c, userID, category) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -8439,7 +8439,7 @@ func (a *OpenTracingAppLayer) GetPreferenceByCategoryForUser(userID string, cate return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) { +func (a *OpenTracingAppLayer) GetPreferencesForUser(c request.CTX, userID string) (model.Preferences, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferencesForUser") @@ -8451,7 +8451,7 @@ func (a *OpenTracingAppLayer) GetPreferencesForUser(userID string) (model.Prefer }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetPreferencesForUser(userID) + resultVar0, resultVar1 := a.app.GetPreferencesForUser(c, userID) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -14031,7 +14031,7 @@ func (a *OpenTracingAppLayer) RemoveNotifications(c request.CTX, post *model.Pos return resultVar0 } -func (a *OpenTracingAppLayer) RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError { +func (a *OpenTracingAppLayer) RemoveRecentCustomStatus(c request.CTX, userID string, status *model.CustomStatus) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveRecentCustomStatus") @@ -14043,7 +14043,7 @@ func (a *OpenTracingAppLayer) RemoveRecentCustomStatus(userID string, status *mo }() defer span.Finish() - resultVar0 := a.app.RemoveRecentCustomStatus(userID, status) + resultVar0 := a.app.RemoveRecentCustomStatus(c, userID, status) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) @@ -17878,7 +17878,7 @@ func (a *OpenTracingAppLayer) UpdatePost(c request.CTX, receivedUpdatedPost *mod return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) UpdatePreferences(userID string, preferences model.Preferences) *model.AppError { +func (a *OpenTracingAppLayer) UpdatePreferences(c request.CTX, userID string, preferences model.Preferences) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdatePreferences") @@ -17890,7 +17890,7 @@ func (a *OpenTracingAppLayer) UpdatePreferences(userID string, preferences model }() defer span.Finish() - resultVar0 := a.app.UpdatePreferences(userID, preferences) + resultVar0 := a.app.UpdatePreferences(c, userID, preferences) if resultVar0 != nil { span.LogFields(spanlog.Error(resultVar0)) diff --git a/server/channels/app/plugin_api.go b/server/channels/app/plugin_api.go index dcae0dccb38..0ed2f56e64b 100644 --- a/server/channels/app/plugin_api.go +++ b/server/channels/app/plugin_api.go @@ -271,15 +271,15 @@ func (api *PluginAPI) GetUsersInTeam(teamID string, page int, perPage int) ([]*m } func (api *PluginAPI) GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) { - return api.app.GetPreferencesForUser(userID) + return api.app.GetPreferencesForUser(api.ctx, userID) } func (api *PluginAPI) UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError { - return api.app.UpdatePreferences(userID, preferences) + return api.app.UpdatePreferences(api.ctx, userID, preferences) } func (api *PluginAPI) DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError { - return api.app.DeletePreferences(userID, preferences) + return api.app.DeletePreferences(api.ctx, userID, preferences) } func (api *PluginAPI) GetSession(sessionID string) (*model.Session, *model.AppError) { diff --git a/server/channels/app/plugin_hooks_test.go b/server/channels/app/plugin_hooks_test.go index 64c2cb56d81..5aa5d861e9e 100644 --- a/server/channels/app/plugin_hooks_test.go +++ b/server/channels/app/plugin_hooks_test.go @@ -1294,6 +1294,10 @@ func TestHookReactionHasBeenRemoved(t *testing.T) { err := th.App.DeleteReactionForPost(th.Context, reaction) require.Nil(t, err) + + time.Sleep(1 * time.Second) + + mockAPI.AssertCalled(t, "LogDebug", "star") } func TestHookRunDataRetention(t *testing.T) { @@ -1632,3 +1636,76 @@ func TestHookMessagesWillBeConsumed(t *testing.T) { assert.Equal(t, "mwbc_plugin:message", post.Message) }) } + +func TestHookPreferencesHaveChanged(t *testing.T) { + t.Run("should be called when preferences are changed by non-plugin code", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Setup plugin + var mockAPI plugintest.API + + tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t, []string{` + package main + + import ( + "fmt" + + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/model" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) PreferencesHaveChanged(c *plugin.Context, preferences []model.Preference) { + for _, preference := range preferences { + p.API.LogDebug(fmt.Sprintf("category=%s name=%s value=%s", preference.Category, preference.Name, preference.Value)) + } + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, func(*model.Manifest) plugin.API { return &mockAPI }) + defer tearDown() + + // Confirm plugin is actually running + require.Len(t, pluginIDs, 1) + pluginID := pluginIDs[0] + + require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID)) + + // Setup test + preferences := model.Preferences{ + { + UserId: th.BasicUser.Id, + Category: "test_category", + Name: "test_name_1", + Value: "test_value_1", + }, + { + UserId: th.BasicUser.Id, + Category: "test_category", + Name: "test_name_2", + Value: "test_value_2", + }, + } + + mockAPI.On("LogDebug", "category=test_category name=test_name_1 value=test_value_1") + mockAPI.On("LogDebug", "category=test_category name=test_name_2 value=test_value_2") + defer mockAPI.AssertExpectations(t) + + // Run test + err := th.App.UpdatePreferences(th.Context, th.BasicUser.Id, preferences) + + require.Nil(t, err) + + // Hooks are run in a goroutine, so wait for those to complete + time.Sleep(1 * time.Second) + + mockAPI.AssertCalled(t, "LogDebug", "category=test_category name=test_name_1 value=test_value_1") + mockAPI.AssertCalled(t, "LogDebug", "category=test_category name=test_name_2 value=test_value_2") + }) +} diff --git a/server/channels/app/preference.go b/server/channels/app/preference.go index 0a2eacd6ac1..0b571660d8d 100644 --- a/server/channels/app/preference.go +++ b/server/channels/app/preference.go @@ -9,6 +9,8 @@ import ( "net/http" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/product" ) @@ -20,19 +22,19 @@ type preferencesServiceWrapper struct { app AppIface } -func (w *preferencesServiceWrapper) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) { - return w.app.GetPreferencesForUser(userID) +func (w *preferencesServiceWrapper) GetPreferencesForUser(c request.CTX, userID string) (model.Preferences, *model.AppError) { + return w.app.GetPreferencesForUser(c, userID) } -func (w *preferencesServiceWrapper) UpdatePreferencesForUser(userID string, preferences model.Preferences) *model.AppError { - return w.app.UpdatePreferences(userID, preferences) +func (w *preferencesServiceWrapper) UpdatePreferencesForUser(c request.CTX, userID string, preferences model.Preferences) *model.AppError { + return w.app.UpdatePreferences(c, userID, preferences) } -func (w *preferencesServiceWrapper) DeletePreferencesForUser(userID string, preferences model.Preferences) *model.AppError { - return w.app.DeletePreferences(userID, preferences) +func (w *preferencesServiceWrapper) DeletePreferencesForUser(c request.CTX, userID string, preferences model.Preferences) *model.AppError { + return w.app.DeletePreferences(c, userID, preferences) } -func (a *App) GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) { +func (a *App) GetPreferencesForUser(c request.CTX, userID string) (model.Preferences, *model.AppError) { preferences, err := a.Srv().Store().Preference().GetAll(userID) if err != nil { return nil, model.NewAppError("GetPreferencesForUser", "app.preference.get_all.app_error", nil, "", http.StatusBadRequest).Wrap(err) @@ -40,7 +42,7 @@ func (a *App) GetPreferencesForUser(userID string) (model.Preferences, *model.Ap return preferences, nil } -func (a *App) GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError) { +func (a *App) GetPreferenceByCategoryForUser(c request.CTX, userID string, category string) (model.Preferences, *model.AppError) { preferences, err := a.Srv().Store().Preference().GetCategory(userID, category) if err != nil { return nil, model.NewAppError("GetPreferenceByCategoryForUser", "app.preference.get_category.app_error", nil, "", http.StatusBadRequest).Wrap(err) @@ -52,7 +54,7 @@ func (a *App) GetPreferenceByCategoryForUser(userID string, category string) (mo return preferences, nil } -func (a *App) GetPreferenceByCategoryAndNameForUser(userID string, category string, preferenceName string) (*model.Preference, *model.AppError) { +func (a *App) GetPreferenceByCategoryAndNameForUser(c request.CTX, userID string, category string, preferenceName string) (*model.Preference, *model.AppError) { res, err := a.Srv().Store().Preference().Get(userID, category, preferenceName) if err != nil { return nil, model.NewAppError("GetPreferenceByCategoryAndNameForUser", "app.preference.get.app_error", nil, "", http.StatusBadRequest).Wrap(err) @@ -60,7 +62,7 @@ func (a *App) GetPreferenceByCategoryAndNameForUser(userID string, category stri return res, nil } -func (a *App) UpdatePreferences(userID string, preferences model.Preferences) *model.AppError { +func (a *App) UpdatePreferences(c request.CTX, userID string, preferences model.Preferences) *model.AppError { for _, preference := range preferences { if userID != preference.UserId { return model.NewAppError("savePreferences", "api.preference.update_preferences.set.app_error", nil, @@ -94,10 +96,18 @@ func (a *App) UpdatePreferences(userID string, preferences model.Preferences) *m message.Add("preferences", string(prefsJSON)) a.Publish(message) + pluginContext := pluginContext(c) + a.Srv().Go(func() { + a.ch.RunMultiHook(func(hooks plugin.Hooks) bool { + hooks.PreferencesHaveChanged(pluginContext, preferences) + return true + }, plugin.PreferencesHaveChangedID) + }) + return nil } -func (a *App) DeletePreferences(userID string, preferences model.Preferences) *model.AppError { +func (a *App) DeletePreferences(c request.CTX, userID string, preferences model.Preferences) *model.AppError { for _, preference := range preferences { if userID != preference.UserId { err := model.NewAppError("DeletePreferences", "api.preference.delete_preferences.delete.app_error", nil, diff --git a/server/channels/app/status.go b/server/channels/app/status.go index 180b22acf70..536ac6f8953 100644 --- a/server/channels/app/status.go +++ b/server/channels/app/status.go @@ -95,7 +95,7 @@ func (a *App) SetCustomStatus(c request.CTX, userID string, cs *model.CustomStat return updateErr } - if err := a.addRecentCustomStatus(userID, cs); err != nil { + if err := a.addRecentCustomStatus(c, userID, cs); err != nil { c.Logger().Error("Can't add recent custom status for", mlog.String("userID", userID), mlog.Err(err)) } @@ -126,10 +126,10 @@ func (a *App) GetCustomStatus(userID string) (*model.CustomStatus, *model.AppErr return user.GetCustomStatus(), nil } -func (a *App) addRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError { +func (a *App) addRecentCustomStatus(c request.CTX, userID string, status *model.CustomStatus) *model.AppError { var newRCS model.RecentCustomStatuses - pref, appErr := a.GetPreferenceByCategoryAndNameForUser(userID, model.PreferenceCategoryCustomStatus, model.PreferenceNameRecentCustomStatuses) + pref, appErr := a.GetPreferenceByCategoryAndNameForUser(c, userID, model.PreferenceCategoryCustomStatus, model.PreferenceNameRecentCustomStatuses) if appErr != nil || pref.Value == "" { newRCS = model.RecentCustomStatuses{*status} } else { @@ -150,15 +150,15 @@ func (a *App) addRecentCustomStatus(userID string, status *model.CustomStatus) * Name: model.PreferenceNameRecentCustomStatuses, Value: string(newRCSJSON), } - if appErr := a.UpdatePreferences(userID, model.Preferences{*pref}); appErr != nil { + if appErr := a.UpdatePreferences(c, userID, model.Preferences{*pref}); appErr != nil { return appErr } return nil } -func (a *App) RemoveRecentCustomStatus(userID string, status *model.CustomStatus) *model.AppError { - pref, appErr := a.GetPreferenceByCategoryAndNameForUser(userID, model.PreferenceCategoryCustomStatus, model.PreferenceNameRecentCustomStatuses) +func (a *App) RemoveRecentCustomStatus(c request.CTX, userID string, status *model.CustomStatus) *model.AppError { + pref, appErr := a.GetPreferenceByCategoryAndNameForUser(c, userID, model.PreferenceCategoryCustomStatus, model.PreferenceNameRecentCustomStatuses) if appErr != nil { return appErr } @@ -186,7 +186,7 @@ func (a *App) RemoveRecentCustomStatus(userID string, status *model.CustomStatus return model.NewAppError("RemoveRecentCustomStatus", "api.marshal_error", nil, "", http.StatusBadRequest).Wrap(err) } pref.Value = string(newRCSJSON) - if appErr := a.UpdatePreferences(userID, model.Preferences{*pref}); appErr != nil { + if appErr := a.UpdatePreferences(c, userID, model.Preferences{*pref}); appErr != nil { return appErr } diff --git a/server/channels/app/user_test.go b/server/channels/app/user_test.go index b20f0689b88..2ab1b989b65 100644 --- a/server/channels/app/user_test.go +++ b/server/channels/app/user_test.go @@ -1813,17 +1813,17 @@ func TestCreateUserWithInitialPreferences(t *testing.T) { testUser := th.CreateUser() defer th.App.PermanentDeleteUser(th.Context, testUser) - tutorialStepPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategoryTutorialSteps, testUser.Id) + tutorialStepPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, testUser.Id, model.PreferenceCategoryTutorialSteps, testUser.Id) require.Nil(t, appErr) assert.Equal(t, testUser.Id, tutorialStepPref.Name) - recommendedNextStepsPref, appErr := th.App.GetPreferenceByCategoryForUser(testUser.Id, model.PreferenceRecommendedNextSteps) + recommendedNextStepsPref, appErr := th.App.GetPreferenceByCategoryForUser(th.Context, testUser.Id, model.PreferenceRecommendedNextSteps) require.Nil(t, appErr) assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category) assert.Equal(t, "hide", recommendedNextStepsPref[0].Name) assert.Equal(t, "false", recommendedNextStepsPref[0].Value) - gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM") + gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM") require.Nil(t, appErr) assert.Equal(t, "GMasDM", gmASdmNoticeViewedPref.Name) assert.Equal(t, "true", gmASdmNoticeViewedPref.Value) @@ -1835,17 +1835,17 @@ func TestCreateUserWithInitialPreferences(t *testing.T) { testUser := th.CreateGuest() defer th.App.PermanentDeleteUser(th.Context, testUser) - tutorialStepPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategoryTutorialSteps, testUser.Id) + tutorialStepPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, testUser.Id, model.PreferenceCategoryTutorialSteps, testUser.Id) require.Nil(t, appErr) assert.Equal(t, testUser.Id, tutorialStepPref.Name) - recommendedNextStepsPref, appErr := th.App.GetPreferenceByCategoryForUser(testUser.Id, model.PreferenceRecommendedNextSteps) + recommendedNextStepsPref, appErr := th.App.GetPreferenceByCategoryForUser(th.Context, testUser.Id, model.PreferenceRecommendedNextSteps) require.Nil(t, appErr) assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category) assert.Equal(t, "hide", recommendedNextStepsPref[0].Name) assert.Equal(t, "false", recommendedNextStepsPref[0].Value) - gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM") + gmASdmNoticeViewedPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(th.Context, testUser.Id, model.PreferenceCategorySystemNotice, "GMasDM") require.Nil(t, appErr) assert.Equal(t, "GMasDM", gmASdmNoticeViewedPref.Name) assert.Equal(t, "true", gmASdmNoticeViewedPref.Value) diff --git a/server/channels/product/api.go b/server/channels/product/api.go index 907b17ef098..00ebc47272a 100644 --- a/server/channels/product/api.go +++ b/server/channels/product/api.go @@ -207,9 +207,9 @@ type SystemService interface { // // The service shall be registered via app.PreferencesKey service key. type PreferencesService interface { - GetPreferencesForUser(userID string) (model.Preferences, *model.AppError) - UpdatePreferencesForUser(userID string, preferences model.Preferences) *model.AppError - DeletePreferencesForUser(userID string, preferences model.Preferences) *model.AppError + GetPreferencesForUser(c request.CTX, userID string) (model.Preferences, *model.AppError) + UpdatePreferencesForUser(c request.CTX, userID string, preferences model.Preferences) *model.AppError + DeletePreferencesForUser(c request.CTX, userID string, preferences model.Preferences) *model.AppError } // SessionService is the API for accessing the session. diff --git a/server/channels/web/oauth.go b/server/channels/web/oauth.go index 2b3f232e694..8562c0b1e0d 100644 --- a/server/channels/web/oauth.go +++ b/server/channels/web/oauth.go @@ -159,7 +159,7 @@ func authorizeOAuthPage(c *Context, w http.ResponseWriter, r *http.Request) { isAuthorized := false - if _, err := c.App.GetPreferenceByCategoryAndNameForUser(c.AppContext.Session().UserId, model.PreferenceCategoryAuthorizedOAuthApp, authRequest.ClientId); err == nil { + if _, err := c.App.GetPreferenceByCategoryAndNameForUser(c.AppContext, c.AppContext.Session().UserId, model.PreferenceCategoryAuthorizedOAuthApp, authRequest.ClientId); err == nil { // when we support scopes we should check if the scopes match isAuthorized = true } diff --git a/server/public/plugin/client_rpc_generated.go b/server/public/plugin/client_rpc_generated.go index 6acb35eb49c..7e103f51804 100644 --- a/server/public/plugin/client_rpc_generated.go +++ b/server/public/plugin/client_rpc_generated.go @@ -1018,6 +1018,40 @@ func (s *hooksRPCServer) OnSharedChannelsPing(args *Z_OnSharedChannelsPingArgs, return nil } +func init() { + hookNameToId["PreferencesHaveChanged"] = PreferencesHaveChangedID +} + +type Z_PreferencesHaveChangedArgs struct { + A *Context + B []model.Preference +} + +type Z_PreferencesHaveChangedReturns struct { +} + +func (g *hooksRPCClient) PreferencesHaveChanged(c *Context, preferences []model.Preference) { + _args := &Z_PreferencesHaveChangedArgs{c, preferences} + _returns := &Z_PreferencesHaveChangedReturns{} + if g.implemented[PreferencesHaveChangedID] { + if err := g.client.Call("Plugin.PreferencesHaveChanged", _args, _returns); err != nil { + g.log.Error("RPC call PreferencesHaveChanged to plugin failed.", mlog.Err(err)) + } + } + +} + +func (s *hooksRPCServer) PreferencesHaveChanged(args *Z_PreferencesHaveChangedArgs, returns *Z_PreferencesHaveChangedReturns) error { + if hook, ok := s.impl.(interface { + PreferencesHaveChanged(c *Context, preferences []model.Preference) + }); ok { + hook.PreferencesHaveChanged(args.A, args.B) + } else { + return encodableError(fmt.Errorf("Hook PreferencesHaveChanged called but not implemented.")) + } + return nil +} + type Z_RegisterCommandArgs struct { A *model.Command } diff --git a/server/public/plugin/hooks.go b/server/public/plugin/hooks.go index dce8d40b1e8..5b3df788ac6 100644 --- a/server/public/plugin/hooks.go +++ b/server/public/plugin/hooks.go @@ -57,6 +57,7 @@ const ( ServeMetricsID = 39 OnSharedChannelsSyncMsgID = 40 OnSharedChannelsPingID = 41 + PreferencesHaveChangedID = 42 TotalHooksID = iota ) @@ -354,4 +355,11 @@ type Hooks interface { // // Minimum server version: 9.5 OnSharedChannelsPing(rc *model.RemoteCluster) bool + + // PreferencesHaveChanged is invoked after one or more of a user's preferences have changed. + // Note that this method will be called for preferences changed by plugins, including the plugin that changed + // the preferences. + // + // Minimum server version: 9.5 + PreferencesHaveChanged(c *Context, preferences []model.Preference) } diff --git a/server/public/plugin/hooks_timer_layer_generated.go b/server/public/plugin/hooks_timer_layer_generated.go index f0a4bdc1941..2a33ab16189 100644 --- a/server/public/plugin/hooks_timer_layer_generated.go +++ b/server/public/plugin/hooks_timer_layer_generated.go @@ -264,3 +264,9 @@ func (hooks *hooksTimerLayer) OnSharedChannelsPing(rc *model.RemoteCluster) bool hooks.recordTime(startTime, "OnSharedChannelsPing", true) return _returnsA } + +func (hooks *hooksTimerLayer) PreferencesHaveChanged(c *Context, preferences []model.Preference) { + startTime := timePkg.Now() + hooks.hooksImpl.PreferencesHaveChanged(c, preferences) + hooks.recordTime(startTime, "PreferencesHaveChanged", true) +} diff --git a/server/public/plugin/plugintest/hooks.go b/server/public/plugin/plugintest/hooks.go index 9e8adc23220..e13dd42f442 100644 --- a/server/public/plugin/plugintest/hooks.go +++ b/server/public/plugin/plugintest/hooks.go @@ -359,6 +359,11 @@ func (_m *Hooks) OnWebSocketDisconnect(webConnID string, userID string) { _m.Called(webConnID, userID) } +// PreferencesHaveChanged provides a mock function with given fields: c, preferences +func (_m *Hooks) PreferencesHaveChanged(c *plugin.Context, preferences []model.Preference) { + _m.Called(c, preferences) +} + // ReactionHasBeenAdded provides a mock function with given fields: c, reaction func (_m *Hooks) ReactionHasBeenAdded(c *plugin.Context, reaction *model.Reaction) { _m.Called(c, reaction) diff --git a/server/public/plugin/product_hooks_generated.go b/server/public/plugin/product_hooks_generated.go index 7452b3f6e61..98a59adf3c2 100644 --- a/server/public/plugin/product_hooks_generated.go +++ b/server/public/plugin/product_hooks_generated.go @@ -147,6 +147,10 @@ type OnSharedChannelsPingIFace interface { OnSharedChannelsPing(rc *model.RemoteCluster) bool } +type PreferencesHaveChangedIFace interface { + PreferencesHaveChanged(c *Context, preferences []model.Preference) +} + type HooksAdapter struct { implemented map[int]struct{} productHooks any @@ -457,6 +461,15 @@ func NewAdapter(productHooks any) (*HooksAdapter, error) { return nil, errors.New("hook has OnSharedChannelsPing method but does not implement plugin.OnSharedChannelsPing interface") } + // Assessing the type of the productHooks if it individually implements PreferencesHaveChanged interface. + tt = reflect.TypeOf((*PreferencesHaveChangedIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[PreferencesHaveChangedID] = struct{}{} + } else if _, ok := ft.MethodByName("PreferencesHaveChanged"); ok { + return nil, errors.New("hook has PreferencesHaveChanged method but does not implement plugin.PreferencesHaveChanged interface") + } + return a, nil } @@ -756,3 +769,12 @@ func (a *HooksAdapter) OnSharedChannelsPing(rc *model.RemoteCluster) bool { return a.productHooks.(OnSharedChannelsPingIFace).OnSharedChannelsPing(rc) } + +func (a *HooksAdapter) PreferencesHaveChanged(c *Context, preferences []model.Preference) { + if _, ok := a.implemented[PreferencesHaveChangedID]; !ok { + panic("product hooks must implement PreferencesHaveChanged") + } + + a.productHooks.(PreferencesHaveChangedIFace).PreferencesHaveChanged(c, preferences) + +}