diff --git a/api4/insights.go b/api4/insights.go index 5e483ec9f5..3c90294ec7 100644 --- a/api4/insights.go +++ b/api4/insights.go @@ -27,6 +27,10 @@ func (api *API) InitInsights() { // user DMs api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopDMsForUserSince)))).Methods("GET") + // Inactive channels + api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopInactiveChannelsForTeamSince)))).Methods("GET") + api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopInactiveChannelsForUserSince)))).Methods("GET") + // New teammembers api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getNewTeamMembersSince)))).Methods("GET") } @@ -360,6 +364,100 @@ func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(js) } +// Top Channels + +func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + + team, err := c.App.GetTeam(c.Params.TeamId) + if err != nil { + c.Err = err + return + } + + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) { + c.SetPermissionError(model.PermissionViewTeam) + return + } + + user, err := c.App.GetUser(c.AppContext.Session().UserId) + if err != nil { + c.Err = err + return + } + + loc := user.GetTimezoneLocation() + startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc) + + topChannels, err := c.App.GetTopInactiveChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{ + StartUnixMilli: startTime.UnixMilli(), + Page: c.Params.Page, + PerPage: c.Params.PerPage, + }) + if err != nil { + c.Err = err + return + } + + if err := json.NewEncoder(w).Encode(topChannels); err != nil { + c.Err = model.NewAppError("getTopInactiveChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + return + } +} + +// top inactive channels + +func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) { + c.Params.TeamId = r.URL.Query().Get("team_id") + + // TeamId is an optional parameter + if c.Params.TeamId != "" { + if !model.IsValidId(c.Params.TeamId) { + c.SetInvalidURLParam("team_id") + return + } + + team, teamErr := c.App.GetTeam(c.Params.TeamId) + if teamErr != nil { + c.Err = teamErr + return + } + + if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) { + c.SetPermissionError(model.PermissionViewTeam) + return + } + } + + user, err := c.App.GetUser(c.AppContext.Session().UserId) + if err != nil { + c.Err = err + return + } + + loc := user.GetTimezoneLocation() + startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc) + + topChannels, err := c.App.GetTopInactiveChannelsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{ + StartUnixMilli: startTime.UnixMilli(), + Page: c.Params.Page, + PerPage: c.Params.PerPage, + }) + + if err != nil { + c.Err = err + return + } + + if err := json.NewEncoder(w).Encode(topChannels); err != nil { + c.Err = model.NewAppError("getTopInactiveChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError) + return + } +} + // postCountByDurationViewModel expects a list of channels that are pre-authorized for the given user to view. func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelList, startTime *time.Time, timeRange string, userID *string, location *time.Location) (model.ChannelPostCountByDuration, *model.AppError) { if len(topChannelList.Items) == 0 { diff --git a/api4/insights_test.go b/api4/insights_test.go index 5ba6f61972..97e3ddf6d6 100644 --- a/api4/insights_test.go +++ b/api4/insights_test.go @@ -817,6 +817,85 @@ func TestGetTopThreadsForUserSince(t *testing.T) { require.Len(t, topUser2ThreadsAfterPrivateReplyDelete.Items, 0) } +func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // delete offtopic channel - which interferes with 'least' active channel results + offTopicChannel, appErr := th.App.GetChannelByName(th.Context, "off-topic", th.BasicTeam.Id, false) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, offTopicChannel) + require.Nil(t, appErr) + + th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) + + client := th.Client + userId := th.BasicUser.Id + + channel4 := th.CreatePublicChannel() + channel5 := th.CreatePrivateChannel() + channel6 := th.CreatePrivateChannel() + th.App.AddUserToChannel(th.Context, th.BasicUser, channel4, false) + th.App.AddUserToChannel(th.Context, th.BasicUser, channel5, false) + th.App.AddUserToChannel(th.Context, th.BasicUser, channel6, false) + + channelIDs := [6]string{th.BasicChannel.Id, th.BasicChannel2.Id, th.BasicPrivateChannel.Id, channel4.Id, channel5.Id, channel6.Id} + + i := len(channelIDs) + for _, channelID := range channelIDs { + for j := i; j > 0; j-- { + _, _, err := client.CreatePost(&model.Post{UserId: userId, ChannelId: channelID, Message: "zz" + model.NewId() + "a"}) + require.NoError(t, err) + } + i-- + } + + teamId := th.BasicChannel.TeamId + + expectedTopChannels := []struct { + ID string + MessageCount int64 + }{{ + ID: channel6.Id, MessageCount: 1}, + {ID: channel5.Id, MessageCount: 2}, + {ID: channel4.Id, MessageCount: 3}, + {ID: th.BasicPrivateChannel.Id, MessageCount: 4}, + {ID: th.BasicChannel2.Id, MessageCount: 5}, + {ID: th.BasicChannel.Id, MessageCount: 7}, + } + + t.Run("get-top-inactive-channels-for-team-since", func(t *testing.T) { + topInactiveChannels, _, err := client.GetTopInactiveChannelsForTeamSince(teamId, model.TimeRangeToday, 0, 5) + require.NoError(t, err) + + for i, channel := range topInactiveChannels.Items { + assert.Equal(t, expectedTopChannels[i].ID, channel.ID) + } + + topInactiveChannels, _, err = client.GetTopInactiveChannelsForTeamSince(teamId, model.TimeRangeToday, 1, 5) + require.NoError(t, err) + assert.Equal(t, th.BasicChannel.Id, topInactiveChannels.Items[0].ID) + }) + + t.Run("get-top-channels-for-user-since exclude channels user is not member of", func(t *testing.T) { + excludedChannel := th.CreatePrivateChannel() + + for i := 0; i < 10; i++ { + _, _, err := client.CreatePost(&model.Post{UserId: userId, ChannelId: excludedChannel.Id, Message: "zz" + model.NewId() + "a"}) + require.NoError(t, err) + } + + th.RemoveUserFromChannel(th.BasicUser, excludedChannel) + + topInactiveChannels, _, err := client.GetTopInactiveChannelsForUserSince(teamId, model.TimeRangeToday, 0, 5) + require.NoError(t, err) + + for i, channel := range topInactiveChannels.Items { + assert.Equal(t, expectedTopChannels[i].ID, channel.ID) + } + }) +} + func TestGetTopDMsForUserSince(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/app_iface.go b/app/app_iface.go index 32b6c04022..561ec1b7c3 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -794,6 +794,8 @@ type AppIface interface { GetTopChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) GetTopChannelsForUserSince(c request.CTX, userID, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) + GetTopInactiveChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) + GetTopInactiveChannelsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) GetTopThreadsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) diff --git a/app/channel.go b/app/channel.go index 15cadcf709..835ac9d47f 100644 --- a/app/channel.go +++ b/app/channel.go @@ -3448,3 +3448,26 @@ func (a *App) PostCountsByDuration(c request.CTX, channelIDs []string, sinceUnix } return postCountByDay, nil } + +func (a *App) GetTopInactiveChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) { + if !a.Config().FeatureFlags.InsightsEnabled { + return nil, model.NewAppError("GetTopChannelsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented) + } + topChannels, err := a.Srv().Store.Channel().GetTopInactiveChannelsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage) + if err != nil { + return nil, model.NewAppError("GetTopInactiveChannelsForTeamSince", "app.channel.get_top_invalid_for_team_since.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return topChannels, nil +} + +func (a *App) GetTopInactiveChannelsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) { + if !a.Config().FeatureFlags.InsightsEnabled { + return nil, model.NewAppError("GetTopChannelsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented) + } + + topChannels, err := a.Srv().Store.Channel().GetTopInactiveChannelsForUserSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage) + if err != nil { + return nil, model.NewAppError("GetTopInactiveChannelsForUserSince", "app.channel.get_top_invalid_for_user_since.app_error", nil, err.Error(), http.StatusInternalServerError) + } + return topChannels, nil +} diff --git a/app/channel_test.go b/app/channel_test.go index 72dc0db3e5..c4525ae706 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -2678,3 +2678,176 @@ func TestPostCountsByDuration(t *testing.T) { } }) } + +// Top inactive channels + +func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + channel2 := th.CreateChannel(th.Context, th.BasicTeam) + + // delete offtopic channel - which interferes with 'least' active channel results + offTopicChannel, appErr := th.App.GetChannelByName(th.Context, "off-topic", th.BasicTeam.Id, false) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, offTopicChannel) + require.Nil(t, appErr) + + // add a bot post to ensure it's counted + _, err := th.Server.Store.Post().Save(&model.Post{ + Message: "hello from a bot", + ChannelId: channel2.Id, + UserId: th.BasicUser.Id, + Props: model.StringInterface{ + "from_bot": true, + }, + }) + require.NoError(t, err) + + channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam) + + // add a webhook post to ensure it's counted + _, err = th.Server.Store.Post().Save(&model.Post{ + Message: "hello from a webhook", + ChannelId: channel3.Id, + UserId: th.BasicUser.Id, + Props: model.StringInterface{ + "from_webhook": true, + }, + }) + require.NoError(t, err) + + channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam) + channel5 := th.CreateChannel(th.Context, th.BasicTeam) + channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + th.AddUserToChannel(th.BasicUser, channel3) + th.AddUserToChannel(th.BasicUser, channel4) + th.AddUserToChannel(th.BasicUser, channel5) + th.AddUserToChannel(th.BasicUser, channel6) + + channels := [6]*model.Channel{th.BasicChannel, channel2, channel3, channel4, channel5, channel6} + + i := len(channels) + for _, channel := range channels { + for j := i; j > 0; j-- { + th.CreatePost(channel) + } + i-- + } + + expectedTopChannels := []struct { + ID string + MessageCount int64 + }{ + {ID: channel6.Id, MessageCount: 1}, + {ID: channel5.Id, MessageCount: 2}, + {ID: channel4.Id, MessageCount: 3}, + {ID: channel3.Id, MessageCount: 5}, + {ID: channel2.Id, MessageCount: 6}, + {ID: th.BasicChannel.Id, MessageCount: 7}, + } + + timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) + + t.Run("get-top-channels-for-team-since", func(t *testing.T) { + topChannels, err := th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 6}) + require.Nil(t, err) + + for i, channel := range topChannels.Items { + assert.Equal(t, expectedTopChannels[i].ID, channel.ID) + assert.Equal(t, expectedTopChannels[i].MessageCount, channel.MessageCount) + } + + topChannels, err = th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 5}) + require.Nil(t, err) + assert.Equal(t, th.BasicChannel.Id, topChannels.Items[0].ID) + assert.Equal(t, int64(7), topChannels.Items[0].MessageCount) + }) +} + +func TestGetTopInactiveChannelsForUserSince(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // delete offtopic channel - which interferes with 'least' active channel results + offTopicChannel, appErr := th.App.GetChannelByName(th.Context, "off-topic", th.BasicTeam.Id, false) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, offTopicChannel) + require.Nil(t, appErr) + + channel2 := th.CreateChannel(th.Context, th.BasicTeam) + + // add a bot post to ensure it's counted + _, err := th.Server.Store.Post().Save(&model.Post{ + Message: "hello from a bot", + ChannelId: channel2.Id, + UserId: th.BasicUser.Id, + Props: model.StringInterface{ + "from_bot": true, + }, + }) + require.NoError(t, err) + + channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam) + + // add a webhook post to ensure it's counted + _, err = th.Server.Store.Post().Save(&model.Post{ + Message: "hello from a webhook", + ChannelId: channel3.Id, + UserId: th.BasicUser.Id, + Props: model.StringInterface{ + "from_webhook": true, + }, + }) + require.NoError(t, err) + + channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam) + channel5 := th.CreateChannel(th.Context, th.BasicTeam) + channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + th.AddUserToChannel(th.BasicUser, channel3) + th.AddUserToChannel(th.BasicUser, channel4) + th.AddUserToChannel(th.BasicUser, channel5) + th.AddUserToChannel(th.BasicUser, channel6) + + channels := [6]*model.Channel{th.BasicChannel, channel2, channel3, channel4, channel5, channel6} + + i := len(channels) + for _, channel := range channels { + for j := i; j > 0; j-- { + th.CreatePost(channel) + } + i-- + } + + expectedTopChannels := []struct { + ID string + MessageCount int64 + }{ + {ID: channel6.Id, MessageCount: 1}, + {ID: channel5.Id, MessageCount: 2}, + {ID: channel4.Id, MessageCount: 3}, + {ID: channel3.Id, MessageCount: 5}, + {ID: channel2.Id, MessageCount: 6}, + {ID: th.BasicChannel.Id, MessageCount: 7}, + } + + timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) + + t.Run("get-top-channels-for-user-since", func(t *testing.T) { + topChannels, err := th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5}) + require.Nil(t, err) + require.Equal(t, len(topChannels.Items), 5) + for i, channel := range topChannels.Items { + assert.Equal(t, expectedTopChannels[i].ID, channel.ID) + assert.Equal(t, expectedTopChannels[i].MessageCount, channel.MessageCount) + } + + topChannels, err = th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 5}) + require.Nil(t, err) + require.Equal(t, len(topChannels.Items), 1) + assert.Equal(t, th.BasicChannel.Id, topChannels.Items[0].ID) + assert.Equal(t, int64(7), topChannels.Items[0].MessageCount) + }) +} diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index d26928490a..20c1ba1b8a 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -9977,6 +9977,50 @@ func (a *OpenTracingAppLayer) GetTopDMsForUserSince(userID string, opts *model.I return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetTopInactiveChannelsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopInactiveChannelsForTeamSince") + + 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.GetTopInactiveChannelsForTeamSince(c, teamID, userID, opts) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + +func (a *OpenTracingAppLayer) GetTopInactiveChannelsForUserSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopInactiveChannelsForUserSince") + + 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.GetTopInactiveChannelsForUserSince(c, teamID, userID, opts) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForTeamSince") diff --git a/i18n/en.json b/i18n/en.json index ed5d7f2726..3873589405 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4539,6 +4539,14 @@ "id": "app.channel.get_top_for_user_since.app_error", "translation": " " }, + { + "id": "app.channel.get_top_invalid_for_team_since.app_error", + "translation": " " + }, + { + "id": "app.channel.get_top_invalid_for_user_since.app_error", + "translation": " " + }, { "id": "app.channel.get_unread.app_error", "translation": "Unable to get the channel unread messages." diff --git a/model/client4.go b/model/client4.go index dc1015c17b..6f026bc0ac 100644 --- a/model/client4.go +++ b/model/client4.go @@ -3642,6 +3642,41 @@ func (c *Client4) GetTopChannelsForUserSince(teamId string, timeRange string, pa return topChannels, BuildResponse(r), nil } +// GetTopInactiveChannelsForTeamSince will return an ordered list of the top channels in a given team. +func (c *Client4) GetTopInactiveChannelsForTeamSince(teamId string, timeRange string, page int, perPage int) (*TopInactiveChannelList, *Response, error) { + query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage) + r, err := c.DoAPIGet(c.teamRoute(teamId)+"/top/inactive_channels"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var topInactiveChannels *TopInactiveChannelList + if jsonErr := json.NewDecoder(r.Body).Decode(&topInactiveChannels); jsonErr != nil { + return nil, nil, NewAppError("GetTopInactiveChannelsForTeamSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return topInactiveChannels, BuildResponse(r), nil +} + +// GetTopInactiveChannelsForUserSince will return an ordered list of your top channels in a given team. +func (c *Client4) GetTopInactiveChannelsForUserSince(teamId string, timeRange string, page int, perPage int) (*TopInactiveChannelList, *Response, error) { + query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage) + + if teamId != "" { + query += fmt.Sprintf("&team_id=%v", teamId) + } + + r, err := c.DoAPIGet(c.usersRoute()+"/me/top/inactive_channels"+query, "") + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var topInactiveChannels *TopInactiveChannelList + if jsonErr := json.NewDecoder(r.Body).Decode(&topInactiveChannels); jsonErr != nil { + return nil, nil, NewAppError("GetTopInactiveChannelsForUserSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return topInactiveChannels, BuildResponse(r), nil +} + // Post Section // CreatePost creates a post based on the provided post struct. diff --git a/model/insights.go b/model/insights.go index e6a5602829..438ca9402c 100644 --- a/model/insights.go +++ b/model/insights.go @@ -63,6 +63,22 @@ type TopChannel struct { MessageCount int64 `json:"message_count"` } +// Top Channels +type TopInactiveChannelList struct { + InsightsListData + Items []*TopInactiveChannel `json:"items"` +} + +type TopInactiveChannel struct { + ID string `json:"id"` + Type ChannelType `json:"type"` + DisplayName string `json:"display_name"` + Name string `json:"name"` + LastActivityAt int64 `json:"last_activity_at"` + Participants StringArray `json:"participants"` + MessageCount int64 `json:"-"` +} + // Top Threads type TopThreadList struct { InsightsListData @@ -286,6 +302,20 @@ func GetTopThreadListWithPagination(threads []*TopThread, limit int) *TopThreadL return &TopThreadList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: threads} } +// GetTopInactiveChannelListWithPagination adds a rank to each item in the given list of TopInactiveChannel and checks if there is +// another page that can be fetched based on the given limit and offset. The given list of TopInactiveChannel is assumed to be +// sorted by Score. Returns a TopInactiveChannelList. +func GetTopInactiveChannelListWithPagination(channels []*TopInactiveChannel, limit int) *TopInactiveChannelList { + // Add pagination support + var hasNext bool + if (limit != 0) && (len(channels) == limit+1) { + hasNext = true + channels = channels[:len(channels)-1] + } + + return &TopInactiveChannelList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: channels} +} + // GetTopDMListWithPagination adds a rank to each item in the given list of TopDM and checks if there is // another page that can be fetched based on the given limit and offset. The given list of TopDM is assumed to be // sorted by MessageCount(score). Returns a TopDMList. diff --git a/model/insights_test.go b/model/insights_test.go index 5505cc1bc8..f95fbeb006 100644 --- a/model/insights_test.go +++ b/model/insights_test.go @@ -121,6 +121,43 @@ func TestGetTopThreadListWithPagination(t *testing.T) { } } +func TestGetTopInactiveChannelListWithPagination(t *testing.T) { + channels := []*TopInactiveChannel{ + {ID: NewId(), MessageCount: 2}, + {ID: NewId(), MessageCount: 5}, + {ID: NewId(), MessageCount: 7}, + {ID: NewId(), MessageCount: 80}, + {ID: NewId(), MessageCount: 85}, + {ID: NewId(), MessageCount: 92}} + + hasNextTC := []struct { + Description string + Limit int + Offset int + Expected *TopInactiveChannelList + }{ + { + Description: "has one page", + Limit: len(channels), + Offset: 0, + Expected: &TopInactiveChannelList{InsightsListData: InsightsListData{HasNext: false}, Items: channels}, + }, + { + Description: "has more than one page", + Limit: len(channels) - 1, + Offset: 0, + Expected: &TopInactiveChannelList{InsightsListData: InsightsListData{HasNext: true}, Items: channels}, + }, + } + + for _, test := range hasNextTC { + t.Run(test.Description, func(t *testing.T) { + actual := GetTopInactiveChannelListWithPagination(channels, test.Limit) + assert.Equal(t, test.Expected.HasNext, actual.HasNext) + }) + } +} + func TestGetTopDMsListWithPagination(t *testing.T) { dms := []*TopDM{ {SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 100}, diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 8d19d63f40..79b10e0182 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -1803,6 +1803,42 @@ func (s *OpenTracingLayerChannelStore) GetTopChannelsForUserSince(userID string, return result, err } +func (s *OpenTracingLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopInactiveChannelsForTeamSince") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + +func (s *OpenTracingLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopInactiveChannelsForUserSince") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) GroupSyncedChannelCount() (int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GroupSyncedChannelCount") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 3c6c6d97e4..9dfa6a19cd 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -2039,6 +2039,48 @@ func (s *RetryLayerChannelStore) GetTopChannelsForUserSince(userID string, teamI } +func (s *RetryLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit) + 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) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit) + 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) GroupSyncedChannelCount() (int64, error) { tries := 0 diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 04bba25a42..dd2023fdb5 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -4319,6 +4319,198 @@ func (s SqlChannelStore) GetTopChannelsForUserSince(userID string, teamID string return model.GetTopChannelListWithPagination(channels, limit), nil } +// GetTopInactiveChannelsForTeamSince returns the filtered post counts of the following Channels sets: +// a) those that are private channels in the given user's membership graph on the given team, and +// b) those that are public channels in the given team. +func (s SqlChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + channels := make([]*model.TopInactiveChannel, 0) + var args []any + + query := ` + SELECT + ID, + Type, + DisplayName, + Name, + MessageCount, + LastActivityAt + FROM + ((SELECT + Posts.ChannelId AS ID, + 'O' AS Type, + PublicChannels.DisplayName AS DisplayName, + PublicChannels.Name AS Name, + count(Posts.Id) AS MessageCount, + max(Posts.CreateAt) AS LastActivityAt + FROM + Posts + LEFT JOIN PublicChannels on Posts.ChannelId = PublicChannels.Id + WHERE + Posts.DeleteAt = 0 + AND Posts.CreateAt > ? + AND (Posts.Type = '' OR Posts.Type = 'system_join_channel') + AND PublicChannels.TeamId = ? + AND PublicChannels.DeleteAt = 0 + GROUP BY + Posts.ChannelId, + PublicChannels.DisplayName, + PublicChannels.Name, + PublicChannels.TeamId) + UNION ALL + (SELECT + Posts.ChannelId AS ID, + Channels.Type AS Type, + Channels.DisplayName AS DisplayName, + Channels.Name AS Name, + count(Posts.Id) AS MessageCount, + max(Posts.CreateAt) AS LastActivityAt + FROM + Posts + LEFT JOIN Channels on Posts.ChannelId = Channels.Id + LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId + WHERE + Posts.DeleteAt = 0 + AND Posts.CreateAt > ? + AND (Posts.Type = '' OR Posts.Type = 'system_join_channel') + AND Channels.TeamId = ? + AND Channels.Type = 'P' + AND Channels.DeleteAt = 0 + AND ChannelMembers.UserId = ? + GROUP BY + Posts.ChannelId, + Channels.Type, + Channels.DisplayName, + Channels.Name)) AS A + ORDER BY + MessageCount ASC, + Name ASC + LIMIT ? + OFFSET ?` + args = append(args, since, teamID, since, teamID, userID, limit+1, offset) + + if err := s.GetReplicaX().Select(&channels, query, args...); err != nil { + return nil, errors.Wrap(err, "failed to get top Channels") + } + + channels, err := postProcessTopInactiveChannels(s, channels) + + if err != nil { + return nil, err + } + + return model.GetTopInactiveChannelListWithPagination(channels, limit), nil +} + +// GetTopInactiveChannelsForUserSince returns the filtered post counts of channels with with posts created by the user +// after the given timestamp within the given team (or across the workspace if no team is given). Excludes DM and GM channels. +func (s SqlChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + channels := make([]*model.TopInactiveChannel, 0) + var args []any + var query string + + query = ` + SELECT + Posts.ChannelId AS ID, + Channels.Type AS Type, + Channels.DisplayName AS DisplayName, + Channels.Name AS Name, + count(Posts.Id) AS MessageCount, + max(Posts.CreateAt) AS LastActivityAt + FROM + Posts + LEFT JOIN Channels on Posts.ChannelId = Channels.Id + LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId + WHERE + Posts.DeleteAt = 0 + AND Posts.CreateAt > ? + AND (Posts.Type = '' OR Posts.Type = 'system_join_channel') + AND Channels.DeleteAt = 0 + AND (Channels.Type = 'O' OR Channels.Type = 'P') + AND ChannelMembers.UserId = ? ` + + args = []any{since, userID} + + if teamID != "" { + query += ` + AND Channels.TeamID = ?` + args = append(args, teamID) + } + + query += ` + Group By + Posts.ChannelId, + Channels.Type, + Channels.DisplayName, + Channels.Name + ORDER BY + MessageCount ASC, + Name ASC + LIMIT ? + OFFSET ?` + args = append(args, limit+1, offset) + + if err := s.GetReplicaX().Select(&channels, query, args...); err != nil { + return nil, errors.Wrap(err, "failed to get top Inactive Channels") + } + + channels, err := postProcessTopInactiveChannels(s, channels) + if err != nil { + return nil, err + } + + return model.GetTopInactiveChannelListWithPagination(channels, limit), nil +} + +func postProcessTopInactiveChannels(s SqlChannelStore, channels []*model.TopInactiveChannel) ([]*model.TopInactiveChannel, error) { + // query channel members for Ids + var conditionalAggrSelector string + if s.DriverName() == model.DatabaseDriverMysql { + conditionalAggrSelector = "GROUP_CONCAT(UserId SEPARATOR ',') as UserIds" + } else if s.DriverName() == model.DatabaseDriverPostgres { + conditionalAggrSelector = "string_agg(UserId, ',') as UserIds" + } + + var channelIds []string + for _, channel := range channels { + channelIds = append(channelIds, channel.ID) + } + q := s.getQueryBuilder().Select("ChannelId", conditionalAggrSelector).From("ChannelMembers"). + Where(sq.Eq{ + "ChannelId": channelIds, + }).GroupBy("ChannelId") + + channelsUserIdsMap := make(map[string]string, len(channels)) + type ChannelUserIdsResult struct { + ChannelId string + UserIds string + } + + channelsUserIdsResultList := make([]ChannelUserIdsResult, len(channels)) + sql, args, err := q.ToSql() + if err != nil { + return nil, errors.Wrap(err, "failed to stringify squirrel query") + } + if err := s.GetReplicaX().Select(&channelsUserIdsResultList, sql, args...); err != nil { + return nil, errors.Wrap(err, "failed to get top Inactive Channels users") + } + + for _, channelUserIds := range channelsUserIdsResultList { + channelsUserIdsMap[channelUserIds.ChannelId] = channelUserIds.UserIds + } + for index, channel := range channels { + userIds := channelsUserIdsMap[channel.ID] + userIdsSlice := strings.Split(userIds, ",") + + channels[index].Participants = userIdsSlice + + // handle channels with 0 participants + if len(userIdsSlice) == 1 && userIdsSlice[0] == "" { + channels[index].Participants = make([]string, 0) + } + } + return channels, nil +} + func (s SqlChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, atLocation *time.Location) ([]*model.DurationPostCount, error) { var unixSelect string var propsQuery string diff --git a/store/store.go b/store/store.go index 94480b8ffe..99882d34b9 100644 --- a/store/store.go +++ b/store/store.go @@ -298,6 +298,10 @@ type ChannelStore interface { GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) + + // Insights - inactive channels + GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) + GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) } type ChannelMemberHistoryStore interface { diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 9f20aa7a17..913ac7e08a 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -149,6 +149,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) { t.Run("SetShared", func(t *testing.T) { testSetShared(t, ss) }) t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, ss) }) t.Run("PostCountsByDuration", func(t *testing.T) { testChannelPostCountsByDuration(t, ss) }) + t.Run("GetTopInactiveChannels", func(t *testing.T) { testGetTopInactiveChannels(t, ss) }) } func testChannelStoreSave(t *testing.T, ss store.Store) { @@ -7961,3 +7962,162 @@ func testChannelPostCountsByDuration(t *testing.T, ss store.Store) { require.Equal(t, channel.Id, dpc[0].ChannelID) require.Equal(t, 1, dpc[0].PostCount) } + +func testGetTopInactiveChannels(t *testing.T, ss store.Store) { + team, err := ss.Team().Save(&model.Team{ + Name: model.NewId(), + DisplayName: "DisplayName", + Email: MakeEmail(), + Type: model.TeamOpen, + }) + require.NoError(t, err) + defer func() { ss.Team().PermanentDelete(team.Id) }() + + channelPublic0 := &model.Channel{ + TeamId: team.Id, + DisplayName: "test_share_flag asdf", + Name: "test_share_flag_public0", + Type: model.ChannelTypeOpen, + } + + channelSaved0, err := ss.Channel().Save(channelPublic0, 999) + require.NoError(t, err) + defer func() { ss.Channel().PermanentDelete(channelSaved0.Id) }() + + channelPublic1 := &model.Channel{ + TeamId: team.Id, + DisplayName: "test_share_flag", + Name: "test_share_flag", + Type: model.ChannelTypeOpen, + } + + channelSaved1, err := ss.Channel().Save(channelPublic1, 999) + require.NoError(t, err) + defer func() { ss.Channel().PermanentDelete(channelSaved1.Id) }() + + // create private channel + c3 := model.Channel{} + c3.TeamId = team.Id + c3.DisplayName = "Channel3" + model.NewId() + c3.Name = NewTestId() + c3.Type = model.ChannelTypePrivate + channelPrivate, nErr := ss.Channel().Save(&c3, -1) + require.NoError(t, nErr) + + // create dm channel + u1 := model.User{} + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + _, err = ss.User().Save(&u1) + require.NoError(t, err) + + u2 := model.User{} + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + _, err = ss.User().Save(&u2) + require.NoError(t, err) + + uBot := model.User{Id: model.NewId()} + _, nErr = ss.Channel().CreateDirectChannel(&u1, &u2) + require.NoError(t, nErr) + + // add u1, u2 to channels + + cm1 := &model.ChannelMember{ChannelId: channelPrivate.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()} + _, err = ss.Channel().SaveMember(cm1) + require.NoError(t, err) + cm1Public := &model.ChannelMember{ChannelId: channelPublic1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()} + _, err = ss.Channel().SaveMember(cm1Public) + require.NoError(t, err) + cm2 := &model.ChannelMember{ChannelId: channelPublic0.Id, UserId: u2.Id, NotifyProps: model.GetDefaultChannelNotifyProps()} + _, err = ss.Channel().SaveMember(cm2) + require.NoError(t, err) + cmBot := &model.ChannelMember{ChannelId: channelPublic0.Id, UserId: uBot.Id, NotifyProps: model.GetDefaultChannelNotifyProps()} + _, err = ss.Channel().SaveMember(cmBot) + require.NoError(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: u1.Id, + ChannelId: channelPrivate.Id, + Message: "test", + }) + require.NoError(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: u1.Id, + ChannelId: channelPrivate.Id, + Message: "test1", + }) + require.NoError(t, err) + + // create posts in channel public 0 + postToCheckLastUpdateAt, err := ss.Post().Save(&model.Post{ + UserId: u2.Id, + ChannelId: channelSaved0.Id, + Message: "test", + }) + require.NoError(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: channelPublic1.Id, + Message: "test", + Props: model.StringInterface{ + "from_bot": true, + }, + }) + require.NoError(t, err) + + // create posts in channel public 1 + for i := 0; i < 3; i++ { + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: channelPublic1.Id, + Message: "test", + }) + require.NoError(t, err) + } + + // for u1 + t.Run("top inactive channels for team - u1 ", func(t *testing.T) { + topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u1.Id, 0, 0, 10) + require.NoError(t, err) + require.Len(t, topInactiveChannels.Items, 3) + require.Equal(t, topInactiveChannels.Items[0].ID, channelSaved0.Id) + require.Equal(t, topInactiveChannels.Items[0].LastActivityAt, postToCheckLastUpdateAt.CreateAt) + require.Equal(t, topInactiveChannels.Items[1].ID, channelPrivate.Id) + require.Equal(t, topInactiveChannels.Items[2].ID, channelPublic1.Id) + // test bot posts are counted + require.Equal(t, topInactiveChannels.Items[2].MessageCount, int64(4)) + + // participants + require.Equal(t, topInactiveChannels.Items[1].Participants[0], u1.Id) + require.Equal(t, topInactiveChannels.Items[2].Participants[0], u1.Id) + }) + + t.Run("top inactive channels for user - u1 ", func(t *testing.T) { + topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u1.Id, 0, 0, 10) + require.NoError(t, err) + require.Len(t, topInactiveChannels.Items, 2) + require.Equal(t, topInactiveChannels.Items[0].ID, channelPrivate.Id) + require.Equal(t, topInactiveChannels.Items[1].ID, channelPublic1.Id) + }) + + // for u2 + t.Run("top inactive channels for team - u2 ", func(t *testing.T) { + topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u2.Id, 0, 0, 10) + require.NoError(t, err) + require.Len(t, topInactiveChannels.Items, 2) + require.Equal(t, topInactiveChannels.Items[0].ID, channelSaved0.Id) + require.Equal(t, topInactiveChannels.Items[0].LastActivityAt, postToCheckLastUpdateAt.CreateAt) + require.Equal(t, topInactiveChannels.Items[1].ID, channelPublic1.Id) + }) + + t.Run("top inactive channels for user - u2 ", func(t *testing.T) { + topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u2.Id, 0, 0, 10) + require.NoError(t, err) + require.Len(t, topInactiveChannels.Items, 1) + require.Equal(t, topInactiveChannels.Items[0].ID, channelPublic0.Id) + }) + +} diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 9bc9594d49..0b73494d83 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -1579,6 +1579,52 @@ func (_m *ChannelStore) GetTopChannelsForUserSince(userID string, teamID string, return r0, r1 } +// GetTopInactiveChannelsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit +func (_m *ChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + ret := _m.Called(teamID, userID, since, offset, limit) + + var r0 *model.TopInactiveChannelList + if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopInactiveChannelList); ok { + r0 = rf(teamID, userID, since, offset, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.TopInactiveChannelList) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok { + r1 = rf(teamID, userID, since, offset, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTopInactiveChannelsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit +func (_m *ChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + ret := _m.Called(teamID, userID, since, offset, limit) + + var r0 *model.TopInactiveChannelList + if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopInactiveChannelList); ok { + r0 = rf(teamID, userID, since, offset, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.TopInactiveChannelList) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok { + r1 = rf(teamID, userID, since, offset, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GroupSyncedChannelCount provides a mock function with given fields: func (_m *ChannelStore) GroupSyncedChannelCount() (int64, error) { ret := _m.Called() diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 5fb7b55deb..02993648c9 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -1653,6 +1653,38 @@ func (s *TimerLayerChannelStore) GetTopChannelsForUserSince(userID string, teamI return result, err } +func (s *TimerLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + start := time.Now() + + result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopInactiveChannelsForTeamSince", success, elapsed) + } + return result, err +} + +func (s *TimerLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) { + start := time.Now() + + result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopInactiveChannelsForUserSince", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) GroupSyncedChannelCount() (int64, error) { start := time.Now()