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 }