Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
490
internal/rpc/presence.go
Normal file
490
internal/rpc/presence.go
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
const userOnlineTTL = 5 * time.Minute
|
||||
const presenceDialogFanoutCandidateLimit = 512
|
||||
|
||||
type presenceSessionKey struct {
|
||||
rawAuthKeyID [8]byte
|
||||
sessionID int64
|
||||
}
|
||||
|
||||
type presenceSessionState struct {
|
||||
userID int64
|
||||
status domain.UserStatus
|
||||
}
|
||||
|
||||
type presenceTracker struct {
|
||||
mu sync.RWMutex
|
||||
bySession map[presenceSessionKey]presenceSessionState
|
||||
byUser map[int64]map[presenceSessionKey]domain.UserStatus
|
||||
}
|
||||
|
||||
func newPresenceTracker() *presenceTracker {
|
||||
return &presenceTracker{
|
||||
bySession: make(map[presenceSessionKey]presenceSessionState),
|
||||
byUser: make(map[int64]map[presenceSessionKey]domain.UserStatus),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *presenceTracker) setSessionStatus(key presenceSessionKey, userID int64, status domain.UserStatus) {
|
||||
if p == nil || userID == 0 {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if old, ok := p.bySession[key]; ok {
|
||||
p.removeSessionLocked(key, old.userID)
|
||||
}
|
||||
p.bySession[key] = presenceSessionState{userID: userID, status: status}
|
||||
sessions := p.byUser[userID]
|
||||
if sessions == nil {
|
||||
sessions = make(map[presenceSessionKey]domain.UserStatus)
|
||||
p.byUser[userID] = sessions
|
||||
}
|
||||
sessions[key] = status
|
||||
}
|
||||
|
||||
func (p *presenceTracker) clearSession(key presenceSessionKey) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if old, ok := p.bySession[key]; ok {
|
||||
p.removeSessionLocked(key, old.userID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *presenceTracker) removeSessionLocked(key presenceSessionKey, userID int64) {
|
||||
delete(p.bySession, key)
|
||||
sessions := p.byUser[userID]
|
||||
delete(sessions, key)
|
||||
if len(sessions) == 0 {
|
||||
delete(p.byUser, userID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *presenceTracker) statusFor(userID int64, now int) (domain.UserStatus, bool) {
|
||||
if p == nil || userID == 0 {
|
||||
return domain.UserStatus{}, false
|
||||
}
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
sessions := p.byUser[userID]
|
||||
if len(sessions) == 0 {
|
||||
return domain.UserStatus{}, false
|
||||
}
|
||||
known := false
|
||||
var bestOnline domain.UserStatus
|
||||
var bestOffline domain.UserStatus
|
||||
for _, status := range sessions {
|
||||
status = normalizePresenceStatus(status, now)
|
||||
switch status.Kind {
|
||||
case domain.UserStatusOnline:
|
||||
if bestOnline.Expires == 0 || status.Expires > bestOnline.Expires {
|
||||
bestOnline = status
|
||||
}
|
||||
known = true
|
||||
case domain.UserStatusOffline:
|
||||
if bestOffline.WasOnline == 0 || status.WasOnline > bestOffline.WasOnline {
|
||||
bestOffline = status
|
||||
}
|
||||
known = true
|
||||
case domain.UserStatusRecently, domain.UserStatusLastWeek, domain.UserStatusLastMonth, domain.UserStatusEmpty:
|
||||
if !known {
|
||||
bestOffline = status
|
||||
known = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if bestOnline.Kind == domain.UserStatusOnline {
|
||||
return bestOnline, true
|
||||
}
|
||||
if known {
|
||||
return bestOffline, true
|
||||
}
|
||||
return domain.UserStatus{}, false
|
||||
}
|
||||
|
||||
func normalizePresenceStatus(status domain.UserStatus, now int) domain.UserStatus {
|
||||
if status.Kind == domain.UserStatusOnline && status.Expires <= now {
|
||||
wasOnline := status.WasOnline
|
||||
if wasOnline == 0 || wasOnline > status.Expires {
|
||||
wasOnline = status.Expires
|
||||
}
|
||||
return domain.UserStatus{Kind: domain.UserStatusOffline, WasOnline: wasOnline}
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func (r *Router) setPresenceFromContext(ctx context.Context, userID int64, offline bool) domain.UserStatus {
|
||||
now := int(r.clock.Now().Unix())
|
||||
status := domain.UserStatus{Kind: domain.UserStatusOffline, WasOnline: now}
|
||||
if !offline {
|
||||
status = domain.UserStatus{
|
||||
Kind: domain.UserStatusOnline,
|
||||
Expires: now + int(userOnlineTTL/time.Second),
|
||||
WasOnline: now,
|
||||
}
|
||||
}
|
||||
if key, ok := presenceSessionKeyFromContext(ctx); ok {
|
||||
r.presence.setSessionStatus(key, userID, status)
|
||||
}
|
||||
r.persistLastSeen(ctx, userID, now)
|
||||
return r.userPresenceStatusForUser(domain.User{ID: userID, LastSeenAt: now})
|
||||
}
|
||||
|
||||
func (r *Router) announceSessionOnline(ctx context.Context, userID int64) {
|
||||
if userID == 0 {
|
||||
return
|
||||
}
|
||||
status := r.setPresenceFromContext(ctx, userID, false)
|
||||
r.pushUserStatus(ctx, userID, status)
|
||||
r.pushOnlinePeerStatusesToCurrentSession(ctx, userID)
|
||||
}
|
||||
|
||||
func (r *Router) userPresenceStatus(userID int64) domain.UserStatus {
|
||||
return r.userPresenceStatusForUser(domain.User{ID: userID})
|
||||
}
|
||||
|
||||
func (r *Router) userPresenceStatusForUser(u domain.User) domain.UserStatus {
|
||||
userID := u.ID
|
||||
if userID == 0 {
|
||||
return domain.UserStatus{Kind: domain.UserStatusRecently}
|
||||
}
|
||||
now := int(r.clock.Now().Unix())
|
||||
if status, ok := r.presence.statusFor(userID, now); ok {
|
||||
return status
|
||||
}
|
||||
if provider, ok := r.deps.Sessions.(OnlineUserProvider); ok && provider.IsUserOnline(userID) {
|
||||
return domain.UserStatus{
|
||||
Kind: domain.UserStatusOnline,
|
||||
Expires: now + int(userOnlineTTL/time.Second),
|
||||
WasOnline: now,
|
||||
}
|
||||
}
|
||||
if u.LastSeenAt > 0 {
|
||||
return domain.UserStatus{Kind: domain.UserStatusOffline, WasOnline: u.LastSeenAt}
|
||||
}
|
||||
return domain.UserStatus{Kind: domain.UserStatusRecently}
|
||||
}
|
||||
|
||||
type userLastSeenUpdater interface {
|
||||
UpdateLastSeen(ctx context.Context, userID int64, lastSeenAt int) error
|
||||
}
|
||||
|
||||
func (r *Router) persistLastSeen(ctx context.Context, userID int64, lastSeenAt int) {
|
||||
if userID == 0 || lastSeenAt <= 0 || r.deps.Users == nil {
|
||||
return
|
||||
}
|
||||
updater, ok := r.deps.Users.(userLastSeenUpdater)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := updater.UpdateLastSeen(ctx, userID, lastSeenAt); err != nil {
|
||||
r.log.Warn("Update user last seen failed", zap.Int64("user_id", userID), zap.Int("last_seen_at", lastSeenAt), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// SessionOffline is called by mtprotoedge when an active connection disappears.
|
||||
// Business-side effects stay here: mtprotoedge only reports lifecycle facts.
|
||||
func (r *Router) SessionOffline(rawAuthKeyID [8]byte, sessionID, userID int64, lastForUser bool) {
|
||||
if userID == 0 {
|
||||
return
|
||||
}
|
||||
ctx := WithSessionID(WithRawAuthKeyID(context.Background(), rawAuthKeyID), sessionID)
|
||||
key := presenceSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID}
|
||||
if !lastForUser {
|
||||
r.presence.clearSession(key)
|
||||
return
|
||||
}
|
||||
now := int(r.clock.Now().Unix())
|
||||
status := domain.UserStatus{Kind: domain.UserStatusOffline, WasOnline: now}
|
||||
r.presence.setSessionStatus(key, userID, status)
|
||||
r.persistLastSeen(ctx, userID, now)
|
||||
r.pushUserStatus(ctx, userID, status)
|
||||
}
|
||||
|
||||
func presenceSessionKeyFromContext(ctx context.Context) (presenceSessionKey, bool) {
|
||||
sessionID, ok := SessionIDFrom(ctx)
|
||||
if !ok {
|
||||
return presenceSessionKey{}, false
|
||||
}
|
||||
if rawAuthKeyID, ok := RawAuthKeyIDFrom(ctx); ok {
|
||||
return presenceSessionKey{rawAuthKeyID: rawAuthKeyID, sessionID: sessionID}, true
|
||||
}
|
||||
if authKeyID, ok := AuthKeyIDFrom(ctx); ok {
|
||||
return presenceSessionKey{rawAuthKeyID: authKeyID, sessionID: sessionID}, true
|
||||
}
|
||||
return presenceSessionKey{sessionID: sessionID}, true
|
||||
}
|
||||
|
||||
func (r *Router) withUserPresence(u domain.User) domain.User {
|
||||
if u.ID == 0 {
|
||||
return u
|
||||
}
|
||||
u.Status = r.userPresenceStatusForUser(u)
|
||||
return u
|
||||
}
|
||||
|
||||
func (r *Router) withUsersPresence(users []domain.User) []domain.User {
|
||||
if len(users) == 0 {
|
||||
return users
|
||||
}
|
||||
out := append([]domain.User(nil), users...)
|
||||
for i := range out {
|
||||
out[i] = r.withUserPresence(out[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) withContactListPresence(list domain.ContactList) domain.ContactList {
|
||||
if len(list.Contacts) == 0 {
|
||||
return list
|
||||
}
|
||||
out := list
|
||||
out.Contacts = append([]domain.Contact(nil), list.Contacts...)
|
||||
for i := range out.Contacts {
|
||||
out.Contacts[i].User = r.withUserPresence(out.Contacts[i].User)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) withMessageListPresence(list domain.MessageList) domain.MessageList {
|
||||
list.Users = r.withUsersPresence(list.Users)
|
||||
return list
|
||||
}
|
||||
|
||||
func (r *Router) withDialogListPresence(list domain.DialogList) domain.DialogList {
|
||||
list.Users = r.withUsersPresence(list.Users)
|
||||
return list
|
||||
}
|
||||
|
||||
func (r *Router) withUserSearchPresence(res domain.UserSearchResult) domain.UserSearchResult {
|
||||
res.MyResults = r.withUsersPresence(res.MyResults)
|
||||
res.Results = r.withUsersPresence(res.Results)
|
||||
return res
|
||||
}
|
||||
|
||||
func (r *Router) tgUser(u domain.User) *tg.User {
|
||||
return tgUser(r.withUserPresence(u))
|
||||
}
|
||||
|
||||
func (r *Router) tgSelfUser(u domain.User) *tg.User {
|
||||
return tgSelfUser(r.withUserPresence(u))
|
||||
}
|
||||
|
||||
func (r *Router) tgUsers(users []domain.User) []tg.UserClass {
|
||||
return tgUsers(r.withUsersPresence(users))
|
||||
}
|
||||
|
||||
func (r *Router) pushUserStatus(ctx context.Context, userID int64, status domain.UserStatus) {
|
||||
if userID == 0 {
|
||||
return
|
||||
}
|
||||
update := &tg.Updates{
|
||||
Updates: []tg.UpdateClass{&tg.UpdateUserStatus{
|
||||
UserID: userID,
|
||||
Status: tgUserStatus(status),
|
||||
}},
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
Seq: 0,
|
||||
}
|
||||
r.pushUserUpdates(ctx, userID, update)
|
||||
for _, recipientID := range r.onlinePresenceRecipientIDs(ctx, userID) {
|
||||
if recipientID == userID {
|
||||
continue
|
||||
}
|
||||
r.pushUserMessage(ctx, recipientID, "push user status", update)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) onlinePresenceRecipientIDs(ctx context.Context, userID int64) []int64 {
|
||||
if userID == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := map[int64]struct{}{}
|
||||
out := make([]int64, 0)
|
||||
add := func(ids []int64) {
|
||||
for _, id := range ids {
|
||||
if id == 0 || id == userID {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
add(r.onlineContactIDs(ctx, userID))
|
||||
add(r.onlinePrivateDialogPeerIDs(ctx, userID, seen))
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) onlineContactIDs(ctx context.Context, userID int64) []int64 {
|
||||
if r.deps.Contacts == nil || userID == 0 {
|
||||
return nil
|
||||
}
|
||||
ids, notModified, err := r.deps.Contacts.ContactIDs(ctx, userID, 0)
|
||||
if err != nil || notModified || len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
candidates := make([]int64, 0, len(ids))
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
contactID := int64(id)
|
||||
if contactID == 0 || contactID == userID {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[contactID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[contactID] = struct{}{}
|
||||
candidates = append(candidates, contactID)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
if provider, ok := r.deps.Sessions.(OnlineUserProvider); ok {
|
||||
return provider.OnlineUserIDsForCandidates(candidates, 0)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func (r *Router) onlinePrivateDialogPeerIDs(ctx context.Context, userID int64, already map[int64]struct{}) []int64 {
|
||||
if r.deps.Dialogs == nil || userID == 0 {
|
||||
return nil
|
||||
}
|
||||
provider, ok := r.deps.Sessions.(OnlineUserProvider)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
candidates := provider.OnlineUserIDs(presenceDialogFanoutCandidateLimit)
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: userID}
|
||||
out := make([]int64, 0, len(candidates))
|
||||
for _, candidateID := range candidates {
|
||||
if candidateID == 0 || candidateID == userID {
|
||||
continue
|
||||
}
|
||||
if _, ok := already[candidateID]; ok {
|
||||
continue
|
||||
}
|
||||
list, err := r.deps.Dialogs.GetPeerDialogs(ctx, candidateID, []domain.Peer{peer})
|
||||
if err != nil || !dialogListHasPeer(list, peer) {
|
||||
continue
|
||||
}
|
||||
out = append(out, candidateID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Router) pushOnlinePeerStatusesToCurrentSession(ctx context.Context, userID int64) {
|
||||
peerIDs := r.onlineRelevantPeerIDs(ctx, userID)
|
||||
if len(peerIDs) == 0 {
|
||||
return
|
||||
}
|
||||
updates := make([]tg.UpdateClass, 0, len(peerIDs))
|
||||
for _, peerID := range peerIDs {
|
||||
status := r.userPresenceStatus(peerID)
|
||||
if status.Kind != domain.UserStatusOnline {
|
||||
continue
|
||||
}
|
||||
updates = append(updates, &tg.UpdateUserStatus{
|
||||
UserID: peerID,
|
||||
Status: tgUserStatus(status),
|
||||
})
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return
|
||||
}
|
||||
r.pushCurrentSessionMessage(ctx, "push online peer statuses", &tg.Updates{
|
||||
Updates: updates,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
Seq: 0,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) onlineRelevantPeerIDs(ctx context.Context, userID int64) []int64 {
|
||||
if userID == 0 {
|
||||
return nil
|
||||
}
|
||||
provider, ok := r.deps.Sessions.(OnlineUserProvider)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
seen := map[int64]struct{}{}
|
||||
candidates := make([]int64, 0)
|
||||
add := func(id int64) {
|
||||
if id == 0 || id == userID {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
return
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
candidates = append(candidates, id)
|
||||
}
|
||||
if r.deps.Contacts != nil {
|
||||
ids, notModified, err := r.deps.Contacts.ContactIDs(ctx, userID, 0)
|
||||
if err == nil && !notModified {
|
||||
for _, id := range ids {
|
||||
add(int64(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.deps.Dialogs != nil {
|
||||
list, err := r.deps.Dialogs.GetDialogs(ctx, userID, domain.DialogFilter{Limit: presenceDialogFanoutCandidateLimit})
|
||||
if err == nil {
|
||||
for _, dialog := range list.Dialogs {
|
||||
if dialog.Peer.Type == domain.PeerTypeUser {
|
||||
add(dialog.Peer.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
return provider.OnlineUserIDsForCandidates(candidates, 0)
|
||||
}
|
||||
|
||||
func dialogListHasPeer(list domain.DialogList, peer domain.Peer) bool {
|
||||
for _, dialog := range list.Dialogs {
|
||||
if dialog.Peer != peer {
|
||||
continue
|
||||
}
|
||||
if dialog.TopMessage != 0 ||
|
||||
dialog.TopMessageDate != 0 ||
|
||||
dialog.ReadInboxMaxID != 0 ||
|
||||
dialog.ReadOutboxMaxID != 0 ||
|
||||
dialog.UnreadCount != 0 ||
|
||||
dialog.UnreadMentions != 0 ||
|
||||
dialog.UnreadReactions != 0 ||
|
||||
dialog.Pinned ||
|
||||
dialog.UnreadMark ||
|
||||
dialog.Draft != nil {
|
||||
return true
|
||||
}
|
||||
for _, msg := range list.Messages {
|
||||
if msg.Peer == peer {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue