diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/api.go | 1 | ||||
-rw-r--r-- | api/emoji.go | 13 | ||||
-rw-r--r-- | api/reaction.go | 203 | ||||
-rw-r--r-- | api/reaction_test.go | 314 |
4 files changed, 530 insertions, 1 deletions
diff --git a/api/api.go b/api/api.go index 3af23b9e0..10dfaa7d5 100644 --- a/api/api.go +++ b/api/api.go @@ -103,6 +103,7 @@ func InitApi() { InitEmoji() InitStatus() InitWebrtc() + InitReaction() InitDeprecated() // 404 on any api route before web.go has a chance to serve it diff --git a/api/emoji.go b/api/emoji.go index 9108db2ad..37adace49 100644 --- a/api/emoji.go +++ b/api/emoji.go @@ -209,11 +209,14 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) { return } + var emoji *model.Emoji if result := <-Srv.Store.Emoji().Get(id); result.Err != nil { c.Err = result.Err return } else { - if c.Session.UserId != result.Data.(*model.Emoji).CreatorId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { + emoji = result.Data.(*model.Emoji) + + if c.Session.UserId != emoji.CreatorId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) { c.Err = model.NewLocAppError("deleteEmoji", "api.emoji.delete.permissions.app_error", nil, "user_id="+c.Session.UserId) c.Err.StatusCode = http.StatusUnauthorized return @@ -226,6 +229,7 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) { } go deleteEmojiImage(id) + go deleteReactionsForEmoji(emoji.Name) ReturnStatusOK(w) } @@ -236,6 +240,13 @@ func deleteEmojiImage(id string) { } } +func deleteReactionsForEmoji(emojiName string) { + if result := <-Srv.Store.Reaction().DeleteAllWithEmojiName(emojiName); result.Err != nil { + l4g.Warn(utils.T("api.emoji.delete.delete_reactions.app_error"), emojiName) + l4g.Warn(result.Err) + } +} + func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) { if !*utils.Cfg.ServiceSettings.EnableCustomEmoji { c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.disabled.app_error", nil, "") diff --git a/api/reaction.go b/api/reaction.go new file mode 100644 index 000000000..5acf09f9e --- /dev/null +++ b/api/reaction.go @@ -0,0 +1,203 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + l4g "github.com/alecthomas/log4go" + "github.com/gorilla/mux" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" + "net/http" +) + +func InitReaction() { + l4g.Debug(utils.T("api.reaction.init.debug")) + + BaseRoutes.NeedPost.Handle("/reactions/save", ApiUserRequired(saveReaction)).Methods("POST") + BaseRoutes.NeedPost.Handle("/reactions/delete", ApiUserRequired(deleteReaction)).Methods("POST") + BaseRoutes.NeedPost.Handle("/reactions", ApiUserRequired(listReactions)).Methods("GET") +} + +func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) { + reaction := model.ReactionFromJson(r.Body) + if reaction == nil { + c.SetInvalidParam("saveReaction", "reaction") + 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 + } + + params := mux.Vars(r) + + channelId := params["channel_id"] + if len(channelId) != 26 { + c.SetInvalidParam("saveReaction", "channelId") + return + } + + if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { + return + } + + postId := params["post_id"] + if len(postId) != 26 || postId != reaction.PostId { + c.SetInvalidParam("saveReaction", "postId") + return + } + + pchan := Srv.Store.Post().Get(reaction.PostId) + + var postHadReactions bool + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else if post := result.Data.(*model.PostList).Posts[postId]; post.ChannelId != channelId { + c.Err = model.NewLocAppError("saveReaction", "api.reaction.save_reaction.mismatched_channel_id.app_error", + nil, "channelId="+channelId+", post.ChannelId="+post.ChannelId+", postId="+postId) + c.Err.StatusCode = http.StatusBadRequest + return + } else { + postHadReactions = post.HasReactions + } + + if result := <-Srv.Store.Reaction().Save(reaction); result.Err != nil { + c.Err = result.Err + return + } else { + go sendReactionEvent(model.WEBSOCKET_EVENT_REACTION_ADDED, channelId, reaction, postHadReactions) + + reaction := result.Data.(*model.Reaction) + + w.Write([]byte(reaction.ToJson())) + } +} + +func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) { + reaction := model.ReactionFromJson(r.Body) + if reaction == nil { + c.SetInvalidParam("deleteReaction", "reaction") + return + } + + if reaction.UserId != c.Session.UserId { + c.Err = model.NewLocAppError("deleteReaction", "api.reaction.delete_reaction.user_id.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + + params := mux.Vars(r) + + channelId := params["channel_id"] + if len(channelId) != 26 { + c.SetInvalidParam("deleteReaction", "channelId") + return + } + + if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { + return + } + + postId := params["post_id"] + if len(postId) != 26 || postId != reaction.PostId { + c.SetInvalidParam("deleteReaction", "postId") + return + } + + pchan := Srv.Store.Post().Get(reaction.PostId) + + var postHadReactions bool + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else if post := result.Data.(*model.PostList).Posts[postId]; post.ChannelId != channelId { + c.Err = model.NewLocAppError("deleteReaction", "api.reaction.delete_reaction.mismatched_channel_id.app_error", + nil, "channelId="+channelId+", post.ChannelId="+post.ChannelId+", postId="+postId) + c.Err.StatusCode = http.StatusBadRequest + return + } else { + postHadReactions = post.HasReactions + } + + if result := <-Srv.Store.Reaction().Delete(reaction); result.Err != nil { + c.Err = result.Err + return + } else { + go sendReactionEvent(model.WEBSOCKET_EVENT_REACTION_REMOVED, channelId, reaction, postHadReactions) + + ReturnStatusOK(w) + } +} + +func sendReactionEvent(event string, channelId string, reaction *model.Reaction, postHadReactions bool) { + // send out that a reaction has been added/removed + go func() { + message := model.NewWebSocketEvent(event, "", channelId, "", nil) + message.Add("reaction", reaction.ToJson()) + + Publish(message) + }() + + // send out that a post was updated if post.HasReactions has changed + go func() { + var post *model.Post + if result := <-Srv.Store.Post().Get(reaction.PostId); result.Err != nil { + l4g.Warn(utils.T("api.reaction.send_reaction_event.post.app_error")) + return + } else { + post = result.Data.(*model.PostList).Posts[reaction.PostId] + } + + if post.HasReactions != postHadReactions { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", channelId, "", nil) + message.Add("post", post.ToJson()) + + Publish(message) + } + }() +} + +func listReactions(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + channelId := params["channel_id"] + if len(channelId) != 26 { + c.SetInvalidParam("deletePost", "channelId") + return + } + + postId := params["post_id"] + if len(postId) != 26 { + c.SetInvalidParam("listReactions", "postId") + return + } + + pchan := Srv.Store.Post().Get(postId) + + if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) { + return + } + + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else if post := result.Data.(*model.PostList).Posts[postId]; post.ChannelId != channelId { + c.Err = model.NewLocAppError("listReactions", "api.reaction.list_reactions.mismatched_channel_id.app_error", + nil, "channelId="+channelId+", post.ChannelId="+post.ChannelId+", postId="+postId) + c.Err.StatusCode = http.StatusBadRequest + return + } + + if result := <-Srv.Store.Reaction().GetForPost(postId); result.Err != nil { + c.Err = result.Err + return + } else { + reactions := result.Data.([]*model.Reaction) + + w.Write([]byte(model.ReactionsToJson(reactions))) + } +} diff --git a/api/reaction_test.go b/api/reaction_test.go new file mode 100644 index 000000000..dad5a6a0c --- /dev/null +++ b/api/reaction_test.go @@ -0,0 +1,314 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "testing" + + "github.com/mattermost/platform/model" +) + +func TestSaveReaction(t *testing.T) { + th := Setup().InitBasic() + + Client := th.BasicClient + + user := th.BasicUser + user2 := th.BasicUser2 + + channel := th.BasicChannel + post := th.BasicPost + + // saving a reaction + reaction := &model.Reaction{ + UserId: user.Id, + PostId: post.Id, + EmojiName: "smile", + } + if returned, err := Client.SaveReaction(channel.Id, reaction); err != nil { + t.Fatal(err) + } else { + reaction = returned + } + + if reactions := Client.MustGeneric(Client.ListReactions(channel.Id, post.Id)).([]*model.Reaction); len(reactions) != 1 || *reactions[0] != *reaction { + t.Fatal("didn't save reaction correctly") + } + + // saving a duplicate reaction + if _, err := Client.SaveReaction(channel.Id, reaction); err != nil { + t.Fatal(err) + } + + // saving a second reaction on a post + reaction2 := &model.Reaction{ + UserId: user.Id, + PostId: post.Id, + EmojiName: "sad", + } + if returned, err := Client.SaveReaction(channel.Id, reaction2); err != nil { + t.Fatal(err) + } else { + reaction2 = returned + } + + if reactions := Client.MustGeneric(Client.ListReactions(channel.Id, post.Id)).([]*model.Reaction); len(reactions) != 2 || + (*reactions[0] != *reaction && *reactions[1] != *reaction) || (*reactions[0] != *reaction2 && *reactions[1] != *reaction2) { + t.Fatal("didn't save multiple reactions correctly") + } + + // saving a reaction without a user id + reaction3 := &model.Reaction{ + PostId: post.Id, + EmojiName: "smile", + } + if _, err := Client.SaveReaction(channel.Id, reaction3); err == nil { + t.Fatal("should've failed to save reaction without user id") + } + + // saving a reaction without a post id + reaction4 := &model.Reaction{ + UserId: user.Id, + EmojiName: "smile", + } + if _, err := Client.SaveReaction(channel.Id, reaction4); err == nil { + t.Fatal("should've failed to save reaction without post id") + } + + // saving a reaction without a emoji name + reaction5 := &model.Reaction{ + UserId: user.Id, + PostId: post.Id, + } + if _, err := Client.SaveReaction(channel.Id, reaction5); err == nil { + t.Fatal("should've failed to save reaction without emoji name") + } + + // saving a reaction for another user + reaction6 := &model.Reaction{ + UserId: user2.Id, + PostId: post.Id, + EmojiName: "smile", + } + if _, err := Client.SaveReaction(channel.Id, reaction6); err == nil { + t.Fatal("should've failed to save reaction for another user") + } + + // saving a reaction to a channel we're not a member of + th.LoginBasic2() + channel2 := th.CreateChannel(th.BasicClient, th.BasicTeam) + post2 := th.CreatePost(th.BasicClient, channel2) + th.LoginBasic() + + reaction7 := &model.Reaction{ + UserId: user.Id, + PostId: post2.Id, + EmojiName: "smile", + } + if _, err := Client.SaveReaction(channel2.Id, reaction7); err == nil { + t.Fatal("should've failed to save reaction to a channel we're not a member of") + } + + // saving a reaction to a direct channel + directChannel := Client.Must(Client.CreateDirectChannel(user2.Id)).Data.(*model.Channel) + directPost := th.CreatePost(th.BasicClient, directChannel) + + reaction8 := &model.Reaction{ + UserId: user.Id, + PostId: directPost.Id, + EmojiName: "smile", + } + if returned, err := Client.SaveReaction(directChannel.Id, reaction8); err != nil { + t.Fatal(err) + } else { + reaction8 = returned + } + + if reactions := Client.MustGeneric(Client.ListReactions(directChannel.Id, directPost.Id)).([]*model.Reaction); len(reactions) != 1 || *reactions[0] != *reaction8 { + t.Fatal("didn't save reaction correctly") + } + + // saving a reaction for a post in the wrong channel + reaction9 := &model.Reaction{ + UserId: user.Id, + PostId: directPost.Id, + EmojiName: "sad", + } + if _, err := Client.SaveReaction(channel.Id, reaction9); err == nil { + t.Fatal("should've failed to save reaction to a post that isn't in the given channel") + } +} + +func TestDeleteReaction(t *testing.T) { + th := Setup().InitBasic() + + Client := th.BasicClient + + user := th.BasicUser + user2 := th.BasicUser2 + + channel := th.BasicChannel + post := th.BasicPost + + reaction1 := &model.Reaction{ + UserId: user.Id, + PostId: post.Id, + EmojiName: "smile", + } + + // deleting a reaction that does exist + Client.MustGeneric(Client.SaveReaction(channel.Id, reaction1)) + if err := Client.DeleteReaction(channel.Id, reaction1); err != nil { + t.Fatal(err) + } + + if reactions := Client.MustGeneric(Client.ListReactions(channel.Id, post.Id)).([]*model.Reaction); len(reactions) != 0 { + t.Fatal("should've deleted reaction") + } + + // deleting one reaction when a post has multiple + reaction2 := &model.Reaction{ + UserId: user.Id, + PostId: post.Id, + EmojiName: "sad", + } + reaction1 = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction1)).(*model.Reaction) + reaction2 = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction2)).(*model.Reaction) + if err := Client.DeleteReaction(channel.Id, reaction2); err != nil { + t.Fatal(err) + } + + if reactions := Client.MustGeneric(Client.ListReactions(channel.Id, post.Id)).([]*model.Reaction); len(reactions) != 1 || *reactions[0] != *reaction1 { + t.Fatal("should've deleted only one reaction") + } + + // deleting a reaction made by another user + reaction3 := &model.Reaction{ + UserId: user2.Id, + PostId: post.Id, + EmojiName: "smile", + } + + th.LoginBasic2() + Client.Must(Client.JoinChannel(channel.Id)) + reaction3 = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction3)).(*model.Reaction) + + th.LoginBasic() + if err := Client.DeleteReaction(channel.Id, reaction3); err == nil { + t.Fatal("should've failed to delete another user's reaction") + } + + // deleting a reaction for a post we can't see + channel2 := th.CreateChannel(th.BasicClient, th.BasicTeam) + post2 := th.CreatePost(th.BasicClient, channel2) + + reaction4 := &model.Reaction{ + UserId: user.Id, + PostId: post2.Id, + EmojiName: "smile", + } + + reaction4 = Client.MustGeneric(Client.SaveReaction(channel2.Id, reaction4)).(*model.Reaction) + Client.Must(Client.LeaveChannel(channel2.Id)) + + if err := Client.DeleteReaction(channel2.Id, reaction4); err == nil { + t.Fatal("should've failed to delete a reaction from a channel we're not in") + } + + // deleting a reaction for a post with the wrong channel + channel3 := th.CreateChannel(th.BasicClient, th.BasicTeam) + + reaction5 := &model.Reaction{ + UserId: user.Id, + PostId: post.Id, + EmojiName: "happy", + } + if _, err := Client.SaveReaction(channel3.Id, reaction5); err == nil { + t.Fatal("should've failed to save reaction to a post that isn't in the given channel") + } +} + +func TestListReactions(t *testing.T) { + th := Setup().InitBasic() + + Client := th.BasicClient + + user := th.BasicUser + user2 := th.BasicUser2 + + channel := th.BasicChannel + + post := th.BasicPost + + userReactions := []*model.Reaction{ + { + UserId: user.Id, + PostId: post.Id, + EmojiName: "smile", + }, + { + UserId: user.Id, + PostId: post.Id, + EmojiName: "happy", + }, + { + UserId: user.Id, + PostId: post.Id, + EmojiName: "sad", + }, + } + + for i, reaction := range userReactions { + userReactions[i] = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction)).(*model.Reaction) + } + + th.LoginBasic2() + Client.Must(Client.JoinChannel(channel.Id)) + + userReactions2 := []*model.Reaction{ + { + UserId: user2.Id, + PostId: post.Id, + EmojiName: "smile", + }, + { + UserId: user2.Id, + PostId: post.Id, + EmojiName: "sad", + }, + } + + for i, reaction := range userReactions2 { + userReactions2[i] = Client.MustGeneric(Client.SaveReaction(channel.Id, reaction)).(*model.Reaction) + } + + if reactions, err := Client.ListReactions(channel.Id, post.Id); err != nil { + t.Fatal(err) + } else if len(reactions) != 5 { + t.Fatal("should've returned 5 reactions") + } else { + checkForReaction := func(expected *model.Reaction) { + found := false + + for _, reaction := range reactions { + if *reaction == *expected { + found = true + break + } + } + + if !found { + t.Fatalf("didn't return expected reaction %v", *expected) + } + } + + for _, reaction := range userReactions { + checkForReaction(reaction) + } + + for _, reaction := range userReactions2 { + checkForReaction(reaction) + } + } +} |