func sendReactionEvent(event string, channelId string, reaction *model.Reaction, postHadReactions bool) { // send out that a reaction has been added/removed go func() { message := model.NewWebSocketEvent(event, "", channelId, "", nil) message.Add("reaction", reaction.ToJson()) app.Publish(message) }() // send out that a post was updated if post.HasReactions has changed go func() { var post *model.Post if result := <-app.Srv.Store.Post().Get(reaction.PostId); result.Err != nil { l4g.Warn(utils.T("api.reaction.send_reaction_event.post.app_error")) return } else { post = result.Data.(*model.PostList).Posts[reaction.PostId] } if post.HasReactions != postHadReactions { message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", channelId, "", nil) message.Add("post", post.ToJson()) app.Publish(message) } }() }
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{} } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil) message.Add("post", post.ToJson()) go Publish(message) }
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 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 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 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 } } 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] } 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 @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) } } // 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 } 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 } } } for id := range toEmailMap { updateMentionCountAndForget(post.ChannelId, id) } } if len(toEmailMap) != 0 { mentionedUsers = make([]string, 0, len(toEmailMap)) for k := range toEmailMap { mentionedUsers = append(mentionedUsers, k) } teamURL := c.GetSiteURL() + "/" + team.Name // Build and send the emails tm := time.Unix(post.CreateAt/1000, 0) for id, doSend := range toEmailMap { 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 := utils.NewHTMLTemplate("post_subject", profileMap[id].Locale) subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, "Month": month[:3], "Day": day, "Year": year}) subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName bodyPage := utils.NewHTMLTemplate("post_body", profileMap[id].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) } 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" } bodyPage.Props["PostMessage"] = userLocale("api.post.send_notifications_and_forget.sent", map[string]interface{}{"Prefix": attachmentPrefix, "Filenames": filenamesString}) } 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) } 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) 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")) } else { 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 msg := model.PushNotification{} if badge := <-Srv.Store.User().GetUnreadCount(id); badge.Err != nil { msg.Badge = 1 l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), id, badge.Err) } else { msg.Badge = int(badge.Data.(int64)) } msg.ServerId = utils.CfgDiagnosticId msg.ChannelId = channel.Id msg.ChannelName = channel.Name 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+":") } if *utils.Cfg.EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION { if channel.Type == model.CHANNEL_DIRECT { msg.Category = model.CATEGORY_DM msg.Message = "@" + senderName + ": " + model.ClearMentionTags(post.Message) } else { msg.Message = "@" + senderName + " @ " + channelName + ": " + model.ClearMentionTags(post.Message) } } else { if channel.Type == model.CHANNEL_DIRECT { msg.Category = model.CATEGORY_DM 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 } } tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, } httpClient := &http.Client{Transport: tr} request, _ := http.NewRequest("POST", pushServer+model.API_URL_SUFFIX_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.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") 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)) } PublishAndForget(message) }
func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) { go func() { // Get a list of user names (to be used as keywords) and ids for the given team uchan := Srv.Store.User().GetProfiles(teamId) echan := Srv.Store.Channel().GetMembers(post.ChannelId) cchan := Srv.Store.Channel().Get(post.ChannelId) tchan := Srv.Store.Team().Get(teamId) var channel *model.Channel var channelName string var bodyText string var subjectText string if result := <-cchan; result.Err != nil { l4g.Error("Failed to retrieve channel channel_id=%v, err=%v", post.ChannelId, result.Err) return } else { channel = result.Data.(*model.Channel) if channel.Type == model.CHANNEL_DIRECT { bodyText = "You have one new message." subjectText = "New Direct Message" } else { bodyText = "You have one new mention." subjectText = "New Mention" channelName = channel.DisplayName } } var mentionedUsers []string if result := <-uchan; result.Err != nil { l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", teamId, result.Err) return } else { profileMap := result.Data.(map[string]*model.User) if _, ok := profileMap[post.UserId]; !ok { l4g.Error("Post user_id not returned by GetProfiles user_id=%v", post.UserId) return } senderName := profileMap[post.UserId].Username 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 } } else { // Find out who is a member of the channel, only keep those profiles if eResult := <-echan; eResult.Err != nil { l4g.Error("Failed to get channel members channel_id=%v err=%v", 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] } 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 @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) } } // 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.FieldsFunc(post.Message, splitF) for _, word := range splitMessage { // Non-case-sensitive check for regular keys userIds1, keyMatch := keywordMap[strings.ToLower(word)] // Case-sensitive check for first name userIds2, firstNameMatch := keywordMap[word] userIds := append(userIds1, userIds2...) // If one of the non-case-senstive keys or the first name matches the word // then we add en entry to the sendEmail map if keyMatch || firstNameMatch { 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 } } } } for id := range toEmailMap { fireAndForgetMentionUpdate(post.ChannelId, id) } } if len(toEmailMap) != 0 { mentionedUsers = make([]string, 0, len(toEmailMap)) for k := range toEmailMap { mentionedUsers = append(mentionedUsers, k) } var teamDisplayName string var teamURL string if result := <-tchan; result.Err != nil { l4g.Error("Failed to retrieve team team_id=%v, err=%v", teamId, result.Err) return } else { teamDisplayName = result.Data.(*model.Team).DisplayName teamURL = siteURL + "/" + result.Data.(*model.Team).Name } // Build and send the emails location, _ := time.LoadLocation("UTC") tm := time.Unix(post.CreateAt/1000, 0).In(location) subjectPage := NewServerTemplatePage("post_subject") subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["TeamDisplayName"] = teamDisplayName subjectPage.Props["SubjectText"] = subjectText subjectPage.Props["Month"] = tm.Month().String()[:3] subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) subjectPage.Props["Year"] = fmt.Sprintf("%d", tm.Year()) for id, doSend := range toEmailMap { if !doSend { continue } // skip if inactive if profileMap[id].DeleteAt > 0 { continue } bodyPage := NewServerTemplatePage("post_body") bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Nickname"] = profileMap[id].FirstName bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["ChannelName"] = channelName bodyPage.Props["BodyText"] = bodyText bodyPage.Props["SenderName"] = senderName bodyPage.Props["Hour"] = fmt.Sprintf("%02d", tm.Hour()) bodyPage.Props["Minute"] = fmt.Sprintf("%02d", tm.Minute()) bodyPage.Props["Month"] = tm.Month().String()[:3] bodyPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.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) } 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" } bodyPage.Props["PostMessage"] = fmt.Sprintf("%s: %s sent", attachmentPrefix, filenamesString) } if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send mention email successfully email=%v err=%v", profileMap[id].Email, err) } if len(utils.Cfg.EmailSettings.ApplePushServer) > 0 { sessionChan := Srv.Store.Session().GetSessions(id) if result := <-sessionChan; result.Err != nil { l4g.Error("Failed to retrieve sessions in notifications id=%v, err=%v", id, result.Err) } else { sessions := result.Data.([]*model.Session) alreadySeen := make(map[string]string) for _, session := range sessions { if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" { alreadySeen[session.DeviceId] = session.DeviceId utils.FireAndForgetSendAppleNotify(session.DeviceId, subjectPage.Render(), 1) } } } } } } } message := model.NewMessage(teamId, post.ChannelId, post.UserId, model.ACTION_POSTED) message.Add("post", post.ToJson()) 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(mentionedUsers) != 0 { message.Add("mentions", model.ArrayToJson(mentionedUsers)) } PublishAndForget(message) }() }
func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) { go func() { // Get a list of user names (to be used as keywords) and ids for the given team uchan := Srv.Store.User().GetProfiles(teamId) echan := Srv.Store.Channel().GetMembers(post.ChannelId) cchan := Srv.Store.Channel().Get(post.ChannelId) tchan := Srv.Store.Team().Get(teamId) var channel *model.Channel var channelName string var bodyText string var subjectText string if result := <-cchan; result.Err != nil { l4g.Error("Failed to retrieve channel channel_id=%v, err=%v", post.ChannelId, result.Err) return } else { channel = result.Data.(*model.Channel) if channel.Type == model.CHANNEL_DIRECT { bodyText = "You have one new message." subjectText = "New Direct Message" } else { bodyText = "You have one new mention." subjectText = "New Mention" channelName = channel.DisplayName } } var mentionedUsers []string if result := <-uchan; result.Err != nil { l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", teamId, result.Err) return } else { profileMap := result.Data.(map[string]*model.User) if _, ok := profileMap[post.UserId]; !ok { l4g.Error("Post user_id not returned by GetProfiles user_id=%v", post.UserId) return } senderName := profileMap[post.UserId].Username 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 } } else { // Find out who is a member of the channel only keep those profiles if eResult := <-echan; eResult.Err != nil { l4g.Error("Failed to get channel members channel_id=%v err=%v", 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] } 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" { splitName := strings.Split(profile.FullName, " ") if len(splitName) > 0 && splitName[0] != "" { keywordMap[splitName[0]] = append(keywordMap[splitName[0]], 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.FieldsFunc(strings.Replace(post.Message, "<br>", " ", -1), splitF) for _, word := range splitMessage { // Non-case-sensitive check for regular keys userIds1, keyMatch := keywordMap[strings.ToLower(word)] // Case-sensitive check for first name userIds2, firstNameMatch := keywordMap[word] userIds := append(userIds1, userIds2...) // If one of the non-case-senstive keys or the first name matches the word // then we add en entry to the sendEmail map if keyMatch || firstNameMatch { 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 } } } } for id, _ := range toEmailMap { fireAndForgetMentionUpdate(post.ChannelId, id) } } if len(toEmailMap) != 0 { mentionedUsers = make([]string, 0, len(toEmailMap)) for k := range toEmailMap { mentionedUsers = append(mentionedUsers, k) } var teamName string if result := <-tchan; result.Err != nil { l4g.Error("Failed to retrieve team team_id=%v, err=%v", teamId, result.Err) return } else { teamName = result.Data.(*model.Team).Name } // Build and send the emails location, _ := time.LoadLocation("UTC") tm := time.Unix(post.CreateAt/1000, 0).In(location) subjectPage := NewServerTemplatePage("post_subject", teamUrl) subjectPage.Props["TeamName"] = teamName subjectPage.Props["SubjectText"] = subjectText subjectPage.Props["Month"] = tm.Month().String()[:3] subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) subjectPage.Props["Year"] = fmt.Sprintf("%d", tm.Year()) for id, doSend := range toEmailMap { if !doSend { continue } // skip if inactive if profileMap[id].DeleteAt > 0 { continue } firstName := strings.Split(profileMap[id].FullName, " ")[0] bodyPage := NewServerTemplatePage("post_body", teamUrl) bodyPage.Props["FullName"] = firstName bodyPage.Props["TeamName"] = teamName bodyPage.Props["ChannelName"] = channelName bodyPage.Props["BodyText"] = bodyText bodyPage.Props["SenderName"] = senderName bodyPage.Props["Hour"] = fmt.Sprintf("%02d", tm.Hour()) bodyPage.Props["Minute"] = fmt.Sprintf("%02d", tm.Minute()) bodyPage.Props["Month"] = tm.Month().String()[:3] bodyPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) bodyPage.Props["TeamLink"] = teamUrl + "/channels/" + channel.Name if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send mention email successfully email=%v err=%v", profileMap[id].Email, err) } if len(utils.Cfg.EmailSettings.ApplePushServer) > 0 { sessionChan := Srv.Store.Session().GetSessions(id) if result := <-sessionChan; result.Err != nil { l4g.Error("Failed to retrieve sessions in notifications id=%v, err=%v", id, result.Err) } else { sessions := result.Data.([]*model.Session) alreadySeen := make(map[string]string) for _, session := range sessions { if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" { alreadySeen[session.DeviceId] = session.DeviceId utils.FireAndForgetSendAppleNotify(session.DeviceId, subjectPage.Render(), 1) } } } } } } } message := model.NewMessage(teamId, post.ChannelId, post.UserId, model.ACTION_POSTED) message.Add("post", post.ToJson()) if len(mentionedUsers) != 0 { message.Add("mentions", model.ArrayToJson(mentionedUsers)) } store.PublishAndForget(message) }() }
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 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 sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel) { 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 if channel.Type == model.CHANNEL_DIRECT { bodyText = "You have one new message." subjectText = "New Direct Message" } else { bodyText = "You have one new mention." subjectText = "New Mention" channelName = channel.DisplayName } var mentionedUsers []string if result := <-uchan; result.Err != nil { l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", c.Session.TeamId, result.Err) return } else { profileMap := result.Data.(map[string]*model.User) if _, ok := profileMap[post.UserId]; !ok { l4g.Error("Post user_id not returned by GetProfiles user_id=%v", post.UserId) return } senderName := profileMap[post.UserId].Username 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 } } else { // Find out who is a member of the channel, only keep those profiles if eResult := <-echan; eResult.Err != nil { l4g.Error("Failed to get channel members channel_id=%v err=%v", 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] } 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 @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) } } // 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 } } } for id := range toEmailMap { updateMentionCountAndForget(post.ChannelId, id) } } if len(toEmailMap) != 0 { mentionedUsers = make([]string, 0, len(toEmailMap)) for k := range toEmailMap { mentionedUsers = append(mentionedUsers, k) } teamURL := c.GetSiteURL() + "/" + team.Name // Build and send the emails tm := time.Unix(post.CreateAt/1000, 0) subjectPage := NewServerTemplatePage("post_subject") subjectPage.Props["SiteURL"] = c.GetSiteURL() subjectPage.Props["TeamDisplayName"] = team.DisplayName subjectPage.Props["SubjectText"] = subjectText subjectPage.Props["Month"] = tm.Month().String()[:3] subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) subjectPage.Props["Year"] = fmt.Sprintf("%d", tm.Year()) for id, doSend := range toEmailMap { if !doSend { continue } // skip if inactive if profileMap[id].DeleteAt > 0 { continue } bodyPage := NewServerTemplatePage("post_body") bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Nickname"] = profileMap[id].FirstName bodyPage.Props["TeamDisplayName"] = team.DisplayName bodyPage.Props["ChannelName"] = channelName bodyPage.Props["BodyText"] = bodyText bodyPage.Props["SenderName"] = senderName bodyPage.Props["Hour"] = fmt.Sprintf("%02d", tm.Hour()) bodyPage.Props["Minute"] = fmt.Sprintf("%02d", tm.Minute()) bodyPage.Props["Month"] = tm.Month().String()[:3] bodyPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) bodyPage.Props["TimeZone"], _ = tm.Zone() bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.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) } 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" } bodyPage.Props["PostMessage"] = fmt.Sprintf("%s: %s sent", attachmentPrefix, filenamesString) } if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send mention email successfully email=%v err=%v", profileMap[id].Email, err) } if *utils.Cfg.EmailSettings.SendPushNotifications { sessionChan := Srv.Store.Session().GetSessions(id) if result := <-sessionChan; result.Err != nil { l4g.Error("Failed to retrieve sessions in notifications id=%v, err=%v", id, result.Err) } else { sessions := result.Data.([]*model.Session) alreadySeen := make(map[string]string) for _, session := range sessions { if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" && strings.HasPrefix(session.DeviceId, "apple:") { alreadySeen[session.DeviceId] = session.DeviceId msg := model.PushNotification{} msg.Platform = model.PUSH_NOTIFY_APPLE msg.Badge = 1 msg.DeviceId = strings.TrimPrefix(session.DeviceId, "apple:") msg.ServerId = utils.CfgDiagnosticId if channel.Type == model.CHANNEL_DIRECT { msg.Message = senderName + " sent you a direct message" } else { msg.Message = senderName + " mentioned you in " + channelName } httpClient := http.Client{} request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+"/api/v1/send_push", strings.NewReader(msg.ToJson())) l4g.Debug("Sending push notification to " + msg.DeviceId + " with msg of '" + msg.Message + "'") if _, err := httpClient.Do(request); err != nil { l4g.Error("Failed to send push notificationid=%v, err=%v", id, err) } } } } } } } } message := model.NewMessage(c.Session.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED) message.Add("post", post.ToJson()) 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(mentionedUsers) != 0 { message.Add("mentions", model.ArrayToJson(mentionedUsers)) } PublishAndForget(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 }