import type { MediaCallSignedContact, IMediaCall, MediaCallContactInformation, MediaCallContact } from '@rocket.chat/core-typings'; import { isBusyState, type ClientMediaSignalBody } from '@rocket.chat/media-signaling'; import { MediaCallNegotiations, MediaCalls } from '@rocket.chat/models'; import type { SipMessage, SrfRequest, SrfResponse } from 'drachtio-srf'; import type Srf from 'drachtio-srf'; import { BaseSipCall } from './BaseSipCall'; import { SIP_CALL_FEATURES } from '../../constants'; import { logger } from '../../logger'; import { BroadcastActorAgent } from '../../server/BroadcastAgent'; import { mediaCallDirector } from '../../server/CallDirector'; import { getMediaCallServer } from '../../server/injection'; import type { SipServerSession } from '../Session'; import { SipError, SipErrorCodes } from '../errorCodes'; import { parseDiversionHeader } from '../utils/parseDiversionHeader'; export class IncomingSipCall extends BaseSipCall { constructor( session: SipServerSession, call: IMediaCall, protected override readonly agent: BroadcastActorAgent, private readonly srf: Srf, private readonly req: SrfRequest, private readonly res: SrfResponse, ) { super(session, call, agent); } public static async processInvite(session: SipServerSession, srf: Srf, req: SrfRequest, res: SrfResponse): Promise { logger.debug({ msg: 'IncomingSipCall.processInvite' }); if (!req.isNewInvite) { logger.error({ msg: 'IncomingSipCall.processInvite received a request that is not a new invite.' }); throw new SipError(SipErrorCodes.NOT_IMPLEMENTED, 'not-a-new-invite'); } let sipCall: IncomingSipCall | null = null; req.on('cancel', (message) => { sipCall?.cancel(message); }); const callee = await this.getCalleeFromInvite(req); logger.debug({ msg: 'incoming call to', callee }); // getCalleeFromInvite already ensures it, but let's safeguard that the callee is an internal user if (callee.type !== 'user' || !callee.id) { throw new SipError(SipErrorCodes.TEMPORARILY_UNAVAILABLE); } // User is literally busy if (await MediaCalls.hasUnfinishedCallsByUid(callee.id)) { throw new SipError(SipErrorCodes.TEMPORARILY_UNAVAILABLE); } if (!(await getMediaCallServer().permissionCheck(callee.id, 'external'))) { logger.debug({ msg: 'User with no permission received a sip call.', uid: callee.id }); throw new SipError(SipErrorCodes.TEMPORARILY_UNAVAILABLE); } const caller = await this.getCallerContactFromInvite(session.sessionId, req); logger.debug({ msg: 'incoming call from', callerContact: caller }); const divertedBy = await this.getDiversionContactFromInvite(req); if (divertedBy) { logger.debug({ msg: 'incoming call diverted by', divertedBy }); } const webrtcOffer = { type: 'offer', sdp: req.body } as const; const callerAgent = await mediaCallDirector.cast.getAgentForActorAndRole(caller, 'caller'); if (!callerAgent) { throw new SipError(SipErrorCodes.NOT_FOUND, 'Caller agent not found'); } if (!(callerAgent instanceof BroadcastActorAgent)) { throw new SipError(SipErrorCodes.INTERNAL_SERVER_ERROR, 'Caller agent not valid'); } const calleeAgent = await mediaCallDirector.cast.getAgentForActorAndRole(callee, 'callee'); if (!calleeAgent) { throw new SipError(SipErrorCodes.NOT_FOUND, 'Callee agent not found'); } const call = await mediaCallDirector.createCall({ caller, callee, callerAgent, calleeAgent, features: SIP_CALL_FEATURES, ...(divertedBy && { divertedBy }), sipCallId: req.get('Call-ID'), }); const negotiationId = await mediaCallDirector.startNewNegotiation(call, 'caller', webrtcOffer); sipCall = new IncomingSipCall(session, call, callerAgent, srf, req, res); callerAgent.provider = sipCall; sipCall.inboundRenegotiations.set(negotiationId, { id: negotiationId, req, res, isFirst: true, offer: webrtcOffer, answer: null, }); // Send the call to the callee client await calleeAgent.onCallCreated(call); return sipCall; } public async createDialog(localSdp: string): Promise { logger.debug({ msg: 'IncomingSipCall.createDialog' }); const uas = await this.srf.createUAS(this.req, this.res, { localSdp, }); if (!uas) { logger.error({ msg: 'IncomingSipCall.createDialog - dialog creation failed', callId: this.callId }); this.hangupCall('signaling-error'); return; } uas.on('modify', (req, res) => { void this.handleDialogModify(req, res); }); uas.on('destroy', () => this.onDialogDestroyed()); this.sipDialog = uas; } protected cancel(res: SipMessage): void { logger.debug({ msg: 'IncomingSipCall.cancel', res: this.session.stripDrachtioServerDetails(res) }); logger.info({ msg: 'The incoming SIP call was canceled by the caller', callId: this.callId }); this.hangupCall('remote'); } protected async reflectCall(call: IMediaCall, params: { dtmf?: ClientMediaSignalBody<'dtmf'> }): Promise { if (params.dtmf && this.sipDialog) { return this.sendDTMF(this.sipDialog, params.dtmf.dtmf, params.dtmf.duration || 2000); } if (call.transferredTo && call.transferredBy) { return this.processTransferredCall(call); } if (call.ended) { return this.processEndedCall(call); } if (isBusyState(call.state)) { return this.processNegotiations(call); } logger.debug({ msg: 'no changes detected', method: 'IncomingSipCall.reflectCall' }); } protected async processEndedCall(call: IMediaCall): Promise { logger.debug({ msg: 'IncomingSipCall.processEndedCall', lastCallState: this.lastCallState, hangupReason: call.hangupReason }); switch (call.hangupReason) { case 'service-error': this.cancelPendingInvites(SipErrorCodes.NOT_ACCEPTABLE_HERE); break; case 'rejected': this.cancelPendingInvites(SipErrorCodes.DECLINED); break; default: this.cancelPendingInvites(SipErrorCodes.TEMPORARILY_UNAVAILABLE); break; } if (this.lastCallState === 'hangup') { return; } const { sipDialog } = this; this.sipDialog = null; this.lastCallState = 'hangup'; if (sipDialog) { try { await sipDialog.destroy(); } catch (err) { logger.error({ msg: 'Failed to destroy SIP dialog', err, method: 'IncomingSipCall.processEndedCall' }); } } } protected override onDialogModifyError(_negotiationId: string): void { // On an incoming call the SIP caller drives the call, so an unrecoverable renegotiation must end it. this.hangupPendingCall(SipErrorCodes.INTERNAL_SERVER_ERROR); } private async processNegotiations(call: IMediaCall): Promise { const localNegotiation = await this.getPendingInboundNegotiation(); if (!localNegotiation) { // Callee-initiated renegotiations are only processed if there's none initiated by the caller return this.processCalleeNegotiation(call); } // If we don't have an sdp, we can't respond to it yet if (!localNegotiation?.answer?.sdp) { return; } logger.debug('IncomingSipCall.processNegotiations'); if (localNegotiation.isFirst) { return this.createDialog(localNegotiation.answer.sdp).catch(() => { logger.error('Failed to create incoming call dialog.'); this.hangupPendingCall(SipErrorCodes.INTERNAL_SERVER_ERROR); }); } localNegotiation.res.send(200, { body: localNegotiation.answer.sdp, }); } private async processCalleeNegotiation(call: IMediaCall): Promise { if (!this.sipDialog) { return; } const negotiation = await MediaCallNegotiations.findLatestByCallId(call._id); if (negotiation?.offerer !== 'callee' || !negotiation.offer?.sdp || negotiation.answer) { return; } logger.debug('IncomingSipCall.processCalleeNegotiation'); let answer: string | void = undefined; try { answer = await this.sipDialog.modify(negotiation.offer.sdp).catch(() => { logger.debug('modify failed'); }); } catch (err) { logger.error({ msg: 'Error on IncomingSipCall.processCalleeNegotiation', err }); } if (!answer) { logger.error({ msg: 'No answer from callee initiated negotiation' }); return; } await mediaCallDirector.saveWebrtcSession( call, this.agent, { sdp: { sdp: answer, type: 'answer' }, negotiationId: negotiation._id }, this.session.sessionId, ); } private cancelPendingInvites(errorCode: number): void { logger.debug({ msg: 'IncomingSipCall.cancelPendingInvites', errorCode, hasDialog: Boolean(this.sipDialog), negotiations: this.inboundRenegotiations.size, }); for (const localNegotiation of this.inboundRenegotiations.values()) { // if it has an answer sdp, we already responded to it if (localNegotiation.answer?.sdp) { continue; } try { localNegotiation.res.send(errorCode); } catch { // } } this.inboundRenegotiations.clear(); } private hangupPendingCall(errorCode: number): void { logger.debug('IncomingSipCall.hangupPendingCall'); this.cancelPendingInvites(errorCode); this.hangupCall('signaling-error'); } private static async getCalleeFromInvite(req: SrfRequest): Promise { logger.debug({ msg: 'IncomingSipCall.getCalleeFromInvite', callingNumber: req.callingNumber, calledNumber: req.calledNumber }); if (req.calledNumber && typeof req.calledNumber === 'string') { const userContact = await mediaCallDirector.cast.getContactForExtensionNumber(req.calledNumber, { requiredType: 'user' }); if (userContact) { return userContact; } // If the invite had an id/extension but we couldn't match it to an user, respond with unavailable throw new SipError(SipErrorCodes.TEMPORARILY_UNAVAILABLE); } // If we couldn't even identify an id/extension from the invite, respond with not found throw new SipError(SipErrorCodes.NOT_FOUND); } private static async getDiversionContactFromInvite(req: SrfRequest): Promise { if (!req.has('diversion')) { return null; } logger.debug({ msg: 'IncomingSipCall.getDiversionContactFromInvite' }); const parsed = parseDiversionHeader(req.get('diversion')); if (!parsed) { logger.warn({ msg: 'IncomingSipCall.getDiversionContactFromInvite: failed to parse Diversion header', raw: req.get('diversion') }); return null; } const { extension, displayName } = parsed; const defaultContactInfo: MediaCallContactInformation = { sipExtension: extension, displayName: displayName || extension, }; const contact = await mediaCallDirector.cast.getContactForExtensionNumber(extension, { requiredType: 'sip' }, defaultContactInfo); return { ...defaultContactInfo, ...contact, type: 'sip', id: extension, }; } private static async getCallerContactFromInvite(sessionId: string, req: SrfRequest): Promise> { logger.debug({ msg: 'IncomingSipCall.getCallerContactFromInvite' }); const displayName = req.callingName || undefined; const sipExtension = req.callingNumber; const defaultContactInfo: MediaCallContactInformation = { sipExtension, displayName: displayName || sipExtension, }; const contact = await mediaCallDirector.cast.getContactForExtensionNumber(sipExtension, { requiredType: 'sip' }, defaultContactInfo); if (contact) { return { ...contact, contractId: sessionId, } as MediaCallSignedContact<'sip'>; } return { type: 'sip', id: sipExtension, contractId: sessionId, ...defaultContactInfo, }; } }