github.com/xzl8028/xenia-server@v0.0.0-20190809101854-18450a97da63/app/notification_push.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 "hash/fnv" 9 "net/http" 10 "strings" 11 12 "github.com/pkg/errors" 13 14 "github.com/xzl8028/go-i18n/i18n" 15 "github.com/xzl8028/xenia-server/mlog" 16 "github.com/xzl8028/xenia-server/model" 17 "github.com/xzl8028/xenia-server/utils" 18 ) 19 20 type NotificationType string 21 22 const NOTIFICATION_TYPE_CLEAR NotificationType = "clear" 23 const NOTIFICATION_TYPE_MESSAGE NotificationType = "message" 24 25 const PUSH_NOTIFICATION_HUB_WORKERS = 1000 26 const PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER = 50 27 28 type PushNotificationsHub struct { 29 Channels []chan PushNotification 30 } 31 32 type PushNotification struct { 33 id string 34 notificationType NotificationType 35 currentSessionId string 36 userId string 37 channelId string 38 post *model.Post 39 user *model.User 40 channel *model.Channel 41 senderName string 42 channelName string 43 explicitMention bool 44 channelWideMention bool 45 replyToThreadType string 46 } 47 48 func (hub *PushNotificationsHub) GetGoChannelFromUserId(userId string) chan PushNotification { 49 h := fnv.New32a() 50 h.Write([]byte(userId)) 51 chanIdx := h.Sum32() % PUSH_NOTIFICATION_HUB_WORKERS 52 return hub.Channels[chanIdx] 53 } 54 55 func (a *App) sendPushNotificationSync(post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, 56 explicitMention, channelWideMention bool, replyToThreadType string) *model.AppError { 57 cfg := a.Config() 58 59 sessions, err := a.getMobileAppSessions(user.Id) 60 if err != nil { 61 return err 62 } 63 64 msg := model.PushNotification{ 65 Category: model.CATEGORY_CAN_REPLY, 66 Version: model.PUSH_MESSAGE_V2, 67 Type: model.PUSH_TYPE_MESSAGE, 68 TeamId: channel.TeamId, 69 ChannelId: channel.Id, 70 PostId: post.Id, 71 RootId: post.RootId, 72 SenderId: post.UserId, 73 } 74 75 if unreadCount, err := a.Srv.Store.User().GetUnreadCount(user.Id); err != nil { 76 msg.Badge = 1 77 mlog.Error(fmt.Sprint("We could not get the unread message count for the user", user.Id, err), mlog.String("user_id", user.Id)) 78 } else { 79 msg.Badge = int(unreadCount) 80 } 81 82 contentsConfig := *cfg.EmailSettings.PushNotificationContents 83 if contentsConfig != model.GENERIC_NO_CHANNEL_NOTIFICATION || channel.Type == model.CHANNEL_DIRECT { 84 msg.ChannelName = channelName 85 } 86 87 msg.SenderName = senderName 88 if ou, ok := post.Props["override_username"].(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride { 89 msg.OverrideUsername = ou 90 msg.SenderName = ou 91 } 92 93 if oi, ok := post.Props["override_icon_url"].(string); ok && *cfg.ServiceSettings.EnablePostIconOverride { 94 msg.OverrideIconUrl = oi 95 } 96 97 if fw, ok := post.Props["from_webhook"].(string); ok { 98 msg.FromWebhook = fw 99 } 100 101 userLocale := utils.GetUserTranslations(user.Locale) 102 hasFiles := post.FileIds != nil && len(post.FileIds) > 0 103 104 msg.Message = a.getPushNotificationMessage(post.Message, explicitMention, channelWideMention, hasFiles, msg.SenderName, channelName, channel.Type, replyToThreadType, userLocale) 105 106 for _, session := range sessions { 107 108 if session.IsExpired() { 109 continue 110 } 111 112 tmpMessage := model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) 113 tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) 114 tmpMessage.AckId = model.NewId() 115 116 err := a.sendToPushProxy(*tmpMessage, session) 117 if err != nil { 118 a.NotificationsLog.Error("Notification error", 119 mlog.String("ackId", tmpMessage.AckId), 120 mlog.String("type", tmpMessage.Type), 121 mlog.String("userId", session.UserId), 122 mlog.String("postId", tmpMessage.PostId), 123 mlog.String("channelId", tmpMessage.ChannelId), 124 mlog.String("deviceId", tmpMessage.DeviceId), 125 mlog.String("status", err.Error()), 126 ) 127 128 continue 129 } 130 131 a.NotificationsLog.Info("Notification sent", 132 mlog.String("ackId", tmpMessage.AckId), 133 mlog.String("type", tmpMessage.Type), 134 mlog.String("userId", session.UserId), 135 mlog.String("postId", tmpMessage.PostId), 136 mlog.String("channelId", tmpMessage.ChannelId), 137 mlog.String("deviceId", tmpMessage.DeviceId), 138 mlog.String("status", model.PUSH_SEND_SUCCESS), 139 ) 140 141 if a.Metrics != nil { 142 a.Metrics.IncrementPostSentPush() 143 } 144 } 145 146 return nil 147 } 148 149 func (a *App) sendPushNotification(notification *postNotification, user *model.User, explicitMention, channelWideMention bool, replyToThreadType string) { 150 cfg := a.Config() 151 channel := notification.channel 152 post := notification.post 153 154 var nameFormat string 155 if data, err := a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT); err != nil { 156 nameFormat = *a.Config().TeamSettings.TeammateNameDisplay 157 } else { 158 nameFormat = data.Value 159 } 160 161 channelName := notification.GetChannelName(nameFormat, user.Id) 162 senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride) 163 164 c := a.Srv.PushNotificationsHub.GetGoChannelFromUserId(user.Id) 165 c <- PushNotification{ 166 notificationType: NOTIFICATION_TYPE_MESSAGE, 167 post: post, 168 user: user, 169 channel: channel, 170 senderName: senderName, 171 channelName: channelName, 172 explicitMention: explicitMention, 173 channelWideMention: channelWideMention, 174 replyToThreadType: replyToThreadType, 175 } 176 } 177 178 func (a *App) getPushNotificationMessage(postMessage string, explicitMention, channelWideMention, hasFiles bool, 179 senderName, channelName, channelType, replyToThreadType string, userLocale i18n.TranslateFunc) string { 180 181 // If the post only has images then push an appropriate message 182 if len(postMessage) == 0 && hasFiles { 183 if channelType == model.CHANNEL_DIRECT { 184 return strings.Trim(userLocale("api.post.send_notifications_and_forget.push_image_only"), " ") 185 } 186 return senderName + userLocale("api.post.send_notifications_and_forget.push_image_only") 187 } 188 189 contentsConfig := *a.Config().EmailSettings.PushNotificationContents 190 191 if contentsConfig == model.FULL_NOTIFICATION { 192 if channelType == model.CHANNEL_DIRECT { 193 return model.ClearMentionTags(postMessage) 194 } 195 return senderName + ": " + model.ClearMentionTags(postMessage) 196 } 197 198 if channelType == model.CHANNEL_DIRECT { 199 return userLocale("api.post.send_notifications_and_forget.push_message") 200 } 201 202 if channelWideMention { 203 return senderName + userLocale("api.post.send_notification_and_forget.push_channel_mention") 204 } 205 206 if explicitMention { 207 return senderName + userLocale("api.post.send_notifications_and_forget.push_explicit_mention") 208 } 209 210 if replyToThreadType == THREAD_ROOT { 211 return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_post") 212 } 213 214 if replyToThreadType == THREAD_ANY { 215 return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_thread") 216 } 217 218 return senderName + userLocale("api.post.send_notifications_and_forget.push_general_message") 219 } 220 221 func (a *App) ClearPushNotificationSync(currentSessionId, userId, channelId string) { 222 sessions, err := a.getMobileAppSessions(userId) 223 if err != nil { 224 mlog.Error(err.Error()) 225 return 226 } 227 228 msg := model.PushNotification{ 229 Type: model.PUSH_TYPE_CLEAR, 230 Version: model.PUSH_MESSAGE_V2, 231 ChannelId: channelId, 232 ContentAvailable: 1, 233 } 234 235 if unreadCount, err := a.Srv.Store.User().GetUnreadCount(userId); err != nil { 236 msg.Badge = 0 237 mlog.Error(fmt.Sprint("We could not get the unread message count for the user", userId, err), mlog.String("user_id", userId)) 238 } else { 239 msg.Badge = int(unreadCount) 240 } 241 242 for _, session := range sessions { 243 if currentSessionId != session.Id { 244 tmpMessage := model.PushNotificationFromJson(strings.NewReader(msg.ToJson())) 245 tmpMessage.SetDeviceIdAndPlatform(session.DeviceId) 246 tmpMessage.AckId = model.NewId() 247 248 err := a.sendToPushProxy(*tmpMessage, session) 249 if err != nil { 250 a.NotificationsLog.Error("Notification error", 251 mlog.String("ackId", tmpMessage.AckId), 252 mlog.String("type", tmpMessage.Type), 253 mlog.String("userId", session.UserId), 254 mlog.String("postId", tmpMessage.PostId), 255 mlog.String("channelId", tmpMessage.ChannelId), 256 mlog.String("deviceId", tmpMessage.DeviceId), 257 mlog.String("status", err.Error()), 258 ) 259 260 continue 261 } 262 263 a.NotificationsLog.Info("Notification sent", 264 mlog.String("ackId", tmpMessage.AckId), 265 mlog.String("type", tmpMessage.Type), 266 mlog.String("userId", session.UserId), 267 mlog.String("postId", tmpMessage.PostId), 268 mlog.String("channelId", tmpMessage.ChannelId), 269 mlog.String("deviceId", tmpMessage.DeviceId), 270 mlog.String("status", model.PUSH_SEND_SUCCESS), 271 ) 272 273 if a.Metrics != nil { 274 a.Metrics.IncrementPostSentPush() 275 } 276 } 277 } 278 } 279 280 func (a *App) ClearPushNotification(currentSessionId, userId, channelId string) { 281 channel := a.Srv.PushNotificationsHub.GetGoChannelFromUserId(userId) 282 channel <- PushNotification{ 283 notificationType: NOTIFICATION_TYPE_CLEAR, 284 currentSessionId: currentSessionId, 285 userId: userId, 286 channelId: channelId, 287 } 288 } 289 290 func (a *App) CreatePushNotificationsHub() { 291 hub := PushNotificationsHub{ 292 Channels: []chan PushNotification{}, 293 } 294 for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ { 295 hub.Channels = append(hub.Channels, make(chan PushNotification, PUSH_NOTIFICATIONS_HUB_BUFFER_PER_WORKER)) 296 } 297 a.Srv.PushNotificationsHub = hub 298 } 299 300 func (a *App) pushNotificationWorker(notifications chan PushNotification) { 301 for notification := range notifications { 302 switch notification.notificationType { 303 case NOTIFICATION_TYPE_CLEAR: 304 a.ClearPushNotificationSync(notification.currentSessionId, notification.userId, notification.channelId) 305 case NOTIFICATION_TYPE_MESSAGE: 306 a.sendPushNotificationSync( 307 notification.post, 308 notification.user, 309 notification.channel, 310 notification.channelName, 311 notification.senderName, 312 notification.explicitMention, 313 notification.channelWideMention, 314 notification.replyToThreadType, 315 ) 316 default: 317 mlog.Error(fmt.Sprintf("Invalid notification type %v", notification.notificationType)) 318 } 319 } 320 } 321 322 func (a *App) StartPushNotificationsHubWorkers() { 323 for x := 0; x < PUSH_NOTIFICATION_HUB_WORKERS; x++ { 324 channel := a.Srv.PushNotificationsHub.Channels[x] 325 a.Srv.Go(func() { a.pushNotificationWorker(channel) }) 326 } 327 } 328 329 func (a *App) StopPushNotificationsHubWorkers() { 330 for _, channel := range a.Srv.PushNotificationsHub.Channels { 331 close(channel) 332 } 333 } 334 335 func (a *App) sendToPushProxy(msg model.PushNotification, session *model.Session) error { 336 msg.ServerId = a.DiagnosticId() 337 338 a.NotificationsLog.Info("Notification will be sent", 339 mlog.String("ackId", msg.AckId), 340 mlog.String("type", msg.Type), 341 mlog.String("userId", session.UserId), 342 mlog.String("postId", msg.PostId), 343 mlog.String("status", model.PUSH_SEND_PREPARE), 344 ) 345 346 request, err := http.NewRequest("POST", strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) 347 if err != nil { 348 return err 349 } 350 351 resp, err := a.HTTPService.MakeClient(true).Do(request) 352 if err != nil { 353 return err 354 } 355 356 defer resp.Body.Close() 357 358 pushResponse := model.PushResponseFromJson(resp.Body) 359 360 if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_REMOVE { 361 a.AttachDeviceId(session.Id, "", session.ExpiresAt) 362 a.ClearSessionCacheForUser(session.UserId) 363 return errors.New("Device was reported as removed") 364 } 365 366 if pushResponse[model.PUSH_STATUS] == model.PUSH_STATUS_FAIL { 367 return errors.New(pushResponse[model.PUSH_STATUS_ERROR_MSG]) 368 } 369 370 return nil 371 } 372 373 func (a *App) SendAckToPushProxy(ack *model.PushNotificationAck) error { 374 if ack == nil { 375 return nil 376 } 377 378 a.NotificationsLog.Info("Notification received", 379 mlog.String("ackId", ack.Id), 380 mlog.String("type", ack.NotificationType), 381 mlog.String("deviceType", ack.ClientPlatform), 382 mlog.Int64("receivedAt", ack.ClientReceivedAt), 383 mlog.String("status", model.PUSH_RECEIVED), 384 ) 385 386 request, err := http.NewRequest( 387 "POST", 388 strings.TrimRight(*a.Config().EmailSettings.PushNotificationServer, "/")+model.API_URL_SUFFIX_V1+"/ack", 389 strings.NewReader(ack.ToJson()), 390 ) 391 392 if err != nil { 393 return err 394 } 395 396 resp, err := a.HTTPService.MakeClient(true).Do(request) 397 if err != nil { 398 return err 399 } 400 401 resp.Body.Close() 402 return nil 403 404 } 405 406 func (a *App) getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) { 407 return a.Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId) 408 } 409 410 func ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post) bool { 411 return DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned) && 412 DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId) 413 } 414 415 func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned bool) bool { 416 userNotifyProps := user.NotifyProps 417 userNotify := userNotifyProps[model.PUSH_NOTIFY_PROP] 418 channelNotify, _ := channelNotifyProps[model.PUSH_NOTIFY_PROP] 419 if channelNotify == "" { 420 channelNotify = model.CHANNEL_NOTIFY_DEFAULT 421 } 422 423 // If the channel is muted do not send push notifications 424 if channelNotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_MARK_UNREAD_MENTION { 425 return false 426 } 427 428 if post.IsSystemMessage() { 429 return false 430 } 431 432 if channelNotify == model.USER_NOTIFY_NONE { 433 return false 434 } 435 436 if channelNotify == model.CHANNEL_NOTIFY_MENTION && !wasMentioned { 437 return false 438 } 439 440 if userNotify == model.USER_NOTIFY_MENTION && channelNotify == model.CHANNEL_NOTIFY_DEFAULT && !wasMentioned { 441 return false 442 } 443 444 if (userNotify == model.USER_NOTIFY_ALL || channelNotify == model.CHANNEL_NOTIFY_ALL) && 445 (post.UserId != user.Id || post.Props["from_webhook"] == "true") { 446 return true 447 } 448 449 if userNotify == model.USER_NOTIFY_NONE && 450 channelNotify == model.CHANNEL_NOTIFY_DEFAULT { 451 return false 452 } 453 454 return true 455 } 456 457 func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelId string) bool { 458 // If User status is DND or OOO return false right away 459 if status.Status == model.STATUS_DND || status.Status == model.STATUS_OUT_OF_OFFICE { 460 return false 461 } 462 463 pushStatus, ok := userNotifyProps[model.PUSH_STATUS_NOTIFY_PROP] 464 if (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) { 465 return true 466 } 467 468 if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) { 469 return true 470 } 471 472 if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE { 473 return true 474 } 475 476 return false 477 }