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 }