fix for calls
This commit is contained in:
parent
406a8805d8
commit
46afb97396
10 changed files with 151 additions and 101 deletions
|
|
@ -165,6 +165,14 @@ type BestEffortSessionBinder interface {
|
|||
PushToUserExceptAuthKeySessionBestEffort(ctx context.Context, userID int64, excludeAuthKeyID [8]byte, excludeSessionID int64, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error)
|
||||
}
|
||||
|
||||
// DeviceExcludingSessionPusher 按【设备】(perm/business auth_key)整体排除后 fan-out。
|
||||
// 与按单个 (raw auth_key, session) 排除的区别:一台设备可能有多条连接(OwpenGram
|
||||
// 客户端把 dc 1..5 都指向同一服务器 → 多连接),只排除受理那一条会让 phoneCall
|
||||
// "stop ringing" 的 discarded 漏到该设备的其它连接上、误杀它刚接起的通话。
|
||||
type DeviceExcludingSessionPusher interface {
|
||||
PushToUserExceptBusinessAuthKey(ctx context.Context, userID int64, excludeBusinessAuthKeyID [8]byte, t proto.MessageType, msg tg.UpdatesClass, timeout time.Duration) (int, error)
|
||||
}
|
||||
|
||||
// TransientSessionBinder 推送短命、不写 durable log 的 update(typing / presence)。
|
||||
// 与普通推送的关键区别:目标 session 未就绪时直接跳过、不进 pending——transient 数据
|
||||
// getDifference 无法补,就绪后由 getState 快照/下次状态变化重建,囤积过期 transient 无意义。
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/iamxvbaba/td/proto"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
|
@ -34,10 +35,29 @@ func (r *Router) pushPhoneCall(ctx context.Context, targetUserID int64, call dom
|
|||
return r.pushUserMessage(ctx, targetUserID, logMessage, r.phoneCallUpdates(ctx, call, targetUserID))
|
||||
}
|
||||
|
||||
// pushPhoneCallStopRinging 向被叫其它设备推合成 phoneCallDiscarded 停振铃(P0-1 修正)。
|
||||
// ctx 必须是接听设备的请求上下文:except 语义恰好把赢家排除在外。
|
||||
// pushPhoneCallStopRinging 向被叫【其它设备】推合成 phoneCallDiscarded 停振铃(P0-1 修正)。
|
||||
// ctx 必须是接听设备的请求上下文。
|
||||
//
|
||||
// ⚠ 排除必须按【设备】(perm/business auth_key)整体做,而不是按单个
|
||||
// (raw auth_key, session)。接听设备可能有多条到本服务器的连接——OwpenGram 客户端
|
||||
// 把 dc 1..5 都指向同一服务器,故一台真机常有数条连接/会话。若只排除受理 accept 的
|
||||
// 那一条 session,停振铃的 phoneCallDiscarded 会漏到同一台设备的其它连接上,客户端
|
||||
// 的 update 处理器按 call_id 匹配后当作"通话被挂断"、立即杀掉它刚接起的通话
|
||||
//(现象:被叫按下接听后立刻 Failed to connect;主叫方/被叫单连接侧无此问题——故表现
|
||||
// 为「A 打 B 正常、B 打 A 一接就断」的方向不对称)。按 business auth_key 排除可覆盖该
|
||||
// 设备的全部连接。See memory: call-*.
|
||||
func (r *Router) pushPhoneCallStopRinging(ctx context.Context, call domain.PhoneCall) int {
|
||||
upd := r.phoneCallUpdatesWith(ctx, tgPhoneCallStopRinging(call), call, call.ParticipantID)
|
||||
if pusher, ok := r.deps.Sessions.(DeviceExcludingSessionPusher); ok {
|
||||
if businessAuthKeyID, has := AuthKeyIDFrom(ctx); has {
|
||||
sent, err := pusher.PushToUserExceptBusinessAuthKey(ctx, call.ParticipantID, businessAuthKeyID, proto.MessageFromServer, upd, r.cfg.OutboundPushTimeout)
|
||||
if err != nil {
|
||||
r.log.Debug("phone call stop ringing", zap.Int64("user_id", call.ParticipantID), zap.Int("sent", sent), zap.Error(err))
|
||||
}
|
||||
return sent
|
||||
}
|
||||
}
|
||||
// 回退:能力不可用时退回按 session 排除(旧行为)。
|
||||
return r.pushUserMessage(ctx, call.ParticipantID, "phone call stop ringing", upd)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,22 +50,8 @@ func (r *Router) phoneCallConnections(callerID int64) []domain.PhoneCallConnecti
|
|||
// stun flag——单条目 stun+turn 在 Android 上只会产出 TURN server、丢失 STUN
|
||||
//(org_telegram_messenger_voip_Instance.cpp:848-884)。TDesktop 两种写法都认。
|
||||
// TURN username 是 REST 格式 "<expiry>:<uid>",天然避开 "reflector" 劫持禁区。
|
||||
//
|
||||
// 每个可达 IP(AdvertiseIP + ExtraIPs)都下发一对 STUN/TURN 候选,ICE 逐一
|
||||
// 尝试并选可达者:LAN 客户端走 LAN IP、外网客户端走公网 IP。id 必须全局唯一
|
||||
// 且从 1 递增(DrKLO 用 id 做 reflector 映射)。凭据与 IP 无关(TURN REST 只
|
||||
// 校验 HMAC),同一份 username/password 对所有 IP 有效。
|
||||
ips := append([]string{t.IP()}, t.ExtraIPs()...)
|
||||
conns := make([]domain.PhoneCallConnection, 0, len(ips)*2)
|
||||
id := int64(1)
|
||||
for _, ip := range ips {
|
||||
if ip == "" {
|
||||
continue
|
||||
}
|
||||
conns = append(conns, domain.PhoneCallConnection{ID: id, IP: ip, Port: t.Port(), Stun: true})
|
||||
id++
|
||||
conns = append(conns, domain.PhoneCallConnection{ID: id, IP: ip, Port: t.Port(), Username: username, Password: password, Turn: true})
|
||||
id++
|
||||
return []domain.PhoneCallConnection{
|
||||
{ID: 1, IP: t.IP(), Port: t.Port(), Stun: true},
|
||||
{ID: 2, IP: t.IP(), Port: t.Port(), Username: username, Password: password, Turn: true},
|
||||
}
|
||||
return conns
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
type fakeTURN struct {
|
||||
ip string
|
||||
port int
|
||||
extra []string
|
||||
enabled bool
|
||||
credUser string
|
||||
credPass string
|
||||
|
|
@ -21,10 +20,9 @@ func (f *fakeTURN) Enabled() bool { return f.enabled }
|
|||
func (f *fakeTURN) Credentials(string) (string, string, error) {
|
||||
return f.credUser, f.credPass, nil
|
||||
}
|
||||
func (f *fakeTURN) IP() string { return f.ip }
|
||||
func (f *fakeTURN) Port() int { return f.port }
|
||||
func (f *fakeTURN) ExtraIPs() []string { return f.extra }
|
||||
func (f *fakeTURN) Close() error { return nil }
|
||||
func (f *fakeTURN) IP() string { return f.ip }
|
||||
func (f *fakeTURN) Port() int { return f.port }
|
||||
func (f *fakeTURN) Close() error { return nil }
|
||||
|
||||
func newTURNRouter(t *testing.T, turn *fakeTURN) *Router {
|
||||
t.Helper()
|
||||
|
|
@ -38,61 +36,21 @@ func TestPhoneCallConnectionsDisabledTURNReturnsNil(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPhoneCallConnectionsSingleIP(t *testing.T) {
|
||||
func TestPhoneCallConnectionsSplitStunAndTurn(t *testing.T) {
|
||||
r := newTURNRouter(t, &fakeTURN{
|
||||
enabled: true, ip: "89.28.58.29", port: 12400,
|
||||
credUser: "u", credPass: "p",
|
||||
})
|
||||
conns := r.phoneCallConnections(1)
|
||||
if len(conns) != 2 {
|
||||
t.Fatalf("single IP: want 2 conns (stun+turn), got %d: %+v", len(conns), conns)
|
||||
t.Fatalf("want 2 conns (stun+turn), got %d: %+v", len(conns), conns)
|
||||
}
|
||||
// STUN and TURN must be SEPARATE entries (DrKLO's JNI ignores the stun flag
|
||||
// on a combined entry, dropping STUN).
|
||||
if !conns[0].Stun || conns[0].Turn || conns[0].ID != 1 || conns[0].IP != "89.28.58.29" {
|
||||
t.Fatalf("conn[0] should be STUN id1 on public IP, got %+v", conns[0])
|
||||
t.Fatalf("conn[0] should be STUN id1, got %+v", conns[0])
|
||||
}
|
||||
if !conns[1].Turn || conns[1].Stun || conns[1].ID != 2 || conns[1].Username != "u" || conns[1].Password != "p" {
|
||||
t.Fatalf("conn[1] should be TURN id2 with creds, got %+v", conns[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneCallConnectionsExtraIPsAddCandidatesWithUniqueIDs(t *testing.T) {
|
||||
r := newTURNRouter(t, &fakeTURN{
|
||||
enabled: true, ip: "89.28.58.29", port: 12400,
|
||||
extra: []string{"192.168.0.20", ""}, // empty entry must be skipped
|
||||
credUser: "u", credPass: "p",
|
||||
})
|
||||
conns := r.phoneCallConnections(1)
|
||||
// public (stun+turn) + LAN (stun+turn) = 4; the empty extra IP is skipped.
|
||||
if len(conns) != 4 {
|
||||
t.Fatalf("want 4 conns, got %d: %+v", len(conns), conns)
|
||||
}
|
||||
seenIDs := map[int64]bool{}
|
||||
for _, c := range conns {
|
||||
if seenIDs[c.ID] {
|
||||
t.Fatalf("duplicate connection id %d: %+v", c.ID, conns)
|
||||
}
|
||||
seenIDs[c.ID] = true
|
||||
if c.IP == "" {
|
||||
t.Fatalf("empty IP leaked into connections: %+v", conns)
|
||||
}
|
||||
}
|
||||
// IDs must be a contiguous 1..4 run (DrKLO maps reflectors by id).
|
||||
for id := int64(1); id <= 4; id++ {
|
||||
if !seenIDs[id] {
|
||||
t.Fatalf("missing contiguous id %d: %+v", id, conns)
|
||||
}
|
||||
}
|
||||
// The LAN IP must appear as both a STUN and a TURN candidate.
|
||||
var lanStun, lanTurn bool
|
||||
for _, c := range conns {
|
||||
if c.IP == "192.168.0.20" && c.Stun {
|
||||
lanStun = true
|
||||
}
|
||||
if c.IP == "192.168.0.20" && c.Turn {
|
||||
lanTurn = true
|
||||
}
|
||||
}
|
||||
if !lanStun || !lanTurn {
|
||||
t.Fatalf("LAN IP must yield both STUN and TURN candidates, got %+v", conns)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue