github.com/xzl8028/xenia-server@v0.0.0-20190809101854-18450a97da63/app/notification.go (about)

     1  // Copyright (c) 2016-present Xenia, Inc. All Rights Reserved.
     2  // See License.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  	"strings"
    10  	"unicode"
    11  	"unicode/utf8"
    12  
    13  	"github.com/xzl8028/xenia-server/mlog"
    14  	"github.com/xzl8028/xenia-server/model"
    15  	"github.com/xzl8028/xenia-server/store"
    16  	"github.com/xzl8028/xenia-server/utils"
    17  	"github.com/xzl8028/xenia-server/utils/markdown"
    18  )
    19  
    20  const (
    21  	THREAD_ANY  = "any"
    22  	THREAD_ROOT = "root"
    23  )
    24  
    25  func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList) ([]string, error) {
    26  	// Do not send notifications in archived channels
    27  	if channel.DeleteAt > 0 {
    28  		return []string{}, nil
    29  	}
    30  
    31  	pchan := a.Srv.Store.User().GetAllProfilesInChannel(channel.Id, true)
    32  
    33  	cmnchan := make(chan store.StoreResult, 1)
    34  	go func() {
    35  		props, err := a.Srv.Store.Channel().GetAllChannelMembersNotifyPropsForChannel(channel.Id, true)
    36  		cmnchan <- store.StoreResult{Data: props, Err: err}
    37  		close(cmnchan)
    38  	}()
    39  
    40  	var fchan chan store.StoreResult
    41  	if len(post.FileIds) != 0 {
    42  		fchan = make(chan store.StoreResult, 1)
    43  		go func() {
    44  			fileInfos, err := a.Srv.Store.FileInfo().GetForPost(post.Id, true, true)
    45  			fchan <- store.StoreResult{Data: fileInfos, Err: err}
    46  			close(fchan)
    47  		}()
    48  	}
    49  
    50  	result := <-pchan
    51  	if result.Err != nil {
    52  		return nil, result.Err
    53  	}
    54  	profileMap := result.Data.(map[string]*model.User)
    55  
    56  	result = <-cmnchan
    57  	if result.Err != nil {
    58  		return nil, result.Err
    59  	}
    60  	channelMemberNotifyPropsMap := result.Data.(map[string]model.StringMap)
    61  
    62  	mentionedUserIds := make(map[string]bool)
    63  	threadMentionedUserIds := make(map[string]string)
    64  	allActivityPushUserIds := []string{}
    65  	hereNotification := false
    66  	channelNotification := false
    67  	allNotification := false
    68  	updateMentionChans := []chan *model.AppError{}
    69  
    70  	if channel.Type == model.CHANNEL_DIRECT {
    71  		var otherUserId string
    72  
    73  		userIds := strings.Split(channel.Name, "__")
    74  
    75  		if userIds[0] != userIds[1] {
    76  			if userIds[0] == post.UserId {
    77  				otherUserId = userIds[1]
    78  			} else {
    79  				otherUserId = userIds[0]
    80  			}
    81  		}
    82  
    83  		otherUser, ok := profileMap[otherUserId]
    84  		if ok {
    85  			mentionedUserIds[otherUserId] = true
    86  		}
    87  
    88  		if post.Props["from_webhook"] == "true" {
    89  			mentionedUserIds[post.UserId] = true
    90  		}
    91  
    92  		if post.Type != model.POST_AUTO_RESPONDER {
    93  			a.Srv.Go(func() {
    94  				a.SendAutoResponse(channel, otherUser)
    95  			})
    96  		}
    97  
    98  	} else {
    99  		keywords := a.getMentionKeywordsInChannel(profileMap, post.Type != model.POST_HEADER_CHANGE && post.Type != model.POST_PURPOSE_CHANGE, channelMemberNotifyPropsMap)
   100  
   101  		m := getExplicitMentions(post, keywords)
   102  
   103  		// Add an implicit mention when a user is added to a channel
   104  		// even if the user has set 'username mentions' to false in account settings.
   105  		if post.Type == model.POST_ADD_TO_CHANNEL {
   106  			val := post.Props[model.POST_PROPS_ADDED_USER_ID]
   107  			if val != nil {
   108  				uid := val.(string)
   109  				m.MentionedUserIds[uid] = true
   110  			}
   111  		}
   112  
   113  		mentionedUserIds, hereNotification, channelNotification, allNotification = m.MentionedUserIds, m.HereMentioned, m.ChannelMentioned, m.AllMentioned
   114  
   115  		// get users that have comment thread mentions enabled
   116  		if len(post.RootId) > 0 && parentPostList != nil {
   117  			for _, threadPost := range parentPostList.Posts {
   118  				profile := profileMap[threadPost.UserId]
   119  				if profile != nil && (profile.NotifyProps[model.COMMENTS_NOTIFY_PROP] == THREAD_ANY || (profile.NotifyProps[model.COMMENTS_NOTIFY_PROP] == THREAD_ROOT && threadPost.Id == parentPostList.Order[0])) {
   120  					if threadPost.Id == parentPostList.Order[0] {
   121  						threadMentionedUserIds[threadPost.UserId] = THREAD_ROOT
   122  					} else {
   123  						threadMentionedUserIds[threadPost.UserId] = THREAD_ANY
   124  					}
   125  
   126  					if _, ok := mentionedUserIds[threadPost.UserId]; !ok {
   127  						mentionedUserIds[threadPost.UserId] = false
   128  					}
   129  				}
   130  			}
   131  		}
   132  
   133  		// prevent the user from mentioning themselves
   134  		if post.Props["from_webhook"] != "true" {
   135  			delete(mentionedUserIds, post.UserId)
   136  		}
   137  
   138  		if len(m.OtherPotentialMentions) > 0 && !post.IsSystemMessage() {
   139  			if profilesResult := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, &model.ViewUsersRestrictions{Teams: []string{team.Id}}); profilesResult.Err == nil {
   140  				channelMentions := model.UserSlice(profilesResult.Data.([]*model.User)).FilterByActive(true)
   141  
   142  				var outOfChannelMentions model.UserSlice
   143  				var outOfGroupsMentions model.UserSlice
   144  
   145  				if channel.IsGroupConstrained() {
   146  					nonMemberIDs, err := a.FilterNonGroupChannelMembers(channelMentions.IDs(), channel)
   147  					if err != nil {
   148  						return nil, err
   149  					}
   150  
   151  					outOfChannelMentions = channelMentions.FilterWithoutID(nonMemberIDs)
   152  					outOfGroupsMentions = channelMentions.FilterByID(nonMemberIDs)
   153  				} else {
   154  					outOfChannelMentions = channelMentions
   155  				}
   156  
   157  				if channel.Type != model.CHANNEL_GROUP {
   158  					a.Srv.Go(func() {
   159  						a.sendOutOfChannelMentions(sender, post, outOfChannelMentions, outOfGroupsMentions)
   160  					})
   161  				}
   162  			}
   163  		}
   164  
   165  		// find which users in the channel are set up to always receive mobile notifications
   166  		for _, profile := range profileMap {
   167  			if (profile.NotifyProps[model.PUSH_NOTIFY_PROP] == model.USER_NOTIFY_ALL ||
   168  				channelMemberNotifyPropsMap[profile.Id][model.PUSH_NOTIFY_PROP] == model.CHANNEL_NOTIFY_ALL) &&
   169  				(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
   170  				!post.IsSystemMessage() {
   171  				allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
   172  			}
   173  		}
   174  	}
   175  
   176  	mentionedUsersList := make([]string, 0, len(mentionedUserIds))
   177  	for id := range mentionedUserIds {
   178  		mentionedUsersList = append(mentionedUsersList, id)
   179  		umc := make(chan *model.AppError, 1)
   180  		go func() {
   181  			umc <- a.Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)
   182  			close(umc)
   183  		}()
   184  		updateMentionChans = append(updateMentionChans, umc)
   185  	}
   186  
   187  	notification := &postNotification{
   188  		post:       post,
   189  		channel:    channel,
   190  		profileMap: profileMap,
   191  		sender:     sender,
   192  	}
   193  
   194  	if *a.Config().EmailSettings.SendEmailNotifications {
   195  		for _, id := range mentionedUsersList {
   196  			if profileMap[id] == nil {
   197  				continue
   198  			}
   199  
   200  			userAllowsEmails := profileMap[id].NotifyProps[model.EMAIL_NOTIFY_PROP] != "false"
   201  			if channelEmail, ok := channelMemberNotifyPropsMap[id][model.EMAIL_NOTIFY_PROP]; ok {
   202  				if channelEmail != model.CHANNEL_NOTIFY_DEFAULT {
   203  					userAllowsEmails = channelEmail != "false"
   204  				}
   205  			}
   206  
   207  			// Remove the user as recipient when the user has muted the channel.
   208  			if channelMuted, ok := channelMemberNotifyPropsMap[id][model.MARK_UNREAD_NOTIFY_PROP]; ok {
   209  				if channelMuted == model.CHANNEL_MARK_UNREAD_MENTION {
   210  					mlog.Debug(fmt.Sprintf("Channel muted for user_id %v, channel_mute %v", id, channelMuted))
   211  					userAllowsEmails = false
   212  				}
   213  			}
   214  
   215  			//If email verification is required and user email is not verified don't send email.
   216  			if *a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified {
   217  				mlog.Error(fmt.Sprintf("Skipped sending notification email to %v, address not verified. [details: user_id=%v]", profileMap[id].Email, id))
   218  				continue
   219  			}
   220  
   221  			var status *model.Status
   222  			var err *model.AppError
   223  			if status, err = a.GetStatus(id); err != nil {
   224  				status = &model.Status{
   225  					UserId:         id,
   226  					Status:         model.STATUS_OFFLINE,
   227  					Manual:         false,
   228  					LastActivityAt: 0,
   229  					ActiveChannel:  "",
   230  				}
   231  			}
   232  
   233  			autoResponderRelated := status.Status == model.STATUS_OUT_OF_OFFICE || post.Type == model.POST_AUTO_RESPONDER
   234  
   235  			if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 && !autoResponderRelated {
   236  				a.sendNotificationEmail(notification, profileMap[id], team)
   237  			}
   238  		}
   239  	}
   240  
   241  	T := utils.GetUserTranslations(sender.Locale)
   242  
   243  	// If the channel has more than 1K users then @here is disabled
   244  	if hereNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
   245  		hereNotification = false
   246  		a.SendEphemeralPost(
   247  			post.UserId,
   248  			&model.Post{
   249  				ChannelId: post.ChannelId,
   250  				Message:   T("api.post.disabled_here", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   251  				CreateAt:  post.CreateAt + 1,
   252  			},
   253  		)
   254  	}
   255  
   256  	// If the channel has more than 1K users then @channel is disabled
   257  	if channelNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
   258  		a.SendEphemeralPost(
   259  			post.UserId,
   260  			&model.Post{
   261  				ChannelId: post.ChannelId,
   262  				Message:   T("api.post.disabled_channel", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   263  				CreateAt:  post.CreateAt + 1,
   264  			},
   265  		)
   266  	}
   267  
   268  	// If the channel has more than 1K users then @all is disabled
   269  	if allNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
   270  		a.SendEphemeralPost(
   271  			post.UserId,
   272  			&model.Post{
   273  				ChannelId: post.ChannelId,
   274  				Message:   T("api.post.disabled_all", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   275  				CreateAt:  post.CreateAt + 1,
   276  			},
   277  		)
   278  	}
   279  
   280  	// Make sure all mention updates are complete to prevent race
   281  	// Probably better to batch these DB updates in the future
   282  	// MUST be completed before push notifications send
   283  	for _, umc := range updateMentionChans {
   284  		if err := <-umc; err != nil {
   285  			mlog.Warn(fmt.Sprintf("Failed to update mention count, post_id=%v channel_id=%v err=%v", post.Id, post.ChannelId, result.Err), mlog.String("post_id", post.Id))
   286  		}
   287  	}
   288  
   289  	sendPushNotifications := false
   290  	if *a.Config().EmailSettings.SendPushNotifications {
   291  		pushServer := *a.Config().EmailSettings.PushNotificationServer
   292  		if license := a.License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
   293  			mlog.Warn("Push notifications are disabled. Go to System Console > Notifications > Mobile Push to enable them.")
   294  			sendPushNotifications = false
   295  		} else {
   296  			sendPushNotifications = true
   297  		}
   298  	}
   299  
   300  	if sendPushNotifications {
   301  		for _, id := range mentionedUsersList {
   302  			if profileMap[id] == nil {
   303  				continue
   304  			}
   305  
   306  			var status *model.Status
   307  			var err *model.AppError
   308  			if status, err = a.GetStatus(id); err != nil {
   309  				status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
   310  			}
   311  
   312  			if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
   313  				replyToThreadType := ""
   314  				if value, ok := threadMentionedUserIds[id]; ok {
   315  					replyToThreadType = value
   316  				}
   317  
   318  				a.sendPushNotification(
   319  					notification,
   320  					profileMap[id],
   321  					mentionedUserIds[id],
   322  					(channelNotification || hereNotification || allNotification),
   323  					replyToThreadType,
   324  				)
   325  			} else {
   326  				// register that a notification was not sent
   327  				a.NotificationsLog.Warn("Notification not sent",
   328  					mlog.String("ackId", ""),
   329  					mlog.String("type", model.PUSH_TYPE_MESSAGE),
   330  					mlog.String("userId", id),
   331  					mlog.String("postId", post.Id),
   332  					mlog.String("status", model.PUSH_NOT_SENT),
   333  				)
   334  			}
   335  		}
   336  
   337  		for _, id := range allActivityPushUserIds {
   338  			if profileMap[id] == nil {
   339  				continue
   340  			}
   341  
   342  			if _, ok := mentionedUserIds[id]; !ok {
   343  				var status *model.Status
   344  				var err *model.AppError
   345  				if status, err = a.GetStatus(id); err != nil {
   346  					status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
   347  				}
   348  
   349  				if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
   350  					a.sendPushNotification(
   351  						notification,
   352  						profileMap[id],
   353  						false,
   354  						false,
   355  						"",
   356  					)
   357  				} else {
   358  					// register that a notification was not sent
   359  					a.NotificationsLog.Warn("Notification not sent",
   360  						mlog.String("ackId", ""),
   361  						mlog.String("type", model.PUSH_TYPE_MESSAGE),
   362  						mlog.String("userId", id),
   363  						mlog.String("postId", post.Id),
   364  						mlog.String("status", model.PUSH_NOT_SENT),
   365  					)
   366  				}
   367  			}
   368  		}
   369  	}
   370  
   371  	message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil)
   372  
   373  	// Note that PreparePostForClient should've already been called by this point
   374  	message.Add("post", post.ToJson())
   375  
   376  	message.Add("channel_type", channel.Type)
   377  	message.Add("channel_display_name", notification.GetChannelName(model.SHOW_USERNAME, ""))
   378  	message.Add("channel_name", channel.Name)
   379  	message.Add("sender_name", notification.GetSenderName(model.SHOW_USERNAME, *a.Config().ServiceSettings.EnablePostUsernameOverride))
   380  	message.Add("team_id", team.Id)
   381  
   382  	if len(post.FileIds) != 0 && fchan != nil {
   383  		message.Add("otherFile", "true")
   384  
   385  		var infos []*model.FileInfo
   386  		if result := <-fchan; result.Err != nil {
   387  			mlog.Warn(fmt.Sprint("Unable to get fileInfo for push notifications.", post.Id, result.Err), mlog.String("post_id", post.Id))
   388  		} else {
   389  			infos = result.Data.([]*model.FileInfo)
   390  		}
   391  
   392  		for _, info := range infos {
   393  			if info.IsImage() {
   394  				message.Add("image", "true")
   395  				break
   396  			}
   397  		}
   398  	}
   399  
   400  	if len(mentionedUsersList) != 0 {
   401  		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
   402  	}
   403  
   404  	a.Publish(message)
   405  	return mentionedUsersList, nil
   406  }
   407  
   408  func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, outOfChannelUsers, outOfGroupsUsers []*model.User) *model.AppError {
   409  	if len(outOfChannelUsers) == 0 && len(outOfGroupsUsers) == 0 {
   410  		return nil
   411  	}
   412  
   413  	allUsers := model.UserSlice(append(outOfChannelUsers, outOfGroupsUsers...))
   414  
   415  	ocUsers := model.UserSlice(outOfChannelUsers)
   416  	ocUsernames := ocUsers.Usernames()
   417  	ocUserIDs := ocUsers.IDs()
   418  
   419  	ogUsers := model.UserSlice(outOfGroupsUsers)
   420  	ogUsernames := ogUsers.Usernames()
   421  
   422  	T := utils.GetUserTranslations(sender.Locale)
   423  
   424  	ephemeralPostId := model.NewId()
   425  	var message string
   426  	if len(outOfChannelUsers) == 1 {
   427  		message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
   428  			"Username": ocUsernames[0],
   429  		})
   430  	} else if len(outOfChannelUsers) > 1 {
   431  		preliminary, final := splitAtFinal(ocUsernames)
   432  
   433  		message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
   434  			"Usernames":    strings.Join(preliminary, ", @"),
   435  			"LastUsername": final,
   436  		})
   437  	}
   438  
   439  	if len(outOfGroupsUsers) == 1 {
   440  		if len(message) > 0 {
   441  			message += "\n"
   442  		}
   443  
   444  		message += T("api.post.check_for_out_of_channel_groups_mentions.message.one", map[string]interface{}{
   445  			"Username": ogUsernames[0],
   446  		})
   447  	} else if len(outOfGroupsUsers) > 1 {
   448  		preliminary, final := splitAtFinal(ogUsernames)
   449  
   450  		if len(message) > 0 {
   451  			message += "\n"
   452  		}
   453  
   454  		message += T("api.post.check_for_out_of_channel_groups_mentions.message.multiple", map[string]interface{}{
   455  			"Usernames":    strings.Join(preliminary, ", @"),
   456  			"LastUsername": final,
   457  		})
   458  	}
   459  
   460  	props := model.StringInterface{
   461  		model.PROPS_ADD_CHANNEL_MEMBER: model.StringInterface{
   462  			"post_id": ephemeralPostId,
   463  
   464  			"usernames":                allUsers.Usernames(), // Kept for backwards compatibility of mobile app.
   465  			"not_in_channel_usernames": ocUsernames,
   466  
   467  			"user_ids":                allUsers.IDs(), // Kept for backwards compatibility of mobile app.
   468  			"not_in_channel_user_ids": ocUserIDs,
   469  
   470  			"not_in_groups_usernames": ogUsernames,
   471  			"not_in_groups_user_ids":  ogUsers.IDs(),
   472  		},
   473  	}
   474  
   475  	a.SendEphemeralPost(
   476  		post.UserId,
   477  		&model.Post{
   478  			Id:        ephemeralPostId,
   479  			RootId:    post.RootId,
   480  			ChannelId: post.ChannelId,
   481  			Message:   message,
   482  			CreateAt:  post.CreateAt + 1,
   483  			Props:     props,
   484  		},
   485  	)
   486  
   487  	return nil
   488  }
   489  
   490  func splitAtFinal(items []string) (preliminary []string, final string) {
   491  	if len(items) == 0 {
   492  		return
   493  	}
   494  	preliminary = items[:len(items)-1]
   495  	final = items[len(items)-1]
   496  	return
   497  }
   498  
   499  type ExplicitMentions struct {
   500  	// MentionedUserIds contains a key for each user mentioned by keyword.
   501  	MentionedUserIds map[string]bool
   502  
   503  	// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
   504  	// a corresponding keyword.
   505  	OtherPotentialMentions []string
   506  
   507  	// HereMentioned is true if the message contained @here.
   508  	HereMentioned bool
   509  
   510  	// AllMentioned is true if the message contained @all.
   511  	AllMentioned bool
   512  
   513  	// ChannelMentioned is true if the message contained @channel.
   514  	ChannelMentioned bool
   515  }
   516  
   517  // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
   518  // users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
   519  func getExplicitMentions(post *model.Post, keywords map[string][]string) *ExplicitMentions {
   520  	ret := &ExplicitMentions{
   521  		MentionedUserIds: make(map[string]bool),
   522  	}
   523  
   524  	buf := ""
   525  	mentionsEnabledFields := getMentionsEnabledFields(post)
   526  	for _, message := range mentionsEnabledFields {
   527  		markdown.Inspect(message, func(node interface{}) bool {
   528  			text, ok := node.(*markdown.Text)
   529  			if !ok {
   530  				ret.processText(buf, keywords)
   531  				buf = ""
   532  				return true
   533  			}
   534  			buf += text.Text
   535  			return false
   536  		})
   537  	}
   538  	ret.processText(buf, keywords)
   539  
   540  	return ret
   541  }
   542  
   543  // Given a post returns the values of the fields in which mentions are possible.
   544  // post.message, preText and text in the attachment are enabled.
   545  func getMentionsEnabledFields(post *model.Post) model.StringArray {
   546  	ret := []string{}
   547  
   548  	ret = append(ret, post.Message)
   549  	for _, attachment := range post.Attachments() {
   550  
   551  		if len(attachment.Pretext) != 0 {
   552  			ret = append(ret, attachment.Pretext)
   553  		}
   554  		if len(attachment.Text) != 0 {
   555  			ret = append(ret, attachment.Text)
   556  		}
   557  	}
   558  	return ret
   559  }
   560  
   561  // Given a map of user IDs to profiles, returns a list of mention
   562  // keywords for all users in the channel.
   563  func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, lookForSpecialMentions bool, channelMemberNotifyPropsMap map[string]model.StringMap) map[string][]string {
   564  	keywords := make(map[string][]string)
   565  
   566  	for id, profile := range profiles {
   567  		userMention := "@" + strings.ToLower(profile.Username)
   568  		keywords[userMention] = append(keywords[userMention], id)
   569  
   570  		if len(profile.NotifyProps[model.MENTION_KEYS_NOTIFY_PROP]) > 0 {
   571  			// Add all the user's mention keys
   572  			splitKeys := strings.Split(profile.NotifyProps[model.MENTION_KEYS_NOTIFY_PROP], ",")
   573  			for _, k := range splitKeys {
   574  				// note that these are made lower case so that we can do a case insensitive check for them
   575  				key := strings.ToLower(k)
   576  				keywords[key] = append(keywords[key], id)
   577  			}
   578  		}
   579  
   580  		// If turned on, add the user's case sensitive first name
   581  		if profile.NotifyProps[model.FIRST_NAME_NOTIFY_PROP] == "true" {
   582  			keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
   583  		}
   584  
   585  		ignoreChannelMentions := false
   586  		if ignoreChannelMentionsNotifyProp, ok := channelMemberNotifyPropsMap[profile.Id][model.IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP]; ok {
   587  			if ignoreChannelMentionsNotifyProp == model.IGNORE_CHANNEL_MENTIONS_ON {
   588  				ignoreChannelMentions = true
   589  			}
   590  		}
   591  
   592  		// Add @channel and @all to keywords if user has them turned on
   593  		if lookForSpecialMentions {
   594  			if int64(len(profiles)) <= *a.Config().TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps[model.CHANNEL_MENTIONS_NOTIFY_PROP] == "true" && !ignoreChannelMentions {
   595  				keywords["@channel"] = append(keywords["@channel"], profile.Id)
   596  				keywords["@all"] = append(keywords["@all"], profile.Id)
   597  
   598  				status := GetStatusFromCache(profile.Id)
   599  				if status != nil && status.Status == model.STATUS_ONLINE {
   600  					keywords["@here"] = append(keywords["@here"], profile.Id)
   601  				}
   602  			}
   603  		}
   604  	}
   605  
   606  	return keywords
   607  }
   608  
   609  // Represents either an email or push notification and contains the fields required to send it to any user.
   610  type postNotification struct {
   611  	channel    *model.Channel
   612  	post       *model.Post
   613  	profileMap map[string]*model.User
   614  	sender     *model.User
   615  }
   616  
   617  // Returns the name of the channel for this notification. For direct messages, this is the sender's name
   618  // preceeded by an at sign. For group messages, this is a comma-separated list of the members of the
   619  // channel, with an option to exclude the recipient of the message from that list.
   620  func (n *postNotification) GetChannelName(userNameFormat string, excludeId string) string {
   621  	switch n.channel.Type {
   622  	case model.CHANNEL_DIRECT:
   623  		return n.sender.GetDisplayName(userNameFormat)
   624  	case model.CHANNEL_GROUP:
   625  		names := []string{}
   626  		for _, user := range n.profileMap {
   627  			if user.Id != excludeId {
   628  				names = append(names, user.GetDisplayName(userNameFormat))
   629  			}
   630  		}
   631  
   632  		sort.Strings(names)
   633  
   634  		return strings.Join(names, ", ")
   635  	default:
   636  		return n.channel.DisplayName
   637  	}
   638  }
   639  
   640  // Returns the name of the sender of this notification, accounting for things like system messages
   641  // and whether or not the username has been overridden by an integration.
   642  func (n *postNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string {
   643  	if n.post.IsSystemMessage() {
   644  		return utils.T("system.message.name")
   645  	}
   646  
   647  	if overridesAllowed && n.channel.Type != model.CHANNEL_DIRECT {
   648  		if value, ok := n.post.Props["override_username"]; ok && n.post.Props["from_webhook"] == "true" {
   649  			return value.(string)
   650  		}
   651  	}
   652  
   653  	return n.sender.GetDisplayName(userNameFormat)
   654  }
   655  
   656  // addMentionedUsers will add the mentioned user id in the struct's list for mentioned users
   657  func (e *ExplicitMentions) addMentionedUsers(ids []string) {
   658  	for _, id := range ids {
   659  		e.MentionedUserIds[id] = true
   660  	}
   661  }
   662  
   663  // checkForMention checks if there is a mention to a specific user or to the keywords here / channel / all
   664  func (e *ExplicitMentions) checkForMention(word string, keywords map[string][]string) bool {
   665  	isMention := false
   666  
   667  	switch strings.ToLower(word) {
   668  	case "@here":
   669  		e.HereMentioned = true
   670  	case "@channel":
   671  		e.ChannelMentioned = true
   672  	case "@all":
   673  		e.AllMentioned = true
   674  	}
   675  
   676  	if ids, match := keywords[strings.ToLower(word)]; match {
   677  		e.addMentionedUsers(ids)
   678  		isMention = true
   679  	}
   680  
   681  	// Case-sensitive check for first name
   682  	if ids, match := keywords[word]; match {
   683  		e.addMentionedUsers(ids)
   684  		isMention = true
   685  	}
   686  
   687  	return isMention
   688  }
   689  
   690  // isKeywordMultibyte checks if a word containing a multibyte character contains a multibyte keyword
   691  func isKeywordMultibyte(keywords map[string][]string, word string) ([]string, bool) {
   692  	ids := []string{}
   693  	match := false
   694  	var multibyteKeywords []string
   695  	for keyword := range keywords {
   696  		if len(keyword) != utf8.RuneCountInString(keyword) {
   697  			multibyteKeywords = append(multibyteKeywords, keyword)
   698  		}
   699  	}
   700  
   701  	if len(word) != utf8.RuneCountInString(word) {
   702  		for _, key := range multibyteKeywords {
   703  			if strings.Contains(word, key) {
   704  				ids, match = keywords[key]
   705  			}
   706  		}
   707  	}
   708  	return ids, match
   709  }
   710  
   711  // Processes text to filter mentioned users and other potential mentions
   712  func (e *ExplicitMentions) processText(text string, keywords map[string][]string) {
   713  	systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
   714  
   715  	for _, word := range strings.FieldsFunc(text, func(c rune) bool {
   716  		// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
   717  		return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
   718  	}) {
   719  		// skip word with format ':word:' with an assumption that it is an emoji format only
   720  		if word[0] == ':' && word[len(word)-1] == ':' {
   721  			continue
   722  		}
   723  
   724  		word = strings.TrimLeft(word, ":.-_")
   725  
   726  		if e.checkForMention(word, keywords) {
   727  			continue
   728  		}
   729  
   730  		foundWithoutSuffix := false
   731  		wordWithoutSuffix := word
   732  		for len(wordWithoutSuffix) > 0 && strings.LastIndexAny(wordWithoutSuffix, ".-:_") == (len(wordWithoutSuffix)-1) {
   733  			wordWithoutSuffix = wordWithoutSuffix[0 : len(wordWithoutSuffix)-1]
   734  
   735  			if e.checkForMention(wordWithoutSuffix, keywords) {
   736  				foundWithoutSuffix = true
   737  				break
   738  			}
   739  		}
   740  
   741  		if foundWithoutSuffix {
   742  			continue
   743  		}
   744  
   745  		if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
   746  			e.OtherPotentialMentions = append(e.OtherPotentialMentions, word[1:])
   747  		} else if strings.ContainsAny(word, ".-:") {
   748  			// This word contains a character that may be the end of a sentence, so split further
   749  			splitWords := strings.FieldsFunc(word, func(c rune) bool {
   750  				return c == '.' || c == '-' || c == ':'
   751  			})
   752  
   753  			for _, splitWord := range splitWords {
   754  				if e.checkForMention(splitWord, keywords) {
   755  					continue
   756  				}
   757  				if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
   758  					e.OtherPotentialMentions = append(e.OtherPotentialMentions, splitWord[1:])
   759  				}
   760  			}
   761  		}
   762  		if ids, match := isKeywordMultibyte(keywords, word); match {
   763  			e.addMentionedUsers(ids)
   764  		}
   765  	}
   766  }