Add top DMs route and handlers
Этот коммит содержится в:
@@ -23,6 +23,9 @@ func (api *API) InitInsights() {
|
|||||||
// Threads
|
// Threads
|
||||||
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForTeamSince)))).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")
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top Reactions
|
// Top Reactions
|
||||||
@@ -326,6 +329,36 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
|
|||||||
w.Write(js)
|
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 = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
|
||||||
|
|
||||||
|
topDMs, err := c.App.GetTopDMsForUserSince(c.AppContext.Session().UserId, &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
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(js)
|
||||||
|
}
|
||||||
|
|
||||||
// postCountByDurationViewModel expects a list of channels that are pre-authorized for the given user to view.
|
// postCountByDurationViewModel expects a list of channels that are pre-authorized for the given user to view.
|
||||||
func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelList, startTime *time.Time, timeRange string, userID *string, location *time.Location) (model.ChannelPostCountByDuration, *model.AppError) {
|
func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelList, startTime *time.Time, timeRange string, userID *string, location *time.Location) (model.ChannelPostCountByDuration, *model.AppError) {
|
||||||
if len(topChannelList.Items) == 0 {
|
if len(topChannelList.Items) == 0 {
|
||||||
|
|||||||
@@ -791,6 +791,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)
|
||||||
|
|||||||
@@ -9904,6 +9904,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
@@ -1932,6 +1932,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 includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) {
|
func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) {
|
||||||
for _, topThread := range topThreadList.Items {
|
for _, topThread := range topThreadList.Items {
|
||||||
topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false)
|
topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false)
|
||||||
|
|||||||
@@ -97,6 +97,18 @@ type DurationPostCount struct {
|
|||||||
PostCount int `db:"postcount"`
|
PostCount int `db:"postcount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Top DMs
|
||||||
|
type TopDM struct {
|
||||||
|
MessageCount int64 `json:"post_count"`
|
||||||
|
Participants string `json:"-"`
|
||||||
|
SecondParticipant string `json:"second_participant"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -243,3 +255,17 @@ 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}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6007,6 +6007,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")
|
||||||
|
|||||||
@@ -6806,6 +6806,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
|
||||||
|
|||||||
@@ -2963,3 +2963,55 @@ 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("count(p.id) as MessageCount", aggregator).FromSelect(channelSelector, "vch").
|
||||||
|
Join("ChannelMembers as cm on cm.ChannelId = vch.Id").
|
||||||
|
Join("Posts as p on p.ChannelId = vch.Id").
|
||||||
|
Where(sq.Gt{
|
||||||
|
"p.UpdateAt": since,
|
||||||
|
}).GroupBy("vch.id").
|
||||||
|
Limit(uint64(limit)).
|
||||||
|
Offset(uint64(offset))
|
||||||
|
|
||||||
|
topDMsBuilder = topDMsBuilder.OrderBy("MessageCount DESC").Limit(uint64(limit)).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 = postProcessTopDMs(userID, topDMs)
|
||||||
|
return model.GetTopDMListWithPagination(topDMs, limit), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func postProcessTopDMs(userID string, topDMs []*model.TopDM) []*model.TopDM {
|
||||||
|
for _, topDM := range topDMs {
|
||||||
|
participants := strings.Split(topDM.Participants, ",")
|
||||||
|
if participants[0] == userID {
|
||||||
|
topDM.SecondParticipant = participants[1]
|
||||||
|
} else {
|
||||||
|
topDM.SecondParticipant = participants[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return topDMs
|
||||||
|
}
|
||||||
|
|||||||
@@ -388,6 +388,9 @@ type PostStore interface {
|
|||||||
GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error)
|
GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, 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 {
|
||||||
|
|||||||
@@ -700,6 +700,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)
|
||||||
|
|||||||
@@ -5434,6 +5434,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