diff --git a/api4/post.go b/api4/post.go index ef9158931a..54327bcbdd 100644 --- a/api4/post.go +++ b/api4/post.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" ) @@ -22,6 +23,8 @@ func (api *API) InitPost() { api.BaseRoutes.PostsForChannel.Handle("", api.ApiSessionRequired(getPostsForChannel)).Methods("GET") api.BaseRoutes.PostsForUser.Handle("/flagged", api.ApiSessionRequired(getFlaggedPostsForUser)).Methods("GET") + api.BaseRoutes.ChannelForUser.Handle("/posts/unread", api.ApiSessionRequired(getPostsForChannelAroundLastUnread)).Methods("GET") + api.BaseRoutes.Team.Handle("/posts/search", api.ApiSessionRequired(searchPosts)).Methods("POST") api.BaseRoutes.Post.Handle("", api.ApiSessionRequired(updatePost)).Methods("PUT") api.BaseRoutes.Post.Handle("/patch", api.ApiSessionRequired(patchPost)).Methods("PUT") @@ -109,12 +112,20 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) { } afterPost := r.URL.Query().Get("after") - beforePost := r.URL.Query().Get("before") - sinceString := r.URL.Query().Get("since") + if len(afterPost) > 0 && !model.IsValidId(afterPost) { + c.SetInvalidParam("after") + return + } + beforePost := r.URL.Query().Get("before") + if len(beforePost) > 0 && !model.IsValidId(beforePost) { + c.SetInvalidParam("before") + return + } + + sinceString := r.URL.Query().Get("since") var since int64 var parseError error - if len(sinceString) > 0 { since, parseError = strconv.ParseInt(sinceString, 10, 64) if parseError != nil { @@ -123,7 +134,11 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) { } } - if !c.App.SessionHasPermissionToChannel(c.App.Session, c.Params.ChannelId, model.PERMISSION_READ_CHANNEL) { + channelId := c.Params.ChannelId + page := c.Params.Page + perPage := c.Params.PerPage + + if !c.App.SessionHasPermissionToChannel(c.App.Session, channelId, model.PERMISSION_READ_CHANNEL) { c.SetPermissionError(model.PERMISSION_READ_CHANNEL) return } @@ -133,31 +148,31 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) { etag := "" if since > 0 { - list, err = c.App.GetPostsSince(c.Params.ChannelId, since) + list, err = c.App.GetPostsSince(channelId, since) } else if len(afterPost) > 0 { - etag = c.App.GetPostsEtag(c.Params.ChannelId) + etag = c.App.GetPostsEtag(channelId) if c.HandleEtag(etag, "Get Posts After", w, r) { return } - list, err = c.App.GetPostsAfterPost(c.Params.ChannelId, afterPost, c.Params.Page, c.Params.PerPage) + list, err = c.App.GetPostsAfterPost(channelId, afterPost, page, perPage) } else if len(beforePost) > 0 { - etag = c.App.GetPostsEtag(c.Params.ChannelId) + etag = c.App.GetPostsEtag(channelId) if c.HandleEtag(etag, "Get Posts Before", w, r) { return } - list, err = c.App.GetPostsBeforePost(c.Params.ChannelId, beforePost, c.Params.Page, c.Params.PerPage) + list, err = c.App.GetPostsBeforePost(channelId, beforePost, page, perPage) } else { - etag = c.App.GetPostsEtag(c.Params.ChannelId) + etag = c.App.GetPostsEtag(channelId) if c.HandleEtag(etag, "Get Posts", w, r) { return } - list, err = c.App.GetPostsPage(c.Params.ChannelId, c.Params.Page, c.Params.PerPage) + list, err = c.App.GetPostsPage(channelId, page, perPage) } if err != nil { @@ -169,7 +184,56 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set(model.HEADER_ETAG_SERVER, etag) } - w.Write([]byte(c.App.PreparePostListForClient(list).ToJson())) + c.App.AddCursorIdsForPostList(list, afterPost, beforePost, since, page, perPage) + clientPostList := c.App.PreparePostListForClient(list) + + w.Write([]byte(clientPostList.ToJson())) +} + +func getPostsForChannelAroundLastUnread(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId().RequireChannelId() + if c.Err != nil { + return + } + + userId := c.Params.UserId + if !c.App.SessionHasPermissionToUser(c.App.Session, userId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + channelId := c.Params.ChannelId + if !c.App.SessionHasPermissionToChannel(c.App.Session, channelId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + postList, err := c.App.GetPostsForChannelAroundLastUnread(channelId, userId, c.Params.LimitBefore, c.Params.LimitAfter) + if err != nil { + c.Err = err + return + } + + etag := "" + if len(postList.Order) == 0 { + etag = c.App.GetPostsEtag(channelId) + + if c.HandleEtag(etag, "Get Posts", w, r) { + return + } + + postList, err = c.App.GetPostsPage(channelId, app.PAGE_DEFAULT, c.Params.LimitBefore) + } + + postList.NextPostId = c.App.GetNextPostIdFromPostList(postList) + postList.PrevPostId = c.App.GetPrevPostIdFromPostList(postList) + + clientPostList := c.App.PreparePostListForClient(postList) + + if len(etag) > 0 { + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + } + w.Write([]byte(clientPostList.ToJson())) } func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api4/post_test.go b/api4/post_test.go index b3af37bffc..2c7dbce784 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/model" @@ -911,6 +912,13 @@ func TestGetPostsForChannel(t *testing.T) { t.Log(posts.Posts) t.Fatal("should return 2 posts") } + // "since" query to return empty NextPostId and PrevPostId + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } found := make([]bool, 2) for _, p := range posts.Posts { @@ -945,6 +953,97 @@ func TestGetPostsForChannel(t *testing.T) { _, resp = th.SystemAdminClient.GetPostsForChannel(th.BasicChannel.Id, 0, 60, "") CheckNoError(t, resp) + + // more tests for next_post_id, prev_post_id, and order + // There are 12 posts composed of first 2 system messages and 10 created posts + Client.Login(th.BasicUser.Email, th.BasicUser.Password) + th.CreatePost() // post6 + post7 := th.CreatePost() + post8 := th.CreatePost() + th.CreatePost() // post9 + post10 := th.CreatePost() + + // get the system post IDs posted before the created posts above + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post1.Id, 0, 2, "") + systemPostId1 := posts.Order[1] + + // similar to '/posts' + posts, resp = Client.GetPostsForChannel(th.BasicChannel.Id, 0, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 12 || posts.Order[0] != post10.Id || posts.Order[11] != systemPostId1 { + t.Fatal("should return 12 posts and match order") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } + + // similar to '/posts?per_page=3' + posts, resp = Client.GetPostsForChannel(th.BasicChannel.Id, 0, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post10.Id || posts.Order[2] != post8.Id { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != post7.Id { + t.Fatal("should return post7.Id as PrevPostId") + } + + // similar to '/posts?per_page=3&page=1' + posts, resp = Client.GetPostsForChannel(th.BasicChannel.Id, 1, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post7.Id || posts.Order[2] != post5.Id { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != post8.Id { + t.Fatal("should return post8.Id as NextPostId") + } + if posts.PrevPostId != post4.Id { + t.Fatal("should return post4.Id as PrevPostId") + } + + // similar to '/posts?per_page=3&page=2' + posts, resp = Client.GetPostsForChannel(th.BasicChannel.Id, 2, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post4.Id || posts.Order[2] != post2.Id { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != post5.Id { + t.Fatal("should return post5.Id as NextPostId") + } + if posts.PrevPostId != post1.Id { + t.Fatal("should return post1.Id as PrevPostId") + } + + // similar to '/posts?per_page=3&page=3' + posts, resp = Client.GetPostsForChannel(th.BasicChannel.Id, 3, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post1.Id || posts.Order[2] != systemPostId1 { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != post2.Id { + t.Fatal("should return post2.Id as NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } + + // similar to '/posts?per_page=3&page=4' + posts, resp = Client.GetPostsForChannel(th.BasicChannel.Id, 4, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 0 { + t.Fatal("should return 0 post") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } } func TestGetFlaggedPostsForUser(t *testing.T) { @@ -1190,7 +1289,7 @@ func TestGetFlaggedPostsForUser(t *testing.T) { CheckNoError(t, resp) } -func TestGetPostsAfterAndBefore(t *testing.T) { +func TestGetPostsBefore(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() Client := th.Client @@ -1223,24 +1322,208 @@ func TestGetPostsAfterAndBefore(t *testing.T) { } } - posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post3.Id, 1, 1, "") + if posts.NextPostId != post3.Id { + t.Fatal("should match NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should match empty PrevPostId") + } + + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post4.Id, 1, 1, "") CheckNoError(t, resp) if len(posts.Posts) != 1 { t.Fatal("too many posts returned") } - - posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, "junk", 1, 1, "") - CheckNoError(t, resp) - - if len(posts.Posts) != 0 { - t.Fatal("should have no posts") + if posts.Order[0] != post2.Id { + t.Fatal("should match returned post") + } + if posts.NextPostId != post3.Id { + t.Fatal("should match NextPostId") + } + if posts.PrevPostId != post1.Id { + t.Fatal("should match PrevPostId") } - posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post3.Id, 0, 100, "") + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, "junk", 1, 1, "") + CheckBadRequestStatus(t, resp) + + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post5.Id, 0, 3, "") CheckNoError(t, resp) - found = make([]bool, 2) + if len(posts.Posts) != 3 { + t.Fatal("should match length of posts returned") + } + if posts.Order[0] != post4.Id { + t.Fatal("should match returned post") + } + if posts.Order[2] != post2.Id { + t.Fatal("should match returned post") + } + if posts.NextPostId != post5.Id { + t.Fatal("should match NextPostId") + } + if posts.PrevPostId != post1.Id { + t.Fatal("should match PrevPostId") + } + + // get the system post IDs posted before the created posts above + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post1.Id, 0, 2, "") + CheckNoError(t, resp) + systemPostId2 := posts.Order[0] + systemPostId1 := posts.Order[1] + + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post5.Id, 1, 3, "") + CheckNoError(t, resp) + + if len(posts.Posts) != 3 { + t.Fatal("should match length of posts returned") + } + if posts.Order[0] != post1.Id { + t.Fatal("should match returned post") + } + if posts.Order[1] != systemPostId2 { + t.Fatal("should match returned post") + } + if posts.Order[2] != systemPostId1 { + t.Fatal("should match returned post") + } + if posts.NextPostId != post2.Id { + t.Fatal("should match NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return empty PrevPostId") + } + + // more tests for next_post_id, prev_post_id, and order + // There are 12 posts composed of first 2 system messages and 10 created posts + post6 := th.CreatePost() + th.CreatePost() // post7 + post8 := th.CreatePost() + post9 := th.CreatePost() + th.CreatePost() // post10 + + // similar to '/posts?before=post9' + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post9.Id, 0, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 10 || posts.Order[0] != post8.Id || posts.Order[9] != systemPostId1 { + t.Fatal("should return 10 posts and match order") + } + if posts.NextPostId != post9.Id { + t.Fatal("should return post9.Id as NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } + + // similar to '/posts?before=post9&per_page=3' + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post9.Id, 0, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post8.Id || posts.Order[2] != post6.Id { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != post9.Id { + t.Fatal("should return post9.Id as NextPostId") + } + if posts.PrevPostId != post5.Id { + t.Fatal("should return post5.Id as PrevPostId") + } + + // similar to '/posts?before=post9&per_page=3&page=1' + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post9.Id, 1, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post5.Id || posts.Order[2] != post3.Id { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != post6.Id { + t.Fatal("should return post6.Id as NextPostId") + } + if posts.PrevPostId != post2.Id { + t.Fatal("should return post2.Id as PrevPostId") + } + + // similar to '/posts?before=post9&per_page=3&page=2' + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post9.Id, 2, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post2.Id || posts.Order[2] != systemPostId2 { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != post3.Id { + t.Fatal("should return post3.Id as NextPostId") + } + if posts.PrevPostId != systemPostId1 { + t.Fatal("should return systemPostId1 as PrevPostId") + } + + // similar to '/posts?before=post1&per_page=3' + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post1.Id, 0, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 2 || posts.Order[0] != systemPostId2 || posts.Order[1] != systemPostId1 { + t.Fatal("should return 2 posts and match order") + } + if posts.NextPostId != post1.Id { + t.Fatal("should return post1.Id as NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } + + // similar to '/posts?before=systemPostId1' + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, systemPostId1, 0, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 0 { + t.Fatal("should return 0 post") + } + if posts.NextPostId != systemPostId1 { + t.Fatal("should return systemPostId1 as NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } + + // similar to '/posts?before=systemPostId1&per_page=60&page=1' + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, systemPostId1, 1, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 0 { + t.Fatal("should return 0 post") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } + + // similar to '/posts?before=non-existent-post' + nonExistentPostId := model.NewId() + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, nonExistentPostId, 0, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 0 { + t.Fatal("should return 0 post") + } + if posts.NextPostId != nonExistentPostId { + t.Fatal("should return nonExistentPostId as NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } +} + +func TestGetPostsAfter(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + Client := th.Client + + post1 := th.CreatePost() + post2 := th.CreatePost() + post3 := th.CreatePost() + post4 := th.CreatePost() + post5 := th.CreatePost() + + posts, resp := Client.GetPostsAfter(th.BasicChannel.Id, post3.Id, 0, 100, "") + CheckNoError(t, resp) + + found := make([]bool, 2) for _, p := range posts.Posts { if p.Id == post4.Id { found[0] = true @@ -1259,18 +1542,298 @@ func TestGetPostsAfterAndBefore(t *testing.T) { } } - posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post3.Id, 1, 1, "") + if posts.NextPostId != "" { + t.Fatal("should match empty NextPostId") + } + if posts.PrevPostId != post3.Id { + t.Fatal("should match PrevPostId") + } + + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post2.Id, 1, 1, "") CheckNoError(t, resp) if len(posts.Posts) != 1 { t.Fatal("too many posts returned") } + if posts.Order[0] != post4.Id { + t.Fatal("should match returned post") + } + if posts.NextPostId != post5.Id { + t.Fatal("should match NextPostId") + } + if posts.PrevPostId != post3.Id { + t.Fatal("should match PrevPostId") + } posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, "junk", 1, 1, "") + CheckBadRequestStatus(t, resp) + + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post1.Id, 0, 3, "") CheckNoError(t, resp) - if len(posts.Posts) != 0 { - t.Fatal("should have no posts") + if len(posts.Posts) != 3 { + t.Fatal("should match length of posts returned") + } + if posts.Order[0] != post4.Id { + t.Fatal("should match returned post") + } + if posts.Order[2] != post2.Id { + t.Fatal("should match returned post") + } + if posts.NextPostId != post5.Id { + t.Fatal("should match NextPostId") + } + if posts.PrevPostId != post1.Id { + t.Fatal("should match PrevPostId") + } + + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post1.Id, 1, 3, "") + CheckNoError(t, resp) + + if len(posts.Posts) != 1 { + t.Fatal("should match length of posts returned") + } + if posts.Order[0] != post5.Id { + t.Fatal("should match returned post") + } + if posts.NextPostId != "" { + t.Fatal("should match NextPostId") + } + if posts.PrevPostId != post4.Id { + t.Fatal("should match PrevPostId") + } + + // more tests for next_post_id, prev_post_id, and order + // There are 12 posts composed of first 2 system messages and 10 created posts + post6 := th.CreatePost() + th.CreatePost() // post7 + post8 := th.CreatePost() + post9 := th.CreatePost() + post10 := th.CreatePost() + + // similar to '/posts?after=post2' + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post2.Id, 0, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 8 || posts.Order[0] != post10.Id || posts.Order[7] != post3.Id { + t.Fatal("should return 8 posts and match order") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != post2.Id { + t.Fatal("should return post2.Id as PrevPostId") + } + + // similar to '/posts?after=post2&per_page=3' + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post2.Id, 0, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post5.Id || posts.Order[2] != post3.Id { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != post6.Id { + t.Fatal("should return post6.Id as NextPostId") + } + if posts.PrevPostId != post2.Id { + t.Fatal("should return post2.Id as PrevPostId") + } + + // similar to '/posts?after=post2&per_page=3&page=1' + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post2.Id, 1, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 3 || posts.Order[0] != post8.Id || posts.Order[2] != post6.Id { + t.Fatal("should return 3 posts and match order") + } + if posts.NextPostId != post9.Id { + t.Fatal("should return post9.Id as NextPostId") + } + if posts.PrevPostId != post5.Id { + t.Fatal("should return post5.Id as PrevPostId") + } + + // similar to '/posts?after=post2&per_page=3&page=2' + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post2.Id, 2, 3, "") + CheckNoError(t, resp) + if len(posts.Order) != 2 || posts.Order[0] != post10.Id || posts.Order[1] != post9.Id { + t.Fatal("should return 2 posts and match order") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != post8.Id { + t.Fatal("should return post8.Id as PrevPostId") + } + + // similar to '/posts?after=post10' + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post10.Id, 0, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 0 { + t.Fatal("should return 0 post") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != post10.Id { + t.Fatal("should return post10.Id as PrevPostId") + } + + // similar to '/posts?after=post10&page=1' + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, post10.Id, 1, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 0 { + t.Fatal("should return 0 post") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } + + // similar to '/posts?after=non-existent-post' + nonExistentPostId := model.NewId() + posts, resp = Client.GetPostsAfter(th.BasicChannel.Id, nonExistentPostId, 0, 60, "") + CheckNoError(t, resp) + if len(posts.Order) != 0 { + t.Fatal("should return 0 post") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != nonExistentPostId { + t.Fatal("should return nonExistentPostId as PrevPostId") + } +} + +func TestGetPostsForChannelAroundLastUnread(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + Client := th.Client + userId := th.BasicUser.Id + channelId := th.BasicChannel.Id + + // 12 posts = 2 systems posts + 10 created posts below + post1 := th.CreatePost() + post2 := th.CreatePost() + post3 := th.CreatePost() + post4 := th.CreatePost() + th.CreatePost() // post5 + post6 := th.CreatePost() + post7 := th.CreatePost() + post8 := th.CreatePost() + post9 := th.CreatePost() + post10 := th.CreatePost() + + // All returned posts are all read by the user, since it's created by the user itself. + posts, resp := Client.GetPostsAroundLastUnread(userId, channelId, 20, 20) + CheckNoError(t, resp) + + if len(posts.Order) != 12 { + t.Fatal("Should return 12 posts only since there's no unread post") + } + + // Set channel member's last viewed to 0. + // All returned posts are latest posts as if all previous posts were already read by the user. + channelMember, err := th.App.Srv.Store.Channel().GetMember(channelId, userId) + require.Nil(t, err) + channelMember.LastViewedAt = 0 + _, err = th.App.Srv.Store.Channel().UpdateMember(channelMember) + require.Nil(t, err) + th.App.Srv.Store.Post().InvalidateLastPostTimeCache(channelId) + + posts, resp = Client.GetPostsAroundLastUnread(userId, channelId, 20, 20) + CheckNoError(t, resp) + + if len(posts.Order) != 12 { + t.Fatal("Should return 12 posts only since there's no unread post") + } + + // get the first system post generated before the created posts above + posts, resp = Client.GetPostsBefore(th.BasicChannel.Id, post1.Id, 0, 2, "") + CheckNoError(t, resp) + systemPostId1 := posts.Order[1] + + // Set channel member's last viewed before post1. + channelMember, err = th.App.Srv.Store.Channel().GetMember(channelId, userId) + require.Nil(t, err) + channelMember.LastViewedAt = post1.CreateAt - 1 + _, err = th.App.Srv.Store.Channel().UpdateMember(channelMember) + require.Nil(t, err) + th.App.Srv.Store.Post().InvalidateLastPostTimeCache(channelId) + + posts, resp = Client.GetPostsAroundLastUnread(userId, channelId, 3, 3) + CheckNoError(t, resp) + + if len(posts.Order) != 5 || posts.Order[0] != post3.Id || posts.Order[4] != systemPostId1 { + t.Fatal("Should return 5 posts and match order") + } + if posts.NextPostId != post4.Id { + t.Fatal("should return post4.Id as NextPostId") + } + if posts.PrevPostId != "" { + t.Fatal("should return an empty PrevPostId") + } + + // Set channel member's last viewed before post6. + channelMember, err = th.App.Srv.Store.Channel().GetMember(channelId, userId) + require.Nil(t, err) + channelMember.LastViewedAt = post6.CreateAt - 1 + _, err = th.App.Srv.Store.Channel().UpdateMember(channelMember) + require.Nil(t, err) + th.App.Srv.Store.Post().InvalidateLastPostTimeCache(channelId) + + posts, resp = Client.GetPostsAroundLastUnread(userId, channelId, 3, 3) + CheckNoError(t, resp) + + if len(posts.Order) != 6 || posts.Order[0] != post8.Id || posts.Order[5] != post3.Id { + t.Fatal("Should return 6 posts and match order") + } + if posts.NextPostId != post9.Id { + t.Fatal("should return post8.Id as NextPostId") + } + if posts.PrevPostId != post2.Id { + t.Fatal("should return post2.Id as PrevPostId") + } + + // Set channel member's last viewed before post10. + channelMember, err = th.App.Srv.Store.Channel().GetMember(channelId, userId) + require.Nil(t, err) + channelMember.LastViewedAt = post10.CreateAt - 1 + _, err = th.App.Srv.Store.Channel().UpdateMember(channelMember) + require.Nil(t, err) + th.App.Srv.Store.Post().InvalidateLastPostTimeCache(channelId) + + posts, resp = Client.GetPostsAroundLastUnread(userId, channelId, 3, 3) + CheckNoError(t, resp) + + if len(posts.Order) != 4 || posts.Order[0] != post10.Id || posts.Order[3] != post7.Id { + t.Fatal("Should return 4 posts and match order") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != post6.Id { + t.Fatal("should return post6.Id as PrevPostId") + } + + // Set channel member's last viewed equal to post10. + channelMember, err = th.App.Srv.Store.Channel().GetMember(channelId, userId) + require.Nil(t, err) + channelMember.LastViewedAt = post10.CreateAt + _, err = th.App.Srv.Store.Channel().UpdateMember(channelMember) + require.Nil(t, err) + th.App.Srv.Store.Post().InvalidateLastPostTimeCache(channelId) + + posts, resp = Client.GetPostsAroundLastUnread(userId, channelId, 3, 3) + CheckNoError(t, resp) + + if len(posts.Order) != 3 || posts.Order[0] != post10.Id || posts.Order[2] != post8.Id { + t.Fatal("Should return 3 posts and match order") + } + if posts.NextPostId != "" { + t.Fatal("should return an empty NextPostId") + } + if posts.PrevPostId != post7.Id { + t.Fatal("should return post7.Id as PrevPostId") } } diff --git a/app/post.go b/app/post.go index 2afc2472d0..4391123dce 100644 --- a/app/post.go +++ b/app/post.go @@ -20,6 +20,7 @@ import ( const ( PENDING_POST_IDS_CACHE_SIZE = 25000 PENDING_POST_IDS_CACHE_TTL = 30 * time.Second + PAGE_DEFAULT = 0 ) func (a *App) CreatePostAsUser(post *model.Post, currentSessionId string) (*model.Post, *model.AppError) { @@ -674,6 +675,126 @@ func (a *App) GetPostsAroundPost(postId, channelId string, offset, limit int, be return a.Srv.Store.Post().GetPostsAfter(channelId, postId, limit, offset) } +func (a *App) GetPostAfterTime(channelId string, time int64) (*model.Post, *model.AppError) { + return a.Srv.Store.Post().GetPostAfterTime(channelId, time) +} + +func (a *App) GetPostIdAfterTime(channelId string, time int64) (string, *model.AppError) { + return a.Srv.Store.Post().GetPostIdAfterTime(channelId, time) +} + +func (a *App) GetPostIdBeforeTime(channelId string, time int64) (string, *model.AppError) { + return a.Srv.Store.Post().GetPostIdBeforeTime(channelId, time) +} + +func (a *App) GetNextPostIdFromPostList(postList *model.PostList) string { + if len(postList.Order) > 0 { + firstPostId := postList.Order[0] + firstPost := postList.Posts[firstPostId] + nextPostId, err := a.GetPostIdAfterTime(firstPost.ChannelId, firstPost.CreateAt) + if err != nil { + mlog.Warn("GetNextPostIdFromPostList: failed in getting next post", mlog.Err(err)) + } + + return nextPostId + } + + return "" +} + +func (a *App) GetPrevPostIdFromPostList(postList *model.PostList) string { + if len(postList.Order) > 0 { + lastPostId := postList.Order[len(postList.Order)-1] + lastPost := postList.Posts[lastPostId] + previousPostId, err := a.GetPostIdBeforeTime(lastPost.ChannelId, lastPost.CreateAt) + if err != nil { + mlog.Warn("GetPrevPostIdFromPostList: failed in getting previous post", mlog.Err(err)) + } + + return previousPostId + } + + return "" +} + +// AddCursorIdsForPostList adds NextPostId and PrevPostId as cursor to the PostList. +// The conditional blocks ensure that it sets those cursor IDs immediately as afterPost, beforePost or empty, +// and only query to database whenever necessary. +func (a *App) AddCursorIdsForPostList(originalList *model.PostList, afterPost, beforePost string, since int64, page, perPage int) { + prevPostIdSet := false + prevPostId := "" + nextPostIdSet := false + nextPostId := "" + + if since > 0 { // "since" query to return empty NextPostId and PrevPostId + nextPostIdSet = true + prevPostIdSet = true + } else if afterPost != "" { + if page == 0 { + prevPostId = afterPost + prevPostIdSet = true + } + + if len(originalList.Order) < perPage { + nextPostIdSet = true + } + } else if beforePost != "" { + if page == 0 { + nextPostId = beforePost + nextPostIdSet = true + } + + if len(originalList.Order) < perPage { + prevPostIdSet = true + } + } + + if !nextPostIdSet { + nextPostId = a.GetNextPostIdFromPostList(originalList) + } + + if !prevPostIdSet { + prevPostId = a.GetPrevPostIdFromPostList(originalList) + } + + originalList.NextPostId = nextPostId + originalList.PrevPostId = prevPostId +} + +func (a *App) GetPostsForChannelAroundLastUnread(channelId, userId string, limitBefore, limitAfter int) (*model.PostList, *model.AppError) { + var member *model.ChannelMember + var err *model.AppError + if member, err = a.GetChannelMember(channelId, userId); err != nil { + return nil, err + } else if member.LastViewedAt == 0 { + return model.NewPostList(), nil + } + + lastUnreadPost, err := a.GetPostAfterTime(channelId, member.LastViewedAt) + if err != nil { + return nil, err + } else if lastUnreadPost == nil { + return model.NewPostList(), nil + } + + var postList *model.PostList + if postList, err = a.GetPostsBeforePost(channelId, lastUnreadPost.Id, PAGE_DEFAULT, limitBefore); err != nil { + return nil, err + } + + if postListAfter, err := a.GetPostsAfterPost(channelId, lastUnreadPost.Id, PAGE_DEFAULT, limitAfter-1); err != nil { + return nil, err + } else if postListAfter != nil { + postList.Extend(postListAfter) + } + + postList.AddPost(lastUnreadPost) + postList.AddOrder(lastUnreadPost.Id) + + postList.SortByCreateAt() + return postList, nil +} + func (a *App) DeletePost(postId, deleteByID string) (*model.Post, *model.AppError) { post, err := a.Srv.Store.Post().GetSingle(postId) if err != nil { diff --git a/app/post_metadata.go b/app/post_metadata.go index 1bc4ff73bf..94d44e266d 100644 --- a/app/post_metadata.go +++ b/app/post_metadata.go @@ -40,8 +40,10 @@ func (a *App) InitPostMetadata() { func (a *App) PreparePostListForClient(originalList *model.PostList) *model.PostList { list := &model.PostList{ - Posts: make(map[string]*model.Post, len(originalList.Posts)), - Order: originalList.Order, // Note that this uses the original Order array, so it isn't a deep copy + Posts: make(map[string]*model.Post, len(originalList.Posts)), + Order: originalList.Order, + NextPostId: originalList.NextPostId, + PrevPostId: originalList.PrevPostId, } for id, originalPost := range originalList.Posts { diff --git a/i18n/en.json b/i18n/en.json index 9a521b9394..bd7b5928a2 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6122,6 +6122,14 @@ "id": "store.sql_post.get_parents_posts.app_error", "translation": "Unable to get the parent post for the channel" }, + { + "id": "store.sql_post.get_post_after_time.app_error", + "translation": "Unable to get post after time bound" + }, + { + "id": "store.sql_post.get_post_id_around.app_error", + "translation": "Unable to get post around time bound" + }, { "id": "store.sql_post.get_posts.app_error", "translation": "Limit exceeded for paging" diff --git a/model/client4.go b/model/client4.go index ff30781187..9514af6c69 100644 --- a/model/client4.go +++ b/model/client4.go @@ -2509,6 +2509,17 @@ func (c *Client4) GetPostsBefore(channelId, postId string, page, perPage int, et return PostListFromJson(r.Body), BuildResponse(r) } +// GetPostsAroundLastUnread gets a list of posts around last unread post by a user in a channel. +func (c *Client4) GetPostsAroundLastUnread(userId, channelId string, limitBefore, limitAfter int) (*PostList, *Response) { + query := fmt.Sprintf("?limit_before=%v&limit_after=%v", limitBefore, limitAfter) + if r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetChannelRoute(channelId)+"/posts/unread"+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return PostListFromJson(r.Body), BuildResponse(r) + } +} + // SearchPosts returns any posts with matching terms string. func (c *Client4) SearchPosts(teamId string, terms string, isOrSearch bool) (*PostList, *Response) { params := SearchParameter{ diff --git a/model/post_list.go b/model/post_list.go index 72f054641b..604fe0e750 100644 --- a/model/post_list.go +++ b/model/post_list.go @@ -10,14 +10,18 @@ import ( ) type PostList struct { - Order []string `json:"order"` - Posts map[string]*Post `json:"posts"` + Order []string `json:"order"` + Posts map[string]*Post `json:"posts"` + NextPostId string `json:"next_post_id"` + PrevPostId string `json:"prev_post_id"` } func NewPostList() *PostList { return &PostList{ - Order: make([]string, 0), - Posts: make(map[string]*Post), + Order: make([]string, 0), + Posts: make(map[string]*Post), + NextPostId: "", + PrevPostId: "", } } diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index c705bf2a1e..f277156fcd 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -4,6 +4,7 @@ package sqlstore import ( + "database/sql" "fmt" "net/http" "regexp" @@ -334,7 +335,7 @@ func (s *SqlPostStore) InvalidateLastPostTimeCache(channelId string) { func (s *SqlPostStore) GetEtag(channelId string, allowFromCache bool) string { if allowFromCache { - if cacheItem, ok := s.lastPostTimeCache.Get(channelId); ok { + if cacheItem, ok := s.lastPostTimeCache.Get(channelId); ok && cacheItem.(int64) > 0 { if s.metrics != nil { s.metrics.IncrementMemCacheHitCounter("Last Post Time") } @@ -661,6 +662,78 @@ func (s *SqlPostStore) getPostsAround(channelId string, postId string, limit int return list, nil } +func (s *SqlPostStore) GetPostIdBeforeTime(channelId string, time int64) (string, *model.AppError) { + return s.getPostIdAroundTime(channelId, time, true) +} + +func (s *SqlPostStore) GetPostIdAfterTime(channelId string, time int64) (string, *model.AppError) { + return s.getPostIdAroundTime(channelId, time, false) +} + +func (s *SqlPostStore) getPostIdAroundTime(channelId string, time int64, before bool) (string, *model.AppError) { + var direction sq.Sqlizer + var sort string + if before { + direction = sq.Lt{"CreateAt": time} + sort = "DESC" + } else { + direction = sq.Gt{"CreateAt": time} + sort = "ASC" + } + + query := s.getQueryBuilder(). + Select("Id"). + From("Posts"). + Where(sq.And{ + direction, + sq.Eq{"ChannelId": channelId}, + sq.Eq{"DeleteAt": int(0)}, + }). + OrderBy("CreateAt " + sort). + Limit(1) + + queryString, args, err := query.ToSql() + if err != nil { + return "", model.NewAppError("SqlPostStore.getPostIdAroundTime", "store.sql_post.get_post_id_around.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var postId string + if err := s.GetMaster().SelectOne(&postId, queryString, args...); err != nil { + if err != sql.ErrNoRows { + return "", model.NewAppError("SqlPostStore.getPostIdAroundTime", "store.sql_post.get_post_id_around.app_error", nil, "channelId="+channelId+err.Error(), http.StatusInternalServerError) + } + } + + return postId, nil +} + +func (s *SqlPostStore) GetPostAfterTime(channelId string, time int64) (*model.Post, *model.AppError) { + query := s.getQueryBuilder(). + Select("*"). + From("Posts"). + Where(sq.And{ + sq.Gt{"CreateAt": time}, + sq.Eq{"ChannelId": channelId}, + sq.Eq{"DeleteAt": int(0)}, + }). + OrderBy("CreateAt ASC"). + Limit(1) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, model.NewAppError("SqlPostStore.GetPostAfterTime", "store.sql_post.get_post_after_time.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var post *model.Post + if err := s.GetMaster().SelectOne(&post, queryString, args...); err != nil { + if err != sql.ErrNoRows { + return nil, model.NewAppError("SqlPostStore.GetPostAfterTime", "store.sql_post.get_post_after_time.app_error", nil, "channelId="+channelId+err.Error(), http.StatusInternalServerError) + } + } + + return post, nil +} + func (s *SqlPostStore) getRootPosts(channelId string, offset int, limit int) store.StoreChannel { return store.Do(func(result *store.StoreResult) { var posts []*model.Post diff --git a/store/store.go b/store/store.go index cc176e354c..e5482fe103 100644 --- a/store/store.go +++ b/store/store.go @@ -226,6 +226,9 @@ type PostStore interface { GetPostsBefore(channelId string, postId string, numPosts int, offset int) (*model.PostList, *model.AppError) GetPostsAfter(channelId string, postId string, numPosts int, offset int) (*model.PostList, *model.AppError) GetPostsSince(channelId string, time int64, allowFromCache bool) (*model.PostList, *model.AppError) + GetPostAfterTime(channelId string, time int64) (*model.Post, *model.AppError) + GetPostIdAfterTime(channelId string, time int64) (string, *model.AppError) + GetPostIdBeforeTime(channelId string, time int64) (string, *model.AppError) GetEtag(channelId string, allowFromCache bool) string Search(teamId string, userId string, params *model.SearchParams) StoreChannel AnalyticsUserCountsWithPostsByDay(teamId string) (model.AnalyticsRows, *model.AppError) diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go index 3cb115621d..ce7068bc0f 100644 --- a/store/storetest/mocks/PostStore.go +++ b/store/storetest/mocks/PostStore.go @@ -310,6 +310,77 @@ func (_m *PostStore) GetParentsForExportAfter(limit int, afterId string) ([]*mod return r0, r1 } +// GetPostAfterTime provides a mock function with given fields: channelId, time +func (_m *PostStore) GetPostAfterTime(channelId string, time int64) (*model.Post, *model.AppError) { + ret := _m.Called(channelId, time) + + var r0 *model.Post + if rf, ok := ret.Get(0).(func(string, int64) *model.Post); ok { + r0 = rf(channelId, time) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Post) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, int64) *model.AppError); ok { + r1 = rf(channelId, time) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetPostIdAfterTime provides a mock function with given fields: channelId, time +func (_m *PostStore) GetPostIdAfterTime(channelId string, time int64) (string, *model.AppError) { + ret := _m.Called(channelId, time) + + var r0 string + if rf, ok := ret.Get(0).(func(string, int64) string); ok { + r0 = rf(channelId, time) + } else { + r0 = ret.Get(0).(string) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, int64) *model.AppError); ok { + r1 = rf(channelId, time) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetPostIdBeforeTime provides a mock function with given fields: channelId, time +func (_m *PostStore) GetPostIdBeforeTime(channelId string, time int64) (string, *model.AppError) { + ret := _m.Called(channelId, time) + + var r0 string + if rf, ok := ret.Get(0).(func(string, int64) string); ok { + r0 = rf(channelId, time) + } else { + r0 = ret.Get(0).(string) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, int64) *model.AppError); ok { + r1 = rf(channelId, time) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetPosts provides a mock function with given fields: channelId, offset, limit, allowFromCache func (_m *PostStore) GetPosts(channelId string, offset int, limit int, allowFromCache bool) (*model.PostList, *model.AppError) { ret := _m.Called(channelId, offset, limit, allowFromCache) diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index 788badcb70..295fda6a12 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -33,6 +33,7 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("GetPostsWithDetails", func(t *testing.T) { testPostStoreGetPostsWithDetails(t, ss) }) t.Run("GetPostsBeforeAfter", func(t *testing.T) { testPostStoreGetPostsBeforeAfter(t, ss) }) t.Run("GetPostsSince", func(t *testing.T) { testPostStoreGetPostsSince(t, ss) }) + t.Run("GetPostBeforeAfter", func(t *testing.T) { testPostStoreGetPostBeforeAfter(t, ss) }) t.Run("Search", func(t *testing.T) { testPostStoreSearch(t, ss) }) t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, ss) }) t.Run("PostCountsByDay", func(t *testing.T) { testPostCountsByDay(t, ss) }) @@ -1027,6 +1028,118 @@ func testPostStoreGetPostsSince(t *testing.T, ss store.Store) { } } +func testPostStoreGetPostBeforeAfter(t *testing.T, ss store.Store) { + channelId := model.NewId() + + o0 := &model.Post{} + o0.ChannelId = channelId + o0.UserId = model.NewId() + o0.Message = "zz" + model.NewId() + "b" + _, err := ss.Post().Save(o0) + require.Nil(t, err) + time.Sleep(2 * time.Millisecond) + + o1 := &model.Post{} + o1.ChannelId = channelId + o1.Type = model.POST_JOIN_CHANNEL + o1.UserId = model.NewId() + o1.Message = "system_join_channel message" + _, err = ss.Post().Save(o1) + require.Nil(t, err) + time.Sleep(2 * time.Millisecond) + + o0a := &model.Post{} + o0a.ChannelId = channelId + o0a.UserId = model.NewId() + o0a.Message = "zz" + model.NewId() + "b" + o0a.ParentId = o1.Id + o0a.RootId = o1.Id + _, err = ss.Post().Save(o0a) + require.Nil(t, err) + time.Sleep(2 * time.Millisecond) + + o0b := &model.Post{} + o0b.ChannelId = channelId + o0b.UserId = model.NewId() + o0b.Message = "deleted message" + o0b.ParentId = o1.Id + o0b.RootId = o1.Id + o0b.DeleteAt = 1 + _, err = ss.Post().Save(o0b) + require.Nil(t, err) + time.Sleep(2 * time.Millisecond) + + otherChannelPost := &model.Post{} + otherChannelPost.ChannelId = model.NewId() + otherChannelPost.UserId = model.NewId() + otherChannelPost.Message = "zz" + model.NewId() + "b" + _, err = ss.Post().Save(otherChannelPost) + require.Nil(t, err) + time.Sleep(2 * time.Millisecond) + + o2 := &model.Post{} + o2.ChannelId = channelId + o2.UserId = model.NewId() + o2.Message = "zz" + model.NewId() + "b" + _, err = ss.Post().Save(o2) + require.Nil(t, err) + time.Sleep(2 * time.Millisecond) + + o2a := &model.Post{} + o2a.ChannelId = channelId + o2a.UserId = model.NewId() + o2a.Message = "zz" + model.NewId() + "b" + o2a.ParentId = o2.Id + o2a.RootId = o2.Id + _, err = ss.Post().Save(o2a) + require.Nil(t, err) + + rPostId1, err := ss.Post().GetPostIdBeforeTime(channelId, o0a.CreateAt) + if rPostId1 != o1.Id || err != nil { + t.Fatal("should return before post o1") + } + + rPostId1, err = ss.Post().GetPostIdAfterTime(channelId, o0b.CreateAt) + if rPostId1 != o2.Id || err != nil { + t.Fatal("should return before post o2") + } + + rPost1, err := ss.Post().GetPostAfterTime(channelId, o0b.CreateAt) + if rPost1.Id != o2.Id || err != nil { + t.Fatal("should return before post o2") + } + + rPostId2, err := ss.Post().GetPostIdBeforeTime(channelId, o0.CreateAt) + if rPostId2 != "" || err != nil { + t.Fatal("should return no post") + } + + rPostId2, err = ss.Post().GetPostIdAfterTime(channelId, o0.CreateAt) + if rPostId2 != o1.Id || err != nil { + t.Fatal("should return before post o1") + } + + rPost2, err := ss.Post().GetPostAfterTime(channelId, o0.CreateAt) + if rPost2.Id != o1.Id || err != nil { + t.Fatal("should return before post o1") + } + + rPostId3, err := ss.Post().GetPostIdBeforeTime(channelId, o2a.CreateAt) + if rPostId3 != o2.Id || err != nil { + t.Fatal("should return before post o2") + } + + rPostId3, err = ss.Post().GetPostIdAfterTime(channelId, o2a.CreateAt) + if rPostId3 != "" || err != nil { + t.Fatal("should return no post") + } + + rPost3, err := ss.Post().GetPostAfterTime(channelId, o2a.CreateAt) + if rPost3 != nil || err != nil { + t.Fatal("should return no post") + } +} + func testPostStoreSearch(t *testing.T, ss store.Store) { teamId := model.NewId() userId := model.NewId() diff --git a/web/params.go b/web/params.go index b206b97b3e..6bb852ad00 100644 --- a/web/params.go +++ b/web/params.go @@ -18,6 +18,8 @@ const ( PER_PAGE_MAXIMUM = 200 LOGS_PER_PAGE_DEFAULT = 10000 LOGS_PER_PAGE_MAXIMUM = 10000 + LIMIT_DEFAULT = 60 + LIMIT_MAXIMUM = 200 ) type Params struct { @@ -68,6 +70,8 @@ type Params struct { IncludeMemberCount bool NotAssociatedToGroup string ExcludeDefaultChannels bool + LimitAfter int + LimitBefore int GroupIDs string IncludeTotalCount bool } @@ -226,6 +230,22 @@ func ParamsFromRequest(r *http.Request) *Params { params.LogsPerPage = val } + if val, err := strconv.Atoi(query.Get("limit_after")); err != nil || val < 0 { + params.LimitAfter = LIMIT_DEFAULT + } else if val > LIMIT_MAXIMUM { + params.LimitAfter = LIMIT_MAXIMUM + } else { + params.LimitAfter = val + } + + if val, err := strconv.Atoi(query.Get("limit_before")); err != nil || val < 0 { + params.LimitBefore = LIMIT_DEFAULT + } else if val > LIMIT_MAXIMUM { + params.LimitBefore = LIMIT_MAXIMUM + } else { + params.LimitBefore = val + } + if val, ok := props["syncable_id"]; ok { params.SyncableId = val }