395 lines
12 KiB
Go
395 lines
12 KiB
Go
/******************************************************************************
|
|
*
|
|
* 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)
|
|
}
|
|
}
|