From d2b86f1b8de4784baf578b611cf80779ccfa722a Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Wed, 19 Apr 2017 05:15:15 +0900 Subject: APIv4 POST /reactions (#6092) * APIv4 POST /reactions * update corresponding V3 endpoint --- api/reaction.go | 11 ++---- api4/api.go | 3 ++ api4/reaction.go | 34 ++++++++++++++++ api4/reaction_test.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ app/reaction.go | 34 ++++++++++++++++ i18n/en.json | 8 ++++ model/client4.go | 14 +++++++ 7 files changed, 202 insertions(+), 8 deletions(-) diff --git a/api/reaction.go b/api/reaction.go index a4992d61b..9def7274a 100644 --- a/api/reaction.go +++ b/api/reaction.go @@ -65,17 +65,12 @@ func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) { return } - if result := <-app.Srv.Store.Reaction().Save(reaction); result.Err != nil { - c.Err = result.Err + if reaction, err := app.SaveReactionForPost(reaction); err != nil { + c.Err = err return } else { - go sendReactionEvent(model.WEBSOCKET_EVENT_REACTION_ADDED, channelId, reaction, post) - - reaction := result.Data.(*model.Reaction) - - app.InvalidateCacheForReactions(reaction.PostId) - w.Write([]byte(reaction.ToJson())) + return } } diff --git a/api4/api.go b/api4/api.go index a91fb80b5..18e1e4022 100644 --- a/api4/api.go +++ b/api4/api.go @@ -82,6 +82,8 @@ type Routes struct { Public *mux.Router // 'api/v4/public' + Reactions *mux.Router // 'api/v4/reactions' + Emojis *mux.Router // 'api/v4/emoji' Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}' @@ -154,6 +156,7 @@ func InitApi(full bool) { BaseRoutes.Preferences = BaseRoutes.User.PathPrefix("/preferences").Subrouter() BaseRoutes.License = BaseRoutes.ApiRoot.PathPrefix("/license").Subrouter() BaseRoutes.Public = BaseRoutes.ApiRoot.PathPrefix("/public").Subrouter() + BaseRoutes.Reactions = BaseRoutes.ApiRoot.PathPrefix("/reactions").Subrouter() BaseRoutes.Emojis = BaseRoutes.ApiRoot.PathPrefix("/emoji").Subrouter() BaseRoutes.Emoji = BaseRoutes.Emojis.PathPrefix("/{emoji_id:[A-Za-z0-9]+}").Subrouter() diff --git a/api4/reaction.go b/api4/reaction.go index 4deae4370..7d5952eea 100644 --- a/api4/reaction.go +++ b/api4/reaction.go @@ -15,9 +15,43 @@ import ( func InitReaction() { l4g.Debug(utils.T("api.reaction.init.debug")) + BaseRoutes.Reactions.Handle("", ApiSessionRequired(saveReaction)).Methods("POST") BaseRoutes.Post.Handle("/reactions", ApiSessionRequired(getReactions)).Methods("GET") } +func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) { + reaction := model.ReactionFromJson(r.Body) + if reaction == nil { + c.SetInvalidParam("reaction") + return + } + + if len(reaction.UserId) != 26 || len(reaction.PostId) != 26 || len(reaction.EmojiName) == 0 || len(reaction.EmojiName) > 64 { + c.Err = model.NewLocAppError("saveReaction", "api.reaction.save_reaction.invalid.app_error", nil, "") + c.Err.StatusCode = http.StatusBadRequest + return + } + + if reaction.UserId != c.Session.UserId { + c.Err = model.NewLocAppError("saveReaction", "api.reaction.save_reaction.user_id.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if !app.SessionHasPermissionToChannelByPost(c.Session, reaction.PostId, model.PERMISSION_READ_CHANNEL) { + c.SetPermissionError(model.PERMISSION_READ_CHANNEL) + return + } + + if reaction, err := app.SaveReactionForPost(reaction); err != nil { + c.Err = err + return + } else { + w.Write([]byte(reaction.ToJson())) + return + } +} + func getReactions(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePostId() if c.Err != nil { diff --git a/api4/reaction_test.go b/api4/reaction_test.go index 9e0847c2d..980a96d68 100644 --- a/api4/reaction_test.go +++ b/api4/reaction_test.go @@ -4,6 +4,7 @@ package api4 import ( + "strings" "testing" "reflect" @@ -12,6 +13,111 @@ import ( "github.com/mattermost/platform/model" ) +func TestSaveReaction(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + userId := th.BasicUser.Id + postId := th.BasicPost.Id + + reaction := &model.Reaction{ + UserId: userId, + PostId: postId, + EmojiName: "smile", + } + + rr, resp := Client.SaveReaction(reaction) + CheckNoError(t, resp) + + if rr.UserId != reaction.UserId { + t.Fatal("UserId did not match") + } + + if rr.PostId != reaction.PostId { + t.Fatal("PostId did not match") + } + + if rr.EmojiName != reaction.EmojiName { + t.Fatal("EmojiName did not match") + } + + if rr.CreateAt == 0 { + t.Fatal("CreateAt should exist") + } + + if reactions, err := app.GetReactionsForPost(postId); err != nil && len(reactions) != 1 { + t.Fatal("didn't save reaction correctly") + } + + // saving a duplicate reaction + rr, resp = Client.SaveReaction(reaction) + CheckNoError(t, resp) + + if reactions, err := app.GetReactionsForPost(postId); err != nil && len(reactions) != 1 { + t.Fatal("should have not save duplicated reaction") + } + + reaction.EmojiName = "sad" + + rr, resp = Client.SaveReaction(reaction) + CheckNoError(t, resp) + + if rr.EmojiName != reaction.EmojiName { + t.Fatal("EmojiName did not match") + } + + if reactions, err := app.GetReactionsForPost(postId); err != nil && len(reactions) != 2 { + t.Fatal("should have save multiple reactions") + } + + reaction.PostId = GenerateTestId() + + _, resp = Client.SaveReaction(reaction) + CheckForbiddenStatus(t, resp) + + reaction.PostId = "junk" + + _, resp = Client.SaveReaction(reaction) + CheckBadRequestStatus(t, resp) + + reaction.PostId = postId + reaction.UserId = GenerateTestId() + + _, resp = Client.SaveReaction(reaction) + CheckForbiddenStatus(t, resp) + + reaction.UserId = "junk" + + _, resp = Client.SaveReaction(reaction) + CheckBadRequestStatus(t, resp) + + reaction.UserId = userId + reaction.EmojiName = "" + + _, resp = Client.SaveReaction(reaction) + CheckBadRequestStatus(t, resp) + + reaction.EmojiName = strings.Repeat("a", 65) + + _, resp = Client.SaveReaction(reaction) + CheckBadRequestStatus(t, resp) + + reaction.EmojiName = "smile" + otherUser := th.CreateUser() + Client.Logout() + Client.Login(otherUser.Email, otherUser.Password) + + _, resp = Client.SaveReaction(reaction) + CheckForbiddenStatus(t, resp) + + Client.Logout() + _, resp = Client.SaveReaction(reaction) + CheckUnauthorizedStatus(t, resp) + + _, resp = th.SystemAdminClient.SaveReaction(reaction) + CheckForbiddenStatus(t, resp) +} + func TestGetReactions(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() diff --git a/app/reaction.go b/app/reaction.go index cc31018ec..eb542286f 100644 --- a/app/reaction.go +++ b/app/reaction.go @@ -7,6 +7,25 @@ import ( "github.com/mattermost/platform/model" ) +func SaveReactionForPost(reaction *model.Reaction) (*model.Reaction, *model.AppError) { + post, err := GetSinglePost(reaction.PostId) + if err != nil { + return nil, err + } + + if result := <-Srv.Store.Reaction().Save(reaction); result.Err != nil { + return nil, result.Err + } else { + reaction = result.Data.(*model.Reaction) + + go sendReactionEvent(model.WEBSOCKET_EVENT_REACTION_ADDED, reaction, post) + + InvalidateCacheForReactions(reaction.PostId) + + return reaction, nil + } +} + func GetReactionsForPost(postId string) ([]*model.Reaction, *model.AppError) { if result := <-Srv.Store.Reaction().GetForPost(postId, true); result.Err != nil { return nil, result.Err @@ -14,3 +33,18 @@ func GetReactionsForPost(postId string) ([]*model.Reaction, *model.AppError) { return result.Data.([]*model.Reaction), nil } } + +func sendReactionEvent(event string, reaction *model.Reaction, post *model.Post) { + // send out that a reaction has been added/removed + message := model.NewWebSocketEvent(event, "", post.ChannelId, "", nil) + message.Add("reaction", reaction.ToJson()) + Publish(message) + + // The post is always modified since the UpdateAt always changes + InvalidateCacheForChannelPosts(post.ChannelId) + post.HasReactions = true + post.UpdateAt = model.GetMillis() + umessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil) + umessage.Add("post", post.ToJson()) + Publish(umessage) +} diff --git a/i18n/en.json b/i18n/en.json index 75954a6c9..731679b8a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1703,10 +1703,18 @@ "id": "api.reaction.list_reactions.mismatched_channel_id.app_error", "translation": "Failed to get reactions because channel ID does not match post ID in the URL" }, + { + "id": "api.reaction.save_reaction.invalid.app_error", + "translation": "Reaction is not valid." + }, { "id": "api.reaction.save_reaction.mismatched_channel_id.app_error", "translation": "Failed to save reaction because channel ID does not match post ID in the URL" }, + { + "id": "api.reaction.save_reaction.user_id.app_error", + "translation": "You cannot save reaction for the other user." + }, { "id": "api.reaction.send_reaction_event.post.app_error", "translation": "Failed to get post when sending websocket event for reaction" diff --git a/model/client4.go b/model/client4.go index ad3ff51a4..a7a3607e6 100644 --- a/model/client4.go +++ b/model/client4.go @@ -238,6 +238,10 @@ func (c *Client4) GetEmojiRoute(emojiId string) string { return fmt.Sprintf(c.GetEmojisRoute()+"/%v", emojiId) } +func (c *Client4) GetReactionsRoute() string { + return fmt.Sprintf("/reactions") +} + func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) { return c.DoApiRequest(http.MethodGet, url, "", etag) } @@ -2374,6 +2378,16 @@ func (c *Client4) GetEmoji(emojiId string) (*Emoji, *Response) { // Reaction Section +// SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned. +func (c *Client4) SaveReaction(reaction *Reaction) (*Reaction, *Response) { + if r, err := c.DoApiPost(c.GetReactionsRoute(), reaction.ToJson()); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return ReactionFromJson(r.Body), BuildResponse(r) + } +} + // GetReactions returns a list of reactions to a post. func (c *Client4) GetReactions(postId string) ([]*Reaction, *Response) { if r, err := c.DoApiGet(c.GetPostRoute(postId)+"/reactions", ""); err != nil { -- cgit v1.2.3-1-g7c22