[MM-44084] Feature: Top threads insights (#20195)

* Add route endpoints, model, store functions, and tests for top threads

* Run make store-layers

* Make the following changes

 - Fix top user threads query
 - Fix passing parameters in api4/insights.go to handler in app
 - Add top user threads test

* Add post-message, user_id, participants information to insights results

* model.TopThread.UserID -> model.TopThread.UserId, for compatibility with MySQL

* Rename name -> channel_name

* Add user information to response

* Link post in response, filter out deleted root posts from top threads

* Handle thread delete cases, add app tests for threads insights

* lint: fix typo

* lint: rename asserts

* lint: require.nil -> require.NoError

* Add integration tests for thread insights

* Add embeds and images to top posts

* Add license checks for top threads endpoints

* Query users in batch to populate post-creator

* Make the following changes

 - Add license to test server in api4/
 - Add tests for threads insights
    - top team threads shouldn't include threads from other teams, DMs
    - Test duration constraint
    - Pagination testing for top threads in model/insights_test.go

* Add i18n-extract

* i18n fixes

* Add username, nickname to user_information

* Hide message, user_id, post_id, reply_count in depth=1 of top threads response

* Fix tests using response.reply_count to use response.post.reply_count

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Shivashis Padhi
2022-06-20 19:57:17 +05:30
коммит произвёл GitHub
родитель de50943d61
Коммит 2cd83d2f8d
17 изменённых файлов: 1460 добавлений и 2 удалений

Просмотреть файл

@@ -777,6 +777,8 @@ type AppIface interface {
GetTopChannelsForUserSince(userID, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *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(teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError)
GetTopThreadsForUserSince(teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError)
GetUploadSession(uploadId string) (*model.UploadSession, *model.AppError)
GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError)
GetUser(userID string) (*model.User, *model.AppError)

Просмотреть файл

@@ -9860,6 +9860,50 @@ func (a *OpenTracingAppLayer) GetTopReactionsForUserSince(userID string, teamID
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopThreadsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopThreadsForTeamSince")
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.GetTopThreadsForTeamSince(teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopThreadsForUserSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopThreadsForUserSince")
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.GetTopThreadsForUserSince(teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTotalUsersStats")

Просмотреть файл

@@ -1717,3 +1717,47 @@ func (a *App) GetPostsByIds(postIDs []string) ([]*model.Post, *model.AppError) {
return posts, nil
}
func (a *App) GetTopThreadsForTeamSince(teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topThreads, err := a.Srv().Store.Thread().GetTopThreadsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, err.Error(), http.StatusInternalServerError)
}
topThreadsWithEmbedAndImage, err := includeEmbedsAndImages(a, topThreads, userID)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return topThreadsWithEmbedAndImage, nil
}
func (a *App) GetTopThreadsForUserSince(teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topThreads, err := a.Srv().Store.Thread().GetTopThreadsForUserSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, err.Error(), http.StatusInternalServerError)
}
topThreadsWithEmbedAndImage, err := includeEmbedsAndImages(a, topThreads, userID)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForUserSince", "app.post.get_top_threads_for_user_since.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return topThreadsWithEmbedAndImage, nil
}
func includeEmbedsAndImages(a *App, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) {
for _, topThread := range topThreadList.Items {
topThread.Post = a.PreparePostForClientWithEmbedsAndImages(topThread.Post, false, false)
sanitizedPost, err := a.SanitizePostMetadataForUser(topThread.Post, userID)
if err != nil {
return nil, err
}
topThread.Post = sanitizedPost
}
return topThreadList, nil
}

Просмотреть файл

@@ -2817,3 +2817,192 @@ func TestShouldNotRefollowOnOthersReply(t *testing.T) {
require.Nil(t, err)
require.True(t, m.Following)
}
func TestGetTopThreadsForTeamSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Server.configStore.SetReadOnlyFF(false)
defer th.Server.configStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
// create a public channel, a private channel
channelPublic := th.CreateChannel(th.BasicTeam)
channelPrivate := th.CreatePrivateChannel(th.BasicTeam)
th.AddUserToChannel(th.BasicUser, channelPublic)
th.AddUserToChannel(th.BasicUser, channelPrivate)
th.AddUserToChannel(th.BasicUser2, channelPublic)
// create two threads: one in public channel, one in private with only basicUser1
rootPostPublicChannel, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPublic.Id,
Message: "root post",
}, channelPublic, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser2.Id,
ChannelId: channelPublic.Id,
RootId: rootPostPublicChannel.Id,
Message: fmt.Sprintf("@%s", th.BasicUser2.Username),
}, channelPublic, false, true)
require.Nil(t, appErr)
rootPostPrivateChannel, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
Message: "root post",
}, channelPrivate, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: fmt.Sprintf("@%s", th.BasicUser2.Username),
}, channelPrivate, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: fmt.Sprintf("@%s", th.BasicUser2.Username),
}, channelPrivate, false, true)
require.Nil(t, appErr)
// get top threads for team, as user 1 and user 2
// user 1 should see both threads, while user 2 should see only thread in public channel.
topTeamThreadsByUser1, appErr := th.App.GetTopThreadsForTeamSince(th.BasicTeam.Id, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topTeamThreadsByUser1.Items, 2)
require.Equal(t, topTeamThreadsByUser1.Items[0].Post.Id, rootPostPrivateChannel.Id)
require.Equal(t, topTeamThreadsByUser1.Items[1].Post.Id, rootPostPublicChannel.Id)
topTeamThreadsByUser2, appErr := th.App.GetTopThreadsForTeamSince(th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topTeamThreadsByUser2.Items, 1)
require.Equal(t, topTeamThreadsByUser2.Items[0].Post.Id, rootPostPublicChannel.Id)
// add user2 to private channel and it can see 2 top threads.
th.AddUserToChannel(th.BasicUser2, channelPrivate)
topTeamThreadsByUser2IncludingPrivate, appErr := th.App.GetTopThreadsForTeamSince(th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topTeamThreadsByUser2IncludingPrivate.Items, 2)
}
func TestGetTopThreadsForUserSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Server.configStore.SetReadOnlyFF(false)
defer th.Server.configStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
// create a public channel, a private channel
channelPublic := th.CreateChannel(th.BasicTeam)
channelPrivate := th.CreatePrivateChannel(th.BasicTeam)
th.AddUserToChannel(th.BasicUser, channelPublic)
th.AddUserToChannel(th.BasicUser, channelPrivate)
th.AddUserToChannel(th.BasicUser2, channelPublic)
th.AddUserToChannel(th.BasicUser2, channelPrivate)
// create two threads: one in public channel, one in private
// post in public channel has both users interacting, post in private only has user1 interacting
rootPostPublicChannel, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPublic.Id,
Message: "root post pub",
}, channelPublic, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser2.Id,
ChannelId: channelPublic.Id,
RootId: rootPostPublicChannel.Id,
Message: "reply post 1",
}, channelPublic, false, true)
require.Nil(t, appErr)
rootPostPrivateChannel, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
Message: "root post priv",
}, channelPrivate, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: "reply post 1",
}, channelPrivate, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: "reply post 2",
}, channelPrivate, false, true)
require.Nil(t, appErr)
// get top threads for user, as user 1 and user 2
// user 1 should see both threads, while user 2 should see only thread in public channel
// (even if user2 is in the private channel it hasn't interacted with the thread there.)
topUser1Threads, appErr := th.App.GetTopThreadsForUserSince(th.BasicTeam.Id, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser1Threads.Items, 2)
require.Equal(t, topUser1Threads.Items[0].Post.Id, rootPostPrivateChannel.Id)
require.Equal(t, topUser1Threads.Items[0].ReplyCount, int64(2))
require.Equal(t, topUser1Threads.Items[1].Post.Id, rootPostPublicChannel.Id)
require.Contains(t, topUser1Threads.Items[1].Participants, th.BasicUser2.Id)
require.Equal(t, topUser1Threads.Items[1].ReplyCount, int64(1))
topUser2Threads, appErr := th.App.GetTopThreadsForUserSince(th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser2Threads.Items, 1)
require.Equal(t, topUser2Threads.Items[0].Post.Id, rootPostPublicChannel.Id)
require.Equal(t, topUser2Threads.Items[0].ReplyCount, int64(1))
// deleting the root post results in the thread not making it to top threads list
_, appErr = th.App.DeletePost(rootPostPublicChannel.Id, th.BasicUser.Id)
require.Nil(t, appErr)
topUser1ThreadsAfterPost1Delete, appErr := th.App.GetTopThreadsForUserSince(th.BasicTeam.Id, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser1ThreadsAfterPost1Delete.Items, 1)
// reply with user2 in thread2. deleting that reply, shouldn't give any top thread for user2 if the user2 unsubscribes to the thread after deleting the comment
replyPostUser2InPrivate, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser2.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: "reply post 3",
}, channelPrivate, false, true)
require.Nil(t, appErr)
topUser2ThreadsAfterPrivateReply, appErr := th.App.GetTopThreadsForUserSince(th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser2ThreadsAfterPrivateReply.Items, 1)
// deleting reply, and unfollowing thread
_, appErr = th.App.DeletePost(replyPostUser2InPrivate.Id, th.BasicUser2.Id)
require.Nil(t, appErr)
// unfollow thread
_, err := th.App.Srv().Store.Thread().MaintainMembership(th.BasicUser2.Id, rootPostPrivateChannel.Id, store.ThreadMembershipOpts{
Following: false,
UpdateFollowing: true,
})
require.NoError(t, err)
topUser2ThreadsAfterPrivateReplyDelete, appErr := th.App.GetTopThreadsForUserSince(th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser2ThreadsAfterPrivateReplyDelete.Items, 0)
}