From d6ef2b70d8862f0a9c6cc72f1a2947fa46e1ce1d Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Fri, 20 Mar 2026 18:33:46 +0530 Subject: [PATCH] feat: track UCID from SIP headers for Ozonetel disposition Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/sip-client.ts | 24 ++++++++++++++---------- src/providers/sip-provider.tsx | 7 ++++++- src/state/sip-manager.ts | 25 +++++++++++++++++++++---- src/state/sip-state.ts | 1 + 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/lib/sip-client.ts b/src/lib/sip-client.ts index 2f3e4be..5f8b9ac 100644 --- a/src/lib/sip-client.ts +++ b/src/lib/sip-client.ts @@ -11,7 +11,7 @@ export class SIPClient { constructor( private config: SIPConfig, private onConnectionChange: (status: ConnectionStatus) => void, - private onCallStateChange: (state: CallState, callerNumber?: string) => void, + private onCallStateChange: (state: CallState, callerNumber?: string, ucid?: string) => void, ) {} connect(): void { @@ -66,8 +66,10 @@ export class SIPClient { this.currentSession = session; - // Extract caller number — try multiple SIP headers - const callerNumber = this.extractCallerNumber(session); + // Extract caller number and UCID — try event request first, then session + const sipRequest = (data as any).request ?? (session as any)._request ?? null; + const callerNumber = this.extractCallerNumber(session, sipRequest); + const ucid = sipRequest?.getHeader ? sipRequest.getHeader('X-UCID') ?? null : null; // Setup audio for this session session.on('peerconnection', (e: PeerConnectionEvent) => { @@ -83,16 +85,16 @@ export class SIPClient { }); session.on('accepted', (() => { - this.onCallStateChange('active', callerNumber); + this.onCallStateChange('active', callerNumber, ucid ?? undefined); }) as CallListener); session.on('confirmed', () => { - this.onCallStateChange('active', callerNumber); + this.onCallStateChange('active', callerNumber, ucid ?? undefined); }); session.on('progress', (() => { if (session.direction === 'outgoing') { - this.onCallStateChange('ringing-out', callerNumber); + this.onCallStateChange('ringing-out', callerNumber, ucid ?? undefined); } }) as CallListener); @@ -107,7 +109,7 @@ export class SIPClient { }); if (session.direction === 'incoming') { - this.onCallStateChange('ringing-in', callerNumber); + this.onCallStateChange('ringing-in', callerNumber, ucid ?? undefined); } }); @@ -222,12 +224,14 @@ export class SIPClient { } } - private extractCallerNumber(session: RTCSession): string { + private extractCallerNumber(session: RTCSession, sipRequest?: any): string { try { - const request = session.direction === 'incoming' ? (session as any)._request : null; + const request = sipRequest ?? (session.direction === 'incoming' ? (session as any)._request : null); + console.log('[SIP] extractCallerNumber: request exists:', !!request, 'direction:', session.direction); if (request) { // Ozonetel sends the real caller number in X-CALLERNO header - const xCallerNo = request.getHeader('X-CALLERNO'); + const xCallerNo = request.getHeader ? request.getHeader('X-CALLERNO') : null; + console.log('[SIP] X-CALLERNO header:', xCallerNo); if (xCallerNo) { // Remove leading 0s or country code prefix (00919... → 919...) const cleaned = xCallerNo.replace(/^0+/, ''); diff --git a/src/providers/sip-provider.tsx b/src/providers/sip-provider.tsx index afbd930..5bbe777 100644 --- a/src/providers/sip-provider.tsx +++ b/src/providers/sip-provider.tsx @@ -8,6 +8,7 @@ import { sipIsOnHoldAtom, sipCallDurationAtom, sipCallStartTimeAtom, + sipCallUcidAtom, } from '@/state/sip-state'; import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager'; import type { SIPConfig } from '@/types/sip'; @@ -24,6 +25,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => { const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom); const [callState, setCallState] = useAtom(sipCallStateAtom); const setCallerNumber = useSetAtom(sipCallerNumberAtom); + const setCallUcid = useSetAtom(sipCallUcidAtom); const setCallDuration = useSetAtom(sipCallDurationAtom); const setCallStartTime = useSetAtom(sipCallStartTimeAtom); @@ -33,8 +35,9 @@ export const SipProvider = ({ children }: PropsWithChildren) => { setConnectionStatus, setCallState, setCallerNumber, + setCallUcid, }); - }, [setConnectionStatus, setCallState, setCallerNumber]); + }, [setConnectionStatus, setCallState, setCallerNumber, setCallUcid]); // Auto-connect SIP on mount useEffect(() => { @@ -82,6 +85,7 @@ export const useSip = () => { const [connectionStatus] = useAtom(sipConnectionStatusAtom); const [callState] = useAtom(sipCallStateAtom); const [callerNumber, setCallerNumber] = useAtom(sipCallerNumberAtom); + const [callUcid] = useAtom(sipCallUcidAtom); const [isMuted, setIsMuted] = useAtom(sipIsMutedAtom); const [isOnHold, setIsOnHold] = useAtom(sipIsOnHoldAtom); const [callDuration] = useAtom(sipCallDurationAtom); @@ -117,6 +121,7 @@ export const useSip = () => { connectionStatus, callState, callerNumber, + callUcid, isMuted, isOnHold, callDuration, diff --git a/src/state/sip-manager.ts b/src/state/sip-manager.ts index 092c5d4..74254c9 100644 --- a/src/state/sip-manager.ts +++ b/src/state/sip-manager.ts @@ -5,11 +5,13 @@ import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip'; let sipClient: SIPClient | null = null; let connected = false; let outboundPending = false; +let outboundActive = false; type StateUpdater = { setConnectionStatus: (status: ConnectionStatus) => void; setCallState: (state: CallState) => void; setCallerNumber: (number: string | null) => void; + setCallUcid: (ucid: string | null) => void; }; let stateUpdater: StateUpdater | null = null; @@ -46,21 +48,34 @@ export function connectSip(config: SIPConfig): void { sipClient = new SIPClient( config, (status) => stateUpdater?.setConnectionStatus(status), - (state, number) => { - // Auto-answer SIP when it's a bridge from our outbound Kookoo call + (state, number, ucid) => { + // Auto-answer SIP when it's a bridge from our outbound call if (state === 'ringing-in' && outboundPending) { outboundPending = false; + outboundActive = true; console.log('[SIP] Outbound bridge detected — auto-answering'); setTimeout(() => { sipClient?.answer(); - // Force active state in case SIP callbacks don't fire setTimeout(() => stateUpdater?.setCallState('active'), 300); }, 500); + // Store UCID even for outbound bridge calls + if (ucid) stateUpdater?.setCallUcid(ucid); return; } + // Don't overwrite caller number on outbound calls — it was set by click-to-call stateUpdater?.setCallState(state); - if (number !== undefined) stateUpdater?.setCallerNumber(number ?? null); + if (!outboundActive && number !== undefined) { + stateUpdater?.setCallerNumber(number ?? null); + } + + // Store UCID if provided + if (ucid) stateUpdater?.setCallUcid(ucid); + + // Reset outbound flag when call ends + if (state === 'ended' || state === 'failed') { + outboundActive = false; + } }, ); @@ -72,7 +87,9 @@ export function disconnectSip(): void { sipClient = null; connected = false; outboundPending = false; + outboundActive = false; stateUpdater?.setConnectionStatus('disconnected'); + stateUpdater?.setCallUcid(null); } export function getSipClient(): SIPClient | null { diff --git a/src/state/sip-state.ts b/src/state/sip-state.ts index 583d27c..a4f13ef 100644 --- a/src/state/sip-state.ts +++ b/src/state/sip-state.ts @@ -8,3 +8,4 @@ export const sipIsMutedAtom = atom(false); export const sipIsOnHoldAtom = atom(false); export const sipCallDurationAtom = atom(0); export const sipCallStartTimeAtom = atom(null); +export const sipCallUcidAtom = atom(null);