func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) []string { pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) fchan := Srv.Store.FileInfo().GetForPost(post.Id) var profileMap map[string]*model.User if result := <-pchan; result.Err != nil { l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err) return nil } else { profileMap = result.Data.(map[string]*model.User) } // If the user who made the post is mention don't send a notification if _, ok := profileMap[post.UserId]; !ok { l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) return nil } mentionedUserIds := make(map[string]bool) allActivityPushUserIds := []string{} hereNotification := false updateMentionChans := []store.StoreChannel{} if channel.Type == model.CHANNEL_DIRECT { var otherUserId string if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { otherUserId = userIds[1] } else { otherUserId = userIds[0] } mentionedUserIds[otherUserId] = true } else { keywords := getMentionKeywordsInChannel(profileMap) var potentialOtherMentions []string mentionedUserIds, potentialOtherMentions, hereNotification = getExplicitMentions(post.Message, keywords) // get users that have comment thread mentions enabled if len(post.RootId) > 0 { if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err) return nil } else { list := result.Data.(*model.PostList) for _, threadPost := range list.Posts { profile := profileMap[threadPost.UserId] if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { mentionedUserIds[threadPost.UserId] = true } } } } // prevent the user from mentioning themselves if post.Props["from_webhook"] != "true" { delete(mentionedUserIds, post.UserId) } if len(potentialOtherMentions) > 0 { if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { outOfChannelMentions := result.Data.(map[string]*model.User) go sendOutOfChannelMentions(c, post, outOfChannelMentions) } } // find which users in the channel are set up to always receive mobile notifications for _, profile := range profileMap { if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && (post.UserId != profile.Id || post.Props["from_webhook"] == "true") && !post.IsSystemMessage() { allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) } } } mentionedUsersList := make([]string, 0, len(mentionedUserIds)) for id := range mentionedUserIds { mentionedUsersList = append(mentionedUsersList, id) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) } senderName := "" var sender *model.User if post.IsSystemMessage() { senderName = c.T("system.message.name") } else if profile, ok := profileMap[post.UserId]; ok { if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { senderName = value.(string) } else { senderName = profile.Username } sender = profile } if utils.Cfg.EmailSettings.SendEmailNotifications { for _, id := range mentionedUsersList { userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} } if userAllowsEmails && status.Status != model.STATUS_ONLINE { sendNotificationEmail(c, post, profileMap[id], channel, team, senderName, sender) } } } if hereNotification { if result := <-Srv.Store.Status().GetOnline(); result.Err != nil { l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err) return nil } else { statuses := result.Data.([]*model.Status) for _, status := range statuses { if status.UserId == post.UserId { continue } _, profileFound := profileMap[status.UserId] _, alreadyMentioned := mentionedUserIds[status.UserId] if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned { mentionedUsersList = append(mentionedUsersList, status.UserId) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId)) } } } } // Make sure all mention updates are complete to prevent race // Probably better to batch these DB updates in the future // MUST be completed before push notifications send for _, uchan := range updateMentionChans { if result := <-uchan; result.Err != nil { l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) } } sendPushNotifications := false if *utils.Cfg.EmailSettings.SendPushNotifications { pushServer := *utils.Cfg.EmailSettings.PushNotificationServer if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) sendPushNotifications = false } else { sendPushNotifications = true } } if sendPushNotifications { for _, id := range mentionedUsersList { var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} } if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { sendPushNotification(post, profileMap[id], channel, senderName, true) } } for _, id := range allActivityPushUserIds { if _, ok := mentionedUserIds[id]; !ok { var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} } if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { sendPushNotification(post, profileMap[id], channel, senderName, false) } } } } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil) message.Add("post", post.ToJson()) message.Add("channel_type", channel.Type) message.Add("channel_display_name", channel.DisplayName) message.Add("sender_name", senderName) message.Add("team_id", team.Id) if len(post.FileIds) != 0 { message.Add("otherFile", "true") var infos []*model.FileInfo if result := <-fchan; result.Err != nil { l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err) } else { infos = result.Data.([]*model.FileInfo) } for _, info := range infos { if info.IsImage() { message.Add("image", "true") break } } } if len(mentionedUsersList) != 0 { message.Add("mentions", model.ArrayToJson(mentionedUsersList)) } Publish(message) return mentionedUsersList }
func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { post := model.PostFromJson(r.Body) if post == nil { c.SetInvalidParam("updatePost", "post") return } pchan := app.Srv.Store.Post().Get(post.Id) if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_POST) { return } var oldPost *model.Post if result := <-pchan; result.Err != nil { c.Err = result.Err return } else { oldPost = result.Data.(*model.PostList).Posts[post.Id] if oldPost == nil { c.Err = model.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id) c.Err.StatusCode = http.StatusBadRequest return } if oldPost.UserId != c.Session.UserId { c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId) c.Err.StatusCode = http.StatusForbidden return } if oldPost.DeleteAt != 0 { c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, c.T("api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id})) c.Err.StatusCode = http.StatusForbidden return } if oldPost.IsSystemMessage() { c.Err = model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id) c.Err.StatusCode = http.StatusForbidden return } } newPost := &model.Post{} *newPost = *oldPost newPost.Message = post.Message newPost.EditAt = model.GetMillis() newPost.Hashtags, _ = model.ParseHashtags(post.Message) if result := <-app.Srv.Store.Post().Update(newPost, oldPost); result.Err != nil { c.Err = result.Err return } else { rpost := result.Data.(*model.Post) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil) message.Add("post", rpost.ToJson()) go app.Publish(message) app.InvalidateCacheForChannelPosts(rpost.ChannelId) w.Write([]byte(rpost.ToJson())) } }
func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { post := model.PostFromJson(r.Body) if post == nil { c.SetInvalidParam("updatePost", "post") return } cchan := Srv.Store.Channel().CheckPermissionsTo(c.TeamId, post.ChannelId, c.Session.UserId) pchan := Srv.Store.Post().Get(post.Id) if !c.HasPermissionsToChannel(cchan, "updatePost") { return } var oldPost *model.Post if result := <-pchan; result.Err != nil { c.Err = result.Err return } else { oldPost = result.Data.(*model.PostList).Posts[post.Id] if oldPost == nil { c.Err = model.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id) c.Err.StatusCode = http.StatusBadRequest return } if oldPost.UserId != c.Session.UserId { c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId) c.Err.StatusCode = http.StatusForbidden return } if oldPost.DeleteAt != 0 { c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, c.T("api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id})) c.Err.StatusCode = http.StatusForbidden return } if oldPost.IsSystemMessage() { c.Err = model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id) c.Err.StatusCode = http.StatusForbidden return } } hashtags, _ := model.ParseHashtags(post.Message) if result := <-Srv.Store.Post().Update(oldPost, post.Message, hashtags); result.Err != nil { c.Err = result.Err return } else { rpost := result.Data.(*model.Post) message := model.NewMessage(c.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED) message.Add("post", rpost.ToJson()) go Publish(message) w.Write([]byte(rpost.ToJson())) } }
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel, profileMap map[string]*model.User, members []model.ChannelMember) { if _, ok := profileMap[post.UserId]; !ok { l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) return } mentionedUserIds := make(map[string]bool) alwaysNotifyUserIds := []string{} if channel.Type == model.CHANNEL_DIRECT { var otherUserId string if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { otherUserId = userIds[1] } else { otherUserId = userIds[0] } mentionedUserIds[otherUserId] = true } else { // Find out who is a member of the channel, only keep those profiles tempProfileMap := make(map[string]*model.User) for _, member := range members { if profile, ok := profileMap[member.UserId]; ok { tempProfileMap[member.UserId] = profile } } profileMap = tempProfileMap // Build map for keywords keywordMap := make(map[string][]string) for _, profile := range profileMap { if len(profile.NotifyProps["mention_keys"]) > 0 { // 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) } } // 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 @channel and @all to keywords if user has them turned on if profile.NotifyProps["channel"] == "true" { keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id) keywordMap["@all"] = append(keywordMap["@all"], profile.Id) } if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && (post.UserId != profile.Id || post.Props["from_webhook"] == "true") && !post.IsSystemMessage() { alwaysNotifyUserIds = append(alwaysNotifyUserIds, 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 && post.Props["from_webhook"] != "true" { continue } mentionedUserIds[userId] = true } } for id := range mentionedUserIds { go updateMentionCount(post.ChannelId, id) } } mentionedUsersList := make([]string, 0, len(mentionedUserIds)) senderName := "" if post.IsSystemMessage() { senderName = c.T("system.message.name") } else if profile, ok := profileMap[post.UserId]; ok { senderName = profile.Username } for id := range mentionedUserIds { mentionedUsersList = append(mentionedUsersList, id) } if utils.Cfg.EmailSettings.SendEmailNotifications { for _, id := range mentionedUsersList { userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" if userAllowsEmails && (profileMap[id].IsAway() || profileMap[id].IsOffline()) { sendNotificationEmail(c, post, profileMap[id], channel, team, senderName) } } } sendPushNotifications := false if *utils.Cfg.EmailSettings.SendPushNotifications { pushServer := *utils.Cfg.EmailSettings.PushNotificationServer if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) sendPushNotifications = false } else { sendPushNotifications = true } } if sendPushNotifications { for _, id := range mentionedUsersList { if profileMap[id].NotifyProps["push"] != "none" { sendPushNotification(post, profileMap[id], channel, senderName, true) } } for _, id := range alwaysNotifyUserIds { if _, ok := mentionedUserIds[id]; !ok { sendPushNotification(post, profileMap[id], channel, senderName, false) } } } message := model.NewMessage(c.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED) message.Add("post", post.ToJson()) message.Add("channel_type", channel.Type) message.Add("channel_display_name", channel.DisplayName) message.Add("sender_name", senderName) message.Add("team_id", team.Id) 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 } } } if len(mentionedUsersList) != 0 { message.Add("mentions", model.ArrayToJson(mentionedUsersList)) } go Publish(message) }
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) { // get profiles for all users we could be mentioning pchan := Srv.Store.User().GetProfiles(c.TeamId) dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId) mchan := Srv.Store.Channel().GetMembers(post.ChannelId) var profileMap map[string]*model.User if result := <-pchan; result.Err != nil { l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err) return } else { profileMap = result.Data.(map[string]*model.User) } if result := <-dpchan; result.Err != nil { l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err) return } else { dps := result.Data.(map[string]*model.User) for k, v := range dps { profileMap[k] = v } } if _, ok := profileMap[post.UserId]; !ok { l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) return } // using a map as a pseudo-set since we're checking for containment a lot members := make(map[string]string) 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 { for _, member := range result.Data.([]model.ChannelMember) { members[member.UserId] = member.UserId } } mentionedUserIds := make(map[string]bool) allActivityPushUserIds := []string{} hereNotification := false updateMentionChans := []store.StoreChannel{} if channel.Type == model.CHANNEL_DIRECT { var otherUserId string if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { otherUserId = userIds[1] } else { otherUserId = userIds[0] } mentionedUserIds[otherUserId] = true } else { keywords := getMentionKeywords(profileMap, members) // get users that are explicitly mentioned var mentioned map[string]bool mentioned, hereNotification = getExplicitMentions(post.Message, keywords) // get users that have comment thread mentions enabled if len(post.RootId) > 0 { if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err) return } else { list := result.Data.(*model.PostList) for _, threadPost := range list.Posts { profile := profileMap[threadPost.UserId] if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { mentioned[threadPost.UserId] = true } } } } // prevent the user from mentioning themselves if post.Props["from_webhook"] != "true" { delete(mentioned, post.UserId) } outOfChannelMentions := make(map[string]bool) for id := range mentioned { if _, inChannel := members[id]; inChannel { mentionedUserIds[id] = true } else { outOfChannelMentions[id] = true } } go sendOutOfChannelMentions(c, post, profileMap, outOfChannelMentions) // find which users in the channel are set up to always receive mobile notifications for id := range members { profile := profileMap[id] if profile == nil { l4g.Warn(utils.T("api.post.notification.member_profile.warn"), id) continue } if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && (post.UserId != profile.Id || post.Props["from_webhook"] == "true") && !post.IsSystemMessage() { allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) } } } mentionedUsersList := make([]string, 0, len(mentionedUserIds)) senderName := "" var sender *model.User if post.IsSystemMessage() { senderName = c.T("system.message.name") } else if profile, ok := profileMap[post.UserId]; ok { senderName = profile.Username sender = profile } for id := range mentionedUserIds { mentionedUsersList = append(mentionedUsersList, id) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) } if utils.Cfg.EmailSettings.SendEmailNotifications { for _, id := range mentionedUsersList { userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, false, 0} } if userAllowsEmails && status.Status != model.STATUS_ONLINE { sendNotificationEmail(c, post, profileMap[id], channel, team, senderName, sender) } } } if hereNotification { if result := <-Srv.Store.Status().GetOnline(); result.Err != nil { l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err) return } else { statuses := result.Data.([]*model.Status) for _, status := range statuses { if status.UserId == post.UserId { continue } _, profileFound := profileMap[status.UserId] _, isChannelMember := members[status.UserId] _, alreadyMentioned := mentionedUserIds[status.UserId] if status.Status == model.STATUS_ONLINE && profileFound && isChannelMember && !alreadyMentioned { mentionedUsersList = append(mentionedUsersList, status.UserId) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId)) } } } } // Make sure all mention updates are complete to prevent race // Probably better to batch these DB updates in the future // MUST be completed before push notifications send for _, uchan := range updateMentionChans { if result := <-uchan; result.Err != nil { l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) } } sendPushNotifications := false if *utils.Cfg.EmailSettings.SendPushNotifications { pushServer := *utils.Cfg.EmailSettings.PushNotificationServer if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) sendPushNotifications = false } else { sendPushNotifications = true } } if sendPushNotifications { for _, id := range mentionedUsersList { var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, false, 0} } if profileMap[id].StatusAllowsPushNotification(status) { sendPushNotification(post, profileMap[id], channel, senderName, true) } } for _, id := range allActivityPushUserIds { if _, ok := mentionedUserIds[id]; !ok { var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, false, 0} } if profileMap[id].StatusAllowsPushNotification(status) { sendPushNotification(post, profileMap[id], channel, senderName, false) } } } } message := model.NewWebSocketEvent(c.TeamId, post.ChannelId, post.UserId, model.WEBSOCKET_EVENT_POSTED) message.Add("post", post.ToJson()) message.Add("channel_type", channel.Type) message.Add("channel_display_name", channel.DisplayName) message.Add("sender_name", senderName) message.Add("team_id", team.Id) 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 } } } if len(mentionedUsersList) != 0 { message.Add("mentions", model.ArrayToJson(mentionedUsersList)) } go Publish(message) }
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel, profileMap map[string]*model.User, members []model.ChannelMember) { if _, ok := profileMap[post.UserId]; !ok { l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId) return } mentionedUserIds := make(map[string]bool) alwaysNotifyUserIds := []string{} hereNotification := false updateMentionChans := []store.StoreChannel{} if channel.Type == model.CHANNEL_DIRECT { var otherUserId string if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { otherUserId = userIds[1] } else { otherUserId = userIds[0] } mentionedUserIds[otherUserId] = true } else { // Find out who is a member of the channel, only keep those profiles tempProfileMap := make(map[string]*model.User) for _, member := range members { if profile, ok := profileMap[member.UserId]; ok { tempProfileMap[member.UserId] = profile } } profileMap = tempProfileMap // Build map for keywords keywordMap := make(map[string][]string) for _, profile := range profileMap { if len(profile.NotifyProps["mention_keys"]) > 0 { // 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) } } // 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 @channel and @all to keywords if user has them turned on if profile.NotifyProps["channel"] == "true" { keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id) keywordMap["@all"] = append(keywordMap["@all"], profile.Id) } if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && (post.UserId != profile.Id || post.Props["from_webhook"] == "true") && !post.IsSystemMessage() { alwaysNotifyUserIds = append(alwaysNotifyUserIds, 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) var userIds []string for _, word := range splitMessage { if word == "@here" { hereNotification = true continue } // 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...) } } } } if len(post.RootId) > 0 { if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err) return } else { list := result.Data.(*model.PostList) for _, threadPost := range list.Posts { profile := profileMap[threadPost.UserId] if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { userIds = append(userIds, threadPost.UserId) } } } } for _, userId := range userIds { if post.UserId == userId && post.Props["from_webhook"] != "true" { continue } mentionedUserIds[userId] = true } for id := range mentionedUserIds { updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) } } mentionedUsersList := make([]string, 0, len(mentionedUserIds)) senderName := "" if post.IsSystemMessage() { senderName = c.T("system.message.name") } else if profile, ok := profileMap[post.UserId]; ok { senderName = profile.Username } for id := range mentionedUserIds { mentionedUsersList = append(mentionedUsersList, id) } if utils.Cfg.EmailSettings.SendEmailNotifications { for _, id := range mentionedUsersList { userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, 0} } if userAllowsEmails && status.Status != model.STATUS_ONLINE { sendNotificationEmail(c, post, profileMap[id], channel, team, senderName) } } } if hereNotification { if result := <-Srv.Store.Status().GetOnline(); result.Err != nil { l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err) return } else { statuses := result.Data.([]*model.Status) for _, status := range statuses { if status.UserId == post.UserId { continue } _, profileFound := profileMap[status.UserId] _, alreadyAdded := mentionedUserIds[status.UserId] if status.Status == model.STATUS_ONLINE && profileFound && !alreadyAdded { mentionedUsersList = append(mentionedUsersList, status.UserId) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId)) } } } } sendPushNotifications := false if *utils.Cfg.EmailSettings.SendPushNotifications { pushServer := *utils.Cfg.EmailSettings.PushNotificationServer if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) sendPushNotifications = false } else { sendPushNotifications = true } } if sendPushNotifications { for _, id := range mentionedUsersList { if profileMap[id].NotifyProps["push"] != "none" { sendPushNotification(post, profileMap[id], channel, senderName, true) } } for _, id := range alwaysNotifyUserIds { if _, ok := mentionedUserIds[id]; !ok { sendPushNotification(post, profileMap[id], channel, senderName, false) } } } message := model.NewWebSocketEvent(c.TeamId, post.ChannelId, post.UserId, model.WEBSOCKET_EVENT_POSTED) message.Add("post", post.ToJson()) message.Add("channel_type", channel.Type) message.Add("channel_display_name", channel.DisplayName) message.Add("sender_name", senderName) message.Add("team_id", team.Id) 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 } } } if len(mentionedUsersList) != 0 { message.Add("mentions", model.ArrayToJson(mentionedUsersList)) } // Make sure all mention updates are complete to prevent race // Probably better to batch these DB updates in the future for _, uchan := range updateMentionChans { if result := <-uchan; result.Err != nil { l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) } } go Publish(message) }
func SendNotifications(post *model.Post, team *model.Team, channel *model.Channel) ([]string, *model.AppError) { pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true) fchan := Srv.Store.FileInfo().GetForPost(post.Id) var profileMap map[string]*model.User if result := <-pchan; result.Err != nil { return nil, result.Err } else { profileMap = result.Data.(map[string]*model.User) } // If the user who made the post isn't in the channel, don't send a notification if _, ok := profileMap[post.UserId]; !ok { l4g.Debug(utils.T("api.post.send_notifications.user_id.debug"), post.Id, channel.Id, post.UserId) return []string{}, nil } mentionedUserIds := make(map[string]bool) allActivityPushUserIds := []string{} hereNotification := false channelNotification := false allNotification := false updateMentionChans := []store.StoreChannel{} if channel.Type == model.CHANNEL_DIRECT { var otherUserId string if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { otherUserId = userIds[1] } else { otherUserId = userIds[0] } mentionedUserIds[otherUserId] = true if post.Props["from_webhook"] == "true" { mentionedUserIds[post.UserId] = true } } else { keywords := GetMentionKeywordsInChannel(profileMap) var potentialOtherMentions []string mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords) // get users that have comment thread mentions enabled if len(post.RootId) > 0 { if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { return nil, result.Err } else { list := result.Data.(*model.PostList) for _, threadPost := range list.Posts { profile := profileMap[threadPost.UserId] if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { mentionedUserIds[threadPost.UserId] = true } } } } // prevent the user from mentioning themselves if post.Props["from_webhook"] != "true" { delete(mentionedUserIds, post.UserId) } if len(potentialOtherMentions) > 0 { if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil { outOfChannelMentions := result.Data.(map[string]*model.User) go sendOutOfChannelMentions(post, team.Id, outOfChannelMentions) } } // find which users in the channel are set up to always receive mobile notifications for _, profile := range profileMap { if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL && (post.UserId != profile.Id || post.Props["from_webhook"] == "true") && !post.IsSystemMessage() { allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) } } } mentionedUsersList := make([]string, 0, len(mentionedUserIds)) for id := range mentionedUserIds { mentionedUsersList = append(mentionedUsersList, id) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) } var sender *model.User senderName := make(map[string]string) for _, id := range mentionedUsersList { senderName[id] = "" if post.IsSystemMessage() { senderName[id] = utils.T("system.message.name") } else if profile, ok := profileMap[post.UserId]; ok { if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { senderName[id] = value.(string) } else { // Get the Display name preference from the receiver if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil { // Show default sender's name if user doesn't set display settings. senderName[id] = profile.Username } else { senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value) } } sender = profile } } var senderUsername string if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { senderUsername = value.(string) } else { senderUsername = profileMap[post.UserId].Username } if utils.Cfg.EmailSettings.SendEmailNotifications { for _, id := range mentionedUsersList { userAllowsEmails := profileMap[id].NotifyProps["email"] != "false" var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{ UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: "", } } if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 { sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender) } } } T := utils.GetUserTranslations(profileMap[post.UserId].Locale) // If the channel has more than 1K users then @here is disabled if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { hereNotification = false SendEphemeralPost( team.Id, post.UserId, &model.Post{ ChannelId: post.ChannelId, Message: T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), CreateAt: post.CreateAt + 1, }, ) } // If the channel has more than 1K users then @channel is disabled if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { SendEphemeralPost( team.Id, post.UserId, &model.Post{ ChannelId: post.ChannelId, Message: T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), CreateAt: post.CreateAt + 1, }, ) } // If the channel has more than 1K users then @all is disabled if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel { SendEphemeralPost( team.Id, post.UserId, &model.Post{ ChannelId: post.ChannelId, Message: T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}), CreateAt: post.CreateAt + 1, }, ) } if hereNotification { if result := <-Srv.Store.Status().GetOnline(); result.Err != nil { return nil, result.Err } else { statuses := result.Data.([]*model.Status) for _, status := range statuses { if status.UserId == post.UserId { continue } _, profileFound := profileMap[status.UserId] _, alreadyMentioned := mentionedUserIds[status.UserId] if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned { mentionedUsersList = append(mentionedUsersList, status.UserId) updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId)) } } } } // Make sure all mention updates are complete to prevent race // Probably better to batch these DB updates in the future // MUST be completed before push notifications send for _, uchan := range updateMentionChans { if result := <-uchan; result.Err != nil { l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) } } sendPushNotifications := false if *utils.Cfg.EmailSettings.SendPushNotifications { pushServer := *utils.Cfg.EmailSettings.PushNotificationServer if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) sendPushNotifications = false } else { sendPushNotifications = true } } if sendPushNotifications { for _, id := range mentionedUsersList { var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} } if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { sendPushNotification(post, profileMap[id], channel, senderName[id], true) } } for _, id := range allActivityPushUserIds { if _, ok := mentionedUserIds[id]; !ok { var status *model.Status var err *model.AppError if status, err = GetStatus(id); err != nil { status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""} } if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) { sendPushNotification(post, profileMap[id], channel, senderName[id], false) } } } } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil) message.Add("post", post.ToJson()) message.Add("channel_type", channel.Type) message.Add("channel_display_name", channel.DisplayName) message.Add("channel_name", channel.Name) message.Add("sender_name", senderUsername) message.Add("team_id", team.Id) if len(post.FileIds) != 0 { message.Add("otherFile", "true") var infos []*model.FileInfo if result := <-fchan; result.Err != nil { l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err) } else { infos = result.Data.([]*model.FileInfo) } for _, info := range infos { if info.IsImage() { message.Add("image", "true") break } } } if len(mentionedUsersList) != 0 { message.Add("mentions", model.ArrayToJson(mentionedUsersList)) } Publish(message) return mentionedUsersList, nil }