package main import ( "encoding/json" "strings" "github.com/tinode/chat/server/logs" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/types" ) // presParams defines parameters for creating a presence notification. type presParams struct { userAgent string seqID int64 delID int delSeq []MsgRange // Uid who performed the action actor string // Subject of the action target string dWant string dGiven string } type presFilters struct { // Send messages only to users with this access mode being non-zero. filterIn types.AccessMode // Exclude users with this access mode being non-zero. filterOut types.AccessMode // Send messages to the sessions of this single user defined by ID as a string 'usrABC'. singleUser string // Do not send messages to sessions of this user defined by ID as a string 'usrABC'. excludeUser string } func (p *presParams) packAcs() *MsgAccessMode { if p.dWant != "" || p.dGiven != "" { return &MsgAccessMode{Want: p.dWant, Given: p.dGiven} } return nil } // Presence: Add another user to the list of contacts to notify of presence and other changes func (t *Topic) addToPerSubs(topic string, online, enabled bool) { if topic == t.name { // No need to push updates to self return } // TODO: maybe skip loading channel subscriptions. They can's send or receive these notifications anyway. // Keep the canonical P2P topic as the key. usr... is accepted only as a // client input alias, while downlink frames should consistently use p2p.... t.perSubs[topic] = perSubsData{online: online, enabled: enabled} } // loadContacts loads topic.perSubs to support presence notifications. // perSubs contains (a) topics that the user wants to notify of his presence and // (b) those which want to receive notifications from this user. func (t *Topic) loadContacts(uid types.Uid) error { subs, err := store.Users.GetSubs(t.tenantID, uid) if err != nil { return err } for i := range subs { t.addToPerSubs(subs[i].Topic, false, (subs[i].ModeGiven & subs[i].ModeWant).IsPresencer()) } return nil } // This topic got a request from a 'me' topic to start/stop sending presence updates. // The originating topic reports its own status in 'what' as "on", "off", "gone" or "?unkn". // // "on" - requester came online // "off" - requester is offline now // "?none" - anchor for "+" command: requester status is unknown, won't generate a response // and isn't forwarded to clients. // "gone" - topic deleted or otherwise gone - equivalent of "off+remove" // "?unkn" - requester wants to initiate online status exchange but it's own status is unknown yet. This // notifications is not forwarded to users. // // "+" commands: // "+en": enable subscription, i.e. start accepting incoming notifications from the user2; // "+rem": terminate and remove the subscription (subscription deleted) // "+dis" disable subscription withot removing it, the opposite of "en". // The "+en/rem/dis" command itself is stripped from the notification. func (t *Topic) procPresReq(fromUserID, what string, wantReply bool) string { if t.isInactive() { return "" } if t.isProxy { // Passthrough on proxy: there is no point in maintaining peer status // at the proxy, it's an exact replica of the master. return what } var reqReply, onlineUpdate bool online := &onlineUpdate replyAs := "on" parts := strings.Split(what, "+") what = parts[0] cmd := "" if len(parts) > 1 { cmd = parts[1] } switch what { case "on": // online *online = true case "off": // offline case "?none": // no change to online status online = nil what = "" case "gone": // offline: off+rem cmd = "rem" case "?unkn": // no change in online status online = nil reqReply = true what = "" default: // All other notifications are not processed here return what } if t.cat == types.TopicCatMe { // Find if the contact is listed. if psd, ok := t.perSubs[fromUserID]; ok { if cmd == "rem" { replyAs = "off+rem" if !psd.enabled && what == "off" { // If it was disabled before, don't send a redundant update. what = "" } delete(t.perSubs, fromUserID) } else { switch cmd { case "": // No change in being enabled or disabled and not being added or removed. if !psd.enabled || online == nil || psd.online == *online { // Not enabled or no change in online status - remove unnecessary notification. what = "" } case "en": if !psd.enabled { psd.enabled = true } else if online == nil || psd.online == *online { // Was active and no change or online before: skip unnecessary update. what = "" } case "dis": if psd.enabled { psd.enabled = false if !psd.online { what = "" } } else { // Was disabled and consequently offline before, still offline - skip the update. what = "" } default: panic("presProcReq: unknown command '" + cmd + "'") } if !psd.enabled { // If we don't care about updates, keep the other user off psd.online = false } else if online != nil { psd.online = *online } t.perSubs[fromUserID] = psd } } else if cmd != "rem" { // Got request from a new topic. This must be a new subscription. Record it. // If it's unknown, recording it as offline. t.addToPerSubs(fromUserID, onlineUpdate, cmd == "en") if cmd != "en" { // If the connection is not enabled, ignore the update. what = "" } } else { // Not in list and asked to be removed from the list - ignore what = "" } } // If requester's online status has not changed, do not reply, otherwise an endless loop will happen. // wantReply is needed to ensure unnecessary {pres} is not sent: // A[online, B:off] to B[online, A:off]: {pres A on} // B[online, A:on] to A[online, B:off]: {pres B on} // A[online, B:on] to B[online, A:on]: {pres A on} <<-- unnecessary, that's why wantReply is needed if (onlineUpdate || reqReply) && wantReply { globals.hub.routeSrv <- &ServerComMessage{ TenantID: t.tenantID, // Topic is 'me' even for group topics; group topics will use 'me' as a signal to drop the message // without forwarding to sessions Pres: &MsgServerPres{ Topic: "me", What: replyAs, Src: t.name, WantReply: reqReply, }, RcptTo: fromUserID, } } return what } // Get user-specific topic name for notifying users of interest, or skip the notification. func notifyOnOrSkip(topic, what string, online bool) string { // Don't send notifications on channels if types.IsChannel(topic) { return "" } // P2P contacts are notified on 'me', group topics are notified on proper topic name. notifyOn := "me" if what == "upd" || what == "ua" { if !online { // Skip "upd" and "ua" notifications if the contact is offline. return "" } if types.GetTopicCat(topic) == types.TopicCatGrp { notifyOn = topic } } return notifyOn } // Publish user's update to his/her subscriptions: p2p on their 'me' topic, group topics on the topic. // Case A: user came online, "on", ua // Case B: user went offline, "off", ua // Case C: user agent change, "ua", ua // Case D: User updated 'public', "upd" func (t *Topic) presUsersOfInterest(what, ua string) { parts := strings.Split(what, "+") wantReply := parts[0] == "on" goOffline := len(parts) > 1 && parts[1] == "dis" // Push update to subscriptions for topic, psd := range t.perSubs { notifyOn := notifyOnOrSkip(topic, what, psd.online) if notifyOn == "" { continue } globals.hub.routeSrv <- &ServerComMessage{ TenantID: t.tenantID, Pres: &MsgServerPres{ Topic: notifyOn, What: what, Src: t.name, UserAgent: ua, WantReply: wantReply, }, RcptTo: topic, } if psd.online && goOffline { psd.online = false t.perSubs[topic] = psd } } } // Publish user's update to his/her users of interest on their 'me' topic while user's 'me' topic is offline // Case A: user is being deleted, "gone". // Case B: user's last session just disconnected, "off". func presUsersOfInterestOffline(tenantID types.TenantID, uid types.Uid, subs []types.Subscription, what string) { // Push update to subscriptions for i := range subs { notifyOn := notifyOnOrSkip(subs[i].Topic, what, true) if notifyOn == "" { continue } if types.GetTopicCat(subs[i].Topic) == types.TopicCatP2P { // A presence request routed to the p2p topic would be silently // dropped: the topic is loaded under its canonical p2p name while // the request carries Topic="me" (notifyOnOrSkip), so the // xoriginal check in handlePresence rejects it. Deliver to the // contact's 'me' topic directly instead, the same way // presSingleUserOffline does. uid1, uid2, err := types.ParseP2P(subs[i].Topic) if err != nil { continue } contact := uid1 if contact == uid { contact = uid2 } if contact == uid || contact.IsZero() { continue } routeUserServerMessage(contact, &ServerComMessage{ TenantID: tenantID, Pres: &MsgServerPres{ Topic: notifyOn, What: what, Src: subs[i].Topic, WantReply: false, }, RcptTo: contact.UserId(), }) continue } globals.hub.routeSrv <- &ServerComMessage{ TenantID: tenantID, Pres: &MsgServerPres{ Topic: notifyOn, What: what, Src: uid.UserId(), WantReply: false, }, RcptTo: subs[i].Topic, } } } // Report change to topic subscribers online, group or p2p // // Case I: User joined the topic, "on" // Case J: User left topic, "off" // Case K.2: User altered WANT (and maybe got default Given), "acs" // Case L.1: Admin altered GIVEN, "acs" to affected user // Case L.3: Admin altered GIVEN (and maybe got assigned default WANT), "acs" to admins // Case M: Topic unaccessible (cluster failure), "left" to everyone currently online // Case V.2: Messages soft deleted, "del" to one user only // Case W.2: Messages hard-deleted, "del" func (t *Topic) presSubsOnline(what, src string, params *presParams, filter *presFilters, skipSid string) { // If affected user is the same as the user making the change, clear 'who' actor := params.actor target := params.target if actor == src { actor = "" } if target == src { target = "" } globals.hub.routeSrv <- &ServerComMessage{ TenantID: t.tenantID, Pres: &MsgServerPres{ Topic: t.xoriginal, What: what, Src: src, Acs: params.packAcs(), AcsActor: actor, AcsTarget: target, SeqId: params.seqID, DelId: params.delID, DelSeq: params.delSeq, FilterIn: int(filter.filterIn), FilterOut: int(filter.filterOut), SingleUser: filter.singleUser, ExcludeUser: filter.excludeUser, }, RcptTo: t.name, SkipSid: skipSid, } } // userIsPresencer returns true if the user (specified by `uid`) may receive presence notifications. func (t *Topic) userIsPresencer(uid types.Uid) bool { var want, given types.AccessMode if uid.IsZero() { // For zero uids (typically for proxy sessions), return the union of all permissions. want = t.modeWantUnion given = t.modeGivenUnion } else { pud := t.perUser[uid] if pud.deleted { return false } want = pud.modeWant given = pud.modeGiven } return (want & given).IsPresencer() } // Send notification to attached sessions directly, without routing though topic. // This is needed because the session(s) may be already disconnected by the time it's routed through topic. func (t *Topic) presSubsOnlineDirect(what string, params *presParams, filter *presFilters, skipSid string) { msg := &ServerComMessage{ TenantID: t.tenantID, Pres: &MsgServerPres{ Topic: t.xoriginal, What: what, Acs: params.packAcs(), SeqId: params.seqID, DelId: params.delID, DelSeq: params.delSeq, }, } for s, pssd := range t.sessions { if !s.isMultiplex() { if skipSid == s.sid { continue } pud := t.perUser[pssd.uid] // Check presence filters if pud.deleted || !presOfflineFilter(pud.modeGiven&pud.modeWant, what, filter) { continue } if filter != nil { if filter.singleUser != "" && filter.singleUser != pssd.uid.UserId() { continue } if filter.excludeUser != "" && filter.excludeUser == pssd.uid.UserId() { continue } } // For p2p topics topic name is dependent on receiver. // It's OK to change the pointer here because the message will be serialized in queueOut // before being placed into the channel. t.prepareBroadcastableMessage(msg, pssd.uid, pssd.isChanSub) } s.queueOut(msg) } } // Communicates "topic unaccessible (cluster rehashing or node connection lost)" event // to a list of topics promting the client to resubscribe to the topics. func (s *Session) presTermDirect(subs []string) { msg := &ServerComMessage{ TenantID: s.tenantID, Pres: &MsgServerPres{Topic: "me", What: "term"}, } for _, topic := range subs { msg.Pres.Src = topic s.queueOut(msg) } } // Publish to topic subscribers's sessions currently offline in the topic, on their 'me' // Group and P2P. // Case E: topic came online, "on" // Case F: topic went offline, "off" // Case G: topic updated 'public', "upd", who // Case H: topic deleted, "gone" // Case K.3: user altered WANT, "acs" to admins // Case L.4: Admin altered GIVEN, "acs" to admins // Case T: message sent, "msg" to all with 'R' // Case W.1: messages hard-deleted, "del" to all with 'R' func (t *Topic) presSubsOffline(what string, params *presParams, filterSource *presFilters, filterTarget *presFilters, skipSid string, offlineOnly bool) { var skipTopic string if offlineOnly { skipTopic = t.name } for uid, pud := range t.perUser { if pud.deleted || !presOfflineFilter(pud.modeGiven&pud.modeWant, what, filterSource) { continue } user := uid.UserId() actor := params.actor target := params.target if actor == user { actor = "" } if target == user { target = "" } msg := &ServerComMessage{ TenantID: t.tenantID, Pres: &MsgServerPres{ Topic: "me", What: what, Src: t.original(uid), Acs: params.packAcs(), AcsActor: actor, AcsTarget: target, SeqId: params.seqID, DelId: params.delID, FilterIn: int(filterTarget.filterIn), FilterOut: int(filterTarget.filterOut), SingleUser: filterTarget.singleUser, ExcludeUser: filterTarget.excludeUser, SkipTopic: skipTopic, }, RcptTo: user, SkipSid: skipSid, } routeUserServerMessage(uid, msg) } } // Publish {info what=read|recv|kp} to topic subscribers's sessions currently offline in the topic, // on subscriber's 'me'. Group and P2P. func (t *Topic) infoSubsOffline(from types.Uid, what string, seq int64, skipSid string) { user := from.UserId() for uid, pud := range t.perUser { mode := pud.modeGiven & pud.modeWant if pud.deleted || !mode.IsPresencer() || !mode.IsReader() { continue } msg := &ServerComMessage{ TenantID: t.tenantID, Info: &MsgServerInfo{ Topic: "me", Src: t.original(uid), From: user, What: what, SeqId: seq, SkipTopic: t.name, }, RcptTo: uid.UserId(), SkipSid: skipSid, } routeUserServerMessage(uid, msg) } } // Publish {info what=call} to topic subscribers's sessions on subscriber's 'me'. func (t *Topic) infoCallSubsOffline(from string, target types.Uid, event string, seq int64, sdp json.RawMessage, skipSid string, offlineOnly bool) { if target.IsZero() { logs.Err.Printf("callSubs could not find target: topic %s - from %s", t.name, from) return } pud := t.perUser[target] mode := pud.modeGiven & pud.modeWant if pud.deleted || !mode.IsPresencer() || !mode.IsReader() { return } msg := &ServerComMessage{ TenantID: t.tenantID, Info: &MsgServerInfo{ Topic: "me", Src: t.original(target), From: from, What: "call", Event: event, SeqId: seq, Payload: sdp, }, RcptTo: target.UserId(), SkipSid: skipSid, } if offlineOnly { msg.Info.SkipTopic = t.name } routeUserServerMessage(target, msg) } // Same as presSubsOffline, but the topic has not been loaded/initialized first: offline topic, offline subscribers func presSubsOfflineOffline(tenantID types.TenantID, topic string, cat types.TopicCat, subs []types.Subscription, what string, params *presParams, skipSid string) { original := topic for i := range subs { sub := &subs[i] // Let "acs" and "gone" through regardless of 'P'. Don't check for deleted subscriptions: // they are not passed here. if !presOfflineFilter(sub.ModeWant&sub.ModeGiven, what, nil) { continue } user := types.ParseUid(sub.User).UserId() actor := params.actor target := params.target if actor == user { actor = "" } if target == user { target = "" } globals.hub.routeSrv <- &ServerComMessage{ TenantID: tenantID, Pres: &MsgServerPres{ Topic: "me", What: what, Src: original, Acs: params.packAcs(), AcsActor: actor, AcsTarget: target, SeqId: params.seqID, DelId: params.delID, }, RcptTo: user, SkipSid: skipSid, } } } // Announce to a single user on 'me' topic // // Case K.1: User altered WANT (includes new subscription, deleted subscription) // Case L.2: Sharer altered GIVEN (inludes invite, eviction) // Case U: read/recv notification // Case V.1: messages soft-deleted func (t *Topic) presSingleUserOffline(uid types.Uid, mode types.AccessMode, what string, params *presParams, skipSid string, offlineOnly bool) { var skipTopic string if offlineOnly { skipTopic = t.name } // ModeInvalid means the user is deleted (pud.deleted == true) if mode != types.ModeInvalid && presOfflineFilter(mode, what, nil) { user := uid.UserId() actor := params.actor target := params.target if actor == user { actor = "" } if target == user { target = "" } msg := &ServerComMessage{ TenantID: t.tenantID, Pres: &MsgServerPres{ Topic: "me", What: what, Src: t.original(uid), SeqId: params.seqID, DelId: params.delID, Acs: params.packAcs(), AcsActor: actor, AcsTarget: target, UserAgent: params.userAgent, WantReply: strings.HasPrefix(what, "?unkn"), SkipTopic: skipTopic, }, RcptTo: user, SkipSid: skipSid, } routeUserServerMessage(uid, msg) } } // Announce to a single user on 'me' topic. The originating topic is not used (not loaded or user // already unsubscribed). func presSingleUserOfflineOffline(tenantID types.TenantID, uid types.Uid, original, what string, params *presParams, skipSid string) { user := uid.UserId() actor := params.actor target := params.target if actor == user { actor = "" } if target == user { target = "" } msg := &ServerComMessage{ TenantID: tenantID, Pres: &MsgServerPres{ Topic: "me", What: what, Src: original, SeqId: params.seqID, DelId: params.delID, Acs: params.packAcs(), AcsActor: actor, AcsTarget: target, }, RcptTo: uid.UserId(), SkipSid: skipSid, } routeUserServerMessage(uid, msg) } func routeUserServerMessage(uid types.Uid, msg *ServerComMessage) { if msg == nil || uid.IsZero() { return } globals.hub.routeSrv <- msg publishRealtimeUserMessage(uid, msg) } // Let other sessions of a given user know what messages are now received/read. // If both 'read' and 'recv' != 0 then 'read' takes precedence over 'recv'. // Cases U func (t *Topic) presPubMessageCount(uid types.Uid, mode types.AccessMode, read, recv int64, skip string) { var what string var seq int64 if read > 0 { what = "read" seq = read } else if recv > 0 { what = "recv" seq = recv } if what != "" { // Announce to user's other sessions on 'me' only if they are not attached to this topic. // Attached topics will receive an {info} t.presSingleUserOffline(uid, mode, what, &presParams{seqID: seq}, skip, true) } } // Let other sessions of a given user know that messages are now deleted // Cases V.1, V.2 func (t *Topic) presPubMessageDelete(uid types.Uid, mode types.AccessMode, delID int, list []MsgRange, skip string) { if len(list) == 0 && delID <= 0 { logs.Warn.Printf("Case V.1, V.2: topic[%s] invalid request - missing payload", t.name) return } // This check is only needed for V.1, but it does not hurt V.2. Let's do it here for both. if !t.userIsPresencer(uid) { return } params := &presParams{delID: delID, delSeq: list} // Case V.2 user := uid.UserId() t.presSubsOnline("del", user, params, &presFilters{singleUser: user}, skip) // Case V.1 t.presSingleUserOffline(uid, mode, "del", params, skip, true) } // Filter by permissions and notification type: check for exceptions, // then check if mode.IsPresencer() AND mode has at least some // bits specified in 'filter' (or filter is ModeNone). func presOfflineFilter(mode types.AccessMode, what string, pf *presFilters) bool { if what == "acs" || what == "gone" { return true } if what == "upd" && mode.IsJoiner() { return true } return mode.IsPresencer() && (pf == nil || ((pf.filterIn == types.ModeNone || mode&pf.filterIn != 0) && (pf.filterOut == types.ModeNone || mode&pf.filterOut == 0))) }