github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/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  	"strings"
     8  	"unicode"
     9  	"unicode/utf8"
    10  
    11  	"github.com/vnforks/kid/v5/mlog"
    12  	"github.com/vnforks/kid/v5/model"
    13  	"github.com/vnforks/kid/v5/store"
    14  	"github.com/vnforks/kid/v5/utils"
    15  	"github.com/vnforks/kid/v5/utils/markdown"
    16  )
    17  
    18  func (a *App) SendNotifications(post *model.Post, branch *model.Branch, class *model.Class, sender *model.User, parentPostList *model.PostList) ([]string, error) {
    19  	// Do not send notifications in archived classes
    20  	if class.DeleteAt > 0 {
    21  		return []string{}, nil
    22  	}
    23  
    24  	pchan := make(chan store.StoreResult, 1)
    25  	go func() {
    26  		props, err := a.Srv().Store.User().GetAllProfilesInClass(class.Id, true)
    27  		pchan <- store.StoreResult{Data: props, Err: err}
    28  		close(pchan)
    29  	}()
    30  
    31  	cmnchan := make(chan store.StoreResult, 1)
    32  	go func() {
    33  		props, err := a.Srv().Store.Class().GetAllClassMembersNotifyPropsForClass(class.Id, true)
    34  		cmnchan <- store.StoreResult{Data: props, Err: err}
    35  		close(cmnchan)
    36  	}()
    37  
    38  	var fchan chan store.StoreResult
    39  	if len(post.FileIds) != 0 {
    40  		fchan = make(chan store.StoreResult, 1)
    41  		go func() {
    42  			fileInfos, err := a.Srv().Store.FileInfo().GetForPost(post.Id, true, false, true)
    43  			fchan <- store.StoreResult{Data: fileInfos, Err: err}
    44  			close(fchan)
    45  		}()
    46  	}
    47  
    48  	result := <-pchan
    49  	if result.Err != nil {
    50  		return nil, result.Err
    51  	}
    52  	profileMap := result.Data.(map[string]*model.User)
    53  
    54  	result = <-cmnchan
    55  	if result.Err != nil {
    56  		return nil, result.Err
    57  	}
    58  	classMemberNotifyPropsMap := result.Data.(map[string]model.StringMap)
    59  
    60  	mentions := &ExplicitMentions{}
    61  	allActivityPushUserIds := []string{}
    62  
    63  	allowClassMentions := a.allowClassMentions(post, len(profileMap))
    64  	keywords := a.getMentionKeywordsInClass(profileMap, allowClassMentions, classMemberNotifyPropsMap)
    65  
    66  	mentions = getExplicitMentions(post, keywords)
    67  
    68  	// Add an implicit mention when a user is added to a class
    69  	// even if the user has set 'username mentions' to false in account settings.
    70  	if post.Type == model.POST_ADD_TO_CLASS {
    71  		addedUserId, ok := post.GetProp(model.POST_PROPS_ADDED_USER_ID).(string)
    72  		if ok {
    73  			mentions.addMention(addedUserId, KeywordMention)
    74  		}
    75  	}
    76  
    77  	// get users that have comment thread mentions enabled
    78  	// if len(post.RootId) > 0 && parentPostList != nil {
    79  	// 	for _, threadPost := range parentPostList.Posts {
    80  	// 		profile := profileMap[threadPost.UserId]
    81  	// 		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])) {
    82  	// 			mentionType := ThreadMention
    83  	// 			if threadPost.Id == parentPostList.Order[0] {
    84  	// 				mentionType = CommentMention
    85  	// 			}
    86  
    87  	// 			mentions.addMention(threadPost.UserId, mentionType)
    88  	// 		}
    89  	// 	}
    90  	// }
    91  
    92  	// prevent the user from mentioning themselves
    93  	if post.GetProp("from_webhook") != "true" {
    94  		mentions.removeMention(post.UserId)
    95  	}
    96  
    97  	go func() {
    98  		_, err := a.sendOutOfClassMentions(sender, post, class, mentions.OtherPotentialMentions)
    99  		if err != nil {
   100  			mlog.Error("Failed to send warning for out of class mentions", mlog.String("user_id", sender.Id), mlog.String("post_id", post.Id), mlog.Err(err))
   101  		}
   102  	}()
   103  
   104  	// find which users in the class are set up to always receive mobile notifications
   105  	for _, profile := range profileMap {
   106  		if (profile.NotifyProps[model.PUSH_NOTIFY_PROP] == model.USER_NOTIFY_ALL ||
   107  			classMemberNotifyPropsMap[profile.Id][model.PUSH_NOTIFY_PROP] == model.CLASS_NOTIFY_ALL) &&
   108  			(post.UserId != profile.Id || post.GetProp("from_webhook") == "true") &&
   109  			!post.IsSystemMessage() {
   110  			allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
   111  		}
   112  	}
   113  
   114  	mentionedUsersList := make([]string, 0, len(mentions.Mentions))
   115  	// updateMentionChans := []chan *model.AppError{}
   116  
   117  	// for id := range mentions.Mentions {
   118  	// 	mentionedUsersList = append(mentionedUsersList, id)
   119  	//
   120  	// 	umc := make(chan *model.AppError, 1)
   121  	// 	go func(userId string) {
   122  	// 		umc <- a.Srv().Store.Class().IncrementMentionCount(post.ClassId, userId)
   123  	// 		close(umc)
   124  	// 	}(id)
   125  	// 	updateMentionChans = append(updateMentionChans, umc)
   126  	// }
   127  
   128  	notification := &PostNotification{
   129  		Post:       post,
   130  		Class:      class,
   131  		ProfileMap: profileMap,
   132  		Sender:     sender,
   133  	}
   134  	/*
   135  		if *a.Config().EmailSettings.SendEmailNotifications {
   136  			for _, id := range mentionedUsersList {
   137  				if profileMap[id] == nil {
   138  					continue
   139  				}
   140  
   141  				//If email verification is required and user email is not verified don't send email.
   142  				if *a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified {
   143  					mlog.Error("Skipped sending notification email, address not verified.", mlog.String("user_email", profileMap[id].Email), mlog.String("user_id", id))
   144  					continue
   145  				}
   146  
   147  				if a.userAllowsEmail(profileMap[id], classMemberNotifyPropsMap[id], post) {
   148  					a.sendNotificationEmail(notification, profileMap[id], branch)
   149  				}
   150  			}
   151  		}
   152  	*/
   153  	// Check for class-wide mentions in classes that have too many members for those to work
   154  	if int64(len(profileMap)) > *a.Config().BranchSettings.MaxNotificationsPerClass {
   155  		T := utils.GetUserTranslations(sender.Locale)
   156  
   157  		if mentions.HereMentioned {
   158  			a.SendEphemeralPost(
   159  				post.UserId,
   160  				&model.Post{
   161  					ClassId:  post.ClassId,
   162  					Message:  T("api.post.disabled_here", map[string]interface{}{"Users": *a.Config().BranchSettings.MaxNotificationsPerClass}),
   163  					CreateAt: post.CreateAt + 1,
   164  				},
   165  			)
   166  		}
   167  
   168  		if mentions.ClassMentioned {
   169  			a.SendEphemeralPost(
   170  				post.UserId,
   171  				&model.Post{
   172  					ClassId:  post.ClassId,
   173  					Message:  T("api.post.disabled_class", map[string]interface{}{"Users": *a.Config().BranchSettings.MaxNotificationsPerClass}),
   174  					CreateAt: post.CreateAt + 1,
   175  				},
   176  			)
   177  		}
   178  
   179  		if mentions.AllMentioned {
   180  			a.SendEphemeralPost(
   181  				post.UserId,
   182  				&model.Post{
   183  					ClassId:  post.ClassId,
   184  					Message:  T("api.post.disabled_all", map[string]interface{}{"Users": *a.Config().BranchSettings.MaxNotificationsPerClass}),
   185  					CreateAt: post.CreateAt + 1,
   186  				},
   187  			)
   188  		}
   189  	}
   190  
   191  	// Make sure all mention updates are complete to prevent race
   192  	// Probably better to batch these DB updates in the future
   193  	// MUST be completed before push notifications send
   194  	// or _, umc := range updateMentionChans {
   195  	// 	if err := <-umc; err != nil {
   196  	// 		mlog.Warn(
   197  	// 			"Failed to update mention count",
   198  	// 			mlog.String("post_id", post.Id),
   199  	// 			mlog.String("class_id", post.ClassId),
   200  	// 			mlog.Err(err),
   201  	// 		)
   202  	// 	}
   203  	//
   204  
   205  	// sendPushNotifications := false
   206  	if *a.Config().EmailSettings.SendPushNotifications {
   207  		pushServer := *a.Config().EmailSettings.PushNotificationServer
   208  		if license := a.License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
   209  			mlog.Warn("Push notifications are disabled. Go to System Console > Notifications > Mobile Push to enable them.")
   210  			// sendPushNotifications = false
   211  		} else {
   212  			// sendPushNotifications = true
   213  		}
   214  	}
   215  
   216  	/*if sendPushNotifications {
   217  		for _, id := range mentionedUsersList {
   218  			if profileMap[id] == nil {
   219  				continue
   220  			}
   221  
   222  			var status *model.Status
   223  			var err *model.AppError
   224  			if status, err = a.GetStatus(id); err != nil {
   225  				status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveClass: ""}
   226  			}
   227  
   228  			if ShouldSendPushNotification(profileMap[id], classMemberNotifyPropsMap[id], true, status, post) {
   229  				mentionType := mentions.Mentions[id]
   230  
   231  				replyToThreadType := ""
   232  				if mentionType == ThreadMention {
   233  					replyToThreadType = model.COMMENTS_NOTIFY_ANY
   234  				} else if mentionType == CommentMention {
   235  					replyToThreadType = model.COMMENTS_NOTIFY_ROOT
   236  				}
   237  
   238  				a.sendPushNotification(
   239  					notification,
   240  					profileMap[id],
   241  					mentionType == KeywordMention || mentionType == ClassMention || mentionType == DMMention,
   242  					mentionType == ClassMention,
   243  					replyToThreadType,
   244  				)
   245  			} else {
   246  				// register that a notification was not sent
   247  				a.NotificationsLog().Warn("Notification not sent",
   248  					mlog.String("ackId", ""),
   249  					mlog.String("type", model.PUSH_TYPE_MESSAGE),
   250  					mlog.String("userId", id),
   251  					mlog.String("postId", post.Id),
   252  					mlog.String("status", model.PUSH_NOT_SENT),
   253  				)
   254  			}
   255  		}
   256  
   257  		for _, id := range allActivityPushUserIds {
   258  			if profileMap[id] == nil {
   259  				continue
   260  			}
   261  
   262  			if _, ok := mentions.Mentions[id]; !ok {
   263  				var status *model.Status
   264  				var err *model.AppError
   265  				if status, err = a.GetStatus(id); err != nil {
   266  					status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveClass: ""}
   267  				}
   268  
   269  				if ShouldSendPushNotification(profileMap[id], classMemberNotifyPropsMap[id], false, status, post) {
   270  					a.sendPushNotification(
   271  						notification,
   272  						profileMap[id],
   273  						false,
   274  						false,
   275  						"",
   276  					)
   277  				} else {
   278  					// register that a notification was not sent
   279  					a.NotificationsLog().Warn("Notification not sent",
   280  						mlog.String("ackId", ""),
   281  						mlog.String("type", model.PUSH_TYPE_MESSAGE),
   282  						mlog.String("userId", id),
   283  						mlog.String("postId", post.Id),
   284  						mlog.String("status", model.PUSH_NOT_SENT),
   285  					)
   286  				}
   287  			}
   288  		}
   289  	}*/
   290  
   291  	message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ClassId, "", nil)
   292  
   293  	// Note that PreparePostForClient should've already been called by this point
   294  	message.Add("post", post.ToJson())
   295  
   296  	message.Add("class_display_name", notification.GetClassName(model.SHOW_USERNAME, ""))
   297  	message.Add("class_name", class.Name)
   298  	message.Add("sender_name", notification.GetSenderName(model.SHOW_USERNAME, *a.Config().ServiceSettings.EnablePostUsernameOverride))
   299  	message.Add("branch_id", branch.Id)
   300  
   301  	if len(post.FileIds) != 0 && fchan != nil {
   302  		message.Add("otherFile", "true")
   303  
   304  		var infos []*model.FileInfo
   305  		if result := <-fchan; result.Err != nil {
   306  			mlog.Warn("Unable to get fileInfo for push notifications.", mlog.String("post_id", post.Id), mlog.Err(result.Err))
   307  		} else {
   308  			infos = result.Data.([]*model.FileInfo)
   309  		}
   310  
   311  		for _, info := range infos {
   312  			if info.IsImage() {
   313  				message.Add("image", "true")
   314  				break
   315  			}
   316  		}
   317  	}
   318  
   319  	if len(mentionedUsersList) != 0 {
   320  		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
   321  	}
   322  
   323  	a.Publish(message)
   324  	return mentionedUsersList, nil
   325  }
   326  
   327  func (a *App) userAllowsEmail(user *model.User, classMemberNotificationProps model.StringMap, post *model.Post) bool {
   328  	userAllowsEmails := user.NotifyProps[model.EMAIL_NOTIFY_PROP] != "false"
   329  	if classEmail, ok := classMemberNotificationProps[model.EMAIL_NOTIFY_PROP]; ok {
   330  		if classEmail != model.CLASS_NOTIFY_DEFAULT {
   331  			userAllowsEmails = classEmail != "false"
   332  		}
   333  	}
   334  
   335  	// Remove the user as recipient when the user has muted the class.
   336  	if classMuted, ok := classMemberNotificationProps[model.MARK_UNREAD_NOTIFY_PROP]; ok {
   337  		if classMuted == model.CLASS_MARK_UNREAD_MENTION {
   338  			mlog.Debug("Class muted for user", mlog.String("user_id", user.Id), mlog.String("class_mute", classMuted))
   339  			userAllowsEmails = false
   340  		}
   341  	}
   342  
   343  	var status *model.Status
   344  	var err *model.AppError
   345  	if status, err = a.GetStatus(user.Id); err != nil {
   346  		status = &model.Status{
   347  			UserId:         user.Id,
   348  			Status:         model.STATUS_OFFLINE,
   349  			Manual:         false,
   350  			LastActivityAt: 0,
   351  			ActiveClass:    "",
   352  		}
   353  	}
   354  
   355  	autoResponderRelated := status.Status == model.STATUS_OUT_OF_OFFICE || post.Type == model.POST_AUTO_RESPONDER
   356  	emailNotificationsAllowedForStatus := status.Status != model.STATUS_ONLINE && status.Status != model.STATUS_DND
   357  
   358  	return userAllowsEmails && emailNotificationsAllowedForStatus && user.DeleteAt == 0 && !autoResponderRelated
   359  }
   360  
   361  // sendOutOfClassMentions sends an ephemeral post to the sender of a post if any of the given potential mentions
   362  // are outside of the post's class. Returns whether or not an ephemeral post was sent.
   363  func (a *App) sendOutOfClassMentions(sender *model.User, post *model.Post, class *model.Class, potentialMentions []string) (bool, error) {
   364  	outOfClassUsers, outOfGroupsUsers, err := a.filterOutOfClassMentions(sender, post, class, potentialMentions)
   365  	if err != nil {
   366  		return false, err
   367  	}
   368  
   369  	if len(outOfClassUsers) == 0 && len(outOfGroupsUsers) == 0 {
   370  		return false, nil
   371  	}
   372  
   373  	a.SendEphemeralPost(post.UserId, makeOutOfClassMentionPost(sender, post, outOfClassUsers, outOfGroupsUsers))
   374  
   375  	return true, nil
   376  }
   377  
   378  func (a *App) filterOutOfClassMentions(sender *model.User, post *model.Post, class *model.Class, potentialMentions []string) ([]*model.User, []*model.User, error) {
   379  	if post.IsSystemMessage() {
   380  		return nil, nil, nil
   381  	}
   382  
   383  	if class.BranchId == "" {
   384  		return nil, nil, nil
   385  	}
   386  
   387  	if len(potentialMentions) == 0 {
   388  		return nil, nil, nil
   389  	}
   390  
   391  	users, err := a.Srv().Store.User().GetProfilesByUsernames(potentialMentions, &model.ViewUsersRestrictions{Branches: []string{class.BranchId}})
   392  	if err != nil {
   393  		return nil, nil, err
   394  	}
   395  
   396  	// Filter out inactive users and bots
   397  	allUsers := model.UserSlice(users).FilterByActive(true)
   398  
   399  	if len(allUsers) == 0 {
   400  		return nil, nil, nil
   401  	}
   402  
   403  	// Differentiate between users who can and can't be added to the class
   404  	var outOfClassUsers model.UserSlice
   405  	var outOfGroupsUsers model.UserSlice
   406  
   407  	outOfClassUsers = users
   408  
   409  	return outOfClassUsers, outOfGroupsUsers, nil
   410  }
   411  
   412  func makeOutOfClassMentionPost(sender *model.User, post *model.Post, outOfClassUsers, outOfGroupsUsers []*model.User) *model.Post {
   413  	allUsers := model.UserSlice(append(outOfClassUsers, outOfGroupsUsers...))
   414  
   415  	ocUsers := model.UserSlice(outOfClassUsers)
   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(outOfClassUsers) == 1 {
   427  		message = T("api.post.check_for_out_of_class_mentions.message.one", map[string]interface{}{
   428  			"Username": ocUsernames[0],
   429  		})
   430  	} else if len(outOfClassUsers) > 1 {
   431  		preliminary, final := splitAtFinal(ocUsernames)
   432  
   433  		message = T("api.post.check_for_out_of_class_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_class_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_class_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_CLASS_MEMBER: model.StringInterface{
   462  			"post_id": ephemeralPostId,
   463  
   464  			"usernames":              allUsers.Usernames(), // Kept for backwards compatibility of mobile app.
   465  			"not_in_class_usernames": ocUsernames,
   466  
   467  			"user_ids":              allUsers.IDs(), // Kept for backwards compatibility of mobile app.
   468  			"not_in_class_user_ids": ocUserIDs,
   469  
   470  			"not_in_groups_usernames": ogUsernames,
   471  			"not_in_groups_user_ids":  ogUsers.IDs(),
   472  		},
   473  	}
   474  
   475  	return &model.Post{
   476  		Id:       ephemeralPostId,
   477  		ClassId:  post.ClassId,
   478  		Message:  message,
   479  		CreateAt: post.CreateAt + 1,
   480  		Props:    props,
   481  	}
   482  }
   483  
   484  func splitAtFinal(items []string) (preliminary []string, final string) {
   485  	if len(items) == 0 {
   486  		return
   487  	}
   488  	preliminary = items[:len(items)-1]
   489  	final = items[len(items)-1]
   490  	return
   491  }
   492  
   493  type ExplicitMentions struct {
   494  	// Mentions contains the ID of each user that was mentioned and how they were mentioned.
   495  	Mentions map[string]MentionType
   496  
   497  	// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
   498  	// a corresponding keyword.
   499  	OtherPotentialMentions []string
   500  
   501  	// HereMentioned is true if the message contained @here.
   502  	HereMentioned bool
   503  
   504  	// AllMentioned is true if the message contained @all.
   505  	AllMentioned bool
   506  
   507  	// ClassMentioned is true if the message contained @class.
   508  	ClassMentioned bool
   509  }
   510  
   511  type MentionType int
   512  
   513  const (
   514  	// Different types of mentions ordered by their priority from lowest to highest
   515  
   516  	// A placeholder that should never be used in practice
   517  	NoMention MentionType = iota
   518  
   519  	// The post is in a thread that the user has commented on
   520  	ThreadMention
   521  
   522  	// The post is a comment on a thread started by the user
   523  	CommentMention
   524  
   525  	// The post contains an at-class, at-all, or at-here
   526  	ClassMention
   527  
   528  	// The post is a DM
   529  	DMMention
   530  
   531  	// The post contains an at-mention for the user
   532  	KeywordMention
   533  )
   534  
   535  func (m *ExplicitMentions) addMention(userId string, mentionType MentionType) {
   536  	if m.Mentions == nil {
   537  		m.Mentions = make(map[string]MentionType)
   538  	}
   539  
   540  	if currentType, ok := m.Mentions[userId]; ok && currentType >= mentionType {
   541  		return
   542  	}
   543  
   544  	m.Mentions[userId] = mentionType
   545  }
   546  
   547  func (m *ExplicitMentions) addMentions(userIds []string, mentionType MentionType) {
   548  	for _, userId := range userIds {
   549  		m.addMention(userId, mentionType)
   550  	}
   551  }
   552  
   553  func (m *ExplicitMentions) removeMention(userId string) {
   554  	delete(m.Mentions, userId)
   555  }
   556  
   557  // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
   558  // users and a slice of potential mention users not in the class and whether or not @here was mentioned.
   559  func getExplicitMentions(post *model.Post, keywords map[string][]string) *ExplicitMentions {
   560  	ret := &ExplicitMentions{}
   561  
   562  	buf := ""
   563  	mentionsEnabledFields := getMentionsEnabledFields(post)
   564  	for _, message := range mentionsEnabledFields {
   565  		markdown.Inspect(message, func(node interface{}) bool {
   566  			text, ok := node.(*markdown.Text)
   567  			if !ok {
   568  				ret.processText(buf, keywords)
   569  				buf = ""
   570  				return true
   571  			}
   572  			buf += text.Text
   573  			return false
   574  		})
   575  	}
   576  	ret.processText(buf, keywords)
   577  
   578  	return ret
   579  }
   580  
   581  // Given a post returns the values of the fields in which mentions are possible.
   582  // post.message, preText and text in the attachment are enabled.
   583  func getMentionsEnabledFields(post *model.Post) model.StringArray {
   584  	ret := []string{}
   585  
   586  	ret = append(ret, post.Message)
   587  	for _, attachment := range post.Attachments() {
   588  
   589  		if len(attachment.Pretext) != 0 {
   590  			ret = append(ret, attachment.Pretext)
   591  		}
   592  		if len(attachment.Text) != 0 {
   593  			ret = append(ret, attachment.Text)
   594  		}
   595  	}
   596  	return ret
   597  }
   598  
   599  // allowClassMentions returns whether or not the class mentions are allowed for the given post.
   600  func (a *App) allowClassMentions(post *model.Post, numProfiles int) bool {
   601  	if !a.HasPermissionToClass(post.UserId, post.ClassId, model.PERMISSION_USE_CLASS_MENTIONS) {
   602  		return false
   603  	}
   604  
   605  	if post.Type == model.POST_HEADER_CHANGE || post.Type == model.POST_PURPOSE_CHANGE {
   606  		return false
   607  	}
   608  
   609  	if int64(numProfiles) >= *a.Config().BranchSettings.MaxNotificationsPerClass {
   610  		return false
   611  	}
   612  
   613  	return true
   614  }
   615  
   616  // Given a map of user IDs to profiles, returns a list of mention
   617  // keywords for all users in the class.
   618  func (a *App) getMentionKeywordsInClass(profiles map[string]*model.User, allowClassMentions bool, classMemberNotifyPropsMap map[string]model.StringMap) map[string][]string {
   619  	keywords := make(map[string][]string)
   620  
   621  	for _, profile := range profiles {
   622  		addMentionKeywordsForUser(
   623  			keywords,
   624  			profile,
   625  			classMemberNotifyPropsMap[profile.Id],
   626  			a.GetStatusFromCache(profile.Id),
   627  			allowClassMentions,
   628  		)
   629  	}
   630  
   631  	return keywords
   632  }
   633  
   634  // addMentionKeywordsForUser adds the mention keywords for a given user to the given keyword map. Returns the provided keyword map.
   635  func addMentionKeywordsForUser(keywords map[string][]string, profile *model.User, classNotifyProps map[string]string, status *model.Status, allowClassMentions bool) map[string][]string {
   636  	userMention := "@" + strings.ToLower(profile.Username)
   637  	keywords[userMention] = append(keywords[userMention], profile.Id)
   638  
   639  	// Add all the user's mention keys
   640  	for _, k := range profile.GetMentionKeys() {
   641  		// note that these are made lower case so that we can do a case insensitive check for them
   642  		key := strings.ToLower(k)
   643  
   644  		if key != "" {
   645  			keywords[key] = append(keywords[key], profile.Id)
   646  		}
   647  	}
   648  
   649  	// If turned on, add the user's case sensitive first name
   650  	if profile.NotifyProps[model.FIRST_NAME_NOTIFY_PROP] == "true" {
   651  		keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
   652  	}
   653  
   654  	// Add @class and @all to keywords if user has them turned on and the server allows them
   655  	if allowClassMentions {
   656  		ignoreClassMentions := classNotifyProps[model.IGNORE_CLASS_MENTIONS_NOTIFY_PROP] == model.IGNORE_CLASS_MENTIONS_ON
   657  
   658  		if profile.NotifyProps[model.CLASS_MENTIONS_NOTIFY_PROP] == "true" && !ignoreClassMentions {
   659  			keywords["@class"] = append(keywords["@class"], profile.Id)
   660  			keywords["@all"] = append(keywords["@all"], profile.Id)
   661  
   662  			if status != nil && status.Status == model.STATUS_ONLINE {
   663  				keywords["@here"] = append(keywords["@here"], profile.Id)
   664  			}
   665  		}
   666  	}
   667  
   668  	return keywords
   669  }
   670  
   671  // Represents either an email or push notification and contains the fields required to send it to any user.
   672  type PostNotification struct {
   673  	Class      *model.Class
   674  	Post       *model.Post
   675  	ProfileMap map[string]*model.User
   676  	Sender     *model.User
   677  }
   678  
   679  // Returns the name of the class for this notification. For direct messages, this is the sender's name
   680  // preceded by an at sign. For group messages, this is a comma-separated list of the members of the
   681  // class, with an option to exclude the recipient of the message from that list.
   682  func (n *PostNotification) GetClassName(userNameFormat, excludeId string) string {
   683  	return n.Class.DisplayName
   684  }
   685  
   686  // Returns the name of the sender of this notification, accounting for things like system messages
   687  // and whether or not the username has been overridden by an integration.
   688  func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed bool) string {
   689  	if n.Post.IsSystemMessage() {
   690  		return utils.T("system.message.name")
   691  	}
   692  
   693  	if overridesAllowed {
   694  		if value, ok := n.Post.GetProps()["override_username"]; ok && n.Post.GetProp("from_webhook") == "true" {
   695  			return value.(string)
   696  		}
   697  	}
   698  
   699  	return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
   700  }
   701  
   702  // checkForMention checks if there is a mention to a specific user or to the keywords here / class / all
   703  func (m *ExplicitMentions) checkForMention(word string, keywords map[string][]string) bool {
   704  	var mentionType MentionType
   705  
   706  	switch strings.ToLower(word) {
   707  	case "@here":
   708  		m.HereMentioned = true
   709  		mentionType = ClassMention
   710  	case "@class":
   711  		m.ClassMentioned = true
   712  		mentionType = ClassMention
   713  	case "@all":
   714  		m.AllMentioned = true
   715  		mentionType = ClassMention
   716  	default:
   717  		mentionType = KeywordMention
   718  	}
   719  
   720  	if ids, match := keywords[strings.ToLower(word)]; match {
   721  		m.addMentions(ids, mentionType)
   722  		return true
   723  	}
   724  
   725  	// Case-sensitive check for first name
   726  	if ids, match := keywords[word]; match {
   727  		m.addMentions(ids, mentionType)
   728  		return true
   729  	}
   730  
   731  	return false
   732  }
   733  
   734  // isKeywordMultibyte checks if a word containing a multibyte character contains a multibyte keyword
   735  func isKeywordMultibyte(keywords map[string][]string, word string) ([]string, bool) {
   736  	ids := []string{}
   737  	match := false
   738  	var multibyteKeywords []string
   739  	for keyword := range keywords {
   740  		if len(keyword) != utf8.RuneCountInString(keyword) {
   741  			multibyteKeywords = append(multibyteKeywords, keyword)
   742  		}
   743  	}
   744  
   745  	if len(word) != utf8.RuneCountInString(word) {
   746  		for _, key := range multibyteKeywords {
   747  			if strings.Contains(word, key) {
   748  				ids, match = keywords[key]
   749  			}
   750  		}
   751  	}
   752  	return ids, match
   753  }
   754  
   755  // Processes text to filter mentioned users and other potential mentions
   756  func (m *ExplicitMentions) processText(text string, keywords map[string][]string) {
   757  	systemMentions := map[string]bool{"@here": true, "@class": true, "@all": true}
   758  
   759  	for _, word := range strings.FieldsFunc(text, func(c rune) bool {
   760  		// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
   761  		return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
   762  	}) {
   763  		// skip word with format ':word:' with an assumption that it is an emoji format only
   764  		if word[0] == ':' && word[len(word)-1] == ':' {
   765  			continue
   766  		}
   767  
   768  		word = strings.TrimLeft(word, ":.-_")
   769  
   770  		if m.checkForMention(word, keywords) {
   771  			continue
   772  		}
   773  
   774  		foundWithoutSuffix := false
   775  		wordWithoutSuffix := word
   776  		for len(wordWithoutSuffix) > 0 && strings.LastIndexAny(wordWithoutSuffix, ".-:_") == (len(wordWithoutSuffix)-1) {
   777  			wordWithoutSuffix = wordWithoutSuffix[0 : len(wordWithoutSuffix)-1]
   778  
   779  			if m.checkForMention(wordWithoutSuffix, keywords) {
   780  				foundWithoutSuffix = true
   781  				break
   782  			}
   783  		}
   784  
   785  		if foundWithoutSuffix {
   786  			continue
   787  		}
   788  
   789  		if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
   790  			// No need to bother about unicode as we are looking for ASCII characters.
   791  			last := word[len(word)-1]
   792  			switch last {
   793  			// If the word is possibly at the end of a sentence, remove that character.
   794  			case '.', '-', ':':
   795  				word = word[:len(word)-1]
   796  			}
   797  			m.OtherPotentialMentions = append(m.OtherPotentialMentions, word[1:])
   798  		} else if strings.ContainsAny(word, ".-:") {
   799  			// This word contains a character that may be the end of a sentence, so split further
   800  			splitWords := strings.FieldsFunc(word, func(c rune) bool {
   801  				return c == '.' || c == '-' || c == ':'
   802  			})
   803  
   804  			for _, splitWord := range splitWords {
   805  				if m.checkForMention(splitWord, keywords) {
   806  					continue
   807  				}
   808  				if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
   809  					m.OtherPotentialMentions = append(m.OtherPotentialMentions, splitWord[1:])
   810  				}
   811  			}
   812  		}
   813  
   814  		if ids, match := isKeywordMultibyte(keywords, word); match {
   815  			m.addMentions(ids, KeywordMention)
   816  		}
   817  	}
   818  }
   819  
   820  func (a *App) GetNotificationNameFormat(user *model.User) string {
   821  	if !*a.Config().PrivacySettings.ShowFullName {
   822  		return model.SHOW_USERNAME
   823  	}
   824  
   825  	data, err := a.Srv().Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT)
   826  	if err != nil {
   827  		return *a.Config().BranchSettings.BranchmateNameDisplay
   828  	}
   829  
   830  	return data.Value
   831  }