summaryrefslogtreecommitdiffstats
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/api.go1
-rw-r--r--api/emoji.go13
-rw-r--r--api/reaction.go203
-rw-r--r--api/reaction_test.go314
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)
+ }
+ }
+}