diff --git a/api4/post.go b/api4/post.go index ad2a16f836..3a6cb7b981 100644 --- a/api4/post.go +++ b/api4/post.go @@ -19,6 +19,7 @@ func (api *API) InitPost() { api.BaseRoutes.Posts.Handle("", api.APISessionRequired(createPost)).Methods("POST") api.BaseRoutes.Post.Handle("", api.APISessionRequired(getPost)).Methods("GET") api.BaseRoutes.Post.Handle("", api.APISessionRequired(deletePost)).Methods("DELETE") + api.BaseRoutes.Posts.Handle("/ids", api.APISessionRequired(getPostsByIds)).Methods("POST") api.BaseRoutes.Posts.Handle("/ephemeral", api.APISessionRequired(createEphemeralPost)).Methods("POST") api.BaseRoutes.Post.Handle("/thread", api.APISessionRequired(getPostThread)).Methods("GET") api.BaseRoutes.Post.Handle("/files/info", api.APISessionRequired(getFileInfosForPost)).Methods("GET") @@ -408,6 +409,57 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) { + postIDs := model.ArrayFromJSON(r.Body) + + if len(postIDs) == 0 { + c.SetInvalidParam("post_ids") + return + } + + if len(postIDs) > 1000 { + c.Err = model.NewAppError("getPostsByIds", "api.post.posts_by_ids.invalid_body.request_error", map[string]interface{}{"MaxLength": 1000}, "", http.StatusBadRequest) + return + } + + postsList, err := c.App.GetPostsByIds(postIDs) + if err != nil { + c.Err = err + return + } + + var posts = []*model.Post{} + channelMap := make(map[string]*model.Channel) + + for _, post := range postsList { + var channel *model.Channel + if val, ok := channelMap[post.ChannelId]; ok { + channel = val + } else { + channel, err = c.App.GetChannel(post.ChannelId) + if err != nil { + c.Err = err + return + } + channelMap[channel.Id] = channel + } + + if !c.App.SessionHasPermissionToChannel(*c.AppContext.Session(), channel.Id, model.PermissionReadChannel) { + if channel.Type != model.ChannelTypeOpen || (channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel)) { + continue + } + } + + post = c.App.PreparePostForClient(post, false, false) + + posts = append(posts, post) + } + + if err := json.NewEncoder(w).Encode(posts); err != nil { + mlog.Warn("Error while writing response", mlog.Err(err)) + } +} + func deletePost(c *Context, w http.ResponseWriter, _ *http.Request) { c.RequirePostId() if c.Err != nil { diff --git a/api4/post_test.go b/api4/post_test.go index 31159c4356..1529525d35 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -2735,3 +2735,26 @@ func TestSetPostUnreadWithoutCollapsedThreads(t *testing.T) { require.Equal(t, int64(3), channelUnread.MsgCountRoot) }) } +func TestGetPostsByIds(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + client := th.Client + + post1 := th.CreatePost() + post2 := th.CreatePost() + + posts, response, err := client.GetPostsByIds([]string{post1.Id, post2.Id}) + require.NoError(t, err) + CheckOKStatus(t, response) + require.Len(t, posts, 2, "wrong number returned") + require.Equal(t, posts[0].Id, post2.Id) + require.Equal(t, posts[1].Id, post1.Id) + + _, response, err = client.GetPostsByIds([]string{}) + require.Error(t, err) + CheckBadRequestStatus(t, response) + + _, response, err = client.GetPostsByIds([]string{"abc123"}) + require.Error(t, err) + CheckNotFoundStatus(t, response) +} diff --git a/app/app_iface.go b/app/app_iface.go index 929a7d3f85..d5a3824642 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -670,6 +670,7 @@ type AppIface interface { GetPostsAfterPost(options model.GetPostsOptions) (*model.PostList, *model.AppError) GetPostsAroundPost(before bool, options model.GetPostsOptions) (*model.PostList, *model.AppError) GetPostsBeforePost(options model.GetPostsOptions) (*model.PostList, *model.AppError) + GetPostsByIds(postIDs []string) ([]*model.Post, *model.AppError) GetPostsEtag(channelID string, collapsedThreads bool) string GetPostsForChannelAroundLastUnread(channelID, userID string, limitBefore, limitAfter int, skipFetchThreads bool, collapsedThreads, collapsedThreadsExtended bool) (*model.PostList, *model.AppError) GetPostsPage(options model.GetPostsOptions) (*model.PostList, *model.AppError) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index e68daa0817..87f48b4680 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -7570,6 +7570,28 @@ func (a *OpenTracingAppLayer) GetPostsBeforePost(options model.GetPostsOptions) return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetPostsByIds(postIDs []string) ([]*model.Post, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsByIds") + + 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.GetPostsByIds(postIDs) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetPostsEtag(channelID string, collapsedThreads bool) string { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsEtag") diff --git a/app/post.go b/app/post.go index bc69ca9b5d..6994c9db24 100644 --- a/app/post.go +++ b/app/post.go @@ -1670,3 +1670,18 @@ func (a *App) GetPostIfAuthorized(postID string, session *model.Session) (*model return post, nil } + +func (a *App) GetPostsByIds(postIDs []string) ([]*model.Post, *model.AppError) { + posts, err := a.Srv().Store.Post().GetPostsByIds(postIDs) + if err != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(err, &nfErr): + return nil, model.NewAppError("GetPostsByIds", "app.post.get.app_error", nil, nfErr.Error(), http.StatusNotFound) + default: + return nil, model.NewAppError("GetPostsByIds", "app.post.get.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + return posts, nil +} diff --git a/i18n/en.json b/i18n/en.json index a850ad3c3e..321beb8d2c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2215,6 +2215,10 @@ "id": "api.post.patch_post.can_not_update_post_in_deleted.error", "translation": "Can not update a post in a deleted channel." }, + { + "id": "api.post.posts_by_ids.invalid_body.request_error", + "translation": "The number of Post IDs received has exceeded the maximum size of {{.MaxLength}}" + }, { "id": "api.post.search_files.invalid_body.app_error", "translation": "Unable to parse the request body." diff --git a/model/client4.go b/model/client4.go index e820a7abfd..dd9de4d000 100644 --- a/model/client4.go +++ b/model/client4.go @@ -3768,6 +3768,27 @@ func (c *Client4) GetPostsForChannel(channelId string, page, perPage int, etag s return &list, BuildResponse(r), nil } +// GetPostsByIds gets a list of posts by taking an array of post ids +func (c *Client4) GetPostsByIds(postIds []string) ([]*Post, *Response, error) { + js, jsonErr := json.Marshal(postIds) + if jsonErr != nil { + return nil, nil, NewAppError("SearchFilesWithParams", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + r, err := c.DoAPIPost(c.postsRoute()+"/ids", string(js)) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + var list []*Post + if r.StatusCode == http.StatusNotModified { + return list, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil { + return nil, nil, NewAppError("GetPostsByIds", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return list, BuildResponse(r), nil +} + // GetFlaggedPostsForUser returns flagged posts of a user based on user id string. func (c *Client4) GetFlaggedPostsForUser(userId string, page int, perPage int) (*PostList, *Response, error) { query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index 9f12f745c1..6504bdd0b4 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -1978,6 +1978,9 @@ func (s *SqlPostStore) GetPostsByIds(postIds []string) ([]*model.Post, error) { if err != nil { return nil, errors.Wrap(err, "failed to find Posts") } + if len(posts) == 0 { + return nil, store.NewErrNotFound("Post", fmt.Sprintf("postIds=%v", postIds)) + } return posts, nil }