diff --git a/api4/channel.go b/api4/channel.go index 5bfbddf460..438958dccb 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -885,7 +885,10 @@ func getChannelsForTeamForUser(c *Context, w http.ResponseWriter, r *http.Reques return } - channels, err := c.App.GetChannelsForTeamForUser(c.Params.TeamId, c.Params.UserId, c.Params.IncludeDeleted, lastDeleteAt) + channels, err := c.App.GetChannelsForTeamForUser(c.Params.TeamId, c.Params.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: c.Params.IncludeDeleted, + LastDeleteAt: lastDeleteAt, + }) if err != nil { c.Err = err return diff --git a/api4/channel_category.go b/api4/channel_category.go index bd7b586907..e14a0f13cb 100644 --- a/api4/channel_category.go +++ b/api4/channel_category.go @@ -210,7 +210,10 @@ func updateCategoriesForTeamForUser(c *Context, w http.ResponseWriter, r *http.R } func validateSidebarCategory(c *Context, teamId, userId string, category *model.SidebarCategoryWithChannels) *model.AppError { - channels, err := c.App.GetChannelsForTeamForUser(teamId, userId, true, 0) + channels, err := c.App.GetChannelsForTeamForUser(teamId, userId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 0, + }) if err != nil { return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, err.Error(), http.StatusBadRequest) } @@ -221,7 +224,10 @@ func validateSidebarCategory(c *Context, teamId, userId string, category *model. } func validateSidebarCategories(c *Context, teamId, userId string, categories []*model.SidebarCategoryWithChannels) *model.AppError { - channels, err := c.App.GetChannelsForTeamForUser(teamId, userId, true, 0) + channels, err := c.App.GetChannelsForTeamForUser(teamId, userId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 0, + }) if err != nil { return model.NewAppError("validateSidebarCategory", "api.invalid_channel", nil, err.Error(), http.StatusBadRequest) } diff --git a/app/app_iface.go b/app/app_iface.go index e179ecd245..434dc4f7c7 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -573,7 +573,8 @@ type AppIface interface { GetChannelsForRetentionPolicy(policyID string, offset, limit int) (*model.ChannelsWithCount, *model.AppError) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int) (model.ChannelList, *model.AppError) GetChannelsForSchemePage(scheme *model.Scheme, page int, perPage int) (model.ChannelList, *model.AppError) - GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, *model.AppError) + GetChannelsForTeamForUser(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) + GetChannelsForTeamForUserWithCursor(teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError) GetChannelsForUser(userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError) GetChannelsUserNotIn(teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) GetCloudSession(token string) (*model.Session, *model.AppError) diff --git a/app/channel.go b/app/channel.go index 2f9851b961..25d7e944df 100644 --- a/app/channel.go +++ b/app/channel.go @@ -1767,8 +1767,23 @@ func (a *App) GetChannelByNameForTeamName(channelName, teamName string, includeD return result, nil } -func (a *App) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, *model.AppError) { - list, err := a.Srv().Store.Channel().GetChannels(teamID, userID, includeDeleted, lastDeleteAt) +func (a *App) GetChannelsForTeamForUser(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) { + list, err := a.Srv().Store.Channel().GetChannels(teamID, userID, opts) + if err != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(err, &nfErr): + return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.not_found.app_error", nil, nfErr.Error(), http.StatusNotFound) + default: + return nil, model.NewAppError("GetChannelsForUser", "app.channel.get_channels.get.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + return list, nil +} + +func (a *App) GetChannelsForTeamForUserWithCursor(teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError) { + list, err := a.Srv().Store.Channel().GetChannelsWithCursor(teamID, userID, opts, afterChannelID) if err != nil { var nfErr *store.ErrNotFound switch { diff --git a/app/channel_test.go b/app/channel_test.go index 1f9dbd053a..92ea3ed434 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -997,19 +997,28 @@ func TestGetChannelsForUser(t *testing.T) { defer th.App.PermanentDeleteChannel(channel) defer th.TearDown() - channelList, err := th.App.GetChannelsForTeamForUser(th.BasicTeam.Id, th.BasicUser.Id, false, 0) + channelList, err := th.App.GetChannelsForTeamForUser(th.BasicTeam.Id, th.BasicUser.Id, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) require.Nil(t, err) require.Len(t, channelList, 4) th.App.DeleteChannel(th.Context, channel, th.BasicUser.Id) // Now we get all the non-archived channels for the user - channelList, err = th.App.GetChannelsForTeamForUser(th.BasicTeam.Id, th.BasicUser.Id, false, 0) + channelList, err = th.App.GetChannelsForTeamForUser(th.BasicTeam.Id, th.BasicUser.Id, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) require.Nil(t, err) require.Len(t, channelList, 3) // Now we get all the channels, even though are archived, for the user - channelList, err = th.App.GetChannelsForTeamForUser(th.BasicTeam.Id, th.BasicUser.Id, true, 0) + channelList, err = th.App.GetChannelsForTeamForUser(th.BasicTeam.Id, th.BasicUser.Id, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 0, + }) require.Nil(t, err) require.Len(t, channelList, 4) } diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 3c8ebd70a8..ea68edecc9 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -5203,7 +5203,7 @@ func (a *OpenTracingAppLayer) GetChannelsForSchemePage(scheme *model.Scheme, pag return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, *model.AppError) { +func (a *OpenTracingAppLayer) GetChannelsForTeamForUser(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForTeamForUser") @@ -5215,7 +5215,29 @@ func (a *OpenTracingAppLayer) GetChannelsForTeamForUser(teamID string, userID st }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetChannelsForTeamForUser(teamID, userID, includeDeleted, lastDeleteAt) + resultVar0, resultVar1 := a.app.GetChannelsForTeamForUser(teamID, userID, opts) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + +func (a *OpenTracingAppLayer) GetChannelsForTeamForUserWithCursor(teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsForTeamForUserWithCursor") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetChannelsForTeamForUserWithCursor(teamID, userID, opts, afterChannelID) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) diff --git a/app/plugin_api.go b/app/plugin_api.go index 44f7113a1a..e93d81cfda 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -437,7 +437,10 @@ func (api *PluginAPI) GetChannelByNameForTeamName(teamName, channelName string, } func (api *PluginAPI) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) { - channels, err := api.app.GetChannelsForTeamForUser(teamID, userID, includeDeleted, 0) + channels, err := api.app.GetChannelsForTeamForUser(teamID, userID, &model.ChannelSearchOpts{ + IncludeDeleted: includeDeleted, + LastDeleteAt: 0, + }) if err != nil { return nil, err } diff --git a/app/team.go b/app/team.go index eb8acb12ad..0e1c2319df 100644 --- a/app/team.go +++ b/app/team.go @@ -1116,7 +1116,10 @@ func (a *App) LeaveTeam(c *request.Context, team *model.Team, user *model.User, var channelList model.ChannelList var nErr error - if channelList, nErr = a.Srv().Store.Channel().GetChannels(team.Id, user.Id, true, 0); nErr != nil { + if channelList, nErr = a.Srv().Store.Channel().GetChannels(team.Id, user.Id, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 0, + }); nErr != nil { var nfErr *store.ErrNotFound if errors.As(nErr, &nfErr) { channelList = model.ChannelList{} diff --git a/app/user.go b/app/user.go index 1cb5396234..30b09dcaea 100644 --- a/app/user.go +++ b/app/user.go @@ -868,7 +868,10 @@ func (a *App) invalidateUserChannelMembersCaches(userID string) *model.AppError } for _, team := range teamsForUser { - channelsForUser, err := a.GetChannelsForTeamForUser(team.Id, userID, false, 0) + channelsForUser, err := a.GetChannelsForTeamForUser(team.Id, userID, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) if err != nil { return err } diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go index 504a44d754..9423f1baa7 100644 --- a/manualtesting/manual_testing.go +++ b/manualtesting/manual_testing.go @@ -178,7 +178,10 @@ func manualTest(c *web.Context, w http.ResponseWriter, r *http.Request) { func getChannelID(a app.AppIface, channelname string, teamid string, userid string) (string, bool) { // Grab all the channels - channels, err := a.Srv().Store.Channel().GetChannels(teamid, userid, false, 0) + channels, err := a.Srv().Store.Channel().GetChannels(teamid, userid, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) if err != nil { mlog.Debug("Unable to get channels") return "", false diff --git a/model/channel.go b/model/channel.go index dfa4034790..2aca822999 100644 --- a/model/channel.go +++ b/model/channel.go @@ -141,6 +141,8 @@ type ChannelSearchOpts struct { Private bool Page *int PerPage *int + LastDeleteAt int + LastUpdateAt int } type ChannelMemberCountByGroup struct { diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 202e1fbafc..ac4ee8aa7b 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -1087,7 +1087,7 @@ func (s *OpenTracingLayerChannelStore) GetChannelUnread(channelID string, userID return result, err } -func (s *OpenTracingLayerChannelStore) GetChannels(teamID string, userID string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, error) { +func (s *OpenTracingLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannels") s.Root.Store.SetContext(newCtx) @@ -1096,7 +1096,7 @@ func (s *OpenTracingLayerChannelStore) GetChannels(teamID string, userID string, }() defer span.Finish() - result, err := s.ChannelStore.GetChannels(teamID, userID, includeDeleted, lastDeleteAt) + result, err := s.ChannelStore.GetChannels(teamID, userID, opts) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) @@ -1177,6 +1177,24 @@ func (s *OpenTracingLayerChannelStore) GetChannelsByUser(userID string, includeD return result, err } +func (s *OpenTracingLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithCursor") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithTeamDataByIds") @@ -1442,6 +1460,24 @@ func (s *OpenTracingLayerChannelStore) GetMembersForUser(teamID string, userID s return result, err } +func (s *OpenTracingLayerChannelStore) GetMembersForUserWithCursor(userID string, afterChannel string, afterUser string, limit int, lastUpdateAt int) (model.ChannelMembers, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersForUserWithCursor") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, afterChannel, afterUser, limit, lastUpdateAt) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersForUserWithPagination") @@ -1460,6 +1496,24 @@ func (s *OpenTracingLayerChannelStore) GetMembersForUserWithPagination(userID st return result, err } +func (s *OpenTracingLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersInfoByChannelIds") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMoreChannels") @@ -2446,6 +2500,24 @@ func (s *OpenTracingLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int return result, err } +func (s *OpenTracingLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.GetChannelsLeftSince") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelMemberHistoryStore.GetUsersInChannelDuring") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 99941f6c21..83b4297b7b 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -1213,11 +1213,11 @@ func (s *RetryLayerChannelStore) GetChannelUnread(channelID string, userID strin } -func (s *RetryLayerChannelStore) GetChannels(teamID string, userID string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, error) { +func (s *RetryLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) { tries := 0 for { - result, err := s.ChannelStore.GetChannels(teamID, userID, includeDeleted, lastDeleteAt) + result, err := s.ChannelStore.GetChannels(teamID, userID, opts) if err == nil { return result, nil } @@ -1318,6 +1318,27 @@ func (s *RetryLayerChannelStore) GetChannelsByUser(userID string, includeDeleted } +func (s *RetryLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) { tries := 0 @@ -1618,6 +1639,27 @@ func (s *RetryLayerChannelStore) GetMembersForUser(teamID string, userID string) } +func (s *RetryLayerChannelStore) GetMembersForUserWithCursor(userID string, afterChannel string, afterUser string, limit int, lastUpdateAt int) (model.ChannelMembers, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, afterChannel, afterUser, limit, lastUpdateAt) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) { tries := 0 @@ -1639,6 +1681,27 @@ func (s *RetryLayerChannelStore) GetMembersForUserWithPagination(userID string, } +func (s *RetryLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) { tries := 0 @@ -2716,6 +2779,27 @@ func (s *RetryLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int } +func (s *RetryLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) { + + tries := 0 + for { + result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) { tries := 0 diff --git a/store/searchlayer/file_info_layer.go b/store/searchlayer/file_info_layer.go index 131e702759..8c5d186d18 100644 --- a/store/searchlayer/file_info_layer.go +++ b/store/searchlayer/file_info_layer.go @@ -160,7 +160,10 @@ func (s SearchFileInfoStore) PermanentDeleteByUser(userId string) (int64, error) func (s SearchFileInfoStore) Search(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.FileInfoList, error) { for _, engine := range s.rootStore.searchEngine.GetActiveEngines() { if engine.IsSearchEnabled() { - userChannels, nErr := s.rootStore.Channel().GetChannels(teamId, userId, paramsList[0].IncludeDeletedChannels, 0) + userChannels, nErr := s.rootStore.Channel().GetChannels(teamId, userId, &model.ChannelSearchOpts{ + IncludeDeleted: paramsList[0].IncludeDeletedChannels, + LastDeleteAt: 0, + }) if nErr != nil { return nil, nErr } diff --git a/store/searchlayer/post_layer.go b/store/searchlayer/post_layer.go index 6c30fc20d8..1f594cdd7b 100644 --- a/store/searchlayer/post_layer.go +++ b/store/searchlayer/post_layer.go @@ -142,7 +142,11 @@ func (s SearchPostStore) searchPostsForUserByEngine(engine searchengine.SearchEn } // We only allow the user to search in channels they are a member of. - userChannels, err2 := s.rootStore.Channel().GetChannels(teamId, userId, paramsList[0].IncludeDeletedChannels, 0) + userChannels, err2 := s.rootStore.Channel().GetChannels(teamId, userId, + &model.ChannelSearchOpts{ + IncludeDeleted: paramsList[0].IncludeDeletedChannels, + LastDeleteAt: 0, + }) if err2 != nil { return nil, errors.Wrap(err2, "error getting channel for user") } diff --git a/store/sqlstore/channel_member_history_store.go b/store/sqlstore/channel_member_history_store.go index 123cc0c61b..2b6186068a 100644 --- a/store/sqlstore/channel_member_history_store.go +++ b/store/sqlstore/channel_member_history_store.go @@ -255,3 +255,24 @@ func (s SqlChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit } return rowsAffected, nil } + +// GetChannelsLeftSince returns list of channels that the user has left after a given time, +// but has not rejoined again. +func (s SqlChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) { + query, params, err := s.getQueryBuilder(). + Select("ChannelId"). + From("ChannelMemberHistory"). + GroupBy("ChannelId"). + Where(sq.Eq{"UserId": userID}). + Having("MAX(LeaveTime) > MAX(JoinTime) AND MAX(LeaveTime) IS NOT NULL AND MAX(LeaveTime) >= ?", since).ToSql() + if err != nil { + return nil, errors.Wrap(err, "channel_member_history_to_sql") + } + channelIds := []string{} + err = s.GetReplicaX().Select(&channelIds, query, params...) + if err != nil { + return nil, errors.Wrapf(err, "GetChannelsLeftSince userId=%s since=%d", userID, since) + } + + return channelIds, nil +} diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index afddc4b852..f33180bf72 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1084,37 +1084,105 @@ func (s SqlChannelStore) PermanentDeleteMembersByChannel(channelId string) error return nil } -func (s SqlChannelStore) GetChannels(teamId string, userId string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, error) { +func (s SqlChannelStore) GetChannels(teamId string, userId string, opts *model.ChannelSearchOpts) (model.ChannelList, error) { query := s.getQueryBuilder(). - Select("Channels.*"). - From("Channels, ChannelMembers"). + Select("ch.*"). + From("Channels ch, ChannelMembers cm"). Where( sq.And{ - sq.Expr("Id = ChannelId"), - sq.Eq{"UserId": userId}, + sq.Expr("ch.Id = cm.ChannelId"), + sq.Eq{"cm.UserId": userId}, }, ). - OrderBy("DisplayName") + OrderBy("ch.DisplayName") if teamId != "" { query = query.Where(sq.Or{ - sq.Eq{"TeamId": teamId}, - sq.Eq{"TeamId": ""}, + sq.Eq{"ch.TeamId": teamId}, + sq.Eq{"ch.TeamId": ""}, }) } - if includeDeleted { - if lastDeleteAt != 0 { + if opts.IncludeDeleted { + if opts.LastDeleteAt != 0 { // We filter by non-archived, and archived >= a timestamp. query = query.Where(sq.Or{ - sq.Eq{"DeleteAt": 0}, - sq.GtOrEq{"DeleteAt": lastDeleteAt}, + sq.Eq{"ch.DeleteAt": 0}, + sq.GtOrEq{"ch.DeleteAt": opts.LastDeleteAt}, }) } - // If lastDeleteAt is not set, we include everything. That means no filter is needed. + // If opts.LastDeleteAt is not set, we include everything. That means no filter is needed. } else { // Don't include archived channels. - query = query.Where(sq.Eq{"DeleteAt": 0}) + query = query.Where(sq.Eq{"ch.DeleteAt": 0}) + } + + if opts.LastUpdateAt > 0 { + query = query.Where(sq.GtOrEq{"ch.UpdateAt": opts.LastUpdateAt}) + } + + channels := model.ChannelList{} + sql, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrapf(err, "getchannels_tosql") + } + + err = s.GetReplicaX().Select(&channels, sql, args...) + if err != nil { + return nil, errors.Wrapf(err, "failed to get channels with TeamId=%s and UserId=%s", teamId, userId) + } + + if len(channels) == 0 { + return nil, store.NewErrNotFound("Channel", "userId="+userId) + } + + return channels, nil +} + +func (s SqlChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) { + query := s.getQueryBuilder(). + Select("ch.*"). + From("Channels ch, ChannelMembers cm"). + Where( + sq.And{ + sq.Expr("ch.Id = cm.ChannelId"), + sq.Eq{"cm.UserId": userId}, + }, + ). + OrderBy("ch.Id") + + if opts.PerPage != nil { + // The limit is verified at the GraphQL layer. + query = query.Limit(uint64(*opts.PerPage)) + } + + if afterChannelID != "" { + query = query.Where(sq.Gt{"ch.Id": afterChannelID}) + } + + if teamId != "" { + query = query.Where(sq.Or{ + sq.Eq{"ch.TeamId": teamId}, + sq.Eq{"ch.TeamId": ""}, + }) + } + + if opts.IncludeDeleted { + if opts.LastDeleteAt != 0 { + // We filter by non-archived, and archived >= a timestamp. + query = query.Where(sq.Or{ + sq.Eq{"ch.DeleteAt": 0}, + sq.GtOrEq{"ch.DeleteAt": opts.LastDeleteAt}, + }) + } + // If opts.LastDeleteAt is not set, we include everything. That means no filter is needed. + } else { + // Don't include archived channels. + query = query.Where(sq.Eq{"ch.DeleteAt": 0}) + } + + if opts.LastUpdateAt > 0 { + query = query.Where(sq.GtOrEq{"ch.UpdateAt": opts.LastUpdateAt}) } channels := model.ChannelList{} @@ -2773,6 +2841,56 @@ func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) (model. return dbMembers.ToModel(), nil } +func (s SqlChannelStore) GetMembersForUserWithCursor(userID, afterChannel, afterUser string, limit, lastUpdateAt int) (model.ChannelMembers, error) { + query := s.getQueryBuilder(). + Select("ChannelMembers.*", + "TeamScheme.DefaultChannelGuestRole TeamSchemeDefaultGuestRole", + "TeamScheme.DefaultChannelUserRole TeamSchemeDefaultUserRole", + "TeamScheme.DefaultChannelAdminRole TeamSchemeDefaultAdminRole", + "ChannelScheme.DefaultChannelGuestRole ChannelSchemeDefaultGuestRole", + "ChannelScheme.DefaultChannelUserRole ChannelSchemeDefaultUserRole", + "ChannelScheme.DefaultChannelAdminRole ChannelSchemeDefaultAdminRole"). + From("ChannelMembers"). + InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id"). + LeftJoin("Schemes ChannelScheme ON Channels.SchemeId = ChannelScheme.Id"). + LeftJoin("Teams ON Channels.TeamId = Teams.Id"). + LeftJoin("Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id"). + Where(sq.Eq{ + "ChannelMembers.UserId": userID, + "Channels.DeleteAt": 0, + }). + OrderBy("ChannelId, UserId ASC"). + // The limit is verified at the GraphQL layer. + Limit(uint64(limit)) + + if afterChannel != "" && afterUser != "" { + query = query.Where(sq.Or{ + sq.Gt{"ChannelMembers.ChannelId": afterChannel}, + sq.And{ + sq.Eq{"ChannelMembers.ChannelId": afterChannel}, + sq.Gt{"ChannelMembers.UserId": afterUser}, + }, + }) + } + + if lastUpdateAt != 0 { + query = query.Where(sq.GtOrEq{"ChannelMembers.LastUpdateAt": lastUpdateAt}) + } + + queryString, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrap(err, "getMembersForUserWithCursor_tosql") + } + + dbMembers := channelMemberWithSchemeRolesList{} + err = s.GetReplicaX().Select(&dbMembers, queryString, args...) + if err != nil { + return nil, errors.Wrapf(err, "failed to find ChannelMembers data with userId=%s", userID) + } + + return dbMembers.ToModel(), nil +} + func (s SqlChannelStore) GetMembersForUserWithPagination(userId string, page, perPage int) (model.ChannelMembersWithTeamData, error) { dbMembers := channelMemberWithTeamWithSchemeRolesList{} offset := page * perPage @@ -3470,6 +3588,43 @@ func (s SqlChannelStore) GetMembersByChannelIds(channelIds []string, userId stri return dbMembers.ToModel(), nil } +func (s SqlChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) { + query := s.getQueryBuilder(). + Select("Channels.Id as ChannelId, Users.Id, Users.FirstName, Users.LastName, Users.Nickname, Users.Username"). + From("ChannelMembers as cm"). + Join("Channels ON cm.ChannelId = Channels.Id"). + Join("Users ON cm.UserId = Users.Id"). + Where(sq.Eq{ + "Channels.Id": channelIDs, + "Channels.DeleteAt": 0, + }) + + sql, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrap(err, "dm_gm_names_tosql") + } + + res := []*struct { + model.User + ChannelId string + }{} + + if err := s.GetReplicaX().Select(&res, sql, args...); err != nil { + return nil, errors.Wrap(err, "failed to find channels display name") + } + + if len(res) == 0 { + return nil, store.NewErrNotFound("User", fmt.Sprintf("%v", channelIDs)) + } + + userInfo := make(map[string][]*model.User) + for _, item := range res { + userInfo[item.ChannelId] = append(userInfo[item.ChannelId], &item.User) + } + + return userInfo, nil +} + func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit int) (model.ChannelList, error) { channels := model.ChannelList{} err := s.GetReplicaX().Select(&channels, "SELECT * FROM Channels WHERE SchemeId = ? ORDER BY DisplayName LIMIT ? OFFSET ?", schemeId, limit, offset) diff --git a/store/store.go b/store/store.go index c56cec4f8b..4020f3c3b1 100644 --- a/store/store.go +++ b/store/store.go @@ -178,7 +178,8 @@ type ChannelStore interface { GetByNameIncludeDeleted(team_id string, name string, allowFromCache bool) (*model.Channel, error) GetDeletedByName(team_id string, name string) (*model.Channel, error) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) - GetChannels(teamID string, userID string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, error) + GetChannels(teamID, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) + GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) GetChannelsByUser(userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, error) GetAllChannelMembersById(id string) ([]string, error) GetAllChannels(page, perPage int, opts ChannelSearchOpts) (model.ChannelListWithTeamData, error) @@ -230,6 +231,7 @@ type ChannelStore interface { GetMembersForUser(teamID string, userID string) (model.ChannelMembers, error) GetTeamMembersForChannel(channelID string) ([]string, error) GetMembersForUserWithPagination(userID string, page, perPage int) (model.ChannelMembersWithTeamData, error) + GetMembersForUserWithCursor(userID, afterChannel, afterUser string, limit, lastUpdateAt int) (model.ChannelMembers, error) Autocomplete(userID, term string, includeDeleted bool) (model.ChannelListWithTeamData, error) AutocompleteInTeam(teamID, userID, term string, includeDeleted bool) (model.ChannelList, error) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) @@ -241,6 +243,7 @@ type ChannelStore interface { SearchGroupChannels(userID, term string) (model.ChannelList, error) GetMembersByIds(channelID string, userIds []string) (model.ChannelMembers, error) GetMembersByChannelIds(channelIds []string, userID string) (model.ChannelMembers, error) + GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) AnalyticsDeletedTypeCount(teamID string, channelType string) (int64, error) GetChannelUnread(channelID, userID string) (*model.ChannelUnread, error) ClearCaches() @@ -284,6 +287,7 @@ type ChannelMemberHistoryStore interface { PermanentDeleteBatchForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error) DeleteOrphanedRows(limit int) (deleted int64, err error) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) + GetChannelsLeftSince(userID string, since int64) ([]string, error) } type ThreadStore interface { GetThreadFollowers(threadID string, fetchOnlyActive bool) ([]string, error) @@ -919,6 +923,8 @@ type ChannelSearchOpts struct { Private bool Page *int PerPage *int + LastDeleteAt int + LastUpdateAt int } func (c *ChannelSearchOpts) IsPaginated() bool { diff --git a/store/storetest/channel_member_history_store.go b/store/storetest/channel_member_history_store.go index 7c701e336a..1e88a66fe3 100644 --- a/store/storetest/channel_member_history_store.go +++ b/store/storetest/channel_member_history_store.go @@ -22,6 +22,7 @@ func TestChannelMemberHistoryStore(t *testing.T, ss store.Store) { t.Run("TestGetUsersInChannelAtChannelMembers", func(t *testing.T) { testGetUsersInChannelAtChannelMembers(t, ss) }) t.Run("TestPermanentDeleteBatch", func(t *testing.T) { testPermanentDeleteBatch(t, ss) }) t.Run("TestPermanentDeleteBatchForRetentionPolicies", func(t *testing.T) { testPermanentDeleteBatchForRetentionPolicies(t, ss) }) + t.Run("TestGetChannelsLeftSince", func(t *testing.T) { testGetChannelsLeftSince(t, ss) }) } func testLogJoinEvent(t *testing.T, ss store.Store) { @@ -29,7 +30,7 @@ func testLogJoinEvent(t *testing.T, ss store.Store) { ch := model.Channel{ TeamId: model.NewId(), DisplayName: "Display " + model.NewId(), - Name: "zz" + model.NewId() + "b", + Name: NewTestId(), Type: model.ChannelTypeOpen, } channel, err := ss.Channel().Save(&ch, -1) @@ -55,7 +56,7 @@ func testLogLeaveEvent(t *testing.T, ss store.Store) { ch := model.Channel{ TeamId: model.NewId(), DisplayName: "Display " + model.NewId(), - Name: "zz" + model.NewId() + "b", + Name: NewTestId(), Type: model.ChannelTypeOpen, } channel, err := ss.Channel().Save(&ch, -1) @@ -84,7 +85,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) { ch := &model.Channel{ TeamId: model.NewId(), DisplayName: "Display " + model.NewId(), - Name: "zz" + model.NewId() + "b", + Name: NewTestId(), Type: model.ChannelTypeOpen, } channel, err := ss.Channel().Save(ch, -1) @@ -180,7 +181,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) { channel := &model.Channel{ TeamId: model.NewId(), DisplayName: "Display " + model.NewId(), - Name: "zz" + model.NewId() + "b", + Name: NewTestId(), Type: model.ChannelTypeOpen, } channel, err := ss.Channel().Save(channel, -1) @@ -292,7 +293,7 @@ func testPermanentDeleteBatch(t *testing.T, ss store.Store) { channel := &model.Channel{ TeamId: model.NewId(), DisplayName: "Display " + model.NewId(), - Name: "zz" + model.NewId() + "b", + Name: NewTestId(), Type: model.ChannelTypeOpen, } channel, err := ss.Channel().Save(channel, -1) @@ -389,3 +390,54 @@ func testPermanentDeleteBatchForRetentionPolicies(t *testing.T, ss store.Store) require.NoError(t, err) require.Empty(t, result, "history should have been deleted by channel policy") } + +func testGetChannelsLeftSince(t *testing.T, ss store.Store) { + team, err := ss.Team().Save(&model.Team{ + DisplayName: "DisplayName", + Name: "team" + model.NewId(), + Email: MakeEmail(), + Type: model.TeamOpen, + }) + require.NoError(t, err) + channel, err := ss.Channel().Save(&model.Channel{ + TeamId: team.Id, + DisplayName: "DisplayName", + Name: "channel" + model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + userID := model.NewId() + + joinTime := int64(1000) + err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime) + require.NoError(t, err) + + // has not left + ids, err := ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime) + require.NoError(t, err) + assert.Empty(t, ids) + + // left + err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, joinTime+100) + require.NoError(t, err) + ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+100) + require.NoError(t, err) + assert.Equal(t, []string{channel.Id}, ids) + ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+200) + require.NoError(t, err) + assert.Empty(t, ids) + + // joined and left again. + err = ss.ChannelMemberHistory().LogJoinEvent(userID, channel.Id, joinTime+200) + require.NoError(t, err) + err = ss.ChannelMemberHistory().LogLeaveEvent(userID, channel.Id, joinTime+300) + require.NoError(t, err) + // should be same for both time stamps + ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+100) + require.NoError(t, err) + assert.Equal(t, []string{channel.Id}, ids) + ids, err = ss.ChannelMemberHistory().GetChannelsLeftSince(userID, joinTime+300) + require.NoError(t, err) + assert.Equal(t, []string{channel.Id}, ids) +} diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 86fb705313..cb4f03c7bf 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -79,6 +79,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("RemoveMembers", func(t *testing.T) { testChannelRemoveMembers(t, ss) }) t.Run("ChannelDeleteMemberStore", func(t *testing.T) { testChannelDeleteMemberStore(t, ss) }) t.Run("GetChannels", func(t *testing.T) { testChannelStoreGetChannels(t, ss) }) + t.Run("GetChannelsWithCursor", func(t *testing.T) { testChannelStoreGetChannelsWithCursor(t, ss) }) t.Run("GetChannelsByUser", func(t *testing.T) { testChannelStoreGetChannelsByUser(t, ss) }) t.Run("GetAllChannels", func(t *testing.T) { testChannelStoreGetAllChannels(t, ss, s) }) t.Run("GetMoreChannels", func(t *testing.T) { testChannelStoreGetMoreChannels(t, ss) }) @@ -87,6 +88,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("GetPublicChannelsByIdsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsByIdsForTeam(t, ss) }) t.Run("GetChannelCounts", func(t *testing.T) { testChannelStoreGetChannelCounts(t, ss) }) t.Run("GetMembersForUser", func(t *testing.T) { testChannelStoreGetMembersForUser(t, ss) }) + t.Run("GetMembersForUserWithCursor", func(t *testing.T) { testChannelStoreGetMembersForUserWithCursor(t, ss) }) t.Run("GetMembersForUserWithPagination", func(t *testing.T) { testChannelStoreGetMembersForUserWithPagination(t, ss) }) t.Run("CountPostsAfter", func(t *testing.T) { testCountPostsAfter(t, ss) }) t.Run("UpdateLastViewedAt", func(t *testing.T) { testChannelStoreUpdateLastViewedAt(t, ss) }) @@ -105,6 +107,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) }) t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) t.Run("GetMembersByChannelIds", func(t *testing.T) { testChannelStoreGetMembersByChannelIds(t, ss) }) + t.Run("GetMembersInfoByChannelIds", func(t *testing.T) { testChannelStoreGetMembersInfoByChannelIds(t, ss) }) t.Run("SearchGroupChannels", func(t *testing.T) { testChannelStoreSearchGroupChannels(t, ss) }) t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) @@ -719,7 +722,10 @@ func testChannelStoreDelete(t *testing.T, ss store.Store) { nErr = ss.Channel().Delete(o3.Id, model.GetMillis()) require.NoError(t, nErr, nErr) - list, nErr := ss.Channel().GetChannels(o1.TeamId, m1.UserId, false, 0) + list, nErr := ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) require.NoError(t, nErr) require.Len(t, list, 1, "invalid number of channels") @@ -730,7 +736,10 @@ func testChannelStoreDelete(t *testing.T, ss store.Store) { cresult := ss.Channel().PermanentDelete(o2.Id) require.NoError(t, cresult) - list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, false, 0) + list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) if assert.Error(t, nErr) { var nfErr *store.ErrNotFound require.True(t, errors.As(nErr, &nfErr)) @@ -3263,12 +3272,13 @@ func testChannelDeleteMemberStore(t *testing.T, ss store.Store) { func testChannelStoreGetChannels(t *testing.T, ss store.Store) { team := model.NewId() - o1 := model.Channel{} + o1 := &model.Channel{} o1.TeamId = team o1.DisplayName = "Channel1" o1.Name = NewTestId() o1.Type = model.ChannelTypeOpen - _, nErr := ss.Channel().Save(&o1, -1) + var nErr error + o1, nErr = ss.Channel().Save(o1, -1) require.NoError(t, nErr) o2 := model.Channel{} @@ -3315,7 +3325,10 @@ func testChannelStoreGetChannels(t *testing.T, ss store.Store) { _, err = ss.Channel().SaveMember(&m4) require.NoError(t, err) - list, nErr := ss.Channel().GetChannels(o1.TeamId, m1.UserId, false, 0) + list, nErr := ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) require.NoError(t, nErr) require.Len(t, list, 3) require.Equal(t, o1.Id, list[0].Id, "missing channel") @@ -3342,6 +3355,25 @@ func testChannelStoreGetChannels(t *testing.T, ss store.Store) { _, ok = ids4[o1.Id] require.True(t, ok, "missing channel") + // Sleeping to guarantee that the + // UpdateAt is different. + // The proper way would be to set UpdateAt during channel creation itself, + // but the *Channel.PreSave method ignores any existing CreateAt value. + // TODO: check if using an existing CreateAt breaks anything. + time.Sleep(time.Millisecond) + + now := model.GetMillis() + _, nErr = ss.Channel().Update(o1) + require.NoError(t, nErr) + + list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastUpdateAt: int(now), + }) + require.NoError(t, nErr) + // should return 1 + require.Len(t, list, 1) + nErr = ss.Channel().Delete(o2.Id, 10) require.NoError(t, nErr) @@ -3349,13 +3381,19 @@ func testChannelStoreGetChannels(t *testing.T, ss store.Store) { require.NoError(t, nErr) // should return 1 - list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, false, 0) + list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }) require.NoError(t, nErr) require.Len(t, list, 1) require.Equal(t, o1.Id, list[0].Id, "missing channel") // Should return all - list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, true, 0) + list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 0, + }) require.NoError(t, nErr) require.Len(t, list, 3) require.Equal(t, o1.Id, list[0].Id, "missing channel") @@ -3363,7 +3401,10 @@ func testChannelStoreGetChannels(t *testing.T, ss store.Store) { require.Equal(t, o3.Id, list[2].Id, "missing channel") // Should still return all - list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, true, 10) + list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 10, + }) require.NoError(t, nErr) require.Len(t, list, 3) require.Equal(t, o1.Id, list[0].Id, "missing channel") @@ -3371,7 +3412,10 @@ func testChannelStoreGetChannels(t *testing.T, ss store.Store) { require.Equal(t, o3.Id, list[2].Id, "missing channel") // Should return 2 - list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, true, 20) + list, nErr = ss.Channel().GetChannels(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 20, + }) require.NoError(t, nErr) require.Len(t, list, 2) require.Equal(t, o1.Id, list[0].Id, "missing channel") @@ -3399,6 +3443,147 @@ func testChannelStoreGetChannels(t *testing.T, ss store.Store) { ss.Channel().InvalidateAllChannelMembersForUser(m1.UserId) } +func testChannelStoreGetChannelsWithCursor(t *testing.T, ss store.Store) { + teamID := model.NewId() + o1 := &model.Channel{} + o1.TeamId = teamID + o1.DisplayName = "Channel1" + o1.Name = NewTestId() + o1.Type = model.ChannelTypeOpen + var nErr error + o1, nErr = ss.Channel().Save(o1, -1) + require.NoError(t, nErr) + + o2 := model.Channel{} + o2.TeamId = teamID + o2.DisplayName = "Channel2" + o2.Name = NewTestId() + o2.Type = model.ChannelTypeOpen + _, nErr = ss.Channel().Save(&o2, -1) + require.NoError(t, nErr) + + o3 := model.Channel{} + o3.TeamId = teamID + o3.DisplayName = "Channel3" + o3.Name = NewTestId() + o3.Type = model.ChannelTypeOpen + _, nErr = ss.Channel().Save(&o3, -1) + require.NoError(t, nErr) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err := ss.Channel().SaveMember(&m1) + require.NoError(t, err) + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = model.NewId() + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = ss.Channel().SaveMember(&m2) + require.NoError(t, err) + + m3 := model.ChannelMember{} + m3.ChannelId = o2.Id + m3.UserId = m1.UserId + m3.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = ss.Channel().SaveMember(&m3) + require.NoError(t, err) + + m4 := model.ChannelMember{} + m4.ChannelId = o3.Id + m4.UserId = m1.UserId + m4.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = ss.Channel().SaveMember(&m4) + require.NoError(t, err) + + list, nErr := ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + PerPage: model.NewInt(2), + }, "") + require.NoError(t, nErr) + require.Len(t, list, 2) + require.Equal(t, teamID, list[0].TeamId, "incorrect teamID") + require.Equal(t, teamID, list[1].TeamId, "incorrect teamID") + + list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + PerPage: model.NewInt(2), + }, list[1].Id) + require.NoError(t, nErr) + require.Len(t, list, 1) + require.Equal(t, teamID, list[0].TeamId, "incorrect teamID") + + // Sleeping to guarantee that the + // UpdateAt is different. + // The proper way would be to set UpdateAt during channel creation itself, + // but the *Channel.PreSave method ignores any existing CreateAt value. + // TODO: check if using an existing CreateAt breaks anything. + time.Sleep(time.Millisecond) + + now := model.GetMillis() + _, nErr = ss.Channel().Update(o1) + require.NoError(t, nErr) + + list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastUpdateAt: int(now), + }, "") + require.NoError(t, nErr) + // should return 1 + require.Len(t, list, 1) + + nErr = ss.Channel().Delete(o2.Id, 10) + require.NoError(t, nErr) + + nErr = ss.Channel().Delete(o3.Id, 20) + require.NoError(t, nErr) + + // should return 1 + list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: false, + LastDeleteAt: 0, + }, "") + require.NoError(t, nErr) + require.Len(t, list, 1) + + // Should return all + list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 0, + PerPage: model.NewInt(2), + }, "") + require.NoError(t, nErr) + require.Len(t, list, 2) + + list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 0, + PerPage: model.NewInt(2), + }, list[1].Id) + require.NoError(t, nErr) + require.Len(t, list, 1) + + // Should still return all + list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 10, + }, "") + require.NoError(t, nErr) + require.Len(t, list, 3) + + // Should return 2 + list, nErr = ss.Channel().GetChannelsWithCursor(o1.TeamId, m1.UserId, &model.ChannelSearchOpts{ + IncludeDeleted: true, + LastDeleteAt: 20, + }, "") + require.NoError(t, nErr) + require.Len(t, list, 2) +} + func testChannelStoreGetChannelsByUser(t *testing.T, ss store.Store) { team := model.NewId() team2 := model.NewId() @@ -4191,6 +4376,120 @@ func testChannelStoreGetMembersForUser(t *testing.T, ss store.Store) { }) } +func testChannelStoreGetMembersForUserWithCursor(t *testing.T, ss store.Store) { + t1 := model.Team{} + t1.DisplayName = "Team1" + t1.Name = NewTestId() + t1.Email = MakeEmail() + t1.Type = model.TeamOpen + _, err := ss.Team().Save(&t1) + require.NoError(t, err) + + o1 := model.Channel{} + o1.TeamId = t1.Id + o1.DisplayName = "Channel1" + o1.Name = NewTestId() + o1.Type = model.ChannelTypeOpen + _, nErr := ss.Channel().Save(&o1, -1) + require.NoError(t, nErr) + + o2 := model.Channel{} + o2.TeamId = o1.TeamId + o2.DisplayName = "Channel2" + o2.Name = NewTestId() + o2.Type = model.ChannelTypeOpen + _, nErr = ss.Channel().Save(&o2, -1) + require.NoError(t, nErr) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = ss.Channel().SaveMember(&m1) + require.NoError(t, err) + + m2 := model.ChannelMember{} + m2.ChannelId = o2.Id + m2.UserId = m1.UserId + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = ss.Channel().SaveMember(&m2) + require.NoError(t, err) + + t.Run("with channels", func(t *testing.T) { + var members model.ChannelMembers + members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", "", 1, 0) + require.NoError(t, err) + assert.Len(t, members, 1) + members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", "", 3, 0) + require.NoError(t, err) + assert.Len(t, members, 2) + members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, members[0].ChannelId, m1.UserId, 1, 0) + require.NoError(t, err) + assert.Len(t, members, 1) + }) + + t.Run("with channels and direct messages", func(t *testing.T) { + user := model.User{Id: m1.UserId} + u1 := model.User{Id: model.NewId()} + u2 := model.User{Id: model.NewId()} + u3 := model.User{Id: model.NewId()} + u4 := model.User{Id: model.NewId()} + _, nErr = ss.Channel().CreateDirectChannel(&u1, &user) + require.NoError(t, nErr) + _, nErr = ss.Channel().CreateDirectChannel(&u2, &user) + require.NoError(t, nErr) + // other user direct message + _, nErr = ss.Channel().CreateDirectChannel(&u3, &u4) + require.NoError(t, nErr) + + members, err2 := ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", "", 10, 0) + require.NoError(t, err2) + assert.Len(t, members, 4) + + members, err2 = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", "", 2, 0) + require.NoError(t, err2) + assert.Len(t, members, 2) + + members, err2 = ss.Channel().GetMembersForUserWithCursor(m1.UserId, members[1].ChannelId, m1.UserId, 2, 0) + require.NoError(t, err2) + assert.Len(t, members, 2) + }) + + t.Run("with channels, direct channels and group messages", func(t *testing.T) { + userIds := []string{model.NewId(), model.NewId(), model.NewId(), m1.UserId} + group := &model.Channel{ + Name: model.GetGroupNameFromUserIds(userIds), + DisplayName: "test", + Type: model.ChannelTypeGroup, + } + var channel *model.Channel + channel, nErr = ss.Channel().Save(group, 10000) + require.NoError(t, nErr) + for _, userId := range userIds { + cm := &model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + SchemeUser: true, + } + + _, err = ss.Channel().SaveMember(cm) + require.NoError(t, err) + } + members, err := ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", "", 10, 0) + require.NoError(t, err) + assert.Len(t, members, 5) + + members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, "", "", 2, 0) + require.NoError(t, err) + assert.Len(t, members, 2) + + members, err = ss.Channel().GetMembersForUserWithCursor(m1.UserId, members[1].ChannelId, m1.UserId, 10, 0) + require.NoError(t, err) + assert.Len(t, members, 3) + }) +} + func testChannelStoreGetMembersForUserWithPagination(t *testing.T, ss store.Store) { t1 := model.Team{ DisplayName: "team1", @@ -6072,6 +6371,62 @@ func testChannelStoreGetMembersByChannelIds(t *testing.T, ss store.Store) { }) } +func testChannelStoreGetMembersInfoByChannelIds(t *testing.T, ss store.Store) { + u, err := ss.User().Save(&model.User{ + Username: "user.test", + Email: MakeEmail(), + Nickname: model.NewId(), + }) + require.NoError(t, err) + + // Create a couple channels and add the user to them + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: model.NewId(), + DisplayName: model.NewId(), + Name: model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: model.NewId(), + DisplayName: model.NewId(), + Name: model.NewId(), + Type: model.ChannelTypeOpen, + }, -1) + require.NoError(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel1.Id, + UserId: u.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel2.Id, + UserId: u.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.NoError(t, err) + + t.Run("should return the user's members for the given channels", func(t *testing.T) { + result, nErr := ss.Channel().GetMembersInfoByChannelIds([]string{channel1.Id, channel2.Id}) + require.NoError(t, nErr) + assert.Len(t, result, 2) + for _, item := range result { + assert.Len(t, item, 1) + assert.Equal(t, u.Id, item[0].Id) + } + }) + + t.Run("should not error or return anything for invalid channel IDs", func(t *testing.T) { + _, err := ss.Channel().GetMembersInfoByChannelIds([]string{model.NewId(), model.NewId()}) + var nfErr *store.ErrNotFound + require.True(t, errors.As(err, &nfErr)) + }) +} + func testChannelStoreSearchGroupChannels(t *testing.T, ss store.Store) { // Users u1 := &model.User{} diff --git a/store/storetest/mocks/ChannelMemberHistoryStore.go b/store/storetest/mocks/ChannelMemberHistoryStore.go index 6f0f9add56..f77709828a 100644 --- a/store/storetest/mocks/ChannelMemberHistoryStore.go +++ b/store/storetest/mocks/ChannelMemberHistoryStore.go @@ -35,6 +35,29 @@ func (_m *ChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int64, error return r0, r1 } +// GetChannelsLeftSince provides a mock function with given fields: userID, since +func (_m *ChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) { + ret := _m.Called(userID, since) + + var r0 []string + if rf, ok := ret.Get(0).(func(string, int64) []string); ok { + r0 = rf(userID, since) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, int64) error); ok { + r1 = rf(userID, since) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetUsersInChannelDuring provides a mock function with given fields: startTime, endTime, channelID func (_m *ChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) { ret := _m.Called(startTime, endTime, channelID) diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 70233def60..1b2a7e8873 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -674,13 +674,13 @@ func (_m *ChannelStore) GetChannelUnread(channelID string, userID string) (*mode return r0, r1 } -// GetChannels provides a mock function with given fields: teamID, userID, includeDeleted, lastDeleteAt -func (_m *ChannelStore) GetChannels(teamID string, userID string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, error) { - ret := _m.Called(teamID, userID, includeDeleted, lastDeleteAt) +// GetChannels provides a mock function with given fields: teamID, userID, opts +func (_m *ChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) { + ret := _m.Called(teamID, userID, opts) var r0 model.ChannelList - if rf, ok := ret.Get(0).(func(string, string, bool, int) model.ChannelList); ok { - r0 = rf(teamID, userID, includeDeleted, lastDeleteAt) + if rf, ok := ret.Get(0).(func(string, string, *model.ChannelSearchOpts) model.ChannelList); ok { + r0 = rf(teamID, userID, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(model.ChannelList) @@ -688,8 +688,8 @@ func (_m *ChannelStore) GetChannels(teamID string, userID string, includeDeleted } var r1 error - if rf, ok := ret.Get(1).(func(string, string, bool, int) error); ok { - r1 = rf(teamID, userID, includeDeleted, lastDeleteAt) + if rf, ok := ret.Get(1).(func(string, string, *model.ChannelSearchOpts) error); ok { + r1 = rf(teamID, userID, opts) } else { r1 = ret.Error(1) } @@ -789,6 +789,29 @@ func (_m *ChannelStore) GetChannelsByUser(userID string, includeDeleted bool, la return r0, r1 } +// GetChannelsWithCursor provides a mock function with given fields: teamId, userId, opts, afterChannelID +func (_m *ChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) { + ret := _m.Called(teamId, userId, opts, afterChannelID) + + var r0 model.ChannelList + if rf, ok := ret.Get(0).(func(string, string, *model.ChannelSearchOpts, string) model.ChannelList); ok { + r0 = rf(teamId, userId, opts, afterChannelID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.ChannelList) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, *model.ChannelSearchOpts, string) error); ok { + r1 = rf(teamId, userId, opts, afterChannelID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetChannelsWithTeamDataByIds provides a mock function with given fields: channelIds, includeDeleted func (_m *ChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) { ret := _m.Called(channelIds, includeDeleted) @@ -1121,6 +1144,29 @@ func (_m *ChannelStore) GetMembersForUser(teamID string, userID string) (model.C return r0, r1 } +// GetMembersForUserWithCursor provides a mock function with given fields: userID, afterChannel, afterUser, limit, lastUpdateAt +func (_m *ChannelStore) GetMembersForUserWithCursor(userID string, afterChannel string, afterUser string, limit int, lastUpdateAt int) (model.ChannelMembers, error) { + ret := _m.Called(userID, afterChannel, afterUser, limit, lastUpdateAt) + + var r0 model.ChannelMembers + if rf, ok := ret.Get(0).(func(string, string, string, int, int) model.ChannelMembers); ok { + r0 = rf(userID, afterChannel, afterUser, limit, lastUpdateAt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.ChannelMembers) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, string, int, int) error); ok { + r1 = rf(userID, afterChannel, afterUser, limit, lastUpdateAt) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetMembersForUserWithPagination provides a mock function with given fields: userID, page, perPage func (_m *ChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) { ret := _m.Called(userID, page, perPage) @@ -1144,6 +1190,29 @@ func (_m *ChannelStore) GetMembersForUserWithPagination(userID string, page int, return r0, r1 } +// GetMembersInfoByChannelIds provides a mock function with given fields: channelIDs +func (_m *ChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) { + ret := _m.Called(channelIDs) + + var r0 map[string][]*model.User + if rf, ok := ret.Get(0).(func([]string) map[string][]*model.User); ok { + r0 = rf(channelIDs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]*model.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]string) error); ok { + r1 = rf(channelIDs) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetMoreChannels provides a mock function with given fields: teamID, userID, offset, limit func (_m *ChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) { ret := _m.Called(teamID, userID, offset, limit) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 08f53ee1d8..7848ad579a 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -1013,10 +1013,10 @@ func (s *TimerLayerChannelStore) GetChannelUnread(channelID string, userID strin return result, err } -func (s *TimerLayerChannelStore) GetChannels(teamID string, userID string, includeDeleted bool, lastDeleteAt int) (model.ChannelList, error) { +func (s *TimerLayerChannelStore) GetChannels(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, error) { start := timemodule.Now() - result, err := s.ChannelStore.GetChannels(teamID, userID, includeDeleted, lastDeleteAt) + result, err := s.ChannelStore.GetChannels(teamID, userID, opts) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil { @@ -1093,6 +1093,22 @@ func (s *TimerLayerChannelStore) GetChannelsByUser(userID string, includeDeleted return result, err } +func (s *TimerLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) { + start := timemodule.Now() + + result, err := s.ChannelStore.GetChannelsWithCursor(teamId, userId, opts, afterChannelID) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsWithCursor", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includeDeleted bool) ([]*model.ChannelWithTeamData, error) { start := timemodule.Now() @@ -1333,6 +1349,22 @@ func (s *TimerLayerChannelStore) GetMembersForUser(teamID string, userID string) return result, err } +func (s *TimerLayerChannelStore) GetMembersForUserWithCursor(userID string, afterChannel string, afterUser string, limit int, lastUpdateAt int) (model.ChannelMembers, error) { + start := timemodule.Now() + + result, err := s.ChannelStore.GetMembersForUserWithCursor(userID, afterChannel, afterUser, limit, lastUpdateAt) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersForUserWithCursor", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) GetMembersForUserWithPagination(userID string, page int, perPage int) (model.ChannelMembersWithTeamData, error) { start := timemodule.Now() @@ -1349,6 +1381,22 @@ func (s *TimerLayerChannelStore) GetMembersForUserWithPagination(userID string, return result, err } +func (s *TimerLayerChannelStore) GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error) { + start := timemodule.Now() + + result, err := s.ChannelStore.GetMembersInfoByChannelIds(channelIDs) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersInfoByChannelIds", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) GetMoreChannels(teamID string, userID string, offset int, limit int) (model.ChannelList, error) { start := timemodule.Now() @@ -2254,6 +2302,22 @@ func (s *TimerLayerChannelMemberHistoryStore) DeleteOrphanedRows(limit int) (int return result, err } +func (s *TimerLayerChannelMemberHistoryStore) GetChannelsLeftSince(userID string, since int64) ([]string, error) { + start := timemodule.Now() + + result, err := s.ChannelMemberHistoryStore.GetChannelsLeftSince(userID, since) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelMemberHistoryStore.GetChannelsLeftSince", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelMemberHistoryStore) GetUsersInChannelDuring(startTime int64, endTime int64, channelID string) ([]*model.ChannelMemberHistoryResult, error) { start := timemodule.Now()