telegabber/calls/signaling/tgsig/manager.go
2026-05-27 08:37:47 -07:00

207 lines
5.5 KiB
Go

package tgsig
import (
"sync"
"dev.narayana.im/narayana/telegabber/calls/signaling"
log "github.com/sirupsen/logrus"
"github.com/zelenin/go-tdlib/client"
)
// runs for a brand-new inbound Pending; must build the bridge, register
// the Callee with Manager.Register, and start the bridge; false drops the call
type IncomingCallHandler func(jid string, callID int32, userID int64, isVideo bool) (ok bool)
// router for tdlib call updates; callIDs are per-tdlib-client so key is (jid, callID)
type Manager struct {
mu sync.Mutex
sides map[managerKey]*callBase
incoming IncomingCallHandler
}
type managerKey struct {
jid string
callID int32
}
// h may be nil and set later via SetIncomingHandler
func NewManager(h IncomingCallHandler) *Manager {
return &Manager{
sides: make(map[managerKey]*callBase),
incoming: h,
}
}
func (m *Manager) SetIncomingHandler(h IncomingCallHandler) {
m.mu.Lock()
m.incoming = h
m.mu.Unlock()
}
func (m *Manager) incomingHandler() IncomingCallHandler {
m.mu.Lock()
defer m.mu.Unlock()
return m.incoming
}
func (m *Manager) Register(jid string, callID int32, s *callBase) {
if s == nil {
return
}
m.mu.Lock()
m.sides[managerKey{jid, callID}] = s
m.mu.Unlock()
}
func (m *Manager) Drop(jid string, callID int32) {
m.mu.Lock()
delete(m.sides, managerKey{jid, callID})
m.mu.Unlock()
}
func (m *Manager) lookup(jid string, callID int32) *callBase {
m.mu.Lock()
defer m.mu.Unlock()
return m.sides[managerKey{jid, callID}]
}
func (m *Manager) OnUpdateCall(jid string, u *client.UpdateCall) {
call := u.Call
entry := log.WithFields(log.Fields{
"jid": jid,
"call_id": call.Id,
"user_id": call.UserId,
"is_outgoing": call.IsOutgoing,
"is_video": call.IsVideo,
})
switch call.State.CallStateType() {
case client.TypeCallStatePending:
entry.Info("Call pending")
if call.IsOutgoing {
// Pending fires twice: server-created (IsReceived=false) and
// peer-notified (IsReceived=true); only the second is ringing.
// Near-instant pickups may skip the second one - the bridge
// state guard at ExchangingKeys covers that.
pending, _ := call.State.(*client.CallStatePending)
if pending == nil || !pending.IsReceived {
return
}
if b := m.bridgeFor(jid, call.Id); b != nil {
b.Ringing()
}
return
}
if existing := m.lookup(jid, call.Id); existing != nil {
// duplicate Pending
return
}
h := m.incomingHandler()
if h == nil {
entry.Warn("Inbound call but no incoming-call handler wired; ignoring")
return
}
if ok := h(jid, call.Id, call.UserId, call.IsVideo); !ok {
entry.Warn("Incoming-call handler dropped the call")
}
case client.TypeCallStateExchangingKeys:
entry.Info("Call exchanging keys")
// outbound: peer accepted, bridge fires Callee.Accept on xmpp side
// inbound: AcceptCall already ran, state guard makes this a no-op
if b := m.bridgeFor(jid, call.Id); b != nil {
b.CalleeAccepted()
}
case client.TypeCallStateReady:
state, ok := call.State.(*client.CallStateReady)
if !ok {
entry.Error("Failed to cast CallStateReady")
return
}
entry.Info("Call ready, setting up P2P connection")
s := m.lookup(jid, call.Id)
if s == nil {
entry.Warn("Ready for unknown call")
return
}
s.onReady(state, call.IsOutgoing, entry)
case client.TypeCallStateHangingUp:
entry.Info("Call hanging up")
m.endCall(jid, call.Id, signaling.ReasonHangup)
case client.TypeCallStateDiscarded:
reason := "unknown"
if disc, ok := call.State.(*client.CallStateDiscarded); ok && disc.Reason != nil {
reason = disc.Reason.CallDiscardReasonType()
}
entry.WithField("reason", reason).Info("Call discarded")
m.endCall(jid, call.Id, mapDiscardReason(reason))
case client.TypeCallStateError:
errMsg := "unknown"
if errState, ok := call.State.(*client.CallStateError); ok && errState.Error != nil {
errMsg = errState.Error.Message
}
entry.WithField("error", errMsg).Error("Call error")
m.endCall(jid, call.Id, signaling.ReasonMediaFailed)
}
}
// forwards tdlib signaling data into ntgcalls
func (m *Manager) OnNewSignalingData(jid string, u *client.UpdateNewCallSignalingData) {
s := m.lookup(jid, u.CallId)
if s == nil {
log.WithFields(log.Fields{
"jid": jid,
"call_id": u.CallId,
}).Warn("Signaling data for unknown call")
return
}
s.adapter.withCallContext(func() {
if err := s.adapter.ntg.SendSignalingData(s.userID, u.Data); err != nil {
log.WithFields(log.Fields{
"jid": jid,
"call_id": u.CallId,
"user_id": s.userID,
}).WithError(err).Error("Failed to forward signaling data to ntgcalls")
}
})
}
func (m *Manager) bridgeFor(jid string, callID int32) *signaling.Bridge {
s := m.lookup(jid, callID)
if s == nil {
return nil
}
return s.getBridge()
}
// unified teardown for HangingUp / Discarded / Error
func (m *Manager) endCall(jid string, callID int32, reason signaling.TerminationReason) {
s := m.lookup(jid, callID)
if s == nil {
return
}
if b := s.getBridge(); b != nil {
b.Terminate(reason)
}
s.cleanup()
}
func mapDiscardReason(tdlibReason string) signaling.TerminationReason {
switch tdlibReason {
case "callDiscardReasonDeclined":
return signaling.ReasonDecline
case "callDiscardReasonMissed", "callDiscardReasonExpired":
return signaling.ReasonRingTimeout
case "callDiscardReasonDisconnected":
return signaling.ReasonMediaFailed
case "callDiscardReasonHungUp", "callDiscardReasonEmpty":
return signaling.ReasonHangup
default:
return signaling.ReasonUnknown
}
}