github.com/psyb0t/mattermost-server@v4.6.1-0.20180125161845-5503a1351abf+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 { 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 pushServer == model.MHPNS && (!utils.IsLicensed() || !*utils.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 utils.IsLicensed() && *utils.License().Features.EmailNotificationContents { 362 emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType 363 } 364 365 teamURL := utils.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"] = utils.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 userLocale := utils.GetUserTranslations(user.Locale) 570 571 msg := model.PushNotification{} 572 if badge := <-a.Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil { 573 msg.Badge = 1 574 l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err) 575 } else { 576 msg.Badge = int(badge.Data.(int64)) 577 } 578 579 msg.Type = model.PUSH_TYPE_MESSAGE 580 msg.TeamId = channel.TeamId 581 msg.ChannelId = channel.Id 582 msg.PostId = post.Id 583 msg.RootId = post.RootId 584 msg.ChannelName = channel.Name 585 msg.SenderId = post.UserId 586 587 if ou, ok := post.Props["override_username"]; ok && ou != nil { 588 msg.OverrideUsername = ou.(string) 589 } 590 591 if oi, ok := post.Props["override_icon_url"]; ok && oi != nil { 592 msg.OverrideIconUrl = oi.(string) 593 } 594 595 if fw, ok := post.Props["from_webhook"]; ok && fw != nil { 596 msg.FromWebhook = fw.(string) 597 } 598 599 if *a.Config().EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION { 600 msg.Category = model.CATEGORY_CAN_REPLY 601 if channel.Type == model.CHANNEL_DIRECT { 602 msg.Message = senderName + ": " + model.ClearMentionTags(post.Message) 603 } else { 604 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(post.Message) 605 } 606 } else if *a.Config().EmailSettings.PushNotificationContents == model.GENERIC_NO_CHANNEL_NOTIFICATION { 607 if channel.Type == model.CHANNEL_DIRECT { 608 msg.Category = model.CATEGORY_CAN_REPLY 609 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") 610 } else if wasMentioned || channel.Type == model.CHANNEL_GROUP { 611 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention_no_channel") 612 } else { 613 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention_no_channel") 614 } 615 } else { 616 if channel.Type == model.CHANNEL_DIRECT { 617 msg.Category = model.CATEGORY_CAN_REPLY 618 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message") 619 } else if wasMentioned || channel.Type == model.CHANNEL_GROUP { 620 msg.Category = model.CATEGORY_CAN_REPLY 621 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName 622 } else { 623 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName 624 } 625 } 626 627 // If the post only has images then push an appropriate message 628 if len(post.Message) == 0 && post.FileIds != nil && len(post.FileIds) > 0 { 629 if channel.Type == model.CHANNEL_DIRECT { 630 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only_dm") 631 } else { 632 msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") + channelName 633 } 634 } 635 636 //l4g.Debug("Sending push notification for user %v with msg of '%v'", user.Id, msg.Message) 637 638 for _, session := range sessions { 639 tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) 640 tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) 641 642 l4g.Debug("Sending push notification to device %v for user %v with msg of '%v'", tmpMessage.DeviceId, user.Id, msg.Message) 643 644 a.Go(func(session *model.Session) func() { 645 return func() { 646 a.sendToPushProxy(tmpMessage, session) 647 } 648 }(session)) 649 650 if a.Metrics != nil { 651 a.Metrics.IncrementPostSentPush() 652 } 653 } 654 655 return nil 656 } 657 658 func (a *App) ClearPushNotification(userId string, channelId string) { 659 a.Go(func() { 660 // Sleep is to allow the read replicas a chance to fully sync 661 // the unread count for sending an accurate count. 662 // Delaying a little doesn't hurt anything and is cheaper than 663 // attempting to read from master. 664 time.Sleep(time.Second * 5) 665 666 sessions, err := a.getMobileAppSessions(userId) 667 if err != nil { 668 l4g.Error(err.Error()) 669 return 670 } 671 672 msg := model.PushNotification{} 673 msg.Type = model.PUSH_TYPE_CLEAR 674 msg.ChannelId = channelId 675 msg.ContentAvailable = 0 676 if badge := <-a.Srv.Store.User().GetUnreadCount(userId); badge.Err != nil { 677 msg.Badge = 0 678 l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err) 679 } else { 680 msg.Badge = int(badge.Data.(int64)) 681 } 682 683 l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId) 684 685 for _, session := range sessions { 686 tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) 687 tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) 688 a.Go(func() { 689 a.sendToPushProxy(tmpMessage, session) 690 }) 691 } 692 }) 693 } 694 695 func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) { 696 msg.ServerId = a.DiagnosticId() 697 698 request, _ := http.NewRequest("POST", *a.Config().EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) 699 700 if resp, err := a.HTTPClient(true).Do(request); err != nil { 701 l4g.Error("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, err.Error()) 702 } else { 703 pushResponse := model.PushResponseFromJson(resp.Body) 704 if resp.Body != nil { 705 consumeAndClose(resp) 706 } 707 708 if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_REMOVE { 709 l4g.Info("Device was reported as removed for UserId=%v SessionId=%v removing push for this session", session.UserId, session.Id) 710 a.AttachDeviceId(session.Id, "", session.ExpiresAt) 711 a.ClearSessionCacheForUser(session.UserId) 712 } 713 714 if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_FAIL { 715 l4g.Error("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, pushResponse[model.PUSH_STATUS_ERROR_MSG]) 716 } 717 } 718 } 719 720 func (a *App) getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) { 721 if result := <-a.Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil { 722 return nil, result.Err 723 } else { 724 return result.Data.([]*model.Session), nil 725 } 726 } 727 728 func (a *App) sendOutOfChannelMentions(sender *model.User, post *model.Post, channelType string, users []*model.User) *model.AppError { 729 if len(users) == 0 { 730 return nil 731 } 732 733 var usernames []string 734 for _, user := range users { 735 usernames = append(usernames, user.Username) 736 } 737 sort.Strings(usernames) 738 739 var userIds []string 740 for _, user := range users { 741 userIds = append(userIds, user.Id) 742 } 743 744 T := utils.GetUserTranslations(sender.Locale) 745 746 var localePhrase string 747 if channelType == model.CHANNEL_OPEN { 748 localePhrase = T("api.post.check_for_out_of_channel_mentions.link.public") 749 } else if channelType == model.CHANNEL_PRIVATE { 750 localePhrase = T("api.post.check_for_out_of_channel_mentions.link.private") 751 } 752 753 ephemeralPostId := model.NewId() 754 var message string 755 if len(users) == 1 { 756 message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{ 757 "Username": usernames[0], 758 "Phrase": localePhrase, 759 }) 760 } else { 761 message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{ 762 "Usernames": strings.Join(usernames[:len(usernames)-1], ", @"), 763 "LastUsername": usernames[len(usernames)-1], 764 "Phrase": localePhrase, 765 }) 766 } 767 768 props := model.StringInterface{ 769 model.PROPS_ADD_CHANNEL_MEMBER: model.StringInterface{ 770 "post_id": ephemeralPostId, 771 "usernames": usernames, 772 "user_ids": userIds, 773 }, 774 } 775 776 a.SendEphemeralPost( 777 post.UserId, 778 &model.Post{ 779 Id: ephemeralPostId, 780 RootId: post.RootId, 781 ChannelId: post.ChannelId, 782 Message: message, 783 CreateAt: post.CreateAt + 1, 784 Props: props, 785 }, 786 ) 787 788 return nil 789 } 790 791 type ExplicitMentions struct { 792 // MentionedUserIds contains a key for each user mentioned by keyword. 793 MentionedUserIds map[string]bool 794 795 // OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have 796 // a corresponding keyword. 797 OtherPotentialMentions []string 798 799 // HereMentioned is true if the message contained @here. 800 HereMentioned bool 801 802 // AllMentioned is true if the message contained @all. 803 AllMentioned bool 804 805 // ChannelMentioned is true if the message contained @channel. 806 ChannelMentioned bool 807 } 808 809 // Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned 810 // users and a slice of potential mention users not in the channel and whether or not @here was mentioned. 811 func GetExplicitMentions(message string, keywords map[string][]string) *ExplicitMentions { 812 ret := &ExplicitMentions{ 813 MentionedUserIds: make(map[string]bool), 814 } 815 systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true} 816 817 addMentionedUsers := func(ids []string) { 818 for _, id := range ids { 819 ret.MentionedUserIds[id] = true 820 } 821 } 822 823 processText := func(text string) { 824 for _, word := range strings.FieldsFunc(text, func(c rune) bool { 825 // Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern 826 return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c)) 827 }) { 828 isMention := false 829 830 // skip word with format ':word:' with an assumption that it is an emoji format only 831 if word[0] == ':' && word[len(word)-1] == ':' { 832 continue 833 } 834 835 if word == "@here" { 836 ret.HereMentioned = true 837 } 838 839 if word == "@channel" { 840 ret.ChannelMentioned = true 841 } 842 843 if word == "@all" { 844 ret.AllMentioned = true 845 } 846 847 // Non-case-sensitive check for regular keys 848 if ids, match := keywords[strings.ToLower(word)]; match { 849 addMentionedUsers(ids) 850 isMention = true 851 } 852 853 // Case-sensitive check for first name 854 if ids, match := keywords[word]; match { 855 addMentionedUsers(ids) 856 isMention = true 857 } 858 859 if isMention { 860 continue 861 } 862 863 if strings.ContainsAny(word, ".-:") { 864 // This word contains a character that may be the end of a sentence, so split further 865 splitWords := strings.FieldsFunc(word, func(c rune) bool { 866 return c == '.' || c == '-' || c == ':' 867 }) 868 869 for _, splitWord := range splitWords { 870 if splitWord == "@here" { 871 ret.HereMentioned = true 872 } 873 874 if splitWord == "@all" { 875 ret.AllMentioned = true 876 } 877 878 if splitWord == "@channel" { 879 ret.ChannelMentioned = true 880 } 881 882 // Non-case-sensitive check for regular keys 883 if ids, match := keywords[strings.ToLower(splitWord)]; match { 884 addMentionedUsers(ids) 885 } 886 887 // Case-sensitive check for first name 888 if ids, match := keywords[splitWord]; match { 889 addMentionedUsers(ids) 890 } else if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") { 891 username := splitWord[1:] 892 ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username) 893 } 894 } 895 } 896 897 if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { 898 username := word[1:] 899 ret.OtherPotentialMentions = append(ret.OtherPotentialMentions, username) 900 } 901 } 902 } 903 904 buf := "" 905 markdown.Inspect(message, func(node interface{}) bool { 906 text, ok := node.(*markdown.Text) 907 if !ok { 908 processText(buf) 909 buf = "" 910 return true 911 } 912 buf += text.Text 913 return false 914 }) 915 processText(buf) 916 917 return ret 918 } 919 920 // Given a map of user IDs to profiles, returns a list of mention 921 // keywords for all users in the channel. 922 func (a *App) GetMentionKeywordsInChannel(profiles map[string]*model.User, lookForSpecialMentions bool) map[string][]string { 923 keywords := make(map[string][]string) 924 925 for id, profile := range profiles { 926 userMention := "@" + strings.ToLower(profile.Username) 927 keywords[userMention] = append(keywords[userMention], id) 928 929 if len(profile.NotifyProps["mention_keys"]) > 0 { 930 // Add all the user's mention keys 931 splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",") 932 for _, k := range splitKeys { 933 // note that these are made lower case so that we can do a case insensitive check for them 934 key := strings.ToLower(k) 935 keywords[key] = append(keywords[key], id) 936 } 937 } 938 939 // If turned on, add the user's case sensitive first name 940 if profile.NotifyProps["first_name"] == "true" { 941 keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id) 942 } 943 944 // Add @channel and @all to keywords if user has them turned on 945 if lookForSpecialMentions { 946 if int64(len(profiles)) <= *a.Config().TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" { 947 keywords["@channel"] = append(keywords["@channel"], profile.Id) 948 keywords["@all"] = append(keywords["@all"], profile.Id) 949 950 status := GetStatusFromCache(profile.Id) 951 if status != nil && status.Status == model.STATUS_ONLINE { 952 keywords["@here"] = append(keywords["@here"], profile.Id) 953 } 954 } 955 } 956 } 957 958 return keywords 959 } 960 961 func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool { 962 return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) && 963 DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId) 964 } 965 966 func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool { 967 userNotifyProps := user.NotifyProps 968 userNotify := userNotifyProps[model.PUSH_NOTIFY_PROP] 969 channelNotify, ok := channelNotifyProps[model.PUSH_NOTIFY_PROP] 970 971 if post.IsSystemMessage() { 972 return false 973 } 974 975 if channelNotify == model.USER_NOTIFY_NONE { 976 return false 977 } 978 979 if channelNotify == model.CHANNEL_NOTIFY_MENTION && !wasMentioned { 980 return false 981 } 982 983 if userNotify == model.USER_NOTIFY_MENTION && (!ok || channelNotify == model.CHANNEL_NOTIFY_DEFAULT) && !wasMentioned { 984 return false 985 } 986 987 if (userNotify == model.USER_NOTIFY_ALL || channelNotify == model.CHANNEL_NOTIFY_ALL) && 988 (post.UserId != user.Id || post.Props["from_webhook"] == "true") { 989 return true 990 } 991 992 if userNotify == model.USER_NOTIFY_NONE && 993 (!ok || channelNotify == model.CHANNEL_NOTIFY_DEFAULT) { 994 return false 995 } 996 997 return true 998 } 999 1000 func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelId string) bool { 1001 // If User status is DND return false right away 1002 if status.Status == model.STATUS_DND { 1003 return false 1004 } 1005 1006 if pushStatus, ok := userNotifyProps["push_status"]; (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) { 1007 return true 1008 } else if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) { 1009 return true 1010 } else if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE { 1011 return true 1012 } 1013 1014 return false 1015 }