Files
im_server/server/push.go
T

312 lines
9.8 KiB
Go

/******************************************************************************
*
* Description:
* Push notifications handling.
*
*****************************************************************************/
package main
import (
"time"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/push"
"github.com/tinode/chat/server/store"
"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 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
}
if online == 0 {
presence, err := store.GetUserPresence(t.tenantID, uid)
if err != nil {
logs.Warn.Printf("topic[%s]: failed to read user presence for push: uid=%s err=%v", t.name, uid.UserId(), err)
} else if presence != nil && presence.Online && presence.SessionCount > 0 {
online = presence.SessionCount
}
}
// 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 != "" {
select {
case globals.usersUpdate <- local:
default:
}
}
}