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
@@ -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")
|
||||
|
||||
Ссылка в новой задаче
Block a user