MM-31713 Server/API: GetUserThread method (#16659)
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c14194d78b
Коммит
77da23e84b
22
api4/user.go
22
api4/user.go
@@ -95,6 +95,7 @@ func (api *API) InitUser() {
|
||||
api.BaseRoutes.UserThreads.Handle("", api.ApiSessionRequired(getThreadsForUser)).Methods("GET")
|
||||
api.BaseRoutes.UserThreads.Handle("/read", api.ApiSessionRequired(updateReadStateAllThreadsByUser)).Methods("PUT")
|
||||
|
||||
api.BaseRoutes.UserThread.Handle("", api.ApiSessionRequired(getThreadForUser)).Methods("GET")
|
||||
api.BaseRoutes.UserThread.Handle("/following", api.ApiSessionRequired(followThreadByUser)).Methods("PUT")
|
||||
api.BaseRoutes.UserThread.Handle("/following", api.ApiSessionRequired(unfollowThreadByUser)).Methods("DELETE")
|
||||
api.BaseRoutes.UserThread.Handle("/read/{timestamp:[0-9]+}", api.ApiSessionRequired(updateReadStateThreadByUser)).Methods("PUT")
|
||||
@@ -2820,6 +2821,27 @@ func migrateAuthToSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getThreadForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId().RequireThreadId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToUser(*c.App.Session(), c.Params.UserId) {
|
||||
c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
|
||||
return
|
||||
}
|
||||
extendedStr := r.URL.Query().Get("extended")
|
||||
|
||||
extended, _ := strconv.ParseBool(extendedStr)
|
||||
threads, err := c.App.GetThreadForUser(c.Params.UserId, c.Params.TeamId, c.Params.ThreadId, extended)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte(threads.ToJson()))
|
||||
}
|
||||
|
||||
func getThreadsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId().RequireTeamId()
|
||||
if c.Err != nil {
|
||||
|
||||
@@ -5668,6 +5668,45 @@ func TestThreadCounts(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSingleThreadGet(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
os.Setenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_COLLAPSEDTHREADS")
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.ThreadAutoFollow = true
|
||||
*cfg.ServiceSettings.CollapsedThreads = model.COLLAPSED_THREADS_DEFAULT_ON
|
||||
})
|
||||
|
||||
Client := th.Client
|
||||
defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.BasicUser.Id)
|
||||
defer th.App.Srv().Store.Post().PermanentDeleteByUser(th.SystemAdminUser.Id)
|
||||
|
||||
// create a post by regular user
|
||||
rpost, _ := postAndCheck(t, Client, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testMsg"})
|
||||
// reply with another
|
||||
time.Sleep(1)
|
||||
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel.Id, Message: "testReply", RootId: rpost.Id})
|
||||
|
||||
// create another thread to check that we are not returning it by mistake
|
||||
rpost2, _ := postAndCheck(t, Client, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testMsg2"})
|
||||
time.Sleep(1)
|
||||
postAndCheck(t, th.SystemAdminClient, &model.Post{ChannelId: th.BasicChannel2.Id, Message: "testReply", RootId: rpost2.Id})
|
||||
|
||||
// regular user should have two threads with 3 replies total
|
||||
threads, _ := checkThreadListReplies(t, th, th.Client, th.BasicUser.Id, 2, 2, nil)
|
||||
|
||||
tr, resp := th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, false)
|
||||
CheckNoError(t, resp)
|
||||
require.NotNil(t, tr)
|
||||
require.Equal(t, threads.Threads[0].PostId, tr.PostId)
|
||||
require.Empty(t, tr.Participants[0].Username)
|
||||
|
||||
tr, resp = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)
|
||||
CheckNoError(t, resp)
|
||||
require.NotEmpty(t, tr.Participants[0].Username)
|
||||
}
|
||||
|
||||
func TestMaintainUnreadMentionsInThread(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -693,6 +693,7 @@ type AppIface interface {
|
||||
GetTeamsForUser(userId string) ([]*model.Team, *model.AppError)
|
||||
GetTeamsUnreadForUser(excludeTeamId string, userId string) ([]*model.TeamUnread, *model.AppError)
|
||||
GetTermsOfService(id string) (*model.TermsOfService, *model.AppError)
|
||||
GetThreadForUser(userId, teamId, threadId string, extended bool) (*model.ThreadResponse, *model.AppError)
|
||||
GetThreadMembershipsForUser(userId, teamId string) ([]*model.ThreadMembership, error)
|
||||
GetThreadsForUser(userId, teamId string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError)
|
||||
GetUploadSession(uploadId string) (*model.UploadSession, *model.AppError)
|
||||
|
||||
@@ -8529,6 +8529,28 @@ func (a *OpenTracingAppLayer) GetTermsOfService(id string) (*model.TermsOfServic
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetThreadForUser(userId string, teamId string, threadId string, extended bool) (*model.ThreadResponse, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadForUser")
|
||||
|
||||
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.GetThreadForUser(userId, teamId, threadId, extended)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetThreadMembershipsForUser(userId string, teamId string) ([]*model.ThreadMembership, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetThreadMembershipsForUser")
|
||||
|
||||
13
app/user.go
13
app/user.go
@@ -2371,6 +2371,19 @@ func (a *App) GetThreadsForUser(userId, teamId string, options model.GetUserThre
|
||||
return threads, nil
|
||||
}
|
||||
|
||||
func (a *App) GetThreadForUser(userId, teamId, threadId string, extended bool) (*model.ThreadResponse, *model.AppError) {
|
||||
thread, err := a.Srv().Store.Thread().GetThreadForUser(userId, teamId, threadId, extended)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if thread == nil {
|
||||
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.not_found", nil, "thread not found/followed", http.StatusNotFound)
|
||||
}
|
||||
a.sanitizeProfiles(thread.Participants, false)
|
||||
thread.Post.SanitizeProps()
|
||||
return thread, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateThreadsReadForUser(userId, teamId string) *model.AppError {
|
||||
nErr := a.Srv().Store.Thread().MarkAllAsRead(userId, teamId)
|
||||
if nErr != nil {
|
||||
|
||||
@@ -5626,6 +5626,10 @@
|
||||
"id": "app.user.get_threads_for_user.app_error",
|
||||
"translation": "Unable to get user threads"
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_threads_for_user.not_found",
|
||||
"translation": "User thread doesn't exist or is not followed"
|
||||
},
|
||||
{
|
||||
"id": "app.user.get_total_users_count.app_error",
|
||||
"translation": "We could not count the users."
|
||||
|
||||
@@ -5824,6 +5824,23 @@ func (c *Client4) GetUserThreads(userId, teamId string, options GetUserThreadsOp
|
||||
return &threads, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client4) GetUserThread(userId, teamId, threadId string, extended bool) (*ThreadResponse, *Response) {
|
||||
url := c.GetUserThreadRoute(userId, teamId, threadId)
|
||||
if extended {
|
||||
url += "?extended=true"
|
||||
}
|
||||
r, appErr := c.DoApiGet(url, "")
|
||||
if appErr != nil {
|
||||
return nil, BuildErrorResponse(r, appErr)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var thread ThreadResponse
|
||||
json.NewDecoder(r.Body).Decode(&thread)
|
||||
|
||||
return &thread, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client4) UpdateThreadsReadForUser(userId, teamId string) *Response {
|
||||
r, appErr := c.DoApiPut(fmt.Sprintf("%s/read", c.GetUserThreadsRoute(userId, teamId)), "")
|
||||
if appErr != nil {
|
||||
|
||||
@@ -50,6 +50,11 @@ type GetUserThreadsOpts struct {
|
||||
Since uint64
|
||||
}
|
||||
|
||||
func (o *ThreadResponse) ToJson() string {
|
||||
b, _ := json.Marshal(o)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (o *Threads) ToJson() string {
|
||||
b, _ := json.Marshal(o)
|
||||
return string(b)
|
||||
|
||||
@@ -7828,6 +7828,24 @@ func (s *OpenTracingLayerThreadStore) GetPosts(threadId string, since int64) ([]
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerThreadStore) GetThreadForUser(userId string, teamId string, threadId string, extended bool) (*model.ThreadResponse, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadForUser")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.ThreadStore.GetThreadForUser(userId, teamId, threadId, extended)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerThreadStore) GetThreadsForUser(userId string, teamId string, opts model.GetUserThreadsOpts) (*model.Threads, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetThreadsForUser")
|
||||
|
||||
@@ -8498,6 +8498,26 @@ func (s *RetryLayerThreadStore) GetPosts(threadId string, since int64) ([]*model
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerThreadStore) GetThreadForUser(userId string, teamId string, threadId string, extended bool) (*model.ThreadResponse, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.ThreadStore.GetThreadForUser(userId, teamId, threadId, extended)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerThreadStore) GetThreadsForUser(userId string, teamId string, opts model.GetUserThreadsOpts) (*model.Threads, error) {
|
||||
|
||||
tries := 0
|
||||
|
||||
@@ -283,6 +283,71 @@ func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.Get
|
||||
|
||||
return result, nil
|
||||
}
|
||||
func (s *SqlThreadStore) GetThreadForUser(userId, teamId, threadId string, extended bool) (*model.ThreadResponse, error) {
|
||||
type JoinedThread struct {
|
||||
PostId string
|
||||
Following bool
|
||||
ReplyCount int64
|
||||
LastReplyAt int64
|
||||
LastViewedAt int64
|
||||
UnreadReplies int64
|
||||
UnreadMentions int64
|
||||
Participants model.StringArray
|
||||
model.Post
|
||||
}
|
||||
|
||||
unreadRepliesQuery := "SELECT COUNT(Posts.Id) From Posts Where Posts.RootId=ThreadMemberships.PostId AND Posts.UpdateAt >= ThreadMemberships.LastViewed AND Posts.DeleteAt=0"
|
||||
fetchConditions := sq.And{
|
||||
sq.Or{sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": ""}},
|
||||
sq.Eq{"ThreadMemberships.UserId": userId},
|
||||
sq.Eq{"Threads.PostId": threadId},
|
||||
}
|
||||
|
||||
var thread JoinedThread
|
||||
query, args, _ := s.getQueryBuilder().
|
||||
Select("Threads.*, Posts.*, ThreadMemberships.LastViewed as LastViewedAt, ThreadMemberships.UnreadMentions as UnreadMentions, ThreadMemberships.Following").
|
||||
From("Threads").
|
||||
Column(sq.Alias(sq.Expr(unreadRepliesQuery), "UnreadReplies")).
|
||||
LeftJoin("Posts ON Posts.Id = Threads.PostId").
|
||||
LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
|
||||
LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Threads.PostId").
|
||||
Where(fetchConditions).ToSql()
|
||||
err := s.GetReplica().SelectOne(&thread, query, args...)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !thread.Following {
|
||||
return nil, nil // in case the thread is not followed anymore - return nil error to be interpreted as 404
|
||||
}
|
||||
|
||||
var users []*model.User
|
||||
if extended {
|
||||
var err error
|
||||
users, err = s.User().GetProfileByIds(thread.Participants, &store.UserGetByIdsOpts{}, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get threads for user id=%s", userId)
|
||||
}
|
||||
} else {
|
||||
for _, userId := range thread.Participants {
|
||||
users = append(users, &model.User{Id: userId})
|
||||
}
|
||||
}
|
||||
|
||||
result := &model.ThreadResponse{
|
||||
PostId: thread.PostId,
|
||||
ReplyCount: thread.ReplyCount,
|
||||
LastReplyAt: thread.LastReplyAt,
|
||||
LastViewedAt: thread.LastViewedAt,
|
||||
UnreadReplies: thread.UnreadReplies,
|
||||
UnreadMentions: thread.UnreadMentions,
|
||||
Participants: users,
|
||||
Post: &thread.Post,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SqlThreadStore) MarkAllAsRead(userId, teamId string) error {
|
||||
memberships, err := s.GetMembershipsForUser(userId, teamId)
|
||||
|
||||
@@ -252,6 +252,7 @@ type ThreadStore interface {
|
||||
Update(thread *model.Thread) (*model.Thread, error)
|
||||
Get(id string) (*model.Thread, error)
|
||||
GetThreadsForUser(userId, teamId string, opts model.GetUserThreadsOpts) (*model.Threads, error)
|
||||
GetThreadForUser(userId, teamId, threadId string, extended bool) (*model.ThreadResponse, error)
|
||||
Delete(postId string) error
|
||||
GetPosts(threadId string, since int64) ([]*model.Post, error)
|
||||
|
||||
|
||||
@@ -171,6 +171,29 @@ func (_m *ThreadStore) GetPosts(threadId string, since int64) ([]*model.Post, er
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetThreadForUser provides a mock function with given fields: userId, teamId, threadId, extended
|
||||
func (_m *ThreadStore) GetThreadForUser(userId string, teamId string, threadId string, extended bool) (*model.ThreadResponse, error) {
|
||||
ret := _m.Called(userId, teamId, threadId, extended)
|
||||
|
||||
var r0 *model.ThreadResponse
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, bool) *model.ThreadResponse); ok {
|
||||
r0 = rf(userId, teamId, threadId, extended)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.ThreadResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
|
||||
r1 = rf(userId, teamId, threadId, extended)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetThreadsForUser provides a mock function with given fields: userId, teamId, opts
|
||||
func (_m *ThreadStore) GetThreadsForUser(userId string, teamId string, opts model.GetUserThreadsOpts) (*model.Threads, error) {
|
||||
ret := _m.Called(userId, teamId, opts)
|
||||
|
||||
@@ -7064,6 +7064,22 @@ func (s *TimerLayerThreadStore) GetPosts(threadId string, since int64) ([]*model
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerThreadStore) GetThreadForUser(userId string, teamId string, threadId string, extended bool) (*model.ThreadResponse, error) {
|
||||
start := timemodule.Now()
|
||||
|
||||
result, err := s.ThreadStore.GetThreadForUser(userId, teamId, threadId, extended)
|
||||
|
||||
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetThreadForUser", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerThreadStore) GetThreadsForUser(userId string, teamId string, opts model.GetUserThreadsOpts) (*model.Threads, error) {
|
||||
start := timemodule.Now()
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user