Compare commits

..

1 Commits

Author SHA1 Message Date
oddsix
a726aa8658 add deleteCall function to properly cleanup Ultravox session 2026-03-11 12:01:54 -04:00
10 changed files with 60 additions and 84 deletions

View File

@@ -1,10 +1,10 @@
FROM --platform=linux/amd64 node:24-alpine AS base
FROM --platform=linux/amd64 node:20-alpine as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
FROM base AS build
FROM base as build
COPY package.json package-lock.json ./

View File

@@ -18,7 +18,6 @@ const { createCallSchema, customSanitizeFunction } = require('../schemas/create-
const { selectHostPort } = require('../../utils/network');
const { JAMBONES_DIAL_SBC_FOR_REGISTERED_USER } = require('../../config');
const { createMediaEndpoint } = require('../../utils/media-endpoint');
const { DbErrorBadRequest } = require('../utils/errors');
const removeNullProperties = (obj) => (Object.keys(obj).forEach((key) => obj[key] === null && delete obj[key]), obj);
const removeNulls = (req, res, next) => {
@@ -123,16 +122,8 @@ router.post('/',
}
break;
case 'user':
let targetName = target.name;
if (!targetName.includes('@')) {
if (!account.sip_realm) {
throw new DbErrorBadRequest('no sip realm configured for account');
}
logger.debug(`appending sip realm ${account.sip_realm} to target name ${targetName}`);
targetName = `${targetName}@${account.sip_realm}`;
}
uri = `sip:${targetName}`;
to = targetName;
uri = `sip:${target.name}`;
to = target.name;
if (target.overrideTo) {
Object.assign(opts.headers, {
'X-Override-To': target.overrideTo
@@ -340,12 +331,6 @@ router.post('/',
}
});
connectStream(dlg.remote.sdp);
/* ensure sbcCallid is set even if no provisional response was received */
if (!cs.callInfo.sbcCallid && dlg.res.has('X-CID')) {
cs.callInfo.sbcCallid = dlg.res.get('X-CID');
}
cs.emit('callStatusChange', {
callStatus: CallStatus.InProgress,
sipStatus: 200,

View File

@@ -6,8 +6,8 @@ function sysError(logger, res, err) {
return res.status(400).json({msg: err.message});
}
if (err instanceof DbErrorUnprocessableRequest) {
logger.info({message: err.message}, 'unprocessable request');
return res.status(422).send(err.message);
logger.info(err, 'unprocessable request');
return res.status(422).json({msg: err.message});
}
if (err.code === 'ER_DUP_ENTRY') {
logger.info(err, 'duplicate entry on insert');

View File

@@ -2315,8 +2315,7 @@ Duration=${duration} `
break;
case 'mute:status':
const status = typeof (data) === 'string' ? data : data.mute_status;
this._lccMuteStatus(status === 'mute', call_sid);
this._lccMuteStatus(data, call_sid);
break;
case 'conf:mute-status':
@@ -2476,7 +2475,7 @@ Duration=${duration} `
this.logger.info(`allocated endpoint ${ep.uuid}`);
this.ep.on('destroy', () => {
this.logger.debug(`endpoint was destroyed!! ${this.ep?.uuid}`);
this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`);
});
if (this.direction === CallDirection.Inbound || this.application?.transferredCall) {
@@ -2785,7 +2784,6 @@ Duration=${duration} `
}
} catch (err) {
this.logger.error(err, 'Error handling reinvite');
res.send(err.status || 500);
}
}

View File

@@ -469,7 +469,6 @@ class Conference extends Task {
assert (cs.isInConference);
const mute = opts.conf_mute_status === 'mute';
this.logger.info(`Conference:doConferenceMute ${mute ? 'muting' : 'unmuting'} member`);
this.ep.api(`conference ${this.confName} ${mute ? 'mute' : 'unmute'} ${this.memberId}`)
.catch((err) => this.logger.info({err}, 'Error muting or unmuting participant'));
}
@@ -571,8 +570,8 @@ class Conference extends Task {
/**
* mute or unmute side of the call
*/
async mute(callSid, doMute) {
this.doConferenceMute(this.callSession, {conf_mute_status: doMute ? 'mute' : 'unmute'});
mute(callSid, doMute) {
this.doConferenceMute(this.callSession, {conf_mute_status: doMute});
}
/**

View File

@@ -1424,26 +1424,7 @@ class TaskGather extends SttTask {
returnedVerbs = await this.performAction({speech:evt, reason: 'stt-low-confidence', ...latencies});
}
}
} catch (err) {
this.logger.info({err}, 'TaskGather:_resolve - error performing action');
this.notifyError({msg: 'invalid actionHook response', details: err.message});
const {writeAlerts, AlertType} = this.cs.srf.locals;
writeAlerts({
account_sid: this.cs.accountSid,
alert_type: AlertType.INVALID_APP_PAYLOAD,
target_sid: this.cs.callSid,
message: `actionHook returned invalid verb syntax: ${err.message}`
}).catch((err) => this.logger.info({err}, 'TaskGather:_resolve - error generating alert'));
try {
const obj = Object.assign({}, this.cs.callInfo.toJSON(), {
error: 'invalid actionHook response',
reason: err.message
});
await this.cs.notifier.request('call:status', this.cs.call_status_hook, obj);
} catch (statusErr) {
this.logger.info({statusErr}, 'TaskGather:_resolve - error sending statusHook');
}
}
} catch (err) { /*already logged error*/ }
// Gather got response from hook, cancel actionHookDelay processing
if (this.cs.actionHookDelayProcessor) {

View File

@@ -146,6 +146,23 @@ class TaskLlmUltravox_S2S extends Task {
return data;
}
async deleteCall() {
if (!this.callId) return;
const baseUrl = 'https://api.ultravox.ai';
const url = `${baseUrl}/api/calls/${this.callId}`;
try {
const {statusCode} = await request(url, {
method: 'DELETE',
headers: {
'X-API-Key': this.apiKey
}
});
this.logger.debug({statusCode, callId: this.callId}, 'Ultravox call deleted');
} catch (err) {
this.logger.info({err, callId: this.callId}, 'TaskLlmUltravox_S2S:deleteCall - error');
}
}
_unregisterHandlers(ep) {
this.removeCustomEventListeners();
ep.removeAllListeners('dtmf');
@@ -164,6 +181,7 @@ class TaskLlmUltravox_S2S extends Task {
try {
const data = await this.createCall();
this.callId = data.callId;
const {joinUrl} = data;
// split the joinUrl into host and path
const {host, pathname, search} = new URL(joinUrl);
@@ -188,6 +206,8 @@ class TaskLlmUltravox_S2S extends Task {
await this.awaitTaskDone();
await this.deleteCall();
/* note: the parent llm verb started the span, which is why this is necessary */
await this.parent.performAction(this.results);
@@ -200,6 +220,8 @@ class TaskLlmUltravox_S2S extends Task {
this._api(cs.ep, [cs.ep.uuid, SessionDelete])
.catch((err) => this.logger.info({err}, 'TaskLlmUltravox_S2S:kill - error deleting session'));
this.deleteCall();
this.notifyTaskDone();
}

View File

@@ -234,11 +234,6 @@ class SingleDialer extends Emitter {
await connectStream(this.dlg.remote.sdp, opts.isVideoCall);
this.dlg.callSid = this.callSid;
this.inviteInProgress = null;
/* ensure sbcCallid is set even if no provisional response was received */
if (!this.callInfo.sbcCallid && this.dlg.res.has('X-CID')) {
this.callInfo.sbcCallid = this.dlg.res.get('X-CID');
}
this.emit('callStatusChange', {
sipStatus: 200,
sipReason: 'OK',
@@ -295,7 +290,6 @@ class SingleDialer extends Emitter {
}
} catch (err) {
this.logger.error(err, 'Error handling reinvite');
res.send(err.status || 500);
}
})
.on('refer', (req, res) => {

47
package-lock.json generated
View File

@@ -30,21 +30,21 @@
"@opentelemetry/sdk-trace-node": "^1.23.0",
"@opentelemetry/semantic-conventions": "^1.23.0",
"bent": "^7.3.12",
"debug": "^4.4.3",
"debug": "^4.3.4",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^4.1.2",
"drachtio-srf": "^5.0.20",
"express": "^4.22.1",
"express": "^4.19.2",
"express-validator": "^7.0.1",
"moment": "^2.30.1",
"parse-url": "^9.2.0",
"pino": "^10.3.1",
"pino": "^10.1.0",
"polly-ssml-split": "^0.1.0",
"sdp-transform": "^2.15.0",
"short-uuid": "^5.1.0",
"sinon": "^17.0.1",
"to-snake-case": "^1.0.0",
"undici": "^7.24.5",
"undici": "^7.5.0",
"verify-aws-sns-signature": "^0.1.0",
"ws": "^8.18.0",
"xml2js": "^0.6.2"
@@ -58,7 +58,7 @@
"tape": "^5.7.5"
},
"engines": {
"node": ">= 20.x"
"node": ">= 18.x"
},
"optionalDependencies": {
"bufferutil": "^4.0.8",
@@ -9015,31 +9015,31 @@
"license": "ISC"
},
"node_modules/pino": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
"integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz",
"integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==",
"license": "MIT",
"dependencies": {
"@pinojs/redact": "^0.4.0",
"atomic-sleep": "^1.0.0",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^3.0.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^4.0.0"
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
@@ -10586,15 +10586,12 @@
"license": "MIT"
},
"node_modules/thread-stream": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
"integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/thriftrw": {
@@ -10860,9 +10857,9 @@
}
},
"node_modules/undici": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz",
"integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==",
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz",
"integrity": "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -10965,9 +10962,9 @@
}
},
"node_modules/utf-8-validate": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz",
"integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.5.tgz",
"integrity": "sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,

View File

@@ -3,7 +3,7 @@
"version": "0.9.5",
"main": "app.js",
"engines": {
"node": ">= 20.x"
"node": ">= 18.x"
},
"keywords": [
"sip",
@@ -46,21 +46,21 @@
"@opentelemetry/sdk-trace-node": "^1.23.0",
"@opentelemetry/semantic-conventions": "^1.23.0",
"bent": "^7.3.12",
"debug": "^4.4.3",
"debug": "^4.3.4",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^4.1.2",
"drachtio-srf": "^5.0.20",
"express": "^4.22.1",
"express": "^4.19.2",
"express-validator": "^7.0.1",
"moment": "^2.30.1",
"parse-url": "^9.2.0",
"pino": "^10.3.1",
"pino": "^10.1.0",
"polly-ssml-split": "^0.1.0",
"sdp-transform": "^2.15.0",
"short-uuid": "^5.1.0",
"sinon": "^17.0.1",
"to-snake-case": "^1.0.0",
"undici": "^7.24.5",
"undici": "^7.5.0",
"verify-aws-sns-signature": "^0.1.0",
"ws": "^8.18.0",
"xml2js": "^0.6.2"