fix: route call accept across pods

This commit is contained in:
richjohnrambo
2026-09-18 11:15:19 +08:00
parent 4c809129eb
commit 192bdede33
5 changed files with 122 additions and 13 deletions
+30 -3
View File
@@ -330,6 +330,23 @@ func (call *videoCall) isStaleUnanswered(now time.Time, timeoutSeconds int) bool
return now.Sub(call.startedAt) > time.Duration(timeoutSeconds)*time.Second return now.Sub(call.startedAt) > time.Duration(timeoutSeconds)*time.Second
} }
func (call *videoCall) hasPartySessionOrUser(sid string, uid types.Uid) bool {
if call == nil {
return false
}
if sid != "" {
if _, ok := call.parties[sid]; ok {
return true
}
}
for _, party := range call.parties {
if party.uid == uid {
return true
}
}
return false
}
func (t *Topic) auditCallCreate(call *videoCall, caller types.Uid) { func (t *Topic) auditCallCreate(call *videoCall, caller types.Uid) {
if store.Calls == nil || call == nil || call.callID == "" || caller.IsZero() { if store.Calls == nil || call == nil || call.callID == "" || caller.IsZero() {
return return
@@ -645,8 +662,14 @@ func (t *Topic) handleCallEvent(msg *ClientComMessage) {
case constCallEventHangUp: case constCallEventHangUp:
switch len(t.currentCall.parties) { switch len(t.currentCall.parties) {
case 2: case 2:
// If it's a call in progress, hangup may arrive only from a call participant session. // If it's a call in progress, hangup may arrive from any current
if _, ok := t.currentCall.parties[msg.sess.sid]; !ok { // session of a call participant. This also covers rtc-join
// auto-accept, where the accepting HTTP request has no WebSocket sid.
sid := ""
if msg.sess != nil {
sid = msg.sess.sid
}
if !t.currentCall.hasPartySessionOrUser(sid, asUid) {
return return
} }
case 1: case 1:
@@ -654,7 +677,11 @@ func (t *Topic) handleCallEvent(msg *ClientComMessage) {
originatorUid, originator := t.getCallOriginator() originatorUid, originator := t.getCallOriginator()
// Hangup may come from either the originating session or // Hangup may come from either the originating session or
// any callee user session. // any callee user session.
if asUid == originatorUid && originator.sid != msg.sess.sid { sid := ""
if msg.sess != nil {
sid = msg.sess.sid
}
if asUid == originatorUid && originator.sid != sid {
return return
} }
default: default:
+1 -1
View File
@@ -594,7 +594,7 @@ func (c *Cluster) TopicMaster(msg *ClusterReq, rejected *bool) error {
logs.Warn.Println("cluster: meta request for unknown topic", msg.RcptTo) logs.Warn.Println("cluster: meta request for unknown topic", msg.RcptTo)
} }
case ProxyReqBroadcast: case ProxyReqBroadcast, ProxyReqCall:
select { select {
case globals.hub.routeCli <- msg.CliMsg: case globals.hub.routeCli <- msg.CliMsg:
default: default:
+59
View File
@@ -10,6 +10,8 @@ import (
"time" "time"
rtctokenbuilder "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/rtctokenbuilder2" rtctokenbuilder "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/rtctokenbuilder2"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types" "github.com/tinode/chat/server/store/types"
) )
@@ -110,6 +112,9 @@ func serveIMCallRTCJoin(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusConflict, 30064, imReasonCallStateInvalid, "call state invalid", map[string]any{"status": record.Status}) writeIMError(wrt, req, http.StatusConflict, 30064, imReasonCallStateInvalid, "call state invalid", map[string]any{"status": record.Status})
return return
} }
if body.Role == "callee" && record.Status == types.CallStatusStarted {
acceptIMCallFromRTCJoin(principal, record)
}
channelName := config.ChannelPrefix + record.CallID channelName := config.ChannelPrefix + record.CallID
uid, err := agoraUIDForUser(config, principal.Uid) uid, err := agoraUIDForUser(config, principal.Uid)
if err != nil { if err != nil {
@@ -267,6 +272,60 @@ func callStateAllowsRTCJoin(status string) bool {
} }
} }
func acceptIMCallFromRTCJoin(principal *imPrincipal, record *types.CallRecord) {
if principal == nil || record == nil || record.Status != types.CallStatusStarted || record.CalleeID != principal.Uid {
return
}
now := types.TimeNow()
if store.Calls != nil {
if err := store.Calls.Accept(principal.TenantID, record.CallID, record.SeqID, now); err != nil {
logs.Warn.Printf("rtc join: failed to mark call accepted tenant=%d topic=%s seq=%d call_id=%s: %v",
principal.TenantID, record.Topic, record.SeqID, record.CallID, err)
}
}
if globals.hub == nil {
return
}
payload, _ := json.Marshal(map[string]any{"callId": record.CallID})
msg := &ClientComMessage{
Note: &MsgClientNote{
Topic: record.Topic,
What: "call",
Event: constCallEventAccept,
SeqId: record.SeqID,
Payload: payload,
},
Original: record.Topic,
RcptTo: record.Topic,
AsUser: principal.Uid.UserId(),
AuthLvl: int(auth.LevelAuth),
Timestamp: now,
TenantID: principal.TenantID,
sess: &Session{
proto: NONE,
sid: "rtc-join:" + principal.Uid.UserId(),
uid: principal.Uid,
tenantID: principal.TenantID,
authLvl: auth.LevelAuth,
lastAction: now.UnixNano(),
},
init: true,
}
if globals.cluster != nil && globals.cluster.isRemoteTopic(types.TopicKey{TenantID: principal.TenantID, Topic: record.Topic}) {
if err := globals.cluster.routeToTopicMaster(ProxyReqCall, msg, record.Topic, msg.sess); err != nil {
logs.Warn.Printf("rtc join: failed to route call accept tenant=%d topic=%s seq=%d call_id=%s: %v",
principal.TenantID, record.Topic, record.SeqID, record.CallID, err)
}
return
}
select {
case globals.hub.routeCli <- msg:
default:
logs.Err.Printf("rtc join: hub route queue full tenant=%d topic=%s seq=%d call_id=%s",
principal.TenantID, record.Topic, record.SeqID, record.CallID)
}
}
func agoraUIDForUser(config imAgoraConfig, uid types.Uid) (any, error) { func agoraUIDForUser(config imAgoraConfig, uid types.Uid) (any, error) {
switch config.UIDMode { switch config.UIDMode {
case "int": case "int":
+17 -1
View File
@@ -209,6 +209,11 @@ type testCallsStore struct {
limit int limit int
cursor string cursor string
next string next string
accepted bool
acceptTenant types.TenantID
acceptCallID string
acceptSeq int64
acceptedAt time.Time
} }
type testThirdPartyConfigStore map[string]string type testThirdPartyConfigStore map[string]string
@@ -830,7 +835,12 @@ func (s *testCallsStore) Create(_ types.TenantID, _ *types.CallRecord) error {
return nil return nil
} }
func (s *testCallsStore) Accept(_ types.TenantID, _ string, _ int64, _ time.Time) error { func (s *testCallsStore) Accept(tenantID types.TenantID, callID string, seq int64, acceptedAt time.Time) error {
s.accepted = true
s.acceptTenant = tenantID
s.acceptCallID = callID
s.acceptSeq = seq
s.acceptedAt = acceptedAt
return nil return nil
} }
@@ -6300,6 +6310,9 @@ func TestServeIMCallRTCJoinReturnsSameAgoraChannelForCallee(t *testing.T) {
if callerRec.Code != http.StatusOK { if callerRec.Code != http.StatusOK {
t.Fatalf("caller rtc join status: got %d body=%s", callerRec.Code, callerRec.Body.String()) t.Fatalf("caller rtc join status: got %d body=%s", callerRec.Code, callerRec.Body.String())
} }
if calls.accepted {
t.Fatal("caller rtc join must not auto-accept the call")
}
calleeReq := httptest.NewRequest(http.MethodPost, "/v1/im/calls/rtc-join", strings.NewReader(`{"topic":"`+topic+`","call_id":"call-1","seq":12,"call_type":"audio","role":"callee"}`)) calleeReq := httptest.NewRequest(http.MethodPost, "/v1/im/calls/rtc-join", strings.NewReader(`{"topic":"`+topic+`","call_id":"call-1","seq":12,"call_type":"audio","role":"callee"}`))
calleeReq.Header.Set("Authorization", "Bearer token-callee") calleeReq.Header.Set("Authorization", "Bearer token-callee")
@@ -6308,6 +6321,9 @@ func TestServeIMCallRTCJoinReturnsSameAgoraChannelForCallee(t *testing.T) {
if calleeRec.Code != http.StatusOK { if calleeRec.Code != http.StatusOK {
t.Fatalf("callee rtc join status: got %d body=%s", calleeRec.Code, calleeRec.Body.String()) t.Fatalf("callee rtc join status: got %d body=%s", calleeRec.Code, calleeRec.Body.String())
} }
if !calls.accepted || calls.acceptTenant != 7 || calls.acceptCallID != "call-1" || calls.acceptSeq != 12 || calls.acceptedAt.IsZero() {
t.Fatalf("callee rtc join did not mark call accepted: %#v", calls)
}
callerData := decodeIMHTTPResponse(t, callerRec).Data.(map[string]any) callerData := decodeIMHTTPResponse(t, callerRec).Data.(map[string]any)
calleeData := decodeIMHTTPResponse(t, calleeRec).Data.(map[string]any) calleeData := decodeIMHTTPResponse(t, calleeRec).Data.(map[string]any)
+7
View File
@@ -1650,6 +1650,13 @@ func (s *Session) note(msg *ClientComMessage) {
// the user about the call. // the user about the call.
// //
// Hub will forward to topic, if appropriate. // Hub will forward to topic, if appropriate.
if msg.Note.What == "call" && globals.cluster != nil &&
globals.cluster.isRemoteTopic(types.TopicKey{TenantID: msg.TenantID, Topic: msg.RcptTo}) {
if err := globals.cluster.routeToTopicMaster(ProxyReqCall, msg, msg.RcptTo, s); err != nil {
logs.Warn.Printf("s.note: failed routing detached call event to topic master topic=%s sid=%s err=%v", msg.RcptTo, s.sid, err)
}
return
}
select { select {
case globals.hub.routeCli <- msg: case globals.hub.routeCli <- msg:
default: default: