github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/notification.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package app
     5  
     6  import (
     7  	"sort"
     8  	"strings"
     9  	"unicode"
    10  	"unicode/utf8"
    11  
    12  	"github.com/mattermost/mattermost-server/v5/mlog"
    13  	"github.com/mattermost/mattermost-server/v5/model"
    14  	"github.com/mattermost/mattermost-server/v5/store"
    15  	"github.com/mattermost/mattermost-server/v5/utils"
    16  	"github.com/mattermost/mattermost-server/v5/utils/markdown"
    17  )
    18  
    19  func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error) {
    20  	// Do not send notifications in archived channels
    21  	if channel.DeleteAt > 0 {
    22  		return []string{}, nil
    23  	}
    24  
    25  	pchan := make(chan store.StoreResult, 1)
    26  	go func() {
    27  		props, err := a.Srv().Store.User().GetAllProfilesInChannel(channel.Id, true)
    28  		pchan <- store.StoreResult{Data: props, Err: err}
    29  		close(pchan)
    30  	}()
    31  
    32  	cmnchan := make(chan store.StoreResult, 1)
    33  	go func() {
    34  		props, err := a.Srv().Store.Channel().GetAllChannelMembersNotifyPropsForChannel(channel.Id, true)
    35  		cmnchan <- store.StoreResult{Data: props, Err: err}
    36  		close(cmnchan)
    37  	}()
    38  
    39  	var gchan chan store.StoreResult
    40  	if a.allowGroupMentions(post) {
    41  		gchan = make(chan store.StoreResult, 1)
    42  		go func() {
    43  			groupsMap, err := a.getGroupsAllowedForReferenceInChannel(channel, team)
    44  			gchan <- store.StoreResult{Data: groupsMap, Err: err}
    45  			close(gchan)
    46  		}()
    47  	}
    48  
    49  	var fchan chan store.StoreResult
    50  	if len(post.FileIds) != 0 {
    51  		fchan = make(chan store.StoreResult, 1)
    52  		go func() {
    53  			fileInfos, err := a.Srv().Store.FileInfo().GetForPost(post.Id, true, false, true)
    54  			fchan <- store.StoreResult{Data: fileInfos, Err: err}
    55  			close(fchan)
    56  		}()
    57  	}
    58  
    59  	result := <-pchan
    60  	if result.Err != nil {
    61  		return nil, result.Err
    62  	}
    63  	profileMap := result.Data.(map[string]*model.User)
    64  
    65  	result = <-cmnchan
    66  	if result.Err != nil {
    67  		return nil, result.Err
    68  	}
    69  	channelMemberNotifyPropsMap := result.Data.(map[string]model.StringMap)
    70  
    71  	groups := make(map[string]*model.Group)
    72  	if gchan != nil {
    73  		result = <-gchan
    74  		if result.Err != nil {
    75  			return nil, result.Err
    76  		}
    77  		groups = result.Data.(map[string]*model.Group)
    78  	}
    79  
    80  	mentions := &ExplicitMentions{}
    81  	allActivityPushUserIds := []string{}
    82  
    83  	if channel.Type == model.CHANNEL_DIRECT {
    84  		otherUserId := channel.GetOtherUserIdForDM(post.UserId)
    85  
    86  		_, ok := profileMap[otherUserId]
    87  		if ok {
    88  			mentions.addMention(otherUserId, DMMention)
    89  		}
    90  
    91  		if post.GetProp("from_webhook") == "true" {
    92  			mentions.addMention(post.UserId, DMMention)
    93  		}
    94  	} else {
    95  		allowChannelMentions := a.allowChannelMentions(post, len(profileMap))
    96  		keywords := a.getMentionKeywordsInChannel(profileMap, allowChannelMentions, channelMemberNotifyPropsMap)
    97  
    98  		mentions = getExplicitMentions(post, keywords, groups)
    99  
   100  		// Add an implicit mention when a user is added to a channel
   101  		// even if the user has set 'username mentions' to false in account settings.
   102  		if post.Type == model.POST_ADD_TO_CHANNEL {
   103  			addedUserId, ok := post.GetProp(model.POST_PROPS_ADDED_USER_ID).(string)
   104  			if ok {
   105  				mentions.addMention(addedUserId, KeywordMention)
   106  			}
   107  		}
   108  
   109  		// Iterate through all groups that were mentioned and insert group members into the list of mentions or potential mentions
   110  		for _, group := range mentions.GroupMentions {
   111  			anyUsersMentionedByGroup, err := a.insertGroupMentions(group, channel, profileMap, mentions)
   112  			if err != nil {
   113  				return nil, err
   114  			}
   115  
   116  			if !anyUsersMentionedByGroup {
   117  				a.sendNoUsersNotifiedByGroupInChannel(sender, post, channel, group)
   118  			}
   119  		}
   120  
   121  		// get users that have comment thread mentions enabled
   122  		if len(post.RootId) > 0 && parentPostList != nil {
   123  			for _, threadPost := range parentPostList.Posts {
   124  				profile := profileMap[threadPost.UserId]
   125  				if profile != nil && (profile.NotifyProps[model.COMMENTS_NOTIFY_PROP] == model.COMMENTS_NOTIFY_ANY || (profile.NotifyProps[model.COMMENTS_NOTIFY_PROP] == model.COMMENTS_NOTIFY_ROOT && threadPost.Id == parentPostList.Order[0])) {
   126  					mentionType := ThreadMention
   127  					if threadPost.Id == parentPostList.Order[0] {
   128  						mentionType = CommentMention
   129  					}
   130  
   131  					mentions.addMention(threadPost.UserId, mentionType)
   132  				}
   133  			}
   134  		}
   135  
   136  		// prevent the user from mentioning themselves
   137  		if post.GetProp("from_webhook") != "true" {
   138  			mentions.removeMention(post.UserId)
   139  		}
   140  
   141  		go func() {
   142  			_, err := a.sendOutOfChannelMentions(sender, post, channel, mentions.OtherPotentialMentions)
   143  			if err != nil {
   144  				mlog.Error("Failed to send warning for out of channel mentions", mlog.String("user_id", sender.Id), mlog.String("post_id", post.Id), mlog.Err(err))
   145  			}
   146  		}()
   147  
   148  		// find which users in the channel are set up to always receive mobile notifications
   149  		for _, profile := range profileMap {
   150  			if (profile.NotifyProps[model.PUSH_NOTIFY_PROP] == model.USER_NOTIFY_ALL ||
   151  				channelMemberNotifyPropsMap[profile.Id][model.PUSH_NOTIFY_PROP] == model.CHANNEL_NOTIFY_ALL) &&
   152  				(post.UserId != profile.Id || post.GetProp("from_webhook") == "true") &&
   153  				!post.IsSystemMessage() {
   154  				allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
   155  			}
   156  		}
   157  	}
   158  
   159  	mentionedUsersList := make([]string, 0, len(mentions.Mentions))
   160  	updateMentionChans := []chan *model.AppError{}
   161  
   162  	for id := range mentions.Mentions {
   163  		mentionedUsersList = append(mentionedUsersList, id)
   164  
   165  		umc := make(chan *model.AppError, 1)
   166  		go func(userId string) {
   167  			umc <- a.Srv().Store.Channel().IncrementMentionCount(post.ChannelId, userId)
   168  			close(umc)
   169  		}(id)
   170  		updateMentionChans = append(updateMentionChans, umc)
   171  	}
   172  
   173  	notification := &PostNotification{
   174  		Post:       post,
   175  		Channel:    channel,
   176  		ProfileMap: profileMap,
   177  		Sender:     sender,
   178  	}
   179  
   180  	if *a.Config().EmailSettings.SendEmailNotifications {
   181  		for _, id := range mentionedUsersList {
   182  			if profileMap[id] == nil {
   183  				continue
   184  			}
   185  
   186  			//If email verification is required and user email is not verified don't send email.
   187  			if *a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified {
   188  				mlog.Error("Skipped sending notification email, address not verified.", mlog.String("user_email", profileMap[id].Email), mlog.String("user_id", id))
   189  				continue
   190  			}
   191  
   192  			if a.userAllowsEmail(profileMap[id], channelMemberNotifyPropsMap[id], post) {
   193  				a.sendNotificationEmail(notification, profileMap[id], team)
   194  			}
   195  		}
   196  	}
   197  
   198  	// Check for channel-wide mentions in channels that have too many members for those to work
   199  	if int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
   200  		T := utils.GetUserTranslations(sender.Locale)
   201  
   202  		if mentions.HereMentioned {
   203  			a.SendEphemeralPost(
   204  				post.UserId,
   205  				&model.Post{
   206  					ChannelId: post.ChannelId,
   207  					Message:   T("api.post.disabled_here", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   208  					CreateAt:  post.CreateAt + 1,
   209  				},
   210  			)
   211  		}
   212  
   213  		if mentions.ChannelMentioned {
   214  			a.SendEphemeralPost(
   215  				post.UserId,
   216  				&model.Post{
   217  					ChannelId: post.ChannelId,
   218  					Message:   T("api.post.disabled_channel", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   219  					CreateAt:  post.CreateAt + 1,
   220  				},
   221  			)
   222  		}
   223  
   224  		if mentions.AllMentioned {
   225  			a.SendEphemeralPost(
   226  				post.UserId,
   227  				&model.Post{
   228  					ChannelId: post.ChannelId,
   229  					Message:   T("api.post.disabled_all", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}),
   230  					CreateAt:  post.CreateAt + 1,
   231  				},
   232  			)
   233  		}
   234  	}
   235  
   236  	// Make sure all mention updates are complete to prevent race
   237  	// Probably better to batch these DB updates in the future
   238  	// MUST be completed before push notifications send
   239  	for _, umc := range updateMentionChans {
   240  		if err := <-umc; err != nil {
   241  			mlog.Warn(
   242  				"Failed to update mention count",
   243  				mlog.String("post_id", post.Id),
   244  				mlog.String("channel_id", post.ChannelId),
   245  				mlog.Err(err),
   246  			)
   247  		}
   248  	}
   249  
   250  	sendPushNotifications := false
   251  	if *a.Config().EmailSettings.SendPushNotifications {
   252  		pushServer := *a.Config().EmailSettings.PushNotificationServer
   253  		if license := a.Srv().License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
   254  			mlog.Warn("Push notifications are disabled. Go to System Console > Notifications > Mobile Push to enable them.")
   255  			sendPushNotifications = false
   256  		} else {
   257  			sendPushNotifications = true
   258  		}
   259  	}
   260  
   261  	if sendPushNotifications {
   262  		for _, id := range mentionedUsersList {
   263  			if profileMap[id] == nil {
   264  				continue
   265  			}
   266  
   267  			var status *model.Status
   268  			var err *model.AppError
   269  			if status, err = a.GetStatus(id); err != nil {
   270  				status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
   271  			}
   272  
   273  			if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
   274  				mentionType := mentions.Mentions[id]
   275  
   276  				replyToThreadType := ""
   277  				if mentionType == ThreadMention {
   278  					replyToThreadType = model.COMMENTS_NOTIFY_ANY
   279  				} else if mentionType == CommentMention {
   280  					replyToThreadType = model.COMMENTS_NOTIFY_ROOT
   281  				}
   282  
   283  				a.sendPushNotification(
   284  					notification,
   285  					profileMap[id],
   286  					mentionType == KeywordMention || mentionType == ChannelMention || mentionType == DMMention,
   287  					mentionType == ChannelMention,
   288  					replyToThreadType,
   289  				)
   290  			} else {
   291  				// register that a notification was not sent
   292  				a.NotificationsLog().Warn("Notification not sent",
   293  					mlog.String("ackId", ""),
   294  					mlog.String("type", model.PUSH_TYPE_MESSAGE),
   295  					mlog.String("userId", id),
   296  					mlog.String("postId", post.Id),
   297  					mlog.String("status", model.PUSH_NOT_SENT),
   298  				)
   299  			}
   300  		}
   301  
   302  		for _, id := range allActivityPushUserIds {
   303  			if profileMap[id] == nil {
   304  				continue
   305  			}
   306  
   307  			if _, ok := mentions.Mentions[id]; !ok {
   308  				var status *model.Status
   309  				var err *model.AppError
   310  				if status, err = a.GetStatus(id); err != nil {
   311  					status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
   312  				}
   313  
   314  				if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
   315  					a.sendPushNotification(
   316  						notification,
   317  						profileMap[id],
   318  						false,
   319  						false,
   320  						"",
   321  					)
   322  				} else {
   323  					// register that a notification was not sent
   324  					a.NotificationsLog().Warn("Notification not sent",
   325  						mlog.String("ackId", ""),
   326  						mlog.String("type", model.PUSH_TYPE_MESSAGE),
   327  						mlog.String("userId", id),
   328  						mlog.String("postId", post.Id),
   329  						mlog.String("status", model.PUSH_NOT_SENT),
   330  					)
   331  				}
   332  			}
   333  		}
   334  	}
   335  
   336  	message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil)
   337  
   338  	// Note that PreparePostForClient should've already been called by this point
   339  	message.Add("post", post.ToJson())
   340  
   341  	message.Add("channel_type", channel.Type)
   342  	message.Add("channel_display_name", notification.GetChannelName(model.SHOW_USERNAME, ""))
   343  	message.Add("channel_name", channel.Name)
   344  	message.Add("sender_name", notification.GetSenderName(model.SHOW_USERNAME, *a.Config().ServiceSettings.EnablePostUsernameOverride))
   345  	message.Add("team_id", team.Id)
   346  	message.Add("set_online", setOnline)
   347  
   348  	if len(post.FileIds) != 0 && fchan != nil {
   349  		message.Add("otherFile", "true")
   350  
   351  		var infos []*model.FileInfo
   352  		if result := <-fchan; result.Err != nil {
   353  			mlog.Warn("Unable to get fileInfo for push notifications.", mlog.String("post_id", post.Id), mlog.Err(result.Err))
   354  		} else {
   355  			infos = result.Data.([]*model.FileInfo)
   356  		}
   357  
   358  		for _, info := range infos {
   359  			if info.IsImage() {
   360  				message.Add("image", "true")
   361  				break
   362  			}
   363  		}
   364  	}
   365  
   366  	if len(mentionedUsersList) != 0 {
   367  		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
   368  	}
   369  
   370  	a.Publish(message)
   371  	return mentionedUsersList, nil
   372  }
   373  
   374  func (a *App) userAllowsEmail(user *model.User, channelMemberNotificationProps model.StringMap, post *model.Post) bool {
   375  	userAllowsEmails := user.NotifyProps[model.EMAIL_NOTIFY_PROP] != "false"
   376  	if channelEmail, ok := channelMemberNotificationProps[model.EMAIL_NOTIFY_PROP]; ok {
   377  		if channelEmail != model.CHANNEL_NOTIFY_DEFAULT {
   378  			userAllowsEmails = channelEmail != "false"
   379  		}
   380  	}
   381  
   382  	// Remove the user as recipient when the user has muted the channel.
   383  	if channelMuted, ok := channelMemberNotificationProps[model.MARK_UNREAD_NOTIFY_PROP]; ok {
   384  		if channelMuted == model.CHANNEL_MARK_UNREAD_MENTION {
   385  			mlog.Debug("Channel muted for user", mlog.String("user_id", user.Id), mlog.String("channel_mute", channelMuted))
   386  			userAllowsEmails = false
   387  		}
   388  	}
   389  
   390  	var status *model.Status
   391  	var err *model.AppError
   392  	if status, err = a.GetStatus(user.Id); err != nil {
   393  		status = &model.Status{
   394  			UserId:         user.Id,
   395  			Status:         model.STATUS_OFFLINE,
   396  			Manual:         false,
   397  			LastActivityAt: 0,
   398  			ActiveChannel:  "",
   399  		}
   400  	}
   401  
   402  	autoResponderRelated := status.Status == model.STATUS_OUT_OF_OFFICE || post.Type == model.POST_AUTO_RESPONDER
   403  	emailNotificationsAllowedForStatus := status.Status != model.STATUS_ONLINE && status.Status != model.STATUS_DND
   404  
   405  	return userAllowsEmails && emailNotificationsAllowedForStatus && user.DeleteAt == 0 && !autoResponderRelated
   406  }
   407  
   408  func (a *App) sendNoUsersNotifiedByGroupInChannel(sender *model.User, post *model.Post, channel *model.Channel, group *model.Group) {
   409  	T := utils.GetUserTranslations(sender.Locale)
   410  	ephemeralPost := &model.Post{
   411  		UserId:    sender.Id,
   412  		RootId:    post.RootId,
   413  		ParentId:  post.ParentId,
   414  		ChannelId: channel.Id,
   415  		Message:   T("api.post.check_for_out_of_channel_group_users.message.none", model.StringInterface{"GroupName": group.Name}),
   416  	}
   417  	a.SendEphemeralPost(post.UserId, ephemeralPost)
   418  }
   419  
   420  // sendOutOfChannelMentions sends an ephemeral post to the sender of a post if any of the given potential mentions
   421  // are outside of the post's channel. Returns whether or not an ephemeral post was sent.
   422  func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) (bool, error) {
   423  	outOfChannelUsers, outOfGroupsUsers, err := a.filterOutOfChannelMentions(sender, post, channel, potentialMentions)
   424  	if err != nil {
   425  		return false, err
   426  	}
   427  
   428  	if len(outOfChannelUsers) == 0 && len(outOfGroupsUsers) == 0 {
   429  		return false, nil
   430  	}
   431  
   432  	a.SendEphemeralPost(post.UserId, makeOutOfChannelMentionPost(sender, post, outOfChannelUsers, outOfGroupsUsers))
   433  
   434  	return true, nil
   435  }
   436  
   437  func (a *App) FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError) {
   438  	result := []*model.User{}
   439  	for _, user := range otherUsers {
   440  		canSee, err := a.UserCanSeeOtherUser(viewer.Id, user.Id)
   441  		if err != nil {
   442  			return nil, err
   443  		}
   444  		if canSee {
   445  			result = append(result, user)
   446  		}
   447  	}
   448  	return result, nil
   449  }
   450  
   451  func (a *App) filterOutOfChannelMentions(sender *model.User, post *model.Post, channel *model.Channel, potentialMentions []string) ([]*model.User, []*model.User, error) {
   452  	if post.IsSystemMessage() {
   453  		return nil, nil, nil
   454  	}
   455  
   456  	if channel.TeamId == "" || channel.Type == model.CHANNEL_DIRECT || channel.Type == model.CHANNEL_GROUP {
   457  		return nil, nil, nil
   458  	}
   459  
   460  	if len(potentialMentions) == 0 {
   461  		return nil, nil, nil
   462  	}
   463  
   464  	users, err := a.Srv().Store.User().GetProfilesByUsernames(potentialMentions, &model.ViewUsersRestrictions{Teams: []string{channel.TeamId}})
   465  	if err != nil {
   466  		return nil, nil, err
   467  	}
   468  
   469  	// Filter out inactive users and bots
   470  	allUsers := model.UserSlice(users).FilterByActive(true)
   471  	allUsers = allUsers.FilterWithoutBots()
   472  	allUsers, err = a.FilterUsersByVisible(sender, allUsers)
   473  	if err != nil {
   474  		return nil, nil, err
   475  	}
   476  
   477  	if len(allUsers) == 0 {
   478  		return nil, nil, nil
   479  	}
   480  
   481  	// Differentiate between users who can and can't be added to the channel
   482  	var outOfChannelUsers model.UserSlice
   483  	var outOfGroupsUsers model.UserSlice
   484  	if channel.IsGroupConstrained() {
   485  		nonMemberIDs, err := a.FilterNonGroupChannelMembers(allUsers.IDs(), channel)
   486  		if err != nil {
   487  			return nil, nil, err
   488  		}
   489  
   490  		outOfChannelUsers = allUsers.FilterWithoutID(nonMemberIDs)
   491  		outOfGroupsUsers = allUsers.FilterByID(nonMemberIDs)
   492  	} else {
   493  		outOfChannelUsers = allUsers
   494  	}
   495  
   496  	return outOfChannelUsers, outOfGroupsUsers, nil
   497  }
   498  
   499  func makeOutOfChannelMentionPost(sender *model.User, post *model.Post, outOfChannelUsers, outOfGroupsUsers []*model.User) *model.Post {
   500  	allUsers := model.UserSlice(append(outOfChannelUsers, outOfGroupsUsers...))
   501  
   502  	ocUsers := model.UserSlice(outOfChannelUsers)
   503  	ocUsernames := ocUsers.Usernames()
   504  	ocUserIDs := ocUsers.IDs()
   505  
   506  	ogUsers := model.UserSlice(outOfGroupsUsers)
   507  	ogUsernames := ogUsers.Usernames()
   508  
   509  	T := utils.GetUserTranslations(sender.Locale)
   510  
   511  	ephemeralPostId := model.NewId()
   512  	var message string
   513  	if len(outOfChannelUsers) == 1 {
   514  		message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
   515  			"Username": ocUsernames[0],
   516  		})
   517  	} else if len(outOfChannelUsers) > 1 {
   518  		preliminary, final := splitAtFinal(ocUsernames)
   519  
   520  		message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
   521  			"Usernames":    strings.Join(preliminary, ", @"),
   522  			"LastUsername": final,
   523  		})
   524  	}
   525  
   526  	if len(outOfGroupsUsers) == 1 {
   527  		if len(message) > 0 {
   528  			message += "\n"
   529  		}
   530  
   531  		message += T("api.post.check_for_out_of_channel_groups_mentions.message.one", map[string]interface{}{
   532  			"Username": ogUsernames[0],
   533  		})
   534  	} else if len(outOfGroupsUsers) > 1 {
   535  		preliminary, final := splitAtFinal(ogUsernames)
   536  
   537  		if len(message) > 0 {
   538  			message += "\n"
   539  		}
   540  
   541  		message += T("api.post.check_for_out_of_channel_groups_mentions.message.multiple", map[string]interface{}{
   542  			"Usernames":    strings.Join(preliminary, ", @"),
   543  			"LastUsername": final,
   544  		})
   545  	}
   546  
   547  	props := model.StringInterface{
   548  		model.PROPS_ADD_CHANNEL_MEMBER: model.StringInterface{
   549  			"post_id": ephemeralPostId,
   550  
   551  			"usernames":                allUsers.Usernames(), // Kept for backwards compatibility of mobile app.
   552  			"not_in_channel_usernames": ocUsernames,
   553  
   554  			"user_ids":                allUsers.IDs(), // Kept for backwards compatibility of mobile app.
   555  			"not_in_channel_user_ids": ocUserIDs,
   556  
   557  			"not_in_groups_usernames": ogUsernames,
   558  			"not_in_groups_user_ids":  ogUsers.IDs(),
   559  		},
   560  	}
   561  
   562  	return &model.Post{
   563  		Id:        ephemeralPostId,
   564  		RootId:    post.RootId,
   565  		ChannelId: post.ChannelId,
   566  		Message:   message,
   567  		CreateAt:  post.CreateAt + 1,
   568  		Props:     props,
   569  	}
   570  }
   571  
   572  func splitAtFinal(items []string) (preliminary []string, final string) {
   573  	if len(items) == 0 {
   574  		return
   575  	}
   576  	preliminary = items[:len(items)-1]
   577  	final = items[len(items)-1]
   578  	return
   579  }
   580  
   581  type ExplicitMentions struct {
   582  	// Mentions contains the ID of each user that was mentioned and how they were mentioned.
   583  	Mentions map[string]MentionType
   584  
   585  	// Contains a map of groups that were mentioned
   586  	GroupMentions map[string]*model.Group
   587  
   588  	// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
   589  	// a corresponding keyword.
   590  	OtherPotentialMentions []string
   591  
   592  	// HereMentioned is true if the message contained @here.
   593  	HereMentioned bool
   594  
   595  	// AllMentioned is true if the message contained @all.
   596  	AllMentioned bool
   597  
   598  	// ChannelMentioned is true if the message contained @channel.
   599  	ChannelMentioned bool
   600  }
   601  
   602  type MentionType int
   603  
   604  const (
   605  	// Different types of mentions ordered by their priority from lowest to highest
   606  
   607  	// A placeholder that should never be used in practice
   608  	NoMention MentionType = iota
   609  
   610  	// The post is in a thread that the user has commented on
   611  	ThreadMention
   612  
   613  	// The post is a comment on a thread started by the user
   614  	CommentMention
   615  
   616  	// The post contains an at-channel, at-all, or at-here
   617  	ChannelMention
   618  
   619  	// The post is a DM
   620  	DMMention
   621  
   622  	// The post contains an at-mention for the user
   623  	KeywordMention
   624  
   625  	// The post contains a group mention for the user
   626  	GroupMention
   627  )
   628  
   629  func (m *ExplicitMentions) addMention(userId string, mentionType MentionType) {
   630  	if m.Mentions == nil {
   631  		m.Mentions = make(map[string]MentionType)
   632  	}
   633  
   634  	if currentType, ok := m.Mentions[userId]; ok && currentType >= mentionType {
   635  		return
   636  	}
   637  
   638  	m.Mentions[userId] = mentionType
   639  }
   640  
   641  func (m *ExplicitMentions) addGroupMention(word string, groups map[string]*model.Group) bool {
   642  	if strings.HasPrefix(word, "@") {
   643  		word = word[1:]
   644  	} else {
   645  		// Only allow group mentions when mentioned directly with @group-name
   646  		return false
   647  	}
   648  
   649  	group, groupFound := groups[word]
   650  	if !groupFound {
   651  		group = groups[strings.ToLower(word)]
   652  	}
   653  
   654  	if group == nil {
   655  		return false
   656  	}
   657  
   658  	if m.GroupMentions == nil {
   659  		m.GroupMentions = make(map[string]*model.Group)
   660  	}
   661  
   662  	if group.Name != nil {
   663  		m.GroupMentions[*group.Name] = group
   664  	}
   665  
   666  	return true
   667  }
   668  
   669  func (m *ExplicitMentions) addMentions(userIds []string, mentionType MentionType) {
   670  	for _, userId := range userIds {
   671  		m.addMention(userId, mentionType)
   672  	}
   673  }
   674  
   675  func (m *ExplicitMentions) removeMention(userId string) {
   676  	delete(m.Mentions, userId)
   677  }
   678  
   679  // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
   680  // users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
   681  func getExplicitMentions(post *model.Post, keywords map[string][]string, groups map[string]*model.Group) *ExplicitMentions {
   682  	ret := &ExplicitMentions{}
   683  
   684  	buf := ""
   685  	mentionsEnabledFields := getMentionsEnabledFields(post)
   686  	for _, message := range mentionsEnabledFields {
   687  		markdown.Inspect(message, func(node interface{}) bool {
   688  			text, ok := node.(*markdown.Text)
   689  			if !ok {
   690  				ret.processText(buf, keywords, groups)
   691  				buf = ""
   692  				return true
   693  			}
   694  			buf += text.Text
   695  			return false
   696  		})
   697  	}
   698  	ret.processText(buf, keywords, groups)
   699  
   700  	return ret
   701  }
   702  
   703  // Given a post returns the values of the fields in which mentions are possible.
   704  // post.message, preText and text in the attachment are enabled.
   705  func getMentionsEnabledFields(post *model.Post) model.StringArray {
   706  	ret := []string{}
   707  
   708  	ret = append(ret, post.Message)
   709  	for _, attachment := range post.Attachments() {
   710  
   711  		if len(attachment.Pretext) != 0 {
   712  			ret = append(ret, attachment.Pretext)
   713  		}
   714  		if len(attachment.Text) != 0 {
   715  			ret = append(ret, attachment.Text)
   716  		}
   717  	}
   718  	return ret
   719  }
   720  
   721  // allowChannelMentions returns whether or not the channel mentions are allowed for the given post.
   722  func (a *App) allowChannelMentions(post *model.Post, numProfiles int) bool {
   723  	if !a.HasPermissionToChannel(post.UserId, post.ChannelId, model.PERMISSION_USE_CHANNEL_MENTIONS) {
   724  		return false
   725  	}
   726  
   727  	if post.Type == model.POST_HEADER_CHANGE || post.Type == model.POST_PURPOSE_CHANGE {
   728  		return false
   729  	}
   730  
   731  	if int64(numProfiles) >= *a.Config().TeamSettings.MaxNotificationsPerChannel {
   732  		return false
   733  	}
   734  
   735  	return true
   736  }
   737  
   738  // allowGroupMentions returns whether or not the group mentions are allowed for the given post.
   739  func (a *App) allowGroupMentions(post *model.Post) bool {
   740  	if license := a.Srv().License(); license == nil || !*license.Features.LDAPGroups {
   741  		return false
   742  	}
   743  
   744  	if !a.HasPermissionToChannel(post.UserId, post.ChannelId, model.PERMISSION_USE_GROUP_MENTIONS) {
   745  		return false
   746  	}
   747  
   748  	if post.Type == model.POST_HEADER_CHANGE || post.Type == model.POST_PURPOSE_CHANGE {
   749  		return false
   750  	}
   751  
   752  	return true
   753  }
   754  
   755  // getGroupsAllowedForReferenceInChannel returns a map of groups allowed for reference in a given channel and team.
   756  func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team *model.Team) (map[string]*model.Group, *model.AppError) {
   757  	var err *model.AppError
   758  	groupsMap := make(map[string]*model.Group)
   759  	opts := model.GroupSearchOpts{FilterAllowReference: true}
   760  
   761  	if channel.IsGroupConstrained() || team.IsGroupConstrained() {
   762  		var groups []*model.GroupWithSchemeAdmin
   763  		if channel.IsGroupConstrained() {
   764  			groups, err = a.Srv().Store.Group().GetGroupsByChannel(channel.Id, opts)
   765  		} else {
   766  			groups, err = a.Srv().Store.Group().GetGroupsByTeam(team.Id, opts)
   767  		}
   768  		if err != nil {
   769  			return nil, err
   770  		}
   771  		for _, group := range groups {
   772  			if group.Group.Name != nil {
   773  				groupsMap[*group.Group.Name] = &group.Group
   774  			}
   775  		}
   776  		return groupsMap, nil
   777  	}
   778  
   779  	groups, err := a.Srv().Store.Group().GetGroups(0, 0, opts)
   780  	if err != nil {
   781  		return nil, err
   782  	}
   783  	for _, group := range groups {
   784  		if group.Name != nil {
   785  			groupsMap[*group.Name] = group
   786  		}
   787  	}
   788  
   789  	return groupsMap, nil
   790  }
   791  
   792  // Given a map of user IDs to profiles, returns a list of mention
   793  // keywords for all users in the channel.
   794  func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, allowChannelMentions bool, channelMemberNotifyPropsMap map[string]model.StringMap) map[string][]string {
   795  	keywords := make(map[string][]string)
   796  
   797  	for _, profile := range profiles {
   798  		addMentionKeywordsForUser(
   799  			keywords,
   800  			profile,
   801  			channelMemberNotifyPropsMap[profile.Id],
   802  			a.GetStatusFromCache(profile.Id),
   803  			allowChannelMentions,
   804  		)
   805  	}
   806  
   807  	return keywords
   808  }
   809  
   810  // insertGroupMentions adds group members in the channel to Mentions, adds group members not in the channel to OtherPotentialMentions
   811  // returns false if no group members present in the team that the channel belongs to
   812  func (a *App) insertGroupMentions(group *model.Group, channel *model.Channel, profileMap map[string]*model.User, mentions *ExplicitMentions) (bool, *model.AppError) {
   813  	var err *model.AppError
   814  	var groupMembers []*model.User
   815  	outOfChannelGroupMembers := []*model.User{}
   816  	isGroupOrDirect := channel.IsGroupOrDirect()
   817  
   818  	if isGroupOrDirect {
   819  		groupMembers, err = a.Srv().Store.Group().GetMemberUsers(group.Id)
   820  	} else {
   821  		groupMembers, err = a.Srv().Store.Group().GetMemberUsersInTeam(group.Id, channel.TeamId)
   822  	}
   823  
   824  	if err != nil {
   825  		return false, err
   826  	}
   827  
   828  	if mentions.Mentions == nil {
   829  		mentions.Mentions = make(map[string]MentionType)
   830  	}
   831  
   832  	for _, member := range groupMembers {
   833  		if _, ok := profileMap[member.Id]; ok {
   834  			mentions.Mentions[member.Id] = GroupMention
   835  		} else {
   836  			outOfChannelGroupMembers = append(outOfChannelGroupMembers, member)
   837  		}
   838  	}
   839  
   840  	potentialGroupMembersMentioned := []string{}
   841  	for _, user := range outOfChannelGroupMembers {
   842  		potentialGroupMembersMentioned = append(potentialGroupMembersMentioned, user.Username)
   843  	}
   844  	if mentions.OtherPotentialMentions == nil {
   845  		mentions.OtherPotentialMentions = potentialGroupMembersMentioned
   846  	} else {
   847  		mentions.OtherPotentialMentions = append(mentions.OtherPotentialMentions, potentialGroupMembersMentioned...)
   848  	}
   849  
   850  	return isGroupOrDirect || len(groupMembers) > 0, nil
   851  }
   852  
   853  // addMentionKeywordsForUser adds the mention keywords for a given user to the given keyword map. Returns the provided keyword map.
   854  func addMentionKeywordsForUser(keywords map[string][]string, profile *model.User, channelNotifyProps map[string]string, status *model.Status, allowChannelMentions bool) map[string][]string {
   855  	userMention := "@" + strings.ToLower(profile.Username)
   856  	keywords[userMention] = append(keywords[userMention], profile.Id)
   857  
   858  	// Add all the user's mention keys
   859  	for _, k := range profile.GetMentionKeys() {
   860  		// note that these are made lower case so that we can do a case insensitive check for them
   861  		key := strings.ToLower(k)
   862  
   863  		if key != "" {
   864  			keywords[key] = append(keywords[key], profile.Id)
   865  		}
   866  	}
   867  
   868  	// If turned on, add the user's case sensitive first name
   869  	if profile.NotifyProps[model.FIRST_NAME_NOTIFY_PROP] == "true" {
   870  		keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
   871  	}
   872  
   873  	// Add @channel and @all to keywords if user has them turned on and the server allows them
   874  	if allowChannelMentions {
   875  		ignoreChannelMentions := channelNotifyProps[model.IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP] == model.IGNORE_CHANNEL_MENTIONS_ON
   876  
   877  		if profile.NotifyProps[model.CHANNEL_MENTIONS_NOTIFY_PROP] == "true" && !ignoreChannelMentions {
   878  			keywords["@channel"] = append(keywords["@channel"], profile.Id)
   879  			keywords["@all"] = append(keywords["@all"], profile.Id)
   880  
   881  			if status != nil && status.Status == model.STATUS_ONLINE {
   882  				keywords["@here"] = append(keywords["@here"], profile.Id)
   883  			}
   884  		}
   885  	}
   886  
   887  	return keywords
   888  }
   889  
   890  // Represents either an email or push notification and contains the fields required to send it to any user.
   891  type PostNotification struct {
   892  	Channel    *model.Channel
   893  	Post       *model.Post
   894  	ProfileMap map[string]*model.User
   895  	Sender     *model.User
   896  }
   897  
   898  // Returns the name of the channel for this notification. For direct messages, this is the sender's name
   899  // preceded by an at sign. For group messages, this is a comma-separated list of the members of the
   900  // channel, with an option to exclude the recipient of the message from that list.
   901  func (n *PostNotification) GetChannelName(userNameFormat, excludeId string) string {
   902  	switch n.Channel.Type {
   903  	case model.CHANNEL_DIRECT:
   904  		return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
   905  	case model.CHANNEL_GROUP:
   906  		names := []string{}
   907  		for _, user := range n.ProfileMap {
   908  			if user.Id != excludeId {
   909  				names = append(names, user.GetDisplayName(userNameFormat))
   910  			}
   911  		}
   912  
   913  		sort.Strings(names)
   914  
   915  		return strings.Join(names, ", ")
   916  	default:
   917  		return n.Channel.DisplayName
   918  	}
   919  }
   920  
   921  // Returns the name of the sender of this notification, accounting for things like system messages
   922  // and whether or not the username has been overridden by an integration.
   923  func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string {
   924  	if n.Post.IsSystemMessage() {
   925  		return utils.T("system.message.name")
   926  	}
   927  
   928  	if overridesAllowed && n.Channel.Type != model.CHANNEL_DIRECT {
   929  		if value, ok := n.Post.GetProps()["override_username"]; ok && n.Post.GetProp("from_webhook") == "true" {
   930  			return value.(string)
   931  		}
   932  	}
   933  
   934  	return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
   935  }
   936  
   937  // checkForMention checks if there is a mention to a specific user or to the keywords here / channel / all
   938  func (m *ExplicitMentions) checkForMention(word string, keywords map[string][]string, groups map[string]*model.Group) bool {
   939  	var mentionType MentionType
   940  
   941  	switch strings.ToLower(word) {
   942  	case "@here":
   943  		m.HereMentioned = true
   944  		mentionType = ChannelMention
   945  	case "@channel":
   946  		m.ChannelMentioned = true
   947  		mentionType = ChannelMention
   948  	case "@all":
   949  		m.AllMentioned = true
   950  		mentionType = ChannelMention
   951  	default:
   952  		mentionType = KeywordMention
   953  	}
   954  
   955  	m.addGroupMention(word, groups)
   956  
   957  	if ids, match := keywords[strings.ToLower(word)]; match {
   958  		m.addMentions(ids, mentionType)
   959  		return true
   960  	}
   961  
   962  	// Case-sensitive check for first name
   963  	if ids, match := keywords[word]; match {
   964  		m.addMentions(ids, mentionType)
   965  		return true
   966  	}
   967  
   968  	return false
   969  }
   970  
   971  // isKeywordMultibyte checks if a word containing a multibyte character contains a multibyte keyword
   972  func isKeywordMultibyte(keywords map[string][]string, word string) ([]string, bool) {
   973  	ids := []string{}
   974  	match := false
   975  	var multibyteKeywords []string
   976  	for keyword := range keywords {
   977  		if len(keyword) != utf8.RuneCountInString(keyword) {
   978  			multibyteKeywords = append(multibyteKeywords, keyword)
   979  		}
   980  	}
   981  
   982  	if len(word) != utf8.RuneCountInString(word) {
   983  		for _, key := range multibyteKeywords {
   984  			if strings.Contains(word, key) {
   985  				ids, match = keywords[key]
   986  			}
   987  		}
   988  	}
   989  	return ids, match
   990  }
   991  
   992  // Processes text to filter mentioned users and other potential mentions
   993  func (m *ExplicitMentions) processText(text string, keywords map[string][]string, groups map[string]*model.Group) {
   994  	systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
   995  
   996  	for _, word := range strings.FieldsFunc(text, func(c rune) bool {
   997  		// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
   998  		return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
   999  	}) {
  1000  		// skip word with format ':word:' with an assumption that it is an emoji format only
  1001  		if word[0] == ':' && word[len(word)-1] == ':' {
  1002  			continue
  1003  		}
  1004  
  1005  		word = strings.TrimLeft(word, ":.-_")
  1006  
  1007  		if m.checkForMention(word, keywords, groups) {
  1008  			continue
  1009  		}
  1010  
  1011  		foundWithoutSuffix := false
  1012  		wordWithoutSuffix := word
  1013  
  1014  		for len(wordWithoutSuffix) > 0 && strings.LastIndexAny(wordWithoutSuffix, ".-:_") == (len(wordWithoutSuffix)-1) {
  1015  			wordWithoutSuffix = wordWithoutSuffix[0 : len(wordWithoutSuffix)-1]
  1016  
  1017  			if m.checkForMention(wordWithoutSuffix, keywords, groups) {
  1018  				foundWithoutSuffix = true
  1019  				break
  1020  			}
  1021  		}
  1022  
  1023  		if foundWithoutSuffix {
  1024  			continue
  1025  		}
  1026  
  1027  		if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
  1028  			// No need to bother about unicode as we are looking for ASCII characters.
  1029  			last := word[len(word)-1]
  1030  			switch last {
  1031  			// If the word is possibly at the end of a sentence, remove that character.
  1032  			case '.', '-', ':':
  1033  				word = word[:len(word)-1]
  1034  			}
  1035  			m.OtherPotentialMentions = append(m.OtherPotentialMentions, word[1:])
  1036  		} else if strings.ContainsAny(word, ".-:") {
  1037  			// This word contains a character that may be the end of a sentence, so split further
  1038  			splitWords := strings.FieldsFunc(word, func(c rune) bool {
  1039  				return c == '.' || c == '-' || c == ':'
  1040  			})
  1041  
  1042  			for _, splitWord := range splitWords {
  1043  				if m.checkForMention(splitWord, keywords, groups) {
  1044  					continue
  1045  				}
  1046  				if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
  1047  					m.OtherPotentialMentions = append(m.OtherPotentialMentions, splitWord[1:])
  1048  				}
  1049  			}
  1050  		}
  1051  
  1052  		if ids, match := isKeywordMultibyte(keywords, word); match {
  1053  			m.addMentions(ids, KeywordMention)
  1054  		}
  1055  	}
  1056  }
  1057  
  1058  func (a *App) GetNotificationNameFormat(user *model.User) string {
  1059  	if !*a.Config().PrivacySettings.ShowFullName {
  1060  		return model.SHOW_USERNAME
  1061  	}
  1062  
  1063  	data, err := a.Srv().Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT)
  1064  	if err != nil {
  1065  		return *a.Config().TeamSettings.TeammateNameDisplay
  1066  	}
  1067  
  1068  	return data.Value
  1069  }