package rpc import ( "context" "encoding/hex" "github.com/iamxvbaba/td/proto" "github.com/iamxvbaba/td/tg" "go.uber.org/zap" "telesrv/internal/domain" ) // 通话信令推送全部走无 pts 直推(pushUserMessage):信令有效期秒级,离线设备 // 重连后经 getDifference 补收一条早已失效的来电毫无意义甚至有害。唯一带 pts 的 // 产物是结束后的 messageActionPhoneCall 服务消息(P2,走 outbox 补偿离线设备)。 // phoneCallUpdates 把 viewer 视角的 phoneCall 状态包成可直推的 Updates 容器。 func (r *Router) phoneCallUpdates(ctx context.Context, call domain.PhoneCall, viewerID int64) *tg.Updates { return r.phoneCallUpdatesWith(ctx, tgPhoneCallForViewer(call, viewerID), call, viewerID) } func (r *Router) phoneCallUpdatesWith(ctx context.Context, view tg.PhoneCallClass, call domain.PhoneCall, viewerID int64) *tg.Updates { return &tg.Updates{ Updates: []tg.UpdateClass{&tg.UpdatePhoneCall{PhoneCall: view}}, Users: r.tgUsersForIDs(ctx, viewerID, []int64{call.AdminID, call.ParticipantID}), Chats: []tg.ChatClass{}, Date: int(r.clock.Now().Unix()), Seq: 0, } } // pushPhoneCall 把 call 当前状态按 targetUserID 视角推给其全部在线设备 // (ctx 携带的发起设备会被 pushUserMessage 的 except 语义排除)。 func (r *Router) pushPhoneCall(ctx context.Context, targetUserID int64, call domain.PhoneCall, logMessage string) int { sent := r.pushUserMessage(ctx, targetUserID, logMessage, r.phoneCallUpdates(ctx, call, targetUserID)) // 诊断:确认服务端把 phoneCall 状态推给了对端【几条】连接。sent==0 说明对端 // 全部连接都没收到(一接就断类问题的关键判据)。 if r.log != nil { r.log.Info("push phoneCall", zap.String("stage", logMessage), zap.Int64("target_user_id", targetUserID), zap.Int64("call_id", call.ID), zap.Int("sent", sent), ) } return sent } // pushPhoneCallToDevice 只把 phoneCall 状态推给【发起呼叫的那台设备】(originating // session),不广播到该用户的其它会话,且是 fail-closed 路径:目标锚点缺失、session // 已断开或编码/发送失败时都不得回退为 user 级广播。 // // ⚠ 为什么必须定向、且不能回退广播:一个「呼出」通话只属于发起它的那台设备。若把 // 呼叫方视角的 phoneCallWaiting(receive_date) 广播到该账号的所有会话,而【同一账号 // 又登录在被叫的那台手机上】(多账号同机),手机上这份呼叫方副本会收到 // phoneCallWaiting——它与来电是同一个 call_id,于是覆写 // VoIPService.callIShouldHavePutIntoIntent 这个【静态全局】pending 来电(stock // DrKLO:MessagesController 只按 call.id 匹配、不校验账号),把带 g_a_hash 的 // phoneCallRequested 换成不含 g_a_hash 的 phoneCallWaiting。被叫接听后 // SHA256(g_a)!=g_a_hash → 「Ga hash doesn't match」→ callFailed → 一接就断。定向到 // CallerDevice 后,手机上的呼叫方副本收不到该更新,pending 来电不被污染。早前版本在 // 定向失败时回退广播,看似"正常路径不会走到",但那正是本条目要防的那类多账号同机场景 // ——回退广播会重新引入污染,因此改为纯粹跳过。See memory: call-ga-hash-multiaccount-clobber。 // // 呼叫方发起会话在 requestCall 时已发过 RPC、必然 ready;PushToSessionForAuthKey 对 // 暂未 ready 的会话也会入队补发,不丢。 func (r *Router) pushPhoneCallToDevice(ctx context.Context, targetUserID int64, device domain.SessionRef, call domain.PhoneCall, logMessage string) { if targetUserID == 0 || device.Zero() || r.deps.Sessions == nil { if r.log != nil { r.log.Debug(logMessage, zap.Int64("target_user_id", targetUserID), zap.Int64("call_id", call.ID), zap.Int64("target_session_id", device.SessionID), zap.String("delivery", "skipped_invalid_device_anchor"), ) } return } updates := r.phoneCallUpdates(ctx, call, targetUserID) err := r.deps.Sessions.PushToSessionForAuthKey(ctx, device.RawAuthKeyID, device.SessionID, proto.MessageFromServer, updates) if r.log != nil { r.log.Info("push phoneCall to device", zap.String("stage", logMessage), zap.Int64("target_user_id", targetUserID), zap.Int64("call_id", call.ID), zap.String("device_auth_key", hex.EncodeToString(device.RawAuthKeyID[:])), zap.Int64("device_session", device.SessionID), zap.Error(err), ) } } // pushPhoneCallStopRinging 向被叫其它设备推合成 phoneCallDiscarded 停振铃(P0-1 修正)。 // ctx 必须是接听设备的请求上下文:except 语义恰好把赢家排除在外。 func (r *Router) pushPhoneCallStopRinging(ctx context.Context, call domain.PhoneCall) int { upd := r.phoneCallUpdatesWith(ctx, tgPhoneCallStopRinging(call), call, call.ParticipantID) return r.pushUserMessage(ctx, call.ParticipantID, "phone call stop ringing", upd) } // pushPhoneCallDiscardedBoth 把终态推给双方全部设备(发起设备由 ctx except 排除, // 其结果从 RPC 响应获得)。 func (r *Router) pushPhoneCallDiscardedBoth(ctx context.Context, call domain.PhoneCall) { r.pushPhoneCall(ctx, call.AdminID, call, "phone call discarded") r.pushPhoneCall(ctx, call.ParticipantID, call, "phone call discarded") } // pushPhoneSignalingData 把信令字节透传给对端。优先走设备锚点定向推送 // (requestCall/acceptCall 受理设备),锚点失效(设备重连换 session)则回退 // user 级扇出——非参与设备按 phone_call_id 不匹配静默丢弃(TDesktop // handleSignalingData 行为),扇出无害。 func (r *Router) pushPhoneSignalingData(ctx context.Context, targetUserID int64, device domain.SessionRef, callID int64, data []byte) { upd := &tg.Updates{ Updates: []tg.UpdateClass{&tg.UpdatePhoneCallSignalingData{ PhoneCallID: callID, Data: data, }}, Users: []tg.UserClass{}, Chats: []tg.ChatClass{}, Date: int(r.clock.Now().Unix()), Seq: 0, } if !device.Zero() && r.deps.Sessions != nil { if err := r.deps.Sessions.PushToSessionForAuthKey(ctx, device.RawAuthKeyID, device.SessionID, proto.MessageFromServer, upd); err == nil { return } } r.pushUserMessage(ctx, targetUserID, "phone call signaling", upd) }