Compare commits

...

71 Commits

Author SHA1 Message Date
Dave Horton
ba32f1ea05 bugfix: transferring queued party to dequeuer on other FS fails if only 1 task 2021-06-28 16:39:44 -04:00
Dave Horton
7de016589b bugfix: sns notifications do not require aws secrets in the env 2021-06-26 18:08:35 -04:00
Dave Horton
9b59d08dcf merge features from hosted branch (#32)
major merge of features from the hosted branch that was created temporarily during the initial launch of jambonz.org
2021-06-17 16:25:50 -04:00
Dave Horton
473a34ec9f update to latest drachtio-srf@4.4.50 for fix for 302 redirect 2021-06-03 09:42:37 -04:00
Dave Horton
686cf1b094 fix snake-case of arrays 2021-05-07 16:29:40 -04:00
Dave Horton
5cc4852bf9 snakecase fix, include sip_status in dial action hook 2021-04-27 08:21:14 -04:00
Dave Horton
576f645489 snake case REST payloads, support for LCC with child_call_hook, handle 302 on outdial 2021-04-22 14:39:54 -04:00
Dave Horton
8eb0cd1520 bugfix: speech to text was ignoring language and setting to en-US always 2021-04-07 18:40:14 -04:00
Dave Horton
e441c5be36 add support for target.overrideTo in dial verb 2021-04-06 07:34:23 -04:00
Dave Horton
dd48b5c9da update y18n 2021-03-31 07:54:41 -04:00
Dave Horton
c6168ce994 add reason property to gather action 2021-02-23 08:10:31 -05:00
Dave Horton
70e4e10a70 dialogflow tts fix and gather fix 2021-02-21 11:30:55 -05:00
Dave Horton
82768a0442 update call uses a PUT now, not POST 2021-02-19 08:48:50 -05:00
Dave Horton
8b3ffe911d bugfix in dialogflow 2021-02-18 12:59:05 -05:00
Dave Horton
a7e0fb2e8a bugfix: dep in bluebird was causing issue, update to latest synthAudio 2021-02-10 09:39:20 -05:00
Dave Horton
f8e84b5ad0 remove some unused deps 2021-02-08 15:44:09 -05:00
Dave Horton
0cff553310 update to latest realtimedb-helpers 2021-02-08 15:36:30 -05:00
Dave Horton
873729edb1 gather now supports aws for transcribe as well as google 2021-02-01 10:21:52 -05:00
Dave Horton
756db59671 update transcribe to support google v1p1beta1 and aws 2021-01-31 15:49:19 -05:00
Dave Horton
59d685319e bugfix #30 - outdial race condition for quick caller cancel scenario 2021-01-22 10:21:52 -05:00
Dave Horton
ec7a1858d6 dialogflow: clear no input timer on caller hangup 2021-01-14 08:58:18 -05:00
Dave Horton
63a00063c1 dialogflow: can optionally specify an environment 2021-01-13 21:21:26 -05:00
Dave Horton
2a8f165468 travis no longer needed -- using github actions 2021-01-08 14:07:18 -05:00
Dave Horton
d3f8e032d1 dialogflow: finish playing a final prompt before replacing application 2021-01-08 14:06:28 -05:00
Dave Horton
a1054d2d38 Merge pull request #28 from radicaldrew/master
Updated Dockerfile
2020-12-30 08:50:58 -05:00
Andrew
fa87a477ac Updated Dockerfile
created a multi stage build and tested in docker environment with compose
2020-12-30 15:34:34 +02:00
Dave Horton
69349dab75 Merge pull request #27 from radicaldrew/master
fixed uuid4 dependency and deprecation
2020-12-23 07:36:48 -05:00
Andrew Karp
b679d11fd7 fixed uui4 dependency and depraction 2020-12-23 13:20:56 +02:00
Dave Horton
ea8609b8c3 minor docs 2020-12-16 13:34:38 -05:00
Dave Horton
ef17ed40f7 include X-Account-Sid on all outgoing INVITEs 2020-12-16 13:27:02 -05:00
Dave Horton
5c5c9d9ae2 docs typo 2020-12-13 16:36:12 -05:00
Dave Horton
6e32d82364 change build status in README to github actions 2020-12-13 16:32:51 -05:00
Dave Horton
bfd8355432 Update npm-publish.yml
change name
2020-12-13 16:29:50 -05:00
Dave Horton
1a29d48334 Create npm-publish.yml 2020-12-13 16:24:02 -05:00
Dave Horton
4d6ef8e334 update deps 2020-12-13 14:27:43 -05:00
Dave Horton
cac259ec1c update to stats-collector that reconnects when socket dropped 2020-12-11 14:43:11 -05:00
Dave Horton
1bc583e805 allow dial to user without supplying sip_realm (will default to that configured for the caller account) 2020-11-29 15:00:42 -05:00
Dave Horton
16c728e246 bugfix for REST outdial to teams 2020-11-24 10:12:19 -05:00
Dave Horton
25c3512e41 lex changes 2020-11-23 09:08:48 -05:00
Dave Horton
5291824501 bugfix: sip:decline was not sending a callstatus Failed webhook 2020-11-23 09:04:22 -05:00
Dave Horton
5f908492d7 deps 2020-10-26 12:02:28 -04:00
Dave Horton
1f32170788 Merge pull request #24 from jambonz/aws-lex
Aws lex
2020-10-26 10:10:05 -04:00
Dave Horton
bd9c7b741d lex changes 2020-10-26 10:02:39 -04:00
Dave Horton
b47e490424 updates for lex v2 2020-10-26 09:59:10 -04:00
Dave Horton
6b63009707 update deps 2020-10-12 10:03:17 -04:00
Dave Horton
91f507bf3f add dmtf verb 2020-10-12 09:59:50 -04:00
Dave Horton
9d3c9accb9 update drachtio-fsrmf 2020-10-09 08:40:39 -04:00
Dave Horton
95e4c22969 add lex support 2020-10-09 08:28:36 -04:00
Dave Horton
c02aa94500 add sms messaging support 2020-10-09 08:00:17 -04:00
Dave Horton
950f1c83b7 bugfix for race condition where incoming call canceled quickly leading to potential endless loop 2020-10-01 12:46:58 -04:00
Dave Horton
e642e13946 bugfix for #22: headers were being incorrectly applied to follow-on INVITEs 2020-09-21 08:29:54 -04:00
Dave Horton
8f65b0de2f bugfix #21: multiple teams target 2020-09-18 09:13:30 -04:00
Dave Horton
e1528da8b1 bugfix #21: multiple teams target 2020-09-18 09:12:31 -04:00
Dave Horton
7abc7866dd add tts option for playing dialogflow audio 2020-09-02 12:14:53 -04:00
Dave Horton
868427216f bump deps 2020-08-18 14:57:34 -04:00
Dave Horton
2c8c161954 fix overlapping requests to freeswitch on outdial 2020-08-03 11:51:58 -04:00
Dave Horton
884e63e0ef allow proxy in dial verb 2020-07-24 15:33:06 -04:00
Dave Horton
3624b05eb6 bugfix: gather can only resolve once 2020-07-20 08:58:37 -04:00
Dave Horton
b739737c29 deps 2020-07-16 16:16:37 -04:00
Dave Horton
15517828d2 typo 2020-07-13 09:58:42 -04:00
Dave Horton
490472ca68 bugfix dialogflow no input timeout 2020-07-10 12:02:11 -04:00
Dave Horton
565ad2948c bugfix dialogflow no input event 2020-07-10 11:50:21 -04:00
Dave Horton
31bed8afbd dialogflow: allow app to specify event to send in case on no input 2020-07-10 11:33:48 -04:00
Dave Horton
6e78b46674 more dialogflow changes 2020-07-08 16:07:49 -04:00
Dave Horton
a4bcfca9e6 added initial support for dialogflow 2020-07-08 14:16:37 -04:00
Dave Horton
c1112ea477 bugfix: synthesizer properties in the say verb were being ignored 2020-06-18 10:00:31 -04:00
Dave Horton
4e4ce0914e bugfix: say text continued to play after task was killed 2020-06-16 14:39:46 -04:00
Dave Horton
1dc4728574 update to use @jambonz modules 2020-06-10 18:33:39 -04:00
Dave Horton
d7eeb52a84 bugfix: tts was only being cached when same prompt played in a single call 2020-06-10 10:23:32 -04:00
Dave Horton
f3fcdfc481 update to drachtio-srf@4.4.34 2020-06-08 14:26:30 -04:00
Dave Horton
cd58d4a4f0 additional teams changes 2020-06-06 08:28:26 -04:00
85 changed files with 11394 additions and 2133 deletions

View File

@@ -8,7 +8,7 @@
"jsx": false,
"modules": false
},
"ecmaVersion": 2017
"ecmaVersion": 2018
},
"plugins": ["promise"],
"rules": {

22
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: CI
on:
push:
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
- run: npm ci
- run: npm run jslint
- run: docker pull drachtio/sipp
- run: npm test
env:
GCP_JSON_KEY: ${{ secrets.GCP_JSON_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}

5
.gitignore vendored
View File

@@ -37,4 +37,7 @@ node_modules
examples/*
ecosystem.config.js
ecosystem.config.js
.vscode
test/credentials/*.json
run-tests.sh

View File

@@ -1,6 +0,0 @@
sudo: required
language: node_js
node_js:
- "lts/*"
script:
- npm test

View File

@@ -1,13 +1,16 @@
FROM node:lts-alpine
FROM node:alpine as builder
RUN apk update && apk add --no-cache python make g++
WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm prune
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
FROM node:alpine as app
WORKDIR /opt/app
COPY . /opt/app
COPY --from=builder /opt/app/node_modules ./node_modules
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
COPY package.json /usr/src/app/
RUN npm install
COPY . /usr/src/app
CMD [ "npm", "start" ]

144
README.md
View File

@@ -1,78 +1,86 @@
# jambones-feature-server [![Build Status](https://secure.travis-ci.org/jambonz/jambones-feature-server.png)](http://travis-ci.org/jambonz/jambones-feature-server)
# jambones-feature-server ![Build Status](https://github.com/jambonz/jambonz-feature-server/workflows/CI/badge.svg)
This application implements the core feature server of the jambones platform.
## Configuration
Configuration is provided via the [npmjs config](https://www.npmjs.com/package/config) package. The following elements make up the configuration for the application:
##### drachtio server location
```
{
"drachtio": {
"port": 3001,
"secret": "cymru"
},
```
the `drachtio` object specifies the port to listen on for tcp connections from drachtio servers as well as the shared secret that is used to authenticate to the server.
Configuration is provided via environment variables:
> Note: either inbound or [outbound connections](https://drachtio.org/docs#outbound-connections) may be used, depending on the configuration supplied. In production, it is the intent to use outbound connections for easier centralization and clustering of application logic.
| variable | meaning | required?|
|----------|----------|---------|
|AWS_ACCESS_KEY_ID| aws access key id, used for TTS/STT as well SNS notifications|no|
|AWS_REGION| aws region| no|
|AWS_SECRET_ACCESS_KEY| aws secret access key, used per above|no|
|AWS_SNS_TOPIC_ARM| aws sns topic arn that scale-in lifecycle notifications will be published to|no|
|DRACHTIO_HOST| ip address of drachtio server (typically '127.0.0.1')|yes|
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|DRACHTIO_SECRET| shared secret|yes|
|ENABLE_METRICS| if 1, metrics will be generated|no|
|GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes|
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes|
|JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes|
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|JAMBONES_MYSQL_HOST| mysql host|yes|
|JAMBONES_MYSQL_USER| mysql username|yes|
|JAMBONES_MYSQL_PASSWORD| mysql password|yes|
|JAMBONES_MYSQL_DATABASE| mysql data|yes|
|JAMBONES_MYSQL_CONNECTION_LIMIT| mysql connection limit |no|
|JAMBONES_NETWORK_CIDR| CIDR of private network that feature server is running in (e.g. '172.31.0.0/16')|yes|
|JAMBONES_REDIS_HOST| redis host|yes|
|JAMBONES_REDIS_PORT|redis port|yes|
|JAMBONES_SBCS| list of IP addresses (on the internal network) of SBCs, comma-separated|yes|
|STATS_HOST| ip address of metrics host (usually '127.0.0.1' since telegraf is installed locally|no|
|STATS_PORT| listening port for metrics host|no|
|STATS_PROTOCOL| 'tcp' or 'udp'|no|
|STATS_TELEGRAF| if 1, metrics will be generated in telegraf format|no|
##### freeswitch location
```
"freeswitch: {
"address": "127.0.0.1",
"port": 8021,
"secret": "ClueCon"
},
```
the `freeswitch` property specifies the location of the freeswitch server to use for media handling.
##### application log level
```
"logging": {
"level": "info"
}
```
##### mysql server location
Login credentials for the mysql server databas.
```
"mysql": {
"host": "127.0.0.1",
"user": "jambones",
"password": "jambones",
"database": "jambones"
}
```
##### redis server location
Login credentials for the redis server databas.
```
"redis": {
"host": "127.0.0.1",
"port": 6379
}
```
##### port to listen on for HTTP API requests
The HTTP listen port can be set by the `HTTP_PORT` environment variable, but it not set the default port will be taken from the configuration file.
```
"defaultHttpPort": 3000,
```
##### REST-initiated outdials
When an outdial is triggered via the REST API, the application needs to select a drachtio sip server to generate the INVITE, and it needs to know the IP addresses of the SBC(s) to send the outbound call through. Both are provided as arrays in the configuration file, and if more than one is supplied they will be used in a round-robin fashion.
```
"outdials": {
"drachtio": [
{
"host": "127.0.0.1",
"port": 9022,
"secret": "cymru"
}
],
"sbc": ["127.0.0.1:5060"]
}
### running under pm2
Typically, this application runs under [pm2](https://pm2.io) using an [ecosystem.config.js](https://pm2.keymetrics.io/docs/usage/application-declaration/) file similar to this:
```js
module.exports = {
apps : [
{
name: 'jambonz-feature-server',
cwd: '/home/admin/apps/jambonz-feature-server',
script: 'app.js',
instance_var: 'INSTANCE_ID',
out_file: '/home/admin/.pm2/logs/jambonz-feature-server.log',
err_file: '/home/admin/.pm2/logs/jambonz-feature-server.log',
exec_mode: 'fork',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
GOOGLE_APPLICATION_CREDENTIALS: '/home/admin/credentials/gcp.json',
AWS_ACCESS_KEY_ID: 'XXXXXXXXXXXX',
AWS_SECRET_ACCESS_KEY: 'YYYYYYYYYYYYYYYYYYYYY',
AWS_REGION: 'us-west-1',
ENABLE_METRICS: 1,
STATS_HOST: '127.0.0.1',
STATS_PORT: 8125,
STATS_PROTOCOL: 'tcp',
STATS_TELEGRAF: 1,
AWS_SNS_TOPIC_ARM: 'arn:aws:sns:us-west-1:xxxxxxxxxxx:terraform-20201107200347128600000002',
JAMBONES_NETWORK_CIDR: '172.31.0.0/16',
JAMBONES_MYSQL_HOST: 'aurora-cluster-jambonz.cluster-yyyyyyyyyyy.us-west-1.rds.amazonaws.com',
JAMBONES_MYSQL_USER: 'admin',
JAMBONES_MYSQL_PASSWORD: 'foobarbz',
JAMBONES_MYSQL_DATABASE: 'jambones',
JAMBONES_MYSQL_CONNECTION_LIMIT: 10,
JAMBONES_REDIS_HOST: 'jambonz.zzzzzzz.0001.usw1.cache.amazonaws.com',
JAMBONES_REDIS_PORT: 6379,
JAMBONES_LOGLEVEL: 'debug',
HTTP_PORT: 3000,
DRACHTIO_HOST: '127.0.0.1',
DRACHTIO_PORT: 9022,
DRACHTIO_SECRET: 'sharedsecret',
JAMBONES_SBCS: '172.31.32.10',
JAMBONES_FREESWITCH: '127.0.0.1:8021:sharedsecret'
}
}]
};
```
#### Running the test suite

29
app.js
View File

@@ -12,9 +12,10 @@ assert.ok(process.env.JAMBONES_NETWORK_CIDR, 'missing JAMBONES_SUBNET env var');
const Srf = require('drachtio-srf');
const srf = new Srf();
const PORT = process.env.HTTP_PORT || 3000;
const opts = Object.assign({
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
}, {level: process.env.JAMBONES_LOGLEVEL || 'info'});
const opts = {
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;},
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const logger = require('pino')(opts);
const {LifeCycleEvents} = require('./lib/utils/constants');
const installSrfLocals = require('./lib/utils/install-srf-locals');
@@ -22,6 +23,7 @@ installSrfLocals(srf, logger);
const {
initLocals,
getAccountDetails,
normalizeNumbers,
retrieveApplication,
invokeWebCallback
@@ -57,7 +59,13 @@ if (process.env.NODE_ENV === 'test') {
});
}
srf.use('invite', [initLocals, normalizeNumbers, retrieveApplication, invokeWebCallback]);
srf.use('invite', [
initLocals,
getAccountDetails,
normalizeNumbers,
retrieveApplication,
invokeWebCallback
]);
srf.invite((req, res) => {
const session = new InboundCallSession(req, res);
@@ -72,7 +80,7 @@ app.use((err, req, res, next) => {
logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
});
app.listen(PORT);
const httpServer = app.listen(PORT);
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
@@ -88,4 +96,13 @@ setInterval(() => {
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
}, 5000);
module.exports = {srf, logger};
const disconnect = () => {
return new Promise ((resolve) => {
httpServer.on('close', resolve);
httpServer.close();
srf.disconnect();
srf.locals.mediaservers.forEach((ms) => ms.disconnect());
});
};
module.exports = {srf, logger, disconnect};

View File

@@ -3,9 +3,11 @@ const makeTask = require('../../tasks/make_task');
const RestCallSession = require('../../session/rest-call-session');
const CallInfo = require('../../session/call-info');
const {CallDirection, CallStatus} = require('../../utils/constants');
const { v4: uuidv4 } = require('uuid');
const SipError = require('drachtio-srf').SipError;
const sysError = require('./error');
const Requestor = require('../../utils/requestor');
const dbUtils = require('../../utils/db-utils');
router.post('/', async(req, res) => {
const {logger} = req.app.locals;
@@ -15,6 +17,7 @@ router.post('/', async(req, res) => {
let uri, cs, to;
const restDial = makeTask(logger, {'rest:dial': req.body});
const {srf} = require('../../..');
const {lookupAccountDetails} = dbUtils(logger, srf);
const {getSBC, getFreeswitch} = srf.locals;
const sbcAddress = getSBC();
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
@@ -24,10 +27,32 @@ router.post('/', async(req, res) => {
headers: req.body.headers || {}
};
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
const account = await lookupAccountBySid(req.body.account_sid);
const accountInfo = await lookupAccountDetails(req.body.account_sid);
const callSid = uuidv4();
opts.headers = {
...opts.headers,
'X-Call-Sid': callSid,
'X-Account-Sid': req.body.account_sid
};
switch (target.type) {
case 'phone':
case 'teams':
uri = `sip:${target.number}@${sbcAddress}`;
to = target.number;
if ('teams' === target.type) {
const obj = await lookupTeamsByAccount(req.body.account_sid);
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
Object.assign(opts.headers, {
'X-MS-Teams-FQDN': obj.ms_teams_fqdn,
'X-MS-Teams-Tenant-FQDN': target.tenant || obj.tenant_fqdn
});
if (target.vmail === true) uri = `${uri};opaque=app:voicemail`;
}
break;
case 'user':
uri = `sip:${target.name}`;
@@ -72,8 +97,10 @@ router.post('/', async(req, res) => {
* attach our requestor and notifier objects
* these will be used for all http requests we make during this call
*/
app.requestor = new Requestor(logger, app.call_hook);
if (app.call_status_hook) app.notifier = new Requestor(logger, app.call_status_hook);
app.requestor = new Requestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
if (app.call_status_hook) {
app.notifier = new Requestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
}
else app.notifier = {request: () => {}};
/* now launch the outdial */
@@ -93,10 +120,11 @@ router.post('/', async(req, res) => {
req: inviteReq,
to,
tag: app.tag,
callSid,
accountSid: req.body.account_sid,
applicationSid: app.application_sid
});
cs = new RestCallSession({logger, application: app, srf, req: inviteReq, ep, tasks, callInfo});
cs = new RestCallSession({logger, application: app, srf, req: inviteReq, ep, tasks, callInfo, accountInfo});
cs.exec(req);
res.status(201).json({sid: cs.callSid});
@@ -124,12 +152,14 @@ router.post('/', async(req, res) => {
if (err instanceof SipError) {
if ([486, 603].includes(err.status)) callStatus = CallStatus.Busy;
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
sipLogger.info(`REST outdial failed with ${err.status}`);
cs.emit('callStatusChange', {callStatus, sipStatus: err.status});
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
else console.log(`REST outdial failed with ${err.status}`);
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: err.status});
}
else {
cs.emit('callStatusChange', {callStatus, sipStatus: 500});
sipLogger.error({err}, 'REST outdial failed');
if (cs) cs.emit('callStatusChange', {callStatus, sipStatus: 500});
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
else console.error(err);
}
ep.destroy();
}

View File

@@ -0,0 +1,38 @@
const router = require('express').Router();
const CallInfo = require('../../session/call-info');
const {CallDirection} = require('../../utils/constants');
const SmsSession = require('../../session/sms-call-session');
const normalizeJambones = require('../../utils/normalize-jambones');
const makeTask = require('../../tasks/make_task');
router.post('/:sid', async(req, res) => {
const {logger} = req.app.locals;
const {srf} = req.app.locals;
const {message_sid, account_sid} = req.body;
logger.debug({body: req.body}, 'got createMessage request');
const data = [{
verb: 'message',
...req.body
}];
delete data[0].message_sid;
try {
const tasks = normalizeJambones(logger, data)
.map((tdata) => makeTask(logger, tdata));
const callInfo = new CallInfo({
direction: CallDirection.None,
messageSid: message_sid,
accountSid: account_sid,
res
});
const cs = new SmsSession({logger, srf, tasks, callInfo});
cs.exec();
} catch (err) {
logger.error({err, body: req.body}, 'OutboundSMS: error launching SmsCallSession');
}
});
module.exports = router;

View File

@@ -7,11 +7,13 @@ const {DbErrorUnprocessableRequest} = require('../utils/errors');
/**
* validate the call state
*/
function retrieveCallSession(callSid, opts) {
function retrieveCallSession(logger, callSid, opts) {
logger.debug(`retrieving session for callSid ${callSid}`);
const cs = sessionTracker.get(callSid);
if (cs) {
const task = cs.currentTask;
if (!task || task.name != TaskName.Enqueue) {
logger.debug({cs}, 'found call session but not in Enqueue task??');
throw new DbErrorUnprocessableRequest(`enqueue api failure: indicated call is not queued: ${task.name}`);
}
}
@@ -19,14 +21,14 @@ function retrieveCallSession(callSid, opts) {
}
/**
* notify a waiting session that a conference has started
* notify a waiting session that a queue event has occurred
*/
router.post('/:callSid', async(req, res) => {
const logger = req.app.locals.logger;
const callSid = req.params.callSid;
logger.debug({body: req.body}, 'got enqueue event');
logger.debug({callSid, body: req.body}, 'got enqueue event');
try {
const cs = retrieveCallSession(callSid, req.body);
const cs = retrieveCallSession(logger, callSid, req.body);
if (!cs) {
logger.info(`enqueue: callSid not found ${callSid}`);
return res.sendStatus(404);

View File

@@ -6,6 +6,9 @@ api.use('/conference', require('./conference'));
api.use('/dequeue', require('./dequeue'));
api.use('/enqueue', require('./enqueue'));
api.use('/messaging', require('./messaging')); // inbound SMS
api.use('/createMessage', require('./create-message')); // outbound SMS (REST)
// health checks
api.get('/', (req, res) => res.sendStatus(200));
api.get('/health', (req, res) => res.sendStatus(200));

View File

@@ -0,0 +1,74 @@
const router = require('express').Router();
const Requestor = require('../../utils/requestor');
const CallInfo = require('../../session/call-info');
const {CallDirection} = require('../../utils/constants');
const SmsSession = require('../../session/sms-call-session');
const normalizeJambones = require('../../utils/normalize-jambones');
const {TaskPreconditions} = require('../../utils/constants');
const makeTask = require('../../tasks/make_task');
router.post('/:partner', async(req, res) => {
const {logger} = req.app.locals;
logger.debug({body: req.body}, `got incomingSms request from partner ${req.params.partner}`);
let tasks;
const {srf} = require('../../..');
const {lookupAccountBySid} = srf.locals.dbHelpers;
const app = req.body.app;
const account = await lookupAccountBySid(app.accountSid);
const hook = app.messaging_hook;
const requestor = new Requestor(logger, account.account_sid, hook, account.webhook_secret);
const payload = {
carrier: req.params.partner,
messageSid: app.messageSid,
accountSid: app.accountSid,
applicationSid: app.applicationSid,
from: req.body.from,
to: req.body.to,
cc: req.body.cc,
text: req.body.text,
media: req.body.media
};
res.status(200).json({sid: req.body.messageSid});
try {
tasks = await requestor.request(hook, payload);
logger.info({tasks}, 'response from incoming SMS webhook');
} catch (err) {
logger.error({err, hook}, 'Error sending incoming SMS message');
return;
}
// process any verbs in response
if (Array.isArray(tasks) && tasks.length) {
const {srf} = req.app.locals;
app.requestor = requestor;
app.notifier = {request: () => {}};
try {
tasks = normalizeJambones(logger, tasks)
.map((tdata) => makeTask(logger, tdata))
.filter((t) => t.preconditions === TaskPreconditions.None);
if (0 === tasks.length) {
logger.info('inboundSMS: after removing invalid verbs there are no tasks left to execute');
return;
}
const callInfo = new CallInfo({
direction: CallDirection.None,
messageSid: app.messageSid,
accountSid: app.accountSid,
applicationSid: app.applicationSid
});
const cs = new SmsSession({logger, srf, application: app, tasks, callInfo});
cs.exec();
} catch (err) {
logger.error({err, tasks}, 'InboundSMS: error launching SmsCallSession');
}
}
});
module.exports = router;

View File

@@ -1,14 +1,15 @@
const uuidv4 = require('uuid/v4');
const { v4: uuidv4 } = require('uuid');
const {CallDirection} = require('./utils/constants');
const CallInfo = require('./session/call-info');
const Requestor = require('./utils/requestor');
const makeTask = require('./tasks/make_task');
const parseUri = require('drachtio-srf').parseUri;
const normalizeJambones = require('./utils/normalize-jambones');
const dbUtils = require('./utils/db-utils');
module.exports = function(srf, logger) {
const {lookupAppByPhoneNumber, lookupAppBySid, lookupAppByRealm, lookupAppByTeamsTenant} = srf.locals.dbHelpers;
const {lookupAccountDetails} = dbUtils(logger, srf);
function initLocals(req, res, next) {
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
req.locals = {
@@ -26,6 +27,32 @@ module.exports = function(srf, logger) {
next();
}
/**
* retrieve account information for the incoming call
*/
async function getAccountDetails(req, res, next) {
if (!req.has('X-Account-Sid')) {
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
return res.send(500);
}
const account_sid = req.locals.account_sid = req.get('X-Account-Sid');
try {
req.locals.accountInfo = await lookupAccountDetails(account_sid);
if (!req.locals.accountInfo.account.is_active) {
logger.info(`Account is inactive or suspended ${account_sid}`);
// TODO: alert
return res.send(503, {headers: {'X-Reason': 'Account exists but is inactive'}});
}
logger.debug({accountInfo: req.locals.accountInfo}, `retrieved account info for ${account_sid}`);
next();
} catch (err) {
logger.info({err}, `Error retrieving account details for account ${account_sid}`);
res.send(503, {headers: {'X-Reason': `No Account exists for sid ${account_sid}`}});
}
}
/**
* Within the system, we deal with E.164 numbers _without_ the leading '+
*/
@@ -52,6 +79,7 @@ module.exports = function(srf, logger) {
*/
async function retrieveApplication(req, res, next) {
const logger = req.locals.logger;
const {accountInfo, account_sid} = req.locals;
try {
let app;
if (req.locals.application_sid) app = await lookupAppBySid(req.locals.application_sid);
@@ -100,8 +128,9 @@ module.exports = function(srf, logger) {
* create a requestor that we will use for all http requests we make during the call.
* also create a notifier for call status events (if not needed, its a no-op).
*/
app.requestor = new Requestor(logger, app.call_hook);
if (app.call_status_hook) app.notifier = new Requestor(logger, app.call_status_hook);
app.requestor = new Requestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
if (app.call_status_hook) app.notifier = new Requestor(logger, account_sid, app.call_status_hook,
accountInfo.account.webhook_secret);
else app.notifier = {request: () => {}};
req.locals.application = app;
@@ -145,6 +174,7 @@ module.exports = function(srf, logger) {
return {
initLocals,
getAccountDetails,
normalizeNumbers,
retrieveApplication,
invokeWebCallback

View File

@@ -0,0 +1,43 @@
const CallSession = require('./call-session');
/**
* @classdesc Subclass of CallSession. Represents a CallSession
* that was initially a child call leg; i.e. established via a Dial verb.
* Now it is all grown up and filling out its own CallSession. Yoo-hoo!
* @extends CallSession
*/
class AdultingCallSession extends CallSession {
constructor({logger, application, singleDialer, tasks, callInfo}) {
super({
logger,
application,
srf: singleDialer.dlg.srf,
tasks,
callInfo
});
this.sd = singleDialer;
this.sd.dlg.on('destroy', () => {
this.logger.info('AdultingCallSession: called party hung up');
this._callReleased();
});
this.sd.emit('adulting');
}
get dlg() {
return this.sd.dlg;
}
get ep() {
return this.sd.ep;
}
get callSid() {
return this.callInfo.callSid;
}
}
module.exports = AdultingCallSession;

View File

@@ -1,6 +1,6 @@
const {CallDirection, CallStatus} = require('../utils/constants');
const parseUri = require('drachtio-srf').parseUri;
const uuidv4 = require('uuid/v4');
const { v4: uuidv4 } = require('uuid');
/**
* @classdesc Represents the common information for all calls
@@ -44,10 +44,18 @@ class CallInfo {
this.callStatus = CallStatus.Trying,
this.sipStatus = 100;
}
else if (this.direction === CallDirection.None) {
// outbound SMS
const {messageSid, accountSid, applicationSid, res} = opts;
this.messageSid = messageSid;
this.accountSid = accountSid;
this.applicationSid = applicationSid;
this.res = res;
}
else {
// outbound call triggered by REST
const {req, accountSid, applicationSid, to, tag} = opts;
this.callSid = uuidv4();
const {req, callSid, accountSid, applicationSid, to, tag} = opts;
this.callSid = callSid;
this.accountSid = accountSid;
this.applicationSid = applicationSid;
this.callStatus = CallStatus.Trying,

View File

@@ -8,6 +8,7 @@ const makeTask = require('../tasks/make_task');
const normalizeJambones = require('../utils/normalize-jambones');
const listTaskNames = require('../utils/summarize-tasks');
const BADPRECONDITIONS = 'preconditions not met';
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
/**
* @classdesc Represents the execution context for a call.
@@ -26,25 +27,28 @@ class CallSession extends Emitter {
* @param {array} opts.tasks - tasks we are to execute
* @param {callInfo} opts.callInfo - information about the call
*/
constructor({logger, application, srf, tasks, callInfo}) {
constructor({logger, application, srf, tasks, callInfo, accountInfo}) {
super();
this.logger = logger;
this.application = application;
this.srf = srf;
this.callInfo = callInfo;
this.accountInfo = accountInfo;
this.tasks = tasks;
this.updateCallStatus = srf.locals.dbHelpers.updateCallStatus;
this.serviceUrl = srf.locals.serviceUrl;
this.taskIdx = 0;
this.stackIdx = 0;
this.callGone = false;
this.tmpFiles = new Set();
// if this is a ConfirmSession
if (!this.isConfirmCallSession) sessionTracker.add(this.callSid, this);
if (!this.isSmsCallSession) {
this.updateCallStatus = srf.locals.dbHelpers.updateCallStatus;
this.serviceUrl = srf.locals.serviceUrl;
}
if (!this.isConfirmCallSession && !this.isSmsCallSession && !this.isAdultingCallSession) {
sessionTracker.add(this.callSid, this);
}
}
/**
@@ -65,7 +69,7 @@ class CallSession extends Emitter {
* SIP call-id for the call
*/
get callId() {
return this.callInfo.direction;
return this.callInfo.callId;
}
/**
@@ -158,6 +162,13 @@ class CallSession extends Emitter {
return this.application.transferredCall === true;
}
/**
* returns true if this session is a ConfirmCallSession
*/
get isAdultingCallSession() {
return this.constructor.name === 'AdultingCallSession';
}
/**
* returns true if this session is a ConfirmCallSession
*/
@@ -165,6 +176,62 @@ class CallSession extends Emitter {
return this.constructor.name === 'ConfirmCallSession';
}
/**
* returns true if this session is a SmsCallSession
*/
get isSmsCallSession() {
return this.constructor.name === 'SmsCallSession';
}
/**
* Check for speech credentials for the specified vendor
* @param {*} vendor - google or aws
*/
getSpeechCredentials(vendor, type) {
const {writeAlerts, AlertType} = this.srf.locals;
this.logger.debug({vendor, type, speech: this.accountInfo.speech}, `searching for speech for vendor ${vendor}`);
if (this.accountInfo.speech && this.accountInfo.speech.length > 0) {
const credential = this.accountInfo.speech.find((s) => s.vendor === vendor);
if (credential && (
(type === 'tts' && credential.use_for_tts) ||
(type === 'stt' && credential.use_for_stt)
)) {
if ('google' === vendor) {
try {
const cred = JSON.parse(credential.service_key.replace(/\n/g, '\\n'));
return {
speech_credential_sid: credential.speech_credential_sid,
credentials: cred
};
} catch (err) {
const sid = this.accountInfo.account.account_sid;
this.logger.info({err}, `malformed google service_key provisioned for account ${sid}`);
writeAlerts({
alert_type: AlertType.TTS_FAILURE,
account_sid: this.accountSid,
vendor
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
}
}
else if (['aws', 'polly'].includes(vendor)) {
return {
speech_credential_sid: credential.speech_credential_sid,
accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key,
region: process.env.AWS_REGION || credential.aws_region
};
}
}
else {
writeAlerts({
alert_type: AlertType.STT_NOT_PROVISIONED,
account_sid: this.accountSid,
vendor
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
}
}
}
/**
* execute the tasks in the CallSession. The tasks are executed in sequence until
* they complete, or the caller hangs up.
@@ -200,7 +267,7 @@ class CallSession extends Emitter {
this._onTasksDone();
this._clearResources();
if (!this.isConfirmCallSession) sessionTracker.remove(this.callSid);
if (!this.isConfirmCallSession && !this.isSmsCallSession) sessionTracker.remove(this.callSid);
}
trackTmpFile(path) {
@@ -286,10 +353,47 @@ class CallSession extends Emitter {
* @param {object} [opts.call_hook] - new call_status_hook
*/
async _lccCallHook(opts) {
const tasks = await this.requestor.request(opts.call_hook, this.callInfo);
if (tasks && tasks.length > 0) {
this.logger.info({tasks: listTaskNames(tasks)}, 'CallSession:updateCall new task list');
this.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
const webhooks = [];
let sd;
if (opts.call_hook) webhooks.push(this.requestor.request(opts.call_hook, this.callInfo));
if (opts.child_call_hook) {
/* child call hook only allowed from a connected Dial state */
const task = this.currentTask;
sd = task.sd;
if (task && TaskName.Dial === task.name && sd) {
webhooks.push(this.requestor.request(opts.child_call_hook, sd.callInfo));
}
}
const [tasks1, tasks2] = await Promise.all(webhooks);
let tasks, childTasks;
if (opts.call_hook) {
tasks = tasks1;
if (opts.child_call_hook) childTasks = tasks2;
}
else childTasks = tasks1;
if (childTasks) {
const {parentLogger} = this.srf.locals;
const childLogger = parentLogger.child({callId: this.callId, callSid: sd.callSid});
const t = normalizeJambones(childLogger, childTasks).map((tdata) => makeTask(childLogger, tdata));
childLogger.info({tasks: listTaskNames(t)}, 'CallSession:updateCall new task list for child call');
const cs = await sd.doAdulting({
logger: childLogger,
application: this.application,
tasks: t
});
/* need to update the callSid of the child with its own (new) AdultingCallSession */
sessionTracker.add(cs.callSid, cs);
}
if (tasks) {
const t = normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata));
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:updateCall new task list');
this.replaceApplication(t);
}
else {
/* we started a new app on the child leg, but nothing given for parent so hang him up */
this.currentTask.kill();
}
}
@@ -330,8 +434,8 @@ class CallSession extends Emitter {
// this whole thing requires us to be in a Dial verb
const task = this.currentTask;
if (!task || TaskName.Dial !== task.name) {
return this.logger.info('CallSession:_lccWhisper - invalid command since we are not in a dial');
if (!task || ![TaskName.Dial, TaskName.Listen].includes(task.name)) {
return this.logger.info('CallSession:_lccWhisper - invalid command since we are not in a dial or listen');
}
// allow user to provide a url object, a url string, an array of tasks, or a single task
@@ -394,7 +498,7 @@ class CallSession extends Emitter {
if (opts.call_status) {
return this._lccCallStatus(opts);
}
if (opts.call_hook) {
if (opts.call_hook || opts.child_call_hook) {
return await this._lccCallHook(opts);
}
if (opts.listen_status) {
@@ -462,11 +566,11 @@ class CallSession extends Emitter {
* @param {Task} task - task to be executed
*/
async _evalEndpointPrecondition(task) {
this.logger.debug('_evalEndpointPrecondition');
this.logger.debug('CallSession:_evalEndpointPrecondition');
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
if (this.ep) {
if (!task.earlyMedia || this.dlg) return this.ep;
if (task.earlyMedia === true || this.dlg) return this.ep;
// we are going from an early media connection to answer
await this.propagateAnswer();
@@ -479,9 +583,16 @@ class CallSession extends Emitter {
const ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
ep.cs = this;
this.ep = ep;
await ep.set('hangup_after_bridge', false);
ep.set({
hangup_after_bridge: false,
park_after_bridge: true
}).catch((err) => this.logger.error({err}, 'Error setting park_after_bridge'));
this.logger.debug('allocated endpoint');
this.logger.debug(`allocated endpoint ${ep.uuid}`);
this.ep.on('destroy', () => {
this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`);
});
if (this.direction === CallDirection.Inbound) {
if (task.earlyMedia && !this.req.finalResponseSent) {
@@ -497,8 +608,15 @@ class CallSession extends Emitter {
return ep;
} catch (err) {
this.logger.error(err, `Error attempting to allocate endpoint for for task ${task.name}`);
throw new Error(`${BADPRECONDITIONS}: unable to allocate endpoint`);
if (err === CALLER_CANCELLED_ERR_MSG) {
this.logger.error(err, 'caller canceled quickly before we could respond, ending call');
this._notifyCallStatusChange({callStatus: CallStatus.NoAnswer, sipStatus: 487});
this._callReleased();
}
else {
this.logger.error(err, `Error attempting to allocate endpoint for for task ${task.name}`);
throw new Error(`${BADPRECONDITIONS}: unable to allocate endpoint`);
}
}
}

View File

@@ -15,6 +15,7 @@ class InboundCallSession extends CallSession {
srf: req.srf,
application: req.locals.application,
callInfo: req.locals.callInfo,
accountInfo: req.locals.accountInfo,
tasks: req.locals.application.tasks
});
this.req = req;

View File

@@ -8,14 +8,15 @@ const moment = require('moment');
* @extends CallSession
*/
class RestCallSession extends CallSession {
constructor({logger, application, srf, req, ep, tasks, callInfo}) {
constructor({logger, application, srf, req, ep, tasks, callInfo, accountInfo}) {
super({
logger,
application,
srf,
callSid: callInfo.callSid,
tasks,
callInfo
callInfo,
accountInfo
});
this.req = req;
this.ep = ep;

View File

@@ -0,0 +1,22 @@
const CallSession = require('./call-session');
/**
* @classdesc Subclass of CallSession. Represents a CallSession
* that is established for the purpose of sending an outbound SMS
* @extends CallSession
*/
class SmsCallSession extends CallSession {
constructor({logger, application, srf, tasks, callInfo}) {
super({
logger,
application,
srf,
tasks,
callInfo
});
}
}
module.exports = SmsCallSession;

View File

@@ -73,7 +73,7 @@ class Conference extends Task {
const dlg = cs.dlg;
// reset answer time if we were transferred from another feature server
if (this.this.connectTime) dlg.connectTime = this.connectTime;
if (this.connectTime) dlg.connectTime = this.connectTime;
this.ep.on('destroy', this._kicked.bind(this, cs, dlg));

View File

@@ -37,12 +37,13 @@ function compareTasks(t1, t2) {
if (t1.type !== t2.type) return false;
switch (t1.type) {
case 'phone':
return t1.number === t1.number;
return t1.number === t2.number;
case 'user':
return t1.name === t2.name;
case 'teams':
return t2.name === t1.name;
return t1.number === t2.number;
case 'sip':
return t2.sipUri === t1.sipUri;
return t1.sipUri === t2.sipUri;
}
}
@@ -83,6 +84,7 @@ class TaskDial extends Task {
this.confirmHook = this.data.confirmHook;
this.confirmMethod = this.data.confirmMethod;
this.dtmfHook = this.data.dtmfHook;
this.proxy = this.data.proxy;
if (this.dtmfHook) {
const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {});
@@ -111,6 +113,11 @@ class TaskDial extends Task {
}
get ep() {
/**
* Note:
* this.ep is the B leg-facing endpoint
* this.epOther is the A leg-facing endpoint
*/
if (this.sd) return this.sd.ep;
}
@@ -128,11 +135,11 @@ class TaskDial extends Task {
this.epOther.play(this.dialMusic).catch((err) => {});
}
}
this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
if (this.epOther) this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
await this._attemptCalls(cs);
await this.awaitTaskDone();
await this.performAction(this.results);
this._removeDtmfDetection(cs, this.epOther);
if (this.epOther) this._removeDtmfDetection(cs, this.epOther);
this._removeDtmfDetection(cs, this.ep);
} catch (err) {
this.logger.error({err}, 'TaskDial:exec terminating with error');
@@ -142,7 +149,7 @@ class TaskDial extends Task {
async kill(cs) {
super.kill(cs);
this._removeDtmfDetection(this.cs, this.epOther);
if (this.epOther) this._removeDtmfDetection(this.cs, this.epOther);
this._removeDtmfDetection(this.cs, this.ep);
this._killOutdials();
if (this.sd) {
@@ -172,7 +179,7 @@ class TaskDial extends Task {
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
}
this.logger.debug('Dial:whisper tasks complete');
if (!cs.callGone) this.epOther.bridge(this.ep);
if (!cs.callGone && this.epOther) this.epOther.bridge(this.ep);
} catch (err) {
this.logger.error(err, 'Dial:whisper error');
}
@@ -217,13 +224,18 @@ class TaskDial extends Task {
_onDtmf(cs, ep, evt) {
if (ep.dtmfDetector) {
const match = ep.dtmfDetector.keyPress(evt.dtmf);
const requestor = ep.dtmfDetector === this.parentDtmfCollector ?
cs.requestor :
this.sd.requestor;
if (match) {
this.logger.debug(`parentCall triggered dtmf match: ${match}`);
requestor.request(this.dtmfHook, Object.assign({dtmf: match}, cs.callInfo))
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
this.logger.debug({callSid: this.cs.callSid}, `Dial:_onDtmf triggered dtmf match: ${match}`);
const requestor = ep.dtmfDetector === this.parentDtmfCollector ?
cs.requestor :
(this.sd ? this.sd.requestor : null);
if (!requestor) {
this.logger.info(`Dial:_onDtmf got digits on B leg after adulting: ${evt.dtmf}`);
}
else {
requestor.request(this.dtmfHook, {dtmf: match, ...cs.callInfo})
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
}
}
}
}
@@ -233,6 +245,9 @@ class TaskDial extends Task {
this.epOther = ep;
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
/* send outbound legs back to the same SBC (to support static IP feature) */
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port};transport=tcp`;
if (this.dialMusic) {
// play dial music to caller while we outdial
ep.play(this.dialMusic).catch((err) => {
@@ -244,9 +259,10 @@ class TaskDial extends Task {
async _attemptCalls(cs) {
const {req, srf} = cs;
const {getSBC} = srf.locals;
const {lookupTeamsByAccount} = srf.locals.dbHelpers;
const sbcAddress = getSBC();
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
const sbcAddress = this.proxy || getSBC();
const teamsInfo = {};
let fqdn;
if (!sbcAddress) throw new Error('no SBC found for outbound call');
const opts = {
@@ -254,11 +270,16 @@ class TaskDial extends Task {
proxy: `sip:${sbcAddress}`,
callingNumber: this.callerId || req.callingNumber
};
opts.headers = {
...opts.headers,
'X-Account-Sid': cs.accountSid
};
if (this.target.find((t) => t.type === 'teams')) {
const t = this.target.find((t) => t.type === 'teams');
if (t) {
const obj = await lookupTeamsByAccount(cs.accountSid);
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
Object.assign(teamsInfo, {tenant_fqdn: obj.tenant_fqdn, ms_teams_fqdn: obj.ms_teams_fqdn});
Object.assign(teamsInfo, {tenant_fqdn: t.tenant || obj.tenant_fqdn, ms_teams_fqdn: obj.ms_teams_fqdn});
}
const ms = await cs.getMS();
@@ -267,11 +288,23 @@ class TaskDial extends Task {
this._killOutdials();
}, this.timeout * 1000);
this.target.forEach((t) => {
this.target.forEach(async(t) => {
try {
t.url = t.url || this.confirmUrl;
t.method = t.method || this.confirmMethod || 'POST';
if (t.type === 'teams') t.teamsInfo = teamsInfo;
if (t.type === 'user' && !t.name.includes('@') && !fqdn) {
const user = t.name;
try {
const {sip_realm} = await lookupAccountBySid(cs.accountSid);
if (sip_realm) {
t.name = `${user}@${sip_realm}`;
this.logger.debug(`appending sip realm ${sip_realm} to dial target user ${user}`);
}
} catch (err) {
this.logger.error({err}, 'Error looking up account by sid');
}
}
const sd = placeCall({
logger: this.logger,
application: cs.application,
@@ -296,6 +329,7 @@ class TaskDial extends Task {
if (this.results.dialCallStatus !== CallStatus.Completed) {
Object.assign(this.results, {
dialCallStatus: obj.callStatus,
dialSipStatus: obj.sipStatus,
dialCallSid: sd.callSid,
});
}
@@ -334,6 +368,16 @@ class TaskDial extends Task {
this.logger.debug('Dial:_attemptCalls - all calls failed after decline, ending task');
this.kill(cs);
}
})
.once('adulting', () => {
/* child call just adulted and got its own session */
this.logger.info('Dial:on_adulting: detaching child call leg');
if (this.ep) {
this.logger.debug(`Dial:on_adulting: removing dtmf from ${this.ep.uuid}`);
this.ep.removeAllListeners('dtmf');
}
this.sd = null;
this.callSid = null;
});
} catch (err) {
this.logger.error(err, 'Dial:_attemptCalls');
@@ -344,8 +388,10 @@ class TaskDial extends Task {
_connectSingleDial(cs, sd) {
if (!this.bridged) {
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
this.epOther.api('uuid_break', this.epOther.uuid);
this.epOther.bridge(sd.ep);
if (this.epOther) {
this.epOther.api('uuid_break', this.epOther.uuid);
this.epOther.bridge(sd.ep);
}
this.bridged = true;
}
@@ -382,15 +428,19 @@ class TaskDial extends Task {
}
sessionTracker.add(this.callSid, cs);
this.dlg.on('destroy', () => {
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
sessionTracker.remove(this.callSid);
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
this.ep.unbridge();
this.kill(cs);
/* if our child is adulting, he's own his own now.. */
if (this.dlg) {
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
sessionTracker.remove(this.callSid);
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
this.ep.unbridge();
this.kill(cs);
}
});
Object.assign(this.results, {
dialCallStatus: CallStatus.Completed,
dialSipStatus: 200,
dialCallSid: sd.callSid,
});

View File

@@ -0,0 +1,70 @@
const Emitter = require('events');
/**
* A dtmf collector
* @class
*/
class DigitBuffer extends Emitter {
/**
* Creates a DigitBuffer
* @param {*} logger - a pino logger
* @param {*} opts - dtmf collection instructions
*/
constructor(logger, opts) {
super();
this.logger = logger;
this.minDigits = opts.min || 1;
this.maxDigits = opts.max || 99;
this.termDigit = opts.term;
this.interdigitTimeout = opts.idt || 8000;
this.template = opts.template;
this.buffer = '';
this.logger.debug(`digitbuffer min: ${this.minDigits} max: ${this.maxDigits} term digit: ${this.termDigit}`);
}
/**
* process a received dtmf digit
* @param {String} a single digit entered by the caller
*/
process(digit) {
this.logger.debug(`digitbuffer process: ${digit}`);
if (digit === this.termDigit) return this._fulfill();
this.buffer += digit;
if (this.buffer.length === this.maxDigits) return this._fulfill();
if (this.buffer.length >= this.minDigits) this._startInterDigitTimer();
this.logger.debug(`digitbuffer buffer: ${this.buffer}`);
}
/**
* clear the digit buffer
*/
flush() {
if (this.idtimer) clearTimeout(this.idtimer);
this.buffer = '';
}
_fulfill() {
this.logger.debug(`digit buffer fulfilled with ${this.buffer}`);
if (this.template && this.template.includes('${digits}')) {
const text = this.template.replace('${digits}', this.buffer);
this.logger.info(`reporting dtmf as ${text}`);
this.emit('fulfilled', text);
}
else {
this.emit('fulfilled', this.buffer);
}
this.flush();
}
_startInterDigitTimer() {
if (this.idtimer) clearTimeout(this.idtimer);
this.idtimer = setTimeout(this._onInterDigitTimeout.bind(this), this.interdigitTimeout);
}
_onInterDigitTimeout() {
this.logger.debug('digit buffer timeout');
this._fulfill();
}
}
module.exports = DigitBuffer;

View File

@@ -0,0 +1,468 @@
const Task = require('../task');
const {TaskName, TaskPreconditions} = require('../../utils/constants');
const Intent = require('./intent');
const DigitBuffer = require('./digit-buffer');
const Transcription = require('./transcription');
const normalizeJambones = require('../../utils/normalize-jambones');
class Dialogflow extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
this.credentials = this.data.credentials;
if (this.data.environment) this.project = `${this.data.project}:${this.data.environment}`;
else this.project = this.data.project;
this.lang = this.data.lang || 'en-US';
this.welcomeEvent = this.data.welcomeEvent || '';
if (this.welcomeEvent.length && this.data.welcomeEventParams && typeof this.data.welcomeEventParams === 'object') {
this.welcomeEventParams = this.data.welcomeEventParams;
}
if (this.data.noInputTimeout) this.noInputTimeout = this.data.noInputTimeout * 1000;
else this.noInputTimeout = 20000;
this.noInputEvent = this.data.noInputEvent || 'actions_intent_NO_INPUT';
this.passDtmfAsInputText = this.passDtmfAsInputText === true;
if (this.data.eventHook) this.eventHook = this.data.eventHook;
if (this.eventHook && Array.isArray(this.data.events)) {
this.events = this.data.events;
}
else if (this.eventHook) {
// send all events by default - except interim transcripts
this.events = [
'intent',
'transcription',
'dtmf',
'start-play',
'stop-play',
'no-input'
];
}
else {
this.events = [];
}
if (this.data.actionHook) this.actionHook = this.data.actionHook;
if (this.data.thinkingMusic) this.thinkingMusic = this.data.thinkingMusic;
if (this.data.tts) {
this.vendor = this.data.tts.vendor || 'default';
this.language = this.data.tts.language || 'default';
this.voice = this.data.tts.voice || 'default';
}
this.bargein = this.data.bargein;
}
get name() { return TaskName.Dialogflow; }
async exec(cs, ep) {
await super.exec(cs);
try {
await this.init(cs, ep);
this.logger.debug(`starting dialogflow bot ${this.project}`);
// kick it off
const baseArgs = `${this.ep.uuid} ${this.project} ${this.lang} ${this.welcomeEvent}`;
if (this.welcomeEventParams) {
this.ep.api('dialogflow_start', `${baseArgs} '${JSON.stringify(this.welcomeEventParams)}'`);
}
else if (this.welcomeEvent.length) {
this.ep.api('dialogflow_start', baseArgs);
}
else {
this.ep.api('dialogflow_start', `${this.ep.uuid} ${this.project} ${this.lang}`);
}
this.logger.debug(`started dialogflow bot ${this.project}`);
await this.awaitTaskDone();
} catch (err) {
this.logger.error({err}, 'Dialogflow:exec error');
}
}
async kill(cs) {
super.kill(cs);
if (this.ep.connected) {
this.logger.debug('TaskDialogFlow:kill');
this.ep.removeCustomEventListener('dialogflow::intent');
this.ep.removeCustomEventListener('dialogflow::transcription');
this.ep.removeCustomEventListener('dialogflow::audio_provided');
this.ep.removeCustomEventListener('dialogflow::end_of_utterance');
this.ep.removeCustomEventListener('dialogflow::error');
this._clearNoinputTimer();
if (!this.reportedFinalAction) this.performAction({dialogflowResult: 'caller hungup'})
.catch((err) => this.logger.error({err}, 'dialogflow - error w/ action webook'));
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
}
this.notifyTaskDone();
}
async init(cs, ep) {
this.ep = ep;
try {
if (this.vendor === 'default') {
this.vendor = cs.speechSynthesisVendor;
this.language = cs.speechSynthesisLanguage;
this.voice = cs.speechSynthesisVoice;
}
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
this.ep.addCustomEventListener('dialogflow::intent', this._onIntent.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow::transcription', this._onTranscription.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow::audio_provided', this._onAudioProvided.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow::end_of_utterance', this._onEndOfUtterance.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow::error', this._onError.bind(this, ep, cs));
const obj = typeof this.credentials === 'string' ? JSON.parse(this.credentials) : this.credentials;
const creds = JSON.stringify(obj);
await this.ep.set('GOOGLE_APPLICATION_CREDENTIALS', creds);
} catch (err) {
this.logger.error({err}, 'Error setting credentials');
throw err;
}
}
/**
* An intent has been returned. Since we are using SINGLE_UTTERANCE on the dialogflow side,
* we may get an empty intent, signified by the lack of a 'response_id' attribute.
* In such a case, we just start another StreamingIntentDetectionRequest.
* @param {*} ep - media server endpoint
* @param {*} evt - event data
*/
async _onIntent(ep, cs, evt) {
const intent = new Intent(this.logger, evt);
if (intent.isEmpty) {
/**
* An empty intent is returned in 3 conditions:
* 1. Our no-input timer fired
* 2. We collected dtmf that needs to be fed to dialogflow
* 3. A normal dialogflow timeout
*/
if (this.noinput && this.greetingPlayed) {
this.logger.info('no input timer fired, reprompting..');
this.noinput = false;
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang} ${this.noInputEvent}`);
}
else if (this.dtmfEntry && this.greetingPlayed) {
this.logger.info('dtmf detected, reprompting..');
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang} none \'${this.dtmfEntry}\'`);
this.dtmfEntry = null;
}
else if (this.greetingPlayed) {
this.logger.info('starting another intent');
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
}
else {
this.logger.info('got empty intent');
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
}
return;
}
if (this.events.includes('intent')) {
this._performHook(cs, this.eventHook, {event: 'intent', data: evt});
}
// clear the no-input timer and the digit buffer
this._clearNoinputTimer();
if (this.digitBuffer) this.digitBuffer.flush();
/* hang up (or tranfer call) after playing next audio file? */
if (intent.saysEndInteraction) {
// if 'end_interaction' is true, end the dialog after playing the final prompt
// (or in 1 second if there is no final prompt)
this.hangupAfterPlayDone = true;
this.waitingForPlayStart = true;
setTimeout(() => {
if (this.waitingForPlayStart) {
this.logger.info('hanging up since intent was marked end interaction');
this.performAction({dialogflowResult: 'completed'});
this.notifyTaskDone();
}
}, 1000);
}
/* collect digits? */
else if (intent.saysCollectDtmf || this.enableDtmfAlways) {
const opts = Object.assign({
idt: this.opts.interDigitTimeout
}, intent.dtmfInstructions || {term: '#'});
this.digitBuffer = new DigitBuffer(this.logger, opts);
this.digitBuffer.once('fulfilled', this._onDtmfEntryComplete.bind(this, ep));
}
/* if we are using tts and a message was provided, play it out */
if (this.vendor && intent.fulfillmentText && intent.fulfillmentText.length > 0) {
const {srf} = cs;
const {synthAudio} = srf.locals.dbHelpers;
this.waitingForPlayStart = false;
// start a new intent, (we want to continue to listen during the audio playback)
// _unless_ we are transferring or ending the session
if (!this.hangupAfterPlayDone) {
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
}
try {
const obj = {
text: intent.fulfillmentText,
vendor: this.vendor,
language: this.language,
voice: this.voice,
salt: cs.callSid,
credentials: this.ttsCredentials
};
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
const {filePath, servedFromCache} = await synthAudio(obj);
if (filePath) cs.trackTmpFile(filePath);
if (!this.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);
if (this.playInProgress) {
await ep.api('uuid_break', ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
}
this.playInProgress = true;
this.curentAudioFile = filePath;
this.logger.debug(`starting to play tts ${filePath}`);
if (this.events.includes('start-play')) {
this._performHook(cs, this.eventHook, {event: 'start-play', data: {path: filePath}});
}
await ep.play(filePath);
if (this.events.includes('stop-play')) {
this._performHook(cs, this.eventHook, {event: 'stop-play', data: {path: filePath}});
}
this.logger.debug(`finished ${filePath}`);
if (this.curentAudioFile === filePath) {
this.playInProgress = false;
if (this.queuedTasks) {
this.logger.debug('finished playing audio and we have queued tasks');
this._redirect(cs, this.queuedTasks);
return;
}
}
this.greetingPlayed = true;
if (this.hangupAfterPlayDone) {
this.logger.info('hanging up since intent was marked end interaction and we completed final prompt');
this.performAction({dialogflowResult: 'completed'});
this.notifyTaskDone();
}
else {
// every time we finish playing a prompt, start the no-input timer
this._startNoinputTimer(ep, cs);
}
} catch (err) {
this.logger.error({err}, 'Dialogflow:_onIntent - error playing tts');
}
}
}
/**
* A transcription - either interim or final - has been returned.
* If we are doing barge-in based on hotword detection, check for the hotword or phrase.
* If we are playing a filler sound, like typing, during the fullfillment phase, start that
* if this is a final transcript.
* @param {*} ep - media server endpoint
* @param {*} evt - event data
*/
async _onTranscription(ep, cs, evt) {
const transcription = new Transcription(this.logger, evt);
if (this.events.includes('transcription') && transcription.isFinal) {
this._performHook(cs, this.eventHook, {event: 'transcription', data: evt});
}
else if (this.events.includes('interim-transcription') && !transcription.isFinal) {
this._performHook(cs, this.eventHook, {event: 'transcription', data: evt});
}
// if a final transcription, start a typing sound
if (this.thinkingSound > 0 && !transcription.isEmpty && transcription.isFinal &&
transcription.confidence > 0.8) {
ep.play(this.data.thinkingSound).catch((err) => this.logger.info(err, 'Error playing typing sound'));
}
// interrupt playback on speaking if bargein = true
if (this.bargein && this.playInProgress) {
this.logger.debug('terminating playback due to speech bargein');
this.playInProgress = false;
await ep.api('uuid_break', ep.uuid);
}
}
/**
* The caller has just finished speaking. No action currently taken.
* @param {*} evt - event data
*/
_onEndOfUtterance(cs, evt) {
if (this.events.includes('end-utterance')) {
this._performHook(cs, this.eventHook, {event: 'end-utterance'});
}
}
/**
* Dialogflow has returned an error of some kind.
* @param {*} evt - event data
*/
_onError(ep, cs, evt) {
this.logger.error(`got error: ${JSON.stringify(evt)}`);
}
/**
* Audio has been received from dialogflow and written to a temporary disk file.
* Start playing the audio, after killing any filler sound that might be playing.
* When the audio completes, start the no-input timer.
* @param {*} ep - media server endpoint
* @param {*} evt - event data
*/
async _onAudioProvided(ep, cs, evt) {
if (this.vendor) return;
this.waitingForPlayStart = false;
// kill filler audio
await ep.api('uuid_break', ep.uuid);
// start a new intent, (we want to continue to listen during the audio playback)
// _unless_ we are transferring or ending the session
if (/*this.greetingPlayed &&*/ !this.hangupAfterPlayDone) {
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
}
this.playInProgress = true;
this.curentAudioFile = evt.path;
this.logger.info(`starting to play ${evt.path}`);
if (this.events.includes('start-play')) {
this._performHook(cs, this.eventHook, {event: 'start-play', data: {path: evt.path}});
}
await ep.play(evt.path);
if (this.events.includes('stop-play')) {
this._performHook(cs, this.eventHook, {event: 'stop-play', data: {path: evt.path}});
}
this.logger.info(`finished ${evt.path}, queued tasks: ${(this.queuedTasks || []).length}`);
if (this.curentAudioFile === evt.path) {
this.playInProgress = false;
if (this.queuedTasks) {
this.logger.debug('finished playing audio and we have queued tasks');
this._redirect(cs, this.queuedTasks);
this.queuedTasks.length = 0;
return;
}
}
/*
if (!this.inbound && !this.greetingPlayed) {
this.logger.info('finished greeting on outbound call, starting new intent');
this.ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
}
*/
this.greetingPlayed = true;
if (this.hangupAfterPlayDone) {
this.logger.info('hanging up since intent was marked end interaction and we completed final prompt');
this.performAction({dialogflowResult: 'completed'});
this.notifyTaskDone();
}
else {
// every time we finish playing a prompt, start the no-input timer
this._startNoinputTimer(ep, cs);
}
}
/**
* receive a dmtf entry from the caller.
* If we have active dtmf instructions, collect and process accordingly.
*/
_onDtmf(ep, cs, evt) {
if (this.digitBuffer) this.digitBuffer.process(evt.dtmf);
if (this.events.includes('dtmf')) {
this._performHook(cs, this.eventHook, {event: 'dtmf', data: evt});
}
}
_onDtmfEntryComplete(ep, dtmfEntry) {
this.logger.info(`collected dtmf entry: ${dtmfEntry}`);
this.dtmfEntry = dtmfEntry;
this.digitBuffer = null;
// if a final transcription, start a typing sound
if (this.thinkingSound > 0) {
ep.play(this.thinkingSound).catch((err) => this.logger.info(err, 'Error playing typing sound'));
}
// kill the current dialogflow, which will result in us getting an immediate intent
ep.api('dialogflow_stop', `${ep.uuid}`)
.catch((err) => this.logger.info(`dialogflow_stop failed: ${err.message}`));
}
/**
* The user has not provided any input for some time.
* Set the 'noinput' member to true and kill the current dialogflow.
* This will result in us re-prompting with an event indicating no input.
* @param {*} ep
*/
_onNoInput(ep, cs) {
this.noinput = true;
if (this.events.includes('no-input')) {
this._performHook(cs, this.eventHook, {event: 'no-input'});
}
// kill the current dialogflow, which will result in us getting an immediate intent
ep.api('dialogflow_stop', `${ep.uuid}`)
.catch((err) => this.logger.info(`dialogflow_stop failed: ${err.message}`));
}
/**
* Stop the no-input timer, if it is running
*/
_clearNoinputTimer() {
if (this.noinputTimer) {
clearTimeout(this.noinputTimer);
this.noinputTimer = null;
}
}
/**
* Start the no-input timer. The duration is set in the configuration file.
* @param {*} ep
*/
_startNoinputTimer(ep, cs) {
if (!this.noInputTimeout) return;
this._clearNoinputTimer();
this.noinputTimer = setTimeout(this._onNoInput.bind(this, ep, cs), this.noInputTimeout);
}
async _performHook(cs, hook, results) {
const json = await this.cs.requestor.request(hook, results);
if (json && Array.isArray(json)) {
const makeTask = require('../make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
if (this.playInProgress) {
this.queuedTasks = tasks;
this.logger.info({tasks: tasks},
`${this.name} replacing application with ${tasks.length} tasks after play completes`);
return;
}
this._redirect(cs, tasks);
}
}
}
_redirect(cs, tasks) {
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
this.performAction({dialogflowResult: 'redirect'}, false);
this.reportedFinalAction = true;
cs.replaceApplication(tasks);
}
}
module.exports = Dialogflow;

View File

@@ -0,0 +1,89 @@
class Intent {
constructor(logger, evt) {
this.logger = logger;
this.evt = evt;
this.logger.debug({evt}, 'intent');
this.dtmfRequest = checkIntentForDtmfEntry(logger, evt);
}
get isEmpty() {
return this.evt.response_id.length === 0;
}
get fulfillmentText() {
return this.evt.query_result.fulfillment_text;
}
get saysEndInteraction() {
return this.evt.query_result.intent.end_interaction ;
}
get saysCollectDtmf() {
return !!this.dtmfRequest;
}
get dtmfInstructions() {
return this.dtmfRequest;
}
get name() {
if (!this.isEmpty) return this.evt.query_result.intent.display_name;
}
toJSON() {
return {
name: this.name,
fulfillmentText: this.fulfillmentText
};
}
}
module.exports = Intent;
/**
* Parse a returned intent for DTMF entry information
* i.e.
* allow-dtmf-x-y-z
* x = min number of digits
* y = optional, max number of digits
* z = optional, terminating character
* e.g.
* allow-dtmf-5 : collect 5 digits
* allow-dtmf-1-4 : collect between 1 to 4 (inclusive) digits
* allow-dtmf-1-4-# : collect 1-4 digits, terminating if '#' is entered
* @param {*} intent - dialogflow intent
*/
const checkIntentForDtmfEntry = (logger, intent) => {
const qr = intent.query_result;
if (!qr || !qr.fulfillment_messages || !qr.output_contexts) {
logger.info({f: qr.fulfillment_messages, o: qr.output_contexts}, 'no dtmfs');
return;
}
// check for custom payloads with a gather verb
const custom = qr.fulfillment_messages.find((f) => f.payload && f.payload.verb === 'gather');
if (custom && custom.payload && custom.payload.verb === 'gather') {
logger.info({custom}, 'found dtmf custom payload');
return {
max: custom.payload.numDigits,
term: custom.payload.finishOnKey,
template: custom.payload.responseTemplate
};
}
// check for an output context with a specific naming convention
const context = qr.output_contexts.find((oc) => oc.name.includes('/contexts/allow-dtmf-'));
if (context) {
const arr = /allow-dtmf-(\d+)(?:-(\d+))?(?:-(.*))?/.exec(context.name);
if (arr) {
logger.info({custom}, 'found dtmf output context');
return {
min: parseInt(arr[1]),
max: arr.length > 2 ? parseInt(arr[2]) : null,
term: arr.length > 3 ? arr[3] : null
};
}
}
};

View File

@@ -0,0 +1,41 @@
class Transcription {
constructor(logger, evt) {
this.logger = logger;
this.recognition_result = evt.recognition_result;
}
get isEmpty() {
return !this.recognition_result;
}
get isFinal() {
return this.recognition_result && this.recognition_result.is_final === true;
}
get confidence() {
if (!this.isEmpty) return this.recognition_result.confidence;
}
get text() {
if (!this.isEmpty) return this.recognition_result.transcript;
}
startsWith(str) {
return (this.text.toLowerCase() || '').startsWith(str.toLowerCase());
}
includes(str) {
return (this.text.toLowerCase() || '').includes(str.toLowerCase());
}
toJSON() {
return {
final: this.recognition_result.is_final === true,
text: this.text,
confidence: this.confidence
};
}
}
module.exports = Transcription;

41
lib/tasks/dtmf.js Normal file
View File

@@ -0,0 +1,41 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
class TaskDtmf extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
this.dtmf = this.data.dtmf;
this.duration = this.data.duration || 500;
}
get name() { return TaskName.Dtmf; }
async exec(cs, ep) {
await super.exec(cs);
this.ep = ep;
try {
this.logger.info({data: this.data}, `sending dtmf ${this.dtmf}`);
await this.ep.execute('send_dtmf', `${this.dtmf}@${this.duration}`);
this.timer = setTimeout(this.notifyTaskDone.bind(this), this.dtmf.length * (this.duration + 250) + 750);
await this.awaitTaskDone();
this.logger.info({data: this.data}, `done sending dtmf ${this.dtmf}`);
} catch (err) {
this.logger.info(err, `TaskDtmf:exec - error playing ${this.dtmf}`);
}
this.emit('playDone');
}
async kill(cs) {
super.kill(cs);
if (this.ep.connected && !this.playComplete) {
this.logger.debug('TaskDtmf:kill - killing audio');
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
}
clearTimeout(this.timer);
this.notifyTaskDone();
}
}
module.exports = TaskDtmf;

View File

@@ -275,18 +275,19 @@ class TaskEnqueue extends Task {
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
}
const json = await cs.application.requestor.request(hook, params);
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
const allowedTasks = json.filter((task) => allowed.includes(task.verb));
if (json.length !== allowedTasks.length) {
this.logger.debug({json, allowedTasks}, 'unsupported task');
throw new Error(`unsupported verb in dial enqueue waitHook: only ${JSON.stringify(allowed)}`);
const allowedTasks = tasks.filter((t) => allowed.includes(t.verb));
if (tasks.length !== allowedTasks.length) {
this.logger.debug({tasks, allowedTasks}, 'unsupported task');
throw new Error(`unsupported verb in enqueue waitHook: only ${JSON.stringify(allowed)}`);
}
this.logger.debug(`TaskEnqueue:_playHook: executing ${json.length} tasks`);
this.logger.debug(`TaskEnqueue:_playHook: executing ${tasks.length} tasks`);
// check for 'leave' verb and only execute tasks up till then
const tasksToRun = [];
let leave = false;
for (const o of json) {
for (const o of tasks) {
if (o.verb === TaskName.Leave) {
leave = true;
this.logger.info('waitHook returned a leave task');
@@ -297,14 +298,13 @@ class TaskEnqueue extends Task {
if (this.killed) return [];
else if (tasksToRun.length > 0) {
const tasks = normalizeJambones(this.logger, tasksToRun).map((tdata) => makeTask(this.logger, tdata));
this._playSession = new ConfirmCallSession({
logger: this.logger,
application: cs.application,
dlg,
ep: cs.ep,
callInfo: cs.callInfo,
tasks
tasksToRun
});
await this._playSession.exec();
this._playSession = null;

View File

@@ -1,5 +1,11 @@
const Task = require('./task');
const {TaskName, TaskPreconditions, TranscriptionEvents} = require('../utils/constants');
const {
TaskName,
TaskPreconditions,
GoogleTranscriptionEvents,
AwsTranscriptionEvents
} = require('../utils/constants');
const makeTask = require('./make_task');
const assert = require('assert');
@@ -10,15 +16,23 @@ class TaskGather extends Task {
[
'finishOnKey', 'hints', 'input', 'numDigits',
'partialResultHook', 'profanityFilter',
'partialResultHook',
'speechTimeout', 'timeout', 'say', 'play'
].forEach((k) => this[k] = this.data[k]);
this.timeout = (this.timeout || 5) * 1000;
this.interim = this.partialResultCallback;
if (this.data.recognizer) {
this.language = this.data.recognizer.language || 'en-US';
this.vendor = this.data.recognizer.vendor;
const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor;
this.language = recognizer.language;
this.hints = recognizer.hints || [];
this.altLanguages = recognizer.altLanguages || [];
/* aws options */
this.vocabularyName = recognizer.vocabularyName;
this.vocabularyFilterName = recognizer.vocabularyFilterName;
this.filterMethod = recognizer.filterMethod;
}
this.digitBuffer = '';
@@ -37,7 +51,23 @@ class TaskGather extends Task {
async exec(cs, ep) {
await super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
this.ep = ep;
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
if (!this.sttCredentials) {
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but not creds supplied`);
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
throw new Error(`no speech-to-text service credentials for ${this.vendor} have been configured`);
}
try {
if (this.sayTask) {
@@ -55,8 +85,10 @@ class TaskGather extends Task {
else this._startTimer();
if (this.input.includes('speech')) {
await this._initSpeech(ep);
await this._initSpeech(cs, ep);
this._startTranscribing(ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */});
}
if (this.input.includes('digits')) {
@@ -67,13 +99,15 @@ class TaskGather extends Task {
} catch (err) {
this.logger.error(err, 'TaskGather:exec error');
}
ep.removeCustomEventListener(TranscriptionEvents.Transcription);
ep.removeCustomEventListener(TranscriptionEvents.EndOfUtterance);
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
}
kill(cs) {
super.kill(cs);
this._killAudio();
this.ep.removeAllListeners('dtmf');
this._resolve('killed');
}
@@ -87,34 +121,65 @@ class TaskGather extends Task {
this._killAudio();
}
async _initSpeech(ep) {
const opts = {
GOOGLE_SPEECH_USE_ENHANCED: true,
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
GOOGLE_SPEECH_MODEL: 'command_and_search'
};
if (this.hints) {
Object.assign(opts, {'GOOGLE_SPEECH_HINTS': this.hints.join(',')});
async _initSpeech(cs, ep) {
const opts = {};
if ('google' === this.vendor) {
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
Object.assign(opts, {
GOOGLE_SPEECH_USE_ENHANCED: true,
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
GOOGLE_SPEECH_MODEL: 'command_and_search'
});
if (this.hints && this.hints.length > 1) opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
if (this.altLanguages && this.altLanguages.length > 1) {
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
}
if (this.profanityFilter === true) {
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
}
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
}
if (this.profanityFilter === true) {
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
else {
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
if (this.vocabularyFilterName) {
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
}
Object.assign(opts, {
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
AWS_REGION: this.sttCredentials.region
});
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
}
this.logger.debug(`setting freeswitch vars ${JSON.stringify(opts)}`);
this.logger.debug({vars: opts}, 'setting freeswitch vars');
await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error set'));
ep.addCustomEventListener(TranscriptionEvents.Transcription, this._onTranscription.bind(this, ep));
ep.addCustomEventListener(TranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, ep));
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
}
_startTranscribing(ep) {
ep.startTranscription({
vendor: this.vendor,
locale: this.language,
interim: this.partialResultCallback ? true : false,
language: this.language || this.callSession.speechRecognizerLanguage
}).catch((err) => this.logger.error(err, 'TaskGather:_startTranscribing error'));
}).catch((err) => {
const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error');
writeAlerts({
account_sid: this.cs.accountSid,
alert_type: AlertType.STT_FAILURE,
vendor: this.vendor,
detail: err.message
});
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
}
_startTimer() {
assert(!this._timeoutTimer);
this.logger.debug(`Gather:_startTimer: timeout ${this.timeout}`);
this._timeoutTimer = setTimeout(() => this._resolve('timeout'), this.timeout);
}
@@ -136,22 +201,35 @@ class TaskGather extends Task {
}
}
_onTranscription(ep, evt) {
_onTranscription(cs, ep, evt) {
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
this.logger.debug(evt, 'TaskGather:_onTranscription');
if (evt.is_final) this._resolve('speech', evt);
const final = evt.is_final;
if (final) {
this._resolve('speech', evt);
}
else if (this.partialResultHook) {
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
}
}
_onEndOfUtterance(ep, evt) {
this.logger.info(evt, 'TaskGather:_onEndOfUtterance');
this._startTranscribing(ep);
_onEndOfUtterance(cs, ep) {
this.logger.info('TaskGather:_onEndOfUtterance');
if (!this.resolved && !this.killed) {
this._startTranscribing(ep);
}
}
async _resolve(reason, evt) {
if (this.resolved) return;
this.resolved = true;
this.logger.debug(`TaskGather:resolve with reason ${reason}`);
if (this.ep && this.ep.connected) {
this.ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));
}
this._clearTimer();
if (reason.startsWith('dtmf')) {
await this.performAction({digits: this.digitBuffer});

305
lib/tasks/lex.js Normal file
View File

@@ -0,0 +1,305 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const normalizeJambones = require('../utils/normalize-jambones');
class Lex extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
if (this.data.credentials) {
this.awsAccessKeyId = this.data.credentials.accessKey;
this.awsSecretAccessKey = this.data.credentials.secretAccessKey;
}
this.bot = this.data.botId;
this.alias = this.data.botAlias;
this.region = this.data.region;
this.locale = this.data.locale || 'en_US';
this.intent = this.data.intent || {};
this.metadata = this.data.metadata;
this.welcomeMessage = this.data.welcomeMessage;
this.bargein = this.data.bargein || false;
this.passDtmf = this.data.passDtmf || false;
if (this.data.noInputTimeout) this.noInputTimeout = this.data.noInputTimeout * 1000;
if (this.data.tts) {
this.vendor = this.data.tts.vendor || 'default';
this.language = this.data.tts.language || 'default';
this.voice = this.data.tts.voice || 'default';
}
this.botName = `${this.bot}:${this.alias}:${this.region}`;
if (this.data.eventHook) this.eventHook = this.data.eventHook;
this.events = this.eventHook ?
[
'intent',
'transcription',
'dtmf',
'start-play',
'stop-play',
'play-interrupted',
'response-text'
] : [];
if (this.data.actionHook) this.actionHook = this.data.actionHook;
}
get name() { return TaskName.Lex; }
async exec(cs, ep) {
await super.exec(cs);
try {
await this.init(cs, ep);
// kick it off
const obj = {};
let cmd = `${this.ep.uuid} ${this.bot} ${this.alias} ${this.region} ${this.locale} `;
if (this.metadata) Object.assign(obj, this.metadata);
if (this.intent.name) {
cmd += this.intent.name;
if (this.intent.slots) Object.assign(obj, {slots: this.intent.slots});
}
if (Object.keys(obj).length > 0) cmd += ` '${JSON.stringify(obj)}'`;
this.logger.debug({cmd}, `starting lex bot ${this.botName} with locale ${this.locale}`);
this.ep.api('aws_lex_start', cmd)
.catch((err) => {
this.logger.error({err}, `Error starting lex bot ${this.botName}`);
this.notifyTaskDone();
});
await this.awaitTaskDone();
} catch (err) {
this.logger.error({err}, 'Lex:exec error');
}
}
async kill(cs) {
super.kill(cs);
if (this.ep.connected) {
this.logger.debug('Lex:kill');
this.ep.removeCustomEventListener('lex::intent');
this.ep.removeCustomEventListener('lex::transcription');
this.ep.removeCustomEventListener('lex::audio_provided');
this.ep.removeCustomEventListener('lex::text_response');
this.ep.removeCustomEventListener('lex::playback_interruption');
this.ep.removeCustomEventListener('lex::error');
this.ep.removeAllListeners('dtmf');
this.performAction({lexResult: 'caller hungup'})
.catch((err) => this.logger.error({err}, 'lex - error w/ action webook'));
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
}
this.notifyTaskDone();
}
async init(cs, ep) {
this.ep = ep;
try {
if (this.vendor === 'default') {
this.vendor = cs.speechSynthesisVendor;
this.language = cs.speechSynthesisLanguage;
this.voice = cs.speechSynthesisVoice;
}
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
this.ep.addCustomEventListener('lex::intent', this._onIntent.bind(this, ep, cs));
this.ep.addCustomEventListener('lex::transcription', this._onTranscription.bind(this, ep, cs));
this.ep.addCustomEventListener('lex::audio_provided', this._onAudioProvided.bind(this, ep, cs));
this.ep.addCustomEventListener('lex::text_response', this._onTextResponse.bind(this, ep, cs));
this.ep.addCustomEventListener('lex::playback_interruption', this._onPlaybackInterruption.bind(this, ep, cs));
this.ep.addCustomEventListener('lex::error', this._onError.bind(this, ep, cs));
this.ep.on('dtmf', this._onDtmf.bind(this, ep, cs));
const channelVars = {};
if (this.bargein) {
Object.assign(channelVars, {'x-amz-lex:barge-in-enabled': 1});
}
if (this.noInputTimeout) {
Object.assign(channelVars, {'x-amz-lex:audio:start-timeout-ms': this.noInputTimeout});
}
if (this.awsAccessKeyId && this.awsSecretAccessKey) {
Object.assign(channelVars, {
AWS_ACCESS_KEY_ID: this.awsAccessKeyId,
AWS_SECRET_ACCESS_KEY: this.awsSecretAccessKey
});
}
if (this.vendor) Object.assign(channelVars, {LEX_USE_TTS: 1});
//if (this.intent.name) Object.assign(channelVars, {LEX_WELCOME_INTENT: this.intent});
if (this.welcomeMessage && this.welcomeMessage.length) {
Object.assign(channelVars, {LEX_WELCOME_MESSAGE: this.welcomeMessage});
}
if (Object.keys(channelVars).length) await this.ep.set(channelVars);
} catch (err) {
this.logger.error({err}, 'Error setting listeners');
throw err;
}
}
/**
* An intent has been returned.
* we may get an empty intent, signified by ...
* In such a case, we just restart the bot.
* @param {*} ep - media server endpoint
* @param {*} evt - event data
*/
_onIntent(ep, cs, evt) {
this.logger.debug({evt}, `got intent for ${this.botName}`);
if (this.events.includes('intent')) {
this._performHook(cs, this.eventHook, {event: 'intent', data: evt});
}
}
/**
* A transcription - either interim or final - has been returned.
* If we are doing barge-in based on hotword detection, check for the hotword or phrase.
* If we are playing a filler sound, like typing, during the fullfillment phase, start that
* if this is a final transcript.
* @param {*} ep - media server endpoint
* @param {*} evt - event data
*/
_onTranscription(ep, cs, evt) {
this.logger.debug({evt}, `got transcription for ${this.botName}`);
if (this.events.includes('transcription')) {
this._performHook(cs, this.eventHook, {event: 'transcription', data: evt});
}
}
/**
* @param {*} evt - event data
*/
async _onTextResponse(ep, cs, evt) {
this.logger.debug({evt}, `got text response for ${this.botName}`);
const messages = evt.messages;
if (this.events.includes('response-text')) {
this._performHook(cs, this.eventHook, {event: 'response-text', data: evt});
}
if (this.vendor && Array.isArray(messages) && messages.length) {
const msg = messages[0].msg;
const type = messages[0].type;
if (['PlainText', 'SSML'].includes(type) && msg) {
const {srf} = cs;
const {synthAudio} = srf.locals.dbHelpers;
try {
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
// eslint-disable-next-line no-unused-vars
const {filePath, servedFromCache} = await synthAudio({
text: msg,
vendor: this.vendor,
language: this.language,
voice: this.voice,
salt: cs.callSid,
credentials: this.ttsCredentials
});
if (filePath) cs.trackTmpFile(filePath);
if (this.events.includes('start-play')) {
this._performHook(cs, this.eventHook, {event: 'start-play', data: {path: filePath}});
}
await ep.play(filePath);
if (this.events.includes('stop-play')) {
this._performHook(cs, this.eventHook, {event: 'stop-play', data: {path: filePath}});
}
this.logger.debug(`finished tts, sending play_done ${this.vendor} ${this.voice}`);
this.ep.api('aws_lex_play_done', this.ep.uuid)
.catch((err) => {
this.logger.error({err}, `Error sending play_done ${this.botName}`);
});
} catch (err) {
this.logger.error({err}, 'Lex:_onTextResponse - error playing tts');
}
}
}
}
/**
* @param {*} evt - event data
*/
_onPlaybackInterruption(ep, cs, evt) {
this.logger.debug({evt}, `got playback interruption for ${this.botName}`);
if (this.bargein) {
if (this.events.includes('play-interrupted')) {
this._performHook(cs, this.eventHook, {event: 'play-interrupted', data: {}});
}
this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Lex::_onPlaybackInterruption - Error killing audio'));
}
}
/**
* Lex has returned an error of some kind.
* @param {*} evt - event data
*/
_onError(ep, cs, evt) {
this.logger.error({evt}, `got error for bot ${this.botName}`);
}
/**
* Audio has been received from lex and written to a temporary disk file.
* Start playing the audio, after killing any filler sound that might be playing.
* When the audio completes, start the no-input timer.
* @param {*} ep - media server endpoint
* @param {*} evt - event data
*/
async _onAudioProvided(ep, cs, evt) {
if (this.vendor) return;
this.waitingForPlayStart = false;
this.logger.debug({evt}, `got audio file for bot ${this.botName}`);
try {
if (this.events.includes('start-play')) {
this._performHook(cs, this.eventHook, {event: 'start-play', data: {path: evt.path}});
}
await ep.play(evt.path);
if (this.events.includes('stop-play')) {
this._performHook(cs, this.eventHook, {event: 'stop-play', data: {path: evt.path}});
}
this.logger.debug({evt}, `done playing audio file for bot ${this.botName}`);
this.ep.api('aws_lex_play_done', this.ep.uuid)
.catch((err) => {
this.logger.error({err}, `Error sending play_done ${this.botName}`);
});
} catch (err) {
this.logger.error({err}, `Error playing file ${evt.path} for both ${this.botName}`);
}
}
/**
* receive a dmtf entry from the caller.
* If we have active dtmf instructions, collect and process accordingly.
*/
_onDtmf(ep, cs, evt) {
this.logger.debug({evt}, 'Lex:_onDtmf');
if (this.events.includes('dtmf')) {
this._performHook(cs, this.eventHook, {event: 'dtmf', data: evt});
}
if (this.passDtmf) {
this.ep.api('aws_lex_dtmf', `${this.ep.uuid} ${evt.dtmf}`)
.catch((err) => {
this.logger.error({err}, `Error sending dtmf ${evt.dtmf} ${this.botName}`);
});
}
}
async _performHook(cs, hook, results) {
const json = await this.cs.requestor.request(hook, results);
if (json && Array.isArray(json)) {
const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
this.performAction({lexResult: 'redirect'}, false);
cs.replaceApplication(tasks);
}
}
}
}
module.exports = Lex;

View File

@@ -159,6 +159,24 @@ class TaskListen extends Task {
this.notifyTaskDone();
}
/**
* play or say something during the call
* @param {*} tasks - array of play/say tasks to execute
*/
async whisper(tasks, callSid) {
try {
const cs = this.callSession;
this.logger.debug('Listen:whisper tasks starting');
while (tasks.length && !cs.callGone) {
const task = tasks.shift();
await task.exec(cs, this.ep);
}
this.logger.debug('Listen:whisper tasks complete');
} catch (err) {
this.logger.error(err, 'Listen:whisper error');
}
}
}
module.exports = TaskListen;

View File

@@ -24,9 +24,15 @@ function makeTask(logger, obj, parent) {
case TaskName.Dial:
const TaskDial = require('./dial');
return new TaskDial(logger, data, parent);
case TaskName.Dialogflow:
const TaskDialogflow = require('./dialogflow');
return new TaskDialogflow(logger, data, parent);
case TaskName.Dequeue:
const TaskDequeue = require('./dequeue');
return new TaskDequeue(logger, data, parent);
case TaskName.Dtmf:
const TaskDtmf = require('./dtmf');
return new TaskDtmf(logger, data, parent);
case TaskName.Enqueue:
const TaskEnqueue = require('./enqueue');
return new TaskEnqueue(logger, data, parent);
@@ -36,6 +42,12 @@ function makeTask(logger, obj, parent) {
case TaskName.Leave:
const TaskLeave = require('./leave');
return new TaskLeave(logger, data, parent);
case TaskName.Lex:
const TaskLex = require('./lex');
return new TaskLex(logger, data, parent);
case TaskName.Message:
const TaskMessage = require('./message');
return new TaskMessage(logger, data, parent);
case TaskName.Say:
const TaskSay = require('./say');
return new TaskSay(logger, data, parent);

89
lib/tasks/message.js Normal file
View File

@@ -0,0 +1,89 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const bent = require('bent');
class TaskMessage extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.None;
this.payload = {
message_sid: this.data.message_sid,
provider: this.data.provider,
to: this.data.to,
from: this.data.from,
cc: this.data.cc,
text: this.data.text,
media: this.data.media
};
}
get name() { return TaskName.Message; }
/**
* Send outbound SMS
*/
async exec(cs) {
const {srf, accountSid} = cs;
const {res} = cs.callInfo;
let payload = this.payload;
await super.exec(cs);
try {
const {getSBC, getSmpp, dbHelpers} = srf.locals;
const {lookupSmppGateways} = dbHelpers;
this.logger.info(`looking up gateways for account_sid: ${accountSid}`);
const r = await lookupSmppGateways(accountSid);
let gw, url, relativeUrl;
if (r.length > 0) {
if (this.payload.provider) gw = r.find((o) => o.vc.name === this.payload.provider);
else gw = r[0];
}
if (gw) {
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
url = getSmpp();
relativeUrl = '/sms';
payload = {
...payload,
...gw.sg,
...gw.vc
};
}
else {
this.logger.info({gw, accountSid, provider: this.payload.provider},
'Message:exec - no smpp gateways found to send message');
relativeUrl = 'v1/outboundSMS';
const sbcAddress = getSBC();
if (sbcAddress) url = `http://${sbcAddress}:3000/`;
//TMP: smpp only at the moment, need to add http back in
return res.sendStatus(404);
}
if (url) {
const post = bent(url, 'POST', 'json', 201);
this.logger.info({payload, url}, 'Message:exec sending outbound SMS');
const response = await post(relativeUrl, payload);
this.logger.info({response}, 'Successfully sent SMS');
if (cs.callInfo.res) {
this.logger.info('Message:exec sending 200 OK response to HTTP POST from api server');
res.status(200).json({
sid: cs.callInfo.messageSid,
providerResponse: response
});
}
// TODO: action Hook
}
else {
this.logger.info('Message:exec - unable to send SMS as there are no available SMS gateways');
res.status(422).json({message: 'no configured SMS gateways'});
}
} catch (err) {
this.logger.error(err, 'TaskMessage:exec - Error sending SMS');
res.status(422).json({message: 'no configured SMS gateways'});
}
}
}
module.exports = TaskMessage;

View File

@@ -15,29 +15,64 @@ class TaskSay extends Task {
get name() { return TaskName.Say; }
async exec(cs, ep) {
const {srf} = cs;
const {synthAudio} = srf.locals.dbHelpers;
await super.exec(cs);
const {srf} = cs;
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
const {writeAlerts, AlertType} = srf.locals;
const {synthAudio} = srf.locals.dbHelpers;
const vendor = this.synthesizer.vendor || cs.speechSynthesisVendor;
const language = this.synthesizer.language || cs.speechSynthesisLanguage;
const voice = this.synthesizer.voice || cs.speechSynthesisVoice;
const salt = cs.callSid;
const credentials = cs.getSpeechCredentials(vendor, 'tts');
this.ep = ep;
try {
const filepath = [];
while (!this.killed && this.loop--) {
if (!credentials) {
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
throw new Error('no provisioned speech credentials for TTS');
}
// synthesize all of the text elements
let lastUpdated = false;
const filepath = (await Promise.all(this.text.map(async(text) => {
const {filePath, servedFromCache} = await synthAudio({
text,
vendor,
language,
voice,
salt,
credentials
}).catch((err) => {
this.logger.info(err, 'Error synthesizing tts');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor,
detail: err.message
});
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
if (filePath) cs.trackTmpFile(filePath);
if (!servedFromCache && !lastUpdated) {
lastUpdated = true;
updateSpeechCredentialLastUsed(credentials.speech_credential_sid)
.catch(() => {/*already logged error */});
}
return filePath;
}))).filter((fp) => fp && fp.length);
this.logger.debug({filepath}, 'synthesized files for tts');
while (!this.killed && this.loop-- && this.ep.connected) {
let segment = 0;
do {
if (filepath.length <= segment) {
const opts = Object.assign({
text: this.text[segment],
vendor: cs.speechSynthesisVendor,
language: cs.speechSynthesisLanguage,
voice: cs.speechSynthesisVoice,
salt: cs.callSid
}, this.synthesizer);
const path = await synthAudio(opts);
filepath.push(path);
cs.trackTmpFile(path);
}
await ep.play(filepath[segment]);
} while (++segment < this.text.length);
} while (!this.killed && ++segment < filepath.length);
}
} catch (err) {
this.logger.info(err, 'TaskSay:exec error');

View File

@@ -1,5 +1,5 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
const {TaskName, TaskPreconditions, CallStatus} = require('../utils/constants');
/**
* Rejects an incoming call with user-specified status code and reason
@@ -19,6 +19,7 @@ class TaskSipDecline extends Task {
res.send(this.data.status, this.data.reason, {
headers: this.headers
});
cs.emit('callStatusChange', {callStatus: CallStatus.Failed, sipStatus: this.data.status});
}
}

View File

@@ -112,12 +112,70 @@
"target": ["#target"],
"timeLimit": "number",
"timeout": "number",
"proxy": "string",
"transcribe": "#transcribe"
},
"required": [
"target"
]
},
"dialogflow": {
"properties": {
"credentials": "object|string",
"project": "string",
"environment": "string",
"lang": "string",
"actionHook": "object|string",
"eventHook": "object|string",
"events": "[string]",
"welcomeEvent": "string",
"welcomeEventParams": "object",
"noInputTimeout": "number",
"noInputEvent": "string",
"passDtmfAsTextInput": "boolean",
"thinkingMusic": "string",
"tts": "#synthesizer",
"bargein": "boolean"
},
"required": [
"project",
"credentials",
"lang"
]
},
"dtmf": {
"properties": {
"dtmf": "string",
"duration": "number"
},
"required": [
"dtmf"
]
},
"lex": {
"properties": {
"botId": "string",
"botAlias": "string",
"credentials": "object",
"region": "string",
"locale": "string",
"intent": "#lexIntent",
"welcomeMessage": "string",
"metadata": "object",
"bargein": "boolean",
"passDtmf": "boolean",
"actionHook": "object|string",
"eventHook": "object|string",
"noInputTimeout": "number",
"tts": "#synthesizer"
},
"required": [
"botId",
"botAlias",
"region",
"credentials"
]
},
"listen": {
"properties": {
"actionHook": "object|string",
@@ -142,6 +200,22 @@
"url"
]
},
"message": {
"properties": {
"carrier": "string",
"account_sid": "string",
"message_sid": "string",
"to": "string",
"from": "string",
"text": "string",
"media": "string|array",
"actionHook": "object|string"
},
"required": [
"to",
"from"
]
},
"pause": {
"properties": {
"length": "number"
@@ -167,6 +241,7 @@
"from": "string",
"speech_synthesis_vendor": "string",
"speech_synthesis_voice": "string",
"speech_synthesis_language": "string",
"speech_recognizer_vendor": "string",
"speech_recognizer_language": "string",
"tag": "object",
@@ -195,7 +270,8 @@
"earlyMedia": "boolean"
},
"required": [
"transcriptionHook"
"transcriptionHook",
"recognizer"
]
},
"target": {
@@ -204,7 +280,7 @@
"type": "string",
"enum": ["phone", "sip", "user", "teams"]
},
"url": "string",
"confirmHook": "object|string",
"method": {
"type": "string",
"enum": ["GET", "POST"]
@@ -213,7 +289,8 @@
"number": "string",
"sipUri": "string",
"auth": "#auth",
"vmail": "boolean"
"vmail": "boolean",
"tenant": "string"
},
"required": [
"type"
@@ -233,7 +310,7 @@
"properties": {
"vendor": {
"type": "string",
"enum": ["google", "aws", "polly"]
"enum": ["google", "aws", "polly", "default"]
},
"language": "string",
"voice": "string",
@@ -250,16 +327,59 @@
"properties": {
"vendor": {
"type": "string",
"enum": ["google"]
"enum": ["google", "aws", "default"]
},
"language": "string",
"hints": "array",
"altLanguages": "array",
"profanityFilter": "boolean",
"interim": "boolean",
"dualChannel": "boolean"
"singleUtterance": "boolean",
"dualChannel": "boolean",
"separateRecognitionPerChannel": "boolean",
"punctuation": "boolean",
"enhancedModel": "boolean",
"words": "boolean",
"diarization": "boolean",
"diarizationMinSpeakers": "number",
"diarizationMaxSpeakers": "number",
"interactionType": {
"type": "string",
"enum": [
"unspecified",
"discussion",
"presentation",
"phone_call",
"voicemail",
"voice_search",
"voice_command",
"dictation"
]
},
"naicsCode": "number",
"identifyChannels": "boolean",
"vocabularyName": "string",
"vocabularyFilterName": "string",
"filterMethod": {
"type": "string",
"enum": [
"remove",
"mask",
"tag"
]
}
},
"required": [
"vendor"
]
},
"lexIntent": {
"properties": {
"name": "string",
"slots": "object"
},
"required": [
"name"
]
}
}

View File

@@ -1,5 +1,5 @@
const Emitter = require('events');
const uuidv4 = require('uuid/v4');
const { v4: uuidv4 } = require('uuid');
const debug = require('debug')('jambonz:feature-server');
const assert = require('assert');
const {TaskPreconditions} = require('../utils/constants');
@@ -106,12 +106,12 @@ class Task extends Emitter {
delete obj.requestor;
delete obj.notifier;
obj.tasks = cs.getRemainingTaskData();
if (opts && obj.tasks.length > 1) {
if (opts && obj.tasks.length > 0) {
const key = Object.keys(obj.tasks[0])[0];
Object.assign(obj.tasks[0][key], {_: opts});
}
this.logger.debug({obj}, 'Task:_doRefer');
this.logger.debug({obj}, 'Task:_doRefer - final object to store for receiving session on othe server');
const success = await addKey(uuid, JSON.stringify(obj), 30);
if (!success) {
@@ -200,6 +200,7 @@ class Task extends Emitter {
}
if (required.length > 0) throw new Error(`${name}: missing value for ${required}`);
}
}
module.exports = Task;

View File

@@ -1,5 +1,10 @@
const Task = require('./task');
const {TaskName, TaskPreconditions, TranscriptionEvents} = require('../utils/constants');
const {
TaskName,
TaskPreconditions,
GoogleTranscriptionEvents,
AwsTranscriptionEvents
} = require('../utils/constants');
class TaskTranscribe extends Task {
constructor(logger, opts, parentTask) {
@@ -8,34 +13,70 @@ class TaskTranscribe extends Task {
this.transcriptionHook = this.data.transcriptionHook;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
if (this.data.recognizer) {
this.language = this.data.recognizer.language || 'en-US';
this.vendor = this.data.recognizer.vendor;
this.interim = this.data.recognizer.interim === true;
this.dualChannel = this.data.recognizer.dualChannel === true;
}
const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor;
this.language = recognizer.language;
this.interim = !!recognizer.interim;
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
/* google-specific options */
this.hints = recognizer.hints || [];
this.profanityFilter = recognizer.profanityFilter;
this.punctuation = !!recognizer.punctuation;
this.enhancedModel = !!recognizer.enhancedModel;
this.words = !!recognizer.words;
this.diarization = !!recognizer.diarization;
this.diarizationMinSpeakers = recognizer.diarizationMinSpeakers || 0;
this.diarizationMaxSpeakers = recognizer.diarizationMaxSpeakers || 0;
this.interactionType = recognizer.interactionType || 'unspecified';
this.naicsCode = recognizer.naicsCode || 0;
this.altLanguages = recognizer.altLanguages || [];
/* aws-specific options */
this.identifyChannels = !!recognizer.identifyChannels;
this.vocabularyName = recognizer.vocabularyName;
this.vocabularyFilterName = recognizer.vocabularyFilterName;
this.filterMethod = recognizer.filterMethod;
}
get name() { return TaskName.Transcribe; }
async exec(cs, ep, parentTask) {
super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
this.ep = ep;
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
try {
await this._startTranscribing(ep);
if (!this.sttCredentials) {
// TODO: generate alert (actually should be done by cs.getSpeechCredentials)
throw new Error('no provisioned speech credentials for TTS');
}
await this._startTranscribing(cs, ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */});
await this.awaitTaskDone();
} catch (err) {
this.logger.info(err, 'TaskTranscribe:exec - error');
}
ep.removeCustomEventListener(TranscriptionEvents.Transcription);
ep.removeCustomEventListener(TranscriptionEvents.NoAudioDetected);
ep.removeCustomEventListener(TranscriptionEvents.MaxDurationExceeded);
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected);
ep.removeCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded);
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AwsTranscriptionEvents.NoAudioDetected);
ep.removeCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded);
}
async kill(cs) {
super.kill(cs);
if (this.ep.connected) {
this.ep.stopTranscription().catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
this.ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
// hangup after 1 sec if we don't get a final transcription
this._timer = setTimeout(() => this.notifyTaskDone(), 1000);
@@ -44,40 +85,101 @@ class TaskTranscribe extends Task {
await this.awaitTaskDone();
}
async _startTranscribing(ep) {
const opts = {
GOOGLE_SPEECH_USE_ENHANCED: true,
GOOGLE_SPEECH_MODEL: 'phone_call'
};
if (this.hints) {
Object.assign(opts, {'GOOGLE_SPEECH_HINTS': this.hints.join(',')});
}
if (this.profanityFilter) {
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
}
if (this.dualChannel) {
Object.assign(opts, {'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL': true});
}
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing'));
async _startTranscribing(cs, ep) {
const opts = {};
ep.addCustomEventListener(TranscriptionEvents.Transcription, this._onTranscription.bind(this, ep));
ep.addCustomEventListener(TranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, ep));
ep.addCustomEventListener(TranscriptionEvents.MaxDurationExceeded, this._onMaxDurationExceeded.bind(this, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep));
if (this.vendor === 'google') {
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
['profanityFilter', 'GOOGLE_SPEECH_PROFANITY_FILTER'],
['punctuation', 'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION'],
['words', 'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS'],
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
});
if (this.hints.length > 1) opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
if (this.altLanguages.length > 1) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
if ('unspecified' !== this.interactionType) {
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
// additionally set model if appropriate
if ('phone_call' === this.interactionType) opts.GOOGLE_SPEECH_MODEL = 'phone_call';
else if (['voice_search', 'voice_command'].includes(this.interactionType)) {
opts.GOOGLE_SPEECH_MODEL = 'command_and_search';
}
else opts.GOOGLE_SPEECH_MODEL = 'phone_call';
}
else opts.GOOGLE_SPEECH_MODEL = 'phone_call';
if (this.diarization && this.diarizationMinSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
}
if (this.diarization && this.diarizationMaxSpeakers > 0) {
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT = this.diarizationMaxSpeakers;
}
if (this.naicsCode > 0) opts.GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE = this.naicsCode;
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with google'));
}
else if (this.vendor === 'aws') {
[
['diarization', 'AWS_SHOW_SPEAKER_LABEL'],
['identifyChannels', 'AWS_ENABLE_CHANNEL_IDENTIFICATION']
].forEach((arr) => {
if (this[arr[0]]) opts[arr[1]] = true;
});
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
if (this.vocabularyFilterName) {
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
}
if (this.sttCredentials) {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
AWS_REGION: this.sttCredentials.region
});
}
else {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
AWS_REGION: process.env.AWS_REGION
});
}
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
}
await this._transcribe(ep);
}
async _transcribe(ep) {
await this.ep.startTranscription({
await ep.startTranscription({
vendor: this.vendor,
interim: this.interim ? true : false,
language: this.language || this.callSession.speechRecognizerLanguage,
channels: this.dualChannel ? 2 : 1
locale: this.language,
channels: this.separateRecognitionPerChannel ? 2 : 1
});
}
_onTranscription(ep, evt) {
_onTranscription(cs, ep, evt) {
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
if (this.killed) {
@@ -87,12 +189,12 @@ class TaskTranscribe extends Task {
}
}
_onNoAudio(ep) {
_onNoAudio(cs, ep) {
this.logger.debug('TaskTranscribe:_onNoAudio restarting transcription');
this._transcribe(ep);
}
_onMaxDurationExceeded(ep) {
_onMaxDurationExceeded(cs, ep) {
this.logger.debug('TaskTranscribe:_onMaxDurationExceeded restarting transcription');
this._transcribe(ep);
}

View File

@@ -3,11 +3,15 @@
"Conference": "conference",
"Dequeue": "dequeue",
"Dial": "dial",
"Dialogflow": "dialogflow",
"Dtmf": "dtmf",
"Enqueue": "enqueue",
"Gather": "gather",
"Hangup": "hangup",
"Leave": "leave",
"Lex": "lex",
"Listen": "listen",
"Message": "message",
"Pause": "pause",
"Play": "play",
"Redirect": "redirect",
@@ -33,7 +37,8 @@
},
"CallDirection": {
"Inbound": "inbound",
"Outbound": "outbound"
"Outbound": "outbound",
"None": "none"
},
"ListenStatus": {
"Pause": "pause",
@@ -46,12 +51,18 @@
"StableCall": "stable-call",
"UnansweredCall": "unanswered-call"
},
"TranscriptionEvents": {
"GoogleTranscriptionEvents": {
"Transcription": "google_transcribe::transcription",
"EndOfUtterance": "google_transcribe::end_of_utterance",
"NoAudioDetected": "google_transcribe::no_audio_detected",
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded"
},
"AwsTranscriptionEvents": {
"Transcription": "aws_transcribe::transcription",
"EndOfTranscript": "aws_transcribe::end_of_transcript",
"NoAudioDetected": "aws_transcribe::no_audio_detected",
"MaxDurationExceeded": "aws_transcribe::max_duration_exceeded"
},
"ListenEvents": {
"Connect": "mod_audio_fork::connect",
"ConnectFailure": "mod_audio_fork::connect_failed",

74
lib/utils/db-utils.js Normal file
View File

@@ -0,0 +1,74 @@
const {decrypt} = require('./encrypt-decrypt');
const sqlAccountDetails = `SELECT *
FROM accounts account
WHERE account.account_sid = ?`;
const sqlSpeechCredentials = `SELECT *
FROM speech_credentials
WHERE account_sid = ? `;
const sqlSpeechCredentialsForSP = `SELECT *
FROM speech_credentials
WHERE service_provider_sid =
(SELECT service_provider_sid from accounts where account_sid = ?)`;
const speechMapper = (cred) => {
const {credential, ...obj} = cred;
if ('google' === obj.vendor) {
obj.service_key = decrypt(credential);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = o.secret_access_key;
}
return obj;
};
module.exports = (logger, srf) => {
const {pool} = srf.locals.dbHelpers;
const pp = pool.promise();
const lookupAccountDetails = async(account_sid) => {
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, account_sid);
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
const speech = r2.map(speechMapper);
/* search at the service provider level if we don't find it at the account level */
const haveGoogle = speech.find((s) => s.vendor === 'google');
const haveAws = speech.find((s) => s.vendor === 'aws');
if (!haveGoogle || !haveAws) {
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
if (r3.length) {
if (!haveGoogle) {
const google = r3.find((s) => s.vendor === 'google');
if (google) speech.push(speechMapper(google));
}
if (!haveAws) {
const aws = r3.find((s) => s.vendor === 'aws');
if (aws) speech.push(speechMapper(aws));
}
}
}
return {
...r[0],
speech
};
};
const updateSpeechCredentialLastUsed = async(speech_credential_sid) => {
const pp = pool.promise();
const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?';
try {
await pp.execute(sql, [speech_credential_sid]);
} catch (err) {
logger.error({err}, `Error updating last_used for speech_credential_sid ${speech_credential_sid}`);
}
};
return {
lookupAccountDetails,
updateSpeechCredentialLastUsed
};
};

View File

@@ -0,0 +1,35 @@
const crypto = require('crypto');
const algorithm = 'aes-256-ctr';
const iv = crypto.randomBytes(16);
const secretKey = crypto.createHash('sha256')
.update(String(process.env.JWT_SECRET))
.digest('base64')
.substr(0, 32);
const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
const data = {
iv: iv.toString('hex'),
content: encrypted.toString('hex')
};
return JSON.stringify(data);
};
const decrypt = (data) => {
let hash;
try {
hash = JSON.parse(data);
} catch (err) {
console.log(`failed to parse json string ${data}`);
throw err;
}
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
return decrpyted.toString();
};
module.exports = {
encrypt,
decrypt
};

View File

@@ -26,7 +26,7 @@ function installSrfLocals(srf, logger) {
logger.debug('installing srf locals');
assert(!srf.locals.dbHelpers);
const {getSBC, lifecycleEmitter} = require('./sbc-pinger')(logger);
const StatsCollector = require('jambonz-stats-collector');
const StatsCollector = require('@jambonz/stats-collector');
const stats = srf.locals.stats = new StatsCollector(logger);
// freeswitch connections (typically we connect to only one)
@@ -38,9 +38,12 @@ function installSrfLocals(srf, logger) {
const fsInventory = process.env.JAMBONES_FREESWITCH
.split(',')
.map((fs) => {
const arr = /^(.*):(.*):(.*)/.exec(fs);
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
assert.ok(arr, `Invalid syntax JAMBONES_FREESWITCH: ${process.env.JAMBONES_FREESWITCH}`);
return {address: arr[1], port: arr[2], secret: arr[3]};
const opts = {address: arr[1], port: arr[2], secret: arr[3]};
if (arr.length > 4) opts.advertisedAddress = arr[4];
if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
return opts;
});
logger.info({fsInventory}, 'freeswitch inventory');
@@ -72,6 +75,7 @@ function installSrfLocals(srf, logger) {
// if we have a single freeswitch (as is typical) report stats periodically
if (mediaservers.length === 1) {
srf.locals.mediaservers = [mediaservers[0].ms];
setInterval(() => {
try {
if (mediaservers[0].ms && mediaservers[0].active) {
@@ -99,19 +103,25 @@ function installSrfLocals(srf, logger) {
}
const {
pool,
lookupAppByPhoneNumber,
lookupAppBySid,
lookupAppByRealm,
lookupAppByTeamsTenant,
lookupTeamsByAccount
lookupTeamsByAccount,
lookupAccountBySid,
lookupAccountCapacitiesBySid,
lookupSmppGateways
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
port: process.env.JAMBONES_MYSQL_PORT || 3306,
password: process.env.JAMBONES_MYSQL_PASSWORD,
database: process.env.JAMBONES_MYSQL_DATABASE,
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
}, logger);
const {
client,
updateCallStatus,
retrieveCall,
listCalls,
@@ -130,18 +140,31 @@ function installSrfLocals(srf, logger) {
removeFromList,
lengthOfList,
getListPosition
} = require('jambonz-realtimedb-helpers')({
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
writeAlerts,
AlertType
} = require('@jambonz/time-series')(logger, {
host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50,
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
});
Object.assign(srf.locals, {
srf.locals = {...srf.locals,
dbHelpers: {
client,
pool,
lookupAppByPhoneNumber,
lookupAppBySid,
lookupAppByRealm,
lookupAppByTeamsTenant,
lookupTeamsByAccount,
lookupAccountBySid,
lookupAccountCapacitiesBySid,
lookupSmppGateways,
updateCallStatus,
retrieveCall,
listCalls,
@@ -165,10 +188,15 @@ function installSrfLocals(srf, logger) {
ipv4: localIp,
serviceUrl: `http://${localIp}:${PORT}`,
getSBC,
getSmpp: () => {
return process.env.SMPP_URL;
},
lifecycleEmitter,
getFreeswitch,
stats: stats
});
stats: stats,
writeAlerts,
AlertType
};
}
module.exports = installSrfLocals;

View File

@@ -6,14 +6,15 @@ const CallInfo = require('../session/call-info');
const assert = require('assert');
const ConfirmCallSession = require('../session/confirm-call-session');
const selectSbc = require('./select-sbc');
const Registrar = require('jambonz-mw-registrar');
const Registrar = require('@jambonz/mw-registrar');
const AdultingCallSession = require('../session/adulting-call-session');
const registrar = new Registrar({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
});
const deepcopy = require('deepcopy');
const moment = require('moment');
const uuidv4 = require('uuid/v4');
const { v4: uuidv4 } = require('uuid');
class SingleDialer extends Emitter {
constructor({logger, sbcAddress, target, opts, application, callInfo}) {
@@ -59,6 +60,8 @@ class SingleDialer extends Emitter {
async exec(srf, ms, opts) {
opts = opts || {};
opts.headers = opts.headers || {};
opts.headers = {...opts.headers, 'X-Call-Sid': this.callSid};
let uri, to;
try {
switch (this.target.type) {
@@ -69,11 +72,10 @@ class SingleDialer extends Emitter {
to = this.target.number;
if ('teams' === this.target.type) {
assert(this.target.teamsInfo);
opts.headers = opts.headers || {};
Object.assign(opts.headers, {
opts.headers = {...opts.headers,
'X-MS-Teams-FQDN': this.target.teamsInfo.ms_teams_fqdn,
'X-MS-Teams-Tenant-FQDN': this.target.teamsInfo.tenant_fqdn
});
};
if (this.target.vmail === true) uri = `${uri};opaque=app:voicemail`;
}
break;
@@ -108,13 +110,22 @@ class SingleDialer extends Emitter {
this.ep = await ms.createEndpoint();
this.logger.debug(`SingleDialer:exec - created endpoint ${this.ep.uuid}`);
let sdp;
/**
* were we killed whilst we were off getting an endpoint ?
* https://github.com/jambonz/jambonz-feature-server/issues/30
*/
if (this.killed) {
this.logger.info('SingleDialer:exec got quick CANCEL from caller, abort outdial');
this.ep.destroy()
.catch((err) => this.logger.error({err}, 'Error destroying endpoint'));
return;
}
let lastSdp;
const connectStream = async(remoteSdp) => {
if (remoteSdp !== sdp) {
this.ep.modify(sdp = remoteSdp);
return true;
}
return false;
if (remoteSdp === lastSdp) return;
lastSdp = remoteSdp;
return this.ep.modify(remoteSdp);
};
Object.assign(opts, {
@@ -122,7 +133,7 @@ class SingleDialer extends Emitter {
localSdp: this.ep.local.sdp
});
if (this.target.auth) opts.auth = this.target.auth;
this.dlg = await srf.createUAC(uri, opts, {
this.dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
cbRequest: (err, req) => {
if (err) {
this.logger.error(err, 'SingleDialer:exec Error creating call');
@@ -153,14 +164,17 @@ class SingleDialer extends Emitter {
cbProvisional: (prov) => {
const status = {sipStatus: prov.status};
if ([180, 183].includes(prov.status) && prov.body) {
status.callStatus = CallStatus.EarlyMedia;
if (connectStream(prov.body)) this.emit('earlyMedia');
if (status.callStatus !== CallStatus.EarlyMedia) {
status.callStatus = CallStatus.EarlyMedia;
this.emit('earlyMedia');
}
connectStream(prov.body);
}
else status.callStatus = CallStatus.Ringing;
this.emit('callStatusChange', status);
}
});
connectStream(this.dlg.remote.sdp);
await connectStream(this.dlg.remote.sdp);
this.dlg.callSid = this.callSid;
this.inviteInProgress = null;
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
@@ -208,6 +222,7 @@ class SingleDialer extends Emitter {
* kill the call in progress or the stable dialog, whichever we have
*/
async kill() {
this.killed = true;
if (this.inviteInProgress) await this.inviteInProgress.cancel();
else if (this.dlg && this.dlg.connected) {
const duration = moment().diff(this.dlg.connectTime, 'seconds');
@@ -265,6 +280,24 @@ class SingleDialer extends Emitter {
}
}
async doAdulting({logger, tasks, application}) {
this.logger = logger;
this.adulting = true;
this.emit('adulting');
await this.ep.unbridge()
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
this.ep.play('silence_stream://1000');
const cs = new AdultingCallSession({
logger: this.logger,
singleDialer: this,
application,
callInfo: this.callInfo,
tasks
});
cs.exec();
return cs;
}
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
assert((typeof duration === 'number' && callStatus === CallStatus.Completed) ||
(!duration && callStatus !== CallStatus.Completed),
@@ -288,8 +321,9 @@ class SingleDialer extends Emitter {
}
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo}) {
const sd = new SingleDialer({logger, sbcAddress, target, opts, application, callInfo});
sd.exec(srf, ms, opts);
const myOpts = deepcopy(opts);
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo});
sd.exec(srf, ms, myOpts);
return sd;
}

View File

@@ -1,9 +1,31 @@
const bent = require('bent');
const parseUrl = require('parse-url');
const assert = require('assert');
const snakeCaseKeys = require('./snakecase-keys');
const crypto = require('crypto');
const timeSeries = require('@jambonz/time-series');
let alerter ;
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
function computeSignature(payload, timestamp, secret) {
assert(secret);
const data = `${timestamp}.${JSON.stringify(payload)}`;
return crypto
.createHmac('sha256', secret)
.update(data, 'utf8')
.digest('hex');
}
function generateSigHeader(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signature = computeSignature(payload, timestamp, secret);
const scheme = 'v1';
return {
'Jambonz-Signature': `t=${timestamp},${scheme}=${signature}`
};
}
function basicAuth(username, password) {
if (!username || !password) return {};
const creds = `${username}:${password || ''}`;
@@ -21,7 +43,7 @@ function isAbsoluteUrl(u) {
}
class Requestor {
constructor(logger, hook) {
constructor(logger, account_sid, hook, secret) {
assert(typeof hook === 'object');
this.logger = logger;
@@ -38,12 +60,22 @@ class Requestor {
this.username = hook.username;
this.password = hook.password;
this.secret = secret;
this.account_sid = account_sid;
assert(isAbsoluteUrl(this.url));
assert(['GET', 'POST'].includes(this.method));
const {stats} = require('../../').srf.locals;
this.stats = stats;
if (!alerter) {
alerter = timeSeries(logger, {
host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50,
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
});
}
}
get baseUrl() {
@@ -62,25 +94,39 @@ class Requestor {
* @param {object} [params] - request parameters
*/
async request(hook, params) {
params = params || null;
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
const url = hook.url || hook;
const method = hook.method || 'POST';
const {username, password} = typeof hook === 'object' ? hook : {};
assert.ok(url, 'Requestor:request url was not provided');
assert.ok, (['GET', 'POST'].includes(method), `Requestor:request method must be 'GET' or 'POST' not ${method}`);
this.logger.debug({hook, params}, `Requestor:request ${method} ${url}`);
this.logger.debug({hook, payload}, `Requestor:request ${method} ${url}`);
const startAt = process.hrtime();
let buf;
try {
const sigHeader = generateSigHeader(payload, this.secret);
const headers = {...sigHeader, ...this.authHeader};
this.logger.info({url, headers}, 'send webhook');
buf = isRelativeUrl(url) ?
await this.post(url, params, this.authHeader) :
await bent(method, 'buffer', 200, 201, 202)(url, params, basicAuth(username, password));
await this.post(url, payload, headers) :
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
} catch (err) {
this.logger.info({baseUrl: this.baseUrl, url: err.statusCode},
this.logger.error({err, secret: this.secret, baseUrl: this.baseUrl, url, statusCode: err.statusCode},
`web callback returned unexpected error code ${err.statusCode}`);
let opts = {account_sid: this.account_sid};
if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
}
else if (err.name === 'StatusError') {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_STATUS_FAILURE, url, status: err.statusCode};
}
else {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url, detail: err.message};
}
alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
throw err;
}
const diff = process.hrtime(startAt);

View File

@@ -18,8 +18,7 @@ module.exports = (logger) => {
// listen for SNS lifecycle changes
let lifecycleEmitter = new Emitter();
let dryUpCalls = false;
if (process.env.AWS_SNS_TOPIC_ARM &&
process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY && process.env.AWS_REGION) {
if (process.env.AWS_SNS_TOPIC_ARM && process.env.AWS_REGION) {
(async function() {
try {
@@ -68,6 +67,8 @@ module.exports = (logger) => {
// send OPTIONS pings to SBCs
async function pingProxies(srf) {
if (process.env.NODE_ENV === 'test') return;
for (const sbc of sbcs) {
try {
const ms = srf.locals.getFreeswitch();

View File

@@ -0,0 +1,25 @@
const snakeCase = require('to-snake-case');
const isObject = (value) => typeof value === 'object' && value !== null;
const snakeObject = (obj, excludes) => {
if (Array.isArray(obj)) return obj.map((o) => {
return isObject(o) ? snakeObject(o, excludes) : o;
});
const target = {};
for (const [key, value] of Object.entries(obj)) {
if (excludes.includes(key)) {
target[key] = value;
continue;
}
const newKey = snakeCase(key);
const newValue = isObject(value) ? snakeObject(value, excludes) : value;
target[newKey] = newValue;
}
return target;
};
module.exports = (obj, excludes = []) => {
return snakeObject(obj, excludes);
};

7488
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-feature-server",
"version": "0.2.1",
"version": "0.3.1",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
@@ -21,34 +21,39 @@
},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test JAMBONES_NETWORK_CIDR=127.0.0.1/32 node test/ | ./node_modules/.bin/tap-spec",
"test": "NODE_ENV=test JAMBONES_HOSTING=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib"
},
"dependencies": {
"bent": "^7.0.6",
"@jambonz/db-helpers": "^0.6.4",
"@jambonz/mw-registrar": "^0.2.1",
"@jambonz/realtimedb-helpers": "^0.4.1",
"@jambonz/stats-collector": "^0.1.5",
"@jambonz/time-series": "^0.1.5",
"aws-sdk": "^2.846.0",
"bent": "^7.3.12",
"cidr-matcher": "^2.1.1",
"debug": "^4.1.1",
"drachtio-fsmrf": "^2.0.1",
"drachtio-srf": "^4.4.28",
"debug": "^4.3.1",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^2.0.7",
"drachtio-srf": "^4.4.50",
"express": "^4.17.1",
"ip": "^1.1.5",
"@jambonz/db-helpers": "^0.3.7",
"jambonz-mw-registrar": "^0.1.3",
"jambonz-realtimedb-helpers": "^0.2.13",
"jambonz-stats-collector": "^0.0.3",
"moment": "^2.24.0",
"parse-url": "^5.0.1",
"pino": "^5.16.0",
"verify-aws-sns-signature": "0.0.5",
"moment": "^2.29.1",
"parse-url": "^5.0.2",
"pino": "^6.11.2",
"to-snake-case": "^1.0.0",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.6",
"xml2js": "^0.4.23"
},
"devDependencies": {
"blue-tape": "^1.0.0",
"async": "^3.2.0",
"clear-module": "^4.1.1",
"eslint": "^6.8.0",
"eslint-plugin-promise": "^4.2.1",
"nyc": "^15.0.1",
"tap-spec": "^5.0.0"
"eslint": "^7.20.0",
"eslint-plugin-promise": "^4.3.1",
"nyc": "^15.1.0",
"tape": "^5.2.2"
}
}

View File

@@ -0,0 +1,34 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('account validation tests', async(t) => {
const {srf, disconnect} = require('../app');
try {
await connect(srf);
await sippUac('uac-expect-500.xml', '172.38.0.10');
t.pass('rejected INVITE without X-Account-Sid header');
await sippUac('uac-invalid-account-expect-503.xml', '172.38.0.10');
t.pass('rejected INVITE with invalid X-Account-Sid header');
await sippUac('uac-inactive-account-expect-503.xml', '172.38.0.10');
t.pass('rejected INVITE from inactive account');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,9 +1,12 @@
const test = require('tape').test ;
const test = require('tape') ;
const exec = require('child_process').exec ;
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
const fs = require('fs');
const {encrypt} = require('../lib/utils/encrypt-decrypt');
test('creating jambones_test database', (t) => {
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => {
console.log(stdout);
console.log(stderr)
if (err) return t.end(err);
t.pass('database successfully created');
t.end();
@@ -11,17 +14,35 @@ test('creating jambones_test database', (t) => {
});
test('creating schema', (t) => {
exec(`mysql -h localhost -u root ${pwd} -D jambones_test < ${__dirname}/db/jambones-sql.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/db/create-and-populate-schema.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('schema successfully created');
t.end();
t.pass('schema and test data successfully created');
if (process.env.GCP_JSON_KEY && process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
const google_credential = encrypt(process.env.GCP_JSON_KEY);
const aws_credential = encrypt(JSON.stringify({
access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY
}));
const cmd = `
UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';
UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';
`;
const path = `${__dirname}/.creds.sql`;
fs.writeFileSync(path, cmd);
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${path}`, (err, stdout, stderr) => {
console.log(stdout);
console.log(stderr);
if (err) return t.end(err);
fs.unlinkSync(path)
fs.writeFileSync(`${__dirname}/credentials/gcp.json`, process.env.GCP_JSON_KEY);
t.pass('set account-level speech credentials');
t.end();
});
}
else {
t.end();
}
});
});
test('populating test case data', (t) => {
exec(`mysql -h localhost -u root ${pwd} -D jambones_test < ${__dirname}/db/populate-test-data.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('test data set created');
t.end();
});
});

0
test/credentials/.keep Normal file
View File

View File

@@ -0,0 +1,750 @@
-- MySQL dump 10.13 Distrib 8.0.18, for macos10.14 (x86_64)
--
-- Host: 127.0.0.1 Database: jambones_test
-- ------------------------------------------------------
-- Server version 5.7.33
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `account_products`
--
DROP TABLE IF EXISTS `account_products`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account_products` (
`account_product_sid` char(36) NOT NULL,
`account_subscription_sid` char(36) NOT NULL,
`product_sid` char(36) NOT NULL,
`quantity` int(11) NOT NULL,
PRIMARY KEY (`account_product_sid`),
UNIQUE KEY `account_product_sid` (`account_product_sid`),
KEY `account_product_sid_idx` (`account_product_sid`),
KEY `account_subscription_sid_idx` (`account_subscription_sid`),
KEY `product_sid_idxfk` (`product_sid`),
CONSTRAINT `account_subscription_sid_idxfk` FOREIGN KEY (`account_subscription_sid`) REFERENCES `account_subscriptions` (`account_subscription_sid`),
CONSTRAINT `product_sid_idxfk` FOREIGN KEY (`product_sid`) REFERENCES `products` (`product_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `account_products`
--
LOCK TABLES `account_products` WRITE;
/*!40000 ALTER TABLE `account_products` DISABLE KEYS */;
INSERT INTO `account_products` VALUES ('bb0e8a44-0e59-4103-a44c-f7ff950319fb','02639178-e073-4f8e-9b7e-48b1d36f4b7a','35a9fb10-233d-4eb9-aada-78de5814d680',10),('e2cd5148-07ad-4cdc-b395-22e4b4e23d7e','02639178-e073-4f8e-9b7e-48b1d36f4b7a','2c815913-5c26-4004-b748-183b459329df',10),('f9b320aa-c287-438b-a4c0-e4383b4f0256','02639178-e073-4f8e-9b7e-48b1d36f4b7a','c4403cdb-8e75-4b27-9726-7d8315e3216d',10);
/*!40000 ALTER TABLE `account_products` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `account_static_ips`
--
DROP TABLE IF EXISTS `account_static_ips`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account_static_ips` (
`account_static_ip_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`ipv4` varchar(16) NOT NULL,
`sbc_address_sid` char(36) NOT NULL,
PRIMARY KEY (`account_static_ip_sid`),
UNIQUE KEY `account_static_ip_sid` (`account_static_ip_sid`),
UNIQUE KEY `ipv4` (`ipv4`),
KEY `account_static_ip_sid_idx` (`account_static_ip_sid`),
KEY `account_sid_idx` (`account_sid`),
KEY `sbc_address_sid_idxfk` (`sbc_address_sid`),
CONSTRAINT `account_sid_idxfk_3` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `sbc_address_sid_idxfk` FOREIGN KEY (`sbc_address_sid`) REFERENCES `sbc_addresses` (`sbc_address_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `account_static_ips`
--
LOCK TABLES `account_static_ips` WRITE;
/*!40000 ALTER TABLE `account_static_ips` DISABLE KEYS */;
/*!40000 ALTER TABLE `account_static_ips` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `account_subscriptions`
--
DROP TABLE IF EXISTS `account_subscriptions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account_subscriptions` (
`account_subscription_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`pending` tinyint(1) NOT NULL DEFAULT '0',
`effective_start_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`effective_end_date` datetime DEFAULT NULL,
`change_reason` varchar(255) DEFAULT NULL,
`stripe_subscription_id` varchar(56) DEFAULT NULL,
`stripe_payment_method_id` varchar(56) DEFAULT NULL,
`stripe_statement_descriptor` varchar(255) DEFAULT NULL,
`last4` char(4) DEFAULT NULL,
`exp_month` int(11) DEFAULT NULL,
`exp_year` int(11) DEFAULT NULL,
`card_type` varchar(16) DEFAULT NULL,
`pending_reason` varbinary(52) DEFAULT NULL,
PRIMARY KEY (`account_subscription_sid`),
UNIQUE KEY `account_subscription_sid` (`account_subscription_sid`),
KEY `account_subscription_sid_idx` (`account_subscription_sid`),
KEY `account_sid_idx` (`account_sid`),
CONSTRAINT `account_sid_idxfk` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `account_subscriptions`
--
LOCK TABLES `account_subscriptions` WRITE;
/*!40000 ALTER TABLE `account_subscriptions` DISABLE KEYS */;
INSERT INTO `account_subscriptions` VALUES ('02639178-e073-4f8e-9b7e-48b1d36f4b7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f',0,'2021-04-03 15:41:03',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `account_subscriptions` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `accounts`
--
DROP TABLE IF EXISTS `accounts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `accounts` (
`account_sid` char(36) NOT NULL,
`name` varchar(64) NOT NULL,
`sip_realm` varchar(132) DEFAULT NULL COMMENT 'sip domain that will be used for devices registering under this account',
`service_provider_sid` char(36) NOT NULL COMMENT 'service provider that owns the customer relationship with this account',
`registration_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call when devices underr this account attempt to register',
`device_calling_application_sid` char(36) DEFAULT NULL COMMENT 'application to use for outbound calling from an account',
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`plan_type` enum('trial','free','paid') NOT NULL DEFAULT 'trial',
`stripe_customer_id` varchar(56) DEFAULT NULL,
`webhook_secret` varchar(36) NOT NULL,
`disable_cdrs` tinyint(1) NOT NULL DEFAULT '0',
`trial_end_date` datetime DEFAULT NULL,
`deactivated_reason` varchar(255) DEFAULT NULL,
PRIMARY KEY (`account_sid`),
UNIQUE KEY `account_sid` (`account_sid`),
UNIQUE KEY `sip_realm` (`sip_realm`),
KEY `account_sid_idx` (`account_sid`),
KEY `sip_realm_idx` (`sip_realm`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
KEY `registration_hook_sid_idxfk_1` (`registration_hook_sid`),
KEY `device_calling_application_sid_idxfk` (`device_calling_application_sid`),
CONSTRAINT `device_calling_application_sid_idxfk` FOREIGN KEY (`device_calling_application_sid`) REFERENCES `applications` (`application_sid`),
CONSTRAINT `registration_hook_sid_idxfk_1` FOREIGN KEY (`registration_hook_sid`) REFERENCES `webhooks` (`webhook_sid`),
CONSTRAINT `service_provider_sid_idxfk_6` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='An enterprise that uses the platform for comm services';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `accounts`
--
LOCK TABLES `accounts` WRITE;
/*!40000 ALTER TABLE `accounts` DISABLE KEYS */;
INSERT INTO `accounts` VALUES ('bb845d4b-83a9-4cde-a6e9-50f3743bab3f','Joe User','test.yakeeda.com','2708b1b3-2736-40ea-b502-c53d8396247f',NULL,NULL,1,'2021-04-03 15:41:03','trial',NULL,'wh_secret_ehV2dVyzNBs5kHxeJcatRQ',0,NULL,NULL);
INSERT INTO `accounts` VALUES ('622f62e4-303a-49f2-bbe0-eb1e1714e37a','Dave Horton','delta.yakeeda.com','2708b1b3-2736-40ea-b502-c53d8396247f',NULL,NULL,0,'2021-04-03 15:41:03','trial',NULL,'wh_secret_ehV2dVyzNBs5kHxeJcatRQ',0,NULL,NULL);
/*!40000 ALTER TABLE `accounts` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `api_keys`
--
DROP TABLE IF EXISTS `api_keys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `api_keys` (
`api_key_sid` char(36) NOT NULL,
`token` char(36) NOT NULL,
`account_sid` char(36) DEFAULT NULL,
`service_provider_sid` char(36) DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`last_used` timestamp NULL DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`api_key_sid`),
UNIQUE KEY `api_key_sid` (`api_key_sid`),
UNIQUE KEY `token` (`token`),
KEY `api_key_sid_idx` (`api_key_sid`),
KEY `account_sid_idx` (`account_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
CONSTRAINT `account_sid_idxfk_4` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `service_provider_sid_idxfk` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='An authorization token that is used to access the REST api';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `api_keys`
--
LOCK TABLES `api_keys` WRITE;
/*!40000 ALTER TABLE `api_keys` DISABLE KEYS */;
INSERT INTO `api_keys` VALUES ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0','38700987-c7a4-4685-a5bb-af378f9734de',NULL,NULL,NULL,NULL,'2021-04-03 15:40:37'),('b00b1025-2b65-453b-a243-599b75be7d0a','52c2eb45-9f72-4545-9c60-9639e3f4eaf7','bb845d4b-83a9-4cde-a6e9-50f3743bab3f',NULL,NULL,NULL,'2021-04-03 15:42:40');
/*!40000 ALTER TABLE `api_keys` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `applications`
--
DROP TABLE IF EXISTS `applications`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `applications` (
`application_sid` char(36) NOT NULL,
`name` varchar(64) NOT NULL,
`service_provider_sid` char(36) DEFAULT NULL COMMENT 'if non-null, this application is a test application that can be used by any account under the associated service provider',
`account_sid` char(36) DEFAULT NULL COMMENT 'account that this application belongs to (if null, this is a service provider test application)',
`call_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for inbound calls ',
`call_status_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for call status events',
`messaging_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for inbound SMS/MMS ',
`speech_synthesis_vendor` varchar(64) NOT NULL DEFAULT 'google',
`speech_synthesis_language` varchar(12) NOT NULL DEFAULT 'en-US',
`speech_synthesis_voice` varchar(64) DEFAULT NULL,
`speech_recognizer_vendor` varchar(64) NOT NULL DEFAULT 'google',
`speech_recognizer_language` varchar(64) NOT NULL DEFAULT 'en-US',
PRIMARY KEY (`application_sid`),
UNIQUE KEY `application_sid` (`application_sid`),
UNIQUE KEY `applications_idx_name` (`account_sid`,`name`),
KEY `application_sid_idx` (`application_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
KEY `account_sid_idx` (`account_sid`),
KEY `call_hook_sid_idxfk` (`call_hook_sid`),
KEY `call_status_hook_sid_idxfk` (`call_status_hook_sid`),
KEY `messaging_hook_sid_idxfk` (`messaging_hook_sid`),
CONSTRAINT `account_sid_idxfk_10` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `call_hook_sid_idxfk` FOREIGN KEY (`call_hook_sid`) REFERENCES `webhooks` (`webhook_sid`),
CONSTRAINT `call_status_hook_sid_idxfk` FOREIGN KEY (`call_status_hook_sid`) REFERENCES `webhooks` (`webhook_sid`),
CONSTRAINT `messaging_hook_sid_idxfk` FOREIGN KEY (`messaging_hook_sid`) REFERENCES `webhooks` (`webhook_sid`),
CONSTRAINT `service_provider_sid_idxfk_5` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A defined set of behaviors to be applied to phone calls ';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `applications`
--
LOCK TABLES `applications` WRITE;
/*!40000 ALTER TABLE `applications` DISABLE KEYS */;
INSERT INTO `applications` VALUES ('0dddaabf-0a30-43e3-84e8-426873b1a78b','decline call',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('308b4f41-1a18-4052-b89a-c054e75ce242','say',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('24d0f6af-e976-44dd-a2e8-41c7b55abe33','say account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('17461c69-56b5-4dab-ad83-1c43a0f93a3d','gather',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','10692465-a511-4277-9807-b7157e4f81e1','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('baf9213b-5556-4c20-870c-586392ed246f','transcribe',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('ae026ab5-3029-47b4-9d7c-236e3a4b4ebe','transcribe account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
/*!40000 ALTER TABLE `applications` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `call_routes`
--
DROP TABLE IF EXISTS `call_routes`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `call_routes` (
`call_route_sid` char(36) NOT NULL,
`priority` int(11) NOT NULL,
`account_sid` char(36) NOT NULL,
`regex` varchar(255) NOT NULL,
`application_sid` char(36) NOT NULL,
PRIMARY KEY (`call_route_sid`),
UNIQUE KEY `call_route_sid` (`call_route_sid`),
KEY `call_route_sid_idx` (`call_route_sid`),
KEY `account_sid_idxfk_1` (`account_sid`),
KEY `application_sid_idxfk` (`application_sid`),
CONSTRAINT `account_sid_idxfk_1` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `application_sid_idxfk` FOREIGN KEY (`application_sid`) REFERENCES `applications` (`application_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='a regex-based pattern match for call routing';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `call_routes`
--
LOCK TABLES `call_routes` WRITE;
/*!40000 ALTER TABLE `call_routes` DISABLE KEYS */;
/*!40000 ALTER TABLE `call_routes` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `dns_records`
--
DROP TABLE IF EXISTS `dns_records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `dns_records` (
`dns_record_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`record_type` varchar(6) NOT NULL,
`record_id` int(11) NOT NULL,
PRIMARY KEY (`dns_record_sid`),
UNIQUE KEY `dns_record_sid` (`dns_record_sid`),
KEY `dns_record_sid_idx` (`dns_record_sid`),
KEY `account_sid_idxfk_2` (`account_sid`),
CONSTRAINT `account_sid_idxfk_2` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `dns_records`
--
LOCK TABLES `dns_records` WRITE;
/*!40000 ALTER TABLE `dns_records` DISABLE KEYS */;
/*!40000 ALTER TABLE `dns_records` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `lcr_carrier_set_entry`
--
DROP TABLE IF EXISTS `lcr_carrier_set_entry`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `lcr_carrier_set_entry` (
`lcr_carrier_set_entry_sid` char(36) NOT NULL,
`workload` int(11) NOT NULL DEFAULT '1' COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
`lcr_route_sid` char(36) NOT NULL,
`voip_carrier_sid` char(36) NOT NULL,
`priority` int(11) NOT NULL DEFAULT '0' COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (`lcr_carrier_set_entry_sid`),
KEY `lcr_route_sid_idxfk` (`lcr_route_sid`),
KEY `voip_carrier_sid_idxfk_2` (`voip_carrier_sid`),
CONSTRAINT `lcr_route_sid_idxfk` FOREIGN KEY (`lcr_route_sid`) REFERENCES `lcr_routes` (`lcr_route_sid`),
CONSTRAINT `voip_carrier_sid_idxfk_2` FOREIGN KEY (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='An entry in the LCR routing list';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `lcr_carrier_set_entry`
--
LOCK TABLES `lcr_carrier_set_entry` WRITE;
/*!40000 ALTER TABLE `lcr_carrier_set_entry` DISABLE KEYS */;
/*!40000 ALTER TABLE `lcr_carrier_set_entry` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `lcr_routes`
--
DROP TABLE IF EXISTS `lcr_routes`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `lcr_routes` (
`lcr_route_sid` char(36) NOT NULL,
`regex` varchar(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
`description` varchar(1024) DEFAULT NULL,
`priority` int(11) NOT NULL COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (`lcr_route_sid`),
UNIQUE KEY `priority` (`priority`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='Least cost routing table';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `lcr_routes`
--
LOCK TABLES `lcr_routes` WRITE;
/*!40000 ALTER TABLE `lcr_routes` DISABLE KEYS */;
/*!40000 ALTER TABLE `lcr_routes` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `ms_teams_tenants`
--
DROP TABLE IF EXISTS `ms_teams_tenants`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `ms_teams_tenants` (
`ms_teams_tenant_sid` char(36) NOT NULL,
`service_provider_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`application_sid` char(36) DEFAULT NULL,
`tenant_fqdn` varchar(255) NOT NULL,
PRIMARY KEY (`ms_teams_tenant_sid`),
UNIQUE KEY `ms_teams_tenant_sid` (`ms_teams_tenant_sid`),
UNIQUE KEY `tenant_fqdn` (`tenant_fqdn`),
KEY `ms_teams_tenant_sid_idx` (`ms_teams_tenant_sid`),
KEY `service_provider_sid_idxfk_1` (`service_provider_sid`),
KEY `account_sid_idxfk_5` (`account_sid`),
KEY `application_sid_idxfk_1` (`application_sid`),
KEY `tenant_fqdn_idx` (`tenant_fqdn`),
CONSTRAINT `account_sid_idxfk_5` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `application_sid_idxfk_1` FOREIGN KEY (`application_sid`) REFERENCES `applications` (`application_sid`),
CONSTRAINT `service_provider_sid_idxfk_1` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A Microsoft Teams customer tenant';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `ms_teams_tenants`
--
LOCK TABLES `ms_teams_tenants` WRITE;
/*!40000 ALTER TABLE `ms_teams_tenants` DISABLE KEYS */;
/*!40000 ALTER TABLE `ms_teams_tenants` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `phone_numbers`
--
DROP TABLE IF EXISTS `phone_numbers`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `phone_numbers` (
`phone_number_sid` char(36) NOT NULL,
`number` varchar(32) NOT NULL,
`voip_carrier_sid` char(36) DEFAULT NULL,
`account_sid` char(36) DEFAULT NULL,
`application_sid` char(36) DEFAULT NULL,
`service_provider_sid` char(36) DEFAULT NULL COMMENT 'if not null, this number is a test number for the associated service provider',
PRIMARY KEY (`phone_number_sid`),
UNIQUE KEY `number` (`number`),
UNIQUE KEY `phone_number_sid` (`phone_number_sid`),
KEY `phone_number_sid_idx` (`phone_number_sid`),
KEY `number_idx` (`number`),
KEY `voip_carrier_sid_idx` (`voip_carrier_sid`),
KEY `account_sid_idxfk_9` (`account_sid`),
KEY `application_sid_idxfk_3` (`application_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
CONSTRAINT `account_sid_idxfk_9` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `application_sid_idxfk_3` FOREIGN KEY (`application_sid`) REFERENCES `applications` (`application_sid`),
CONSTRAINT `service_provider_sid_idxfk_4` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`),
CONSTRAINT `voip_carrier_sid_idxfk` FOREIGN KEY (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A phone number that has been assigned to an account';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `phone_numbers`
--
LOCK TABLES `phone_numbers` WRITE;
/*!40000 ALTER TABLE `phone_numbers` DISABLE KEYS */;
INSERT INTO `phone_numbers` VALUES ('4b439355-debc-40c7-9cfa-5be58c2bed6b','16174000000','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','0dddaabf-0a30-43e3-84e8-426873b1a78b', NULL);
INSERT INTO `phone_numbers` VALUES ('9cc9e7fc-b7b0-4101-8f3c-9fe13ce5df0a','16174000001','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','308b4f41-1a18-4052-b89a-c054e75ce242', NULL);
INSERT INTO `phone_numbers` VALUES ('e686a320-0725-418f-be65-532159bdc3ed','16174000002','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','24d0f6af-e976-44dd-a2e8-41c7b55abe33', NULL);
INSERT INTO `phone_numbers` VALUES ('05eeed62-b29b-4679-bf38-d7a4e318be44','16174000003','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','17461c69-56b5-4dab-ad83-1c43a0f93a3d', NULL);
INSERT INTO `phone_numbers` VALUES ('f3c53863-b629-4cf6-9dcb-c7fb7072314b','16174000004','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','baf9213b-5556-4c20-870c-586392ed246f', NULL);
INSERT INTO `phone_numbers` VALUES ('f6416c17-829a-4f11-9c32-f0d00e4a9ae9','16174000005','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','ae026ab5-3029-47b4-9d7c-236e3a4b4ebe', NULL);
/*!40000 ALTER TABLE `phone_numbers` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `products`
--
DROP TABLE IF EXISTS `products`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `products` (
`product_sid` char(36) NOT NULL,
`name` varchar(32) NOT NULL,
`category` enum('api_rate','voice_call_session','device') NOT NULL,
PRIMARY KEY (`product_sid`),
UNIQUE KEY `product_sid` (`product_sid`),
KEY `product_sid_idx` (`product_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `products`
--
LOCK TABLES `products` WRITE;
/*!40000 ALTER TABLE `products` DISABLE KEYS */;
INSERT INTO `products` VALUES ('2c815913-5c26-4004-b748-183b459329df','registered device','device'),('35a9fb10-233d-4eb9-aada-78de5814d680','api call','api_rate'),('c4403cdb-8e75-4b27-9726-7d8315e3216d','concurrent call session','voice_call_session');
/*!40000 ALTER TABLE `products` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `sbc_addresses`
--
DROP TABLE IF EXISTS `sbc_addresses`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sbc_addresses` (
`sbc_address_sid` char(36) NOT NULL,
`ipv4` varchar(255) NOT NULL,
`port` int(11) NOT NULL DEFAULT '5060',
`service_provider_sid` char(36) DEFAULT NULL,
PRIMARY KEY (`sbc_address_sid`),
UNIQUE KEY `sbc_address_sid` (`sbc_address_sid`),
KEY `sbc_addresses_idx_host_port` (`ipv4`,`port`),
KEY `sbc_address_sid_idx` (`sbc_address_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
CONSTRAINT `service_provider_sid_idxfk_2` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `sbc_addresses`
--
LOCK TABLES `sbc_addresses` WRITE;
/*!40000 ALTER TABLE `sbc_addresses` DISABLE KEYS */;
INSERT INTO `sbc_addresses` VALUES ('8d6d0fda-4550-41ab-8e2f-60761d81fe7d','3.39.45.30',5060,NULL);
/*!40000 ALTER TABLE `sbc_addresses` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `service_providers`
--
DROP TABLE IF EXISTS `service_providers`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `service_providers` (
`service_provider_sid` char(36) NOT NULL,
`name` varchar(64) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`root_domain` varchar(128) DEFAULT NULL,
`registration_hook_sid` char(36) DEFAULT NULL,
`ms_teams_fqdn` varchar(255) DEFAULT NULL,
PRIMARY KEY (`service_provider_sid`),
UNIQUE KEY `service_provider_sid` (`service_provider_sid`),
UNIQUE KEY `name` (`name`),
UNIQUE KEY `root_domain` (`root_domain`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
KEY `name_idx` (`name`),
KEY `root_domain_idx` (`root_domain`),
KEY `registration_hook_sid_idxfk` (`registration_hook_sid`),
CONSTRAINT `registration_hook_sid_idxfk` FOREIGN KEY (`registration_hook_sid`) REFERENCES `webhooks` (`webhook_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A partition of the platform used by one service provider';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `service_providers`
--
LOCK TABLES `service_providers` WRITE;
/*!40000 ALTER TABLE `service_providers` DISABLE KEYS */;
INSERT INTO `service_providers` VALUES ('2708b1b3-2736-40ea-b502-c53d8396247f','jambonz.us','jambonz.us service provider','yakeeda.com',NULL,NULL);
/*!40000 ALTER TABLE `service_providers` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `sip_gateways`
--
DROP TABLE IF EXISTS `sip_gateways`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sip_gateways` (
`sip_gateway_sid` char(36) NOT NULL,
`ipv4` varchar(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
`port` int(11) NOT NULL DEFAULT '5060' COMMENT 'sip signaling port',
`inbound` tinyint(1) NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
`outbound` tinyint(1) NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
`voip_carrier_sid` char(36) NOT NULL,
`is_active` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`sip_gateway_sid`),
KEY `sip_gateway_idx_hostport` (`ipv4`,`port`),
KEY `voip_carrier_sid_idx` (`voip_carrier_sid`),
CONSTRAINT `voip_carrier_sid_idxfk_1` FOREIGN KEY (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A whitelisted sip gateway used for origination/termination';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `sip_gateways`
--
LOCK TABLES `sip_gateways` WRITE;
/*!40000 ALTER TABLE `sip_gateways` DISABLE KEYS */;
INSERT INTO `sip_gateways` VALUES ('46b727eb-c7dc-44fa-b063-96e48d408e4a','3.3.3.3',5060,1,1,'5145b436-2f38-4029-8d4c-fd8c67831c7a',1),('81629182-6904-4588-8c72-a78d70053fb9','54.172.60.1',5060,1,1,'df0aefbf-ca7b-4d48-9fbf-3c66fef72060',1);
/*!40000 ALTER TABLE `sip_gateways` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `speech_credentials`
--
DROP TABLE IF EXISTS `speech_credentials`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `speech_credentials` (
`speech_credential_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`vendor` varchar(255) NOT NULL,
`credential` VARCHAR(8192) NOT NULL,
`use_for_tts` tinyint(1) DEFAULT '1',
`use_for_stt` tinyint(1) DEFAULT '1',
`last_used` datetime DEFAULT NULL,
`last_tested` datetime DEFAULT NULL,
`tts_tested_ok` tinyint(1) DEFAULT NULL,
`stt_tested_ok` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`speech_credential_sid`),
UNIQUE KEY `speech_credential_sid` (`speech_credential_sid`),
UNIQUE KEY `speech_credentials_idx_1` (`vendor`,`account_sid`),
KEY `speech_credential_sid_idx` (`speech_credential_sid`),
KEY `account_sid_idx` (`account_sid`),
CONSTRAINT `account_sid_idxfk_6` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `speech_credentials`
--
LOCK TABLES `speech_credentials` WRITE;
/*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */;
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),('84154212-5c99-4c94-8993-bc2a46288daa','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`user_sid` char(36) NOT NULL,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`pending_email` varchar(255) DEFAULT NULL,
`phone` varchar(20) DEFAULT NULL,
`hashed_password` varchar(1024) DEFAULT NULL,
`salt` char(16) DEFAULT NULL,
`account_sid` char(36) DEFAULT NULL,
`service_provider_sid` char(36) DEFAULT NULL,
`force_change` tinyint(1) NOT NULL DEFAULT '0',
`provider` varchar(255) NOT NULL,
`provider_userid` varchar(255) DEFAULT NULL,
`scope` varchar(16) NOT NULL DEFAULT 'read-write',
`phone_activation_code` varchar(16) DEFAULT NULL,
`email_activation_code` varchar(16) DEFAULT NULL,
`email_validated` tinyint(1) NOT NULL DEFAULT '0',
`phone_validated` tinyint(1) NOT NULL DEFAULT '0',
`email_content_opt_out` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`user_sid`),
UNIQUE KEY `user_sid` (`user_sid`),
UNIQUE KEY `phone` (`phone`),
KEY `user_sid_idx` (`user_sid`),
KEY `email_idx` (`email`),
KEY `phone_idx` (`phone`),
KEY `account_sid_idx` (`account_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
KEY `email_activation_code_idx` (`email_activation_code`),
CONSTRAINT `account_sid_idxfk_7` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `service_provider_sid_idxfk_3` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users`
--
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES ('d9cdf199-78d1-4f92-b717-5f9dbdf56565','Dave Horton','daveh@drachtio.org',NULL,NULL,NULL,NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f',NULL,0,'github','davehorton','read-write',NULL,NULL,1,0,0);
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `voip_carriers`
--
DROP TABLE IF EXISTS `voip_carriers`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `voip_carriers` (
`voip_carrier_sid` char(36) NOT NULL,
`name` varchar(64) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`account_sid` char(36) DEFAULT NULL COMMENT 'if provided, indicates this entity represents a sip trunk that is associated with a specific account',
`application_sid` char(36) DEFAULT NULL COMMENT 'If provided, all incoming calls from this source will be routed to the associated application',
`e164_leading_plus` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'if true, a leading plus should be prepended to outbound phone numbers',
`requires_register` tinyint(1) NOT NULL DEFAULT '0',
`register_username` varchar(64) DEFAULT NULL,
`register_sip_realm` varchar(64) DEFAULT NULL,
`register_password` varchar(64) DEFAULT NULL,
`tech_prefix` varchar(16) DEFAULT NULL COMMENT 'tech prefix to prepend to outbound calls to this carrier',
PRIMARY KEY (`voip_carrier_sid`),
UNIQUE KEY `voip_carrier_sid` (`voip_carrier_sid`),
KEY `voip_carrier_sid_idx` (`voip_carrier_sid`),
KEY `account_sid_idx` (`account_sid`),
KEY `application_sid_idxfk_2` (`application_sid`),
CONSTRAINT `account_sid_idxfk_8` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `application_sid_idxfk_2` FOREIGN KEY (`application_sid`) REFERENCES `applications` (`application_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A Carrier or customer PBX that can send or receive calls';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `voip_carriers`
--
LOCK TABLES `voip_carriers` WRITE;
/*!40000 ALTER TABLE `voip_carriers` DISABLE KEYS */;
INSERT INTO `voip_carriers` VALUES ('5145b436-2f38-4029-8d4c-fd8c67831c7a','my test carrier',NULL,NULL,NULL,0,0,NULL,NULL,NULL,NULL),('df0aefbf-ca7b-4d48-9fbf-3c66fef72060','my test carrier',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f',NULL,0,0,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `voip_carriers` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `webhooks`
--
DROP TABLE IF EXISTS `webhooks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `webhooks` (
`webhook_sid` char(36) NOT NULL,
`url` varchar(1024) NOT NULL,
`method` enum('GET','POST') NOT NULL DEFAULT 'POST',
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`webhook_sid`),
UNIQUE KEY `webhook_sid` (`webhook_sid`),
KEY `webhook_sid_idx` (`webhook_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='An HTTP callback';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `webhooks`
--
LOCK TABLES `webhooks` WRITE;
/*!40000 ALTER TABLE `webhooks` DISABLE KEYS */;
INSERT INTO `webhooks` VALUES ('6ac36aeb-6bd0-428a-80a1-aed95640a296','https://flows.jambonz.us/callStatus','POST',NULL,NULL),('d9c205c6-a129-443e-a9c0-d1bb437d4bb7','https://flows.jambonz.us/testCall','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('293904c1-351b-4bca-8d58-1a29b853c7db','http://127.0.0.1:3100/callStatus','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('c71e79db-24f2-4866-a3ee-febb0f97b341','http://127.0.0.1:3100/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('54ab0976-a6c0-45d8-89a4-d90d45bf9d96','http://127.0.0.1:3101/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('10692465-a511-4277-9807-b7157e4f81e1','http://127.0.0.1:3102/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','http://127.0.0.1:3103/','POST',NULL,NULL);
/*!40000 ALTER TABLE `webhooks` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2021-04-03 11:50:25

View File

@@ -1,3 +1,3 @@
create database jambones_test;
create user jambones_test@localhost IDENTIFIED WITH mysql_native_password by 'jambones_test';
grant all on jambones_test.* to jambones_test@localhost;
create user jambones_test@'%' IDENTIFIED WITH mysql_native_password by 'jambones_test';
grant all on jambones_test.* to jambones_test@'%';

View File

@@ -1,272 +1,548 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `call_routes`;
DROP TABLE IF EXISTS account_static_ips;
DROP TABLE IF EXISTS `conference_participants`;
DROP TABLE IF EXISTS account_products;
DROP TABLE IF EXISTS `queue_members`;
DROP TABLE IF EXISTS account_subscriptions;
DROP TABLE IF EXISTS `calls`;
DROP TABLE IF EXISTS beta_invite_codes;
DROP TABLE IF EXISTS `phone_numbers`;
DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS `applications`;
DROP TABLE IF EXISTS dns_records;
DROP TABLE IF EXISTS `conferences`;
DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS `queues`;
DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS `subscriptions`;
DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS `registrations`;
DROP TABLE IF EXISTS predefined_carriers;
DROP TABLE IF EXISTS `api_keys`;
DROP TABLE IF EXISTS account_offers;
DROP TABLE IF EXISTS `accounts`;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS `service_providers`;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS `sip_gateways`;
DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS `voip_carriers`;
DROP TABLE IF EXISTS ms_teams_tenants;
CREATE TABLE IF NOT EXISTS `applications`
DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS smpp_gateways;
DROP TABLE IF EXISTS phone_numbers;
DROP TABLE IF EXISTS sip_gateways;
DROP TABLE IF EXISTS voip_carriers;
DROP TABLE IF EXISTS accounts;
DROP TABLE IF EXISTS applications;
DROP TABLE IF EXISTS service_providers;
DROP TABLE IF EXISTS webhooks;
CREATE TABLE account_static_ips
(
`application_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL,
`account_sid` CHAR(36) NOT NULL,
`call_hook` VARCHAR(255) NOT NULL,
`call_status_hook` VARCHAR(255) NOT NULL,
PRIMARY KEY (`application_sid`)
) ENGINE=InnoDB COMMENT='A defined set of behaviors to be applied to phone calls with';
CREATE TABLE IF NOT EXISTS `call_routes`
(
`call_route_sid` CHAR(36) NOT NULL UNIQUE ,
`order` INTEGER NOT NULL,
`account_sid` CHAR(36) NOT NULL,
`regex` VARCHAR(255) NOT NULL,
`application_sid` CHAR(36) NOT NULL,
PRIMARY KEY (`call_route_sid`)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `conferences`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`conference_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An audio conference';
CREATE TABLE IF NOT EXISTS `conference_participants`
(
`conference_participant_sid` CHAR(36) NOT NULL UNIQUE ,
`call_sid` CHAR(36),
`conference_sid` CHAR(36) NOT NULL,
PRIMARY KEY (`conference_participant_sid`)
) ENGINE=InnoDB COMMENT='A relationship between a call and a conference that it is co';
CREATE TABLE IF NOT EXISTS `queues`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`queue_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='A set of behaviors to be applied to parked calls';
CREATE TABLE IF NOT EXISTS `registrations`
(
`registration_sid` CHAR(36) NOT NULL UNIQUE ,
`username` VARCHAR(255) NOT NULL,
`domain` VARCHAR(255) NOT NULL,
`sip_contact` VARCHAR(255) NOT NULL,
`sip_user_agent` VARCHAR(255),
PRIMARY KEY (`registration_sid`)
) ENGINE=InnoDB COMMENT='An active sip registration';
CREATE TABLE IF NOT EXISTS `queue_members`
(
`queue_member_sid` CHAR(36) NOT NULL UNIQUE ,
`call_sid` CHAR(36),
`queue_sid` CHAR(36) NOT NULL,
`position` INTEGER,
PRIMARY KEY (`queue_member_sid`)
) ENGINE=InnoDB COMMENT='A relationship between a call and a queue that it is waiting';
CREATE TABLE IF NOT EXISTS `calls`
(
`call_sid` CHAR(36) NOT NULL UNIQUE ,
`parent_call_sid` CHAR(36),
`application_sid` CHAR(36),
`status_url` VARCHAR(255),
`time_start` DATETIME NOT NULL,
`time_alerting` DATETIME,
`time_answered` DATETIME,
`time_ended` DATETIME,
`direction` ENUM('inbound','outbound'),
`phone_number_sid` CHAR(36),
`inbound_user_sid` CHAR(36),
`outbound_user_sid` CHAR(36),
`calling_number` VARCHAR(255),
`called_number` VARCHAR(255),
`caller_name` VARCHAR(255),
`status` VARCHAR(255) NOT NULL COMMENT 'Possible values are queued, ringing, in-progress, completed, failed, busy and no-answer',
`sip_uri` VARCHAR(255) NOT NULL,
`sip_call_id` VARCHAR(255) NOT NULL,
`sip_cseq` INTEGER NOT NULL,
`sip_from_tag` VARCHAR(255) NOT NULL,
`sip_via_branch` VARCHAR(255) NOT NULL,
`sip_contact` VARCHAR(255),
`sip_final_status` INTEGER UNSIGNED,
`sdp_offer` VARCHAR(4096),
`sdp_answer` VARCHAR(4096),
`source_address` VARCHAR(255) NOT NULL,
`source_port` INTEGER UNSIGNED NOT NULL,
`dest_address` VARCHAR(255),
`dest_port` INTEGER UNSIGNED,
`url` VARCHAR(255),
PRIMARY KEY (`call_sid`)
) ENGINE=InnoDB COMMENT='A phone call';
CREATE TABLE IF NOT EXISTS `service_providers`
(
`service_provider_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL UNIQUE ,
`description` VARCHAR(255),
`root_domain` VARCHAR(255) UNIQUE ,
`registration_hook` VARCHAR(255),
`hook_basic_auth_user` VARCHAR(255),
`hook_basic_auth_password` VARCHAR(255),
PRIMARY KEY (`service_provider_sid`)
) ENGINE=InnoDB COMMENT='An organization that provides communication services to its ';
CREATE TABLE IF NOT EXISTS `api_keys`
(
`api_key_sid` CHAR(36) NOT NULL UNIQUE ,
`token` CHAR(36) NOT NULL UNIQUE ,
`account_sid` CHAR(36),
`service_provider_sid` CHAR(36),
PRIMARY KEY (`api_key_sid`)
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
CREATE TABLE IF NOT EXISTS `accounts`
(
`account_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL,
`sip_realm` VARCHAR(255) UNIQUE ,
`service_provider_sid` CHAR(36) NOT NULL,
`registration_hook` VARCHAR(255),
`hook_basic_auth_user` VARCHAR(255),
`hook_basic_auth_password` VARCHAR(255),
`is_active` BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (`account_sid`)
) ENGINE=InnoDB COMMENT='A single end-user of the platform';
CREATE TABLE IF NOT EXISTS `subscriptions`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
`subscription_sid` CHAR(36) NOT NULL UNIQUE ,
`registration_sid` CHAR(36) NOT NULL,
`event` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='An active sip subscription';
CREATE TABLE IF NOT EXISTS `voip_carriers`
(
`voip_carrier_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(255) NOT NULL UNIQUE ,
`description` VARCHAR(255),
PRIMARY KEY (`voip_carrier_sid`)
) ENGINE=InnoDB COMMENT='An external organization that can provide sip trunking and D';
CREATE TABLE IF NOT EXISTS `phone_numbers`
(
`phone_number_sid` CHAR(36) UNIQUE ,
`number` VARCHAR(255) NOT NULL UNIQUE ,
`voip_carrier_sid` CHAR(36) NOT NULL,
`account_sid` CHAR(36),
`application_sid` CHAR(36),
PRIMARY KEY (`phone_number_sid`)
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
CREATE TABLE IF NOT EXISTS `sip_gateways`
(
`sip_gateway_sid` CHAR(36),
`ipv4` VARCHAR(32) NOT NULL,
`port` INTEGER NOT NULL DEFAULT 5060,
`inbound` BOOLEAN NOT NULL,
`outbound` BOOLEAN NOT NULL,
`voip_carrier_sid` CHAR(36) NOT NULL,
`is_active` BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (`sip_gateway_sid`)
account_static_ip_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
public_ipv4 VARCHAR(16) NOT NULL UNIQUE ,
private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
PRIMARY KEY (account_static_ip_sid)
);
CREATE UNIQUE INDEX `applications_idx_name` ON `applications` (`account_sid`,`name`);
CREATE TABLE account_subscriptions
(
account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
pending BOOLEAN NOT NULL DEFAULT false,
effective_start_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
effective_end_date DATETIME,
change_reason VARCHAR(255),
stripe_subscription_id VARCHAR(56),
stripe_payment_method_id VARCHAR(56),
stripe_statement_descriptor VARCHAR(255),
last4 VARCHAR(512),
exp_month INTEGER,
exp_year INTEGER,
card_type VARCHAR(16),
pending_reason VARBINARY(52),
PRIMARY KEY (account_subscription_sid)
);
CREATE INDEX `applications_application_sid_idx` ON `applications` (`application_sid`);
CREATE INDEX `applications_name_idx` ON `applications` (`name`);
CREATE INDEX `applications_account_sid_idx` ON `applications` (`account_sid`);
ALTER TABLE `applications` ADD FOREIGN KEY account_sid_idxfk (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE TABLE beta_invite_codes
(
invite_code CHAR(6) NOT NULL UNIQUE ,
in_use BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (invite_code)
);
CREATE INDEX `call_routes_call_route_sid_idx` ON `call_routes` (`call_route_sid`);
ALTER TABLE `call_routes` ADD FOREIGN KEY account_sid_idxfk_1 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE TABLE call_routes
(
call_route_sid CHAR(36) NOT NULL UNIQUE ,
priority INTEGER NOT NULL,
account_sid CHAR(36) NOT NULL,
regex VARCHAR(255) NOT NULL,
application_sid CHAR(36) NOT NULL,
PRIMARY KEY (call_route_sid)
) COMMENT='a regex-based pattern match for call routing';
ALTER TABLE `call_routes` ADD FOREIGN KEY application_sid_idxfk (`application_sid`) REFERENCES `applications` (`application_sid`);
CREATE TABLE dns_records
(
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
record_type VARCHAR(6) NOT NULL,
record_id INTEGER NOT NULL,
PRIMARY KEY (dns_record_sid)
);
CREATE INDEX `conferences_conference_sid_idx` ON `conferences` (`conference_sid`);
CREATE INDEX `conference_participants_conference_participant_sid_idx` ON `conference_participants` (`conference_participant_sid`);
ALTER TABLE `conference_participants` ADD FOREIGN KEY call_sid_idxfk (`call_sid`) REFERENCES `calls` (`call_sid`);
CREATE TABLE lcr_routes
(
lcr_route_sid CHAR(36),
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
description VARCHAR(1024),
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table';
ALTER TABLE `conference_participants` ADD FOREIGN KEY conference_sid_idxfk (`conference_sid`) REFERENCES `conferences` (`conference_sid`);
CREATE TABLE predefined_carriers
(
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
requires_static_ip BOOLEAN NOT NULL DEFAULT false,
e164_leading_plus BOOLEAN NOT NULL DEFAULT false COMMENT 'if true, a leading plus should be prepended to outbound phone numbers',
requires_register BOOLEAN NOT NULL DEFAULT false,
register_username VARCHAR(64),
register_sip_realm VARCHAR(64),
register_password VARCHAR(64),
tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to this carrier',
inbound_auth_username VARCHAR(64),
inbound_auth_password VARCHAR(64),
diversion VARCHAR(32),
PRIMARY KEY (predefined_carrier_sid)
);
CREATE INDEX `queues_queue_sid_idx` ON `queues` (`queue_sid`);
CREATE INDEX `registrations_registration_sid_idx` ON `registrations` (`registration_sid`);
CREATE INDEX `queue_members_queue_member_sid_idx` ON `queue_members` (`queue_member_sid`);
ALTER TABLE `queue_members` ADD FOREIGN KEY call_sid_idxfk_1 (`call_sid`) REFERENCES `calls` (`call_sid`);
CREATE TABLE predefined_sip_gateways
(
predefined_sip_gateway_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
netmask INTEGER NOT NULL DEFAULT 32,
predefined_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (predefined_sip_gateway_sid)
);
ALTER TABLE `queue_members` ADD FOREIGN KEY queue_sid_idxfk (`queue_sid`) REFERENCES `queues` (`queue_sid`);
CREATE TABLE products
(
product_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
PRIMARY KEY (product_sid)
);
CREATE INDEX `calls_call_sid_idx` ON `calls` (`call_sid`);
ALTER TABLE `calls` ADD FOREIGN KEY parent_call_sid_idxfk (`parent_call_sid`) REFERENCES `calls` (`call_sid`);
CREATE TABLE account_products
(
account_product_sid CHAR(36) NOT NULL UNIQUE ,
account_subscription_sid CHAR(36) NOT NULL,
product_sid CHAR(36) NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_product_sid)
);
ALTER TABLE `calls` ADD FOREIGN KEY application_sid_idxfk_1 (`application_sid`) REFERENCES `applications` (`application_sid`);
CREATE TABLE account_offers
(
account_offer_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
product_sid CHAR(36) NOT NULL,
stripe_product_id VARCHAR(56) NOT NULL,
PRIMARY KEY (account_offer_sid)
);
CREATE INDEX `calls_phone_number_sid_idx` ON `calls` (`phone_number_sid`);
ALTER TABLE `calls` ADD FOREIGN KEY phone_number_sid_idxfk (`phone_number_sid`) REFERENCES `phone_numbers` (`phone_number_sid`);
CREATE TABLE api_keys
(
api_key_sid CHAR(36) NOT NULL UNIQUE ,
token CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36),
service_provider_sid CHAR(36),
expires_at TIMESTAMP NULL DEFAULT NULL,
last_used TIMESTAMP NULL DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (api_key_sid)
) COMMENT='An authorization token that is used to access the REST api';
ALTER TABLE `calls` ADD FOREIGN KEY inbound_user_sid_idxfk (`inbound_user_sid`) REFERENCES `registrations` (`registration_sid`);
CREATE TABLE sbc_addresses
(
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060,
service_provider_sid CHAR(36),
PRIMARY KEY (sbc_address_sid)
);
ALTER TABLE `calls` ADD FOREIGN KEY outbound_user_sid_idxfk (`outbound_user_sid`) REFERENCES `registrations` (`registration_sid`);
CREATE TABLE ms_teams_tenants
(
ms_teams_tenant_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
account_sid CHAR(36) NOT NULL,
application_sid CHAR(36),
tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant';
CREATE INDEX `service_providers_service_provider_sid_idx` ON `service_providers` (`service_provider_sid`);
CREATE INDEX `service_providers_name_idx` ON `service_providers` (`name`);
CREATE INDEX `service_providers_root_domain_idx` ON `service_providers` (`root_domain`);
CREATE INDEX `api_keys_api_key_sid_idx` ON `api_keys` (`api_key_sid`);
CREATE INDEX `api_keys_account_sid_idx` ON `api_keys` (`account_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY account_sid_idxfk_2 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE TABLE signup_history
(
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
signed_up_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (email)
);
CREATE INDEX `api_keys_service_provider_sid_idx` ON `api_keys` (`service_provider_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY service_provider_sid_idxfk (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
CREATE TABLE smpp_addresses
(
smpp_address_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060,
use_tls BOOLEAN NOT NULL DEFAULT 0,
is_primary BOOLEAN NOT NULL DEFAULT 1,
service_provider_sid CHAR(36),
PRIMARY KEY (smpp_address_sid)
);
CREATE INDEX `accounts_account_sid_idx` ON `accounts` (`account_sid`);
CREATE INDEX `accounts_name_idx` ON `accounts` (`name`);
CREATE INDEX `accounts_sip_realm_idx` ON `accounts` (`sip_realm`);
CREATE INDEX `accounts_service_provider_sid_idx` ON `accounts` (`service_provider_sid`);
ALTER TABLE `accounts` ADD FOREIGN KEY service_provider_sid_idxfk_1 (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
CREATE TABLE speech_credentials
(
speech_credential_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36),
account_sid CHAR(36),
vendor VARCHAR(32) NOT NULL,
credential VARCHAR(8192) NOT NULL,
use_for_tts BOOLEAN DEFAULT true,
use_for_stt BOOLEAN DEFAULT true,
last_used DATETIME,
last_tested DATETIME,
tts_tested_ok BOOLEAN,
stt_tested_ok BOOLEAN,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (speech_credential_sid)
);
ALTER TABLE `subscriptions` ADD FOREIGN KEY registration_sid_idxfk (`registration_sid`) REFERENCES `registrations` (`registration_sid`);
CREATE TABLE users
(
user_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
pending_email VARCHAR(255),
phone VARCHAR(20) UNIQUE ,
hashed_password VARCHAR(1024),
account_sid CHAR(36),
service_provider_sid CHAR(36),
force_change BOOLEAN NOT NULL DEFAULT FALSE,
provider VARCHAR(255) NOT NULL,
provider_userid VARCHAR(255),
scope VARCHAR(16) NOT NULL DEFAULT 'read-write',
phone_activation_code VARCHAR(16),
email_activation_code VARCHAR(16),
email_validated BOOLEAN NOT NULL DEFAULT false,
phone_validated BOOLEAN NOT NULL DEFAULT false,
email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (user_sid)
);
CREATE INDEX `voip_carriers_voip_carrier_sid_idx` ON `voip_carriers` (`voip_carrier_sid`);
CREATE INDEX `voip_carriers_name_idx` ON `voip_carriers` (`name`);
CREATE INDEX `phone_numbers_phone_number_sid_idx` ON `phone_numbers` (`phone_number_sid`);
CREATE INDEX `phone_numbers_voip_carrier_sid_idx` ON `phone_numbers` (`voip_carrier_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY voip_carrier_sid_idxfk (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
CREATE TABLE voip_carriers
(
voip_carrier_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
description VARCHAR(255),
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a sip trunk that is associated with a specific account',
service_provider_sid CHAR(36),
application_sid CHAR(36) COMMENT 'If provided, all incoming calls from this source will be routed to the associated application',
e164_leading_plus BOOLEAN NOT NULL DEFAULT false COMMENT 'if true, a leading plus should be prepended to outbound phone numbers',
requires_register BOOLEAN NOT NULL DEFAULT false,
register_username VARCHAR(64),
register_sip_realm VARCHAR(64),
register_password VARCHAR(64),
tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to this carrier',
inbound_auth_username VARCHAR(64),
inbound_auth_password VARCHAR(64),
diversion VARCHAR(32),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
smpp_system_id VARCHAR(255),
smpp_password VARCHAR(64),
smpp_enquire_link_interval INTEGER DEFAULT 0,
smpp_inbound_system_id VARCHAR(255),
smpp_inbound_password VARCHAR(64),
PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls';
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_sid_idxfk_3 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE TABLE smpp_gateways
(
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(128) NOT NULL,
port INTEGER NOT NULL DEFAULT 2775,
netmask INTEGER NOT NULL DEFAULT 32,
is_primary BOOLEAN NOT NULL DEFAULT 1,
inbound BOOLEAN NOT NULL DEFAULT 0 COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL DEFAULT 1 COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
use_tls BOOLEAN DEFAULT 0,
voip_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (smpp_gateway_sid)
);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY application_sid_idxfk_2 (`application_sid`) REFERENCES `applications` (`application_sid`);
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(32) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
service_provider_sid CHAR(36) COMMENT 'if not null, this number is a test number for the associated service provider',
PRIMARY KEY (phone_number_sid)
) COMMENT='A phone number that has been assigned to an account';
CREATE UNIQUE INDEX `sip_gateways_sip_gateway_idx_hostport` ON `sip_gateways` (`ipv4`,`port`);
CREATE TABLE sip_gateways
(
sip_gateway_sid CHAR(36),
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
netmask INTEGER NOT NULL DEFAULT 32,
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
ALTER TABLE `sip_gateways` ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
CREATE TABLE lcr_carrier_set_entry
(
lcr_carrier_set_entry_sid CHAR(36),
workload INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
lcr_route_sid CHAR(36) NOT NULL,
voip_carrier_sid CHAR(36) NOT NULL,
priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (lcr_carrier_set_entry_sid)
) COMMENT='An entry in the LCR routing list';
CREATE TABLE webhooks
(
webhook_sid CHAR(36) NOT NULL UNIQUE ,
url VARCHAR(1024) NOT NULL,
method ENUM("GET","POST") NOT NULL DEFAULT 'POST',
username VARCHAR(255),
password VARCHAR(255),
PRIMARY KEY (webhook_sid)
) COMMENT='An HTTP callback';
CREATE TABLE applications
(
application_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
service_provider_sid CHAR(36) COMMENT 'if non-null, this application is a test application that can be used by any account under the associated service provider',
account_sid CHAR(36) COMMENT 'account that this application belongs to (if null, this is a service provider test application)',
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (application_sid)
) COMMENT='A defined set of behaviors to be applied to phone calls ';
CREATE TABLE service_providers
(
service_provider_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL UNIQUE ,
description VARCHAR(255),
root_domain VARCHAR(128) UNIQUE ,
registration_hook_sid CHAR(36),
ms_teams_fqdn VARCHAR(255),
PRIMARY KEY (service_provider_sid)
) COMMENT='A partition of the platform used by one service provider';
CREATE TABLE accounts
(
account_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
sip_realm VARCHAR(132) UNIQUE COMMENT 'sip domain that will be used for devices registering under this account',
service_provider_sid CHAR(36) NOT NULL COMMENT 'service provider that owns the customer relationship with this account',
registration_hook_sid CHAR(36) COMMENT 'webhook to call when devices underr this account attempt to register',
queue_event_hook_sid CHAR(36),
device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
plan_type ENUM('trial','free','paid') NOT NULL DEFAULT 'trial',
stripe_customer_id VARCHAR(56),
webhook_secret VARCHAR(36) NOT NULL,
disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
trial_end_date DATETIME,
deactivated_reason VARCHAR(255),
device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services';
CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_sid);
CREATE INDEX account_sid_idx ON account_static_ips (account_sid);
ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
CREATE INDEX account_sid_idx ON account_subscriptions (account_sid);
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
CREATE INDEX product_sid_idx ON products (product_sid);
CREATE INDEX account_product_sid_idx ON account_products (account_product_sid);
CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid);
ALTER TABLE account_products ADD FOREIGN KEY account_subscription_sid_idxfk (account_subscription_sid) REFERENCES account_subscriptions (account_subscription_sid);
ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REFERENCES products (product_sid);
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
CREATE INDEX account_sid_idx ON account_offers (account_sid);
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX product_sid_idx ON account_offers (product_sid);
ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid);
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
CREATE INDEX account_sid_idx ON api_keys (account_sid);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX sbc_addresses_idx_host_port ON sbc_addresses (ipv4,port);
CREATE INDEX sbc_address_sid_idx ON sbc_addresses (sbc_address_sid);
CREATE INDEX service_provider_sid_idx ON sbc_addresses (service_provider_sid);
ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
CREATE INDEX email_idx ON signup_history (email);
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone);
CREATE INDEX account_sid_idx ON users (account_sid);
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON users (service_provider_sid);
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX email_activation_code_idx ON users (email_activation_code);
CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
CREATE INDEX account_sid_idx ON voip_carriers (account_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid);
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid);
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_3 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid);
CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
CREATE INDEX application_sid_idx ON applications (application_sid);
CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid);
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON applications (account_sid);
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE applications ADD FOREIGN KEY call_status_hook_sid_idxfk (call_status_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE applications ADD FOREIGN KEY messaging_hook_sid_idxfk (messaging_hook_sid) REFERENCES webhooks (webhook_sid);
CREATE INDEX service_provider_sid_idx ON service_providers (service_provider_sid);
CREATE INDEX name_idx ON service_providers (name);
CREATE INDEX root_domain_idx ON service_providers (root_domain);
ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (registration_hook_sid) REFERENCES webhooks (webhook_sid);
CREATE INDEX account_sid_idx ON accounts (account_sid);
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -1,3 +1,3 @@
DROP DATABASE jambones_test;
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'jambones_test'@'localhost';
DROP USER 'jambones_test'@'localhost';
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'jambones_test'@'%';
DROP USER 'jambones_test'@'%';

View File

@@ -1,55 +1,131 @@
version: '3'
version: '3.9'
networks:
sbc-inbound:
fs:
driver: bridge
ipam:
config:
- subnet: 172.38.0.0/16
services:
sbc:
image: drachtio/drachtio-server:latest
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
mysql:
image: mysql:5.7
ports:
- "9060:9022/tcp"
- "3360:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "127.0.0.1", "--protocol", "tcp"]
timeout: 5s
retries: 10
networks:
sbc-inbound:
ipv4_address: 172.38.0.10
appserver:
image: drachtio/sipp:latest
command: sipp -sf /tmp/uas.xml
volumes:
- ./scenarios:/tmp
tty: true
networks:
sbc-inbound:
ipv4_address: 172.38.0.11
auth-server:
image: jambonz/customer-auth-server:latest
command: npm start
ports:
- "4000:4000/tcp"
env_file: docker.env
networks:
sbc-inbound:
ipv4_address: 172.38.0.12
fs:
ipv4_address: 172.38.0.5
redis:
image: redis:5-alpine
ports:
- "16379:6379/tcp"
depends_on:
- mysql
networks:
sbc-inbound:
ipv4_address: 172.38.0.13
fs:
ipv4_address: 172.38.0.6
rtpengine:
image: drachtio/rtpengine:latest
docker-host:
image: qoomon/docker-host
cap_add: [ 'NET_ADMIN', 'NET_RAW' ]
mem_limit: 8M
restart: on-failure
networks:
fs:
ipv4_address: 172.38.0.7
drachtio:
image: drachtio/drachtio-server:latest
restart: always
command: drachtio --contact "sip:*;transport=udp,tcp" --address 0.0.0.0 --port 9022
ports:
- "12222:22222/udp"
- "9060:9022/tcp"
networks:
sbc-inbound:
ipv4_address: 172.38.0.14
fs:
ipv4_address: 172.38.0.50
depends_on:
mysql:
condition: service_healthy
freeswitch:
condition: service_healthy
freeswitch:
image: drachtio/drachtio-freeswitch-mrf:v1.10.1-full
restart: always
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
environment:
GOOGLE_APPLICATION_CREDENTIALS: /opt/credentials/gcp.json
ports:
- "8022:8021/tcp"
volumes:
- /tmp:/tmp
- ./credentials:/opt/credentials
healthcheck:
test: ['CMD', 'fs_cli' ,'-x', '"sofia status"']
timeout: 5s
retries: 15
networks:
fs:
ipv4_address: 172.38.0.51
webhook-decline:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/decline.json
ports:
- "3100:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.60
webhook-say:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/say.json
ports:
- "3101:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.61
webhook-gather:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/gather.json
ports:
- "3102:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.62
webhook-transcribe:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/transcribe.json
ports:
- "3103:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.63
influxdb:
image: influxdb:1.8-alpine
ports:
- "8086:8086"
networks:
fs:
ipv4_address: 172.38.0.90

View File

@@ -1,9 +1,10 @@
const test = require('tape').test ;
const test = require('tape') ;
const exec = require('child_process').exec ;
const async = require('async');
test('starting docker network..', (t) => {
test('starting docker network..takes a bit for mysql and freeswitch to come up..patience..', (t) => {
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
t.pass('docker network is up');
t.end(err);
});

View File

@@ -1,4 +1,4 @@
const test = require('tape').test ;
const test = require('tape') ;
const exec = require('child_process').exec ;
test('stopping docker network..', (t) => {

36
test/gather-tests.js Normal file
View File

@@ -0,0 +1,36 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'gather\' and \'transcribe\' tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10');
let obj = await getJSON('http://127.0.0.1:3102/actionHook');
t.ok(obj.speech.alternatives[0].transcript = 'I\'d like to speak to customer support',
'gather: succeeds when using account credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,8 +1,9 @@
require('./unit-tests');
/*
require('./docker_start');
require('./create-test-db');
require('./sip-tests');
require('./account-validation-tests');
require('./webhooks-tests');
require('./say-tests');
require('./gather-tests');
require('./remove-test-db');
require('./docker_stop');
*/

View File

@@ -1,11 +1,11 @@
const test = require('tape').test ;
const test = require('tape') ;
const exec = require('child_process').exec ;
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
const fs = require('fs');
test('dropping jambones_test database', (t) => {
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('database successfully dropped');
fs.unlinkSync(`${__dirname}/credentials/gcp.json`);
t.end();
});
});

32
test/say-tests.js Normal file
View File

@@ -0,0 +1,32 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'say\' tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
await sippUac('uac-say-account-creds-success.xml', '172.38.0.10');
t.pass('say: succeeds when using using account credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<send retrans="500">
<![CDATA[
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-expect-500
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="500">
</recv>
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-expect-500
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<send retrans="500">
<![CDATA[
INVITE sip:16174000000@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-expect-603
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="603">
</recv>
<send>
<![CDATA[
ACK sip:16174000000@[remote_ip]:[remote_port] SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-expect-603
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000003@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000003@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-gather-account-creds-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000003@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000003 <sip:16174000003@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-gather-account-creds-success
Content-Length: 0
]]>
</send>
<nop>
<action>
<exec rtp_stream="/tmp/scenarios/wav/speak-to-customer-support.wav,1,0"/>
</action>
</nop>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<send retrans="500">
<![CDATA[
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000001@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-inactive-account-expect-503
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="503">
</recv>
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000001@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-inactive-account-expect-503
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<send retrans="500">
<![CDATA[
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: deadbeef
Subject: uac-invalid-account-expect-503
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="503">
</recv>
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-invalid-account-expect-503
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000001@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000001@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000001@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000001 <sip:16174000001@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000002@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000002@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000002@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000002 <sip:16174000002@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000002@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000002@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000002@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000002 <sip:16174000002@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000004@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000004@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-transcribe-account-creds-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000004@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000004 <sip:16174000004@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-transcribe-account-creds-success
Content-Length: 0
]]>
</send>
<nop>
<action>
<exec rtp_stream="/tmp/scenarios/wav/speak-to-customer-support.wav,1,0"/>
</action>
</nop>
<pause milliseconds="10000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:sip:16174000004@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000004 <sip:16174000004@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Max-Forwards: 70
Subject: uac-transcribe-account-creds-success
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000005@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-transcribe-our-creds-fail-low-balance
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000005 <sip:16174000004@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-transcribe-our-creds-fail-low-balance
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</scenario>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000005@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-transcribe-our-creds-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000005 <sip:16174000004@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-transcribe-our-creds-success
Content-Length: 0
]]>
</send>
<nop>
<action>
<exec rtp_stream="/tmp/scenarios/wav/speak-to-customer-support.wav,1,0"/>
</action>
</nop>
<pause milliseconds="10000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000005 <sip:16174000005@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Max-Forwards: 70
Subject: uac-transcribe-our-creds-success
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>

Binary file not shown.

View File

@@ -27,19 +27,19 @@ obj.output = () => {
obj.sippUac = (file, bindAddress) => {
const cmd = 'docker';
const args = [
'run', '-ti', '--rm', '--net', `${network}`,
'run', '-t', '--rm', '--net', `${network}`,
'-v', `${__dirname}/scenarios:/tmp/scenarios`,
'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
'-m', '1',
'-sleep', '250ms',
'-nostdin',
'-cid_str', `%u-%p@%s-${idx++}`,
'sbc'
'172.38.0.50'
];
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
//console.log(args.join(' '));
console.log(args.join(' '));
clearOutput();
return new Promise((resolve, reject) => {
@@ -57,11 +57,11 @@ obj.sippUac = (file, bindAddress) => {
});
child_process.stdout.on('data', (data) => {
//debug(`stdout: ${data}`);
//console.log(`stdout: ${data}`);
addOutput(data.toString());
});
child_process.stdout.on('data', (data) => {
//debug(`stdout: ${data}`);
//console.log(`stdout: ${data}`);
addOutput(data.toString());
});
});

View File

@@ -0,0 +1,6 @@
[
{
"verb": "sip:decline",
"status": 603
}
]

View File

@@ -0,0 +1,12 @@
[
{
"verb": "gather",
"input": ["speech"],
"recognizer": {
"vendor": "google",
"hints": ["customer support", "sales", "human resources", "HR"]
},
"timeout": 10,
"actionHook": "/actionHook"
}
]

6
test/test-apps/say.json Normal file
View File

@@ -0,0 +1,6 @@
[
{
"verb": "say",
"text": "hello"
}
]

View File

@@ -0,0 +1,9 @@
[
{
"verb": "transcribe",
"recognizer": {
"vendor": "google"
},
"transcriptionHook": "/actionHook"
}
]

View File

@@ -9,7 +9,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('app payload parsing tests', (t) => {
test('unit tests', (t) => {
let task = makeTask(logger, require('./data/good/sip-decline'));
t.ok(task.name === 'sip:decline', 'parsed sip:decline');
@@ -53,7 +53,6 @@ test('app payload parsing tests', (t) => {
t.pass('alternate syntax works');
t.end();
process.exit(0);
});

15
test/webhook/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM node:alpine as builder
RUN apk update && apk add --no-cache python make g++
WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm prune
FROM node:alpine as webapp
RUN apk add curl
WORKDIR /opt/app
COPY . /opt/app
COPY --from=builder /opt/app/node_modules ./node_modules
COPY ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

49
test/webhook/app.js Normal file
View File

@@ -0,0 +1,49 @@
const assert = require('assert');
const fs = require('fs');
const express = require('express');
const app = express();
const listenPort = process.env.HTTP_PORT || 3000;
let lastAction, lastEvent;
assert.ok(process.env.APP_PATH, 'env var APP_PATH is required');
app.listen(listenPort, () => {
console.log(`sample jambones app server listening on ${listenPort}`);
});
const applicationData = JSON.parse(fs.readFileSync(process.env.APP_PATH));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.all('/', (req, res) => {
console.log(applicationData, `${req.method} /`);
return res.json(applicationData);
});
app.post('/callStatus', (req, res) => {
console.log({payload: req.body}, 'POST /callStatus');
return res.sendStatus(200);
});
app.post('/actionHook', (req, res) => {
console.log({payload: req.body}, 'POST /actionHook');
lastAction = req.body;
return res.sendStatus(200);
});
app.get('/actionHook', (req, res) => {
console.log({payload: lastAction}, 'GET /actionHook');
return res.json(lastAction);
});
app.post('/eventHook', (req, res) => {
console.log({payload: req.body}, 'POST /eventHook');
lastEvent = req.body;
return res.sendStatus(200);
});
app.get('/eventHook', (req, res) => {
console.log({payload: lastEvent}, 'GET /eventHook');
return res.json(lastEvent);
});

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd /opt/app/
npm start

374
test/webhook/package-lock.json generated Normal file
View File

@@ -0,0 +1,374 @@
{
"name": "webhook",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w=="
},
"mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"requires": {
"mime-db": "1.45.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}

14
test/webhook/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "webhook",
"version": "1.0.0",
"description": "simple webhook app for test purposes",
"main": "app.js",
"scripts": {
"start": "node app"
},
"author": "Dave Horton",
"license": "MIT",
"dependencies": {
"express": "^4.17.1"
}
}

32
test/webhooks-tests.js Normal file
View File

@@ -0,0 +1,32 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('basic webhook tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
await sippUac('uac-expect-603.xml', '172.38.0.10');
t.pass('webhook successfully declines call');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});