/****************************************************************************** * * Description: * Push notifications handling. * *****************************************************************************/ package main import ( "sync" "sync/atomic" "time" "github.com/tinode/chat/server/logs" immetrics "github.com/tinode/chat/server/metrics" "github.com/tinode/chat/server/push" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/types" ) const ( pushDispatchQueueCapacity = 4096 pushDispatchWorkers = 4 ) type pushDispatchJob struct { rcpt *push.Receipt resolvePresence bool queuedAt time.Time } var asyncPushDispatcher struct { once sync.Once queues [pushDispatchWorkers]chan pushDispatchJob depth atomic.Int64 } func initAsyncPushDispatcher() { asyncPushDispatcher.once.Do(func() { for i := range pushDispatchWorkers { asyncPushDispatcher.queues[i] = make(chan pushDispatchJob, pushDispatchQueueCapacity/pushDispatchWorkers) go func(queue <-chan pushDispatchJob) { for job := range queue { depth := asyncPushDispatcher.depth.Add(-1) immetrics.SetPushDispatchQueueDepth(int(depth)) processPushDispatch(job) } }(asyncPushDispatcher.queues[i]) } }) } func pushDispatchShard(rcpt *push.Receipt) int { hash := uint32(2166136261) for _, value := range []byte(rcpt.Payload.Topic) { hash ^= uint32(value) hash *= 16777619 } hash ^= uint32(rcpt.TenantID) hash *= 16777619 return int(hash % pushDispatchWorkers) } func dispatchPushAsync(rcpt *push.Receipt, resolvePresence bool) bool { if rcpt == nil || globals.usersUpdate == nil { return false } initAsyncPushDispatcher() queue := asyncPushDispatcher.queues[pushDispatchShard(rcpt)] job := pushDispatchJob{rcpt: rcpt, resolvePresence: resolvePresence, queuedAt: time.Now()} depth := asyncPushDispatcher.depth.Add(1) select { case queue <- job: immetrics.ObservePushDispatchEnqueue(int(asyncPushDispatcher.depth.Load()), true) return true default: depth = asyncPushDispatcher.depth.Add(-1) immetrics.ObservePushDispatchEnqueue(int(depth), false) logs.Err.Printf("push dispatch queue full: tenant=%d topic=%s recipients=%d shard_len=%d shard_cap=%d total_depth=%d", rcpt.TenantID, rcpt.Payload.Topic, len(rcpt.To), len(queue), cap(queue), depth) return false } } func processPushDispatch(job pushDispatchJob) { started := time.Now() var dispatchErr error if job.resolvePresence { unresolved := make([]types.Uid, 0, len(job.rcpt.To)) for uid, recipient := range job.rcpt.To { if recipient.Delivered == 0 { unresolved = append(unresolved, uid) } } presences, err := store.GetUsersPresence(job.rcpt.TenantID, unresolved) if err != nil { dispatchErr = err logs.Warn.Printf("push dispatch: failed to read presence topic=%s recipients=%d err=%v", job.rcpt.Payload.Topic, len(unresolved), err) } for uid, presence := range presences { if presence != nil && presence.Online && presence.SessionCount > 0 { recipient := job.rcpt.To[uid] recipient.Delivered = presence.SessionCount job.rcpt.To[uid] = recipient } } } sendPush(job.rcpt) immetrics.ObservePushDispatch(job.queuedAt, len(job.rcpt.To), started, dispatchErr) } // Subscribe or unsubscribe user to/from FCM topic (channel). func (t *Topic) channelSubUnsub(uid types.Uid, sub bool) { push.ChannelSub(&push.ChannelReq{ TenantID: t.tenantID, Uid: uid, Channel: types.GrpToChn(t.name), Unsub: !sub, }) } // Prepares a payload to be delivered to a mobile device as a push notification in response to a {data} message. func (t *Topic) pushForData(fromUid types.Uid, data *MsgServerData, msgMarkedAsReadBySender bool) *push.Receipt { // Passing `Topic` as `t.name` for group topics and P2P topics. The p2p topic name is later rewritten for // each recipient then the payload is created: p2p recipient sees the topic as the ID of the other user. // Initialize the push receipt. contentType, _ := data.Head["mime"].(string) receipt := push.Receipt{ TenantID: t.tenantID, To: make(map[types.Uid]push.Recipient, t.subsCount()), Payload: push.Payload{ What: push.ActMsg, Silent: false, Topic: t.name, From: data.From, Timestamp: data.Timestamp, SeqId: data.SeqId, ContentType: contentType, Content: data.Content, }, } if webrtc, found := data.Head["webrtc"].(string); found { receipt.Payload.Webrtc = webrtc audioOnly, _ := data.Head["aonly"].(bool) receipt.Payload.AudioOnly = audioOnly if webrtc == constCallMsgStarted { timeout := imCallTimeoutSeconds() if timeout <= 0 { timeout = defaultCallEstablishmentTimeout } expiresAt := data.Timestamp.Add(time.Duration(timeout) * time.Second) receipt.Payload.ExpiresAt = &expiresAt } } if replace, found := data.Head["replace"].(string); found { receipt.Payload.Replace = replace } if t.isChan { // Channel readers should get a push on a channel name (as an FCM topic push). receipt.Channel = types.GrpToChn(t.name) } for uid, pud := range t.perUser { online := pud.online if uid == fromUid && online == 0 { // Make sure the sender's devices receive a silent push. online = 1 } // Send only to those who have notifications enabled. mode := pud.modeWant & pud.modeGiven if mode.IsPresencer() && mode.IsReader() && !pud.deleted && !pud.isChan { receipt.To[uid] = push.Recipient{ // Number of attached sessions the data message will be delivered to. // Push notifications sent to users with non-zero online sessions will be marked silent. Delivered: online, Silent: t.cat == types.TopicCatGrp && boolFromAny(groupPrivateMap(pud.private)["mute_notifications"]), // Unread counts are incremented for all recipients, // and for sender only if the message wasnt't marked 'read' by the sender ShouldIncrementUnreadCountInCache: uid != fromUid || !msgMarkedAsReadBySender, } } } if len(receipt.To) > 0 || receipt.Channel != "" { return &receipt } // If there are no recipient there is no need to send the push notification. return nil } // sendPushForStoredGroupMessage handles server-generated group messages which // bypass Topic.handlePubBroadcast but must still update per-user unread counts // and notify offline devices. func sendPushForStoredGroupMessage(tenantID types.TenantID, msg *types.Message) { if msg == nil || globals.usersUpdate == nil || types.GetTopicCat(msg.Topic) != types.TopicCatGrp || store.Topics == nil { return } subs, err := store.Topics.GetSubs(tenantID, msg.Topic, nil) if err != nil { logs.Warn.Printf("group system push: load recipients topic=%s: %v", msg.Topic, err) return } receipt := &push.Receipt{ TenantID: tenantID, To: make(map[types.Uid]push.Recipient, len(subs)), Payload: push.Payload{ What: push.ActMsg, Topic: msg.Topic, Timestamp: msg.CreatedAt, SeqId: msg.SeqId, Content: msg.Content, }, } if mime, ok := msg.Head["mime"].(string); ok { receipt.Payload.ContentType = mime } for _, sub := range subs { uid := types.ParseUid(sub.User) mode := sub.ModeWant & sub.ModeGiven if uid.IsZero() || !mode.IsPresencer() || !mode.IsReader() { continue } delivered := 0 if globals.sessionStore != nil { globals.sessionStore.Range(func(_ string, sess *Session) bool { if sess != nil && !sess.isCluster() && !sess.isProxy() && sess.tenantID == tenantID && sess.uid == uid { delivered++ } return true }) } if presence, presenceErr := store.GetUserPresence(tenantID, uid); presenceErr != nil { logs.Warn.Printf("group system push: read presence topic=%s uid=%s: %v", msg.Topic, uid.UserId(), presenceErr) } else if presence != nil && presence.Online && presence.SessionCount > delivered { delivered = presence.SessionCount } receipt.To[uid] = push.Recipient{ Delivered: delivered, Silent: boolFromAny(groupPrivateMap(sub.Private)["mute_notifications"]), ShouldIncrementUnreadCountInCache: true, } } if len(receipt.To) > 0 { sendPush(receipt) } } func (t *Topic) preparePushForSubReceipt(fromUid types.Uid, now time.Time) *push.Receipt { // P2P push payloads use the canonical p2p... topic. usr... is accepted only // as an input alias. topic := t.xoriginal if t.cat == types.TopicCatP2P { topic = t.name } // Initialize the push receipt. receipt := &push.Receipt{ TenantID: t.tenantID, To: make(map[types.Uid]push.Recipient, t.subsCount()), Payload: push.Payload{ What: push.ActSub, Silent: false, Topic: topic, From: fromUid.UserId(), Timestamp: now, SeqId: t.lastID, }, } return receipt } // Prepares payload to be delivered to a mobile device as a push notification in response to a new subscription in a p2p topic. func (t *Topic) pushForP2PSub(fromUid, toUid types.Uid, want, given types.AccessMode, now time.Time) *push.Receipt { receipt := t.preparePushForSubReceipt(fromUid, now) receipt.Payload.ModeWant = want receipt.Payload.ModeGiven = given receipt.To[toUid] = push.Recipient{} return receipt } // Prepares payload to be delivered to a mobile device as a push notification in response to a new subscription in a group topic. func (t *Topic) pushForGroupSub(fromUid types.Uid, now time.Time) *push.Receipt { receipt := t.preparePushForSubReceipt(fromUid, now) if pud, ok := t.perUser[fromUid]; ok { receipt.Payload.ModeWant = pud.modeWant receipt.Payload.ModeGiven = pud.modeGiven } else { // Sender is not a subscriber (BUG?) return nil } for uid, pud := range t.perUser { // Send only to those who have notifications enabled. mode := pud.modeWant & pud.modeGiven if mode.IsPresencer() && mode.IsReader() && !pud.deleted && !pud.isChan { receipt.To[uid] = push.Recipient{} } } if len(receipt.To) > 0 || receipt.Channel != "" { return receipt } return nil } // Prepares payload to be delivered to a mobile device as a push notification in response to owner deleting a channel. func pushForChanDelete(tenantID types.TenantID, topicName string, now time.Time) *push.Receipt { topicName = types.GrpToChn(topicName) // Initialize the push receipt. return &push.Receipt{ TenantID: tenantID, Payload: push.Payload{ What: push.ActSub, Silent: true, Topic: topicName, Timestamp: now, ModeWant: types.ModeNone, ModeGiven: types.ModeNone, }, Channel: topicName, } } // Prepares payload to be delivered to a mobile device as a push notification in response to receiving "read" notification. func (t *Topic) pushForReadRcpt(uid types.Uid, seq int64, now time.Time) *push.Receipt { // P2P push payloads use the canonical p2p... topic. usr... is accepted only // as an input alias. topic := t.xoriginal if t.cat == types.TopicCatP2P { topic = t.name } // Initialize the push receipt. receipt := &push.Receipt{ TenantID: t.tenantID, To: make(map[types.Uid]push.Recipient, 1), Payload: push.Payload{ What: push.ActRead, Silent: true, Topic: topic, From: uid.UserId(), Timestamp: now, SeqId: seq, }, } receipt.To[uid] = push.Recipient{} return receipt } // Process push notification. func sendPush(rcpt *push.Receipt) { if rcpt == nil || globals.usersUpdate == nil { return } var local *UserCacheReq // In case of a cluster pushes will be initiated at the nodes which own the users. // Sort users into local and remote. if globals.cluster != nil { local = &UserCacheReq{TenantID: rcpt.TenantID, PushRcpt: &push.Receipt{ TenantID: rcpt.TenantID, Payload: rcpt.Payload, Channel: rcpt.Channel, To: make(map[types.Uid]push.Recipient), }} remote := &UserCacheReq{TenantID: rcpt.TenantID, PushRcpt: &push.Receipt{ TenantID: rcpt.TenantID, Payload: rcpt.Payload, Channel: rcpt.Channel, To: make(map[types.Uid]push.Recipient), }} for uid, recipient := range rcpt.To { if globals.cluster.isRemoteTopic(types.TopicKey{TenantID: rcpt.TenantID, Topic: uid.UserId()}) { remote.PushRcpt.To[uid] = recipient } else { local.PushRcpt.To[uid] = recipient } } if len(remote.PushRcpt.To) > 0 || remote.PushRcpt.Channel != "" { globals.cluster.routeUserReq(remote) } } else { local = &UserCacheReq{TenantID: rcpt.TenantID, PushRcpt: rcpt} } if len(local.PushRcpt.To) > 0 || local.PushRcpt.Channel != "" { enqueueUserUpdate(local, "push", false) } }