github.com/dschalla/mattermost-server@v4.8.1-rc1+incompatible/app/notification.go (about) 1 // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package app 5 6 import ( 7 "fmt" 8 "html" 9 "html/template" 10 "net/http" 11 "net/url" 12 "path/filepath" 13 "sort" 14 "strings" 15 "time" 16 "unicode" 17 18 l4g "github.com/alecthomas/log4go" 19 "github.com/mattermost/mattermost-server/model" 20 "github.com/mattermost/mattermost-server/store" 21 "github.com/mattermost/mattermost-server/utils" 22 "github.com/mattermost/mattermost-server/utils/markdown" 23 "github.com/nicksnyder/go-i18n/i18n" 24 ) 25 26 func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList) ([]string, *model.AppError) { 27 pchan := a.Srv.Store.User().GetAllProfilesInChannel(channel.Id, true) 28 cmnchan := a.Srv.Store.Channel().GetAllChannelMembersNotifyPropsForChannel(channel.Id, true) 29 var fchan store.StoreChannel 30 31 if len(post.FileIds) != 0 { 32 fchan = a.Srv.Store.FileInfo().GetForPost(post.Id, true, true) 33 } 34 35 var profileMap map[string]*model.User 36 if result := <-pchan; result.Err != nil { 37 return nil, result.Err 38 } else { 39 profileMap = result.Data.(map[string]*model.User) 40 } 41 42 var channelMemberNotifyPropsMap map[string]model.StringMap 43 if result := <-cmnchan; result.Err != nil { 44 return nil, result.Err 45 } else { 46 channelMemberNotifyPropsMap = result.Data.(map[string]model.StringMap) 47 } 48 49 mentionedUserIds := make(map[string]bool) 50 allActivityPushUserIds := []string{} 51 hereNotification := false 52 channelNotification := false 53 allNotification := false 54 updateMentionChans := []store.StoreChannel{} 55 56 if channel.Type == model.CHANNEL_DIRECT { 57 var otherUserId string 58 if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId { 59 otherUserId = userIds[1] 60 } else { 61 otherUserId = userIds[0] 62 } 63 64 if _, ok := profileMap[otherUserId]; ok { 65 mentionedUserIds[otherUserId] = true 66 } 67 68 if post.Props["from_webhook"] == "true" { 69 mentionedUserIds[post.UserId] = true 70 } 71 } else { 72 keywords := a.GetMentionKeywordsInChannel(profileMap, post.Type != model.POST_HEADER_CHANGE && post.Type != model.POST_PURPOSE_CHANGE) 73 74 m := GetExplicitMentions(post.Message, keywords) 75 mentionedUserIds, hereNotification, channelNotification, allNotification = m.MentionedUserIds, m.HereMentioned, m.ChannelMentioned, m.AllMentioned 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["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == parentPostList.Order[0])) { 82 mentionedUserIds[threadPost.UserId] = true 83 } 84 } 85 } 86 87 // prevent the user from mentioning themselves 88 if post.Props["from_webhook"] != "true" { 89 delete(mentionedUserIds, post.UserId) 90 } 91 92 if len(m.OtherPotentialMentions) > 0 && !post.IsSystemMessage() { 93 if result := <-a.Srv.Store.User().GetProfilesByUsernames(m.OtherPotentialMentions, team.Id); result.Err == nil { 94 outOfChannelMentions := result.Data.([]*model.User) 95 if channel.Type != model.CHANNEL_GROUP { 96 a.Go(func() { 97 a.sendOutOfChannelMentions(sender, post, channel.Type, outOfChannelMentions) 98 }) 99 } 100 } 101 } 102 103 // find which users in the channel are set up to always receive mobile notifications 104 for _, profile := range profileMap { 105 if (profile.NotifyProps[model.PUSH_NOTIFY_PROP] == model.USER_NOTIFY_ALL || 106 channelMemberNotifyPropsMap[profile.Id][model.PUSH_NOTIFY_PROP] == model.CHANNEL_NOTIFY_ALL) && 107 (post.UserId != profile.Id || post.Props["from_webhook"] == "true") && 108 !post.IsSystemMessage() { 109 allActivityPushUserIds = append(allActivityPushUserIds, profile.Id) 110 } 111 } 112 } 113 114 mentionedUsersList := make([]string, 0, len(mentionedUserIds)) 115 for id := range mentionedUserIds { 116 mentionedUsersList = append(mentionedUsersList, id) 117 updateMentionChans = append(updateMentionChans, a.Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id)) 118 } 119 120 senderName := "" 121 channelName := "" 122 if post.IsSystemMessage() { 123 senderName = utils.T("system.message.name") 124 } else { 125 if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { 126 senderName = value.(string) 127 } else { 128 senderName = sender.Username 129 } 130 } 131 132 if channel.Type == model.CHANNEL_GROUP { 133 userList := []*model.User{} 134 for _, u := range profileMap { 135 if u.Id != sender.Id { 136 userList = append(userList, u) 137 } 138 } 139 userList = append(userList, sender) 140 channelName = model.GetGroupDisplayNameFromUsers(userList, false) 141 } else { 142 channelName = channel.DisplayName 143 } 144 145 var senderUsername string 146 if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" { 147 senderUsername = value.(string) 148 } else { 149 senderUsername = sender.Username 150 } 151 152 if a.Config().EmailSettings.SendEmailNotifications { 153 for _, id := range mentionedUsersList { 154 userAllowsEmails := profileMap[id].NotifyProps[model.EMAIL_NOTIFY_PROP] != "false" 155 if channelEmail, ok := channelMemberNotifyPropsMap[id][model.EMAIL_NOTIFY_PROP]; ok { 156 if channelEmail != model.CHANNEL_NOTIFY_DEFAULT { 157 userAllowsEmails = channelEmail != "false" 158 } 159 } 160 161 //If email verification is required and user email is not verified don't send email. 162 if a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified { 163 l4g.Error("Skipped sending notification email to %v, address not verified. [details: user_id=%v]", profileMap[id].Email, id) 164 continue 165 } 166 167 var status *model.Status 168 var err *model.AppError 169 if status, err = a.GetStatus(id); err != nil { 170 status = &model.Status{ 171 UserId: id, 172 Status: model.STATUS_OFFLINE, 173 Manual: false, 174 LastActivityAt: 0, 175 ActiveChannel: "", 176 } 177 } 178 179 if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 { 180 a.sendNotificationEmail(post, profileMap[id], channel, team, senderName, sender) 181 } 182 } 183 } 184 185 T := utils.GetUserTranslations(sender.Locale) 186 187 // If the channel has more than 1K users then @here is disabled 188 if hereNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel { 189 hereNotification = false 190 a.SendEphemeralPost( 191 post.UserId, 192 &model.Post{ 193 ChannelId: post.ChannelId, 194 Message: T("api.post.disabled_here", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}), 195 CreateAt: post.CreateAt + 1, 196 }, 197 ) 198 } 199 200 // If the channel has more than 1K users then @channel is disabled 201 if channelNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel { 202 a.SendEphemeralPost( 203 post.UserId, 204 &model.Post{ 205 ChannelId: post.ChannelId, 206 Message: T("api.post.disabled_channel", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}), 207 CreateAt: post.CreateAt + 1, 208 }, 209 ) 210 } 211 212 // If the channel has more than 1K users then @all is disabled 213 if allNotification && int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel { 214 a.SendEphemeralPost( 215 post.UserId, 216 &model.Post{ 217 ChannelId: post.ChannelId, 218 Message: T("api.post.disabled_all", map[string]interface{}{"Users": *a.Config().TeamSettings.MaxNotificationsPerChannel}), 219 CreateAt: post.CreateAt + 1, 220 }, 221 ) 222 } 223 224 // Make sure all mention updates are complete to prevent race 225 // Probably better to batch these DB updates in the future 226 // MUST be completed before push notifications send 227 for _, uchan := range updateMentionChans { 228 if result := <-uchan; result.Err != nil { 229 l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err) 230 } 231 } 232 233 sendPushNotifications := false 234 if *a.Config().EmailSettings.SendPushNotifications { 235 pushServer := *a.Config().EmailSettings.PushNotificationServer 236 if license := a.License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) { 237 l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) 238 sendPushNotifications = false 239 } else { 240 sendPushNotifications = true 241 } 242 } 243 244 if sendPushNotifications { 245 for _, id := range mentionedUsersList { 246 var status *model.Status 247 var err *model.AppError 248 if status, err = a.GetStatus(id); err != nil { 249 status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""} 250 } 251 252 if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) { 253 a.sendPushNotification(post, profileMap[id], channel, senderName, channelName, true) 254 } 255 } 256 257 for _, id := range allActivityPushUserIds { 258 if _, ok := mentionedUserIds[id]; !ok { 259 var status *model.Status 260 var err *model.AppError 261 if status, err = a.GetStatus(id); err != nil { 262 status = &model.Status{UserId: id, Status: model.STATUS_OFFLINE, Manual: false, LastActivityAt: 0, ActiveChannel: ""} 263 } 264 265 if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) { 266 a.sendPushNotification(post, profileMap[id], channel, senderName, channelName, false) 267 } 268 } 269 } 270 } 271 272 message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil) 273 message.Add("post", a.PostWithProxyAddedToImageURLs(post).ToJson()) 274 message.Add("channel_type", channel.Type) 275 message.Add("channel_display_name", channelName) 276 message.Add("channel_name", channel.Name) 277 message.Add("sender_name", senderUsername) 278 message.Add("team_id", team.Id) 279 280 if len(post.FileIds) != 0 && fchan != nil { 281 message.Add("otherFile", "true") 282 283 var infos []*model.FileInfo 284 if result := <-fchan; result.Err != nil { 285 l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err) 286 } else { 287 infos = result.Data.([]*model.FileInfo) 288 } 289 290 for _, info := range infos { 291 if info.IsImage() { 292 message.Add("image", "true") 293 break 294 } 295 } 296 } 297 298 if len(mentionedUsersList) != 0 { 299 message.Add("mentions", model.ArrayToJson(mentionedUsersList)) 300 } 301 302 a.Publish(message) 303 return mentionedUsersList, nil 304 } 305 306 func (a *App) sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError { 307 if channel.IsGroupOrDirect() { 308 if result := <-a.Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil { 309 return result.Err 310 } else { 311 // if the recipient isn't in the current user's team, just pick one 312 teams := result.Data.([]*model.Team) 313 found := false 314 315 for i := range teams { 316 if teams[i].Id == team.Id { 317 found = true 318 break 319 } 320 } 321 322 if !found && len(teams) > 0 { 323 team = teams[0] 324 } else { 325 // in case the user hasn't joined any teams we send them to the select_team page 326 team = &model.Team{Name: "select_team", DisplayName: a.Config().TeamSettings.SiteName} 327 } 328 } 329 } 330 if *a.Config().EmailSettings.EnableEmailBatching { 331 var sendBatched bool 332 if result := <-a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.Err != nil { 333 // if the call fails, assume that the interval has not been explicitly set and batch the notifications 334 sendBatched = true 335 } else { 336 // if the user has chosen to receive notifications immediately, don't batch them 337 sendBatched = result.Data.(model.Preference).Value != model.PREFERENCE_EMAIL_INTERVAL_NO_BATCHING_SECONDS 338 } 339 340 if sendBatched { 341 if err := a.AddNotificationEmailToBatch(user, post, team); err == nil { 342 return nil 343 } 344 } 345 346 // fall back to sending a single email if we can't batch it for some reason 347 } 348 349 translateFunc := utils.GetUserTranslations(user.Locale) 350 351 var subjectText string 352 if channel.Type == model.CHANNEL_DIRECT { 353 subjectText = getDirectMessageNotificationEmailSubject(post, translateFunc, a.Config().TeamSettings.SiteName, senderName) 354 } else if *a.Config().EmailSettings.UseChannelInEmailNotifications { 355 subjectText = getNotificationEmailSubject(post, translateFunc, a.Config().TeamSettings.SiteName, team.DisplayName+" ("+channel.DisplayName+")") 356 } else { 357 subjectText = getNotificationEmailSubject(post, translateFunc, a.Config().TeamSettings.SiteName, team.DisplayName) 358 } 359 360 emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL 361 if license := a.License(); license != nil && *license.Features.EmailNotificationContents { 362 emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType 363 } 364 365 teamURL := a.GetSiteURL() + "/" + team.Name 366 var bodyText = a.getNotificationEmailBody(user, post, channel, senderName, team.Name, teamURL, emailNotificationContentsType, translateFunc) 367 368 a.Go(func() { 369 if err := a.SendMail(user.Email, html.UnescapeString(subjectText), bodyText); err != nil { 370 l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), user.Email, err) 371 } 372 }) 373 374 if a.Metrics != nil { 375 a.Metrics.IncrementPostSentEmail() 376 } 377 378 return nil 379 } 380 381 /** 382 * Computes the subject line for direct notification email messages 383 */ 384 func getDirectMessageNotificationEmailSubject(post *model.Post, translateFunc i18n.TranslateFunc, siteName string, senderName string) string { 385 t := getFormattedPostTime(post, translateFunc) 386 var subjectParameters = map[string]interface{}{ 387 "SiteName": siteName, 388 "SenderDisplayName": senderName, 389 "Month": t.Month, 390 "Day": t.Day, 391 "Year": t.Year, 392 } 393 return translateFunc("app.notification.subject.direct.full", subjectParameters) 394 } 395 396 /** 397 * Computes the subject line for group, public, and private email messages 398 */ 399 func getNotificationEmailSubject(post *model.Post, translateFunc i18n.TranslateFunc, siteName string, teamName string) string { 400 t := getFormattedPostTime(post, translateFunc) 401 var subjectParameters = map[string]interface{}{ 402 "SiteName": siteName, 403 "TeamName": teamName, 404 "Month": t.Month, 405 "Day": t.Day, 406 "Year": t.Year, 407 } 408 return translateFunc("app.notification.subject.notification.full", subjectParameters) 409 } 410 411 /** 412 * Computes the email body for notification messages 413 */ 414 func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, channel *model.Channel, senderName string, teamName string, teamURL string, emailNotificationContentsType string, translateFunc i18n.TranslateFunc) string { 415 // only include message contents in notification email if email notification contents type is set to full 416 var bodyPage *utils.HTMLTemplate 417 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 418 bodyPage = a.NewEmailTemplate("post_body_full", recipient.Locale) 419 bodyPage.Props["PostMessage"] = a.GetMessageForNotification(post, translateFunc) 420 } else { 421 bodyPage = a.NewEmailTemplate("post_body_generic", recipient.Locale) 422 } 423 424 bodyPage.Props["SiteURL"] = a.GetSiteURL() 425 if teamName != "select_team" { 426 bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id 427 } else { 428 bodyPage.Props["TeamLink"] = teamURL 429 } 430 431 var channelName = channel.DisplayName 432 if channel.Type == model.CHANNEL_GROUP { 433 channelName = translateFunc("api.templates.channel_name.group") 434 } 435 t := getFormattedPostTime(post, translateFunc) 436 437 var bodyText string 438 var info template.HTML 439 if channel.Type == model.CHANNEL_DIRECT { 440 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 441 bodyText = translateFunc("app.notification.body.intro.direct.full") 442 info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.direct.full", 443 map[string]interface{}{ 444 "SenderName": senderName, 445 "Hour": t.Hour, 446 "Minute": t.Minute, 447 "TimeZone": t.TimeZone, 448 "Month": t.Month, 449 "Day": t.Day, 450 }) 451 } else { 452 bodyText = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{ 453 "SenderName": senderName, 454 }) 455 info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.direct.generic", 456 map[string]interface{}{ 457 "Hour": t.Hour, 458 "Minute": t.Minute, 459 "TimeZone": t.TimeZone, 460 "Month": t.Month, 461 "Day": t.Day, 462 }) 463 } 464 } else { 465 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 466 bodyText = translateFunc("app.notification.body.intro.notification.full") 467 info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.notification.full", 468 map[string]interface{}{ 469 "ChannelName": channelName, 470 "SenderName": senderName, 471 "Hour": t.Hour, 472 "Minute": t.Minute, 473 "TimeZone": t.TimeZone, 474 "Month": t.Month, 475 "Day": t.Day, 476 }) 477 } else { 478 bodyText = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{ 479 "SenderName": senderName, 480 }) 481 info = utils.TranslateAsHtml(translateFunc, "app.notification.body.text.notification.generic", 482 map[string]interface{}{ 483 "Hour": t.Hour, 484 "Minute": t.Minute, 485 "TimeZone": t.TimeZone, 486 "Month": t.Month, 487 "Day": t.Day, 488 }) 489 } 490 } 491 492 bodyPage.Props["BodyText"] = bodyText 493 bodyPage.Html["Info"] = info 494 bodyPage.Props["Button"] = translateFunc("api.templates.post_body.button") 495 496 return bodyPage.Render() 497 } 498 499 type formattedPostTime struct { 500 Time time.Time 501 Year string 502 Month string 503 Day string 504 Hour string 505 Minute string 506 TimeZone string 507 } 508 509 func getFormattedPostTime(post *model.Post, translateFunc i18n.TranslateFunc) formattedPostTime { 510 tm := time.Unix(post.CreateAt/1000, 0) 511 zone, _ := tm.Zone() 512 513 return formattedPostTime{ 514 Time: tm, 515 Year: fmt.Sprintf("%d", tm.Year()), 516 Month: translateFunc(tm.Month().String()), 517 Day: fmt.Sprintf("%d", tm.Day()), 518 Hour: fmt.Sprintf("%02d", tm.Hour()), 519 Minute: fmt.Sprintf("%02d", tm.Minute()), 520 TimeZone: zone, 521 } 522 } 523 524 func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { 525 if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 { 526 return post.Message 527 } 528 529 // extract the filenames from their paths and determine what type of files are attached 530 var infos []*model.FileInfo 531 if result := <-a.Srv.Store.FileInfo().GetForPost(post.Id, true, true); result.Err != nil { 532 l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err) 533 } else { 534 infos = result.Data.([]*model.FileInfo) 535 } 536 537 filenames := make([]string, len(infos)) 538 onlyImages := true 539 for i, info := range infos { 540 if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil { 541 // this should never error since filepath was escaped using url.QueryEscape 542 filenames[i] = escaped 543 } else { 544 filenames[i] = info.Name 545 } 546 547 onlyImages = onlyImages && info.IsImage() 548 } 549 550 props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")} 551 552 if onlyImages { 553 return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props) 554 } else { 555 return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props) 556 } 557 } 558 559 func (a *App) sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName, channelName string, wasMentioned bool) *model.AppError { 560 sessions, err := a.getMobileAppSessions(user.Id) 561 if err != nil { 562 return err 563 } 564 565 if channel.Type == model.CHANNEL_DIRECT { 566 channelName = senderName 567 } 568 569 msg := model.PushNotification{} 570 if badge := <-a.Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil { 571 msg.Badge = 1 572 l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err) 573 } else { 574 msg.Badge = int(badge.Data.(int64)) 575 } 576 577 msg.Type = model.PUSH_TYPE_MESSAGE 578 msg.TeamId = channel.TeamId 579 msg.ChannelId = channel.Id 580 msg.PostId = post.Id 581 msg.RootId = post.RootId 582 msg.ChannelName = channel.Name 583 msg.SenderId = post.UserId 584 585 if ou, ok := post.Props["override_username"].(string); ok { 586 msg.OverrideUsername = ou 587 } 588 589 if oi, ok := post.Props["override_icon_url"].(string); ok { 590 msg.OverrideIconUrl = oi 591 } 592 593 if fw, ok := post.Props["from_webhook"].(string); ok { 594 msg.FromWebhook = fw 595 } 596 597 userLocale := utils.GetUserTranslations(user.Locale) 598 hasFiles := post.FileIds != nil && len(post.FileIds) > 0 599 600 msg.Message, msg.Category = a.getPushNotificationMessage(post.Message, wasMentioned, hasFiles, senderName, channelName, channel.Type, userLocale) 601 602 for _, session := range sessions { 603 tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) 604 tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) 605 606 l4g.Debug("Sending push notification to device %v for user %v with msg of '%v'", tmpMessage.DeviceId, user.Id, msg.Message) 607 608 a.Go(func(session *model.Session) func() { 609 return func() { 610 a.sendToPushProxy(tmpMessage, session) 611 } 612 }(session)) 613 614 if a.Metrics != nil { 615 a.Metrics.IncrementPostSentPush() 616 } 617 } 618 619 return nil 620 } 621 622 func (a *App) getPushNotificationMessage(postMessage string, wasMentioned bool, hasFiles bool, senderName string, channelName string, channelType string, userLocale i18n.TranslateFunc) (string, string) { 623 message := "" 624 category := "" 625 626 contentsConfig := *a.Config().EmailSettings.PushNotificationContents 627 628 if contentsConfig == model.FULL_NOTIFICATION { 629 category = model.CATEGORY_CAN_REPLY 630 631 if channelType == model.CHANNEL_DIRECT { 632 message = senderName + ": " + model.ClearMentionTags(postMessage) 633 } else { 634 message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(postMessage) 635 } 636 } else if contentsConfig == model.GENERIC_NO_CHANNEL_NOTIFICATION { 637 if channelType == model.CHANNEL_DIRECT { 638 category = model.CATEGORY_CAN_REPLY 639 640 message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") 641 } else if wasMentioned { 642 message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention_no_channel") 643 } else { 644 message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention_no_channel") 645 } 646 } else { 647 if channelType == model.CHANNEL_DIRECT { 648 category = model.CATEGORY_CAN_REPLY 649 650 message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") 651 } else if wasMentioned { 652 category = model.CATEGORY_CAN_REPLY 653 654 message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName 655 } else { 656 message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName 657 } 658 } 659 660 // If the post only has images then push an appropriate message 661 if len(postMessage) == 0 && hasFiles { 662 if channelType == model.CHANNEL_DIRECT { 663 message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_dm") 664 } else if contentsConfig == model.GENERIC_NO_CHANNEL_NOTIFICATION { 665 message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_no_channel") 666 } else { 667 message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + channelName 668 } 669 } 670 671 return message, category 672 } 673 674 func (a *App) ClearPushNotification(userId string, channelId string) { 675 a.Go(func() { 676 // Sleep is to allow the read replicas a chance to fully sync 677 // the unread count for sending an accurate count. 678 // Delaying a little doesn't hurt anything and is cheaper than 679 // attempting to read from master. 680 time.Sleep(time.Second * 5) 681 682 sessions, err := a.getMobileAppSessions(userId) 683 if err != nil { 684 l4g.Error(err.Error()) 685 return 686 } 687 688 msg := model.PushNotification{} 689 msg.Type = model.PUSH_TYPE_CLEAR 690 msg.ChannelId = channelId 691 msg.ContentAvailable = 0 692 if badge := <-a.Srv.Store.User().GetUnreadCount(userId); badge.Err != nil { 693 msg.Badge = 0 694 l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err) 695 } else { 696 msg.Badge = int(badge.Data.(int64)) 697 } 698 699 l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId) 700 701 for _, session := range sessions { 702 tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) 703 tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) 704 a.Go(func() { 705 a.sendToPushProxy(tmpMessage, session) 706 }) 707 } 708 }) 709 } 710 711 func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) { 712 msg.ServerId = a.DiagnosticId() 713 714 request, _ := http.NewRequest("POST", *a.Config().EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) 715 716 if resp, err := a.HTTPClient(true).Do(request); err != nil { 717 l4g.Error("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, err.Error()) 718 } else { 719 pushResponse := model.PushResponseFromJson(resp.Body) 720 if resp.Body != nil { 721 consumeAndClose(resp) 722 } 723 724 if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_REMOVE { 725 l4g.Info("Device was reported as removed for UserId=%v SessionId=%v removing push for this session", session.UserId, session.Id) 726 a.AttachDeviceId(session.Id, "", session.ExpiresAt) 727 a.ClearSessionCacheForUser(session.UserId) 728 } 729 730 if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_FAIL { 731 l4g.Error("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, pushResponse[model.PUSH_STATUS_ERROR_MSG]) 732 } 733 } 734 } 735 736 func (a *App) getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) { 737 if result := <-a.Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil { 738 return nil, result.Err 739 } else { 740 return result.Data.([]*model.Session), nil 741 } 742 } 743 744 func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, channelType string, users []*model.User) *model.AppError { 745 if len(users) == 0 { 746 return nil 747 } 748 749 var usernames []string 750 for _, user := range users { 751 usernames = append(usernames, user.Username) 752 } 753 sort.Strings(usernames) 754 755 var userIds []string 756 for _, user := range users { 757 userIds = append(userIds, user.Id) 758 } 759 760 T := utils.GetUserTranslations(sender.Locale) 761 762 var localePhrase string 763 if channelType == model.CHANNEL_OPEN { 764 localePhrase = T("api.post.check_for_out_of_channel_mentions.link.public") 765 } else if channelType == model.CHANNEL_PRIVATE { 766 localePhrase = T("api.post.check_for_out_of_channel_mentions.link.private") 767 } 768 769 ephemeralPostId := model.NewId() 770 var message string 771 if len(users) == 1 { 772 message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{ 773 "Username": usernames[0], 774 "Phrase": localePhrase, 775 }) 776 } else { 777 message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{ 778 "Usernames": strings.Join(usernames[:len(usernames)-1], ", @"), 779 "LastUsername": usernames[len(usernames)-1], 780 "Phrase": localePhrase, 781 }) 782 } 783 784 props := model.StringInterface{ 785 model.PROPS_ADD_CHANNEL_MEMBER: model.StringInterface{ 786 "post_id": ephemeralPostId, 787 "usernames": usernames, 788 "user_ids": userIds, 789 }, 790 } 791 792 a.SendEphemeralPost( 793 post.UserId, 794 &model.Post{ 795 Id: ephemeralPostId, 796 RootId: post.RootId, 797 ChannelId: post.ChannelId, 798 Message: message, 799 CreateAt: post.CreateAt + 1, 800 Props: props, 801 }, 802 ) 803 804 return nil 805 } 806 807 type ExplicitMentions struct { 808 // MentionedUserIds contains a key for each user mentioned by keyword. 809 MentionedUserIds map[string]bool 810 811 // OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have 812 // a corresponding keyword. 813 OtherPotentialMentions []string 814 815 // HereMentioned is true if the message contained @here. 816 HereMentioned bool 817 818 // AllMentioned is true if the message contained @all. 819 AllMentioned bool 820 821 // ChannelMentioned is true if the message contained @channel. 822 ChannelMentioned bool 823 } 824 825 // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned 826 // users and a slice of potential mention users not in the channel and whether or not @here was mentioned. 827 func GetExplicitMentions(message string, keywords map[string][]string) *ExplicitMentions { 828 ret := &ExplicitMentions{ 829 MentionedUserIds: make(map[string]bool), 830 } 831 systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true} 832 833 addMentionedUsers := func(ids []string) { 834 for _, id := range ids { 835 ret.MentionedUserIds[id] = true 836 } 837 } 838 checkForMention := func(word string) bool { 839 isMention := false 840 841 if word == "@here" { 842 ret.HereMentioned = true 843 } 844 845 if word == "@channel" { 846 ret.ChannelMentioned = true 847 } 848 849 if word == "@all" { 850 ret.AllMentioned = true 851 } 852 853 // Non-case-sensitive check for regular keys 854 if ids, match := keywords[strings.ToLower(word)]; match { 855 addMentionedUsers(ids) 856 isMention = true 857 } 858 859 // Case-sensitive check for first name 860 if ids, match := keywords[word]; match { 861 addMentionedUsers(ids) 862 isMention = true 863 } 864 865 return isMention 866 } 867 processText := func(text string) { 868 for _, word := range strings.FieldsFunc(text, func(c rune) bool { 869 // Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern 870 return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c)) 871 }) { 872 // skip word with format ':word:' with an assumption that it is an emoji format only 873 if word[0] == ':' && word[len(word)-1] == ':' { 874 continue 875 } 876 877 if checkForMention(word) { 878 continue 879 } 880 881 // remove trailing '.', as that is the end of a sentence 882 word = strings.TrimSuffix(word, ".") 883 if checkForMention(word) { 884 continue 885 } 886 887 if strings.ContainsAny(word, ".-:") { 888 // This word contains a character that may be the end of a sentence, so split further 889 splitWords := strings.FieldsFunc(word, func(c rune) bool { 890 return c == '.' || c == '-' || c == ':' 891 }) 892 893 for _, splitWord := range splitWords { 894 if checkForMention(splitWord) { 895 continue 896 } 897 if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") { 898 username := splitWord[1:] 899 ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username) 900 } 901 } 902 } 903 904 if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { 905 username := word[1:] 906 ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username) 907 } 908 } 909 } 910 911 buf := "" 912 markdown.Inspect(message, func(node interface{}) bool { 913 text, ok := node.(*markdown.Text) 914 if !ok { 915 processText(buf) 916 buf = "" 917 return true 918 } 919 buf += text.Text 920 return false 921 }) 922 processText(buf) 923 924 return ret 925 } 926 927 // Given a map of user IDs to profiles, returns a list of mention 928 // keywords for all users in the channel. 929 func (a *App) GetMentionKeywordsInChannel(profiles map[string]*model.User, lookForSpecialMentions bool) map[string][]string { 930 keywords := make(map[string][]string) 931 932 for id, profile := range profiles { 933 userMention := "@" + strings.ToLower(profile.Username) 934 keywords[userMention] = append(keywords[userMention], id) 935 936 if len(profile.NotifyProps["mention_keys"]) > 0 { 937 // Add all the user's mention keys 938 splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") 939 for _, k := range splitKeys { 940 // note that these are made lower case so that we can do a case insensitive check for them 941 key := strings.ToLower(k) 942 keywords[key] = append(keywords[key], id) 943 } 944 } 945 946 // If turned on, add the user's case sensitive first name 947 if profile.NotifyProps["first_name"] == "true" { 948 keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id) 949 } 950 951 // Add @channel and @all to keywords if user has them turned on 952 if lookForSpecialMentions { 953 if int64(len(profiles)) <= *a.Config().TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" { 954 keywords["@channel"] = append(keywords["@channel"], profile.Id) 955 keywords["@all"] = append(keywords["@all"], profile.Id) 956 957 status := GetStatusFromCache(profile.Id) 958 if status != nil && status.Status == model.STATUS_ONLINE { 959 keywords["@here"] = append(keywords["@here"], profile.Id) 960 } 961 } 962 } 963 } 964 965 return keywords 966 } 967 968 func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool { 969 return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) && 970 DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId) 971 } 972 973 func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool { 974 userNotifyProps := user.NotifyProps 975 userNotify := userNotifyProps[model.PUSH_NOTIFY_PROP] 976 channelNotify, ok := channelNotifyProps[model.PUSH_NOTIFY_PROP] 977 978 if post.IsSystemMessage() { 979 return false 980 } 981 982 if channelNotify == model.USER_NOTIFY_NONE { 983 return false 984 } 985 986 if channelNotify == model.CHANNEL_NOTIFY_MENTION && !wasMentioned { 987 return false 988 } 989 990 if userNotify == model.USER_NOTIFY_MENTION && (!ok || channelNotify == model.CHANNEL_NOTIFY_DEFAULT) && !wasMentioned { 991 return false 992 } 993 994 if (userNotify == model.USER_NOTIFY_ALL || channelNotify == model.CHANNEL_NOTIFY_ALL) && 995 (post.UserId != user.Id || post.Props["from_webhook"] == "true") { 996 return true 997 } 998 999 if userNotify == model.USER_NOTIFY_NONE && 1000 (!ok || channelNotify == model.CHANNEL_NOTIFY_DEFAULT) { 1001 return false 1002 } 1003 1004 return true 1005 } 1006 1007 func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelId string) bool { 1008 // If User status is DND return false right away 1009 if status.Status == model.STATUS_DND { 1010 return false 1011 } 1012 1013 if pushStatus, ok := userNotifyProps["push_status"]; (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) { 1014 return true 1015 } else if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) { 1016 return true 1017 } else if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE { 1018 return true 1019 } 1020 1021 return false 1022 }