MM-45899: Insights: least active channels (#20796)
* Add api endpoints, app layers for top inactive channels with dummy store calls * Add store functions for top inactive channels * Add model, store, app tests. * Add client function and api tests * Add participants information to TopInactiveChannel * Translation fix * Style fix while writing response * Return channelmember IDs instead of profiles, query in batch avoiding inside the loop * Make the following changes - move DeleteAt to subqueries, to avoid select, group by - Remove TeamId from response - Count bots and webhook posts * SQL query lint fix, store test fix to include bot messages * make app-layers * Fix empty participant lists being sent as [""] * Track channel joins, to distinguish 0 activity channels vs new channels Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8fd1762c3b
Коммит
6adbcc5d05
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user