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 } }