From b7d045844c4d00d8268da50f2388be1d12c91829 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Sat, 30 Jan 2016 13:44:39 -0500 Subject: [PATCH 01/10] Added serverside check for a post that @mentions someone who isn't in the channel --- api/post.go | 53 +++++++++++++++++++++++++++++++++++++ api/post_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ i18n/en.json | 12 +++++++++ i18n/es.json | 12 +++++++++ 4 files changed, 145 insertions(+) diff --git a/api/post.go b/api/post.go index e8345b5e5e..b72af7c48a 100644 --- a/api/post.go +++ b/api/post.go @@ -249,6 +249,7 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo } sendNotificationsAndForget(c, post, team, channel) + go checkForOutOfChannelMentions(post, channel) var user *model.User if result := <-uchan; result.Err != nil { @@ -728,6 +729,58 @@ func updateMentionCountAndForget(channelId, userId string) { }() } +func checkForOutOfChannelMentions(post *model.Post, channel *model.Channel) { + // don't check for out of channel mentions in direct channels + if channel.Type == model.CHANNEL_DIRECT { + return + } + + mentioned := getOutOfChannelMentions(post, channel.TeamId) + + // TODO come up with a way to alert the client of these + for _, user := range mentioned { + l4g.Debug("%v was mentioned and wasn't in the channel", user.Username) + } +} + +// Gets a list of users that were mentioned in a given post that aren't in the channel that the post was made in +func getOutOfChannelMentions(post *model.Post, teamId string) []*model.User { + pchan := Srv.Store.User().GetProfiles(teamId) + mchan := Srv.Store.Channel().GetMembers(post.ChannelId) + + var profiles map[string]*model.User + if result := <-pchan; result.Err != nil { + l4g.Error(utils.T("api.post.get_out_of_channel_mentions.retrieve_profiles.error"), teamId, result.Err) + return []*model.User{} + } else { + profiles = result.Data.(map[string]*model.User) + } + + // only keep profiles which aren't in the current channel + if result := <-mchan; result.Err != nil { + l4g.Error(utils.T("api.post.get_out_of_channel_mentions.retrieve_members.error"), post.ChannelId, result.Err) + return []*model.User{} + } else { + members := result.Data.([]model.ChannelMember) + + for _, member := range members { + delete(profiles, member.UserId) + } + } + + var mentioned []*model.User + + for _, profile := range profiles { + if pattern, err := regexp.Compile(`(\W|^)@` + regexp.QuoteMeta(profile.Username) + `(\W|$)`); err != nil { + l4g.Error(utils.T("api.post.get_out_of_channel_mentions.regex.error"), profile.Id, err) + } else if pattern.MatchString(post.Message) { + mentioned = append(mentioned, profile) + } + } + + return mentioned +} + func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { post := model.PostFromJson(r.Body) diff --git a/api/post_test.go b/api/post_test.go index 1a9fd25796..1b05bd39ff 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -857,3 +857,71 @@ func TestMakeDirectChannelVisible(t *testing.T) { t.Fatal("Failed to set direct channel to be visible for user2") } } + +func TestGetOutOfChannelMentions(t *testing.T) { + Setup() + + team1 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team1 = Client.Must(Client.CreateTeam(team1)).Data.(*model.Team) + + user1 := &model.User{TeamId: team1.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user1"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + user2 := &model.User{TeamId: team1.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user2"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + user3 := &model.User{TeamId: team1.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user3"} + user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user3.Id)) + + Client.Must(Client.LoginByEmail(team1.Name, user1.Email, "pwd")) + + channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team1.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + // test a post that doesn't @mention anybody + post1 := &model.Post{ChannelId: channel1.Id, Message: "user1 user2 user3"} + if mentioned := getOutOfChannelMentions(post1, team1.Id); len(mentioned) != 0 { + t.Fatalf("getOutOfChannelMentions returned %v when no users were mentioned", mentioned) + } + + // test a post that @mentions someone in the channel + post2 := &model.Post{ChannelId: channel1.Id, Message: "@user1 is user1"} + if mentioned := getOutOfChannelMentions(post2, team1.Id); len(mentioned) != 0 { + t.Fatalf("getOutOfChannelMentions returned %v when only users in the channel were mentioned", mentioned) + } + + // test a post that @mentions someone not in the channel + post3 := &model.Post{ChannelId: channel1.Id, Message: "@user2 and @user3 aren't in the channel"} + if mentioned := getOutOfChannelMentions(post3, team1.Id); len(mentioned) != 2 || (mentioned[0].Id != user2.Id && mentioned[0].Id != user3.Id) || (mentioned[1].Id != user2.Id && mentioned[1].Id != user3.Id) { + t.Fatalf("getOutOfChannelMentions returned %v when two users outside the channel were mentioned", mentioned) + } + + // test a post that @mentions someone not in the channel as well as someone in the channel + post4 := &model.Post{ChannelId: channel1.Id, Message: "@user2 and @user1 might be in the channel"} + if mentioned := getOutOfChannelMentions(post4, team1.Id); len(mentioned) != 1 || mentioned[0].Id != user2.Id { + t.Fatalf("getOutOfChannelMentions returned %v when someone in the channel and someone outside the channel were mentioned", mentioned) + } + + Client.Must(Client.Logout()) + + team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user4 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user4"} + user4 = Client.Must(Client.CreateUser(user4, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user4.Id)) + + Client.Must(Client.LoginByEmail(team2.Name, user4.Email, "pwd")) + + channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id} + channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + + // test a post that @mentions someone on a different team + post5 := &model.Post{ChannelId: channel2.Id, Message: "@user2 and @user3 might be in the channel"} + if mentioned := getOutOfChannelMentions(post5, team2.Id); len(mentioned) != 0 { + t.Fatalf("getOutOfChannelMentions returned %v when two users on a different team were mentioned", mentioned) + } +} diff --git a/i18n/en.json b/i18n/en.json index 2d86e1ee51..e620a1f0d6 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -683,6 +683,18 @@ "id": "api.post.delete_post.permissions.app_error", "translation": "You do not have the appropriate permissions" }, + { + "id": "api.post.get_out_of_channel_mentions.retrieve_members.error", + "translation": "Failed to get channel members channel_id=%v err=%v" + }, + { + "id": "api.post.get_out_of_channel_mentions.retrieve_profiles.error", + "translation": "Failed to retrieve user profiles team_id=%v, err=%v" + }, + { + "id": "api.post.get_out_of_channel_mentions.regex.error", + "translation": "Failed to compile @mention regex user_id=%v, err=%v" + }, { "id": "api.post.get_post.permissions.app_error", "translation": "You do not have the appropriate permissions" diff --git a/i18n/es.json b/i18n/es.json index 9599fe8791..328571f3bd 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -683,6 +683,18 @@ "id": "api.post.delete_post.permissions.app_error", "translation": "No tienes los permisos apropiados" }, + { + "id": "api.post.get_out_of_channel_mentions.retrieve_members.error", + "translation": "Falla al obtener los miembros del canal channel_id=%v err=%v" + }, + { + "id": "api.post.get_out_of_channel_mentions.retrieve_profiles.error", + "translation": "Falla al recuperar los perfiles de usuario team_id=%v, err=%v" + }, + { + "id": "api.post.get_out_of_channel_mentions.regex.error", + "translation": "Failed to compile @mention regex user_id=%v, err=%v" + }, { "id": "api.post.get_post.permissions.app_error", "translation": "No tienes los permisos apropiados" From db13b7a3ca3d3099392be557e47f838a7851a69a Mon Sep 17 00:00:00 2001 From: hmhealey Date: Mon, 1 Feb 2016 15:22:44 -0500 Subject: [PATCH 02/10] Changed deleted posts to be more general ephemeral posts --- web/react/components/delete_post_modal.jsx | 2 +- web/react/components/post_info.jsx | 25 +++++- web/react/dispatcher/event_helpers.jsx | 7 ++ web/react/stores/post_store.jsx | 91 ++++++---------------- web/react/utils/constants.jsx | 2 + web/sass-files/sass/partials/_post.scss | 9 ++- 6 files changed, 64 insertions(+), 72 deletions(-) diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 34fd724f57..9d7dcb3e56 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -88,7 +88,7 @@ export default class DeletePostModal extends React.Component { } } - PostStore.removePost(this.state.post.id, this.state.post.channel_id); + PostStore.deletePost(this.state.post); AsyncClient.getPosts(this.state.post.channel_id); }, (err) => { diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index ddb3935204..02150bd9de 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -23,13 +23,14 @@ export default class PostInfo extends React.Component { }; this.handlePermalinkCopy = this.handlePermalinkCopy.bind(this); + this.removePost = this.removePost.bind(this); } createDropdown() { var post = this.props.post; var isOwner = UserStore.getCurrentId() === post.user_id; var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles); - if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) { + if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.ephemeral) { return ''; } @@ -166,6 +167,25 @@ export default class PostInfo extends React.Component { this.setState({copiedLink: false}); } } + removePost() { + EventHelpers.emitRemovePost(this.props.post); + } + createRemovePostButton(post) { + if (!post.ephemeral) { + return null; + } + + return ( + + {'×'} + + ); + } render() { var post = this.props.post; var comments = ''; @@ -178,7 +198,7 @@ export default class PostInfo extends React.Component { commentCountText = ''; } - if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && post.state !== Constants.POST_DELETED) { + if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !post.ephemeral) { comments = ( {permalinkOverlay} + {this.createRemovePostButton(post)} ); diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx index 5eb319320d..cb41bd1bba 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/dispatcher/event_helpers.jsx @@ -180,3 +180,10 @@ export function emitPreferenceChangedEvent(preference) { preference }); } + +export function emitRemovePost(post) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.REMOVE_POST, + post + }); +} diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 08ffef8224..93e565403c 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -57,6 +57,7 @@ class PostStoreClass extends EventEmitter { this.clearFocusedPost = this.clearFocusedPost.bind(this); this.clearChannelVisibility = this.clearChannelVisibility.bind(this); + this.deletePost = this.deletePost.bind(this); this.removePost = this.removePost.bind(this); this.getPendingPosts = this.getPendingPosts.bind(this); @@ -65,10 +66,6 @@ class PostStoreClass extends EventEmitter { this.clearPendingPosts = this.clearPendingPosts.bind(this); this.updatePendingPost = this.updatePendingPost.bind(this); - this.storeUnseenDeletedPost = this.storeUnseenDeletedPost.bind(this); - this.getUnseenDeletedPosts = this.getUnseenDeletedPosts.bind(this); - this.clearUnseenDeletedPosts = this.clearUnseenDeletedPosts.bind(this); - // These functions are bad and work should be done to remove this system when the RHS dies this.storeSelectedPost = this.storeSelectedPost.bind(this); this.getSelectedPost = this.getSelectedPost.bind(this); @@ -211,28 +208,6 @@ class PostStoreClass extends EventEmitter { postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order); } - // Add deleted posts - if (this.postsInfo[id].hasOwnProperty('deletedPosts')) { - Object.assign(postList.posts, this.postsInfo[id].deletedPosts); - - for (const postID in this.postsInfo[id].deletedPosts) { - if (this.postsInfo[id].deletedPosts.hasOwnProperty(postID)) { - postList.order.push(postID); - } - } - - // Merge would be faster - postList.order.sort((a, b) => { - if (postList.posts[a].create_at > postList.posts[b].create_at) { - return -1; - } - if (postList.posts[a].create_at < postList.posts[b].create_at) { - return 1; - } - return 0; - }); - } - return postList; } @@ -286,15 +261,6 @@ class PostStoreClass extends EventEmitter { if (combinedPosts.order.indexOf(pid) === -1) { combinedPosts.order.push(pid); } - } else { - if (pid in combinedPosts.posts) { - Reflect.deleteProperty(combinedPosts.posts, pid); - } - - const index = combinedPosts.order.indexOf(pid); - if (index !== -1) { - combinedPosts.order.splice(index, 1); - } } } } @@ -365,6 +331,24 @@ class PostStoreClass extends EventEmitter { this.postsInfo[id].atBottom = atBottom; } + deletePost(post) { + const postList = this.postsInfo[post.channel_id].postList; + + if (isPostListNull(postList)) { + return; + } + + if (post.id in postList.posts) { + // make sure to copy the post so that component state changes work properly + postList.posts[post.id] = Object.assign({}, post, { + message: this.delete_message, // TODO + state: Constants.POST_DELETED, + filenames: [], + ephemeral: true + }); + } + } + removePost(post) { const channelId = post.channel_id; this.makePostsInfo(channelId); @@ -439,37 +423,6 @@ class PostStoreClass extends EventEmitter { this.emitChange(); } - storeUnseenDeletedPost(post) { - let posts = this.getUnseenDeletedPosts(post.channel_id); - - if (!posts) { - posts = {}; - } - - post.message = this.delete_message; - post.state = Constants.POST_DELETED; - post.filenames = []; - - posts[post.id] = post; - - this.makePostsInfo(post.channel_id); - this.postsInfo[post.channel_id].deletedPosts = posts; - } - - getUnseenDeletedPosts(channelId) { - if (this.postsInfo.hasOwnProperty(channelId)) { - return this.postsInfo[channelId].deletedPosts; - } - - return null; - } - - clearUnseenDeletedPosts(channelId) { - if (this.postsInfo.hasOwnProperty(channelId)) { - Reflect.deleteProperty(this.postsInfo[channelId], 'deletedPosts'); - } - } - storeSelectedPost(postList) { this.selectedPost = postList; } @@ -615,7 +568,6 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { case ActionTypes.CLICK_CHANNEL: PostStore.clearFocusedPost(); PostStore.clearChannelVisibility(action.id, true); - PostStore.clearUnseenDeletedPosts(action.prev); break; case ActionTypes.CREATE_POST: PostStore.storePendingPost(action.post); @@ -623,7 +575,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => { PostStore.jumpPostsViewToBottom(); break; case ActionTypes.POST_DELETED: - PostStore.storeUnseenDeletedPost(action.post); + PostStore.deletePost(action.post); + PostStore.emitChange(); + break; + case ActionTypes.REMOVE_POST: PostStore.removePost(action.post); PostStore.emitChange(); break; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 11a8da6694..7d103a2709 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -12,6 +12,7 @@ export default { LEAVE_CHANNEL: null, CREATE_POST: null, POST_DELETED: null, + REMOVE_POST: null, RECIEVED_CHANNELS: null, RECIEVED_CHANNEL: null, @@ -127,6 +128,7 @@ export default { POST_FAILED: 'failed', POST_DELETED: 'deleted', POST_TYPE_JOIN_LEAVE: 'system_join_leave', + POST_TYPE_EPHEMERAL: 'system_ephemeral', SYSTEM_MESSAGE_PREFIX: 'system_', SYSTEM_MESSAGE_PROFILE_NAME: 'System', SYSTEM_MESSAGE_PROFILE_IMAGE: '/static/images/logo_compact.png', diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 3f80e6664a..4a3415bb58 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -408,7 +408,7 @@ body.ios { @include legacy-pie-clearfix; &:hover { - .dropdown, .comment-icon__container, .post__reply { + .dropdown, .comment-icon__container, .post__reply, .post__remove { visibility: visible; } .permalink-icon { @@ -646,6 +646,13 @@ body.ios { } } + .post__remove { + display: inline-block; + visibility: hidden; + margin-right: 5px; + top: -1px; + } + .post__body { word-wrap: break-word; padding: 0.2em 0.5em 0em; From fcb92fa1b53a2b67323a881e7cb03965d3ec24d1 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 2 Feb 2016 10:52:18 -0500 Subject: [PATCH 03/10] Added ephemeral messages sent when a user mentions someone not in the channel --- api/post.go | 47 +++++++++++++++++++++++++++---- api/web_team_hub.go | 3 ++ i18n/en.json | 10 ++++++- i18n/es.json | 10 ++++++- model/message.go | 1 + model/post.go | 12 ++++---- web/react/stores/socket_store.jsx | 1 + web/react/utils/constants.jsx | 3 +- 8 files changed, 73 insertions(+), 14 deletions(-) diff --git a/api/post.go b/api/post.go index b72af7c48a..ee2b9a4b24 100644 --- a/api/post.go +++ b/api/post.go @@ -15,6 +15,7 @@ import ( "net/url" "path/filepath" "regexp" + "sort" "strconv" "strings" "time" @@ -249,7 +250,7 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo } sendNotificationsAndForget(c, post, team, channel) - go checkForOutOfChannelMentions(post, channel) + go checkForOutOfChannelMentions(c, post, channel) var user *model.User if result := <-uchan; result.Err != nil { @@ -729,18 +730,52 @@ func updateMentionCountAndForget(channelId, userId string) { }() } -func checkForOutOfChannelMentions(post *model.Post, channel *model.Channel) { +func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.Channel) { // don't check for out of channel mentions in direct channels if channel.Type == model.CHANNEL_DIRECT { return } mentioned := getOutOfChannelMentions(post, channel.TeamId) - - // TODO come up with a way to alert the client of these - for _, user := range mentioned { - l4g.Debug("%v was mentioned and wasn't in the channel", user.Username) + if len(mentioned) == 0 { + return } + + usernames := make([]string, len(mentioned)) + for i, user := range mentioned { + usernames[i] = user.Username + } + sort.Strings(usernames) + + var messageText string + if len(usernames) == 1 { + messageText = c.T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{ + "Username": usernames[0], + }) + } else { + messageText = c.T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{ + "Usernames": strings.Join(usernames[:len(usernames)-1], ", "), + "LastUsername": usernames[len(usernames)-1], + }) + } + + // create an ephemeral post that will be sent only to the sender of this original post and not stored in the DB + warningPost := model.Post{ + Id: model.NewId(), + ChannelId: post.ChannelId, + Message: messageText, + Type: model.POST_OUT_OF_CHANNEL_MENTION, + CreateAt: post.CreateAt + 1, + Ephemeral: true, + Props: model.StringInterface{}, + Filenames: []string{}, + } + + message := model.NewMessage(c.Session.TeamId, channel.Id, post.UserId, model.ACTION_EPHEMERAL_MESSAGE) + message.Add("post", warningPost.ToJson()) + message.Add("channel_type", channel.Type) + + PublishAndForget(message) } // Gets a list of users that were mentioned in a given post that aren't in the channel that the post was made in diff --git a/api/web_team_hub.go b/api/web_team_hub.go index 55300c8286..9d1c56f153 100644 --- a/api/web_team_hub.go +++ b/api/web_team_hub.go @@ -101,6 +101,9 @@ func ShouldSendEvent(webCon *WebConn, msg *model.Message) bool { return false } else if msg.Action == model.ACTION_PREFERENCE_CHANGED { return false + } else if msg.Action == model.ACTION_EPHEMERAL_MESSAGE { + // For now, ephemeral messages are sent directly to individual users + return false } // Only report events to a user who is the subject of the event, or is in the channel of the event diff --git a/i18n/en.json b/i18n/en.json index e620a1f0d6..8ffbc8b14e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -655,6 +655,14 @@ "id": "api.oauth.revoke_access_token.get.app_error", "translation": "Error getting access token from DB before deletion" }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.one", + "translation": "{{.Username}} was mentioned, but they do not belong to this channel." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.multiple", + "translation": "{{.Usernames}} and {{.LastUsername}} were mentioned, but they do not belong to this channel." + }, { "id": "api.post.create_post.bad_filename.error", "translation": "Bad filename discarded, filename=%v" @@ -3459,4 +3467,4 @@ "id": "web.watcher_fail.error", "translation": "Failed to add directory to watcher %v" } -] \ No newline at end of file +] diff --git a/i18n/es.json b/i18n/es.json index 328571f3bd..0dbcaeadf8 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -655,6 +655,14 @@ "id": "api.oauth.revoke_access_token.get.app_error", "translation": "Error obteniendo el token de acceso desde la BD antes de ser eliminado" }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.one", + "translation": "{ .Username } was mentioned, but they do not belong to this channel." + }, + { + "id": "api.post.check_for_out_of_channel_mentions.message.multiple", + "translation": "{ .Usernames } and { .LastUsername} were mentioned, but they do not belong to this channel." + }, { "id": "api.post.create_post.bad_filename.error", "translation": "Nombre errado de archivo descartado, archivo=%v" @@ -3459,4 +3467,4 @@ "id": "web.watcher_fail.error", "translation": "Falla al agregar el directorio a ser vigilado %v" } -] \ No newline at end of file +] diff --git a/model/message.go b/model/message.go index 1cb350bbfb..cce0ec094c 100644 --- a/model/message.go +++ b/model/message.go @@ -18,6 +18,7 @@ const ( ACTION_USER_ADDED = "user_added" ACTION_USER_REMOVED = "user_removed" ACTION_PREFERENCE_CHANGED = "preference_changed" + ACTION_EPHEMERAL_MESSAGE = "ephemeral_message" ) type Message struct { diff --git a/model/post.go b/model/post.go index f9f5a4d1c0..cc9b0bda40 100644 --- a/model/post.go +++ b/model/post.go @@ -10,11 +10,12 @@ import ( ) const ( - POST_SYSTEM_MESSAGE_PREFIX = "system_" - POST_DEFAULT = "" - POST_SLACK_ATTACHMENT = "slack_attachment" - POST_JOIN_LEAVE = "system_join_leave" - POST_HEADER_CHANGE = "system_header_change" + POST_SYSTEM_MESSAGE_PREFIX = "system_" + POST_DEFAULT = "" + POST_SLACK_ATTACHMENT = "slack_attachment" + POST_JOIN_LEAVE = "system_join_leave" + POST_HEADER_CHANGE = "system_header_change" + POST_OUT_OF_CHANNEL_MENTION = "system_out_of_channel_mention" ) type Post struct { @@ -33,6 +34,7 @@ type Post struct { Hashtags string `json:"hashtags"` Filenames StringArray `json:"filenames"` PendingPostId string `json:"pending_post_id" db:"-"` + Ephemeral bool `json:"ephemeral" db:"-"` } func (o *Post) ToJson() string { diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 744c2c8e56..745b943131 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -109,6 +109,7 @@ class SocketStoreClass extends EventEmitter { handleMessage(msg) { switch (msg.action) { case SocketEvents.POSTED: + case SocketEvents.EPHEMERAL_MESSAGE: handleNewPostEvent(msg, this.translations); break; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 7d103a2709..cfb33a79c7 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -79,7 +79,8 @@ export default { USER_ADDED: 'user_added', USER_REMOVED: 'user_removed', TYPING: 'typing', - PREFERENCE_CHANGED: 'preference_changed' + PREFERENCE_CHANGED: 'preference_changed', + EPHEMERAL_MESSAGE: 'ephemeral_message' }, //SPECIAL_MENTIONS: ['all', 'channel'], From 0e44860d9bc0dd3acad4f30f907bf5d63225c793 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 2 Feb 2016 10:53:05 -0500 Subject: [PATCH 04/10] Removed untranslated phrases from es.json --- i18n/es.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/i18n/es.json b/i18n/es.json index 0dbcaeadf8..50adbe092a 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -655,14 +655,6 @@ "id": "api.oauth.revoke_access_token.get.app_error", "translation": "Error obteniendo el token de acceso desde la BD antes de ser eliminado" }, - { - "id": "api.post.check_for_out_of_channel_mentions.message.one", - "translation": "{ .Username } was mentioned, but they do not belong to this channel." - }, - { - "id": "api.post.check_for_out_of_channel_mentions.message.multiple", - "translation": "{ .Usernames } and { .LastUsername} were mentioned, but they do not belong to this channel." - }, { "id": "api.post.create_post.bad_filename.error", "translation": "Nombre errado de archivo descartado, archivo=%v" @@ -699,10 +691,6 @@ "id": "api.post.get_out_of_channel_mentions.retrieve_profiles.error", "translation": "Falla al recuperar los perfiles de usuario team_id=%v, err=%v" }, - { - "id": "api.post.get_out_of_channel_mentions.regex.error", - "translation": "Failed to compile @mention regex user_id=%v, err=%v" - }, { "id": "api.post.get_post.permissions.app_error", "translation": "No tienes los permisos apropiados" From f8005744a26b7196f8b652e47bd7c094193dbb30 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 2 Feb 2016 11:02:13 -0500 Subject: [PATCH 05/10] Changed out of channel mention tests to use proper email addresses --- api/post_test.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/api/post_test.go b/api/post_test.go index 1b05bd39ff..86e9292d66 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -8,6 +8,7 @@ import ( "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "net/http" + "strings" "testing" "time" ) @@ -861,18 +862,18 @@ func TestMakeDirectChannelVisible(t *testing.T) { func TestGetOutOfChannelMentions(t *testing.T) { Setup() - team1 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team1 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Type: model.TEAM_OPEN} team1 = Client.Must(Client.CreateTeam(team1)).Data.(*model.Team) - user1 := &model.User{TeamId: team1.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user1"} + user1 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user1"} user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user1.Id)) - user2 := &model.User{TeamId: team1.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user2"} + user2 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user2"} user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user2.Id)) - user3 := &model.User{TeamId: team1.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user3"} + user3 := &model.User{TeamId: team1.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user3"} user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user3.Id)) @@ -907,10 +908,10 @@ func TestGetOutOfChannelMentions(t *testing.T) { Client.Must(Client.Logout()) - team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Type: model.TEAM_OPEN} team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) - user4 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user4"} + user4 := &model.User{TeamId: team2.Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd", Username: "user4"} user4 = Client.Must(Client.CreateUser(user4, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user4.Id)) From fd123a6e4a7cfd19ff7ddc7141bc928c27e0a890 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 2 Feb 2016 15:34:22 -0500 Subject: [PATCH 06/10] Changed PostStore to not clear a deleted post's message --- web/react/components/create_post.jsx | 7 +------ web/react/components/post_body.jsx | 30 ++++++++++++++++++++-------- web/react/stores/post_store.jsx | 4 ---- web/static/i18n/en.json | 4 ++-- web/static/i18n/es.json | 4 ++-- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index ed672cd347..3dfc919704 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -40,10 +40,6 @@ const holders = defineMessages({ write: { id: 'create_post.write', defaultMessage: 'Write a message...' - }, - deleteMsg: { - id: 'create_post.deleteMsg', - defaultMessage: '(message deleted)' } }); @@ -70,7 +66,6 @@ class CreatePost extends React.Component { this.sendMessage = this.sendMessage.bind(this); PostStore.clearDraftUploads(); - PostStore.deleteMessage(this.props.intl.formatMessage(holders.deleteMsg)); const draft = this.getCurrentDraft(); @@ -511,4 +506,4 @@ CreatePost.propTypes = { intl: intlShape.isRequired }; -export default injectIntl(CreatePost); \ No newline at end of file +export default injectIntl(CreatePost); diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 16f8528b20..d71ac6ec76 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -44,7 +44,6 @@ class PostBody extends React.Component { this.state = { links: linkData.links, - message: linkData.text, post: this.props.post, hasUserProfiles: profiles && Object.keys(profiles).length > 1 }; @@ -106,7 +105,9 @@ class PostBody extends React.Component { if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) { this.embed = this.createEmbed(linkData.links[0]); } - this.setState({links: linkData.links, message: linkData.text}); + this.setState({ + links: linkData.links + }); } createEmbed(link) { @@ -310,6 +311,23 @@ class PostBody extends React.Component { ); } + let message; + if (this.props.post.state === Constants.POST_DELETED) { + message = ( + + ); + } else { + message = ( + + ); + } + return (
{comment} @@ -320,11 +338,7 @@ class PostBody extends React.Component { className={postClass} > {loading} - + {message}
Sending Messages

Type here to write a message and press Enter to post it.

Click the Attachment button to upload an image or a file.

", "delete_channel.channel": "channel", "delete_channel.group": "group", @@ -768,6 +767,7 @@ "members_popover.title": "Members", "post_attachment.collapse": "▲ collapse text", "post_attachment.more": "▼ read more", + "post_body.deleted": "(message deleted)", "post_body.plusOne": " plus 1 other file", "post_body.plusMore": " plus {count} other files", "post_body.commentedOn": "Commented on {name}{apostrophe} message: ", @@ -1220,4 +1220,4 @@ "intro_messages.beginning": "Beginning of {name}", "intro_messages.invite": "Invite others to this {type}", "intro_messages.setHeader": "Set a Header" -} \ No newline at end of file +} diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index 981e1f06f3..98cb2db664 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -589,7 +589,6 @@ "create_comment.file": "Subiendo archivo", "create_comment.files": "Subiendo archivos", "create_post.comment": "Comentario", - "create_post.deleteMsg": "(mensaje eliminado)", "create_post.post": "Mensaje", "create_post.tutorialTip": "

Enviar Mensajes

Escribe aquí para redactar un mensaje y presiona Retorno para enviarlo.

Pincha el botón de Adjuntar para subir una imagen o archivo.

", "create_post.write": "Escribe un mensaje...", @@ -809,6 +808,7 @@ "post_attachment.collapse": "▲ colapsar texto", "post_attachment.more": "▼ leer más", "post_body.commentedOn": "Comentó el mensaje de {name}{apostrophe}: ", + "post_body.deleted": "(mensaje eliminado)", "post_body.plusMore": " más {count} otros archivos", "post_body.plusOne": " más 1 archivo", "post_body.retry": "Reintentar", @@ -1220,4 +1220,4 @@ "view_image_popover.download": "Descargar", "view_image_popover.file": "Archivo {count} de {total}", "view_image_popover.publicLink": "Obtener Enlace Público" -} \ No newline at end of file +} From 994358c31a93296a225f7d34942bbedfeac025c4 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Wed, 3 Feb 2016 10:56:47 -0500 Subject: [PATCH 07/10] Reworked the code for sending notifications and checking for out of channel mentions to share some data --- api/post.go | 478 +++++++++++++++++++++++------------------------ api/post_test.go | 36 +++- i18n/en.json | 24 +-- i18n/es.json | 24 +-- 4 files changed, 280 insertions(+), 282 deletions(-) diff --git a/api/post.go b/api/post.go index ee2b9a4b24..0f0a83639e 100644 --- a/api/post.go +++ b/api/post.go @@ -232,6 +232,8 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo tchan := Srv.Store.Team().Get(c.Session.TeamId) cchan := Srv.Store.Channel().Get(post.ChannelId) uchan := Srv.Store.User().Get(post.UserId) + pchan := Srv.Store.User().GetProfiles(c.Session.TeamId) + mchan := Srv.Store.Channel().GetMembers(post.ChannelId) var team *model.Team if result := <-tchan; result.Err != nil { @@ -249,8 +251,24 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo channel = result.Data.(*model.Channel) } - sendNotificationsAndForget(c, post, team, channel) - go checkForOutOfChannelMentions(c, post, channel) + var profiles map[string]*model.User + if result := <-pchan; result.Err != nil { + l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.Session.TeamId, result.Err) + return + } else { + profiles = result.Data.(map[string]*model.User) + } + + var members []model.ChannelMember + if result := <-mchan; result.Err != nil { + l4g.Error(utils.T("api.post.handle_post_events_and_forget.members.error"), post.ChannelId, result.Err) + return + } else { + members = result.Data.([]model.ChannelMember) + } + + sendNotificationsAndForget(c, post, team, channel, profiles, members) + go checkForOutOfChannelMentions(c, post, channel, profiles, members) var user *model.User if result := <-uchan; result.Err != nil { @@ -415,281 +433,263 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team } -func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel) { +func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel, profileMap map[string]*model.User, members []model.ChannelMember) { go func() { - // Get a list of user names (to be used as keywords) and ids for the given team - uchan := Srv.Store.User().GetProfiles(c.Session.TeamId) - echan := Srv.Store.Channel().GetMembers(post.ChannelId) - var channelName string var bodyText string var subjectText string var mentionedUsers []string - if result := <-uchan; result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.retrive_profiles.error"), c.Session.TeamId, result.Err) + if _, ok := profileMap[post.UserId]; !ok { + l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) return - } else { - profileMap := result.Data.(map[string]*model.User) + } + senderName := profileMap[post.UserId].Username - if _, ok := profileMap[post.UserId]; !ok { - l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) - return - } - senderName := profileMap[post.UserId].Username + toEmailMap := make(map[string]bool) - toEmailMap := make(map[string]bool) - - if channel.Type == model.CHANNEL_DIRECT { - - var otherUserId string - if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { - otherUserId = userIds[1] - channelName = profileMap[userIds[1]].Username - } else { - otherUserId = userIds[0] - channelName = profileMap[userIds[0]].Username - } - - otherUser := profileMap[otherUserId] - sendEmail := true - if _, ok := otherUser.NotifyProps["email"]; ok && otherUser.NotifyProps["email"] == "false" { - sendEmail = false - } - if sendEmail && (otherUser.IsOffline() || otherUser.IsAway()) { - toEmailMap[otherUserId] = true - } + if channel.Type == model.CHANNEL_DIRECT { + var otherUserId string + if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { + otherUserId = userIds[1] + channelName = profileMap[userIds[1]].Username } else { + otherUserId = userIds[0] + channelName = profileMap[userIds[0]].Username + } - // Find out who is a member of the channel, only keep those profiles - if eResult := <-echan; eResult.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.members.error"), post.ChannelId, eResult.Err.Message) - return - } else { - tempProfileMap := make(map[string]*model.User) - members := eResult.Data.([]model.ChannelMember) - for _, member := range members { - tempProfileMap[member.UserId] = profileMap[member.UserId] - } + otherUser := profileMap[otherUserId] + sendEmail := true + if _, ok := otherUser.NotifyProps["email"]; ok && otherUser.NotifyProps["email"] == "false" { + sendEmail = false + } + if sendEmail && (otherUser.IsOffline() || otherUser.IsAway()) { + toEmailMap[otherUserId] = true + } - profileMap = tempProfileMap - } + } else { + // Find out who is a member of the channel, only keep those profiles + tempProfileMap := make(map[string]*model.User) + for _, member := range members { + tempProfileMap[member.UserId] = profileMap[member.UserId] + } - // Build map for keywords - keywordMap := make(map[string][]string) - for _, profile := range profileMap { - if len(profile.NotifyProps["mention_keys"]) > 0 { + profileMap = tempProfileMap - // Add all the user's mention keys - splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") - for _, k := range splitKeys { - keywordMap[k] = append(keywordMap[strings.ToLower(k)], profile.Id) - } - } + // Build map for keywords + keywordMap := make(map[string][]string) + for _, profile := range profileMap { + if len(profile.NotifyProps["mention_keys"]) > 0 { - // If turned on, add the user's case sensitive first name - if profile.NotifyProps["first_name"] == "true" { - keywordMap[profile.FirstName] = append(keywordMap[profile.FirstName], profile.Id) - } - - // Add @all to keywords if user has them turned on - // if profile.NotifyProps["all"] == "true" { - // keywordMap["@all"] = append(keywordMap["@all"], profile.Id) - // } - - // Add @channel to keywords if user has them turned on - if profile.NotifyProps["channel"] == "true" { - keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id) + // Add all the user's mention keys + splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") + for _, k := range splitKeys { + keywordMap[k] = append(keywordMap[strings.ToLower(k)], profile.Id) } } - // Build a map as a list of unique user_ids that are mentioned in this post - splitF := func(c rune) bool { - return model.SplitRunes[c] - } - splitMessage := strings.Fields(post.Message) - for _, word := range splitMessage { - var userIds []string - - // Non-case-sensitive check for regular keys - if ids, match := keywordMap[strings.ToLower(word)]; match { - userIds = append(userIds, ids...) - } - - // Case-sensitive check for first name - if ids, match := keywordMap[word]; match { - userIds = append(userIds, ids...) - } - - if len(userIds) == 0 { - // No matches were found with the string split just on whitespace so try further splitting - // the message on punctuation - splitWords := strings.FieldsFunc(word, splitF) - - for _, splitWord := range splitWords { - // Non-case-sensitive check for regular keys - if ids, match := keywordMap[strings.ToLower(splitWord)]; match { - userIds = append(userIds, ids...) - } - - // Case-sensitive check for first name - if ids, match := keywordMap[splitWord]; match { - userIds = append(userIds, ids...) - } - } - } - - for _, userId := range userIds { - if post.UserId == userId { - continue - } - sendEmail := true - if _, ok := profileMap[userId].NotifyProps["email"]; ok && profileMap[userId].NotifyProps["email"] == "false" { - sendEmail = false - } - if sendEmail && (profileMap[userId].IsAway() || profileMap[userId].IsOffline()) { - toEmailMap[userId] = true - } else { - toEmailMap[userId] = false - } - } + // If turned on, add the user's case sensitive first name + if profile.NotifyProps["first_name"] == "true" { + keywordMap[profile.FirstName] = append(keywordMap[profile.FirstName], profile.Id) } - for id := range toEmailMap { - updateMentionCountAndForget(post.ChannelId, id) + // Add @all to keywords if user has them turned on + // if profile.NotifyProps["all"] == "true" { + // keywordMap["@all"] = append(keywordMap["@all"], profile.Id) + // } + + // Add @channel to keywords if user has them turned on + if profile.NotifyProps["channel"] == "true" { + keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id) } } - if len(toEmailMap) != 0 { - mentionedUsers = make([]string, 0, len(toEmailMap)) - for k := range toEmailMap { - mentionedUsers = append(mentionedUsers, k) + // Build a map as a list of unique user_ids that are mentioned in this post + splitF := func(c rune) bool { + return model.SplitRunes[c] + } + splitMessage := strings.Fields(post.Message) + for _, word := range splitMessage { + var userIds []string + + // Non-case-sensitive check for regular keys + if ids, match := keywordMap[strings.ToLower(word)]; match { + userIds = append(userIds, ids...) } - teamURL := c.GetSiteURL() + "/" + team.Name + // Case-sensitive check for first name + if ids, match := keywordMap[word]; match { + userIds = append(userIds, ids...) + } - // Build and send the emails - tm := time.Unix(post.CreateAt/1000, 0) + if len(userIds) == 0 { + // No matches were found with the string split just on whitespace so try further splitting + // the message on punctuation + splitWords := strings.FieldsFunc(word, splitF) - for id, doSend := range toEmailMap { + for _, splitWord := range splitWords { + // Non-case-sensitive check for regular keys + if ids, match := keywordMap[strings.ToLower(splitWord)]; match { + userIds = append(userIds, ids...) + } - if !doSend { + // Case-sensitive check for first name + if ids, match := keywordMap[splitWord]; match { + userIds = append(userIds, ids...) + } + } + } + + for _, userId := range userIds { + if post.UserId == userId { continue } - - // skip if inactive - if profileMap[id].DeleteAt > 0 { - continue + sendEmail := true + if _, ok := profileMap[userId].NotifyProps["email"]; ok && profileMap[userId].NotifyProps["email"] == "false" { + sendEmail = false } - - userLocale := utils.GetUserTranslations(profileMap[id].Locale) - - if channel.Type == model.CHANNEL_DIRECT { - bodyText = userLocale("api.post.send_notifications_and_forget.message_body") - subjectText = userLocale("api.post.send_notifications_and_forget.message_subject") + if sendEmail && (profileMap[userId].IsAway() || profileMap[userId].IsOffline()) { + toEmailMap[userId] = true } else { - bodyText = userLocale("api.post.send_notifications_and_forget.mention_body") - subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject") - channelName = channel.DisplayName + toEmailMap[userId] = false } + } + } - month := userLocale(tm.Month().String()) - day := fmt.Sprintf("%d", tm.Day()) - year := fmt.Sprintf("%d", tm.Year()) - zone, _ := tm.Zone() + for id := range toEmailMap { + updateMentionCountAndForget(post.ChannelId, id) + } + } - subjectPage := NewServerTemplatePage("post_subject", c.Locale) - subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", - map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, - "Month": month[:3], "Day": day, "Year": year}) + if len(toEmailMap) != 0 { + mentionedUsers = make([]string, 0, len(toEmailMap)) + for k := range toEmailMap { + mentionedUsers = append(mentionedUsers, k) + } - bodyPage := NewServerTemplatePage("post_body", c.Locale) - bodyPage.Props["SiteURL"] = c.GetSiteURL() - bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) - bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name - bodyPage.Props["BodyText"] = bodyText - bodyPage.Props["Button"] = userLocale("api.templates.post_body.button") - bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info", - map[string]interface{}{"ChannelName": channelName, "SenderName": senderName, - "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()), - "TimeZone": zone, "Month": month, "Day": day})) + teamURL := c.GetSiteURL() + "/" + team.Name - // attempt to fill in a message body if the post doesn't have any text - if len(strings.TrimSpace(bodyPage.Props["PostMessage"])) == 0 && len(post.Filenames) > 0 { - // extract the filenames from their paths and determine what type of files are attached - filenames := make([]string, len(post.Filenames)) - onlyImages := true - for i, filename := range post.Filenames { - var err error - if filenames[i], err = url.QueryUnescape(filepath.Base(filename)); err != nil { - // this should never error since filepath was escaped using url.QueryEscape - filenames[i] = filepath.Base(filename) - } + // Build and send the emails + tm := time.Unix(post.CreateAt/1000, 0) - ext := filepath.Ext(filename) - onlyImages = onlyImages && model.IsFileExtImage(ext) - } - filenamesString := strings.Join(filenames, ", ") + for id, doSend := range toEmailMap { - var attachmentPrefix string - if onlyImages { - attachmentPrefix = "Image" - } else { - attachmentPrefix = "File" - } - if len(post.Filenames) > 1 { - attachmentPrefix += "s" + if !doSend { + continue + } + + // skip if inactive + if profileMap[id].DeleteAt > 0 { + continue + } + + userLocale := utils.GetUserTranslations(profileMap[id].Locale) + + if channel.Type == model.CHANNEL_DIRECT { + bodyText = userLocale("api.post.send_notifications_and_forget.message_body") + subjectText = userLocale("api.post.send_notifications_and_forget.message_subject") + } else { + bodyText = userLocale("api.post.send_notifications_and_forget.mention_body") + subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject") + channelName = channel.DisplayName + } + + month := userLocale(tm.Month().String()) + day := fmt.Sprintf("%d", tm.Day()) + year := fmt.Sprintf("%d", tm.Year()) + zone, _ := tm.Zone() + + subjectPage := NewServerTemplatePage("post_subject", c.Locale) + subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", + map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, + "Month": month[:3], "Day": day, "Year": year}) + + bodyPage := NewServerTemplatePage("post_body", c.Locale) + bodyPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) + bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name + bodyPage.Props["BodyText"] = bodyText + bodyPage.Props["Button"] = userLocale("api.templates.post_body.button") + bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info", + map[string]interface{}{"ChannelName": channelName, "SenderName": senderName, + "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()), + "TimeZone": zone, "Month": month, "Day": day})) + + // attempt to fill in a message body if the post doesn't have any text + if len(strings.TrimSpace(bodyPage.Props["PostMessage"])) == 0 && len(post.Filenames) > 0 { + // extract the filenames from their paths and determine what type of files are attached + filenames := make([]string, len(post.Filenames)) + onlyImages := true + for i, filename := range post.Filenames { + var err error + if filenames[i], err = url.QueryUnescape(filepath.Base(filename)); err != nil { + // this should never error since filepath was escaped using url.QueryEscape + filenames[i] = filepath.Base(filename) } - bodyPage.Props["PostMessage"] = userLocale("api.post.send_notifications_and_forget.sent", - map[string]interface{}{"Prefix": attachmentPrefix, "Filenames": filenamesString}) + ext := filepath.Ext(filename) + onlyImages = onlyImages && model.IsFileExtImage(ext) + } + filenamesString := strings.Join(filenames, ", ") + + var attachmentPrefix string + if onlyImages { + attachmentPrefix = "Image" + } else { + attachmentPrefix = "File" + } + if len(post.Filenames) > 1 { + attachmentPrefix += "s" } - if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), profileMap[id].Email, err) - } + bodyPage.Props["PostMessage"] = userLocale("api.post.send_notifications_and_forget.sent", + map[string]interface{}{"Prefix": attachmentPrefix, "Filenames": filenamesString}) + } - if *utils.Cfg.EmailSettings.SendPushNotifications { - sessionChan := Srv.Store.Session().GetSessions(id) - if result := <-sessionChan; result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), id, result.Err) - } else { - sessions := result.Data.([]*model.Session) - alreadySeen := make(map[string]string) + if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), profileMap[id].Email, err) + } - for _, session := range sessions { - if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && - (strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")) { - alreadySeen[session.DeviceId] = session.DeviceId + if *utils.Cfg.EmailSettings.SendPushNotifications { + sessionChan := Srv.Store.Session().GetSessions(id) + if result := <-sessionChan; result.Err != nil { + l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), id, result.Err) + } else { + sessions := result.Data.([]*model.Session) + alreadySeen := make(map[string]string) - msg := model.PushNotification{} - msg.Badge = 1 - msg.ServerId = utils.CfgDiagnosticId + for _, session := range sessions { + if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && + (strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")) { + alreadySeen[session.DeviceId] = session.DeviceId - if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") { - msg.Platform = model.PUSH_NOTIFY_APPLE - msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") - } else if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") { - msg.Platform = model.PUSH_NOTIFY_ANDROID - msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") - } + msg := model.PushNotification{} + msg.Badge = 1 + msg.ServerId = utils.CfgDiagnosticId - if channel.Type == model.CHANNEL_DIRECT { - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") - } else { - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName - } + if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") { + msg.Platform = model.PUSH_NOTIFY_APPLE + msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") + } else if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") { + msg.Platform = model.PUSH_NOTIFY_ANDROID + msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") + } - httpClient := http.Client{} - request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson())) + if channel.Type == model.CHANNEL_DIRECT { + msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") + } else { + msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName + } - l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) - if _, err := httpClient.Do(request); err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), id, err) - } + httpClient := http.Client{} + request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson())) + + l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) + if _, err := httpClient.Do(request); err != nil { + l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), id, err) } } } @@ -730,13 +730,13 @@ func updateMentionCountAndForget(channelId, userId string) { }() } -func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.Channel) { +func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.Channel, allProfiles map[string]*model.User, members []model.ChannelMember) { // don't check for out of channel mentions in direct channels if channel.Type == model.CHANNEL_DIRECT { return } - mentioned := getOutOfChannelMentions(post, channel.TeamId) + mentioned := getOutOfChannelMentions(post, allProfiles, members) if len(mentioned) == 0 { return } @@ -779,28 +779,16 @@ func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.C } // Gets a list of users that were mentioned in a given post that aren't in the channel that the post was made in -func getOutOfChannelMentions(post *model.Post, teamId string) []*model.User { - pchan := Srv.Store.User().GetProfiles(teamId) - mchan := Srv.Store.Channel().GetMembers(post.ChannelId) - - var profiles map[string]*model.User - if result := <-pchan; result.Err != nil { - l4g.Error(utils.T("api.post.get_out_of_channel_mentions.retrieve_profiles.error"), teamId, result.Err) - return []*model.User{} - } else { - profiles = result.Data.(map[string]*model.User) +func getOutOfChannelMentions(post *model.Post, allProfiles map[string]*model.User, members []model.ChannelMember) []*model.User { + // copy the profiles map since we'll be removing items from it + profiles := make(map[string]*model.User) + for id, profile := range allProfiles { + profiles[id] = profile } // only keep profiles which aren't in the current channel - if result := <-mchan; result.Err != nil { - l4g.Error(utils.T("api.post.get_out_of_channel_mentions.retrieve_members.error"), post.ChannelId, result.Err) - return []*model.User{} - } else { - members := result.Data.([]model.ChannelMember) - - for _, member := range members { - delete(profiles, member.UserId) - } + for _, member := range members { + delete(profiles, member.UserId) } var mentioned []*model.User diff --git a/api/post_test.go b/api/post_test.go index 86e9292d66..0270437668 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -882,27 +882,41 @@ func TestGetOutOfChannelMentions(t *testing.T) { channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team1.Id} channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + var allProfiles map[string]*model.User + if result := <-Srv.Store.User().GetProfiles(team1.Id); result.Err != nil { + t.Fatal(result.Err) + } else { + allProfiles = result.Data.(map[string]*model.User) + } + + var members []model.ChannelMember + if result := <-Srv.Store.Channel().GetMembers(channel1.Id); result.Err != nil { + t.Fatal(result.Err) + } else { + members = result.Data.([]model.ChannelMember) + } + // test a post that doesn't @mention anybody post1 := &model.Post{ChannelId: channel1.Id, Message: "user1 user2 user3"} - if mentioned := getOutOfChannelMentions(post1, team1.Id); len(mentioned) != 0 { + if mentioned := getOutOfChannelMentions(post1, allProfiles, members); len(mentioned) != 0 { t.Fatalf("getOutOfChannelMentions returned %v when no users were mentioned", mentioned) } // test a post that @mentions someone in the channel post2 := &model.Post{ChannelId: channel1.Id, Message: "@user1 is user1"} - if mentioned := getOutOfChannelMentions(post2, team1.Id); len(mentioned) != 0 { + if mentioned := getOutOfChannelMentions(post2, allProfiles, members); len(mentioned) != 0 { t.Fatalf("getOutOfChannelMentions returned %v when only users in the channel were mentioned", mentioned) } // test a post that @mentions someone not in the channel post3 := &model.Post{ChannelId: channel1.Id, Message: "@user2 and @user3 aren't in the channel"} - if mentioned := getOutOfChannelMentions(post3, team1.Id); len(mentioned) != 2 || (mentioned[0].Id != user2.Id && mentioned[0].Id != user3.Id) || (mentioned[1].Id != user2.Id && mentioned[1].Id != user3.Id) { + if mentioned := getOutOfChannelMentions(post3, allProfiles, members); len(mentioned) != 2 || (mentioned[0].Id != user2.Id && mentioned[0].Id != user3.Id) || (mentioned[1].Id != user2.Id && mentioned[1].Id != user3.Id) { t.Fatalf("getOutOfChannelMentions returned %v when two users outside the channel were mentioned", mentioned) } // test a post that @mentions someone not in the channel as well as someone in the channel post4 := &model.Post{ChannelId: channel1.Id, Message: "@user2 and @user1 might be in the channel"} - if mentioned := getOutOfChannelMentions(post4, team1.Id); len(mentioned) != 1 || mentioned[0].Id != user2.Id { + if mentioned := getOutOfChannelMentions(post4, allProfiles, members); len(mentioned) != 1 || mentioned[0].Id != user2.Id { t.Fatalf("getOutOfChannelMentions returned %v when someone in the channel and someone outside the channel were mentioned", mentioned) } @@ -920,9 +934,21 @@ func TestGetOutOfChannelMentions(t *testing.T) { channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team2.Id} channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) + if result := <-Srv.Store.User().GetProfiles(team2.Id); result.Err != nil { + t.Fatal(result.Err) + } else { + allProfiles = result.Data.(map[string]*model.User) + } + + if result := <-Srv.Store.Channel().GetMembers(channel2.Id); result.Err != nil { + t.Fatal(result.Err) + } else { + members = result.Data.([]model.ChannelMember) + } + // test a post that @mentions someone on a different team post5 := &model.Post{ChannelId: channel2.Id, Message: "@user2 and @user3 might be in the channel"} - if mentioned := getOutOfChannelMentions(post5, team2.Id); len(mentioned) != 0 { + if mentioned := getOutOfChannelMentions(post5, allProfiles, members); len(mentioned) != 0 { t.Fatalf("getOutOfChannelMentions returned %v when two users on a different team were mentioned", mentioned) } } diff --git a/i18n/en.json b/i18n/en.json index 8ffbc8b14e..12d53022e7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -691,14 +691,6 @@ "id": "api.post.delete_post.permissions.app_error", "translation": "You do not have the appropriate permissions" }, - { - "id": "api.post.get_out_of_channel_mentions.retrieve_members.error", - "translation": "Failed to get channel members channel_id=%v err=%v" - }, - { - "id": "api.post.get_out_of_channel_mentions.retrieve_profiles.error", - "translation": "Failed to retrieve user profiles team_id=%v, err=%v" - }, { "id": "api.post.get_out_of_channel_mentions.regex.error", "translation": "Failed to compile @mention regex user_id=%v, err=%v" @@ -711,6 +703,14 @@ "id": "api.post.handle_post_events_and_forget.channel.error", "translation": "Encountered error getting channel, channel_id=%s, err=%v" }, + { + "id": "api.post.handle_post_events_and_forget.members.error", + "translation": "Failed to get channel members channel_id=%v err=%v" + }, + { + "id": "api.post.handle_post_events_and_forget.profiles.error", + "translation": "Failed to retrieve user profiles team_id=%v, err=%v" + }, { "id": "api.post.handle_post_events_and_forget.team.error", "translation": "Encountered error getting team, team_id=%s, err=%v" @@ -751,10 +751,6 @@ "id": "api.post.make_direct_channel_visible.update_pref.error", "translation": "Failed to update direct channel preference user_id=%v other_user_id=%v err=%v" }, - { - "id": "api.post.send_notifications_and_forget.members.error", - "translation": "Failed to get channel members channel_id=%v err=%v" - }, { "id": "api.post.send_notifications_and_forget.mention_body", "translation": "You have one new mention." @@ -787,10 +783,6 @@ "id": "api.post.send_notifications_and_forget.push_notification.error", "translation": "Failed to send push notificationid=%v, err=%v" }, - { - "id": "api.post.send_notifications_and_forget.retrive_profiles.error", - "translation": "Failed to retrieve user profiles team_id=%v, err=%v" - }, { "id": "api.post.send_notifications_and_forget.send.error", "translation": "Failed to send mention email successfully email=%v err=%v" diff --git a/i18n/es.json b/i18n/es.json index 50adbe092a..6b1f18d926 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -683,14 +683,6 @@ "id": "api.post.delete_post.permissions.app_error", "translation": "No tienes los permisos apropiados" }, - { - "id": "api.post.get_out_of_channel_mentions.retrieve_members.error", - "translation": "Falla al obtener los miembros del canal channel_id=%v err=%v" - }, - { - "id": "api.post.get_out_of_channel_mentions.retrieve_profiles.error", - "translation": "Falla al recuperar los perfiles de usuario team_id=%v, err=%v" - }, { "id": "api.post.get_post.permissions.app_error", "translation": "No tienes los permisos apropiados" @@ -699,6 +691,14 @@ "id": "api.post.handle_post_events_and_forget.channel.error", "translation": "Se encontró un error obteniendo el canal, channel_id=%s, err=%v" }, + { + "id": "api.post.handle_post_events_and_forget.members.error", + "translation": "Falla al obtener los miembros del canal channel_id=%v err=%v" + }, + { + "id": "api.post.handle_post_events_and_forget.profiles.error", + "translation": "Falla al recuperar los perfiles de usuario team_id=%v, err=%v" + }, { "id": "api.post.handle_post_events_and_forget.team.error", "translation": "Se encontró un error obteniendo el equipo, team_id=%s, err=%v" @@ -739,10 +739,6 @@ "id": "api.post.make_direct_channel_visible.update_pref.error", "translation": "Falla al actualizar las preferencias del canal directo user_id=%v other_user_id=%v err=%v" }, - { - "id": "api.post.send_notifications_and_forget.members.error", - "translation": "Falla al obtener los miembros del canal channel_id=%v err=%v" - }, { "id": "api.post.send_notifications_and_forget.mention_body", "translation": "Tienes una mención nueva." @@ -775,10 +771,6 @@ "id": "api.post.send_notifications_and_forget.push_notification.error", "translation": "Falló el envio de la notificación push notificationid=%v, err=%v" }, - { - "id": "api.post.send_notifications_and_forget.retrive_profiles.error", - "translation": "Falla al recuperar los perfiles de usuario team_id=%v, err=%v" - }, { "id": "api.post.send_notifications_and_forget.send.error", "translation": "Falla al enviar el correo con la mención satisfactoriamente email=%v err=%v" From 70c3715b963cd8f2a710f8909f23696f9de7fcc7 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Wed, 3 Feb 2016 12:12:10 -0500 Subject: [PATCH 08/10] Changed how posts are marked ephemeral --- api/post.go | 3 +-- model/post.go | 14 +++++++------- web/react/components/post_info.jsx | 6 +++--- web/react/stores/post_store.jsx | 3 +-- web/react/utils/constants.jsx | 2 +- web/react/utils/utils.jsx | 4 ++++ 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/api/post.go b/api/post.go index 0f0a83639e..e7c3f69139 100644 --- a/api/post.go +++ b/api/post.go @@ -764,9 +764,8 @@ func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.C Id: model.NewId(), ChannelId: post.ChannelId, Message: messageText, - Type: model.POST_OUT_OF_CHANNEL_MENTION, + Type: model.POST_EPHEMERAL, CreateAt: post.CreateAt + 1, - Ephemeral: true, Props: model.StringInterface{}, Filenames: []string{}, } diff --git a/model/post.go b/model/post.go index cc9b0bda40..8a451831ce 100644 --- a/model/post.go +++ b/model/post.go @@ -10,12 +10,13 @@ import ( ) const ( - POST_SYSTEM_MESSAGE_PREFIX = "system_" - POST_DEFAULT = "" - POST_SLACK_ATTACHMENT = "slack_attachment" - POST_JOIN_LEAVE = "system_join_leave" - POST_HEADER_CHANGE = "system_header_change" - POST_OUT_OF_CHANNEL_MENTION = "system_out_of_channel_mention" + POST_SYSTEM_MESSAGE_PREFIX = "system_" + POST_DEFAULT = "" + POST_SLACK_ATTACHMENT = "slack_attachment" + POST_SYSTEM_GENERIC = "system_generic" + POST_JOIN_LEAVE = "system_join_leave" + POST_HEADER_CHANGE = "system_header_change" + POST_EPHEMERAL = "system_ephemeral" ) type Post struct { @@ -34,7 +35,6 @@ type Post struct { Hashtags string `json:"hashtags"` Filenames StringArray `json:"filenames"` PendingPostId string `json:"pending_post_id" db:"-"` - Ephemeral bool `json:"ephemeral" db:"-"` } func (o *Post) ToJson() string { diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index 02150bd9de..b1bc8ca144 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -30,7 +30,7 @@ export default class PostInfo extends React.Component { var isOwner = UserStore.getCurrentId() === post.user_id; var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles); - if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.ephemeral) { + if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) { return ''; } @@ -171,7 +171,7 @@ export default class PostInfo extends React.Component { EventHelpers.emitRemovePost(this.props.post); } createRemovePostButton(post) { - if (!post.ephemeral) { + if (!Utils.isPostEphemeral(post)) { return null; } @@ -198,7 +198,7 @@ export default class PostInfo extends React.Component { commentCountText = ''; } - if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !post.ephemeral) { + if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !Utils.isPostEphemeral(post)) { comments = (
Date: Wed, 3 Feb 2016 15:21:03 -0500 Subject: [PATCH 09/10] Added helper methods to send ephemeral messages to specific users --- api/post.go | 54 +++++++++++++++++--------- web/react/dispatcher/event_helpers.jsx | 18 +++++++++ web/react/stores/socket_store.jsx | 3 +- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/api/post.go b/api/post.go index e7c3f69139..a2018bdbd8 100644 --- a/api/post.go +++ b/api/post.go @@ -747,34 +747,27 @@ func checkForOutOfChannelMentions(c *Context, post *model.Post, channel *model.C } sort.Strings(usernames) - var messageText string + var message string if len(usernames) == 1 { - messageText = c.T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{ + message = c.T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{ "Username": usernames[0], }) } else { - messageText = c.T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{ + message = c.T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{ "Usernames": strings.Join(usernames[:len(usernames)-1], ", "), "LastUsername": usernames[len(usernames)-1], }) } - // create an ephemeral post that will be sent only to the sender of this original post and not stored in the DB - warningPost := model.Post{ - Id: model.NewId(), - ChannelId: post.ChannelId, - Message: messageText, - Type: model.POST_EPHEMERAL, - CreateAt: post.CreateAt + 1, - Props: model.StringInterface{}, - Filenames: []string{}, - } - - message := model.NewMessage(c.Session.TeamId, channel.Id, post.UserId, model.ACTION_EPHEMERAL_MESSAGE) - message.Add("post", warningPost.ToJson()) - message.Add("channel_type", channel.Type) - - PublishAndForget(message) + SendEphemeralPost( + c.Session.TeamId, + post.UserId, + &model.Post{ + ChannelId: post.ChannelId, + Message: message, + CreateAt: post.CreateAt + 1, + }, + ) } // Gets a list of users that were mentioned in a given post that aren't in the channel that the post was made in @@ -803,6 +796,29 @@ func getOutOfChannelMentions(post *model.Post, allProfiles map[string]*model.Use return mentioned } +func SendEphemeralPost(teamId, userId string, post *model.Post) { + post.Type = model.POST_EPHEMERAL + + // fill in fields which haven't been specified which have sensible defaults + if post.Id == "" { + post.Id = model.NewId() + } + if post.CreateAt == 0 { + post.CreateAt = model.GetMillis() + } + if post.Props == nil { + post.Props = model.StringInterface{} + } + if post.Filenames == nil { + post.Filenames = []string{} + } + + message := model.NewMessage(teamId, post.ChannelId, userId, model.ACTION_EPHEMERAL_MESSAGE) + message.Add("post", post.ToJson()) + + PublishAndForget(message) +} + func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { post := model.PostFromJson(r.Body) diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx index cb41bd1bba..c1041e4386 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/dispatcher/event_helpers.jsx @@ -9,6 +9,7 @@ import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; import * as AsyncClient from '../utils/async_client.jsx'; import * as Client from '../utils/client.jsx'; +import * as Utils from '../utils/utils.jsx'; export function emitChannelClickEvent(channel) { AsyncClient.getChannels(true); @@ -187,3 +188,20 @@ export function emitRemovePost(post) { post }); } + +export function sendEphemeralPost(message, channelId) { + const timestamp = Utils.getTimestamp(); + const post = { + id: Utils.generateId(), + user_id: '0', + channel_id: channelId || ChannelStore.getCurrentId(), + message, + type: Constants.POST_TYPE_EPHEMERAL, + create_at: timestamp, + update_at: timestamp, + filenames: [], + props: {} + }; + + emitPostRecievedEvent(post); +} diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 745b943131..33604f44b1 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -180,7 +180,6 @@ function handleNewPostEvent(msg, translations) { mentions = JSON.parse(msg.props.mentions); } - const channelType = msgProps.channel_type; const channel = ChannelStore.get(msg.channel_id); const user = UserStore.getCurrentUser(); const member = ChannelStore.getMember(msg.channel_id); @@ -192,7 +191,7 @@ function handleNewPostEvent(msg, translations) { if (notifyLevel === 'none') { return; - } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channelType !== Constants.DM_CHANNEL) { + } else if (notifyLevel === 'mention' && mentions.indexOf(user.id) === -1 && channel.type !== Constants.DM_CHANNEL) { return; } From 21318b7505babaebbb5174c62b75595b2936ebe0 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 4 Feb 2016 11:45:19 -0500 Subject: [PATCH 10/10] Changed sendNotificationsAndForget to be synchronous and called as a go func --- api/post.go | 455 ++++++++++++++++++++++++++-------------------------- 1 file changed, 226 insertions(+), 229 deletions(-) diff --git a/api/post.go b/api/post.go index a2018bdbd8..c17da262f9 100644 --- a/api/post.go +++ b/api/post.go @@ -267,7 +267,7 @@ func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks boo members = result.Data.([]model.ChannelMember) } - sendNotificationsAndForget(c, post, team, channel, profiles, members) + go sendNotifications(c, post, team, channel, profiles, members) go checkForOutOfChannelMentions(c, post, channel, profiles, members) var user *model.User @@ -433,293 +433,290 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team } -func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel, profileMap map[string]*model.User, members []model.ChannelMember) { +func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel, profileMap map[string]*model.User, members []model.ChannelMember) { + var channelName string + var bodyText string + var subjectText string - go func() { - var channelName string - var bodyText string - var subjectText string + var mentionedUsers []string - var mentionedUsers []string + if _, ok := profileMap[post.UserId]; !ok { + l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) + return + } + senderName := profileMap[post.UserId].Username - if _, ok := profileMap[post.UserId]; !ok { - l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) - return - } - senderName := profileMap[post.UserId].Username + toEmailMap := make(map[string]bool) - toEmailMap := make(map[string]bool) - - if channel.Type == model.CHANNEL_DIRECT { - - var otherUserId string - if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { - otherUserId = userIds[1] - channelName = profileMap[userIds[1]].Username - } else { - otherUserId = userIds[0] - channelName = profileMap[userIds[0]].Username - } - - otherUser := profileMap[otherUserId] - sendEmail := true - if _, ok := otherUser.NotifyProps["email"]; ok && otherUser.NotifyProps["email"] == "false" { - sendEmail = false - } - if sendEmail && (otherUser.IsOffline() || otherUser.IsAway()) { - toEmailMap[otherUserId] = true - } + if channel.Type == model.CHANNEL_DIRECT { + var otherUserId string + if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { + otherUserId = userIds[1] + channelName = profileMap[userIds[1]].Username } else { - // Find out who is a member of the channel, only keep those profiles - tempProfileMap := make(map[string]*model.User) - for _, member := range members { - tempProfileMap[member.UserId] = profileMap[member.UserId] - } + otherUserId = userIds[0] + channelName = profileMap[userIds[0]].Username + } - profileMap = tempProfileMap + otherUser := profileMap[otherUserId] + sendEmail := true + if _, ok := otherUser.NotifyProps["email"]; ok && otherUser.NotifyProps["email"] == "false" { + sendEmail = false + } + if sendEmail && (otherUser.IsOffline() || otherUser.IsAway()) { + toEmailMap[otherUserId] = true + } - // Build map for keywords - keywordMap := make(map[string][]string) - for _, profile := range profileMap { - if len(profile.NotifyProps["mention_keys"]) > 0 { + } else { + // Find out who is a member of the channel, only keep those profiles + tempProfileMap := make(map[string]*model.User) + for _, member := range members { + tempProfileMap[member.UserId] = profileMap[member.UserId] + } - // Add all the user's mention keys - splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") - for _, k := range splitKeys { - keywordMap[k] = append(keywordMap[strings.ToLower(k)], profile.Id) - } - } + profileMap = tempProfileMap - // If turned on, add the user's case sensitive first name - if profile.NotifyProps["first_name"] == "true" { - keywordMap[profile.FirstName] = append(keywordMap[profile.FirstName], profile.Id) - } + // Build map for keywords + keywordMap := make(map[string][]string) + for _, profile := range profileMap { + if len(profile.NotifyProps["mention_keys"]) > 0 { - // Add @all to keywords if user has them turned on - // if profile.NotifyProps["all"] == "true" { - // keywordMap["@all"] = append(keywordMap["@all"], profile.Id) - // } - - // Add @channel to keywords if user has them turned on - if profile.NotifyProps["channel"] == "true" { - keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id) + // Add all the user's mention keys + splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") + for _, k := range splitKeys { + keywordMap[k] = append(keywordMap[strings.ToLower(k)], profile.Id) } } - // Build a map as a list of unique user_ids that are mentioned in this post - splitF := func(c rune) bool { - return model.SplitRunes[c] - } - splitMessage := strings.Fields(post.Message) - for _, word := range splitMessage { - var userIds []string - - // Non-case-sensitive check for regular keys - if ids, match := keywordMap[strings.ToLower(word)]; match { - userIds = append(userIds, ids...) - } - - // Case-sensitive check for first name - if ids, match := keywordMap[word]; match { - userIds = append(userIds, ids...) - } - - if len(userIds) == 0 { - // No matches were found with the string split just on whitespace so try further splitting - // the message on punctuation - splitWords := strings.FieldsFunc(word, splitF) - - for _, splitWord := range splitWords { - // Non-case-sensitive check for regular keys - if ids, match := keywordMap[strings.ToLower(splitWord)]; match { - userIds = append(userIds, ids...) - } - - // Case-sensitive check for first name - if ids, match := keywordMap[splitWord]; match { - userIds = append(userIds, ids...) - } - } - } - - for _, userId := range userIds { - if post.UserId == userId { - continue - } - sendEmail := true - if _, ok := profileMap[userId].NotifyProps["email"]; ok && profileMap[userId].NotifyProps["email"] == "false" { - sendEmail = false - } - if sendEmail && (profileMap[userId].IsAway() || profileMap[userId].IsOffline()) { - toEmailMap[userId] = true - } else { - toEmailMap[userId] = false - } - } + // If turned on, add the user's case sensitive first name + if profile.NotifyProps["first_name"] == "true" { + keywordMap[profile.FirstName] = append(keywordMap[profile.FirstName], profile.Id) } - for id := range toEmailMap { - updateMentionCountAndForget(post.ChannelId, id) + // Add @all to keywords if user has them turned on + // if profile.NotifyProps["all"] == "true" { + // keywordMap["@all"] = append(keywordMap["@all"], profile.Id) + // } + + // Add @channel to keywords if user has them turned on + if profile.NotifyProps["channel"] == "true" { + keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id) } } - if len(toEmailMap) != 0 { - mentionedUsers = make([]string, 0, len(toEmailMap)) - for k := range toEmailMap { - mentionedUsers = append(mentionedUsers, k) + // Build a map as a list of unique user_ids that are mentioned in this post + splitF := func(c rune) bool { + return model.SplitRunes[c] + } + splitMessage := strings.Fields(post.Message) + for _, word := range splitMessage { + var userIds []string + + // Non-case-sensitive check for regular keys + if ids, match := keywordMap[strings.ToLower(word)]; match { + userIds = append(userIds, ids...) } - teamURL := c.GetSiteURL() + "/" + team.Name + // Case-sensitive check for first name + if ids, match := keywordMap[word]; match { + userIds = append(userIds, ids...) + } - // Build and send the emails - tm := time.Unix(post.CreateAt/1000, 0) + if len(userIds) == 0 { + // No matches were found with the string split just on whitespace so try further splitting + // the message on punctuation + splitWords := strings.FieldsFunc(word, splitF) - for id, doSend := range toEmailMap { + for _, splitWord := range splitWords { + // Non-case-sensitive check for regular keys + if ids, match := keywordMap[strings.ToLower(splitWord)]; match { + userIds = append(userIds, ids...) + } - if !doSend { + // Case-sensitive check for first name + if ids, match := keywordMap[splitWord]; match { + userIds = append(userIds, ids...) + } + } + } + + for _, userId := range userIds { + if post.UserId == userId { continue } - - // skip if inactive - if profileMap[id].DeleteAt > 0 { - continue + sendEmail := true + if _, ok := profileMap[userId].NotifyProps["email"]; ok && profileMap[userId].NotifyProps["email"] == "false" { + sendEmail = false } - - userLocale := utils.GetUserTranslations(profileMap[id].Locale) - - if channel.Type == model.CHANNEL_DIRECT { - bodyText = userLocale("api.post.send_notifications_and_forget.message_body") - subjectText = userLocale("api.post.send_notifications_and_forget.message_subject") + if sendEmail && (profileMap[userId].IsAway() || profileMap[userId].IsOffline()) { + toEmailMap[userId] = true } else { - bodyText = userLocale("api.post.send_notifications_and_forget.mention_body") - subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject") - channelName = channel.DisplayName + toEmailMap[userId] = false } + } + } - month := userLocale(tm.Month().String()) - day := fmt.Sprintf("%d", tm.Day()) - year := fmt.Sprintf("%d", tm.Year()) - zone, _ := tm.Zone() + for id := range toEmailMap { + updateMentionCountAndForget(post.ChannelId, id) + } + } - subjectPage := NewServerTemplatePage("post_subject", c.Locale) - subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", - map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, - "Month": month[:3], "Day": day, "Year": year}) + if len(toEmailMap) != 0 { + mentionedUsers = make([]string, 0, len(toEmailMap)) + for k := range toEmailMap { + mentionedUsers = append(mentionedUsers, k) + } - bodyPage := NewServerTemplatePage("post_body", c.Locale) - bodyPage.Props["SiteURL"] = c.GetSiteURL() - bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) - bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name - bodyPage.Props["BodyText"] = bodyText - bodyPage.Props["Button"] = userLocale("api.templates.post_body.button") - bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info", - map[string]interface{}{"ChannelName": channelName, "SenderName": senderName, - "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()), - "TimeZone": zone, "Month": month, "Day": day})) + teamURL := c.GetSiteURL() + "/" + team.Name - // attempt to fill in a message body if the post doesn't have any text - if len(strings.TrimSpace(bodyPage.Props["PostMessage"])) == 0 && len(post.Filenames) > 0 { - // extract the filenames from their paths and determine what type of files are attached - filenames := make([]string, len(post.Filenames)) - onlyImages := true - for i, filename := range post.Filenames { - var err error - if filenames[i], err = url.QueryUnescape(filepath.Base(filename)); err != nil { - // this should never error since filepath was escaped using url.QueryEscape - filenames[i] = filepath.Base(filename) - } + // Build and send the emails + tm := time.Unix(post.CreateAt/1000, 0) - ext := filepath.Ext(filename) - onlyImages = onlyImages && model.IsFileExtImage(ext) - } - filenamesString := strings.Join(filenames, ", ") + for id, doSend := range toEmailMap { - var attachmentPrefix string - if onlyImages { - attachmentPrefix = "Image" - } else { - attachmentPrefix = "File" - } - if len(post.Filenames) > 1 { - attachmentPrefix += "s" + if !doSend { + continue + } + + // skip if inactive + if profileMap[id].DeleteAt > 0 { + continue + } + + userLocale := utils.GetUserTranslations(profileMap[id].Locale) + + if channel.Type == model.CHANNEL_DIRECT { + bodyText = userLocale("api.post.send_notifications_and_forget.message_body") + subjectText = userLocale("api.post.send_notifications_and_forget.message_subject") + } else { + bodyText = userLocale("api.post.send_notifications_and_forget.mention_body") + subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject") + channelName = channel.DisplayName + } + + month := userLocale(tm.Month().String()) + day := fmt.Sprintf("%d", tm.Day()) + year := fmt.Sprintf("%d", tm.Year()) + zone, _ := tm.Zone() + + subjectPage := NewServerTemplatePage("post_subject", c.Locale) + subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", + map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, + "Month": month[:3], "Day": day, "Year": year}) + + bodyPage := NewServerTemplatePage("post_body", c.Locale) + bodyPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) + bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name + bodyPage.Props["BodyText"] = bodyText + bodyPage.Props["Button"] = userLocale("api.templates.post_body.button") + bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info", + map[string]interface{}{"ChannelName": channelName, "SenderName": senderName, + "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()), + "TimeZone": zone, "Month": month, "Day": day})) + + // attempt to fill in a message body if the post doesn't have any text + if len(strings.TrimSpace(bodyPage.Props["PostMessage"])) == 0 && len(post.Filenames) > 0 { + // extract the filenames from their paths and determine what type of files are attached + filenames := make([]string, len(post.Filenames)) + onlyImages := true + for i, filename := range post.Filenames { + var err error + if filenames[i], err = url.QueryUnescape(filepath.Base(filename)); err != nil { + // this should never error since filepath was escaped using url.QueryEscape + filenames[i] = filepath.Base(filename) } - bodyPage.Props["PostMessage"] = userLocale("api.post.send_notifications_and_forget.sent", - map[string]interface{}{"Prefix": attachmentPrefix, "Filenames": filenamesString}) + ext := filepath.Ext(filename) + onlyImages = onlyImages && model.IsFileExtImage(ext) + } + filenamesString := strings.Join(filenames, ", ") + + var attachmentPrefix string + if onlyImages { + attachmentPrefix = "Image" + } else { + attachmentPrefix = "File" + } + if len(post.Filenames) > 1 { + attachmentPrefix += "s" } - if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), profileMap[id].Email, err) - } + bodyPage.Props["PostMessage"] = userLocale("api.post.send_notifications_and_forget.sent", + map[string]interface{}{"Prefix": attachmentPrefix, "Filenames": filenamesString}) + } - if *utils.Cfg.EmailSettings.SendPushNotifications { - sessionChan := Srv.Store.Session().GetSessions(id) - if result := <-sessionChan; result.Err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), id, result.Err) - } else { - sessions := result.Data.([]*model.Session) - alreadySeen := make(map[string]string) + if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil { + l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), profileMap[id].Email, err) + } - for _, session := range sessions { - if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && - (strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")) { - alreadySeen[session.DeviceId] = session.DeviceId + if *utils.Cfg.EmailSettings.SendPushNotifications { + sessionChan := Srv.Store.Session().GetSessions(id) + if result := <-sessionChan; result.Err != nil { + l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), id, result.Err) + } else { + sessions := result.Data.([]*model.Session) + alreadySeen := make(map[string]string) - msg := model.PushNotification{} - msg.Badge = 1 - msg.ServerId = utils.CfgDiagnosticId + for _, session := range sessions { + if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && + (strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") || strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":")) { + alreadySeen[session.DeviceId] = session.DeviceId - if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") { - msg.Platform = model.PUSH_NOTIFY_APPLE - msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") - } else if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") { - msg.Platform = model.PUSH_NOTIFY_ANDROID - msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") - } + msg := model.PushNotification{} + msg.Badge = 1 + msg.ServerId = utils.CfgDiagnosticId - if channel.Type == model.CHANNEL_DIRECT { - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") - } else { - msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName - } + if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") { + msg.Platform = model.PUSH_NOTIFY_APPLE + msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_APPLE+":") + } else if strings.HasPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") { + msg.Platform = model.PUSH_NOTIFY_ANDROID + msg.DeviceId = strings.TrimPrefix(session.DeviceId, model.PUSH_NOTIFY_ANDROID+":") + } - httpClient := http.Client{} - request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson())) + if channel.Type == model.CHANNEL_DIRECT { + msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") + } else { + msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName + } - l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) - if _, err := httpClient.Do(request); err != nil { - l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), id, err) - } + httpClient := http.Client{} + request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson())) + + l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message) + if _, err := httpClient.Do(request); err != nil { + l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), id, err) } } } } } } + } - message := model.NewMessage(c.Session.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED) - message.Add("post", post.ToJson()) - message.Add("channel_type", channel.Type) + message := model.NewMessage(c.Session.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED) + message.Add("post", post.ToJson()) + message.Add("channel_type", channel.Type) - if len(post.Filenames) != 0 { - message.Add("otherFile", "true") + if len(post.Filenames) != 0 { + message.Add("otherFile", "true") - for _, filename := range post.Filenames { - ext := filepath.Ext(filename) - if model.IsFileExtImage(ext) { - message.Add("image", "true") - break - } + for _, filename := range post.Filenames { + ext := filepath.Ext(filename) + if model.IsFileExtImage(ext) { + message.Add("image", "true") + break } } + } - if len(mentionedUsers) != 0 { - message.Add("mentions", model.ArrayToJson(mentionedUsers)) - } + if len(mentionedUsers) != 0 { + message.Add("mentions", model.ArrayToJson(mentionedUsers)) + } - PublishAndForget(message) - }() + PublishAndForget(message) } func updateMentionCountAndForget(channelId, userId string) {