package tgsig import ( "sync" "dev.narayana.im/narayana/telegabber/calls/audio" "dev.narayana.im/narayana/telegabber/calls/signaling" log "github.com/sirupsen/logrus" "github.com/zelenin/go-tdlib/client" ) // per-call tg-side state shared by Caller and Callee type callBase struct { adapter *Adapter userID int64 isVideo bool // CallerSide / CalleeSide - lets ntg callbacks fire MediaConnected per-side side signaling.Side mu sync.Mutex bridge *signaling.Bridge callID int32 } // must run before any callback that re-enters the bridge func (b *callBase) Bind(br *signaling.Bridge) { b.mu.Lock() b.bridge = br b.mu.Unlock() } func (b *callBase) getBridge() *signaling.Bridge { b.mu.Lock() defer b.mu.Unlock() return b.bridge } func (b *callBase) getCallID() int32 { b.mu.Lock() defer b.mu.Unlock() return b.callID } func (b *callBase) setCallID(id int32) { b.mu.Lock() b.callID = id b.mu.Unlock() } // audio-layer view of the ntgcalls client; nil for signaling-only tests func (b *callBase) NtgClient() audio.NtgClient { return b.adapter.audioNtg } // for P2P calls this is ntgcalls' "chatId" - the remote user_id func (b *callBase) ChatID() int64 { return b.userID } // ask tdlib to discard; map removal happens in cleanup from // Manager.endCall once tdlib echoes Discarded. // // Duration matters: with 0 the call shows up as missed on other tdlib // clients of the same account. func (b *callBase) Terminate(reason signaling.TerminationReason) { callID := b.getCallID() if callID == 0 { // CreateCall never succeeded return } var duration int32 if br := b.getBridge(); br != nil { duration = int32(br.Duration().Seconds()) } b.adapter.withCallContext(func() { if _, err := b.adapter.tdlib.DiscardCall(&client.DiscardCallRequest{ CallId: callID, IsDisconnected: reason == signaling.ReasonMediaFailed, Duration: duration, IsVideo: b.isVideo, ConnectionId: 0, }); err != nil { log.WithFields(log.Fields{ "call_id": callID, "user_id": b.userID, "reason": reason, "duration": duration, }).WithError(err).Error("DiscardCall failed") } }) } // drives the ntgcalls P2P setup on tdlib's CallStateReady func (b *callBase) onReady(state *client.CallStateReady, isOutgoing bool, entry *log.Entry) { b.adapter.withCallContext(func() { a := b.adapter if err := a.ntg.CreateP2PCall(b.userID); err != nil { entry.WithError(err).Error("Failed to create P2P call") return } if err := a.ntg.SkipExchange(b.userID, state.EncryptionKey, isOutgoing); err != nil { entry.WithError(err).Error("Failed to skip exchange") _ = a.ntg.Stop(b.userID) return } rtcServers := convertCallServers(state.Servers) versions := state.Protocol.LibraryVersions if err := a.ntg.ConnectP2P(b.userID, rtcServers, versions, state.AllowP2p); err != nil { entry.WithError(err).Error("Failed to connect P2P") _ = a.ntg.Stop(b.userID) return } entry.Info("P2P connection established") }) } // drops the side from both maps and stops ntgcalls func (b *callBase) cleanup() { b.adapter.withCallContext(func() { a := b.adapter callID := b.getCallID() if callID != 0 { a.mgr.Drop(a.jid, callID) } a.dropSide(b.userID) if err := a.ntg.Stop(b.userID); err != nil { log.WithFields(log.Fields{ "call_id": callID, "user_id": b.userID, }).WithError(err).Debug("ntgcalls Stop returned error (call may not have started)") } }) }