/****************************************************************************** * * Description: * Push notifications handling. * *****************************************************************************/ package main import ( "time" "github.com/tinode/chat/server/push" "github.com/tinode/chat/server/store/types" ) // 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 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, // 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 } 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 != "" { select { case globals.usersUpdate <- local: default: } } }