summaryrefslogtreecommitdiffstats
path: root/app/notification.go
diff options
context:
space:
mode:
Diffstat (limited to 'app/notification.go')
-rw-r--r--app/notification.go732
1 files changed, 732 insertions, 0 deletions
diff --git a/app/notification.go b/app/notification.go
new file mode 100644
index 000000000..d5e3c7b13
--- /dev/null
+++ b/app/notification.go
@@ -0,0 +1,732 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "crypto/tls"
+ "fmt"
+ "html"
+ "html/template"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+ "github.com/nicksnyder/go-i18n/i18n"
+)
+
+func SendNotifications(post *model.Post, team *model.Team, channel *model.Channel) ([]string, *model.AppError) {
+ mentionedUsersList := make([]string, 0)
+ var fchan store.StoreChannel
+ var senderUsername string
+
+ if post.IsSystemMessage() {
+ senderUsername = utils.T("system.message.name")
+ } else {
+ 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 {
+ if profile, ok := profileMap[threadPost.UserId]; ok {
+ 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") {
+ 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 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
+ }
+ }
+
+ 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 {
+ if err := sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender); err != nil {
+ l4g.Error(err.Error())
+ }
+ }
+ }
+ }
+
+ 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 {
+ statuses := GetAllStatuses()
+ 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) {
+ if err := sendPushNotification(post, profileMap[id], channel, senderName[id], true); err != nil {
+ l4g.Error(err.Error())
+ }
+ }
+ }
+
+ 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) {
+ if err := sendPushNotification(post, profileMap[id], channel, senderName[id], false); err != nil {
+ l4g.Error(err.Error())
+ }
+ }
+ }
+ }
+ }
+ }
+
+ 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 && fchan != nil {
+ 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
+}
+
+func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError {
+
+ if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id {
+ // this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link
+ if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil {
+ return result.Err
+ } else {
+ // if the recipient isn't in the current user's team, just pick one
+ teams := result.Data.([]*model.Team)
+ found := false
+
+ for i := range teams {
+ if teams[i].Id == team.Id {
+ found = true
+ team = teams[i]
+ break
+ }
+ }
+
+ if !found {
+ if len(teams) > 0 {
+ team = teams[0]
+ } else {
+ // in case the user hasn't joined any teams we send them to the select_team page
+ team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName}
+ }
+ }
+ }
+ }
+ if *utils.Cfg.EmailSettings.EnableEmailBatching {
+ var sendBatched bool
+
+ if result := <-Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.Err != nil {
+ // if the call fails, assume it hasn't been set and use the default
+ sendBatched = false
+ } else {
+ // default to not using batching if the setting is set to immediate
+ sendBatched = result.Data.(model.Preference).Value != model.PREFERENCE_DEFAULT_EMAIL_INTERVAL
+ }
+
+ if sendBatched {
+ if err := AddNotificationEmailToBatch(user, post, team); err == nil {
+ return nil
+ }
+ }
+
+ // fall back to sending a single email if we can't batch it for some reason
+ }
+
+ var channelName string
+ var bodyText string
+ var subjectText string
+ var mailTemplate string
+ var mailParameters map[string]interface{}
+
+ teamURL := utils.GetSiteURL() + "/" + team.Name
+ tm := time.Unix(post.CreateAt/1000, 0)
+
+ userLocale := utils.GetUserTranslations(user.Locale)
+ month := userLocale(tm.Month().String())
+ day := fmt.Sprintf("%d", tm.Day())
+ year := fmt.Sprintf("%d", tm.Year())
+ zone, _ := tm.Zone()
+
+ 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")
+
+ senderDisplayName := senderName
+
+ mailTemplate = "api.templates.post_subject_in_direct_message"
+ mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
+ "SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year}
+ } else {
+ bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
+ subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject")
+ channelName = channel.DisplayName
+ mailTemplate = "api.templates.post_subject_in_channel"
+ mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
+ "ChannelName": channelName, "Month": month, "Day": day, "Year": year}
+ }
+
+ subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, userLocale(mailTemplate, mailParameters))
+
+ bodyPage := utils.NewHTMLTemplate("post_body", user.Locale)
+ bodyPage.Props["SiteURL"] = utils.GetSiteURL()
+ bodyPage.Props["PostMessage"] = GetMessageForNotification(post, userLocale)
+ if team.Name != "select_team" {
+ bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id
+ } else {
+ bodyPage.Props["TeamLink"] = teamURL
+ }
+
+ 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}))
+
+ if err := utils.SendMail(user.Email, html.UnescapeString(subject), bodyPage.Render()); err != nil {
+ return err
+ }
+
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementPostSentEmail()
+ }
+
+ return nil
+}
+
+func GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
+ if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 {
+ return post.Message
+ }
+
+ // extract the filenames from their paths and determine what type of files are attached
+ var infos []*model.FileInfo
+ if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil {
+ l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err)
+ } else {
+ infos = result.Data.([]*model.FileInfo)
+ }
+
+ filenames := make([]string, len(infos))
+ onlyImages := true
+ for i, info := range infos {
+ if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
+ // this should never error since filepath was escaped using url.QueryEscape
+ filenames[i] = escaped
+ } else {
+ filenames[i] = info.Name
+ }
+
+ onlyImages = onlyImages && info.IsImage()
+ }
+
+ props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")}
+
+ if onlyImages {
+ return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
+ } else {
+ return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
+ }
+}
+
+func sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName string, wasMentioned bool) *model.AppError {
+ sessions, err := getMobileAppSessions(user.Id)
+ if err != nil {
+ return err
+ }
+
+ var channelName string
+
+ if channel.Type == model.CHANNEL_DIRECT {
+ channelName = senderName
+ } else {
+ channelName = channel.DisplayName
+ }
+
+ userLocale := utils.GetUserTranslations(user.Locale)
+
+ msg := model.PushNotification{}
+ if badge := <-Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil {
+ msg.Badge = 1
+ l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err)
+ } else {
+ msg.Badge = int(badge.Data.(int64))
+ }
+ msg.Type = model.PUSH_TYPE_MESSAGE
+ msg.TeamId = channel.TeamId
+ msg.ChannelId = channel.Id
+ msg.ChannelName = channel.Name
+
+ 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 + userLocale("api.post.send_notifications_and_forget.push_in") + 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 if wasMentioned {
+ msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
+ } else {
+ msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
+ }
+ }
+
+ l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message)
+
+ for _, session := range sessions {
+ tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
+ tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
+ if err := sendToPushProxy(tmpMessage); err != nil {
+ l4g.Error(err.Error)
+ }
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementPostSentPush()
+ }
+ }
+
+ return nil
+}
+
+func ClearPushNotification(userId string, channelId string) *model.AppError {
+ sessions, err := getMobileAppSessions(userId)
+ if err != nil {
+ return err
+ }
+
+ msg := model.PushNotification{}
+ msg.Type = model.PUSH_TYPE_CLEAR
+ msg.ChannelId = channelId
+ msg.ContentAvailable = 0
+ if badge := <-Srv.Store.User().GetUnreadCount(userId); badge.Err != nil {
+ msg.Badge = 0
+ l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err)
+ } else {
+ msg.Badge = int(badge.Data.(int64))
+ }
+
+ l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId)
+ for _, session := range sessions {
+ tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
+ tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
+ if err := sendToPushProxy(tmpMessage); err != nil {
+ l4g.Error(err.Error)
+ }
+ }
+
+ return nil
+}
+
+func sendToPushProxy(msg model.PushNotification) *model.AppError {
+ msg.ServerId = utils.CfgDiagnosticId
+
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
+ }
+ httpClient := &http.Client{Transport: tr}
+ request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson()))
+
+ if resp, err := httpClient.Do(request); err != nil {
+ return model.NewLocAppError("sendToPushProxy", "api.post.send_notifications_and_forget.push_notification.error", map[string]interface{}{"DeviceId": msg.DeviceId, "Error": err.Error()}, "")
+ } else {
+ ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ }
+
+ return nil
+}
+
+func getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) {
+ if result := <-Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Session), nil
+ }
+}
+
+func sendOutOfChannelMentions(post *model.Post, teamId string, profiles map[string]*model.User) *model.AppError {
+ if len(profiles) == 0 {
+ return nil
+ }
+
+ var usernames []string
+ for _, user := range profiles {
+ usernames = append(usernames, user.Username)
+ }
+ sort.Strings(usernames)
+
+ T := utils.GetUserTranslations(profiles[post.UserId].Locale)
+
+ var message string
+ if len(usernames) == 1 {
+ message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
+ "Username": usernames[0],
+ })
+ } else {
+ message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
+ "Usernames": strings.Join(usernames[:len(usernames)-1], ", "),
+ "LastUsername": usernames[len(usernames)-1],
+ })
+ }
+
+ SendEphemeralPost(
+ teamId,
+ post.UserId,
+ &model.Post{
+ ChannelId: post.ChannelId,
+ Message: message,
+ CreateAt: post.CreateAt + 1,
+ },
+ )
+
+ return nil
+}
+
+// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
+// users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
+func GetExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool, bool, bool) {
+ mentioned := make(map[string]bool)
+ potentialOthersMentioned := make([]string, 0)
+ systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
+ hereMentioned := false
+ allMentioned := false
+ channelMentioned := false
+
+ addMentionedUsers := func(ids []string) {
+ for _, id := range ids {
+ mentioned[id] = true
+ }
+ }
+
+ for _, word := range strings.Fields(message) {
+ isMention := false
+
+ if word == "@here" {
+ hereMentioned = true
+ }
+
+ if word == "@channel" {
+ channelMentioned = true
+ }
+
+ if word == "@all" {
+ allMentioned = true
+ }
+
+ // Non-case-sensitive check for regular keys
+ if ids, match := keywords[strings.ToLower(word)]; match {
+ addMentionedUsers(ids)
+ isMention = true
+ }
+
+ // Case-sensitive check for first name
+ if ids, match := keywords[word]; match {
+ addMentionedUsers(ids)
+ isMention = true
+ }
+
+ if !isMention {
+ // No matches were found with the string split just on whitespace so try further splitting
+ // the message on punctuation
+ splitWords := strings.FieldsFunc(word, func(c rune) bool {
+ return model.SplitRunes[c]
+ })
+
+ for _, splitWord := range splitWords {
+ if splitWord == "@here" {
+ hereMentioned = true
+ }
+
+ if splitWord == "@all" {
+ allMentioned = true
+ }
+
+ if splitWord == "@channel" {
+ channelMentioned = true
+ }
+
+ // Non-case-sensitive check for regular keys
+ if ids, match := keywords[strings.ToLower(splitWord)]; match {
+ addMentionedUsers(ids)
+ }
+
+ // Case-sensitive check for first name
+ if ids, match := keywords[splitWord]; match {
+ addMentionedUsers(ids)
+ } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
+ username := word[1:len(splitWord)]
+ potentialOthersMentioned = append(potentialOthersMentioned, username)
+ }
+ }
+ }
+ }
+
+ return mentioned, potentialOthersMentioned, hereMentioned, channelMentioned, allMentioned
+}
+
+// Given a map of user IDs to profiles, returns a list of mention
+// keywords for all users in the channel.
+func GetMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]string {
+ keywords := make(map[string][]string)
+
+ for id, profile := range profiles {
+ userMention := "@" + strings.ToLower(profile.Username)
+ keywords[userMention] = append(keywords[userMention], id)
+
+ 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 {
+ // note that these are made lower case so that we can do a case insensitive check for them
+ key := strings.ToLower(k)
+ keywords[key] = append(keywords[key], id)
+ }
+ }
+
+ // If turned on, add the user's case sensitive first name
+ if profile.NotifyProps["first_name"] == "true" {
+ keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
+ }
+
+ // Add @channel and @all to keywords if user has them turned on
+ if int64(len(profiles)) < *utils.Cfg.TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" {
+ keywords["@channel"] = append(keywords["@channel"], profile.Id)
+ keywords["@all"] = append(keywords["@all"], profile.Id)
+ }
+ }
+
+ return keywords
+}