github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/app/notification_push.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package app 5 6 import ( 7 "hash/fnv" 8 "net/http" 9 "strings" 10 11 "github.com/pkg/errors" 12 13 "github.com/mattermost/go-i18n/i18n" 14 "github.com/vnforks/kid/v5/mlog" 15 "github.com/vnforks/kid/v5/model" 16 "github.com/vnforks/kid/v5/utils" 17 ) 18 19 type notificationType string 20 21 const ( 22 notificationTypeClear notificationType = "clear" 23 notificationTypeMessage notificationType = "message" 24 notificationTypeUpdateBadge notificationType = "update_badge" 25 ) 26 27 const PUSH_NOTIFICATION_HUB_WORKERS = 1000 28 const PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER = 50 29 30 type PushNotificationsHub struct { 31 Classes []chan PushNotification 32 } 33 34 type PushNotification struct { 35 notificationType notificationType 36 currentSessionId string 37 userId string 38 classId string 39 post *model.Post 40 user *model.User 41 class *model.Class 42 senderName string 43 className string 44 explicitMention bool 45 classWideMention bool 46 replyToThreadType string 47 } 48 49 func (hub *PushNotificationsHub) GetGoClassFromUserId(userId string) chan PushNotification { 50 h := fnv.New32a() 51 h.Write([]byte(userId)) 52 chanIdx := h.Sum32() % PUSH_NOTIFICATION_HUB_WORKERS 53 return hub.Classes[chanIdx] 54 } 55 56 func (a *App) sendPushNotificationSync(post *model.Post, user *model.User, class *model.Class, className string, senderName string, 57 explicitMention bool, classWideMention bool, replyToThreadType string) *model.AppError { 58 cfg := a.Config() 59 msg, err := a.BuildPushNotificationMessage( 60 *cfg.EmailSettings.PushNotificationContents, 61 post, 62 user, 63 class, 64 className, 65 senderName, 66 explicitMention, 67 classWideMention, 68 replyToThreadType, 69 ) 70 if err != nil { 71 return err 72 } 73 74 return a.sendPushNotificationToAllSessions(msg, user.Id, "") 75 } 76 77 func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, userId string, skipSessionId string) *model.AppError { 78 sessions, err := a.getMobileAppSessions(userId) 79 if err != nil { 80 return err 81 } 82 83 if msg == nil { 84 return model.NewAppError( 85 "pushNotification", 86 "api.push_notifications.message.parse.app_error", 87 nil, 88 "", 89 http.StatusBadRequest, 90 ) 91 } 92 93 notification, parseError := model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) 94 if parseError != nil { 95 return model.NewAppError( 96 "pushNotification", 97 "api.push_notifications.message.parse.app_error", 98 nil, 99 parseError.Error(), 100 http.StatusInternalServerError, 101 ) 102 } 103 104 for _, session := range sessions { 105 // Don't send notifications to this session if it's expired or we want to skip it 106 if session.IsExpired() || (skipSessionId != "" && skipSessionId == session.Id) { 107 continue 108 } 109 110 // We made a copy to avoid decoding and parsing all the time 111 tmpMessage := notification 112 tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) 113 tmpMessage.AckId = model.NewId() 114 115 err := a.sendToPushProxy(*tmpMessage, session) 116 if err != nil { 117 a.NotificationsLog().Error("Notification error", 118 mlog.String("ackId", tmpMessage.AckId), 119 mlog.String("type", tmpMessage.Type), 120 mlog.String("userId", session.UserId), 121 mlog.String("postId", tmpMessage.PostId), 122 mlog.String("classId", tmpMessage.ClassId), 123 mlog.String("deviceId", tmpMessage.DeviceId), 124 mlog.String("status", err.Error()), 125 ) 126 127 continue 128 } 129 130 a.NotificationsLog().Info("Notification sent", 131 mlog.String("ackId", tmpMessage.AckId), 132 mlog.String("type", tmpMessage.Type), 133 mlog.String("userId", session.UserId), 134 mlog.String("postId", tmpMessage.PostId), 135 mlog.String("classId", tmpMessage.ClassId), 136 mlog.String("deviceId", tmpMessage.DeviceId), 137 mlog.String("status", model.PUSH_SEND_SUCCESS), 138 ) 139 140 if a.Metrics() != nil { 141 a.Metrics().IncrementPostSentPush() 142 } 143 } 144 145 return nil 146 } 147 148 func (a *App) sendPushNotification(notification *PostNotification, user *model.User, explicitMention, classWideMention bool, replyToThreadType string) { 149 cfg := a.Config() 150 class := notification.Class 151 post := notification.Post 152 153 nameFormat := a.GetNotificationNameFormat(user) 154 155 className := notification.GetClassName(nameFormat, user.Id) 156 senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride) 157 158 c := a.Srv().PushNotificationsHub.GetGoClassFromUserId(user.Id) 159 c <- PushNotification{ 160 notificationType: notificationTypeMessage, 161 post: post, 162 user: user, 163 class: class, 164 senderName: senderName, 165 className: className, 166 explicitMention: explicitMention, 167 classWideMention: classWideMention, 168 replyToThreadType: replyToThreadType, 169 } 170 } 171 172 func (a *App) getPushNotificationMessage(contentsConfig, postMessage string, explicitMention, classWideMention, hasFiles bool, 173 senderName, className, replyToThreadType string, userLocale i18n.TranslateFunc) string { 174 175 // If the post only has images then push an appropriate message 176 if len(postMessage) == 0 && hasFiles { 177 return senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") 178 } 179 180 if contentsConfig == model.FULL_NOTIFICATION { 181 return senderName + ": " + model.ClearMentionTags(postMessage) 182 } 183 184 if classWideMention { 185 return senderName + userLocale("api.post.send_notification_and_forget.push_class_mention") 186 } 187 188 if explicitMention { 189 return senderName + userLocale("api.post.send_notifications_and_forget.push_explicit_mention") 190 } 191 192 if replyToThreadType == model.COMMENTS_NOTIFY_ROOT { 193 return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_post") 194 } 195 196 if replyToThreadType == model.COMMENTS_NOTIFY_ANY { 197 return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_thread") 198 } 199 200 return senderName + userLocale("api.post.send_notifications_and_forget.push_general_message") 201 } 202 203 func (a *App) clearPushNotificationSync(currentSessionId, userId, classId string) *model.AppError { 204 msg := &model.PushNotification{ 205 Type: model.PUSH_TYPE_CLEAR, 206 Version: model.PUSH_MESSAGE_V2, 207 ClassId: classId, 208 ContentAvailable: 1, 209 } 210 211 unreadCount, err := a.Srv().Store.User().GetUnreadCount(userId) 212 if err != nil { 213 return err 214 } 215 216 msg.Badge = int(unreadCount) 217 218 return a.sendPushNotificationToAllSessions(msg, userId, currentSessionId) 219 } 220 221 func (a *App) clearPushNotification(currentSessionId, userId, classId string) { 222 class := a.Srv().PushNotificationsHub.GetGoClassFromUserId(userId) 223 class <- PushNotification{ 224 notificationType: notificationTypeClear, 225 currentSessionId: currentSessionId, 226 userId: userId, 227 classId: classId, 228 } 229 } 230 231 func (a *App) updateMobileAppBadgeSync(userId string) *model.AppError { 232 msg := &model.PushNotification{ 233 Type: model.PUSH_TYPE_UPDATE_BADGE, 234 Version: model.PUSH_MESSAGE_V2, 235 Sound: "none", 236 ContentAvailable: 1, 237 } 238 239 unreadCount, err := a.Srv().Store.User().GetUnreadCount(userId) 240 if err != nil { 241 return err 242 } 243 244 msg.Badge = int(unreadCount) 245 246 return a.sendPushNotificationToAllSessions(msg, userId, "") 247 } 248 249 func (a *App) UpdateMobileAppBadge(userId string) { 250 class := a.Srv().PushNotificationsHub.GetGoClassFromUserId(userId) 251 class <- PushNotification{ 252 notificationType: notificationTypeUpdateBadge, 253 userId: userId, 254 } 255 } 256 257 func (a *App) createPushNotificationsHub() { 258 hub := PushNotificationsHub{ 259 Classes: []chan PushNotification{}, 260 } 261 for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ { 262 hub.Classes = append(hub.Classes, make(chan PushNotification, PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER)) 263 } 264 a.Srv().PushNotificationsHub = hub 265 } 266 267 func (a *App) pushNotificationWorker(notifications chan PushNotification) { 268 for notification := range notifications { 269 var err *model.AppError 270 switch notification.notificationType { 271 case notificationTypeClear: 272 err = a.clearPushNotificationSync(notification.currentSessionId, notification.userId, notification.classId) 273 case notificationTypeMessage: 274 err = a.sendPushNotificationSync( 275 notification.post, 276 notification.user, 277 notification.class, 278 notification.className, 279 notification.senderName, 280 notification.explicitMention, 281 notification.classWideMention, 282 notification.replyToThreadType, 283 ) 284 case notificationTypeUpdateBadge: 285 err = a.updateMobileAppBadgeSync(notification.userId) 286 default: 287 mlog.Error("Invalid notification type", mlog.String("notification_type", string(notification.notificationType))) 288 } 289 290 if err != nil { 291 mlog.Error("Unable to send push notification", mlog.String("notification_type", string(notification.notificationType)), mlog.Err(err)) 292 } 293 } 294 } 295 296 func (a *App) StartPushNotificationsHubWorkers() { 297 for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ { 298 class := a.Srv().PushNotificationsHub.Classes[x] 299 a.Srv().Go(func() { a.pushNotificationWorker(class) }) 300 } 301 } 302 303 func (a *App) StopPushNotificationsHubWorkers() { 304 for _, class := range a.Srv().PushNotificationsHub.Classes { 305 close(class) 306 } 307 } 308 309 func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) error { 310 msg.ServerId = a.DiagnosticId() 311 312 a.NotificationsLog().Info("Notification will be sent", 313 mlog.String("ackId", msg.AckId), 314 mlog.String("type", msg.Type), 315 mlog.String("userId", session.UserId), 316 mlog.String("postId", msg.PostId), 317 mlog.String("status", model.PUSH_SEND_PREPARE), 318 ) 319 320 url := strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/") + model.API_URL_SUFFIX_V1 + "/send_push" 321 request, err := http.NewRequest("POST", url, strings.NewReader(msg.ToJson())) 322 if err != nil { 323 return err 324 } 325 326 resp, err := a.Srv().pushNotificationClient.Do(request) 327 if err != nil { 328 return err 329 } 330 defer resp.Body.Close() 331 332 pushResponse := model.PushResponseFromJson(resp.Body) 333 334 switch pushResponse[model.PUSH_STATUS] { 335 case model.PUSH_STATUS_REMOVE: 336 a.AttachDeviceId(session.Id, "", session.ExpiresAt) 337 a.ClearSessionCacheForUser(session.UserId) 338 return errors.New("Device was reported as removed") 339 case model.PUSH_STATUS_FAIL: 340 return errors.New(pushResponse[model.PUSH_STATUS_ERROR_MSG]) 341 } 342 return nil 343 } 344 345 func (a *App) SendAckToPushProxy(ack *model.PushNotificationAck) error { 346 if ack == nil { 347 return nil 348 } 349 350 a.NotificationsLog().Info("Notification received", 351 mlog.String("ackId", ack.Id), 352 mlog.String("type", ack.NotificationType), 353 mlog.String("deviceType", ack.ClientPlatform), 354 mlog.Int64("receivedAt", ack.ClientReceivedAt), 355 mlog.String("status", model.PUSH_RECEIVED), 356 ) 357 358 request, err := http.NewRequest( 359 "POST", 360 strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.API_URL_SUFFIX_V1+"/ack", 361 strings.NewReader(ack.ToJson()), 362 ) 363 364 if err != nil { 365 return err 366 } 367 368 resp, err := a.HTTPService().MakeClient(true).Do(request) 369 if err != nil { 370 return err 371 } 372 373 resp.Body.Close() 374 return nil 375 376 } 377 378 func (a *App) getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) { 379 return a.Srv().Store.Session().GetSessionsWithActiveDeviceIds(userId) 380 } 381 382 func ShouldSendPushNotification(user *model.User, classNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool { 383 return DoesNotifyPropsAllowPushNotification(user, classNotifyProps, post, wasMentioned) && 384 DoesStatusAllowPushNotification(user.NotifyProps, status, post.ClassId) 385 } 386 387 func DoesNotifyPropsAllowPushNotification(user *model.User, classNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool { 388 userNotifyProps := user.NotifyProps 389 userNotify := userNotifyProps[model.PUSH_NOTIFY_PROP] 390 classNotify, ok := classNotifyProps[model.PUSH_NOTIFY_PROP] 391 if !ok || classNotify == "" { 392 classNotify = model.CLASS_NOTIFY_DEFAULT 393 } 394 395 // If the class is muted do not send push notifications 396 if classNotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CLASS_MARK_UNREAD_MENTION { 397 return false 398 } 399 400 if post.IsSystemMessage() { 401 return false 402 } 403 404 if classNotify == model.USER_NOTIFY_NONE { 405 return false 406 } 407 408 if classNotify == model.CLASS_NOTIFY_MENTION && !wasMentioned { 409 return false 410 } 411 412 if userNotify == model.USER_NOTIFY_MENTION && classNotify == model.CLASS_NOTIFY_DEFAULT && !wasMentioned { 413 return false 414 } 415 416 if (userNotify == model.USER_NOTIFY_ALL || classNotify == model.CLASS_NOTIFY_ALL) && 417 (post.UserId != user.Id || post.GetProp("from_webhook") == "true") { 418 return true 419 } 420 421 if userNotify == model.USER_NOTIFY_NONE && 422 classNotify == model.CLASS_NOTIFY_DEFAULT { 423 return false 424 } 425 426 return true 427 } 428 429 func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, classId string) bool { 430 // If User status is DND or OOO return false right away 431 if status.Status == model.STATUS_DND || status.Status == model.STATUS_OUT_OF_OFFICE { 432 return false 433 } 434 435 pushStatus, ok := userNotifyProps[model.PUSH_STATUS_NOTIFY_PROP] 436 if (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveClass != classId || model.GetMillis()-status.LastActivityAt > model.STATUS_CLASS_TIMEOUT) { 437 return true 438 } 439 440 if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) { 441 return true 442 } 443 444 if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE { 445 return true 446 } 447 448 return false 449 } 450 451 func (a *App) BuildPushNotificationMessage(contentsConfig string, post *model.Post, user *model.User, class *model.Class, className string, senderName string, 452 explicitMention bool, classWideMention bool, replyToThreadType string) (*model.PushNotification, *model.AppError) { 453 454 var msg *model.PushNotification 455 456 notificationInterface := a.Srv().Notification 457 if (notificationInterface == nil || notificationInterface.CheckLicense() != nil) && contentsConfig == model.ID_LOADED_NOTIFICATION { 458 contentsConfig = model.GENERIC_NOTIFICATION 459 } 460 461 if contentsConfig == model.ID_LOADED_NOTIFICATION { 462 msg = a.buildIdLoadedPushNotificationMessage(post, user) 463 } else { 464 msg = a.buildFullPushNotificationMessage(contentsConfig, post, user, class, className, senderName, explicitMention, classWideMention, replyToThreadType) 465 } 466 467 unreadCount, err := a.Srv().Store.User().GetUnreadCount(user.Id) 468 if err != nil { 469 return nil, err 470 } 471 msg.Badge = int(unreadCount) 472 473 return msg, nil 474 } 475 476 func (a *App) buildIdLoadedPushNotificationMessage(post *model.Post, user *model.User) *model.PushNotification { 477 userLocale := utils.GetUserTranslations(user.Locale) 478 msg := &model.PushNotification{ 479 PostId: post.Id, 480 ClassId: post.ClassId, 481 Category: model.CATEGORY_CAN_REPLY, 482 Version: model.PUSH_MESSAGE_V2, 483 Type: model.PUSH_TYPE_MESSAGE, 484 IsIdLoaded: true, 485 SenderId: user.Id, 486 Message: userLocale("api.push_notification.id_loaded.default_message"), 487 } 488 489 return msg 490 } 491 492 func (a *App) buildFullPushNotificationMessage(contentsConfig string, post *model.Post, user *model.User, class *model.Class, className string, senderName string, 493 explicitMention bool, classWideMention bool, replyToThreadType string) *model.PushNotification { 494 495 msg := &model.PushNotification{ 496 Category: model.CATEGORY_CAN_REPLY, 497 Version: model.PUSH_MESSAGE_V2, 498 Type: model.PUSH_TYPE_MESSAGE, 499 BranchId: class.BranchId, 500 ClassId: class.Id, 501 PostId: post.Id, 502 SenderId: post.UserId, 503 IsIdLoaded: false, 504 } 505 506 cfg := a.Config() 507 if contentsConfig != model.GENERIC_NO_CLASS_NOTIFICATION { 508 msg.ClassName = className 509 } 510 511 msg.SenderName = senderName 512 if ou, ok := post.GetProp("override_username").(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride { 513 msg.OverrideUsername = ou 514 msg.SenderName = ou 515 } 516 517 if oi, ok := post.GetProp("override_icon_url").(string); ok && *cfg.ServiceSettings.EnablePostIconOverride { 518 msg.OverrideIconUrl = oi 519 } 520 521 if fw, ok := post.GetProp("from_webhook").(string); ok { 522 msg.FromWebhook = fw 523 } 524 525 userLocale := utils.GetUserTranslations(user.Locale) 526 hasFiles := post.FileIds != nil && len(post.FileIds) > 0 527 528 msg.Message = a.getPushNotificationMessage(contentsConfig, post.Message, explicitMention, classWideMention, hasFiles, msg.SenderName, className, replyToThreadType, userLocale) 529 530 return msg 531 }