Merge pull request #20690 from plant99/MM-45118_my_top_dms
[MM-45118] Add insights endpoint for top dms
Этот коммит содержится в:
@@ -21,8 +21,11 @@ func (api *API) InitInsights() {
|
|||||||
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForUserSince)))).Methods("GET")
|
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForUserSince)))).Methods("GET")
|
||||||
|
|
||||||
// Threads
|
// Threads
|
||||||
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(requireLicense(getTopThreadsForTeamSince))).Methods("GET")
|
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForTeamSince)))).Methods("GET")
|
||||||
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(requireLicense(getTopThreadsForUserSince))).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
|
// New teammembers
|
||||||
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getNewTeamMembersSince)))).Methods("GET")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
js, err := json.Marshal(topChannels)
|
js, jsonErr := json.Marshal(topChannels)
|
||||||
if err != nil {
|
if jsonErr != nil {
|
||||||
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,21 +248,14 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// license check
|
// restrict users with no access to team
|
||||||
lic := c.App.Srv().License()
|
user, err := c.App.GetUser(c.AppContext.Session().UserId)
|
||||||
if lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise {
|
if err != nil {
|
||||||
c.Err = model.NewAppError("", "api.insights.license_error", nil, "", http.StatusNotImplemented)
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// restrict guests and users with no access to team
|
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
|
||||||
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() {
|
|
||||||
c.SetPermissionError(model.PermissionViewTeam)
|
c.SetPermissionError(model.PermissionViewTeam)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -276,8 +272,8 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
js, err := json.Marshal(topThreads)
|
js, jsonError := json.Marshal(topThreads)
|
||||||
if err != nil {
|
if jsonError != nil {
|
||||||
c.Err = model.NewAppError("getTopThreadsForTeamSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
c.Err = model.NewAppError("getTopThreadsForTeamSince", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
return
|
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) {
|
func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
c.Params.TeamId = r.URL.Query().Get("team_id")
|
c.Params.TeamId = r.URL.Query().Get("team_id")
|
||||||
|
|
||||||
// restrict guests and users with no access to team
|
// restrict users with no access to team
|
||||||
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
|
user, err := c.App.GetUser(c.AppContext.Session().UserId)
|
||||||
if appErr != nil {
|
if err != nil {
|
||||||
c.Err = appErr
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TeamId is an optional parameter
|
// TeamId is an optional parameter
|
||||||
@@ -307,14 +303,7 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// license check
|
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
|
||||||
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() {
|
|
||||||
c.SetPermissionError(model.PermissionViewTeam)
|
c.SetPermissionError(model.PermissionViewTeam)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -332,9 +321,39 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -817,6 +817,126 @@ func TestGetTopThreadsForUserSince(t *testing.T) {
|
|||||||
require.Len(t, topUser2ThreadsAfterPrivateReplyDelete.Items, 0)
|
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) {
|
func TestNewTeamMembersSince(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|||||||
@@ -793,6 +793,7 @@ type AppIface interface {
|
|||||||
GetTokenById(token string) (*model.Token, *model.AppError)
|
GetTokenById(token string) (*model.Token, *model.AppError)
|
||||||
GetTopChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *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)
|
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)
|
GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError)
|
||||||
GetTopReactionsForUserSince(userID string, teamID 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)
|
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
|
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) {
|
func (a *OpenTracingAppLayer) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForTeamSince")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForTeamSince")
|
||||||
|
|||||||
11
app/post.go
11
app/post.go
@@ -1939,6 +1939,17 @@ func (a *App) GetTopThreadsForUserSince(c request.CTX, teamID, userID string, op
|
|||||||
return topThreadsWithEmbedAndImage, nil
|
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 {
|
func (a *App) SetPostReminder(postID, userID string, targetTime int64) *model.AppError {
|
||||||
// Store the reminder in the DB
|
// Store the reminder in the DB
|
||||||
reminder := &model.PostReminder{
|
reminder := &model.PostReminder{
|
||||||
|
|||||||
100
app/post_test.go
100
app/post_test.go
@@ -3062,3 +3062,103 @@ func TestGetTopThreadsForUserSince(t *testing.T) {
|
|||||||
require.Nil(t, appErr)
|
require.Nil(t, appErr)
|
||||||
require.Len(t, topUser2ThreadsAfterPrivateReplyDelete.Items, 0)
|
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",
|
"id": "api.insights.feature_disabled",
|
||||||
"translation": "Insights is behind a feature flag which is not enabled."
|
"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",
|
"id": "api.invalid_channel",
|
||||||
"translation": "Channel listed in the request doesn't belong to the user"
|
"translation": "Channel listed in the request doesn't belong to the user"
|
||||||
@@ -5859,6 +5855,10 @@
|
|||||||
"id": "app.post.get_root_posts.app_error",
|
"id": "app.post.get_root_posts.app_error",
|
||||||
"translation": "Unable to get the posts for the channel."
|
"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",
|
"id": "app.post.get_top_threads_for_team_since.app_error",
|
||||||
"translation": "Unable to get top threads for team."
|
"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
|
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
|
// Timezone Section
|
||||||
|
|
||||||
// GetSupportedTimezone returns a page of supported timezones on the system.
|
// GetSupportedTimezone returns a page of supported timezones on the system.
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ type InsightUserInformation struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TopDMInsightUserInformation struct {
|
||||||
|
InsightUserInformation
|
||||||
|
Position string `json:"position"`
|
||||||
|
}
|
||||||
type NewTeamMembersList struct {
|
type NewTeamMembersList struct {
|
||||||
InsightsListData
|
InsightsListData
|
||||||
Items []*NewTeamMember `json:"items"`
|
Items []*NewTeamMember `json:"items"`
|
||||||
@@ -113,6 +117,25 @@ type DurationPostCount struct {
|
|||||||
PostCount int `db:"postcount"`
|
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 {
|
func TimeRangeToNumberDays(timeRange string) int {
|
||||||
var n int
|
var n int
|
||||||
switch timeRange {
|
switch timeRange {
|
||||||
@@ -263,6 +286,20 @@ func GetTopThreadListWithPagination(threads []*TopThread, limit int) *TopThreadL
|
|||||||
return &TopThreadList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: threads}
|
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 {
|
func GetNewTeamMembersListWithPagination(teamMembers []*NewTeamMember, limit int) *NewTeamMembersList {
|
||||||
var hasNext bool
|
var hasNext bool
|
||||||
if (limit != 0) && (len(teamMembers) == limit+1) {
|
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
|
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) {
|
func (s *OpenTracingLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
||||||
origCtx := s.Root.Store.Context()
|
origCtx := s.Root.Store.Context()
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.HasAutoResponsePostByUserSince")
|
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) {
|
func (s *RetryLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
|
|||||||
@@ -2986,6 +2986,148 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts
|
|||||||
return nil
|
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 {
|
func (s *SqlPostStore) SetPostReminder(reminder *model.PostReminder) error {
|
||||||
transaction, err := s.GetMasterX().Beginx()
|
transaction, err := s.GetMasterX().Beginx()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -393,6 +393,9 @@ type PostStore interface {
|
|||||||
GetPostReminderMetadata(postID string) (*PostReminderMetadata, error)
|
GetPostReminderMetadata(postID string) (*PostReminderMetadata, error)
|
||||||
// GetNthRecentPostTime returns the CreateAt time of the nth most recent post.
|
// GetNthRecentPostTime returns the CreateAt time of the nth most recent post.
|
||||||
GetNthRecentPostTime(n int64) (int64, error)
|
GetNthRecentPostTime(n int64) (int64, error)
|
||||||
|
|
||||||
|
// Insights - top DMs
|
||||||
|
GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
|
|||||||
@@ -748,6 +748,29 @@ func (_m *PostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error)
|
|||||||
return r0, r1
|
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
|
// HasAutoResponsePostByUserSince provides a mock function with given fields: options, userId
|
||||||
func (_m *PostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
func (_m *PostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
||||||
ret := _m.Called(options, userId)
|
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("GetPostReminders", func(t *testing.T) { testGetPostReminders(t, ss, s) })
|
||||||
t.Run("GetPostReminderMetadata", func(t *testing.T) { testGetPostReminderMetadata(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("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) {
|
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.Error(t, err)
|
||||||
assert.IsType(t, &store.ErrNotFound{}, 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
|
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) {
|
func (s *TimerLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user