MM-11210 Add "GET /posts/unread" API to support landing on the last unread post (#11486)
* [MM-11210] Add API GET 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts/unread' for scrolling overhaul (#9108)
* Add API GET 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts/unread'
* add constants
* refactor GetPostSince and added more tests
* move constants to app package
* [MM-11528 && MM-11583] Add userId to in the "posts/unread" path and update test with time delay to fix intermittent failure (#9229)
* add userId to in the "posts/unread" path and update test with time delay to fix intermittent failure
* add limit before and after to query
* remove time delay on test and put pretermined value of Post.CreateAt
* Fix conflict
* [MM-11876] Add cursor to posts list such as next_post_id and previous_post_id (#9707)
* add cursor to posts list such as next_post_id and previous_post_id
add publish previous_post_id on WEBSOCKET_EVENT_POSTED and only get next or previous post IDs if necessary
revert change on adding previous_post_id in WEBSOCKET_EVENT_POSTED
add missing strings import
fix merge conflicts
* update per comment
* update per feedback
* corrected the logic in getting the next and previous post ID
* fix logic to determine next and post IDs, and rename function to have suffix of "Time"
* rearrange logics and add mote tests
* fix merge conflict
* fix missing message when using unread API (#10233)
* MM-15569 Fixes failing test on TestGetPostsForChannelAroundLastUnread (#11039)
* Fix missing posts when getting posts since
* revert changes to GetPostsSince
* migrate Post.GetPostAfterTime and Post.GetPostBeforeTime to sync by default
* revert change to cacheItem
* Fix post ID validation, build query on squirrel and only return post ID as necessary
Этот коммит содержится в:
коммит произвёл
Sudheer
родитель
f56a8f5a99
Коммит
b832985f1d
88
api4/post.go
88
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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
121
app/post.go
121
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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user