Files
im_server/server/hub.go
T

1018 lines
32 KiB
Go

/******************************************************************************
*
* Description :
*
* Main hub for processing events such as creating/tearing down topics,
* routing messages between topics.
*
*****************************************************************************/
package main
import (
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/logs"
immetrics "github.com/tinode/chat/server/metrics"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
// RequestLatencyDistribution is an array of request latency distribution bounds (in milliseconds).
// "var" because Go does not support array constants.
var requestLatencyDistribution = []float64{1, 2, 3, 4, 5, 6, 8, 10, 13, 16, 20, 25, 30, 40, 50, 65, 80, 100, 130,
160, 200, 250, 300, 400, 500, 650, 800, 1000, 2000, 5000, 10000, 20000, 50000, 100000}
// OutgoingMessageSizeDistribution is an array of outgoing message size distribution bounds (in bytes).
var outgoingMessageSizeDistribution = []float64{1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 16384,
65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824, 4294967296}
const (
defaultTopicClientQueue = 4096
defaultTopicServerQueue = 512
defaultTopicPublishWait = 200 * time.Millisecond
topicClientQueueEnv = "TOPIC_CLIENT_QUEUE_BUFFER"
topicServerQueueEnv = "TOPIC_SERVER_QUEUE_BUFFER"
topicPublishEnqueueWaitEnv = "TOPIC_PUBLISH_ENQUEUE_WAIT_MS"
)
var topicQueueConfig = loadTopicQueueConfig()
type topicQueueRuntimeConfig struct {
clientBuffer int
serverBuffer int
publishEnqueueWait time.Duration
}
func loadTopicQueueConfig() topicQueueRuntimeConfig {
return topicQueueRuntimeConfig{
clientBuffer: intEnvOrDefault(topicClientQueueEnv, defaultTopicClientQueue),
serverBuffer: intEnvOrDefault(topicServerQueueEnv, defaultTopicServerQueue),
publishEnqueueWait: time.Duration(intEnvOrDefault(topicPublishEnqueueWaitEnv, int(defaultTopicPublishWait/time.Millisecond))) * time.Millisecond,
}
}
func intEnvOrDefault(name string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := strconv.Atoi(raw)
if err != nil || parsed <= 0 {
logs.Warn.Printf("invalid %s=%q, using %d", name, raw, fallback)
return fallback
}
return parsed
}
// Request to hub to remove the topic
type topicUnreg struct {
// Tenant which owns the topic.
tenantID types.TenantID
// Original request, could be nil.
pkt *ClientComMessage
// Session making the request, could be nil.
sess *Session
// Routable name of the topic to drop. Duplicated here because pkt could be nil.
rcptTo string
// UID of the user being deleted. Duplicated here because pkt could be nil.
forUser types.Uid
// Unregister then delete the topic.
del bool
// Channel for reporting operation completion when deleting topics for a user.
done chan<- bool
}
type userStatusReq struct {
// Tenant which owns the user.
tenantID types.TenantID
// UID of the user being affected.
forUser types.Uid
// New topic state value. Only types.StateSuspended is supported at this time.
state types.ObjState
}
type deferredPresenceKey struct {
tenantID types.TenantID
topic string
source string
}
// Hub is the core structure which holds topics.
type Hub struct {
// Topics must be indexed by name
topics *sync.Map
// Current number of loaded topics
numTopics int
// Channel for routing client-side messages, buffered at 4096
routeCli chan *ClientComMessage
// Process get.info requests for topic not subscribed to, buffered 128.
meta chan *ClientComMessage
// Channel for routing server-generated messages, buffered at 4096
routeSrv chan *ServerComMessage
// Presence updates which could not immediately enter a topic queue.
deferredPresence chan *ServerComMessage
deferredPresenceStop chan struct{}
// subscribe session to topic, possibly creating a new topic, buffered at 256
join chan *ClientComMessage
// Remove topic from hub, possibly deleting it afterwards, buffered at 32
unreg chan *topicUnreg
// Channel for suspending/resuming users, buffered 128.
userStatus chan *userStatusReq
// Cluster request to rehash topics, unbuffered
rehash chan bool
// Request to shutdown, unbuffered
shutdown chan chan<- bool
}
func (h *Hub) topicGet(key types.TopicKey) *Topic {
if !key.IsValid() {
return nil
}
if t, ok := h.topics.Load(key); ok {
return t.(*Topic)
}
return nil
}
func (h *Hub) topicPut(key types.TopicKey, t *Topic) {
if !key.IsValid() || t == nil || t.tenantID != key.TenantID || t.name != key.Topic {
panic("invalid tenant topic key")
}
h.numTopics++
h.topics.Store(key, t)
}
func (h *Hub) topicDel(key types.TopicKey) {
if !key.IsValid() {
return
}
h.numTopics--
h.topics.Delete(key)
}
func newHub() *Hub {
h := &Hub{
topics: &sync.Map{},
// TODO: verify if these channels have to be buffered.
routeCli: make(chan *ClientComMessage, 4096),
routeSrv: make(chan *ServerComMessage, 4096),
deferredPresence: make(chan *ServerComMessage, 8192),
deferredPresenceStop: make(chan struct{}),
join: make(chan *ClientComMessage, 256),
unreg: make(chan *topicUnreg, 256),
rehash: make(chan bool),
meta: make(chan *ClientComMessage, 128),
userStatus: make(chan *userStatusReq, 128),
shutdown: make(chan chan<- bool),
}
statsRegisterInt("LiveTopics")
statsRegisterInt("TotalTopics")
statsRegisterInt("IncomingMessagesWebsockTotal")
statsRegisterInt("OutgoingMessagesWebsockTotal")
statsRegisterInt("IncomingMessagesLongpollTotal")
statsRegisterInt("OutgoingMessagesLongpollTotal")
statsRegisterInt("IncomingMessagesGrpcTotal")
statsRegisterInt("OutgoingMessagesGrpcTotal")
statsRegisterInt("FileDownloadsTotal")
statsRegisterInt("FileUploadsTotal")
statsRegisterInt("CtrlCodesTotal2xx")
statsRegisterInt("CtrlCodesTotal3xx")
statsRegisterInt("CtrlCodesTotal4xx")
statsRegisterInt("CtrlCodesTotal5xx")
statsRegisterHistogram("RequestLatency", requestLatencyDistribution)
statsRegisterHistogram("OutgoingMessageSize", outgoingMessageSizeDistribution)
go h.runDeferredPresence()
go h.run()
return h
}
func (h *Hub) deferPresence(msg *ServerComMessage) bool {
select {
case h.deferredPresence <- msg:
immetrics.ObserveInternalQueueEnqueue("topic_presence_deferred", len(h.deferredPresence), cap(h.deferredPresence), true)
return true
default:
immetrics.ObserveInternalQueueEnqueue("topic_presence_deferred", len(h.deferredPresence), cap(h.deferredPresence), false)
return false
}
}
func (h *Hub) runDeferredPresence() {
const maxPending = 16384
ticker := time.NewTicker(5 * time.Millisecond)
defer ticker.Stop()
pending := make(map[deferredPresenceKey]*ServerComMessage)
for {
select {
case msg := <-h.deferredPresence:
if msg == nil || msg.Pres == nil {
continue
}
key := deferredPresenceKey{tenantID: msg.TenantID, topic: msg.RcptTo, source: msg.Pres.Src}
if _, exists := pending[key]; exists || len(pending) < maxPending {
pending[key] = msg
} else {
immetrics.ObserveInternalQueueEnqueue("topic_presence_pending", len(pending), maxPending, false)
logs.Err.Printf("hub: deferred presence queue full topic=%s tenant=%d source=%s",
msg.RcptTo, msg.TenantID, msg.Pres.Src)
}
immetrics.SetInternalQueueCurrent("topic_presence_pending", len(pending))
case <-ticker.C:
budget := 64
for key, msg := range pending {
dst := h.topicGet(types.TopicKey{TenantID: key.tenantID, Topic: key.topic})
if dst == nil || dst.serverMsg == nil {
delete(pending, key)
continue
}
select {
case dst.serverMsg <- msg:
immetrics.ObserveInternalQueueEnqueue("topic_server", len(dst.serverMsg), cap(dst.serverMsg), true)
delete(pending, key)
budget--
default:
}
if budget == 0 {
break
}
}
immetrics.SetInternalQueueCurrent("topic_presence_pending", len(pending))
case <-h.deferredPresenceStop:
return
}
}
}
func (h *Hub) run() {
for {
select {
case join := <-h.join:
if !join.TenantID.IsValid() {
logs.Warn.Println("hub.join: missing tenant context", join.RcptTo)
if join.sess != nil {
join.sess.queueOut(ErrMalformedReply(join, join.Timestamp))
}
continue
}
// Handle a subscription request:
// 1. Init topic
// 1.1 If a new topic is requested, create it
// 1.2 If a new subscription to an existing topic is requested:
// 1.2.1 check if topic is already loaded
// 1.2.2 if not, load it
// 1.2.3 if it cannot be loaded (not found), fail
// 2. Check access rights and reject, if appropriate
// 3. Attach session to the topic
// Is the topic already loaded?
key := types.TopicKey{TenantID: join.TenantID, Topic: join.RcptTo}
t := h.topicGet(key)
if t == nil {
// Topic does not exist or not loaded.
t = &Topic{
tenantID: join.TenantID,
name: join.RcptTo,
xoriginal: join.Original,
// Indicates a proxy topic.
isProxy: globals.cluster.isRemoteTopic(key),
sessions: make(map[*Session]perSessionData),
clientMsg: make(chan *ClientComMessage, topicQueueConfig.clientBuffer),
serverMsg: make(chan *ServerComMessage, topicQueueConfig.serverBuffer),
reg: make(chan *ClientComMessage, 256),
unreg: make(chan *ClientComMessage, 256),
meta: make(chan *ClientComMessage, 64),
perUser: make(map[types.Uid]perUserData),
exit: make(chan *shutDown, 1),
}
if globals.cluster != nil {
if t.isProxy {
t.proxy = make(chan *ClusterResp, 128)
t.masterNode = globals.cluster.ring.Get(key.RoutingKey())
} else {
// It's a master topic. Make a channel for handling
// direct messages from the proxy.
t.master = make(chan *ClusterSessUpdate, 8)
}
}
// Topic is created in suspended state because it's not yet configured.
t.markPaused(true)
// Save topic now to prevent race condition.
h.topicPut(key, t)
// Configure the topic.
go topicInit(t, join, h)
} else {
// Topic found.
if t.isInactive() {
// Topic is either not ready or being deleted.
if join.sess.inflightReqs != nil {
join.sess.inflightReqs.Done()
}
join.sess.queueOut(ErrLockedReply(join, join.Timestamp))
continue
}
// Topic will check access rights and send appropriate {ctrl}
select {
case t.reg <- join:
default:
if join.sess.inflightReqs != nil {
join.sess.inflightReqs.Done()
}
join.sess.queueOut(ErrServiceUnavailableReply(join, join.Timestamp))
logs.Err.Println("hub.join loop: topic's reg queue full", join.RcptTo, join.sess.sid,
" - total queue len:", len(t.reg))
}
}
case msg := <-h.routeCli:
// This is a message from a session not subscribed to topic
// Route incoming message to topic if topic permits such routing.
key := types.TopicKey{TenantID: msg.TenantID, Topic: msg.RcptTo}
if dst := h.topicGet(key); dst != nil {
// Everything is OK, sending packet to known topic
if dst.clientMsg != nil {
msg.queuedAt = time.Now()
select {
case dst.clientMsg <- msg:
immetrics.ObserveInternalQueueEnqueue("topic_client", len(dst.clientMsg), cap(dst.clientMsg), true)
default:
immetrics.ObserveInternalQueueEnqueue("topic_client", len(dst.clientMsg), cap(dst.clientMsg), false)
if msg.sess != nil {
msg.sess.queueOut(ErrServiceUnavailableReply(msg, types.TimeNow()))
}
logs.Err.Printf("hub: topic's broadcast queue is full topic=%s tenant=%d len=%d cap=%d",
dst.name, dst.tenantID, len(dst.clientMsg), cap(dst.clientMsg))
}
} else {
logs.Warn.Println("hub: invalid topic category for broadcast", dst.name)
}
} else if msg.Note == nil {
// Topic is unknown or offline.
// Note is silently ignored. Other client messages cannot be accepted here
// because they are not routed to a live topic and therefore cannot reserve a
// message seq or be persisted.
// TODO(gene): validate topic name, discarding invalid topics.
logs.Info.Printf("Hub. Topic[%s] is unknown or offline", msg.RcptTo)
if msg.sess != nil {
msg.sess.queueOut(ErrAttachFirst(msg, types.TimeNow()))
}
}
case msg := <-h.routeSrv:
// This is a server message from a connection not subscribed to topic
// Route incoming message to topic if topic permits such routing.
key := types.TopicKey{TenantID: msg.TenantID, Topic: msg.RcptTo}
if dst := h.topicGet(key); dst != nil {
// Everything is OK, sending packet to known topic
select {
case dst.serverMsg <- msg:
immetrics.ObserveInternalQueueEnqueue("topic_server", len(dst.serverMsg), cap(dst.serverMsg), true)
default:
if msg.Pres == nil || !h.deferPresence(msg) {
immetrics.ObserveInternalQueueEnqueue("topic_server", len(dst.serverMsg), cap(dst.serverMsg), false)
logs.Err.Println("hub: topic's broadcast queue is full", dst.name)
}
}
} else if (strings.HasPrefix(msg.RcptTo, "usr") || strings.HasPrefix(msg.RcptTo, "grp")) &&
globals.cluster.isRemoteTopic(key) {
// It is a remote topic.
if err := globals.cluster.routeToTopicIntraCluster(msg.RcptTo, msg, msg.sess); err != nil {
logs.Warn.Printf("hub: routing to '%s' failed", msg.RcptTo)
}
}
case msg := <-h.meta:
// Metadata read or update from a user who is not attached to the topic.
if msg.Get != nil {
if msg.MetaWhat == constMsgMetaDesc {
go replyOfflineTopicGetDesc(msg.sess, msg)
} else {
go replyOfflineTopicGetSub(msg.sess, msg)
}
} else if msg.Set != nil {
go replyOfflineTopicSetSub(msg.sess, msg)
}
case status := <-h.userStatus:
// Suspend/activate user's topics.
go h.topicsStateForUser(status.tenantID, status.forUser, status.state == types.StateSuspended)
case unreg := <-h.unreg:
reason := StopNone
if unreg.del {
reason = StopDeleted
}
if unreg.forUser.IsZero() {
// The topic is being garbage collected or deleted.
if err := h.topicUnreg(unreg.tenantID, unreg.sess, unreg.rcptTo, unreg.pkt, reason); err != nil {
logs.Err.Println("hub.topicUnreg failed:", err)
}
} else {
// User is being deleted.
go h.stopTopicsForUser(unreg.tenantID, unreg.forUser, reason, unreg.done)
}
case <-h.rehash:
// Cluster rehashing. Some previously local topics became remote,
// and the other way round.
// Such topics must be shut down at this node.
h.topics.Range(func(_, t any) bool {
topic := t.(*Topic)
// Handle two cases:
// 1. Master topic has moved out to another node.
// 2. Proxy topic is running on a new master node
// (i.e. the master topic has moved to this node).
key := types.TopicKey{TenantID: topic.tenantID, Topic: topic.name}
if topic.isProxy != globals.cluster.isRemoteTopic(key) {
h.topicUnreg(topic.tenantID, nil, topic.name, nil, StopRehashing)
}
return true
})
case hubdone := <-h.shutdown:
// start cleanup process
topicsdone := make(chan bool)
topicCount := 0
h.topics.Range(func(_, topic any) bool {
topic.(*Topic).exit <- &shutDown{done: topicsdone}
topicCount++
return true
})
for range topicCount {
<-topicsdone
}
logs.Info.Printf("Hub shutdown completed with %d topics", topicCount)
if h.deferredPresenceStop != nil {
close(h.deferredPresenceStop)
}
// let the main goroutine know we are done with the cleanup
hubdone <- true
return
case <-time.After(idleSessionTimeout):
}
}
}
// Update state of all topics associated with the given user:
// * all p2p topics with the given user
// * group topics where the given user is the owner.
// 'me' and fnd' are ignored here because they are direcly tied to the user object.
func (h *Hub) topicsStateForUser(tenantID types.TenantID, uid types.Uid, suspended bool) {
h.topics.Range(func(name any, t any) bool {
topic := t.(*Topic)
if topic.tenantID != tenantID {
return true
}
if topic.cat == types.TopicCatMe || topic.cat == types.TopicCatFnd {
return true
}
if _, isMember := topic.perUser[uid]; (topic.cat == types.TopicCatP2P && isMember) || topic.owner == uid {
topic.markReadOnly(suspended)
// Don't send "off" notification on suspension. They will be sent when the user is evicted.
}
return true
})
}
// topicUnreg deletes or unregisters the topic:
//
// Cases:
// 1. Topic being deleted
// 1.1 Topic is online
// 1.1.1 If the requester is the owner or if it's the last sub in a p2p topic (p2p may be sent internally when the last user unsubscribes):
// 1.1.1.1 Tell topic to stop accepting requests.
// 1.1.1.2 Hub deletes the topic from database
// 1.1.1.3 Hub unregisters the topic
// 1.1.1.4 Hub informs the origin of success or failure
// 1.1.1.5 Hub forwards request to topic
// 1.1.1.6 Topic evicts all sessions
// 1.1.1.7 Topic exits the run() loop
// 1.1.2 If the requester is not the owner
// 1.1.2.1 Send it to topic to be treated like {leave unsub=true}
//
// 1.2 Topic is offline
// 1.2.1 If requester is the owner
// 1.2.1.1 Hub deletes topic from database
// 1.2.2 If not the owner
// 1.2.2.1 Delete subscription from DB
// 1.2.3 Hub informs the origin of success or failure
// 1.2.4 Send notification to subscribers that the topic was deleted
// 2. Topic is just being unregistered (topic is going offline)
// 2.1 Unregister it with no further action
func (h *Hub) topicUnreg(tenantID types.TenantID, sess *Session, topic string, msg *ClientComMessage, reason int) error {
now := types.TimeNow()
key := types.TopicKey{TenantID: tenantID, Topic: topic}
// TODO: when channel is deleted unsubscribe all devices from channel's FCM topic.
if reason == StopDeleted {
var asUid types.Uid
if msg != nil {
asUid = types.ParseUserId(msg.AsUser)
}
// Case 1 (unregister and delete)
if t := h.topicGet(key); t != nil {
// Case 1.1: topic is online
if (!asUid.IsZero() && t.owner == asUid) || (t.cat == types.TopicCatP2P && t.subsCount() < 2) {
// Case 1.1.1: requester is the owner or last sub in a p2p topic
t.markPaused(true)
hard := true
if msg != nil && msg.Del != nil {
// Soft-deleting does not make sense for p2p topics.
hard = msg.Del.Hard || t.cat == types.TopicCatP2P
}
if err := store.Topics.Delete(tenantID, topic, t.isChan, hard); err != nil {
t.markPaused(false)
if sess != nil {
sess.queueOut(ErrUnknownReply(msg, now))
}
return err
}
if sess != nil {
sess.queueOut(NoErrReply(msg, now))
}
if t.isChan {
// Notify channel subscribers that the channel is deleted.
sendPush(pushForChanDelete(t.tenantID, t.name, now))
}
h.topicDel(key)
t.markDeleted()
t.exit <- &shutDown{reason: StopDeleted}
statsInc("LiveTopics", -1)
} else {
// Case 1.1.2: requester is NOT the owner or not empty P2P.
msg.MetaWhat = constMsgDelTopic
msg.sess = sess
t.meta <- msg
}
} else {
// Case 1.2: topic is offline.
// Is user a channel subscriber? Use chnABC instead of grpABC and get only this user's subscription.
var opts *types.QueryOpt
if types.IsChannel(msg.Original) {
topic = msg.Original
opts = &types.QueryOpt{User: asUid}
}
// Get all subscribers of non-channel topics: we need to know how many are left and notify them.
// Get only one subscription for channel users.
subs, err := store.Topics.GetSubs(tenantID, topic, opts)
if err != nil {
sess.queueOut(ErrUnknownReply(msg, now))
return err
}
tcat := topicCat(topic)
if len(subs) == 0 {
if tcat == types.TopicCatP2P {
// No subscribers: delete.
store.Topics.Delete(tenantID, topic, false, true)
}
sess.queueOut(InfoNoActionReply(msg, now))
return nil
}
// Find subscription of the current user.
var sub *types.Subscription
user := asUid.String()
for i := range subs {
if subs[i].User == user {
sub = &subs[i]
break
}
}
if sub == nil {
// If user has no subscription, tell him all is fine
sess.queueOut(InfoNoActionReply(msg, now))
return nil
}
if !(sub.ModeGiven & sub.ModeWant).IsOwner() {
// Case 1.2.2.1 Not the owner, but possibly last subscription in a P2P topic.
if tcat == types.TopicCatP2P && len(subs) < 2 {
// This is a P2P topic and fewer than 2 subscriptions, delete the entire topic
if err := store.Topics.Delete(tenantID, topic, false, msg.Del.Hard); err != nil {
sess.queueOut(ErrUnknownReply(msg, now))
return err
}
// Inform plugin that the topic was deleted.
pluginTopic(&Topic{tenantID: tenantID, name: topic}, plgActDel)
} else if err := store.Subs.Delete(tenantID, topic, asUid); err != nil {
// Not P2P or more than 1 subscription left.
// Delete user's own subscription only
if err == types.ErrNotFound {
sess.queueOut(InfoNoActionReply(msg, now))
err = nil
} else {
sess.queueOut(ErrUnknownReply(msg, now))
}
return err
}
// Notify user's other sessions that the subscription is gone
presSingleUserOfflineOffline(tenantID, asUid, msg.Original, "gone", nilPresParams, sess.sid)
if tcat == types.TopicCatP2P && len(subs) == 2 {
uname1 := asUid.UserId()
uid2 := types.ParseUserId(msg.Original)
// Tell user1 to stop sending updates to user2 without passing change to user1's sessions.
presSingleUserOfflineOffline(tenantID, asUid, uid2.UserId(), "?none+rem", nilPresParams, "")
// Don't change the online status of user1, just ask user2 to stop notification exchange.
// Tell user2 that user1 is offline but let him keep sending updates in case user1 resubscribes.
presSingleUserOfflineOffline(tenantID, uid2, uname1, "off", nilPresParams, "")
}
// Inform plugin that the subscription was deleted.
sub.TenantID = tenantID
pluginSubscription(sub, plgActDel)
} else {
// Case 1.2.1.1: owner, delete the group topic from db. Only group topics have owners.
// We don't know if the group topic is a channel, but cleaning it as a channel does no harm
// other than a small performance penalty.
if err := store.Topics.Delete(tenantID, topic, true, msg.Del.Hard); err != nil {
sess.queueOut(ErrUnknownReply(msg, now))
return err
}
// Notify subscribers that the group topic is gone.
presSubsOfflineOffline(tenantID, topic, tcat, subs, "gone", &presParams{}, sess.sid)
// Notify channel subscribers that the channel is deleted.
// The push will not be delivered to anybody if the topic is not a channel.
sendPush(pushForChanDelete(tenantID, topic, now))
// Inform plugin that the topic was deleted.
pluginTopic(&Topic{tenantID: tenantID, name: topic}, plgActDel)
}
sess.queueOut(NoErrReply(msg, now))
}
} else {
// Case 2: just unregister.
// If t is nil, it's not registered, no action is needed
if t := h.topicGet(key); t != nil {
t.markDeleted()
h.topicDel(key)
t.exit <- &shutDown{reason: reason}
statsInc("LiveTopics", -1)
}
// sess && msg could be nil if the topic is being killed by timer or due to rehashing.
if sess != nil && msg != nil {
sess.queueOut(NoErrReply(msg, now))
}
}
return nil
}
// Terminate all topics associated with the given user:
// * all p2p topics with the given user
// * group topics where the given user is the owner.
// * user's 'me', 'fnd', 'slf' topics.
func (h *Hub) stopTopicsForUser(tenantID types.TenantID, uid types.Uid, reason int, alldone chan<- bool) {
var done chan bool
if alldone != nil {
done = make(chan bool, 128)
}
count := 0
h.topics.Range(func(name any, t any) bool {
topic := t.(*Topic)
if topic.tenantID != tenantID {
return true
}
if _, isMember := topic.perUser[uid]; (topic.cat != types.TopicCatGrp && isMember) ||
topic.owner == uid {
topic.markDeleted()
h.topics.Delete(name)
// This call is non-blocking unless some other routine tries to stop it at the same time.
topic.exit <- &shutDown{reason: reason, done: done}
// Just send to p2p topics here.
if topic.cat == types.TopicCatP2P && len(topic.perUser) == 2 {
presSingleUserOfflineOffline(tenantID, topic.p2pOtherUser(uid), uid.UserId(), "gone", nilPresParams, "")
}
count++
}
return true
})
statsInc("LiveTopics", -count)
if alldone != nil {
for range count {
<-done
}
alldone <- true
}
}
// replyOfflineTopicGetDesc reads a minimal topic Desc from the database.
// The requester may or maynot be subscribed to the topic.
func replyOfflineTopicGetDesc(sess *Session, msg *ClientComMessage) {
now := types.TimeNow()
desc := &MsgTopicDesc{}
asUid := types.ParseUserId(msg.AsUser)
topic := msg.RcptTo
if strings.HasPrefix(topic, "grp") || topic == "sys" {
stopic, err := store.Topics.Get(msg.TenantID, topic)
if err != nil {
logs.Info.Println("replyOfflineTopicGetDesc", err)
sess.queueOut(decodeStoreErrorExplicitTs(err, msg.Id, msg.Original, now, msg.Timestamp, nil))
return
}
if stopic == nil {
sess.queueOut(ErrTopicNotFoundReply(msg, now))
return
}
desc.CreatedAt = &stopic.CreatedAt
desc.UpdatedAt = &stopic.UpdatedAt
desc.Public = stopic.Public
desc.Trusted = stopic.Trusted
desc.IsChan = stopic.UseBt
desc.SubCnt = stopic.SubCnt
if stopic.Owner == msg.AsUser {
desc.DefaultAcs = &MsgDefaultAcsMode{
Auth: stopic.Access.Auth.String(),
Anon: stopic.Access.Anon.String(),
}
}
// Report appropriate access level. Could be overridden below if subscription exists.
desc.Acs = &MsgAccessMode{}
switch sess.authLvl {
case auth.LevelAuth, auth.LevelRoot:
desc.Acs.Mode = stopic.Access.Auth.String()
case auth.LevelAnon:
desc.Acs.Mode = stopic.Access.Anon.String()
}
} else {
// 'me' and p2p topics
uid := types.ZeroUid
if strings.HasPrefix(topic, "usr") {
// User specified as usrXXX
uid = types.ParseUserId(topic)
topic = asUid.P2PName(uid)
} else if strings.HasPrefix(topic, "p2p") {
// User specified as p2pXXXYYY
uid1, uid2, _ := types.ParseP2P(topic)
if uid1 == asUid {
uid = uid2
} else if uid2 == asUid {
uid = uid1
}
}
if uid.IsZero() {
logs.Warn.Println("replyOfflineTopicGetDesc: malformed p2p topic name")
sess.queueOut(ErrMalformedReply(msg, now))
return
}
suser, err := store.Users.Get(msg.TenantID, uid)
if err != nil {
sess.queueOut(decodeStoreErrorExplicitTs(err, msg.Id, msg.Original, now, msg.Timestamp, nil))
return
}
if suser == nil {
sess.queueOut(ErrUserNotFoundReply(msg, now))
return
}
desc.CreatedAt = &suser.CreatedAt
desc.UpdatedAt = &suser.UpdatedAt
desc.Public = suser.Public
desc.Trusted = suser.Trusted
if sess.authLvl == auth.LevelRoot {
desc.State = suser.State.String()
}
// Report appropriate access level. Could be overridden below if subscription exists.
desc.Acs = &MsgAccessMode{}
switch sess.authLvl {
case auth.LevelAuth, auth.LevelRoot:
desc.Acs.Mode = suser.Access.Auth.String()
case auth.LevelAnon:
desc.Acs.Mode = suser.Access.Anon.String()
}
}
sub, err := store.Subs.Get(msg.TenantID, topic, asUid, false)
if err != nil {
logs.Warn.Println("replyOfflineTopicGetDesc:", err)
sess.queueOut(decodeStoreErrorExplicitTs(err, msg.Id, msg.Original, now, msg.Timestamp, nil))
return
}
if sub != nil {
desc.Private = sub.Private
// FIXME: suspended topics should get no AW access.
desc.Acs = &MsgAccessMode{
Want: sub.ModeWant.String(),
Given: sub.ModeGiven.String(),
Mode: (sub.ModeGiven & sub.ModeWant).String(),
}
}
sess.queueOut(&ServerComMessage{
Meta: &MsgServerMeta{
Id: msg.Id, Topic: msg.Original, Timestamp: &now, Desc: desc,
},
})
}
// replyOfflineTopicGetSub reads user's subscription from the database.
// Only own subscription is available.
// The requester must be subscribed but need not be attached.
func replyOfflineTopicGetSub(sess *Session, msg *ClientComMessage) {
now := types.TimeNow()
if msg.Get.Sub != nil && msg.Get.Sub.User != "" && msg.Get.Sub.User != msg.AsUser {
sess.queueOut(ErrPermissionDeniedReply(msg, now))
return
}
topicName := msg.RcptTo
if types.IsChannel(msg.Original) {
topicName = msg.Original
}
ssub, err := store.Subs.Get(msg.TenantID, topicName, types.ParseUserId(msg.AsUser), true)
if err != nil {
logs.Warn.Println("replyOfflineTopicGetSub:", err)
sess.queueOut(decodeStoreErrorExplicitTs(err, msg.Id, msg.Original, now, msg.Timestamp, nil))
return
}
if ssub == nil {
sess.queueOut(ErrNotFoundExplicitTs(msg.Id, msg.Original, now, msg.Timestamp))
return
}
sub := MsgTopicSub{}
if ssub.DeletedAt == nil {
sub.UpdatedAt = &ssub.UpdatedAt
sub.Acs = MsgAccessMode{
Want: ssub.ModeWant.String(),
Given: ssub.ModeGiven.String(),
Mode: (ssub.ModeGiven & ssub.ModeWant).String(),
}
// Fnd is asymmetric: desc.private is a string, but sub.private is a []string.
if types.GetTopicCat(msg.RcptTo) != types.TopicCatFnd {
sub.Private = ssub.Private
}
sub.User = types.ParseUid(ssub.User).UserId()
if (ssub.ModeGiven & ssub.ModeWant).IsReader() && (ssub.ModeWant & ssub.ModeGiven).IsJoiner() {
sub.DelId = ssub.DelId
sub.ReadSeqId = ssub.ReadSeqId
sub.RecvSeqId = ssub.RecvSeqId
}
} else {
sub.DeletedAt = ssub.DeletedAt
}
sess.queueOut(&ServerComMessage{
Meta: &MsgServerMeta{
Id: msg.Id, Topic: msg.Original, Timestamp: &now, Sub: []MsgTopicSub{sub},
},
})
}
// replyOfflineTopicSetSub updates Desc.Private and Sub.Mode when the topic is not loaded in memory.
// Only Private and Mode are updated and only for the requester. The requester must be subscribed to the
// topic but does not need to be attached.
func replyOfflineTopicSetSub(sess *Session, msg *ClientComMessage) {
now := types.TimeNow()
if (msg.Set.Desc == nil || msg.Set.Desc.Private == nil) && (msg.Set.Sub == nil || msg.Set.Sub.Mode == "") {
sess.queueOut(InfoNotModifiedReply(msg, now))
return
}
if msg.Set.Sub != nil && msg.Set.Sub.User != "" && msg.Set.Sub.User != msg.AsUser {
sess.queueOut(ErrPermissionDeniedReply(msg, now))
return
}
asUid := types.ParseUserId(msg.AsUser)
topicName := msg.RcptTo
if types.IsChannel(msg.Original) {
topicName = msg.Original
}
sub, err := store.Subs.Get(msg.TenantID, topicName, asUid, false)
if err != nil {
logs.Warn.Println("replyOfflineTopicSetSub get sub:", err)
sess.queueOut(decodeStoreErrorExplicitTs(err, msg.Id, msg.Original, now, msg.Timestamp, nil))
return
}
if sub == nil {
sess.queueOut(ErrNotFoundExplicitTs(msg.Id, msg.Original, now, msg.Timestamp))
return
}
update := make(map[string]any)
if msg.Set.Desc != nil && msg.Set.Desc.Private != nil {
private, ok := msg.Set.Desc.Private.(map[string]any)
if !ok {
update = map[string]any{"Private": msg.Set.Desc.Private}
} else if private, changed := mergeInterfaces(sub.Private, private); changed {
update = map[string]any{"Private": private}
}
}
if msg.Set.Sub != nil && msg.Set.Sub.Mode != "" {
var modeWant types.AccessMode
if err = modeWant.UnmarshalText([]byte(msg.Set.Sub.Mode)); err != nil {
logs.Warn.Println("replyOfflineTopicSetSub mode:", err)
sess.queueOut(decodeStoreErrorExplicitTs(err, msg.Id, msg.Original, now, msg.Timestamp, nil))
return
}
if modeWant.IsOwner() != sub.ModeWant.IsOwner() {
// No ownership changes here.
sess.queueOut(ErrPermissionDeniedReply(msg, now))
return
}
if types.GetTopicCat(msg.RcptTo) == types.TopicCatP2P {
// For P2P topics ignore requests exceeding typesModeCP2P and do not allow
// removal of 'A' permission.
modeWant = modeWant&globals.typesModeCP2P | types.ModeApprove
}
if modeWant != sub.ModeWant {
update["ModeWant"] = modeWant
// Cache it for later use
sub.ModeWant = modeWant
}
}
if len(update) > 0 {
err = store.Subs.Update(msg.TenantID, topicName, asUid, update)
if err != nil {
logs.Warn.Println("replyOfflineTopicSetSub update:", err)
sess.queueOut(decodeStoreErrorExplicitTs(err, msg.Id, msg.Original, now, msg.Timestamp, nil))
} else {
var params any
if update["ModeWant"] != nil {
params = map[string]any{
"acs": MsgAccessMode{
Given: sub.ModeGiven.String(),
Want: sub.ModeWant.String(),
Mode: (sub.ModeGiven & sub.ModeWant).String(),
},
}
}
sess.queueOut(NoErrParamsReply(msg, now, params))
}
} else {
sess.queueOut(InfoNotModifiedReply(msg, now))
}
}