[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 удалений

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

@@ -9646,6 +9646,42 @@ func (s *OpenTracingLayerThreadStore) GetThreadsForUser(userId string, teamID st
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalThreads")

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

@@ -11024,6 +11024,48 @@ func (s *RetryLayerThreadStore) GetThreadsForUser(userId string, teamID string,
}
func (s *RetryLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTopThreadsForTeamSince(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 *RetryLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTopThreadsForUserSince(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 *RetryLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0

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

@@ -935,3 +935,209 @@ func (s *SqlThreadStore) GetThreadUnreadReplyCount(threadMembership *model.Threa
return unreadReplies, nil
}
// Top threads in all public channels and private channels userID is a member of. Returns a list of threads ranked by interactions.
func (s *SqlThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
var args []interface{}
query := `select
threads_list.PostId,
threads_list.ReplyCount,
threads_list.ChannelId,
threads_list.DisplayName,
threads_list.Name,
threads_list.Participants,
p.UserId
from((
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
WHERE
t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND c.TeamId = ?
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)
UNION
ALL (
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
LEFT JOIN Channels c ON t.ChannelId = c.Id
WHERE
t.threaddeleteat IS NULL
AND cm.UserId = ?
AND c.Type = 'P'
AND c.TeamId = ?
AND t.LastReplyAt > ?
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)) as threads_list
LEFT JOIN Posts as p on p.Id = threads_list.PostId
ORDER BY ReplyCount DESC
limit ? offset ?`
args = append(args, since, teamID, userID, teamID, since, limit+1, offset)
topThreads := make([]*model.TopThread, 0)
err := s.GetReplicaX().Select(&topThreads, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
}
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
if err != nil {
return nil, err
}
return model.GetTopThreadListWithPagination(topThreads, limit), nil
}
func (s *SqlThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
var args []interface{}
// gets all threads within the team which user follows.
query := `select
threads_list.PostId,
threads_list.ReplyCount,
threads_list.ChannelId,
threads_list.DisplayName,
threads_list.Name,
threads_list.Participants,
p.UserId
from((
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
WHERE
t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND c.TeamId = ?
AND tm.UserId = ?
AND tm.Following = TRUE
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)
UNION
ALL (
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
LEFT JOIN Channels c ON t.ChannelId = c.Id
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
WHERE
cm.UserId = ?
AND c.Type = 'P'
AND c.TeamId = ?
AND t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND tm.UserId = ?
AND tm.Following = TRUE
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)) as threads_list
LEFT JOIN Posts as p on p.Id = threads_list.PostId
ORDER BY ReplyCount DESC
limit ? offset ?`
args = append(args, since, teamID, userID, userID, teamID, since, userID, limit+1, offset)
topThreads := make([]*model.TopThread, 0)
err := s.GetReplicaX().Select(&topThreads, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
}
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
if err != nil {
return nil, err
}
return model.GetTopThreadListWithPagination(topThreads, limit), nil
}
func userContains(userIDs []string, searchedUserID string) bool {
for _, userID := range userIDs {
if userID == searchedUserID {
return true
}
}
return false
}
func postProcessTopThreads(topThreads []*model.TopThread, s *SqlThreadStore, teamID string) ([]*model.TopThread, error) {
// create list of userIDs
var userIDs []string
for _, topThread := range topThreads {
userID := topThread.UserId
if !userContains(userIDs, userID) {
userIDs = append(userIDs, userID)
}
}
usersMap := map[string]*model.User{}
users, err := s.User().GetProfileByIds(context.Background(), userIDs, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get users for top threads in team=%s", teamID)
}
for _, user := range users {
usersMap[user.Id] = user
}
// resolve user, root post for each top thread
for _, topThread := range topThreads {
postCreator := usersMap[topThread.UserId]
topThread.UserInformation = &model.InsightUserInformation{
Id: postCreator.Id,
LastPictureUpdate: postCreator.LastPictureUpdate,
FirstName: postCreator.FirstName,
LastName: postCreator.LastName,
Username: postCreator.Username,
NickName: postCreator.Nickname,
}
post, err := s.Post().GetSingle(topThread.PostId, false)
if err != nil {
return nil, errors.Wrapf(err, "failed to get extended post for post id=%s", topThread.PostId)
}
topThread.Post = post
}
return topThreads, nil
}

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

@@ -290,7 +290,7 @@ type ChannelStore interface {
// GetTeamForChannel returns the team for a given channelID.
GetTeamForChannel(channelID string) (*model.Team, error)
// Insights
// Insights - channels
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)
@@ -331,6 +331,10 @@ type ThreadStore interface {
PermanentDeleteBatchThreadMembershipsForRetentionPolicies(now, globalPolicyEndTime, limit int64, cursor model.RetentionPolicyCursor) (int64, model.RetentionPolicyCursor, error)
DeleteOrphanedRows(limit int) (deleted int64, err error)
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
// Insights - threads
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
}
type PostStore interface {

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

@@ -255,6 +255,52 @@ func (_m *ThreadStore) GetThreadsForUser(userId string, teamID string, opts mode
return r0, r1
}
// GetTopThreadsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopThreadList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopThreadList)
}
}
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
}
// GetTopThreadsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopThreadList
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopThreadList)
}
}
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
}
// GetTotalThreads provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)

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

@@ -27,6 +27,7 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("GetTeamsUnreadForUser", func(t *testing.T) { testGetTeamsUnreadForUser(t, ss) })
t.Run("GetVarious", func(t *testing.T) { testVarious(t, ss) })
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
}
func testThreadStorePopulation(t *testing.T, ss store.Store) {
@@ -1226,3 +1227,345 @@ func testMarkAllAsReadByChannels(t *testing.T, ss store.Store) {
assertThreadReplyCount(t, userBID, 0)
})
}
func testGetTopThreads(t *testing.T, ss store.Store) {
// create two users
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err := ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u2)
require.NoError(t, err)
u3 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u3)
require.NoError(t, err)
t.Run("test get top team threads", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 2000)
// get top threads
topThreadsInTeam, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeam.Items, 2)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeam.Items[0].PostId, post1.Id)
require.Equal(t, topThreadsInTeam.Items[0].UserId, post1.UserId)
require.Equal(t, topThreadsInTeam.Items[0].UserInformation.Id, post1.UserId)
require.Equal(t, topThreadsInTeam.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsInTeam.Items[0].Post.Message, post1.Message)
// require second element to be post2 with 2 replyCount=2
require.Equal(t, topThreadsInTeam.Items[1].PostId, post2.Id)
require.Equal(t, topThreadsInTeam.Items[1].Post.ReplyCount, int64(1))
require.Equal(t, topThreadsInTeam.Items[1].UserId, post2.UserId)
require.Equal(t, topThreadsInTeam.Items[1].UserInformation.Id, post2.UserId)
require.Equal(t, topThreadsInTeam.Items[1].Post.Message, post2.Message)
// require topThreads[i].Post is not null
require.Equal(t, topThreadsInTeam.Items[0].Post.Id, post1.Id)
require.Equal(t, topThreadsInTeam.Items[1].Post.Id, post2.Id)
})
t.Run("test get top user threads", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
})
require.NoError(t, err)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u3.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post3.Id, post3.UserId, 2000)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
// create threadmemberships entries.
_, err = ss.Thread().MaintainMembership(post1.UserId, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(post2.UserId, post2.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(post2.UserId, post3.Id, opts)
require.NoError(t, err)
// get top threads by user
topThreadsByUser1, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post1.UserId, 12, 0, limit)
require.NoError(t, err)
topThreadsByUser2, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post2.UserId, 12, 0, limit)
require.NoError(t, err)
// require length of top threads by users to be 1,2 respectively
require.Len(t, topThreadsByUser1.Items, 1)
require.Len(t, topThreadsByUser2.Items, 2)
// require first element of topThreadsByUser1 to be post1 with 2 replyCount=2
require.Equal(t, topThreadsByUser1.Items[0].PostId, post1.Id)
require.Equal(t, topThreadsByUser1.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsByUser1.Items[0].Post.Message, post1.Message)
require.Equal(t, topThreadsByUser1.Items[0].UserId, post1.UserId)
require.Equal(t, topThreadsByUser1.Items[0].UserInformation.Id, post1.UserId)
// require elements of topThreadsByUser2 to be post2 and post3 respectively
require.Equal(t, topThreadsByUser2.Items[0].PostId, post2.Id)
require.Equal(t, topThreadsByUser2.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsByUser2.Items[0].Post.Message, post2.Message)
require.Equal(t, topThreadsByUser2.Items[0].UserId, post2.UserId)
require.Equal(t, topThreadsByUser2.Items[0].UserInformation.Id, post2.UserId)
require.Equal(t, topThreadsByUser2.Items[1].PostId, post3.Id)
require.Equal(t, topThreadsByUser2.Items[1].Post.ReplyCount, int64(1))
require.Equal(t, topThreadsByUser2.Items[1].Post.Message, post3.Message)
require.Equal(t, topThreadsByUser2.Items[1].UserId, post3.UserId)
require.Equal(t, topThreadsByUser2.Items[1].UserInformation.Id, post3.UserId)
// require topThreads[i].Post is not null
require.Equal(t, topThreadsByUser1.Items[0].Post.Id, post1.Id)
require.Equal(t, topThreadsByUser2.Items[1].Post.Id, post3.Id)
})
t.Run("test get top threads only from given teamid", func(t *testing.T) {
const limit = 10
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, post2.UserId, 2000)
// assert that getting top threads from teamid 1 doesn't have post1.Id
topThreadsTeam2, err := ss.Thread().GetTopThreadsForTeamSince(team2.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topThreadsTeam2.Items, 1)
require.Equal(t, topThreadsTeam2.Items[0].Post.Id, post2.Id)
})
t.Run("test get top threads only from non-direct channels", func(t *testing.T) {
const limit = 10
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, u1.Id, 2000)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
// create threadmemberships entries.
_, err = ss.Thread().MaintainMembership(u1.Id, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u1.Id, post2.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u2.Id, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u2.Id, post2.Id, opts)
require.NoError(t, err)
// assert that getting top threads from teamid 1 doesn't have DMs
topThreadsTeam1, err := ss.Thread().GetTopThreadsForTeamSince(team1.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topThreadsTeam1.Items, 1)
require.Equal(t, topThreadsTeam1.Items[0].Post.Id, post2.Id)
// assert that getting top threads from user 1 doesn't contain dm threads.
topUserThreads, err := ss.Thread().GetTopThreadsForUserSince(team1.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topUserThreads.Items, 1)
require.Equal(t, topUserThreads.Items[0].Post.Id, post2.Id)
})
t.Run("test get top threads doesn't exceed duration", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
// post 2 has replies after 10 ms unix time.
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
CreateAt: 1,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 10)
// get top threads
topThreadsInTeamNewer, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeamNewer.Items, 1)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeamNewer.Items[0].PostId, post1.Id)
// get top threads
topThreadsInTeamOlder, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 9, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeamOlder.Items, 2)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeamOlder.Items[1].PostId, post2.Id)
})
}

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

@@ -8679,6 +8679,38 @@ func (s *TimerLayerThreadStore) GetThreadsForUser(userId string, teamID string,
return result, err
}
func (s *TimerLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
start := time.Now()
result, err := s.ThreadStore.GetTopThreadsForTeamSince(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("ThreadStore.GetTopThreadsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
start := time.Now()
result, err := s.ThreadStore.GetTopThreadsForUserSince(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("ThreadStore.GetTopThreadsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()