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