Merge pull request #20690 from plant99/MM-45118_my_top_dms

[MM-45118] Add insights endpoint for top dms
Этот коммит содержится в:
Shivashis Padhi
2022-08-19 19:33:44 +05:30
коммит произвёл GitHub
родитель 6305b79475 e87a2bbe19
Коммит 7643a28263
17 изменённых файлов: 745 добавлений и 37 удалений

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

@@ -21,8 +21,11 @@ func (api *API) InitInsights() {
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForUserSince)))).Methods("GET")
// Threads
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(requireLicense(getTopThreadsForTeamSince))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(requireLicense(getTopThreadsForUserSince))).Methods("GET")
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForTeamSince)))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForUserSince)))).Methods("GET")
// user DMs
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopDMsForUserSince)))).Methods("GET")
// New teammembers
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getNewTeamMembersSince)))).Methods("GET")
@@ -223,9 +226,9 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque
return
}
js, err := json.Marshal(topChannels)
if err != nil {
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
js, jsonErr := json.Marshal(topChannels)
if jsonErr != nil {
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}
@@ -245,21 +248,14 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
return
}
// license check
lic := c.App.Srv().License()
if lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise {
c.Err = model.NewAppError("", "api.insights.license_error", nil, "", http.StatusNotImplemented)
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// restrict guests and users with no access to team
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) || user.IsGuest() {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
@@ -276,8 +272,8 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
return
}
js, err := json.Marshal(topThreads)
if err != nil {
js, jsonError := json.Marshal(topThreads)
if jsonError != nil {
c.Err = model.NewAppError("getTopThreadsForTeamSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
@@ -288,10 +284,10 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
c.Params.TeamId = r.URL.Query().Get("team_id")
// restrict guests and users with no access to team
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// TeamId is an optional parameter
@@ -307,14 +303,7 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
return
}
// license check
lic := c.App.Srv().License()
if lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise {
c.Err = model.NewAppError("", "api.insights.license_error", nil, "", http.StatusNotImplemented)
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) || user.IsGuest() {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
@@ -332,9 +321,39 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
return
}
js, err := json.Marshal(topThreads)
js, jsonErr := json.Marshal(topThreads)
if jsonErr != nil {
c.Err = model.NewAppError("getTopThreadsForUserSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
return
}
w.Write(js)
}
// Top DMs
func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = model.NewAppError("getTopThreadsForUserSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.Err = err
return
}
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
topDMs, err := c.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(topDMs)
if jsonErr != nil {
c.Err = model.NewAppError("getTopDMsForUserSince", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
return
}

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

@@ -817,6 +817,126 @@ func TestGetTopThreadsForUserSince(t *testing.T) {
require.Len(t, topUser2ThreadsAfterPrivateReplyDelete.Items, 0)
}
func TestGetTopDMsForUserSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(c *model.Config) {
*c.TeamSettings.EnableUserDeactivation = true
})
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableBotAccountCreation = true })
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
// basicuser1 - bu1, basicuser - bu
// create dm channels for bu-bu, bu1-bu1, bu-bu1, bot-bu
basicUser := th.BasicUser
basicUser1 := th.BasicUser2
th.LoginBasic2()
client := th.Client
channelBu1Bu1, _, err := client.CreateDirectChannel(basicUser1.Id, basicUser1.Id)
require.NoError(t, err)
th.LoginBasic()
client = th.Client
channelBuBu, _, err := client.CreateDirectChannel(basicUser.Id, basicUser.Id)
require.NoError(t, err)
channelBuBu1, _, err := client.CreateDirectChannel(basicUser.Id, basicUser1.Id)
require.NoError(t, err)
// bot creation with permission
th.AddPermissionToRole(model.PermissionCreateBot.Id, model.TeamUserRoleId)
th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.TeamUserRoleId+" "+model.SystemUserRoleId, false)
bot := &model.Bot{
Username: GenerateTestUsername(),
DisplayName: "a bot",
Description: "bot",
}
createdBot, resp, err := th.Client.CreateBot(bot)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
defer th.App.PermanentDeleteBot(createdBot.UserId)
channelBuBot, _, err := client.CreateDirectChannel(basicUser.Id, createdBot.UserId)
require.NoError(t, err)
// create 2 posts in channelBu, 1 in channelBu1, 3 in channelBu12
postsGenConfig := []map[string]interface{}{
{
"chId": channelBuBu.Id,
"postCount": 2,
},
{
"chId": channelBu1Bu1.Id,
"postCount": 1,
},
{
"chId": channelBuBu1.Id,
"postCount": 3,
},
{
"chId": channelBuBot.Id,
"postCount": 3,
},
}
for _, postGen := range postsGenConfig {
postCount := postGen["postCount"].(int)
for i := 0; i < postCount; i++ {
if postGen["chId"] == channelBu1Bu1.Id {
th.LoginBasic2()
client = th.Client
userId := basicUser1.Id
post := &model.Post{UserId: userId, ChannelId: postGen["chId"].(string), Message: "zz" + model.NewId() + "a"}
_, _, err = client.CreatePost(post)
require.NoError(t, err)
} else {
th.LoginBasic()
client = th.Client
userId := basicUser.Id
post := &model.Post{UserId: userId, ChannelId: postGen["chId"].(string), Message: "zz" + model.NewId() + "a"}
_, _, err = client.CreatePost(post)
require.NoError(t, err)
}
}
}
// get top dms for bu
t.Run("get top dms for basic user 1", func(t *testing.T) {
th.LoginBasic()
client = th.Client
topDMs, _, topDmsErr := client.GetTopDMsForUserSince("today", 0, 100)
require.NoError(t, topDmsErr)
require.Len(t, topDMs.Items, 1)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, basicUser1.Id)
})
// get top dms for bu1
t.Run("get top dms for basic user 2", func(t *testing.T) {
th.LoginBasic2()
client = th.Client
topDMs, _, topDmsErr := client.GetTopDMsForUserSince("today", 0, 100)
require.NoError(t, topDmsErr)
require.Len(t, topDMs.Items, 1)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
})
// deactivate basicuser1
_, err = th.Client.DeleteUser(basicUser1.Id)
require.NoError(t, err)
// deactivated users DMs should show in topDMs
t.Run("get top dms for basic user 1", func(t *testing.T) {
th.LoginBasic()
client = th.Client
topDMs, _, topDmsErr := client.GetTopDMsForUserSince("today", 0, 100)
require.NoError(t, topDmsErr)
require.Len(t, topDMs.Items, 1)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
})
}
func TestNewTeamMembersSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()

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

@@ -793,6 +793,7 @@ type AppIface interface {
GetTokenById(token string) (*model.Token, *model.AppError)
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)
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)

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

@@ -9955,6 +9955,28 @@ func (a *OpenTracingAppLayer) GetTopChannelsForUserSince(c request.CTX, userID s
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopDMsForUserSince")
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.GetTopDMsForUserSince(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")

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

@@ -1939,6 +1939,17 @@ func (a *App) GetTopThreadsForUserSince(c request.CTX, teamID, userID string, op
return topThreadsWithEmbedAndImage, nil
}
func (a *App) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopDMsForUserSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topDMs, err := a.Srv().Store.Post().GetTopDMsForUserSince(userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopDMsForUserSince", "app.post.get_top_dms_for_user_since.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return topDMs, nil
}
func (a *App) SetPostReminder(postID, userID string, targetTime int64) *model.AppError {
// Store the reminder in the DB
reminder := &model.PostReminder{

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

@@ -3062,3 +3062,103 @@ func TestGetTopThreadsForUserSince(t *testing.T) {
require.Nil(t, appErr)
require.Len(t, topUser2ThreadsAfterPrivateReplyDelete.Items, 0)
}
func TestGetTopDMsForUserSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
// users
user := th.CreateUser()
u1 := th.CreateUser()
u2 := th.CreateUser()
u3 := th.CreateUser()
u4 := th.CreateUser()
// user direct messages
chUser1, nErr := th.App.createDirectChannel(th.Context, u1.Id, user.Id)
fmt.Println(chUser1, nErr)
require.Nil(t, nErr)
chUser2, nErr := th.App.createDirectChannel(th.Context, u2.Id, user.Id)
require.Nil(t, nErr)
chUser3, nErr := th.App.createDirectChannel(th.Context, u3.Id, user.Id)
require.Nil(t, nErr)
// other user direct message
chUser3User4, nErr := th.App.createDirectChannel(th.Context, u3.Id, u4.Id)
require.Nil(t, nErr)
// sample post data
// for u1
_, err := th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser1.Id,
UserId: u1.Id,
}, "", false)
require.Nil(t, err)
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser1.Id,
UserId: user.Id,
}, "", false)
require.Nil(t, err)
// for u2: 1 post
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
}, "", false)
require.Nil(t, err)
// for user-u3: 3 posts
for i := 0; i < 3; i++ {
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3.Id,
UserId: user.Id,
}, "", false)
require.Nil(t, err)
}
// for u4-u3: 4 posts
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
}, "", false)
require.Nil(t, err)
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
}, "", false)
require.Nil(t, err)
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
}, "", false)
require.Nil(t, err)
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
}, "", false)
require.Nil(t, err)
t.Run("should return topDMs when userid is specified ", func(t *testing.T) {
topDMs, err := th.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{StartUnixMilli: 100, Page: 0, PerPage: 100})
require.Nil(t, err)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 3)
// check order, magnitude of items
// fmt.Println(topDMs.Items[0].MessageCount, topDMs.Items[1].MessageCount, topDMs.Items[2].MessageCount)
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
require.Equal(t, topDMs.Items[1].SecondParticipant.Id, u1.Id)
require.Equal(t, topDMs.Items[1].MessageCount, int64(2))
require.Equal(t, topDMs.Items[2].SecondParticipant.Id, u2.Id)
require.Equal(t, topDMs.Items[2].MessageCount, int64(1))
// this also ensures that u3-u4 conversation doesn't show up in others' top DMs.
})
t.Run("topDMs should only consider user's DM channels ", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, err := th.App.GetTopDMsForUserSince(u4.Id, &model.InsightsOpts{StartUnixMilli: 100, Page: 0, PerPage: 100})
require.Nil(t, err)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 1)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(4))
})
}

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

@@ -1913,10 +1913,6 @@
"id": "api.insights.feature_disabled",
"translation": "Insights is behind a feature flag which is not enabled."
},
{
"id": "api.insights.license_error",
"translation": "Your license doesn't support Insights feature."
},
{
"id": "api.invalid_channel",
"translation": "Channel listed in the request doesn't belong to the user"
@@ -5859,6 +5855,10 @@
"id": "app.post.get_root_posts.app_error",
"translation": "Unable to get the posts for the channel."
},
{
"id": "app.post.get_top_dms_for_user_since.app_error",
"translation": "Unable to get top DMs for user."
},
{
"id": "app.post.get_top_threads_for_team_since.app_error",
"translation": "Unable to get top threads for team."

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

@@ -6698,6 +6698,21 @@ func (c *Client4) GetTopReactionsForUserSince(teamId string, timeRange string, p
return topReactions, BuildResponse(r), nil
}
func (c *Client4) GetTopDMsForUserSince(timeRange string, page int, perPage int) (*TopDMList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(c.usersRoute()+"/me/top/dms"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topDMs *TopDMList
if jsonErr := json.NewDecoder(r.Body).Decode(&topDMs); jsonErr != nil {
return nil, nil, NewAppError("GetTopReactionsForUserSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return topDMs, BuildResponse(r), nil
}
// Timezone Section
// GetSupportedTimezone returns a page of supported timezones on the system.

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

@@ -90,6 +90,10 @@ type InsightUserInformation struct {
Username string `json:"username"`
}
type TopDMInsightUserInformation struct {
InsightUserInformation
Position string `json:"position"`
}
type NewTeamMembersList struct {
InsightsListData
Items []*NewTeamMember `json:"items"`
@@ -113,6 +117,25 @@ type DurationPostCount struct {
PostCount int `db:"postcount"`
}
// Top DMs
type TopDM struct {
MessageCount int64 `json:"post_count"`
OutgoingMessageCount int64 `json:"outgoing_message_count"`
Participants string `json:"-"`
ChannelId string `json:"-"`
SecondParticipant *TopDMInsightUserInformation `json:"second_participant"`
}
type OutgoingMessageQueryResult struct {
ChannelId string
MessageCount int
}
type TopDMList struct {
InsightsListData
Items []*TopDM `json:"items"`
}
func TimeRangeToNumberDays(timeRange string) int {
var n int
switch timeRange {
@@ -263,6 +286,20 @@ func GetTopThreadListWithPagination(threads []*TopThread, limit int) *TopThreadL
return &TopThreadList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: threads}
}
// 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.
func GetTopDMListWithPagination(dms []*TopDM, limit int) *TopDMList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(dms) == limit+1) {
hasNext = true
dms = dms[:len(dms)-1]
}
return &TopDMList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: dms}
}
func GetNewTeamMembersListWithPagination(teamMembers []*NewTeamMember, limit int) *NewTeamMembersList {
var hasNext bool
if (limit != 0) && (len(teamMembers) == limit+1) {

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

@@ -120,3 +120,41 @@ func TestGetTopThreadListWithPagination(t *testing.T) {
})
}
}
func TestGetTopDMsListWithPagination(t *testing.T) {
dms := []*TopDM{
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 100},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 80},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 90},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 76},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 43},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 2},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 1},
}
hasNextTT := []struct {
Description string
Limit int
Offset int
Expected *TopDMList
}{
{
Description: "has one page",
Limit: len(dms),
Offset: 0,
Expected: &TopDMList{InsightsListData: InsightsListData{HasNext: false}, Items: dms},
},
{
Description: "has more than one page",
Limit: len(dms) - 1,
Offset: 0,
Expected: &TopDMList{InsightsListData: InsightsListData{HasNext: true}, Items: dms},
},
}
for _, test := range hasNextTT {
t.Run(test.Description, func(t *testing.T) {
actual := GetTopDMListWithPagination(dms, test.Limit)
assert.Equal(t, test.Expected.HasNext, actual.HasNext)
})
}
}

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

@@ -6043,6 +6043,24 @@ func (s *OpenTracingLayerPostStore) GetSingle(id string, inclDeleted bool) (*mod
return result, err
}
func (s *OpenTracingLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetTopDMsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.HasAutoResponsePostByUserSince")

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

@@ -6848,6 +6848,27 @@ func (s *RetryLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Pos
}
func (s *RetryLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
tries := 0
for {
result, err := s.PostStore.GetTopDMsForUserSince(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 *RetryLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
tries := 0

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

@@ -2986,6 +2986,148 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts
return nil
}
func (s *SqlPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
channelSelector := s.getQueryBuilder().Select("Id", "TotalMsgCount").From("Channels").Join("ChannelMembers as cm on cm.ChannelId = Channels.Id").
Where(sq.And{
sq.Expr("Channels.Type = 'D'"),
sq.Eq{"cm.UserId": userID},
})
var aggregator string
if s.DriverName() == model.DatabaseDriverMysql {
aggregator = "group_concat(distinct cm.UserId) as Participants"
} else {
aggregator = "string_agg(distinct cm.UserId, ',') as Participants"
}
topDMsBuilder := s.getQueryBuilder().Select("vch.TotalMsgCount as MessageCount", aggregator, "vch.Id as ChannelId").FromSelect(channelSelector, "vch").
Join("ChannelMembers as cm on cm.ChannelId = vch.Id").
Join("Posts as p on p.ChannelId = vch.Id").
Where(sq.And{
sq.Gt{
"p.UpdateAt": since,
},
sq.Eq{
"p.DeleteAt": 0,
},
}).GroupBy("vch.id", "vch.TotalMsgCount")
topDMsBuilder = topDMsBuilder.OrderBy("MessageCount DESC").Limit(uint64(limit + 1)).Offset(uint64(offset))
topDMs := make([]*model.TopDM, 0)
sql, args, err := topDMsBuilder.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetTopDMsForUserSince_ToSql")
}
err = s.GetReplicaX().Select(&topDMs, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find top DMs for user-id: %s", userID)
}
// fill SecondParticipant column
topDMs, err = postProcessTopDMs(s, userID, topDMs, since)
if err != nil {
return nil, err
}
return model.GetTopDMListWithPagination(topDMs, limit), nil
}
func postProcessTopDMs(s *SqlPostStore, userID string, topDMs []*model.TopDM, since int64) ([]*model.TopDM, error) {
var topDMsFiltered = []*model.TopDM{}
var secondParticipantIds []string
var channelIds []string
// identify second participant in a list of participants
for _, topDM := range topDMs {
participants := strings.Split(topDM.Participants, ",")
var secondParticipantId string
if len(participants) == 1 {
// channel with self
secondParticipantId = "-1"
} else {
if participants[0] == userID {
secondParticipantId = participants[1]
} else {
secondParticipantId = participants[0]
}
}
secondParticipantIds = append(secondParticipantIds, secondParticipantId)
channelIds = append(channelIds, topDM.ChannelId)
}
// get user profiles
users, err := s.User().GetProfileByIds(context.Background(), secondParticipantIds, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get second participants' information")
}
// get outgoing message count for userId
outgoingMessagesQuery := s.getQueryBuilder().Select("ch.Id as ChannelId, count(p.Id) as MessageCount").From("Channels as ch").
Join("Posts as p on p.ChannelId=ch.Id").Where(
sq.And{
sq.Gt{
"p.UpdateAt": since,
},
sq.Eq{
"p.DeleteAt": 0,
},
sq.Eq{
"ch.Id": channelIds,
},
sq.Eq{
"p.UserId": userID,
},
}).GroupBy("ch.Id")
outgoingMessages := make([]*model.OutgoingMessageQueryResult, 0)
sql, args, err := outgoingMessagesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetTopDMsForUserSince_outgoingMessagesQuery_ToSql")
}
err = s.GetReplicaX().Select(&outgoingMessages, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find top DMs for user-id: %s", userID)
}
// create map of channelId -> MessageCount
outgoingMessagesMap := make(map[string]int)
for _, outgoingMessage := range outgoingMessages {
outgoingMessagesMap[outgoingMessage.ChannelId] = outgoingMessage.MessageCount
}
// create map of userId -> User
usersMap := make(map[string]*model.User)
for _, user := range users {
usersMap[user.Id] = user
}
for index, topDM := range topDMs {
if secondParticipantIds[index] == "-1" {
continue
}
user := usersMap[secondParticipantIds[index]]
if user.IsBot {
continue
}
topDM.SecondParticipant = &model.TopDMInsightUserInformation{
InsightUserInformation: model.InsightUserInformation{
Id: user.Id,
LastPictureUpdate: user.LastPictureUpdate,
FirstName: user.FirstName,
LastName: user.LastName,
Username: user.Username,
NickName: user.Nickname,
},
Position: user.Position,
}
topDM.OutgoingMessageCount = int64(outgoingMessagesMap[topDM.ChannelId])
topDMsFiltered = append(topDMsFiltered, topDM)
}
return topDMsFiltered, nil
}
func (s *SqlPostStore) SetPostReminder(reminder *model.PostReminder) error {
transaction, err := s.GetMasterX().Beginx()
if err != nil {

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

@@ -393,6 +393,9 @@ type PostStore interface {
GetPostReminderMetadata(postID string) (*PostReminderMetadata, error)
// GetNthRecentPostTime returns the CreateAt time of the nth most recent post.
GetNthRecentPostTime(n int64) (int64, error)
// Insights - top DMs
GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error)
}
type UserStore interface {

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

@@ -748,6 +748,29 @@ func (_m *PostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error)
return r0, r1
}
// GetTopDMsForUserSince provides a mock function with given fields: userID, since, offset, limit
func (_m *PostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
ret := _m.Called(userID, since, offset, limit)
var r0 *model.TopDMList
if rf, ok := ret.Get(0).(func(string, int64, int, int) *model.TopDMList); ok {
r0 = rf(userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopDMList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64, int, int) error); ok {
r1 = rf(userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasAutoResponsePostByUserSince provides a mock function with given fields: options, userId
func (_m *PostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
ret := _m.Called(options, userId)

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

@@ -62,6 +62,7 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("GetPostReminders", func(t *testing.T) { testGetPostReminders(t, ss, s) })
t.Run("GetPostReminderMetadata", func(t *testing.T) { testGetPostReminderMetadata(t, ss, s) })
t.Run("GetNthRecentPostTime", func(t *testing.T) { testGetNthRecentPostTime(t, ss) })
t.Run("GetTopDMsForUserSince", func(t *testing.T) { testGetTopDMsForUserSince(t, ss, s) })
}
func testPostStoreSave(t *testing.T, ss store.Store) {
@@ -4058,3 +4059,124 @@ func testGetNthRecentPostTime(t *testing.T, ss store.Store) {
assert.Error(t, err)
assert.IsType(t, &store.ErrNotFound{}, err)
}
func testGetTopDMsForUserSince(t *testing.T, ss store.Store, s SqlStore) {
// users
user := model.User{Email: MakeEmail(), Username: model.NewId()}
u1 := model.User{Email: MakeEmail(), Username: model.NewId()}
u2 := model.User{Email: MakeEmail(), Username: model.NewId()}
u3 := model.User{Email: MakeEmail(), Username: model.NewId()}
u4 := model.User{Email: MakeEmail(), Username: model.NewId()}
_, err := ss.User().Save(&user)
require.NoError(t, err)
_, err = ss.User().Save(&u1)
require.NoError(t, err)
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, err = ss.User().Save(&u3)
require.NoError(t, err)
_, err = ss.User().Save(&u4)
require.NoError(t, err)
// user direct messages
chUser1, nErr := ss.Channel().CreateDirectChannel(&u1, &user)
require.NoError(t, nErr)
chUser2, nErr := ss.Channel().CreateDirectChannel(&u2, &user)
require.NoError(t, nErr)
chUser3, nErr := ss.Channel().CreateDirectChannel(&u3, &user)
require.NoError(t, nErr)
// other user direct message
chUser3User4, nErr := ss.Channel().CreateDirectChannel(&u3, &u4)
require.NoError(t, nErr)
// sample post data
// for u1
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser1.Id,
UserId: user.Id,
})
require.NoError(t, err)
// for u2: 1 post
postToDelete, err := ss.Post().Save(&model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
// for user-u3: 3 posts
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3.Id,
UserId: user.Id,
})
require.NoError(t, err)
}
// for u4-u3: 4 posts
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
})
require.NoError(t, err)
t.Run("should return topDMs when userid is specified ", func(t *testing.T) {
topDMs, storeErr := ss.Post().GetTopDMsForUserSince(user.Id, 100, 0, 100)
require.NoError(t, storeErr)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 3)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
require.Equal(t, topDMs.Items[0].OutgoingMessageCount, int64(3))
require.Equal(t, topDMs.Items[1].SecondParticipant.Id, u1.Id)
require.Equal(t, topDMs.Items[1].MessageCount, int64(2))
require.Equal(t, topDMs.Items[1].OutgoingMessageCount, int64(1))
require.Equal(t, topDMs.Items[2].SecondParticipant.Id, u2.Id)
require.Equal(t, topDMs.Items[2].MessageCount, int64(1))
require.Equal(t, topDMs.Items[2].OutgoingMessageCount, int64(0))
// this also ensures that u3-u4 conversation doesn't show up in others' top DMs.
})
t.Run("topDMs should only consider user's DM channels ", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, storeErr := ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, storeErr)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 1)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(4))
})
t.Run("topDMs will not consider self dms", func(t *testing.T) {
chUser, nErr := ss.Channel().CreateDirectChannel(&user, &user)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser.Id,
UserId: user.Id,
})
// delete u2 post
err := ss.Post().Delete(postToDelete.Id, 200, user.Id)
require.NoError(t, err)
// u4 only takes part in one conversation
topDMs, err := ss.Post().GetTopDMsForUserSince(user.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 2)
})
}

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

@@ -5466,6 +5466,22 @@ func (s *TimerLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Pos
return result, err
}
func (s *TimerLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
start := time.Now()
result, err := s.PostStore.GetTopDMsForUserSince(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("PostStore.GetTopDMsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
start := time.Now()