mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-11 00:39:56 +00:00
Compare commits
391 Commits
v0.7.8-1
...
v0.9.0-rc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd002ede48 | ||
|
|
1a2aa91973 | ||
|
|
e322b7d8d3 | ||
|
|
7da11df88e | ||
|
|
09cf1345f6 | ||
|
|
2595f527ff | ||
|
|
1d77c0cd20 | ||
|
|
9eab81268b | ||
|
|
ecf3d140d6 | ||
|
|
4a52be9171 | ||
|
|
9b722ae36d | ||
|
|
370b046fac | ||
|
|
fca391c32e | ||
|
|
043860c4a3 | ||
|
|
a021ee3112 | ||
|
|
8999c85a71 | ||
|
|
72147a8110 | ||
|
|
93d0e41e31 | ||
|
|
5b1d8a8ff3 | ||
|
|
ec58232b61 | ||
|
|
65c241bcd1 | ||
|
|
75b6f89e0c | ||
|
|
b80d39d205 | ||
|
|
40f70e3531 | ||
|
|
1914b88af9 | ||
|
|
c946a5d14d | ||
|
|
878578fe0f | ||
|
|
9b3be6c0b9 | ||
|
|
4ae661daea | ||
|
|
dbd3b59901 | ||
|
|
06b066a3f2 | ||
|
|
fc3655c9bd | ||
|
|
1b5f801830 | ||
|
|
d0ebe3f99f | ||
|
|
51a379998f | ||
|
|
c2ae42a456 | ||
|
|
c187685054 | ||
|
|
81234a583c | ||
|
|
206849fa25 | ||
|
|
662b6d3d95 | ||
|
|
5c070597cf | ||
|
|
42be9ff1ca | ||
|
|
f0533c881b | ||
|
|
c894369a13 | ||
|
|
565478cc0a | ||
|
|
cdd25ca33d | ||
|
|
ef2306e558 | ||
|
|
9c33a790bd | ||
|
|
9f9a9ec598 | ||
|
|
75566bb268 | ||
|
|
a55f81676b | ||
|
|
48a81072e8 | ||
|
|
74ede31cd3 | ||
|
|
048229f019 | ||
|
|
71e266ae32 | ||
|
|
5b607693dc | ||
|
|
0491c5ce25 | ||
|
|
a7fa2f95dd | ||
|
|
901e412343 | ||
|
|
e57c7ba90a | ||
|
|
b867395d87 | ||
|
|
1a80910f91 | ||
|
|
5d4f25622d | ||
|
|
aabf37e269 | ||
|
|
b45275789b | ||
|
|
6d5ef6a215 | ||
|
|
b423a51638 | ||
|
|
b4ff2ea702 | ||
|
|
f22d66dfd6 | ||
|
|
09a83e3a31 | ||
|
|
d3d494191f | ||
|
|
859e816a8e | ||
|
|
29bbcf1be0 | ||
|
|
6f6d7a06b0 | ||
|
|
a2ba80a9a3 | ||
|
|
9d70ed96a1 | ||
|
|
8173a306f7 | ||
|
|
2e69630544 | ||
|
|
15829139c1 | ||
|
|
2c48083c26 | ||
|
|
9d8291f892 | ||
|
|
3e8474867f | ||
|
|
9eb315ecd6 | ||
|
|
2ec1460b4e | ||
|
|
e30782ea7b | ||
|
|
83c1c07eb0 | ||
|
|
47fbc1a4a4 | ||
|
|
7474a359a4 | ||
|
|
30977b309c | ||
|
|
bcb4bf43bf | ||
|
|
077460d0e2 | ||
|
|
6629b45671 | ||
|
|
353a9c1917 | ||
|
|
230fe9ea11 | ||
|
|
bb81f9f3da | ||
|
|
a7673c1819 | ||
|
|
59248c7638 | ||
|
|
46755f909c | ||
|
|
4273196447 | ||
|
|
e5b60ca9b0 | ||
|
|
86a14daf79 | ||
|
|
c66ad39001 | ||
|
|
0a0cbd57ba | ||
|
|
eb2d90ffaa | ||
|
|
454ff7d1b8 | ||
|
|
7e349fe4e5 | ||
|
|
9478f3a1b8 | ||
|
|
a3c241b569 | ||
|
|
5a68563f96 | ||
|
|
1cdd0cf611 | ||
|
|
9ae4b04fc5 | ||
|
|
170c3c7ec4 | ||
|
|
7c36a08852 | ||
|
|
633237da1b | ||
|
|
708c2c661f | ||
|
|
87632c549e | ||
|
|
31559cbb3b | ||
|
|
1156bae2de | ||
|
|
c6c599ab99 | ||
|
|
4d0f0fe75f | ||
|
|
6d625d87ad | ||
|
|
7fee2ba2dc | ||
|
|
4b3234f4e4 | ||
|
|
6b9f6a7d90 | ||
|
|
3cdf568fb6 | ||
|
|
e73bef4af0 | ||
|
|
42d1069617 | ||
|
|
e5772d6b85 | ||
|
|
f43a5c1491 | ||
|
|
67f8f7181a | ||
|
|
ddab4d7548 | ||
|
|
916d988dbd | ||
|
|
d6b74c3da8 | ||
|
|
3171b138f9 | ||
|
|
168b4dc051 | ||
|
|
cf0f4d405f | ||
|
|
24fccbdae5 | ||
|
|
7992bc6ca0 | ||
|
|
4b7b0e309b | ||
|
|
1ff4f01d64 | ||
|
|
4a5dbb0115 | ||
|
|
0a2808e64e | ||
|
|
320baf4ac8 | ||
|
|
a92ea9c5da | ||
|
|
4ffa9f915b | ||
|
|
2285ec5329 | ||
|
|
09ae083c9a | ||
|
|
6a3e12e293 | ||
|
|
48f2c57ae2 | ||
|
|
f651cfa0b7 | ||
|
|
cb78627e66 | ||
|
|
ae9386791f | ||
|
|
1aa0b07b8f | ||
|
|
4e916acf6c | ||
|
|
991fff3386 | ||
|
|
76cf4e527f | ||
|
|
d7affddd85 | ||
|
|
d42798e0b4 | ||
|
|
6a8a2aa955 | ||
|
|
6587b1f758 | ||
|
|
c29def92e8 | ||
|
|
a1793ac359 | ||
|
|
d220733dea | ||
|
|
a09605fc51 | ||
|
|
7f59bba634 | ||
|
|
1477605e66 | ||
|
|
4f0ab83f5f | ||
|
|
2935574440 | ||
|
|
c10c561ba1 | ||
|
|
2ccd33e212 | ||
|
|
a03baa8461 | ||
|
|
90df33a15c | ||
|
|
a15479e6dc | ||
|
|
dd74cb2cc6 | ||
|
|
7a02c36bad | ||
|
|
78fd4549af | ||
|
|
b1ecf069bf | ||
|
|
6f0dbef433 | ||
|
|
32dcb2adfa | ||
|
|
ee514f7459 | ||
|
|
4cfea0707a | ||
|
|
f8c5abe9e9 | ||
|
|
ad722a55ee | ||
|
|
82939214a2 | ||
|
|
043a171f41 | ||
|
|
c8e9b34b53 | ||
|
|
d7dcdb1d0c | ||
|
|
fbd0782258 | ||
|
|
38f9329b12 | ||
|
|
d4bfdf0916 | ||
|
|
9203deef0f | ||
|
|
48b182c891 | ||
|
|
e8e987cb9d | ||
|
|
38ea9e7411 | ||
|
|
7b11a56a53 | ||
|
|
66305b5aea | ||
|
|
6793bbf330 | ||
|
|
d8543f73f2 | ||
|
|
e1dad569dc | ||
|
|
643bee48c5 | ||
|
|
487bfd90d9 | ||
|
|
810f6eb695 | ||
|
|
62bc6b4bac | ||
|
|
91fe3ceb06 | ||
|
|
a7d07ce7ae | ||
|
|
7cd6c27f90 | ||
|
|
aad24744f3 | ||
|
|
ab0452879e | ||
|
|
ffdb7a0bb5 | ||
|
|
354818b974 | ||
|
|
30beb9c093 | ||
|
|
b978b3bc2f | ||
|
|
a1c38f8a2e | ||
|
|
37f3668016 | ||
|
|
55935e3f35 | ||
|
|
b7070121ee | ||
|
|
01260ad054 | ||
|
|
bd911c88f9 | ||
|
|
d96712a8d6 | ||
|
|
fdd8f7e743 | ||
|
|
bb852600c0 | ||
|
|
210bbcbdbf | ||
|
|
5910dbf0d3 | ||
|
|
90468ffe48 | ||
|
|
863c4dfa34 | ||
|
|
484be8442c | ||
|
|
7393e3bcb7 | ||
|
|
32a84b7b19 | ||
|
|
6933e82d46 | ||
|
|
fb1801ce11 | ||
|
|
09abb23968 | ||
|
|
eb1e0d3bf5 | ||
|
|
3b6c103618 | ||
|
|
feccc0fca7 | ||
|
|
51bcb5a2d2 | ||
|
|
7a184a8bbc | ||
|
|
5043edfd4e | ||
|
|
9948592080 | ||
|
|
6dc019e836 | ||
|
|
a22bc8ea42 | ||
|
|
0356b996ba | ||
|
|
271587617e | ||
|
|
0b29e67a0c | ||
|
|
e656d275fe | ||
|
|
fabf01f8b5 | ||
|
|
85ab75d8e3 | ||
|
|
5c2630fe1f | ||
|
|
9942313ea1 | ||
|
|
e67cb18b6d | ||
|
|
86df53f8c4 | ||
|
|
5d50f68725 | ||
|
|
f22b236dfc | ||
|
|
2862c827e0 | ||
|
|
266980d770 | ||
|
|
04003a709e | ||
|
|
565ee609ef | ||
|
|
9587465e85 | ||
|
|
845d80a23d | ||
|
|
3109db7861 | ||
|
|
11c5047465 | ||
|
|
e19ea629f0 | ||
|
|
fe529c6bfb | ||
|
|
e980b82ec4 | ||
|
|
318ca19791 | ||
|
|
e2bd211346 | ||
|
|
410c07fae6 | ||
|
|
2ebfbfb3d8 | ||
|
|
a29795839d | ||
|
|
28088a4cdd | ||
|
|
afb381eec9 | ||
|
|
ed00ccb681 | ||
|
|
6e945dde9a | ||
|
|
efdea3e514 | ||
|
|
5131d524ce | ||
|
|
c0114015ea | ||
|
|
a293ec09d0 | ||
|
|
f71ae83ce4 | ||
|
|
0dd161913c | ||
|
|
63ab554908 | ||
|
|
e1bd075ebc | ||
|
|
9de89258a1 | ||
|
|
145ed488db | ||
|
|
c06a43adfa | ||
|
|
bebc82d194 | ||
|
|
cdc82e99ff | ||
|
|
dd4d9aa261 | ||
|
|
1dcf9ee5a2 | ||
|
|
4b28db0946 | ||
|
|
e7ff76b938 | ||
|
|
f245275983 | ||
|
|
690deed89d | ||
|
|
26053ec709 | ||
|
|
34e8203338 | ||
|
|
7be3c64116 | ||
|
|
f71d3aed8b | ||
|
|
5ab24337b2 | ||
|
|
2af76d94a6 | ||
|
|
4919c05181 | ||
|
|
3084a9d6ba | ||
|
|
1c683f1142 | ||
|
|
ab1947e23e | ||
|
|
5527abff09 | ||
|
|
68827112fc | ||
|
|
8a9a2df128 | ||
|
|
3a3544a5e8 | ||
|
|
cbeb706946 | ||
|
|
f005262615 | ||
|
|
67ec28484c | ||
|
|
803a944240 | ||
|
|
a5cd342e46 | ||
|
|
e91feb64f5 | ||
|
|
ae688ddc7e | ||
|
|
9b21b65478 | ||
|
|
c09425fa89 | ||
|
|
6706992b4b | ||
|
|
0fdcb3a6d6 | ||
|
|
50057deca9 | ||
|
|
c7eacdd0f8 | ||
|
|
e990b5dbf9 | ||
|
|
7ae37b1e60 | ||
|
|
ed284c367d | ||
|
|
272380dc62 | ||
|
|
61dbb659b3 | ||
|
|
fbae7c0eab | ||
|
|
4894a85569 | ||
|
|
0e5bb876ce | ||
|
|
8658d03f1f | ||
|
|
f2ff5250b0 | ||
|
|
c37fba541f | ||
|
|
f9921cf4e9 | ||
|
|
86fed4ec90 | ||
|
|
9d07a1354c | ||
|
|
2775c7ddd1 | ||
|
|
70822cb278 | ||
|
|
14a02735be | ||
|
|
4b3ebe37ac | ||
|
|
f4fbd07f8e | ||
|
|
6ebba8673f | ||
|
|
2b06177dc5 | ||
|
|
088316d266 | ||
|
|
8c0044a378 | ||
|
|
dae307d71f | ||
|
|
1b5b37184b | ||
|
|
2f8efb80d0 | ||
|
|
c57e88b496 | ||
|
|
7122d955fe | ||
|
|
028aeea856 | ||
|
|
567b03fd36 | ||
|
|
d5c04d2133 | ||
|
|
a2e909b057 | ||
|
|
c3627cecb8 | ||
|
|
6753fdc2b4 | ||
|
|
740d996739 | ||
|
|
714d06a600 | ||
|
|
0c52324915 | ||
|
|
2e3fb60e72 | ||
|
|
05a4665f87 | ||
|
|
b16d49d8ea | ||
|
|
aad2d52efd | ||
|
|
83d767116b | ||
|
|
b4673ad942 | ||
|
|
9b8bb07a97 | ||
|
|
29f578ff5c | ||
|
|
6d86793494 | ||
|
|
9f95fde67e | ||
|
|
010b4d2778 | ||
|
|
8d81c20c1a | ||
|
|
69f796e960 | ||
|
|
4db03d3d1b | ||
|
|
a60c6a4740 | ||
|
|
5b875c3ad4 | ||
|
|
bf19d2ae6d | ||
|
|
37efdc62be | ||
|
|
78a76bb1f4 | ||
|
|
39fb762a15 | ||
|
|
2cc3140de0 | ||
|
|
1a1f2770b6 | ||
|
|
23f3b44b8b | ||
|
|
753d46e513 | ||
|
|
71a2435c63 | ||
|
|
8686348454 | ||
|
|
f511e6ab6b | ||
|
|
706cd4b94b | ||
|
|
e5c209e269 | ||
|
|
d903dbe28d | ||
|
|
d88321c24d | ||
|
|
6e1761bab6 | ||
|
|
509bb065bb | ||
|
|
203b9774ca | ||
|
|
fade47d423 | ||
|
|
26e52d131e |
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on: [push, pull_request]
|
||||||
push:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -10,7 +9,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version: 20
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run jslint
|
- run: npm run jslint
|
||||||
- run: docker pull drachtio/sipp
|
- run: docker pull drachtio/sipp
|
||||||
@@ -20,3 +19,5 @@ jobs:
|
|||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }}
|
||||||
|
MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }}
|
||||||
59
.github/workflows/docker-publish.yml
vendored
59
.github/workflows/docker-publish.yml
vendored
@@ -2,16 +2,10 @@ name: Docker
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
# Publish `main` as Docker `latest` image.
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
# Publish `v1.2.3` tags as releases.
|
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- '*'
|
||||||
|
|
||||||
env:
|
|
||||||
IMAGE_NAME: feature-server
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
@@ -20,32 +14,41 @@ jobs:
|
|||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build image
|
- name: prepare tag
|
||||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
id: prepare_tag
|
||||||
|
|
||||||
- name: Log into registry
|
|
||||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
|
||||||
|
|
||||||
- name: Push image
|
|
||||||
run: |
|
run: |
|
||||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
IMAGE_ID=jambonz/feature-server
|
||||||
|
|
||||||
# Change all uppercase to lowercase
|
# Strip git ref prefix from version
|
||||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||||
|
|
||||||
# Strip git ref prefix from version
|
# Strip "v" prefix from tag name
|
||||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||||
|
|
||||||
# Strip "v" prefix from tag name
|
# Use Docker `latest` tag convention
|
||||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
[ "$VERSION" == "main" ] && VERSION=latest
|
||||||
|
|
||||||
# Use Docker `latest` tag convention
|
echo IMAGE_ID=$IMAGE_ID
|
||||||
[ "$VERSION" == "main" ] && VERSION=latest
|
echo VERSION=$VERSION
|
||||||
|
|
||||||
echo IMAGE_ID=$IMAGE_ID
|
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||||
echo VERSION=$VERSION
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
- name: Login to Docker Hub
|
||||||
docker push $IMAGE_ID:$VERSION
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
|
||||||
|
build-args: |
|
||||||
|
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
|
||||||
|
GITHUB_REF=$GITHUB_REF
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -40,4 +40,6 @@ examples/*
|
|||||||
ecosystem.config.js
|
ecosystem.config.js
|
||||||
.vscode
|
.vscode
|
||||||
test/credentials/*.json
|
test/credentials/*.json
|
||||||
run-tests.sh
|
run-tests.sh
|
||||||
|
run-coverage.sh
|
||||||
|
.vscode
|
||||||
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Launch Program",
|
|
||||||
"program": "${workspaceFolder}/test/index.js",
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=linux/amd64 node:18.12.1-alpine3.16 as base
|
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
|
||||||
|
|
||||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||||
|
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -1,4 +1,4 @@
|
|||||||
# jambones-feature-server 
|
# jambonz-feature-server [](https://github.com/jambonz/jambonz-feature-server/actions/workflows/build.yml)
|
||||||
|
|
||||||
This application implements the core feature server of the jambones platform.
|
This application implements the core feature server of the jambones platform.
|
||||||
|
|
||||||
@@ -18,8 +18,10 @@ Configuration is provided via environment variables:
|
|||||||
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|
||||||
|DRACHTIO_SECRET| shared secret|yes|
|
|DRACHTIO_SECRET| shared secret|yes|
|
||||||
|ENABLE_METRICS| if 1, metrics will be generated|no|
|
|ENABLE_METRICS| if 1, metrics will be generated|no|
|
||||||
|
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
|
||||||
|GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes|
|
|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|
|
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes|
|
||||||
|
|JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no|
|
||||||
|JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|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_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|
||||||
|JAMBONES_MYSQL_HOST| mysql host|yes|
|
|JAMBONES_MYSQL_HOST| mysql host|yes|
|
||||||
@@ -35,6 +37,11 @@ Configuration is provided via environment variables:
|
|||||||
|STATS_PORT| listening port for metrics host|no|
|
|STATS_PORT| listening port for metrics host|no|
|
||||||
|STATS_PROTOCOL| 'tcp' or 'udp'|no|
|
|STATS_PROTOCOL| 'tcp' or 'udp'|no|
|
||||||
|STATS_TELEGRAF| if 1, metrics will be generated in telegraf format|no|
|
|STATS_TELEGRAF| if 1, metrics will be generated in telegraf format|no|
|
||||||
|
|JAMBONZ_RECORD_WS_BASE_URL| recording websocket URL to send the recording audio|no|
|
||||||
|
|JAMBONZ_RECORD_WS_USERNAME| recording websocket username|no|
|
||||||
|
|JAMBONZ_RECORD_WS_PASSWORD| recording websocket password|no|
|
||||||
|
|ANCHOR_MEDIA_ALWAYS| keep media on media server|no|
|
||||||
|
|JAMBONZ_DISABLE_DIAL_PAI_HEADER| control P-Asserted-Identity header on B-Leg|no|
|
||||||
|
|
||||||
### running under pm2
|
### 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:
|
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:
|
||||||
@@ -87,4 +94,4 @@ module.exports = {
|
|||||||
|
|
||||||
#### Running the test suite
|
#### Running the test suite
|
||||||
|
|
||||||
Please [see this]](./docs/contributing.md#run-the-regression-test-suite).
|
Please [see this](./docs/contributing.md#run-the-regression-test-suite).
|
||||||
71
app.js
71
app.js
@@ -1,21 +1,28 @@
|
|||||||
const assert = require('assert');
|
const {
|
||||||
assert.ok(process.env.JAMBONES_MYSQL_HOST &&
|
DRACHTIO_PORT,
|
||||||
process.env.JAMBONES_MYSQL_USER &&
|
DRACHTIO_HOST,
|
||||||
process.env.JAMBONES_MYSQL_PASSWORD &&
|
DRACHTIO_SECRET,
|
||||||
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
|
JAMBONES_OTEL_SERVICE_NAME,
|
||||||
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
|
JAMBONES_LOGLEVEL,
|
||||||
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
|
JAMBONES_CLUSTER_ID,
|
||||||
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
|
JAMBONZ_CLEANUP_INTERVAL_MINS,
|
||||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
getCleanupIntervalMins,
|
||||||
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
|
K8S,
|
||||||
|
NODE_ENV,
|
||||||
|
checkEnvs,
|
||||||
|
} = require('./lib/config');
|
||||||
|
|
||||||
|
checkEnvs();
|
||||||
|
|
||||||
const Srf = require('drachtio-srf');
|
const Srf = require('drachtio-srf');
|
||||||
const srf = new Srf();
|
const srf = new Srf();
|
||||||
const tracer = require('./tracer')(process.env.JAMBONES_OTEL_SERVICE_NAME || 'jambonz-feature-server');
|
const tracer = require('./tracer')(JAMBONES_OTEL_SERVICE_NAME);
|
||||||
const api = require('@opentelemetry/api');
|
const api = require('@opentelemetry/api');
|
||||||
srf.locals = {...srf.locals, otel: {tracer, api}};
|
srf.locals = {...srf.locals, otel: {tracer, api}};
|
||||||
|
|
||||||
const opts = {level: process.env.JAMBONES_LOGLEVEL || 'info'};
|
const opts = {
|
||||||
|
level: JAMBONES_LOGLEVEL
|
||||||
|
};
|
||||||
const pino = require('pino');
|
const pino = require('pino');
|
||||||
const logger = pino(opts, pino.destination({sync: false}));
|
const logger = pino(opts, pino.destination({sync: false}));
|
||||||
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
|
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
|
||||||
@@ -35,8 +42,8 @@ const {
|
|||||||
const InboundCallSession = require('./lib/session/inbound-call-session');
|
const InboundCallSession = require('./lib/session/inbound-call-session');
|
||||||
const SipRecCallSession = require('./lib/session/siprec-call-session');
|
const SipRecCallSession = require('./lib/session/siprec-call-session');
|
||||||
|
|
||||||
if (process.env.DRACHTIO_HOST) {
|
if (DRACHTIO_HOST) {
|
||||||
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
|
srf.connect({host: DRACHTIO_HOST, port: DRACHTIO_PORT, secret: DRACHTIO_SECRET });
|
||||||
srf.on('connect', (err, hp) => {
|
srf.on('connect', (err, hp) => {
|
||||||
const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop());
|
const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop());
|
||||||
srf.locals.localSipAddress = `${arr[2]}`;
|
srf.locals.localSipAddress = `${arr[2]}`;
|
||||||
@@ -44,10 +51,10 @@ if (process.env.DRACHTIO_HOST) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.info(`listening for drachtio requests on port ${process.env.DRACHTIO_PORT}`);
|
logger.info(`listening for drachtio requests on port ${DRACHTIO_PORT}`);
|
||||||
srf.listen({port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET});
|
srf.listen({port: DRACHTIO_PORT, secret: DRACHTIO_SECRET});
|
||||||
}
|
}
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (NODE_ENV === 'test') {
|
||||||
srf.on('error', (err) => {
|
srf.on('error', (err) => {
|
||||||
logger.info(err, 'Error connecting to drachtio');
|
logger.info(err, 'Error connecting to drachtio');
|
||||||
});
|
});
|
||||||
@@ -102,23 +109,37 @@ const disconnect = () => {
|
|||||||
httpServer?.on('close', resolve);
|
httpServer?.on('close', resolve);
|
||||||
httpServer?.close();
|
httpServer?.close();
|
||||||
srf.disconnect();
|
srf.disconnect();
|
||||||
srf.locals.mediaservers.forEach((ms) => ms.disconnect());
|
srf.locals.mediaservers?.forEach((ms) => ms.disconnect());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('SIGUSR2', handle);
|
|
||||||
process.on('SIGTERM', handle);
|
process.on('SIGTERM', handle);
|
||||||
|
|
||||||
function handle(signal) {
|
function handle(signal) {
|
||||||
const {removeFromSet} = srf.locals.dbHelpers;
|
const {removeFromSet} = srf.locals.dbHelpers;
|
||||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
|
||||||
logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
|
|
||||||
removeFromSet(setName, srf.locals.localSipAddress);
|
|
||||||
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
|
|
||||||
srf.locals.disabled = true;
|
srf.locals.disabled = true;
|
||||||
|
logger.info(`got signal ${signal}`);
|
||||||
|
const setName = `${(JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||||
|
const fsServiceUrlSetName = `${(JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
|
||||||
|
if (setName && srf.locals.localSipAddress) {
|
||||||
|
logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
|
||||||
|
removeFromSet(setName, srf.locals.localSipAddress);
|
||||||
|
}
|
||||||
|
if (fsServiceUrlSetName && srf.locals.serviceUrl) {
|
||||||
|
logger.info(`got signal ${signal}, removing ${srf.locals.serviceUrl} from set ${fsServiceUrlSetName}`);
|
||||||
|
removeFromSet(fsServiceUrlSetName, srf.locals.serviceUrl);
|
||||||
|
}
|
||||||
|
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
|
||||||
|
if (K8S) {
|
||||||
|
srf.locals.lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
|
||||||
|
}
|
||||||
|
if (getCount() === 0) {
|
||||||
|
logger.info('no calls in progress, exiting');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS) {
|
if (JAMBONZ_CLEANUP_INTERVAL_MINS) {
|
||||||
const {clearFiles} = require('./lib/utils/cron-jobs');
|
const {clearFiles} = require('./lib/utils/cron-jobs');
|
||||||
|
|
||||||
/* cleanup orphaned files or channels every so often */
|
/* cleanup orphaned files or channels every so often */
|
||||||
@@ -128,7 +149,7 @@ if (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({err}, 'app.js: error clearing files');
|
logger.error({err}, 'app.js: error clearing files');
|
||||||
}
|
}
|
||||||
}, 1000 * 60 * (process.env.JAMBONZ_CLEANUP_INTERVAL_MINS || 60));
|
}, getCleanupIntervalMins());
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {srf, logger, disconnect};
|
module.exports = {srf, logger, disconnect};
|
||||||
|
|||||||
2
bin/k8s-pre-stop-hook.js
Executable file → Normal file
2
bin/k8s-pre-stop-hook.js
Executable file → Normal file
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const getJSON = bent('json');
|
const getJSON = bent('json');
|
||||||
const PORT = process.env.HTTP_PORT || 3000;
|
const {PORT} = require('../lib/config')
|
||||||
|
|
||||||
const sleep = (ms) => {
|
const sleep = (ms) => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|||||||
@@ -5,14 +5,11 @@
|
|||||||
"at the tone",
|
"at the tone",
|
||||||
"leave a message",
|
"leave a message",
|
||||||
"leave me a message",
|
"leave me a message",
|
||||||
"not available right now",
|
"not available",
|
||||||
"not available to take your call",
|
|
||||||
"can't take your call",
|
"can't take your call",
|
||||||
"I will get back to you",
|
"will get back to you",
|
||||||
"I'll get back to you",
|
"I'll get back to you",
|
||||||
"we will get back to you",
|
"we are unable"
|
||||||
"we are unable",
|
|
||||||
"we are not available"
|
|
||||||
],
|
],
|
||||||
"es-ES": [
|
"es-ES": [
|
||||||
"le pasamos la llamada",
|
"le pasamos la llamada",
|
||||||
@@ -48,5 +45,18 @@
|
|||||||
"ens posarem en contacto",
|
"ens posarem en contacto",
|
||||||
"ara no estem disponibles",
|
"ara no estem disponibles",
|
||||||
"no hi som"
|
"no hi som"
|
||||||
|
],
|
||||||
|
"de-DE": [
|
||||||
|
"nicht erreichbar",
|
||||||
|
"nnruf wurde weitergeleitet",
|
||||||
|
"beim piepsen",
|
||||||
|
"am ton",
|
||||||
|
"eine nachricht hinterlassen",
|
||||||
|
"hinterlasse mir eine Nachricht",
|
||||||
|
"nicht verfügbar",
|
||||||
|
"kann ihren anruf nicht entgegennehmen",
|
||||||
|
"wird sich bei Ihnen melden",
|
||||||
|
"ich melde mich bei dir",
|
||||||
|
"wir können nicht"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ The GCP credential is the JSON service key in stringified format.
|
|||||||
|
|
||||||
#### Install Docker
|
#### Install Docker
|
||||||
|
|
||||||
The test suite ralso equires [Docker](https://www.docker.com/) and docker-compose to be installed on your laptop. Docker is used to set up a network with all of the elements required to test the jambonz-feature-server in a black-box type of fashion.
|
The test suite also requires [Docker](https://www.docker.com/) and docker-compose to be installed on your laptop. Docker is used to set up a network with all of the elements required to test the jambonz-feature-server in a black-box type of fashion.
|
||||||
|
|
||||||
Once you have docker installed, you can optionally make sure everything Docker-wise is working properly by running this command from the project folder:
|
Once you have docker installed, you can optionally make sure everything Docker-wise is working properly by running this command from the project folder:
|
||||||
|
|
||||||
|
|||||||
220
lib/config.js
Normal file
220
lib/config.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const checkEnvs = () => {
|
||||||
|
assert.ok(process.env.JAMBONES_MYSQL_HOST &&
|
||||||
|
process.env.JAMBONES_MYSQL_USER &&
|
||||||
|
process.env.JAMBONES_MYSQL_PASSWORD &&
|
||||||
|
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
|
||||||
|
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
|
||||||
|
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
|
||||||
|
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
|
||||||
|
if (process.env.JAMBONES_REDIS_SENTINELS) {
|
||||||
|
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
|
||||||
|
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
|
||||||
|
} else {
|
||||||
|
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||||
|
}
|
||||||
|
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
|
||||||
|
};
|
||||||
|
|
||||||
|
const NODE_ENV = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
/* database mySQL */
|
||||||
|
const JAMBONES_MYSQL_HOST = process.env.JAMBONES_MYSQL_HOST;
|
||||||
|
const JAMBONES_MYSQL_USER = process.env.JAMBONES_MYSQL_USER;
|
||||||
|
const JAMBONES_MYSQL_PASSWORD = process.env.JAMBONES_MYSQL_PASSWORD;
|
||||||
|
const JAMBONES_MYSQL_DATABASE = process.env.JAMBONES_MYSQL_DATABASE;
|
||||||
|
const JAMBONES_MYSQL_PORT = parseInt(process.env.JAMBONES_MYSQL_PORT, 10) || 3306;
|
||||||
|
const JAMBONES_MYSQL_REFRESH_TTL = parseInt(process.env.JAMBONES_MYSQL_REFRESH_TTL, 10) || 0;
|
||||||
|
const JAMBONES_MYSQL_CONNECTION_LIMIT = parseInt(process.env.JAMBONES_MYSQL_CONNECTION_LIMIT, 10) || 10;
|
||||||
|
|
||||||
|
/* gather and hints */
|
||||||
|
const JAMBONES_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONES_GATHER_EARLY_HINTS_MATCH;
|
||||||
|
const JAMBONZ_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH;
|
||||||
|
const JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS = process.env.JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS;
|
||||||
|
|
||||||
|
const SMPP_URL = process.env.SMPP_URL;
|
||||||
|
|
||||||
|
/* drachtio */
|
||||||
|
const DRACHTIO_PORT = process.env.DRACHTIO_PORT;
|
||||||
|
const DRACHTIO_HOST = process.env.DRACHTIO_HOST;
|
||||||
|
const DRACHTIO_SECRET = process.env.DRACHTIO_SECRET;
|
||||||
|
|
||||||
|
/* freeswitch */
|
||||||
|
const JAMBONES_API_BASE_URL = process.env.JAMBONES_API_BASE_URL;
|
||||||
|
const JAMBONES_FREESWITCH = process.env.JAMBONES_FREESWITCH;
|
||||||
|
const JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS = parseInt(process.env.JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS, 10)
|
||||||
|
|| 180;
|
||||||
|
|
||||||
|
|
||||||
|
const JAMBONES_SBCS = process.env.JAMBONES_SBCS;
|
||||||
|
|
||||||
|
/* websockets */
|
||||||
|
const JAMBONES_WS_HANDSHAKE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS, 10) || 1500;
|
||||||
|
const JAMBONES_WS_MAX_PAYLOAD = parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD, 10) || 24 * 1024;
|
||||||
|
const JAMBONES_WS_PING_INTERVAL_MS = parseInt(process.env.JAMBONES_WS_PING_INTERVAL_MS, 10) || 0;
|
||||||
|
const MAX_RECONNECTS = 5;
|
||||||
|
const RESPONSE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT, 10) || 5000;
|
||||||
|
|
||||||
|
const JAMBONES_NETWORK_CIDR = process.env.JAMBONES_NETWORK_CIDR;
|
||||||
|
const JAMBONES_TIME_SERIES_HOST = process.env.JAMBONES_TIME_SERIES_HOST;
|
||||||
|
const JAMBONES_CLUSTER_ID = process.env.JAMBONES_CLUSTER_ID || 'default';
|
||||||
|
const JAMBONES_ESL_LISTEN_ADDRESS = process.env.JAMBONES_ESL_LISTEN_ADDRESS;
|
||||||
|
|
||||||
|
/* tracing */
|
||||||
|
const JAMBONES_OTEL_ENABLED = process.env.JAMBONES_OTEL_ENABLED;
|
||||||
|
const JAMBONES_OTEL_SERVICE_NAME = process.env.JAMBONES_OTEL_SERVICE_NAME || 'jambonz-feature-server';
|
||||||
|
const OTEL_EXPORTER_JAEGER_AGENT_HOST = process.env.OTEL_EXPORTER_JAEGER_AGENT_HOST;
|
||||||
|
const OTEL_EXPORTER_JAEGER_ENDPOINT = process.env.OTEL_EXPORTER_JAEGER_ENDPOINT;
|
||||||
|
const OTEL_EXPORTER_ZIPKIN_URL = process.env.OTEL_EXPORTER_ZIPKIN_URL;
|
||||||
|
const OTEL_EXPORTER_COLLECTOR_URL = process.env.OTEL_EXPORTER_COLLECTOR_URL;
|
||||||
|
|
||||||
|
const JAMBONES_LOGLEVEL = process.env.JAMBONES_LOGLEVEL || 'info';
|
||||||
|
const JAMBONES_INJECT_CONTENT = process.env.JAMBONES_INJECT_CONTENT;
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.HTTP_PORT, 10) || 3000;
|
||||||
|
const HTTP_PORT_MAX = parseInt(process.env.HTTP_PORT_MAX, 10);
|
||||||
|
|
||||||
|
const K8S = process.env.K8S;
|
||||||
|
const K8S_SBC_SIP_SERVICE_NAME = process.env.K8S_SBC_SIP_SERVICE_NAME;
|
||||||
|
|
||||||
|
const JAMBONES_SUBNET = process.env.JAMBONES_SUBNET;
|
||||||
|
|
||||||
|
/* clean up */
|
||||||
|
const JAMBONZ_CLEANUP_INTERVAL_MINS = process.env.JAMBONZ_CLEANUP_INTERVAL_MINS;
|
||||||
|
const getCleanupIntervalMins = () => {
|
||||||
|
const interval = parseInt(JAMBONZ_CLEANUP_INTERVAL_MINS, 10) || 60;
|
||||||
|
return 1000 * 60 * interval;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* speech vendors */
|
||||||
|
const AWS_REGION = process.env.AWS_REGION;
|
||||||
|
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
|
||||||
|
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
|
||||||
|
const AWS_SNS_PORT = parseInt(process.env.AWS_SNS_PORT, 10) || 3001;
|
||||||
|
const AWS_SNS_TOPIC_ARM = process.env.AWS_SNS_TOPIC_ARM;
|
||||||
|
const AWS_SNS_PORT_MAX = parseInt(process.env.AWS_SNS_PORT_MAX, 10) || 3005;
|
||||||
|
|
||||||
|
const GCP_JSON_KEY = process.env.GCP_JSON_KEY;
|
||||||
|
|
||||||
|
const MICROSOFT_REGION = process.env.MICROSOFT_REGION;
|
||||||
|
const MICROSOFT_API_KEY = process.env.MICROSOFT_API_KEY;
|
||||||
|
|
||||||
|
const SONIOX_API_KEY = process.env.SONIOX_API_KEY;
|
||||||
|
|
||||||
|
const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY;
|
||||||
|
|
||||||
|
const ANCHOR_MEDIA_ALWAYS = process.env.ANCHOR_MEDIA_ALWAYS;
|
||||||
|
const VMD_HINTS_FILE = process.env.VMD_HINTS_FILE;
|
||||||
|
|
||||||
|
/* security, secrets */
|
||||||
|
const LEGACY_CRYPTO = !!process.env.LEGACY_CRYPTO;
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
|
const ENCRYPTION_SECRET = process.env.ENCRYPTION_SECRET;
|
||||||
|
|
||||||
|
/* HTTP/1 pool dispatcher */
|
||||||
|
const HTTP_POOL = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
|
||||||
|
const HTTP_POOLSIZE = parseInt(process.env.HTTP_POOLSIZE, 10) || 10;
|
||||||
|
const HTTP_PIPELINING = parseInt(process.env.HTTP_PIPELINING, 10) || 1;
|
||||||
|
const HTTP_TIMEOUT = 10000;
|
||||||
|
const HTTP_PROXY_IP = process.env.JAMBONES_HTTP_PROXY_IP;
|
||||||
|
const HTTP_PROXY_PORT = process.env.JAMBONES_HTTP_PROXY_PORT;
|
||||||
|
const HTTP_PROXY_PROTOCOL = process.env.JAMBONES_HTTP_PROXY_PROTOCOL || 'http';
|
||||||
|
const HTTP_USER_AGENT_HEADER = process.env.JAMBONES_HTTP_USER_AGENT_HEADER || 'jambonz';
|
||||||
|
|
||||||
|
const OPTIONS_PING_INTERVAL = parseInt(process.env.OPTIONS_PING_INTERVAL, 10) || 30000;
|
||||||
|
|
||||||
|
const JAMBONZ_RECORD_WS_BASE_URL = process.env.JAMBONZ_RECORD_WS_BASE_URL || process.env.JAMBONES_RECORD_WS_BASE_URL;
|
||||||
|
const JAMBONZ_RECORD_WS_USERNAME = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME;
|
||||||
|
const JAMBONZ_RECORD_WS_PASSWORD = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD;
|
||||||
|
const JAMBONZ_DISABLE_DIAL_PAI_HEADER = process.env.JAMBONZ_DISABLE_DIAL_PAI_HEADER || false;
|
||||||
|
const JAMBONES_DISABLE_DIRECT_P2P_CALL = process.env.JAMBONES_DISABLE_DIRECT_P2P_CALL || false;
|
||||||
|
|
||||||
|
const JAMBONES_EAGERLY_PRE_CACHE_AUDIO = parseInt(process.env.JAMBONES_EAGERLY_PRE_CACHE_AUDIO, 10) || 0;
|
||||||
|
|
||||||
|
const JAMBONES_USE_FREESWITCH_TIMER_FD = process.env.JAMBONES_USE_FREESWITCH_TIMER_FD;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
JAMBONES_MYSQL_HOST,
|
||||||
|
JAMBONES_MYSQL_USER,
|
||||||
|
JAMBONES_MYSQL_PASSWORD,
|
||||||
|
JAMBONES_MYSQL_DATABASE,
|
||||||
|
JAMBONES_MYSQL_REFRESH_TTL,
|
||||||
|
JAMBONES_MYSQL_CONNECTION_LIMIT,
|
||||||
|
JAMBONES_MYSQL_PORT,
|
||||||
|
|
||||||
|
DRACHTIO_PORT,
|
||||||
|
DRACHTIO_HOST,
|
||||||
|
DRACHTIO_SECRET,
|
||||||
|
|
||||||
|
JAMBONES_GATHER_EARLY_HINTS_MATCH,
|
||||||
|
JAMBONZ_GATHER_EARLY_HINTS_MATCH,
|
||||||
|
JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS,
|
||||||
|
JAMBONES_FREESWITCH,
|
||||||
|
SMPP_URL,
|
||||||
|
JAMBONES_NETWORK_CIDR,
|
||||||
|
JAMBONES_API_BASE_URL,
|
||||||
|
JAMBONES_TIME_SERIES_HOST,
|
||||||
|
JAMBONES_INJECT_CONTENT,
|
||||||
|
JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
|
||||||
|
JAMBONES_ESL_LISTEN_ADDRESS,
|
||||||
|
JAMBONES_SBCS,
|
||||||
|
JAMBONES_OTEL_ENABLED,
|
||||||
|
JAMBONES_OTEL_SERVICE_NAME,
|
||||||
|
OTEL_EXPORTER_JAEGER_AGENT_HOST,
|
||||||
|
OTEL_EXPORTER_JAEGER_ENDPOINT,
|
||||||
|
OTEL_EXPORTER_ZIPKIN_URL,
|
||||||
|
OTEL_EXPORTER_COLLECTOR_URL,
|
||||||
|
|
||||||
|
JAMBONES_LOGLEVEL,
|
||||||
|
JAMBONES_CLUSTER_ID,
|
||||||
|
PORT,
|
||||||
|
HTTP_PORT_MAX,
|
||||||
|
K8S,
|
||||||
|
K8S_SBC_SIP_SERVICE_NAME,
|
||||||
|
JAMBONES_SUBNET,
|
||||||
|
NODE_ENV,
|
||||||
|
JAMBONZ_CLEANUP_INTERVAL_MINS,
|
||||||
|
getCleanupIntervalMins,
|
||||||
|
checkEnvs,
|
||||||
|
|
||||||
|
AWS_REGION,
|
||||||
|
AWS_ACCESS_KEY_ID,
|
||||||
|
AWS_SECRET_ACCESS_KEY,
|
||||||
|
AWS_SNS_PORT,
|
||||||
|
AWS_SNS_TOPIC_ARM,
|
||||||
|
AWS_SNS_PORT_MAX,
|
||||||
|
|
||||||
|
ANCHOR_MEDIA_ALWAYS,
|
||||||
|
VMD_HINTS_FILE,
|
||||||
|
JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS,
|
||||||
|
|
||||||
|
LEGACY_CRYPTO,
|
||||||
|
JWT_SECRET,
|
||||||
|
ENCRYPTION_SECRET,
|
||||||
|
HTTP_POOL,
|
||||||
|
HTTP_POOLSIZE,
|
||||||
|
HTTP_PIPELINING,
|
||||||
|
HTTP_TIMEOUT,
|
||||||
|
HTTP_PROXY_IP,
|
||||||
|
HTTP_PROXY_PORT,
|
||||||
|
HTTP_PROXY_PROTOCOL,
|
||||||
|
HTTP_USER_AGENT_HEADER,
|
||||||
|
OPTIONS_PING_INTERVAL,
|
||||||
|
RESPONSE_TIMEOUT_MS,
|
||||||
|
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
|
||||||
|
JAMBONES_WS_MAX_PAYLOAD,
|
||||||
|
JAMBONES_WS_PING_INTERVAL_MS,
|
||||||
|
MAX_RECONNECTS,
|
||||||
|
GCP_JSON_KEY,
|
||||||
|
MICROSOFT_REGION,
|
||||||
|
MICROSOFT_API_KEY,
|
||||||
|
SONIOX_API_KEY,
|
||||||
|
DEEPGRAM_API_KEY,
|
||||||
|
JAMBONZ_RECORD_WS_BASE_URL,
|
||||||
|
JAMBONZ_RECORD_WS_USERNAME,
|
||||||
|
JAMBONZ_RECORD_WS_PASSWORD,
|
||||||
|
JAMBONZ_DISABLE_DIAL_PAI_HEADER,
|
||||||
|
JAMBONES_DISABLE_DIRECT_P2P_CALL,
|
||||||
|
JAMBONES_USE_FREESWITCH_TIMER_FD
|
||||||
|
};
|
||||||
54
lib/dynamic-apps.js
Normal file
54
lib/dynamic-apps.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const appsMap = {
|
||||||
|
queue: {
|
||||||
|
// Dummy hook to follow later feature server logic.
|
||||||
|
call_hook: {
|
||||||
|
url: 'https://jambonz.org',
|
||||||
|
method: 'GET'
|
||||||
|
},
|
||||||
|
account_sid: '',
|
||||||
|
app_json: [{
|
||||||
|
verb: 'dequeue',
|
||||||
|
name: '',
|
||||||
|
timeout: 5
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
// Dummy hook to follow later feature server logic.
|
||||||
|
call_hook: {
|
||||||
|
url: 'https://jambonz.org',
|
||||||
|
method: 'GET'
|
||||||
|
},
|
||||||
|
account_sid: '',
|
||||||
|
app_json: [{
|
||||||
|
verb: 'dial',
|
||||||
|
callerId: '',
|
||||||
|
answerOnBridge: true,
|
||||||
|
target: [
|
||||||
|
{
|
||||||
|
type: 'user',
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createJambonzApp = (type, {account_sid, name, caller_id}) => {
|
||||||
|
const app = {...appsMap[type]};
|
||||||
|
app.account_sid = account_sid;
|
||||||
|
switch (type) {
|
||||||
|
case 'queue':
|
||||||
|
app.app_json[0].name = name;
|
||||||
|
break;
|
||||||
|
case 'user':
|
||||||
|
app.app_json[0].callerId = caller_id;
|
||||||
|
app.app_json[0].target[0].name = name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
app.app_json = JSON.stringify(app.app_json);
|
||||||
|
return app;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createJambonzApp
|
||||||
|
};
|
||||||
@@ -5,247 +5,337 @@ const CallInfo = require('../../session/call-info');
|
|||||||
const {CallDirection, CallStatus} = require('../../utils/constants');
|
const {CallDirection, CallStatus} = require('../../utils/constants');
|
||||||
const uuidv4 = require('uuid-random');
|
const uuidv4 = require('uuid-random');
|
||||||
const SipError = require('drachtio-srf').SipError;
|
const SipError = require('drachtio-srf').SipError;
|
||||||
|
const { validationResult, body } = require('express-validator');
|
||||||
|
const { validate } = require('@jambonz/verb-specifications');
|
||||||
const sysError = require('./error');
|
const sysError = require('./error');
|
||||||
const HttpRequestor = require('../../utils/http-requestor');
|
const HttpRequestor = require('../../utils/http-requestor');
|
||||||
const WsRequestor = require('../../utils/ws-requestor');
|
const WsRequestor = require('../../utils/ws-requestor');
|
||||||
const RootSpan = require('../../utils/call-tracer');
|
const RootSpan = require('../../utils/call-tracer');
|
||||||
const dbUtils = require('../../utils/db-utils');
|
const dbUtils = require('../../utils/db-utils');
|
||||||
|
const { mergeSdpMedia, extractSdpMedia } = require('../../utils/sdp-utils');
|
||||||
|
const { createCallSchema, customSanitizeFunction } = require('../schemas/create-call');
|
||||||
|
|
||||||
router.post('/', async(req, res) => {
|
const removeNullProperties = (obj) => (Object.keys(obj).forEach((key) => obj[key] === null && delete obj[key]), obj);
|
||||||
const {logger} = req.app.locals;
|
const removeNulls = (req, res, next) => {
|
||||||
const accountSid = req.body.account_sid;
|
req.body = removeNullProperties(req.body);
|
||||||
const {srf} = require('../../..');
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
logger.debug({body: req.body}, 'got createCall request');
|
router.post('/',
|
||||||
try {
|
removeNulls,
|
||||||
let uri, cs, to;
|
createCallSchema,
|
||||||
const restDial = makeTask(logger, {'rest:dial': req.body});
|
body('tag').custom((value) => {
|
||||||
const {lookupAccountDetails} = dbUtils(logger, srf);
|
if (value) {
|
||||||
const {getSBC, getFreeswitch} = srf.locals;
|
customSanitizeFunction(value);
|
||||||
const sbcAddress = getSBC();
|
}
|
||||||
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
|
return true;
|
||||||
const target = restDial.to;
|
}),
|
||||||
const opts = {
|
async(req, res) => {
|
||||||
callingNumber: restDial.from,
|
const {logger} = req.app.locals;
|
||||||
headers: req.body.headers || {}
|
const errors = validationResult(req);
|
||||||
};
|
if (!errors.isEmpty()) {
|
||||||
|
logger.info({errors: errors.array()}, 'POST /Calls: validation errors');
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
const accountSid = req.body.account_sid;
|
||||||
|
const {srf} = require('../../..');
|
||||||
|
|
||||||
|
const app_json = req.body['app_json'];
|
||||||
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
|
try {
|
||||||
const account = await lookupAccountBySid(req.body.account_sid);
|
// app_json is created only by api-server.
|
||||||
const accountInfo = await lookupAccountDetails(req.body.account_sid);
|
if (app_json) {
|
||||||
const callSid = uuidv4();
|
// if available, delete from req before creating task
|
||||||
|
delete req.body.app_json;
|
||||||
opts.headers = {
|
// validate possible app_json via verb-specifications
|
||||||
...opts.headers,
|
validate(logger, JSON.parse(app_json));
|
||||||
'X-Jambonz-Routing': target.type,
|
}
|
||||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
} catch (err) {
|
||||||
'X-Call-Sid': callSid,
|
logger.debug({ err }, `invalid app_json: ${err.message}`);
|
||||||
'X-Account-Sid': accountSid,
|
|
||||||
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost})
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (target.type) {
|
|
||||||
case 'phone':
|
|
||||||
case 'teams':
|
|
||||||
uri = `sip:${target.number}@${sbcAddress}`;
|
|
||||||
to = target.number;
|
|
||||||
if ('teams' === target.type) {
|
|
||||||
const obj = await lookupTeamsByAccount(accountSid);
|
|
||||||
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}`;
|
|
||||||
to = target.name;
|
|
||||||
if (target.overrideTo) {
|
|
||||||
Object.assign(opts.headers, {
|
|
||||||
'X-Override-To': target.overrideTo
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'sip':
|
|
||||||
uri = target.sipUri;
|
|
||||||
to = uri;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target.type === 'phone' && target.trunk) {
|
logger.debug({body: req.body}, 'got createCall request');
|
||||||
const {lookupCarrier} = dbUtils(this.logger, srf);
|
try {
|
||||||
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
let uri, cs, to;
|
||||||
logger.info(
|
|
||||||
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
const restDial = makeTask(logger, { 'rest:dial': req.body });
|
||||||
if (voip_carrier_sid) {
|
restDial.appJson = app_json;
|
||||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
|
||||||
|
const {lookupAccountDetails, lookupCarrierByPhoneNumber, lookupCarrier} = dbUtils(logger, srf);
|
||||||
|
const {
|
||||||
|
lookupAppBySid
|
||||||
|
} = srf.locals.dbHelpers;
|
||||||
|
const {getSBC, getFreeswitch} = srf.locals;
|
||||||
|
const sbcAddress = getSBC();
|
||||||
|
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
|
||||||
|
const target = restDial.to;
|
||||||
|
const opts = {
|
||||||
|
callingNumber: restDial.from,
|
||||||
|
...(restDial.callerName && {callingName: restDial.callerName}),
|
||||||
|
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();
|
||||||
|
const application = req.body.application_sid ? await lookupAppBySid(req.body.application_sid) : null;
|
||||||
|
const record_all_calls = account.record_all_calls || (application && application.record_all_calls);
|
||||||
|
const recordOutputFormat = account.record_format || 'mp3';
|
||||||
|
const rootSpan = new RootSpan('rest-call', {
|
||||||
|
callSid,
|
||||||
|
accountSid,
|
||||||
|
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid})
|
||||||
|
});
|
||||||
|
|
||||||
|
opts.headers = {
|
||||||
|
...opts.headers,
|
||||||
|
'X-Jambonz-Routing': target.type,
|
||||||
|
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||||
|
'X-Call-Sid': callSid,
|
||||||
|
'X-Account-Sid': accountSid,
|
||||||
|
'X-Trace-ID': rootSpan.traceId,
|
||||||
|
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
||||||
|
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
|
||||||
|
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat})
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (target.type) {
|
||||||
|
case 'phone':
|
||||||
|
case 'teams':
|
||||||
|
uri = `sip:${target.number}@${sbcAddress}`;
|
||||||
|
to = target.number;
|
||||||
|
if ('teams' === target.type) {
|
||||||
|
const obj = await lookupTeamsByAccount(accountSid);
|
||||||
|
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}`;
|
||||||
|
to = target.name;
|
||||||
|
if (target.overrideTo) {
|
||||||
|
Object.assign(opts.headers, {
|
||||||
|
'X-Override-To': target.overrideTo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'sip':
|
||||||
|
uri = target.sipUri;
|
||||||
|
to = uri;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* create endpoint for outdial */
|
if (target.type === 'phone' && target.trunk) {
|
||||||
const ms = getFreeswitch();
|
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
||||||
if (!ms) throw new Error('no available Freeswitch for outbound call creation');
|
logger.info(
|
||||||
const ep = await ms.createEndpoint();
|
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
||||||
logger.debug(`createCall: successfully allocated endpoint, sending INVITE to ${sbcAddress}`);
|
if (voip_carrier_sid) {
|
||||||
|
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||||
/* launch outdial */
|
}
|
||||||
let sdp, sipLogger;
|
|
||||||
const connectStream = async(remoteSdp) => {
|
|
||||||
if (remoteSdp !== sdp) {
|
|
||||||
ep.modify(sdp = remoteSdp);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
/**
|
||||||
Object.assign(opts, {
|
* trunk isn't specified,
|
||||||
proxy: `sip:${sbcAddress}`,
|
* check if from-number matches any existing numbers on Jambonz
|
||||||
localSdp: ep.local.sdp
|
* */
|
||||||
});
|
if (target.type === 'phone' && !target.trunk) {
|
||||||
if (target.auth) opts.auth = this.target.auth;
|
const str = restDial.from || '';
|
||||||
|
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||||
|
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
|
||||||
|
logger.info(
|
||||||
|
`createCall: selected ${voip_carrier_sid} for requested phone number: ${callingNumber || 'unspecified'})`);
|
||||||
|
if (voip_carrier_sid) {
|
||||||
|
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* create endpoint for outdial */
|
||||||
|
const ms = getFreeswitch();
|
||||||
|
if (!ms) throw new Error('no available Freeswitch for outbound call creation');
|
||||||
|
const ep = await ms.createEndpoint();
|
||||||
|
logger.debug(`createCall: successfully allocated endpoint, sending INVITE to ${sbcAddress}`);
|
||||||
|
|
||||||
|
/* launch outdial */
|
||||||
|
let sdp, sipLogger;
|
||||||
|
let dualEp;
|
||||||
|
let localSdp = ep.local.sdp;
|
||||||
|
|
||||||
|
if (req.body.dual_streams) {
|
||||||
|
dualEp = await ms.createEndpoint();
|
||||||
|
localSdp = mergeSdpMedia(localSdp, dualEp.local.sdp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectStream = async(remoteSdp) => {
|
||||||
|
if (remoteSdp !== sdp) {
|
||||||
|
sdp = remoteSdp;
|
||||||
|
if (req.body.dual_streams) {
|
||||||
|
const [sdpLegA, sdpLebB] = extractSdpMedia(remoteSdp);
|
||||||
|
|
||||||
|
await ep.modify(sdpLegA);
|
||||||
|
await dualEp.modify(sdpLebB);
|
||||||
|
await ep.bridge(dualEp);
|
||||||
|
} else {
|
||||||
|
ep.modify(sdp);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
Object.assign(opts, {
|
||||||
|
proxy: `sip:${sbcAddress}`,
|
||||||
|
localSdp
|
||||||
|
});
|
||||||
|
if (target.auth) opts.auth = target.auth;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create our application object -
|
* create our application object -
|
||||||
* not from the database as per an inbound call,
|
* not from the database as per an inbound call,
|
||||||
* but from the provided params in the request
|
* but from the provided params in the request
|
||||||
*/
|
*/
|
||||||
const app = req.body;
|
const app = req.body;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* attach our requestor and notifier objects
|
* attach our requestor and notifier objects
|
||||||
* these will be used for all http requests we make during this call
|
* these will be used for all http requests we make during this call
|
||||||
*/
|
*/
|
||||||
if ('WS' === app.call_hook?.method || /^wss?:/.test(app.call_hook.url)) {
|
if ('WS' === app.call_hook?.method || /^wss?:/.test(app.call_hook.url)) {
|
||||||
logger.debug({call_hook: app.call_hook}, 'creating websocket for call hook');
|
logger.debug({call_hook: app.call_hook}, 'creating websocket for call hook');
|
||||||
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
|
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
|
||||||
if (app.call_hook.url === app.call_status_hook.url || !app.call_status_hook?.url) {
|
if (app.call_hook.url === app.call_status_hook.url || !app.call_status_hook?.url) {
|
||||||
logger.debug('reusing websocket for call status hook');
|
logger.debug('reusing websocket for call status hook');
|
||||||
app.notifier = app.requestor;
|
app.notifier = app.requestor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.debug({call_hook: app.call_hook}, 'creating http client for call hook');
|
||||||
|
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
||||||
|
}
|
||||||
|
if (!app.notifier && app.call_status_hook) {
|
||||||
|
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
|
||||||
|
logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
|
||||||
|
}
|
||||||
|
else if (!app.notifier) {
|
||||||
|
logger.debug('creating null call status hook');
|
||||||
|
app.notifier = {request: () => {}, close: () => {}};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
|
||||||
logger.debug({call_hook: app.call_hook}, 'creating http client for call hook');
|
|
||||||
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
|
|
||||||
}
|
|
||||||
if (!app.notifier && app.call_status_hook) {
|
|
||||||
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
|
|
||||||
logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
|
|
||||||
}
|
|
||||||
else if (!app.notifier) {
|
|
||||||
logger.debug('creating null call status hook');
|
|
||||||
app.notifier = {request: () => {}, close: () => {}};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* now launch the outdial */
|
/* now launch the outdial */
|
||||||
try {
|
try {
|
||||||
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
|
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
|
||||||
cbRequest: (err, inviteReq) => {
|
cbRequest: (err, inviteReq) => {
|
||||||
/* in case of 302 redirect, this gets called twice, ignore the second
|
/* in case of 302 redirect, this gets called twice, ignore the second
|
||||||
except to update the req so that it can later be canceled if need be
|
except to update the req so that it can later be canceled if need be
|
||||||
*/
|
*/
|
||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
logger.info(`create-call: got redirect, updating request to new call-id ${req.get('Call-ID')}`);
|
logger.info(`create-call: got redirect, updating request to new call-id ${req.get('Call-ID')}`);
|
||||||
if (cs) cs.req = inviteReq;
|
if (cs) cs.req = inviteReq;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
logger.error(err, 'createCall Error creating call');
|
||||||
|
res.status(500).send('Call Failure');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inviteReq.srf = srf;
|
||||||
|
inviteReq.locals = {
|
||||||
|
...(inviteReq || {}),
|
||||||
|
callSid,
|
||||||
|
application_sid: app.application_sid
|
||||||
|
};
|
||||||
|
/* ok our outbound INVITE is in flight */
|
||||||
|
|
||||||
|
const tasks = [restDial];
|
||||||
|
sipLogger = logger.child({
|
||||||
|
callSid,
|
||||||
|
callId: inviteReq.get('Call-ID'),
|
||||||
|
accountSid,
|
||||||
|
traceId: rootSpan.traceId
|
||||||
|
});
|
||||||
|
app.requestor.logger = app.notifier.logger = sipLogger;
|
||||||
|
const callInfo = new CallInfo({
|
||||||
|
direction: CallDirection.Outbound,
|
||||||
|
req: inviteReq,
|
||||||
|
to,
|
||||||
|
tag: app.tag,
|
||||||
|
callSid,
|
||||||
|
accountSid: req.body.account_sid,
|
||||||
|
applicationSid: app.application_sid,
|
||||||
|
traceId: rootSpan.traceId
|
||||||
|
});
|
||||||
|
cs = new RestCallSession({
|
||||||
|
logger: sipLogger,
|
||||||
|
application: app,
|
||||||
|
srf,
|
||||||
|
req: inviteReq,
|
||||||
|
ep,
|
||||||
|
ep2: dualEp,
|
||||||
|
tasks,
|
||||||
|
callInfo,
|
||||||
|
accountInfo,
|
||||||
|
rootSpan
|
||||||
|
});
|
||||||
|
cs.exec(req);
|
||||||
|
|
||||||
|
res.status(201).json({sid: cs.callSid, callId: inviteReq.get('Call-ID')});
|
||||||
|
|
||||||
|
sipLogger.info({sid: cs.callSid, callId: inviteReq.get('Call-ID')},
|
||||||
|
`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
|
||||||
|
},
|
||||||
|
cbProvisional: (prov) => {
|
||||||
|
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;
|
||||||
|
if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body);
|
||||||
|
restDial.emit('callStatus', prov.status, !!prov.body);
|
||||||
|
cs.emit('callStatusChange', {callStatus, sipStatus: prov.status});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if (err) {
|
connectStream(dlg.remote.sdp);
|
||||||
logger.error(err, 'createCall Error creating call');
|
cs.emit('callStatusChange', {
|
||||||
res.status(500).send('Call Failure');
|
callStatus: CallStatus.InProgress,
|
||||||
return;
|
sipStatus: 200,
|
||||||
}
|
sipReason: 'OK'
|
||||||
inviteReq.srf = srf;
|
});
|
||||||
inviteReq.locals = {
|
restDial.emit('callStatus', 200);
|
||||||
...(inviteReq || {}),
|
restDial.emit('connect', dlg);
|
||||||
callSid,
|
}
|
||||||
application_sid: app.application_sid
|
catch (err) {
|
||||||
};
|
let callStatus = CallStatus.Failed;
|
||||||
/* ok our outbound INVITE is in flight */
|
if (err instanceof SipError) {
|
||||||
|
if ([486, 603].includes(err.status)) callStatus = CallStatus.Busy;
|
||||||
const tasks = [restDial];
|
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
|
||||||
const rootSpan = new RootSpan('rest-call', inviteReq);
|
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
|
||||||
sipLogger = logger.child({
|
else console.log(`REST outdial failed with ${err.status}`);
|
||||||
callSid,
|
if (cs) cs.emit('callStatusChange', {
|
||||||
callId: inviteReq.get('Call-ID'),
|
callStatus,
|
||||||
accountSid,
|
sipStatus: err.status,
|
||||||
traceId: rootSpan.traceId
|
sipReason: err.reason
|
||||||
});
|
});
|
||||||
app.requestor.logger = app.notifier.logger = sipLogger;
|
cs.callGone = true;
|
||||||
const callInfo = new CallInfo({
|
|
||||||
direction: CallDirection.Outbound,
|
|
||||||
req: inviteReq,
|
|
||||||
to,
|
|
||||||
tag: app.tag,
|
|
||||||
callSid,
|
|
||||||
accountSid: req.body.account_sid,
|
|
||||||
applicationSid: app.application_sid,
|
|
||||||
traceId: rootSpan.traceId
|
|
||||||
});
|
|
||||||
cs = new RestCallSession({
|
|
||||||
logger: sipLogger,
|
|
||||||
application: app,
|
|
||||||
srf,
|
|
||||||
req: inviteReq,
|
|
||||||
ep,
|
|
||||||
tasks,
|
|
||||||
callInfo,
|
|
||||||
accountInfo,
|
|
||||||
rootSpan
|
|
||||||
});
|
|
||||||
cs.exec(req);
|
|
||||||
|
|
||||||
res.status(201).json({sid: cs.callSid, callId: inviteReq.get('Call-ID')});
|
|
||||||
|
|
||||||
sipLogger.info({sid: cs.callSid, callId: inviteReq.get('Call-ID')},
|
|
||||||
`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
|
|
||||||
},
|
|
||||||
cbProvisional: (prov) => {
|
|
||||||
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;
|
|
||||||
if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body);
|
|
||||||
restDial.emit('callStatus', prov.status, !!prov.body);
|
|
||||||
cs.emit('callStatusChange', {callStatus, sipStatus: prov.status});
|
|
||||||
}
|
}
|
||||||
});
|
else {
|
||||||
connectStream(dlg.remote.sdp);
|
if (cs) cs.emit('callStatusChange', {
|
||||||
cs.emit('callStatusChange', {
|
callStatus,
|
||||||
callStatus: CallStatus.InProgress,
|
sipStatus: 500,
|
||||||
sipStatus: 200,
|
sipReason: 'Internal Server Error'
|
||||||
sipReason: 'OK'
|
});
|
||||||
});
|
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
|
||||||
restDial.emit('callStatus', 200);
|
else console.error(err);
|
||||||
restDial.emit('connect', dlg);
|
}
|
||||||
}
|
ep.destroy();
|
||||||
catch (err) {
|
if (dualEp) {
|
||||||
let callStatus = CallStatus.Failed;
|
dualEp.destroy();
|
||||||
if (err instanceof SipError) {
|
}
|
||||||
if ([486, 603].includes(err.status)) callStatus = CallStatus.Busy;
|
setTimeout(restDial.kill.bind(restDial, cs), 5000);
|
||||||
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
|
|
||||||
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,
|
|
||||||
sipReason: err.reason
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
} catch (err) {
|
||||||
if (cs) cs.emit('callStatusChange', {
|
sysError(logger, res, err);
|
||||||
callStatus,
|
|
||||||
sipStatus: 500,
|
|
||||||
sipReason: 'Internal Server Error'
|
|
||||||
});
|
|
||||||
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
|
|
||||||
else console.error(err);
|
|
||||||
}
|
|
||||||
ep.destroy();
|
|
||||||
setTimeout(restDial.kill.bind(restDial), 5000);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
});
|
||||||
sysError(logger, res, err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const router = require('express').Router();
|
|||||||
const CallInfo = require('../../session/call-info');
|
const CallInfo = require('../../session/call-info');
|
||||||
const {CallDirection} = require('../../utils/constants');
|
const {CallDirection} = require('../../utils/constants');
|
||||||
const SmsSession = require('../../session/sms-call-session');
|
const SmsSession = require('../../session/sms-call-session');
|
||||||
const normalizeJambones = require('../../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
const makeTask = require('../../tasks/make_task');
|
const makeTask = require('../../tasks/make_task');
|
||||||
|
|
||||||
router.post('/:sid', async(req, res) => {
|
router.post('/:sid', async(req, res) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const WsRequestor = require('../../utils/ws-requestor');
|
|||||||
const CallInfo = require('../../session/call-info');
|
const CallInfo = require('../../session/call-info');
|
||||||
const {CallDirection} = require('../../utils/constants');
|
const {CallDirection} = require('../../utils/constants');
|
||||||
const SmsSession = require('../../session/sms-call-session');
|
const SmsSession = require('../../session/sms-call-session');
|
||||||
const normalizeJambones = require('../../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
const {TaskPreconditions} = require('../../utils/constants');
|
const {TaskPreconditions} = require('../../utils/constants');
|
||||||
const makeTask = require('../../tasks/make_task');
|
const makeTask = require('../../tasks/make_task');
|
||||||
|
|
||||||
|
|||||||
@@ -9,25 +9,29 @@ const {CallStatus, CallDirection} = require('../../utils/constants');
|
|||||||
*/
|
*/
|
||||||
function retrieveCallSession(callSid, opts) {
|
function retrieveCallSession(callSid, opts) {
|
||||||
if (opts.call_status_hook && !opts.call_hook) {
|
if (opts.call_status_hook && !opts.call_hook) {
|
||||||
throw new DbErrorBadRequest('call_status_hook can be updated only when call_hook is also being updated');
|
throw new DbErrorBadRequest(
|
||||||
|
`call_status_hook can be updated only when call_hook is also being updated for call_sid ${callSid}`);
|
||||||
}
|
}
|
||||||
const cs = sessionTracker.get(callSid);
|
const cs = sessionTracker.get(callSid);
|
||||||
if (!cs) {
|
if (!cs) {
|
||||||
throw new DbErrorUnprocessableRequest('call session is gone');
|
throw new DbErrorUnprocessableRequest(`call session is gone for call_sid ${callSid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
|
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
|
||||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
throw new DbErrorUnprocessableRequest(
|
||||||
|
`current call state is incompatible with requested action for call_sid ${callSid}`);
|
||||||
}
|
}
|
||||||
else if (opts.call_status === CallStatus.NoAnswer) {
|
else if (opts.call_status === CallStatus.NoAnswer) {
|
||||||
if (cs.direction === CallDirection.Outbound) {
|
if (cs.direction === CallDirection.Outbound) {
|
||||||
if (!cs.isOutboundCallRinging) {
|
if (!cs.isOutboundCallRinging) {
|
||||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
throw new DbErrorUnprocessableRequest(
|
||||||
|
`current call state is incompatible with requested action for call_sid ${callSid}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (cs.isInboundCallAnswered) {
|
if (cs.isInboundCallAnswered) {
|
||||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
throw new DbErrorUnprocessableRequest(
|
||||||
|
`current call state is incompatible with requested action for call_sid ${callSid}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
134
lib/http-routes/schemas/create-call.js
Normal file
134
lib/http-routes/schemas/create-call.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
const { checkSchema } = require('express-validator');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @path api-server {{base_url}}/v1/Accounts/:account_sid/Calls
|
||||||
|
* @see https://api.jambonz.org/#243a2edd-7999-41db-bd0d-08082bbab401
|
||||||
|
*/
|
||||||
|
const createCallSchema = checkSchema({
|
||||||
|
application_sid: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
isLength: { options: { min: 36, max: 36 } },
|
||||||
|
errorMessage: 'Invalid application_sid',
|
||||||
|
},
|
||||||
|
answerOnBridge: {
|
||||||
|
isBoolean: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid answerOnBridge',
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
errorMessage: 'Invalid from',
|
||||||
|
isString: true,
|
||||||
|
isLength: {
|
||||||
|
options: { min: 1, max: 256 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fromHost: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid fromHost',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
errorMessage: 'Invalid to',
|
||||||
|
isObject: true,
|
||||||
|
},
|
||||||
|
callerName: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid callerName',
|
||||||
|
},
|
||||||
|
amd: {
|
||||||
|
isObject: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
isObject: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid tag',
|
||||||
|
},
|
||||||
|
app_json: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid app_json',
|
||||||
|
},
|
||||||
|
account_sid: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid account_sid',
|
||||||
|
isLength: { options: { min: 36, max: 36 } },
|
||||||
|
},
|
||||||
|
timeout: {
|
||||||
|
isInt: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid timeout',
|
||||||
|
},
|
||||||
|
timeLimit: {
|
||||||
|
isInt: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid timeLimit',
|
||||||
|
},
|
||||||
|
call_hook: {
|
||||||
|
isObject: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid call_hook',
|
||||||
|
},
|
||||||
|
call_status_hook: {
|
||||||
|
isObject: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid call_status_hook',
|
||||||
|
},
|
||||||
|
speech_synthesis_vendor: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_synthesis_vendor',
|
||||||
|
},
|
||||||
|
speech_synthesis_language: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_synthesis_language',
|
||||||
|
},
|
||||||
|
speech_synthesis_voice: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_synthesis_voice',
|
||||||
|
},
|
||||||
|
speech_recognizer_vendor: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_recognizer_vendor',
|
||||||
|
},
|
||||||
|
speech_recognizer_language: {
|
||||||
|
isString: true,
|
||||||
|
optional: true,
|
||||||
|
errorMessage: 'Invalid speech_recognizer_language',
|
||||||
|
}
|
||||||
|
}, ['body']);
|
||||||
|
|
||||||
|
const customSanitizeFunction = (value) => {
|
||||||
|
try {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value = value.map((item) => customSanitizeFunction(item));
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
Object.keys(value).forEach((key) => {
|
||||||
|
value[key] = customSanitizeFunction(value[key]);
|
||||||
|
});
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
/* trims characters at the beginning and at the end of a string */
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
/* Verify strings including 'http' via new URL */
|
||||||
|
if (value.includes('http')) {
|
||||||
|
value = new URL(value).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
value = `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createCallSchema,
|
||||||
|
customSanitizeFunction
|
||||||
|
};
|
||||||
@@ -6,10 +6,15 @@ const HttpRequestor = require('./utils/http-requestor');
|
|||||||
const WsRequestor = require('./utils/ws-requestor');
|
const WsRequestor = require('./utils/ws-requestor');
|
||||||
const makeTask = require('./tasks/make_task');
|
const makeTask = require('./tasks/make_task');
|
||||||
const parseUri = require('drachtio-srf').parseUri;
|
const parseUri = require('drachtio-srf').parseUri;
|
||||||
const normalizeJambones = require('./utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
const dbUtils = require('./utils/db-utils');
|
const dbUtils = require('./utils/db-utils');
|
||||||
const RootSpan = require('./utils/call-tracer');
|
const RootSpan = require('./utils/call-tracer');
|
||||||
const listTaskNames = require('./utils/summarize-tasks');
|
const listTaskNames = require('./utils/summarize-tasks');
|
||||||
|
const {
|
||||||
|
JAMBONES_MYSQL_REFRESH_TTL,
|
||||||
|
JAMBONES_DISABLE_DIRECT_P2P_CALL
|
||||||
|
} = require('./config');
|
||||||
|
const { createJambonzApp } = require('./dynamic-apps');
|
||||||
|
|
||||||
module.exports = function(srf, logger) {
|
module.exports = function(srf, logger) {
|
||||||
const {
|
const {
|
||||||
@@ -17,17 +22,25 @@ module.exports = function(srf, logger) {
|
|||||||
lookupAppByRegex,
|
lookupAppByRegex,
|
||||||
lookupAppBySid,
|
lookupAppBySid,
|
||||||
lookupAppByRealm,
|
lookupAppByRealm,
|
||||||
lookupAppByTeamsTenant
|
lookupAppByTeamsTenant,
|
||||||
|
registrar,
|
||||||
|
lookupClientByAccountAndUsername
|
||||||
} = srf.locals.dbHelpers;
|
} = srf.locals.dbHelpers;
|
||||||
const {
|
const {
|
||||||
writeAlerts,
|
writeAlerts,
|
||||||
AlertType
|
AlertType
|
||||||
} = srf.locals;
|
} = srf.locals;
|
||||||
const {lookupAccountDetails} = dbUtils(logger, srf);
|
const {lookupAccountDetails, lookupGoogleCustomVoice} = dbUtils(logger, srf);
|
||||||
|
|
||||||
function initLocals(req, res, next) {
|
async function initLocals(req, res, next) {
|
||||||
const callId = req.get('Call-ID');
|
const callId = req.get('Call-ID');
|
||||||
logger.info({callId}, 'new incoming call');
|
const uri = parseUri(req.uri);
|
||||||
|
logger.info({
|
||||||
|
uri,
|
||||||
|
callId,
|
||||||
|
callingNumber: req.callingNumber,
|
||||||
|
calledNumber: req.calledNumber
|
||||||
|
}, 'new incoming call');
|
||||||
if (!req.has('X-Account-Sid')) {
|
if (!req.has('X-Account-Sid')) {
|
||||||
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
|
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
|
||||||
return res.send(500);
|
return res.send(500);
|
||||||
@@ -35,14 +48,63 @@ module.exports = function(srf, logger) {
|
|||||||
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
|
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
|
||||||
const account_sid = req.get('X-Account-Sid');
|
const account_sid = req.get('X-Account-Sid');
|
||||||
req.locals = {callSid, account_sid, callId};
|
req.locals = {callSid, account_sid, callId};
|
||||||
if (req.has('X-Application-Sid')) {
|
|
||||||
|
let clientDb = null;
|
||||||
|
if (req.has('X-Authenticated-User')) {
|
||||||
|
req.locals.originatingUser = req.get('X-Authenticated-User');
|
||||||
|
let clientSettings;
|
||||||
|
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
|
||||||
|
if (arr) {
|
||||||
|
[clientSettings] = await lookupClientByAccountAndUsername(account_sid, arr[1]);
|
||||||
|
}
|
||||||
|
clientDb = await registrar.query(req.locals.originatingUser);
|
||||||
|
clientDb = {
|
||||||
|
...clientDb,
|
||||||
|
...clientSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for call to application
|
||||||
|
if (uri.user?.startsWith('app-') && req.locals.originatingUser && clientDb?.allow_direct_app_calling) {
|
||||||
|
const application_sid = uri.user.match(/app-(.*)/)[1];
|
||||||
|
logger.debug(`got application from Request URI header: ${application_sid}`);
|
||||||
|
req.locals.application_sid = application_sid;
|
||||||
|
} else if (req.has('X-Application-Sid')) {
|
||||||
const application_sid = req.get('X-Application-Sid');
|
const application_sid = req.get('X-Application-Sid');
|
||||||
logger.debug(`got application from X-Application-Sid header: ${application_sid}`);
|
logger.debug(`got application from X-Application-Sid header: ${application_sid}`);
|
||||||
req.locals.application_sid = application_sid;
|
req.locals.application_sid = application_sid;
|
||||||
}
|
}
|
||||||
if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User');
|
// check for call to queue
|
||||||
if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN');
|
if (uri.user?.startsWith('queue-') && req.locals.originatingUser && clientDb?.allow_direct_queue_calling) {
|
||||||
|
const queue_name = uri.user.match(/queue-(.*)/)[1];
|
||||||
|
logger.debug(`got Queue from Request URI header: ${queue_name}`);
|
||||||
|
req.locals.queue_name = queue_name;
|
||||||
|
}
|
||||||
|
// check for call to registered user
|
||||||
|
if (!JAMBONES_DISABLE_DIRECT_P2P_CALL && req.locals.originatingUser && clientDb?.allow_direct_user_calling) {
|
||||||
|
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
|
||||||
|
if (arr) {
|
||||||
|
const sipRealm = arr[2];
|
||||||
|
const called_user = `${req.calledNumber}@${sipRealm}`;
|
||||||
|
const reg = await registrar.query(called_user);
|
||||||
|
if (reg) {
|
||||||
|
logger.debug(`got called Number is a registered user: ${called_user}`);
|
||||||
|
req.locals.called_user = called_user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN');
|
||||||
|
if (req.has('X-Cisco-Recording-Participant')) {
|
||||||
|
const ciscoParticipants = req.get('X-Cisco-Recording-Participant');
|
||||||
|
const regex = /sip:[a-zA-Z0-9]+@[a-zA-Z0-9.-_]+/g;
|
||||||
|
const sipURIs = ciscoParticipants.match(regex);
|
||||||
|
logger.info(`X-Cisco-Recording-Participant : ${sipURIs} `);
|
||||||
|
if (sipURIs && sipURIs.length > 0) {
|
||||||
|
req.locals.calledNumber = sipURIs[0];
|
||||||
|
req.locals.callingNumber = sipURIs[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,8 +152,10 @@ module.exports = function(srf, logger) {
|
|||||||
.find((p) => p.type === 'application/sdp')
|
.find((p) => p.type === 'application/sdp')
|
||||||
.content;
|
.content;
|
||||||
const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger);
|
const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger);
|
||||||
req.locals.calledNumber = metadata.caller.number;
|
if (!req.locals.calledNumber && !req.locals.calledNumber) {
|
||||||
req.locals.callingNumber = metadata.callee.number;
|
req.locals.calledNumber = metadata.caller.number;
|
||||||
|
req.locals.callingNumber = metadata.callee.number;
|
||||||
|
}
|
||||||
req.locals = {
|
req.locals = {
|
||||||
...req.locals,
|
...req.locals,
|
||||||
siprec: {
|
siprec: {
|
||||||
@@ -102,7 +166,7 @@ module.exports = function(srf, logger) {
|
|||||||
};
|
};
|
||||||
logger.info({callId, metadata, sdp}, 'successfully parsed SIPREC payload');
|
logger.info({callId, metadata, sdp}, 'successfully parsed SIPREC payload');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({callId}, 'Error parsing multipart payload');
|
logger.info({err, callId}, 'Error parsing multipart payload');
|
||||||
return res.send(503);
|
return res.send(503);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,15 +230,24 @@ module.exports = function(srf, logger) {
|
|||||||
const {span} = rootSpan.startChildSpan('lookupApplication');
|
const {span} = rootSpan.startChildSpan('lookupApplication');
|
||||||
try {
|
try {
|
||||||
let app;
|
let app;
|
||||||
if (req.locals.application_sid) app = await lookupAppBySid(req.locals.application_sid);
|
if (req.locals.queue_name) {
|
||||||
else if (req.locals.originatingUser) {
|
logger.debug(`calling to queue ${req.locals.queue_name}, generating queue app`);
|
||||||
|
app = createJambonzApp('queue', {account_sid, name: req.locals.queue_name});
|
||||||
|
} else if (req.locals.called_user) {
|
||||||
|
logger.debug(`calling to registered user ${req.locals.called_user}, generating dial app`);
|
||||||
|
app = createJambonzApp('user',
|
||||||
|
{account_sid, name: req.locals.called_user, caller_id: req.locals.callingNumber});
|
||||||
|
} else if (req.locals.application_sid) {
|
||||||
|
app = await lookupAppBySid(req.locals.application_sid);
|
||||||
|
} else if (req.locals.originatingUser) {
|
||||||
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
|
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
const sipRealm = arr[2];
|
const sipRealm = arr[2];
|
||||||
logger.debug(`looking for device calling app for realm ${sipRealm}`);
|
logger.debug(`looking for device calling app for realm ${sipRealm}`);
|
||||||
app = await lookupAppByRealm(sipRealm);
|
app = await lookupAppByRealm(sipRealm);
|
||||||
if (app) logger.debug({app}, `retrieved device calling app for realm ${sipRealm}`);
|
if (app) {
|
||||||
|
logger.debug({app}, `retrieved device calling app for realm ${sipRealm}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (req.locals.msTeamsTenant) {
|
else if (req.locals.msTeamsTenant) {
|
||||||
@@ -227,25 +300,43 @@ module.exports = function(srf, logger) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* allow for caching data - when caching treat retrieved data as immutable */
|
/* allow for caching data - when caching treat retrieved data as immutable */
|
||||||
const app2 = process.env.JAMBONES_MYSQL_REFRESH_TTL ? JSON.parse(JSON.stringify(app)) : app;
|
const app2 = JAMBONES_MYSQL_REFRESH_TTL ? JSON.parse(JSON.stringify(app)) : app;
|
||||||
if ('WS' === app.call_hook?.method ||
|
if ('WS' === app.call_hook?.method ||
|
||||||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
|
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
|
||||||
app2.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
|
const requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
|
||||||
app2.notifier = app.requestor;
|
app2.requestor = requestor;
|
||||||
|
app2.notifier = requestor;
|
||||||
app2.call_hook.method = 'WS';
|
app2.call_hook.method = 'WS';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
app2.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
|
app2.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
|
||||||
if (app.call_status_hook) app2.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook,
|
if (app.call_status_hook) app2.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook,
|
||||||
accountInfo.account.webhook_secret);
|
accountInfo.account.webhook_secret);
|
||||||
else app2.notifier = {request: () => {}};
|
else app2.notifier = {request: () => {}, close: () => {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve application.speech_synthesis_voice if it's custom voice
|
||||||
|
if (app2.speech_synthesis_vendor === 'google' && app2.speech_synthesis_voice.startsWith('custom_')) {
|
||||||
|
const arr = /custom_(.*)/.exec(app2.speech_synthesis_voice);
|
||||||
|
if (arr) {
|
||||||
|
const google_custom_voice_sid = arr[1];
|
||||||
|
const [custom_voice] = await lookupGoogleCustomVoice(google_custom_voice_sid);
|
||||||
|
if (custom_voice) {
|
||||||
|
app2.speech_synthesis_voice = {
|
||||||
|
reportedUsage: custom_voice.reported_usage,
|
||||||
|
model: custom_voice.model
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.locals.application = app2;
|
req.locals.application = app2;
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook
|
const {call_hook, call_status_hook, ...appInfo} = app; // mask sensitive data like user/pass on webhook
|
||||||
logger.info({app: appInfo}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const {requestor, notifier, ...loggable} = appInfo;
|
||||||
|
logger.info({app: loggable}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
||||||
req.locals.callInfo = new CallInfo({
|
req.locals.callInfo = new CallInfo({
|
||||||
req,
|
req,
|
||||||
app: app2,
|
app: app2,
|
||||||
@@ -268,40 +359,54 @@ module.exports = function(srf, logger) {
|
|||||||
const {rootSpan, siprec, application:app} = req.locals;
|
const {rootSpan, siprec, application:app} = req.locals;
|
||||||
let span;
|
let span;
|
||||||
try {
|
try {
|
||||||
if (app.tasks && !process.env.JAMBONES_MYSQL_REFRESH_TTL) {
|
if (app.tasks && !JAMBONES_MYSQL_REFRESH_TTL) {
|
||||||
app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata));
|
app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata));
|
||||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
/* retrieve the application to execute for this inbound call */
|
/* retrieve the application to execute for this inbound call */
|
||||||
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? {sip: req.msg} : {},
|
let json;
|
||||||
req.locals.callInfo,
|
if (app.app_json) {
|
||||||
{service_provider_sid: req.locals.service_provider_sid},
|
json = JSON.parse(app.app_json);
|
||||||
{
|
} else {
|
||||||
defaults: {
|
const defaults = {
|
||||||
synthesizer: {
|
synthesizer: {
|
||||||
vendor: app.speech_synthesis_vendor,
|
vendor: app.speech_synthesis_vendor,
|
||||||
language: app.speech_synthesis_language,
|
...(app.speech_synthesis_label && {label: app.speech_synthesis_label}),
|
||||||
voice: app.speech_synthesis_voice
|
language: app.speech_synthesis_language,
|
||||||
},
|
voice: app.speech_synthesis_voice,
|
||||||
recognizer: {
|
...(app.fallback_speech_synthesis_vendor && {fallback_vendor: app.fallback_speech_synthesis_vendor}),
|
||||||
vendor: app.speech_recognizer_vendor,
|
...(app.fallback_speech_synthesis_label && {fallback_label: app.fallback_speech_synthesis_label}),
|
||||||
language: app.speech_recognizer_language
|
...(app.fallback_speech_synthesis_language && {fallback_language: app.fallback_speech_synthesis_language}),
|
||||||
}
|
...(app.fallback_speech_synthesis_voice && {fallback_voice: app.fallback_speech_synthesis_voice})
|
||||||
|
},
|
||||||
|
recognizer: {
|
||||||
|
vendor: app.speech_recognizer_vendor,
|
||||||
|
...(app.speech_recognizer_label && {label: app.speech_recognizer_label}),
|
||||||
|
language: app.speech_recognizer_language,
|
||||||
|
...(app.fallback_speech_recognizer_vendor && {fallback_vendor: app.fallback_speech_recognizer_vendor}),
|
||||||
|
...(app.fallback_speech_recognizer_label && {fallback_label: app.fallback_speech_recognizer_label}),
|
||||||
|
...(app.fallback_speech_recognizer_language && {fallback_language: app.fallback_speech_recognizer_language})
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
logger.debug({params}, 'sending initial webhook');
|
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? { sip: req.msg } : {},
|
||||||
const obj = rootSpan.startChildSpan('performAppWebhook');
|
req.locals.callInfo,
|
||||||
span = obj.span;
|
{ service_provider_sid: req.locals.service_provider_sid },
|
||||||
const b3 = rootSpan.getTracingPropagation();
|
{ defaults });
|
||||||
const httpHeaders = b3 && {b3};
|
logger.debug({ params }, 'sending initial webhook');
|
||||||
const json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
|
const obj = rootSpan.startChildSpan('performAppWebhook');
|
||||||
|
span = obj.span;
|
||||||
|
const b3 = rootSpan.getTracingPropagation();
|
||||||
|
const httpHeaders = b3 && { b3 };
|
||||||
|
json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||||
span.setAttributes({
|
span?.setAttributes({
|
||||||
'http.statusCode': 200,
|
'http.statusCode': 200,
|
||||||
'app.tasks': listTaskNames(app.tasks)
|
'app.tasks': listTaskNames(app.tasks)
|
||||||
});
|
});
|
||||||
span.end();
|
span?.end();
|
||||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||||
|
|
||||||
if (siprec) {
|
if (siprec) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
const CallSession = require('./call-session');
|
const CallSession = require('./call-session');
|
||||||
|
const {CallStatus} = require('../utils/constants');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @classdesc Subclass of CallSession. Represents a CallSession
|
* @classdesc Subclass of CallSession. Represents a CallSession
|
||||||
@@ -19,12 +21,14 @@ class AdultingCallSession extends CallSession {
|
|||||||
rootSpan
|
rootSpan
|
||||||
});
|
});
|
||||||
this.sd = singleDialer;
|
this.sd = singleDialer;
|
||||||
|
this.req = callInfo.req;
|
||||||
|
|
||||||
this.sd.dlg.on('destroy', () => {
|
this.sd.dlg.on('destroy', () => {
|
||||||
this.logger.info('AdultingCallSession: called party hung up');
|
this.logger.info('AdultingCallSession: called party hung up');
|
||||||
this._callReleased();
|
this._callReleased();
|
||||||
});
|
});
|
||||||
this.sd.emit('adulting');
|
this.sd.emit('adulting');
|
||||||
|
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
get dlg() {
|
get dlg() {
|
||||||
@@ -49,6 +53,26 @@ class AdultingCallSession extends CallSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_callerHungup() {
|
_callerHungup() {
|
||||||
|
this._hangup('caller');
|
||||||
|
}
|
||||||
|
|
||||||
|
_jambonzHangup() {
|
||||||
|
this._hangup();
|
||||||
|
}
|
||||||
|
|
||||||
|
_hangup(terminatedBy = 'jambonz') {
|
||||||
|
if (this.dlg.connectTime) {
|
||||||
|
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||||
|
this.rootSpan.setAttributes({'call.termination': `hangup by ${terminatedBy}`});
|
||||||
|
this.callInfo.callTerminationBy = terminatedBy;
|
||||||
|
this.emit('callStatusChange', {
|
||||||
|
callStatus: CallStatus.Completed,
|
||||||
|
duration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.logger.info(`InboundCallSession: ${terminatedBy} hung up`);
|
||||||
|
this._callReleased();
|
||||||
|
this.req.removeAllListeners('cancel');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const {CallDirection, CallStatus} = require('../utils/constants');
|
const {CallDirection, CallStatus} = require('../utils/constants');
|
||||||
const parseUri = require('drachtio-srf').parseUri;
|
const parseUri = require('drachtio-srf').parseUri;
|
||||||
const uuidv4 = require('uuid-random');
|
const uuidv4 = require('uuid-random');
|
||||||
|
const {JAMBONES_API_BASE_URL} = require('../config');
|
||||||
/**
|
/**
|
||||||
* @classdesc Represents the common information for all calls
|
* @classdesc Represents the common information for all calls
|
||||||
* that is provided in call status webhooks
|
* that is provided in call status webhooks
|
||||||
@@ -33,6 +34,23 @@ class CallInfo {
|
|||||||
this.callStatus = CallStatus.Trying;
|
this.callStatus = CallStatus.Trying;
|
||||||
this.originatingSipIp = req.get('X-Forwarded-For');
|
this.originatingSipIp = req.get('X-Forwarded-For');
|
||||||
this.originatingSipTrunkName = req.get('X-Originating-Carrier');
|
this.originatingSipTrunkName = req.get('X-Originating-Carrier');
|
||||||
|
const {siprec} = req.locals;
|
||||||
|
if (siprec) {
|
||||||
|
const caller = parseUri(req.locals.callingNumber);
|
||||||
|
const callee = parseUri(req.locals.calledNumber);
|
||||||
|
this.participants = [
|
||||||
|
{
|
||||||
|
participant: 'caller',
|
||||||
|
uriUser: caller?.user,
|
||||||
|
uriHost: caller?.host
|
||||||
|
},
|
||||||
|
{
|
||||||
|
participant: 'callee',
|
||||||
|
uriUser: callee?.user,
|
||||||
|
uriHost: callee?.host
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (opts.parentCallInfo) {
|
else if (opts.parentCallInfo) {
|
||||||
// outbound call that is a child of an existing call
|
// outbound call that is a child of an existing call
|
||||||
@@ -129,8 +147,8 @@ class CallInfo {
|
|||||||
Object.assign(obj, {customerData: this._customerData});
|
Object.assign(obj, {customerData: this._customerData});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.JAMBONES_API_BASE_URL) {
|
if (JAMBONES_API_BASE_URL) {
|
||||||
Object.assign(obj, {apiBaseUrl: process.env.JAMBONES_API_BASE_URL});
|
Object.assign(obj, {apiBaseUrl: JAMBONES_API_BASE_URL});
|
||||||
}
|
}
|
||||||
if (this.publicIp) {
|
if (this.publicIp) {
|
||||||
Object.assign(obj, {fsPublicIp: this.publicIp});
|
Object.assign(obj, {fsPublicIp: this.publicIp});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,9 @@ class ConfirmCallSession extends CallSession {
|
|||||||
_callerHungup() {
|
_callerHungup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_jambonzHangup() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,15 +67,27 @@ class InboundCallSession extends CallSession {
|
|||||||
* This is invoked when the caller hangs up, in order to calculate the call duration.
|
* This is invoked when the caller hangs up, in order to calculate the call duration.
|
||||||
*/
|
*/
|
||||||
_callerHungup() {
|
_callerHungup() {
|
||||||
|
this._hangup('caller');
|
||||||
|
}
|
||||||
|
|
||||||
|
_jambonzHangup() {
|
||||||
|
this._hangup();
|
||||||
|
}
|
||||||
|
|
||||||
|
_hangup(terminatedBy = 'jambonz') {
|
||||||
|
if (this.dlg === null) {
|
||||||
|
this.logger.info('InboundCallSession:_hangup - race condition, dlg cleared by app hangup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
assert(this.dlg.connectTime);
|
assert(this.dlg.connectTime);
|
||||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||||
this.rootSpan.setAttributes({'call.termination': 'hangup by caller'});
|
this.rootSpan.setAttributes({'call.termination': `hangup by ${terminatedBy}`});
|
||||||
this.callInfo.callTerminationBy = 'caller';
|
this.callInfo.callTerminationBy = terminatedBy;
|
||||||
this.emit('callStatusChange', {
|
this.emit('callStatusChange', {
|
||||||
callStatus: CallStatus.Completed,
|
callStatus: CallStatus.Completed,
|
||||||
duration
|
duration
|
||||||
});
|
});
|
||||||
this.logger.info('InboundCallSession: caller hung up');
|
this.logger.info(`InboundCallSession: ${terminatedBy} hung up`);
|
||||||
this._callReleased();
|
this._callReleased();
|
||||||
this.req.removeAllListeners('cancel');
|
this.req.removeAllListeners('cancel');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const moment = require('moment');
|
|||||||
* @extends CallSession
|
* @extends CallSession
|
||||||
*/
|
*/
|
||||||
class RestCallSession extends CallSession {
|
class RestCallSession extends CallSession {
|
||||||
constructor({logger, application, srf, req, ep, tasks, callInfo, accountInfo, rootSpan}) {
|
constructor({logger, application, srf, req, ep, ep2, tasks, callInfo, accountInfo, rootSpan}) {
|
||||||
super({
|
super({
|
||||||
logger,
|
logger,
|
||||||
application,
|
application,
|
||||||
@@ -21,6 +21,11 @@ class RestCallSession extends CallSession {
|
|||||||
});
|
});
|
||||||
this.req = req;
|
this.req = req;
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
|
this.ep2 = ep2;
|
||||||
|
// keep restDialTask reference for closing AMD
|
||||||
|
if (tasks.length) {
|
||||||
|
this.restDialTask = tasks[0];
|
||||||
|
}
|
||||||
|
|
||||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||||
this._notifyCallStatusChange({
|
this._notifyCallStatusChange({
|
||||||
@@ -44,10 +49,21 @@ class RestCallSession extends CallSession {
|
|||||||
* This is invoked when the called party hangs up, in order to calculate the call duration.
|
* This is invoked when the called party hangs up, in order to calculate the call duration.
|
||||||
*/
|
*/
|
||||||
_callerHungup() {
|
_callerHungup() {
|
||||||
this.callInfo.callTerminationBy = 'caller';
|
this._hangup('caller');
|
||||||
|
}
|
||||||
|
|
||||||
|
_jambonzHangup() {
|
||||||
|
this._hangup();
|
||||||
|
}
|
||||||
|
|
||||||
|
_hangup(terminatedBy = 'jamboz') {
|
||||||
|
if (this.restDialTask) {
|
||||||
|
this.restDialTask.turnOffAmd();
|
||||||
|
}
|
||||||
|
this.callInfo.callTerminationBy = terminatedBy;
|
||||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||||
this.logger.debug('RestCallSession: called party hung up');
|
this.logger.debug(`RestCallSession: called party hung up by ${terminatedBy}`);
|
||||||
this._callReleased();
|
this._callReleased();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const InboundCallSession = require('./inbound-call-session');
|
const InboundCallSession = require('./inbound-call-session');
|
||||||
const {createSipRecPayload} = require('../utils/siprec-utils');
|
const {createSipRecPayload} = require('../utils/siprec-utils');
|
||||||
const {CallStatus} = require('../utils/constants');
|
const {CallStatus} = require('../utils/constants');
|
||||||
|
const {parseSiprecPayload} = require('../utils/siprec-utils');
|
||||||
/**
|
/**
|
||||||
* @classdesc Subclass of InboundCallSession. This represents a CallSession that is
|
* @classdesc Subclass of InboundCallSession. This represents a CallSession that is
|
||||||
* established for an inbound SIPREC call.
|
* established for an inbound SIPREC call.
|
||||||
@@ -16,6 +17,32 @@ class SipRecCallSession extends InboundCallSession {
|
|||||||
this.metadata = metadata;
|
this.metadata = metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _onReinvite(req, res) {
|
||||||
|
try {
|
||||||
|
this.logger.info(req.payload, 'SipRec Re-INVITE payload');
|
||||||
|
const {sdp1: reSdp1, sdp2: reSdp2, metadata: reMetadata} = await parseSiprecPayload(req, this.logger);
|
||||||
|
this.sdp1 = reSdp1;
|
||||||
|
this.sdp2 = reSdp2;
|
||||||
|
this.metadata = reMetadata;
|
||||||
|
|
||||||
|
if (this.ep && this.ep2) {
|
||||||
|
let remoteSdp = this.sdp1.replace(/sendonly/, 'sendrecv');
|
||||||
|
const newSdp1 = await this.ep.modify(remoteSdp);
|
||||||
|
remoteSdp = this.sdp2.replace(/sendonly/, 'sendrecv');
|
||||||
|
const newSdp2 = await this.ep2.modify(remoteSdp);
|
||||||
|
const combinedSdp = await createSipRecPayload(newSdp1, newSdp2, this.logger);
|
||||||
|
res.send(200, {body: combinedSdp});
|
||||||
|
this.logger.info({offer: req.body, answer: combinedSdp}, 'SipRec handling reINVITE');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.logger.info('got SipRec reINVITE but no endpoint and media has not been released');
|
||||||
|
res.send(488);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err, 'Error handling reinvite');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async answerSipRecCall() {
|
async answerSipRecCall() {
|
||||||
try {
|
try {
|
||||||
this.ms = this.getMS();
|
this.ms = this.getMS();
|
||||||
|
|||||||
22
lib/tasks/answer.js
Normal file
22
lib/tasks/answer.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const Task = require('./task');
|
||||||
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answer the call.
|
||||||
|
* Note: This is rarely used, as the call is typically answered automatically when required by the app,
|
||||||
|
* but it can be useful to force an answer before a pause in some cases
|
||||||
|
*/
|
||||||
|
class TaskAnswer extends Task {
|
||||||
|
constructor(logger, opts) {
|
||||||
|
super(logger, opts);
|
||||||
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return TaskName.Answer; }
|
||||||
|
|
||||||
|
async exec(cs) {
|
||||||
|
super.exec(cs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TaskAnswer;
|
||||||
@@ -2,7 +2,7 @@ const Task = require('./task');
|
|||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||||
const {TaskName, TaskPreconditions, BONG_TONE} = require('../utils/constants');
|
const {TaskName, TaskPreconditions, BONG_TONE} = require('../utils/constants');
|
||||||
const normalizeJambones = require('../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
const makeTask = require('./make_task');
|
const makeTask = require('./make_task');
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
@@ -48,7 +48,7 @@ class Conference extends Task {
|
|||||||
this.confName = this.data.name;
|
this.confName = this.data.name;
|
||||||
[
|
[
|
||||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
||||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook'
|
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook', 'endConferenceDuration'
|
||||||
].forEach((attr) => this[attr] = this.data[attr]);
|
].forEach((attr) => this[attr] = this.data[attr]);
|
||||||
this.record = this.data.record || {};
|
this.record = this.data.record || {};
|
||||||
this.statusEvents = [];
|
this.statusEvents = [];
|
||||||
@@ -108,9 +108,18 @@ class Conference extends Task {
|
|||||||
async kill(cs) {
|
async kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
this.logger.info(`Conference:kill ${this.confName}`);
|
this.logger.info(`Conference:kill ${this.confName}`);
|
||||||
|
if (this._playSession) {
|
||||||
|
this._playSession.kill();
|
||||||
|
this._playSession = null;
|
||||||
|
}
|
||||||
this.emitter.emit('kill');
|
this.emitter.emit('kill');
|
||||||
await this._doFinalMemberCheck(cs);
|
await this._doFinalMemberCheck(cs);
|
||||||
if (this.ep && this.ep.connected) this.ep.conn.removeAllListeners('esl::event::CUSTOM::*') ;
|
if (this.ep && this.ep.connected) {
|
||||||
|
this.ep.conn.removeAllListeners('esl::event::CUSTOM::*');
|
||||||
|
this.ep.api(`conference ${this.confName} kick ${this.memberId}`)
|
||||||
|
.catch((err) => this.logger.info({err}, 'Error kicking participant'));
|
||||||
|
}
|
||||||
|
cs.clearConferenceDetails();
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,9 +344,19 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const opts = {};
|
const opts = {};
|
||||||
if (this.endConferenceOnExit) Object.assign(opts, {flags: {endconf: true}});
|
if (this.endConferenceOnExit || this.startConferenceOnEnter || this.joinMuted) {
|
||||||
if (this.startConferenceOnEnter) Object.assign(opts, {flags: {moderator: true}});
|
Object.assign(opts, {flags: {
|
||||||
if (this.joinMuted) Object.assign(opts, {flags: {mute: true}});
|
...(this.endConferenceOnExit && {endconf: true}),
|
||||||
|
...(this.startConferenceOnEnter && {moderator: true}),
|
||||||
|
...((this.joinMuted || this.data.speakOnlyTo) && {joinMuted: true}),
|
||||||
|
}});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note on the above: if we are joining in "coaching" mode (ie only going to heard by a subset of participants)
|
||||||
|
* then we join muted temporarily, and then unmute ourselves once we have identified the subset of participants
|
||||||
|
* to whom we will be speaking.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {memberId, confUuid} = await this.ep.join(this.confName, opts);
|
const {memberId, confUuid} = await this.ep.join(this.confName, opts);
|
||||||
@@ -345,6 +364,11 @@ class Conference extends Task {
|
|||||||
this.memberId = memberId;
|
this.memberId = memberId;
|
||||||
this.confUuid = confUuid;
|
this.confUuid = confUuid;
|
||||||
|
|
||||||
|
// set a tag for this member, if provided
|
||||||
|
if (this.data.memberTag) {
|
||||||
|
this.setMemberTag(this.data.memberTag);
|
||||||
|
}
|
||||||
|
|
||||||
cs.setConferenceDetails(memberId, this.confName, confUuid);
|
cs.setConferenceDetails(memberId, this.confName, confUuid);
|
||||||
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
|
const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
|
||||||
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
|
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
|
||||||
@@ -371,6 +395,9 @@ class Conference extends Task {
|
|||||||
.catch((err) => {});
|
.catch((err) => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.data.speakOnlyTo) {
|
||||||
|
this.setCoachMode(this.data.speakOnlyTo);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err, `Failed to join conference ${this.confName}`);
|
this.logger.error(err, `Failed to join conference ${this.confName}`);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -380,6 +407,11 @@ class Conference extends Task {
|
|||||||
this.ep.api('conference', `${this.confName} set max_members ${this.maxParticipants}`)
|
this.ep.api('conference', `${this.confName} set max_members ${this.maxParticipants}`)
|
||||||
.catch((err) => this.logger.error(err, `Error setting max participants to ${this.maxParticipants}`));
|
.catch((err) => this.logger.error(err, `Error setting max participants to ${this.maxParticipants}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof this.endConferenceDuration === 'number' && this.endConferenceDuration >= 0) {
|
||||||
|
this.ep.api('conference', `${this.confName} set endconference_grace_time ${this.endConferenceDuration}`)
|
||||||
|
.catch((err) => this.logger.error(err, `Error setting end conference time to ${this.endConferenceDuration}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -410,7 +442,15 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async doConferenceHold(cs, opts) {
|
doConferenceMute(cs, opts) {
|
||||||
|
assert (cs.isInConference);
|
||||||
|
|
||||||
|
const mute = opts.conf_mute_status === 'mute';
|
||||||
|
this.ep.api(`conference ${this.confName} ${mute ? 'mute' : 'unmute'} ${this.memberId}`)
|
||||||
|
.catch((err) => this.logger.info({err}, 'Error muting or unmuting participant'));
|
||||||
|
}
|
||||||
|
|
||||||
|
doConferenceHold(cs, opts) {
|
||||||
assert (cs.isInConference);
|
assert (cs.isInConference);
|
||||||
|
|
||||||
const {conf_hold_status, wait_hook} = opts;
|
const {conf_hold_status, wait_hook} = opts;
|
||||||
@@ -427,13 +467,19 @@ class Conference extends Task {
|
|||||||
.catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant'));
|
.catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wait_hook) {
|
||||||
|
if (this.wait_hook)
|
||||||
|
delete this.wait_hook.url;
|
||||||
|
this.wait_hook = {url: wait_hook};
|
||||||
|
}
|
||||||
|
|
||||||
if (hookOnly && this._playSession) {
|
if (hookOnly && this._playSession) {
|
||||||
this._playSession.kill();
|
this._playSession.kill();
|
||||||
this._playSession = null;
|
this._playSession = null;
|
||||||
}
|
}
|
||||||
if (wait_hook && this.conf_hold_status === 'hold') {
|
if (this.wait_hook?.url && this.conf_hold_status === 'hold') {
|
||||||
const {dlg} = cs;
|
const {dlg} = cs;
|
||||||
this._doWaitHookWhileOnHold(cs, dlg, wait_hook);
|
this._doWaitHookWhileOnHold(cs, dlg, this.wait_hook);
|
||||||
}
|
}
|
||||||
else if (this.conf_hold_status !== 'hold' && this._playSession) {
|
else if (this.conf_hold_status !== 'hold' && this._playSession) {
|
||||||
this._playSession.kill();
|
this._playSession.kill();
|
||||||
@@ -441,10 +487,46 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async doConferenceParticipantAction(cs, opts) {
|
||||||
|
const {action, tag} = opts;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'tag':
|
||||||
|
await this.setMemberTag(tag);
|
||||||
|
break;
|
||||||
|
case 'untag':
|
||||||
|
await this.clearMemberTag();
|
||||||
|
break;
|
||||||
|
case 'coach':
|
||||||
|
await this.setCoachMode(tag);
|
||||||
|
break;
|
||||||
|
case 'uncoach':
|
||||||
|
await this.clearCoachMode();
|
||||||
|
break;
|
||||||
|
case 'hold':
|
||||||
|
this.doConferenceHold(cs, {conf_hold_status: 'hold'});
|
||||||
|
break;
|
||||||
|
case 'unhold':
|
||||||
|
this.doConferenceHold(cs, {conf_hold_status: 'unhold'});
|
||||||
|
break;
|
||||||
|
case 'mute':
|
||||||
|
this.doConferenceMute(cs, {conf_mute_status: 'mute'});
|
||||||
|
break;
|
||||||
|
case 'unmute':
|
||||||
|
this.doConferenceMute(cs, {conf_mute_status: 'unmute'});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.logger.info(`Conference:doConferenceParticipantState - unhandled action ${action}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
|
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
const tasks = await this._playHook(cs, dlg, wait_hook);
|
let tasks = [];
|
||||||
|
if (wait_hook.url)
|
||||||
|
tasks = await this._playHook(cs, dlg, wait_hook.url);
|
||||||
if (0 === tasks.length) break;
|
if (0 === tasks.length) break;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!this.killed) {
|
if (!this.killed) {
|
||||||
@@ -571,6 +653,10 @@ class Conference extends Task {
|
|||||||
*/
|
*/
|
||||||
_kicked(cs, dlg) {
|
_kicked(cs, dlg) {
|
||||||
this.logger.info(`Conference:kicked - I was dropped from conference ${this.confName}, task is complete`);
|
this.logger.info(`Conference:kicked - I was dropped from conference ${this.confName}, task is complete`);
|
||||||
|
if (this._playSession) {
|
||||||
|
this._playSession.kill();
|
||||||
|
this._playSession = null;
|
||||||
|
}
|
||||||
this.replaceEndpointAndEnd(cs);
|
this.replaceEndpointAndEnd(cs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,11 +698,14 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// conference event handlers
|
// conference event handlers
|
||||||
|
_onAddMember(logger, cs, evt) {
|
||||||
|
logger.debug({evt}, `Conference:_onAddMember - member added to conference ${this.confName}`);
|
||||||
|
}
|
||||||
_onDelMember(logger, cs, evt) {
|
_onDelMember(logger, cs, evt) {
|
||||||
const memberId = parseInt(evt.getHeader('Member-ID')) ;
|
const memberId = parseInt(evt.getHeader('Member-ID')) ;
|
||||||
this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
this.participantCount = parseInt(evt.getHeader('Conference-Size'));
|
||||||
if (memberId === this.memberId) {
|
if (memberId === this.memberId) {
|
||||||
this.logger.info(`Conference:_onDelMember - I was dropped from conference ${this.confName}, task is complete`);
|
logger.info(`Conference:_onDelMember - I was dropped from conference ${this.confName}, task is complete`);
|
||||||
this.replaceEndpointAndEnd(cs);
|
this.replaceEndpointAndEnd(cs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,6 +734,53 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setCoachMode(speakOnlyTo) {
|
||||||
|
try {
|
||||||
|
const response = await this.ep.api('conference', [this.confName, 'gettag', speakOnlyTo, 'nomatch']);
|
||||||
|
this.logger.info(`Conference:_setCoachMode: my audio will only be sent to particpants ${response}`);
|
||||||
|
await this.ep.api('conference', [this.confName, 'relate', this.memberId, response, 'nospeak']);
|
||||||
|
this.speakOnlyTo = speakOnlyTo;
|
||||||
|
this.coaching = response;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err, speakOnlyTo}, '_setCoachMode: Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearCoachMode() {
|
||||||
|
try {
|
||||||
|
if (!this.coaching) {
|
||||||
|
this.logger.info('Conference:_clearCoachMode: no coaching mode to clear');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger.info(`Conference:_clearCoachMode: now sending my audio to all, including ${this.coaching}`);
|
||||||
|
await this.ep.api('conference', [this.confName, 'relate', this.memberId, this.coaching, 'clear']);
|
||||||
|
this.speakOnlyTo = null;
|
||||||
|
this.coaching = null;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, '_clearCoachMode: Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setMemberTag(tag) {
|
||||||
|
try {
|
||||||
|
await this.ep.api('conference', [this.confName, 'tag', this.memberId, tag]);
|
||||||
|
this.logger.info(`Conference:setMemberTag: set tag for ${this.memberId} to ${tag}`);
|
||||||
|
this.memberTag = tag;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, `Error setting tag for ${this.memberId} to ${tag}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearMemberTag() {
|
||||||
|
try {
|
||||||
|
await this.ep.api('conference', [this.confName, 'tag', this.memberId]);
|
||||||
|
this.logger.info(`Conference:setMemberTag: clearing tag for ${this.memberId}`);
|
||||||
|
this.memberTag = null;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, `Error clearing tag for ${this.memberId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Conference;
|
module.exports = Conference;
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
|
const parseDecibels = require('../utils/parse-decibels');
|
||||||
|
|
||||||
class TaskConfig extends Task {
|
class TaskConfig extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
|
|
||||||
[
|
[
|
||||||
'synthesizer',
|
'synthesizer',
|
||||||
'recognizer',
|
'recognizer',
|
||||||
'bargeIn',
|
'bargeIn',
|
||||||
'record'
|
'record',
|
||||||
|
'listen',
|
||||||
|
'transcribe',
|
||||||
|
'fillerNoise',
|
||||||
|
'actionHookDelayAction',
|
||||||
|
'boostAudioSignal'
|
||||||
].forEach((k) => this[k] = this.data[k] || {});
|
].forEach((k) => this[k] = this.data[k] || {});
|
||||||
|
|
||||||
|
if ('notifyEvents' in this.data) {
|
||||||
|
this.notifyEvents = !!this.data.notifyEvents;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.bargeIn.enable) {
|
if (this.bargeIn.enable) {
|
||||||
this.gatherOpts = {
|
this.gatherOpts = {
|
||||||
verb: 'gather',
|
verb: 'gather',
|
||||||
@@ -25,20 +36,48 @@ class TaskConfig extends Task {
|
|||||||
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
|
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (this.transcribe?.enable) {
|
||||||
|
this.transcribeOpts = {
|
||||||
|
verb: 'transcribe',
|
||||||
|
...this.transcribe
|
||||||
|
};
|
||||||
|
delete this.transcribeOpts.enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.data.reset) {
|
||||||
|
if (typeof this.data.reset === 'string') this.data.reset = [this.data.reset];
|
||||||
|
}
|
||||||
|
else this.data.reset = [];
|
||||||
|
|
||||||
if (this.bargeIn.sticky) this.autoEnable = true;
|
if (this.bargeIn.sticky) this.autoEnable = true;
|
||||||
this.preconditions = (this.bargeIn.enable || this.record?.action || this.data.amd) ?
|
this.preconditions = (this.bargeIn.enable ||
|
||||||
|
this.record?.action ||
|
||||||
|
this.listen?.url ||
|
||||||
|
this.data.amd ||
|
||||||
|
'boostAudioSignal' in this.data ||
|
||||||
|
this.transcribe?.enable) ?
|
||||||
TaskPreconditions.Endpoint :
|
TaskPreconditions.Endpoint :
|
||||||
TaskPreconditions.None;
|
TaskPreconditions.None;
|
||||||
|
|
||||||
|
this.onHoldMusic = this.data.onHoldMusic;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Config; }
|
get name() { return TaskName.Config; }
|
||||||
|
|
||||||
get hasSynthesizer() { return Object.keys(this.synthesizer).length; }
|
get hasSynthesizer() { return Object.keys(this.synthesizer).length; }
|
||||||
|
|
||||||
get hasRecognizer() { return Object.keys(this.recognizer).length; }
|
get hasRecognizer() { return Object.keys(this.recognizer).length; }
|
||||||
|
get hasRecording() { return Object.keys(this.record).length; }
|
||||||
|
get hasListen() { return Object.keys(this.listen).length; }
|
||||||
|
get hasTranscribe() { return Object.keys(this.transcribe).length; }
|
||||||
|
get hasDub() { return Object.keys(this.dub).length; }
|
||||||
|
get hasFillerNoise() { return Object.keys(this.fillerNoise).length; }
|
||||||
|
|
||||||
get summary() {
|
get summary() {
|
||||||
const phrase = [];
|
const phrase = [];
|
||||||
|
|
||||||
|
/* reset recognizer and/or synthesizer to default values? */
|
||||||
|
if (this.data.reset.length) phrase.push(`reset ${this.data.reset.join(',')}`);
|
||||||
|
|
||||||
if (this.bargeIn.enable) phrase.push('enable barge-in');
|
if (this.bargeIn.enable) phrase.push('enable barge-in');
|
||||||
if (this.hasSynthesizer) {
|
if (this.hasSynthesizer) {
|
||||||
const {vendor:v, language:l, voice} = this.synthesizer;
|
const {vendor:v, language:l, voice} = this.synthesizer;
|
||||||
@@ -50,13 +89,33 @@ class TaskConfig extends Task {
|
|||||||
const s = `{${v},${l}}`;
|
const s = `{${v},${l}}`;
|
||||||
phrase.push(`set recognizer${s}`);
|
phrase.push(`set recognizer${s}`);
|
||||||
}
|
}
|
||||||
|
if (this.hasRecording) phrase.push(this.record.action);
|
||||||
|
if (this.hasListen) {
|
||||||
|
phrase.push(this.listen.enable ? `listen ${this.listen.url}` : 'stop listen');
|
||||||
|
}
|
||||||
|
if (this.hasTranscribe) {
|
||||||
|
phrase.push(this.transcribe.enable ? `transcribe ${this.transcribe.transcriptionHook}` : 'stop transcribe');
|
||||||
|
}
|
||||||
|
if (this.hasFillerNoise) phrase.push(`fillerNoise ${this.fillerNoise.enable ? 'on' : 'off'}`);
|
||||||
if (this.data.amd) phrase.push('enable amd');
|
if (this.data.amd) phrase.push('enable amd');
|
||||||
return `${this.name}{${phrase.join(',')}`;
|
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||||
|
if (this.onHoldMusic) phrase.push(`onHoldMusic: ${this.onHoldMusic}`);
|
||||||
|
if ('boostAudioSignal' in this.data) phrase.push(`setGain ${this.data.boostAudioSignal}`);
|
||||||
|
return `${this.name}{${phrase.join(',')}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async exec(cs, {ep} = {}) {
|
async exec(cs, {ep} = {}) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
|
|
||||||
|
if (this.notifyEvents) {
|
||||||
|
this.logger.debug(`turning event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||||
|
cs.notifyEvents = !!this.data.notifyEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onHoldMusic) {
|
||||||
|
cs.onHoldMusic = this.onHoldMusic;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.data.amd) {
|
if (this.data.amd) {
|
||||||
this.startAmd = cs.startAmd;
|
this.startAmd = cs.startAmd;
|
||||||
this.stopAmd = cs.stopAmd;
|
this.stopAmd = cs.stopAmd;
|
||||||
@@ -70,25 +129,64 @@ class TaskConfig extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.data.reset.forEach((k) => {
|
||||||
|
if (k === 'synthesizer') cs.resetSynthesizer();
|
||||||
|
else if (k === 'recognizer') cs.resetRecognizer();
|
||||||
|
});
|
||||||
|
|
||||||
if (this.hasSynthesizer) {
|
if (this.hasSynthesizer) {
|
||||||
|
cs.synthesizer = this.synthesizer;
|
||||||
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
|
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
|
||||||
? this.synthesizer.vendor
|
? this.synthesizer.vendor
|
||||||
: cs.speechSynthesisVendor;
|
: cs.speechSynthesisVendor;
|
||||||
|
cs.speechSynthesisLabel = this.synthesizer.label !== 'default'
|
||||||
|
? this.synthesizer.label
|
||||||
|
: cs.speechSynthesisLabel;
|
||||||
cs.speechSynthesisLanguage = this.synthesizer.language !== 'default'
|
cs.speechSynthesisLanguage = this.synthesizer.language !== 'default'
|
||||||
? this.synthesizer.language
|
? this.synthesizer.language
|
||||||
: cs.speechSynthesisLanguage;
|
: cs.speechSynthesisLanguage;
|
||||||
cs.speechSynthesisVoice = this.synthesizer.voice !== 'default'
|
cs.speechSynthesisVoice = this.synthesizer.voice !== 'default'
|
||||||
? this.synthesizer.voice
|
? this.synthesizer.voice
|
||||||
: cs.speechSynthesisVoice;
|
: cs.speechSynthesisVoice;
|
||||||
|
|
||||||
|
// fallback vendor
|
||||||
|
cs.fallbackSpeechSynthesisVendor = this.synthesizer.fallbackVendor !== 'default'
|
||||||
|
? this.synthesizer.fallbackVendor
|
||||||
|
: cs.fallbackSpeechSynthesisVendor;
|
||||||
|
cs.fallbackSpeechSynthesisLabel = this.synthesizer.fallbackLabel !== 'default'
|
||||||
|
? this.synthesizer.fallbackLabel
|
||||||
|
: cs.fallbackSpeechSynthesisLabel;
|
||||||
|
cs.fallbackSpeechSynthesisLanguage = this.synthesizer.fallbackLanguage !== 'default'
|
||||||
|
? this.synthesizer.fallbackLanguage
|
||||||
|
: cs.fallbackSpeechSynthesisLanguage;
|
||||||
|
cs.fallbackSpeechSynthesisVoice = this.synthesizer.fallbackVoice !== 'default'
|
||||||
|
? this.synthesizer.fallbackVoice
|
||||||
|
: cs.fallbackSpeechSynthesisVoice;
|
||||||
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
|
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
|
||||||
}
|
}
|
||||||
if (this.hasRecognizer) {
|
if (this.hasRecognizer) {
|
||||||
|
cs.recognizer = this.recognizer;
|
||||||
cs.speechRecognizerVendor = this.recognizer.vendor !== 'default'
|
cs.speechRecognizerVendor = this.recognizer.vendor !== 'default'
|
||||||
? this.recognizer.vendor
|
? this.recognizer.vendor
|
||||||
: cs.speechRecognizerVendor;
|
: cs.speechRecognizerVendor;
|
||||||
|
cs.speechRecognizerLabel = this.recognizer.label !== 'default'
|
||||||
|
? this.recognizer.label
|
||||||
|
: cs.speechRecognizerLabel;
|
||||||
cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
|
cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
|
||||||
? this.recognizer.language
|
? this.recognizer.language
|
||||||
: cs.speechRecognizerLanguage;
|
: cs.speechRecognizerLanguage;
|
||||||
|
|
||||||
|
//fallback
|
||||||
|
cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== 'default'
|
||||||
|
? this.recognizer.fallbackVendor
|
||||||
|
: cs.fallbackSpeechRecognizerVendor;
|
||||||
|
cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel !== 'default'
|
||||||
|
? this.recognizer.fallbackLabel
|
||||||
|
: cs.fallbackSpeechRecognizerLabel;
|
||||||
|
cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== 'default'
|
||||||
|
? this.recognizer.fallbackLanguage
|
||||||
|
: cs.fallbackSpeechRecognizerLanguage;
|
||||||
|
|
||||||
cs.isContinuousAsr = typeof this.recognizer.asrTimeout === 'number' ? true : false;
|
cs.isContinuousAsr = typeof this.recognizer.asrTimeout === 'number' ? true : false;
|
||||||
if (cs.isContinuousAsr) {
|
if (cs.isContinuousAsr) {
|
||||||
cs.asrTimeout = this.recognizer.asrTimeout;
|
cs.asrTimeout = this.recognizer.asrTimeout;
|
||||||
@@ -136,11 +234,65 @@ class TaskConfig extends Task {
|
|||||||
this.logger.info({err}, 'Config: error starting recording');
|
this.logger.info({err}, 'Config: error starting recording');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.hasListen) {
|
||||||
|
const {enable, ...opts} = this.listen;
|
||||||
|
if (enable) {
|
||||||
|
this.logger.debug({opts}, 'Config: enabling listen');
|
||||||
|
cs.startBackgroundTask('listen', {verb: 'listen', ...opts});
|
||||||
|
} else {
|
||||||
|
this.logger.info('Config: disabling listen');
|
||||||
|
cs.stopBackgroundTask('listen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.hasTranscribe) {
|
||||||
|
if (this.transcribe.enable) {
|
||||||
|
this.transcribeOpts.recognizer = this.hasRecognizer ?
|
||||||
|
this.recognizer :
|
||||||
|
{
|
||||||
|
vendor: cs.speechRecognizerVendor,
|
||||||
|
language: cs.speechRecognizerLanguage
|
||||||
|
};
|
||||||
|
this.logger.debug(this.transcribeOpts, 'Config: enabling transcribe');
|
||||||
|
cs.startBackgroundTask('transcribe', this.transcribeOpts);
|
||||||
|
} else {
|
||||||
|
this.logger.info('Config: disabling transcribe');
|
||||||
|
cs.stopBackgroundTask('transcribe');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.actionHookDelayAction) {
|
||||||
|
cs.actionHookDelayEnabled = this.actionHookDelayAction.enabled || false;
|
||||||
|
cs.actionHookNoResponseTimeout = this.actionHookDelayAction.noResponseTimeout || 0;
|
||||||
|
cs.actionHookNoResponseGiveUpTimeout = this.actionHookDelayAction.noResponseGiveUpTimeout || 0;
|
||||||
|
cs.actionHookDelayRetries = this.actionHookDelayAction.retries || 1;
|
||||||
|
cs.actionHookDelayActions = this.actionHookDelayAction.actions || [];
|
||||||
|
}
|
||||||
|
if (this.data.sipRequestWithinDialogHook) {
|
||||||
|
cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('boostAudioSignal' in this.data) {
|
||||||
|
const db = parseDecibels(this.data.boostAudioSignal);
|
||||||
|
this.logger.info(`Config: boosting audio signal by ${db} dB`);
|
||||||
|
const args = [ep.uuid, 'setGain', db];
|
||||||
|
ep.api('uuid_dub', args).catch((err) => {
|
||||||
|
this.logger.error(err, 'Error boosting audio signal');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasFillerNoise) {
|
||||||
|
const {enable, ...opts} = this.fillerNoise;
|
||||||
|
this.logger.info({fillerNoise: this.fillerNoise}, 'Config: fillerNoise');
|
||||||
|
if (!enable) cs.disableFillerNoise();
|
||||||
|
else {
|
||||||
|
cs.enableFillerNoise(opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill(cs) {
|
async kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
|
//if (this.ep && this.stopAmd) this.stopAmd(this.ep, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAmdEvent(cs, evt) {
|
_onAmdEvent(cs, evt) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class TaskDequeue extends Task {
|
|||||||
this.queueName = this.data.name;
|
this.queueName = this.data.name;
|
||||||
this.timeout = this.data.timeout || 0;
|
this.timeout = this.data.timeout || 0;
|
||||||
this.beep = this.data.beep === true;
|
this.beep = this.data.beep === true;
|
||||||
|
this.callSid = this.data.callSid;
|
||||||
|
|
||||||
this.emitter = new Emitter();
|
this.emitter = new Emitter();
|
||||||
this.state = DequeueResults.Timeout;
|
this.state = DequeueResults.Timeout;
|
||||||
@@ -53,7 +54,7 @@ class TaskDequeue extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getMemberFromQueue(cs) {
|
_getMemberFromQueue(cs) {
|
||||||
const {popFront} = cs.srf.locals.dbHelpers;
|
const {retrieveFromSortedSet, retrieveByPatternSortedSet} = cs.srf.locals.dbHelpers;
|
||||||
|
|
||||||
return new Promise(async(resolve) => {
|
return new Promise(async(resolve) => {
|
||||||
let timer;
|
let timer;
|
||||||
@@ -70,7 +71,13 @@ class TaskDequeue extends Task {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
const url = await popFront(this.queueName);
|
let url;
|
||||||
|
if (this.callSid) {
|
||||||
|
const r = await retrieveByPatternSortedSet(this.queueName, `*${this.callSid}`);
|
||||||
|
url = r[0];
|
||||||
|
} else {
|
||||||
|
url = await retrieveFromSortedSet(this.queueName);
|
||||||
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
found = true;
|
found = true;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
@@ -78,7 +85,7 @@ class TaskDequeue extends Task {
|
|||||||
resolve(url);
|
resolve(url);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.debug({err}, 'TaskDequeue:_getMemberFromQueue error popFront');
|
this.logger.debug({err}, 'TaskDequeue:_getMemberFromQueue error Sorted Set');
|
||||||
}
|
}
|
||||||
await sleepFor(5000);
|
await sleepFor(5000);
|
||||||
} while (!this.killed && !timedout && !found);
|
} while (!this.killed && !timedout && !found);
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ const assert = require('assert');
|
|||||||
const placeCall = require('../utils/place-outdial');
|
const placeCall = require('../utils/place-outdial');
|
||||||
const sessionTracker = require('../session/session-tracker');
|
const sessionTracker = require('../session/session-tracker');
|
||||||
const DtmfCollector = require('../utils/dtmf-collector');
|
const DtmfCollector = require('../utils/dtmf-collector');
|
||||||
|
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||||
const dbUtils = require('../utils/db-utils');
|
const dbUtils = require('../utils/db-utils');
|
||||||
|
const parseDecibels = require('../utils/parse-decibels');
|
||||||
const debug = require('debug')('jambonz:feature-server');
|
const debug = require('debug')('jambonz:feature-server');
|
||||||
const {parseUri} = require('drachtio-srf');
|
const {parseUri} = require('drachtio-srf');
|
||||||
|
const {ANCHOR_MEDIA_ALWAYS, JAMBONZ_DISABLE_DIAL_PAI_HEADER} = require('../config');
|
||||||
|
const { isOnhold, isOpusFirst } = require('../utils/sdp-utils');
|
||||||
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
|
||||||
function parseDtmfOptions(logger, dtmfCapture) {
|
function parseDtmfOptions(logger, dtmfCapture) {
|
||||||
let parentDtmfCollector, childDtmfCollector;
|
let parentDtmfCollector, childDtmfCollector;
|
||||||
@@ -84,6 +89,7 @@ class TaskDial extends Task {
|
|||||||
|
|
||||||
this.earlyMedia = this.data.answerOnBridge === true;
|
this.earlyMedia = this.data.answerOnBridge === true;
|
||||||
this.callerId = this.data.callerId;
|
this.callerId = this.data.callerId;
|
||||||
|
this.callerName = this.data.callerName;
|
||||||
this.dialMusic = this.data.dialMusic;
|
this.dialMusic = this.data.dialMusic;
|
||||||
this.headers = this.data.headers || {};
|
this.headers = this.data.headers || {};
|
||||||
this.method = this.data.method || 'POST';
|
this.method = this.data.method || 'POST';
|
||||||
@@ -95,6 +101,8 @@ class TaskDial extends Task {
|
|||||||
this.referHook = this.data.referHook;
|
this.referHook = this.data.referHook;
|
||||||
this.dtmfHook = this.data.dtmfHook;
|
this.dtmfHook = this.data.dtmfHook;
|
||||||
this.proxy = this.data.proxy;
|
this.proxy = this.data.proxy;
|
||||||
|
this.tag = this.data.tag;
|
||||||
|
this.boostAudioSignal = this.data.boostAudioSignal;
|
||||||
|
|
||||||
if (this.dtmfHook) {
|
if (this.dtmfHook) {
|
||||||
const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {});
|
const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {});
|
||||||
@@ -112,6 +120,9 @@ class TaskDial extends Task {
|
|||||||
if (this.data.transcribe) {
|
if (this.data.transcribe) {
|
||||||
this.transcribeTask = makeTask(logger, {'transcribe' : this.data.transcribe}, this);
|
this.transcribeTask = makeTask(logger, {'transcribe' : this.data.transcribe}, this);
|
||||||
}
|
}
|
||||||
|
if (this.data.dub && Array.isArray(this.data.dub) && this.data.dub.length > 0) {
|
||||||
|
this.dubTasks = this.data.dub.map((d) => makeTask(logger, {'dub': d}, this));
|
||||||
|
}
|
||||||
|
|
||||||
this.results = {};
|
this.results = {};
|
||||||
this.bridged = false;
|
this.bridged = false;
|
||||||
@@ -133,11 +144,21 @@ class TaskDial extends Task {
|
|||||||
|
|
||||||
get name() { return TaskName.Dial; }
|
get name() { return TaskName.Dial; }
|
||||||
|
|
||||||
|
get isOnHoldEnabled() {
|
||||||
|
return !!this.data.onHoldHook;
|
||||||
|
}
|
||||||
|
|
||||||
get canReleaseMedia() {
|
get canReleaseMedia() {
|
||||||
return !process.env.ANCHOR_MEDIA_ALWAYS &&
|
const keepAnchor = this.data.anchorMedia ||
|
||||||
!this.listenTask &&
|
this.cs.isBackGroundListen ||
|
||||||
!this.transcribeTask &&
|
this.cs.onHoldMusic ||
|
||||||
!this.startAmd;
|
ANCHOR_MEDIA_ALWAYS ||
|
||||||
|
this.listenTask ||
|
||||||
|
this.dubTasks ||
|
||||||
|
this.transcribeTask ||
|
||||||
|
this.startAmd;
|
||||||
|
|
||||||
|
return !keepAnchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
get summary() {
|
get summary() {
|
||||||
@@ -161,6 +182,16 @@ class TaskDial extends Task {
|
|||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
try {
|
try {
|
||||||
|
if (this.listenTask) {
|
||||||
|
const {span, ctx} = this.startChildSpan(`nested:${this.listenTask.summary}`);
|
||||||
|
this.listenTask.span = span;
|
||||||
|
this.listenTask.ctx = ctx;
|
||||||
|
}
|
||||||
|
if (this.transcribeTask) {
|
||||||
|
const {span, ctx} = this.startChildSpan(`nested:${this.transcribeTask.summary}`);
|
||||||
|
this.transcribeTask.span = span;
|
||||||
|
this.transcribeTask.ctx = ctx;
|
||||||
|
}
|
||||||
if (this.data.amd) {
|
if (this.data.amd) {
|
||||||
this.startAmd = cs.startAmd;
|
this.startAmd = cs.startAmd;
|
||||||
this.stopAmd = cs.stopAmd;
|
this.stopAmd = cs.stopAmd;
|
||||||
@@ -181,6 +212,7 @@ class TaskDial extends Task {
|
|||||||
await this.performAction(this.results, this.killReason !== KillReason.Replaced);
|
await this.performAction(this.results, this.killReason !== KillReason.Replaced);
|
||||||
this._removeDtmfDetection(cs.dlg);
|
this._removeDtmfDetection(cs.dlg);
|
||||||
this._removeDtmfDetection(this.dlg);
|
this._removeDtmfDetection(this.dlg);
|
||||||
|
this._removeSipIndialogRequestListener(this.dlg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, 'TaskDial:exec terminating with error');
|
this.logger.error({err}, 'TaskDial:exec terminating with error');
|
||||||
this.kill(cs);
|
this.kill(cs);
|
||||||
@@ -209,7 +241,7 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
this._removeDtmfDetection(cs.dlg);
|
this._removeDtmfDetection(cs.dlg);
|
||||||
this._removeDtmfDetection(this.dlg);
|
this._removeDtmfDetection(this.dlg);
|
||||||
this._killOutdials();
|
await this._killOutdials();
|
||||||
if (this.sd) {
|
if (this.sd) {
|
||||||
this.sd.kill();
|
this.sd.kill();
|
||||||
this.sd.removeAllListeners();
|
this.sd.removeAllListeners();
|
||||||
@@ -218,10 +250,12 @@ class TaskDial extends Task {
|
|||||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||||
if (this.listenTask) {
|
if (this.listenTask) {
|
||||||
await this.listenTask.kill(cs);
|
await this.listenTask.kill(cs);
|
||||||
|
this.listenTask.span.end();
|
||||||
this.listenTask = null;
|
this.listenTask = null;
|
||||||
}
|
}
|
||||||
if (this.transcribeTask) {
|
if (this.transcribeTask) {
|
||||||
await this.transcribeTask.kill(cs);
|
await this.transcribeTask.kill(cs);
|
||||||
|
this.transcribeTask.span.end();
|
||||||
this.transcribeTask = null;
|
this.transcribeTask = null;
|
||||||
}
|
}
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
@@ -296,18 +330,41 @@ class TaskDial extends Task {
|
|||||||
const to = parseUri(req.getParsedHeader('Refer-To').uri);
|
const to = parseUri(req.getParsedHeader('Refer-To').uri);
|
||||||
const by = parseUri(req.getParsedHeader('Referred-By').uri);
|
const by = parseUri(req.getParsedHeader('Referred-By').uri);
|
||||||
this.logger.info({to}, 'refer to parsed');
|
this.logger.info({to}, 'refer to parsed');
|
||||||
await cs.requestor.request('verb:hook', this.referHook, {
|
const json = await cs.requestor.request('verb:hook', this.referHook, {
|
||||||
...callInfo,
|
...(callInfo.toJSON()),
|
||||||
refer_details: {
|
refer_details: {
|
||||||
sip_refer_to: req.get('Refer-To'),
|
sip_refer_to: req.get('Refer-To'),
|
||||||
sip_referred_by: req.get('Referred-By'),
|
sip_referred_by: req.get('Referred-By'),
|
||||||
sip_user_agent: req.get('User-Agent'),
|
sip_user_agent: req.get('User-Agent'),
|
||||||
refer_to_user: to.user,
|
refer_to_user: to.scheme === 'tel' ? to.number : to.user,
|
||||||
referred_by_user: by.user,
|
referred_by_user: by.scheme === 'tel' ? by.number : by.user,
|
||||||
referring_call_sid,
|
referring_call_sid,
|
||||||
referred_call_sid
|
referred_call_sid
|
||||||
}
|
}
|
||||||
}, httpHeaders);
|
}, httpHeaders);
|
||||||
|
if (json && Array.isArray(json)) {
|
||||||
|
try {
|
||||||
|
const logger = isChild ? this.logger : this.sd.logger;
|
||||||
|
const tasks = normalizeJambones(logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||||
|
if (tasks && tasks.length > 0) {
|
||||||
|
const legs = isChild ? ['child', 'parent'] : ['parent', 'child'];
|
||||||
|
logger.info(`Dial:handleRefer received REFER on ${legs[0]} leg, setting new app on ${legs[1]} leg`);
|
||||||
|
if (isChild) this.redirect(cs, tasks);
|
||||||
|
else {
|
||||||
|
logger.info({tasks: json}, 'Dial:handleRefer - new application for for child leg');
|
||||||
|
const adultingSession = await this.sd.doAdulting({
|
||||||
|
logger,
|
||||||
|
application: cs.application,
|
||||||
|
tasks
|
||||||
|
});
|
||||||
|
/* need to update the callSid of the child with its own (new) AdultingCallSession */
|
||||||
|
sessionTracker.add(adultingSession.callSid, adultingSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info(err, 'Dial:handleRefer - error setting new application after receiving REFER');
|
||||||
|
}
|
||||||
|
}
|
||||||
res.send(202);
|
res.send(202);
|
||||||
this.logger.info('DialTask:handleRefer - sent 202 Accepted');
|
this.logger.info('DialTask:handleRefer - sent 202 Accepted');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -328,11 +385,16 @@ class TaskDial extends Task {
|
|||||||
sd.removeAllListeners('callCreateFail');
|
sd.removeAllListeners('callCreateFail');
|
||||||
}
|
}
|
||||||
|
|
||||||
_killOutdials() {
|
async _killOutdials() {
|
||||||
for (const [callSid, sd] of Array.from(this.dials)) {
|
for (const [callSid, sd] of Array.from(this.dials)) {
|
||||||
this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`);
|
this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`);
|
||||||
sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`));
|
try {
|
||||||
|
await sd.kill();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`);
|
||||||
|
}
|
||||||
this._removeHandlers(sd);
|
this._removeHandlers(sd);
|
||||||
|
this.logger.debug(`Dial:_killOutdials killed callSid ${callSid}`);
|
||||||
}
|
}
|
||||||
this.dials.clear();
|
this.dials.clear();
|
||||||
}
|
}
|
||||||
@@ -345,8 +407,14 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onInfo(cs, dlg, req, res) {
|
_onInfo(cs, dlg, req, res) {
|
||||||
|
// SIP Indialog will be handled by another handler
|
||||||
|
if (cs.sipRequestWithinDialogHook) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
res.send(200);
|
res.send(200);
|
||||||
if (req.get('Content-Type') !== 'application/dtmf-relay') return;
|
if (req.get('Content-Type') !== 'application/dtmf-relay') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dtmfDetector = dlg === cs.dlg ? this.parentDtmfCollector : this.childDtmfCollector;
|
const dtmfDetector = dlg === cs.dlg ? this.parentDtmfCollector : this.childDtmfCollector;
|
||||||
if (!dtmfDetector) return;
|
if (!dtmfDetector) return;
|
||||||
@@ -375,6 +443,20 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initSipIndialogRequestListener(cs, dlg) {
|
||||||
|
dlg.on('info', this._onRequestWithinDialog.bind(this, cs));
|
||||||
|
dlg.on('message', this._onRequestWithinDialog.bind(this, cs));
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeSipIndialogRequestListener(dlg) {
|
||||||
|
dlg && dlg.removeAllListeners('message');
|
||||||
|
dlg && dlg.removeAllListeners('info');
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onRequestWithinDialog(cs, req, res) {
|
||||||
|
cs._onRequestWithinDialog(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
async _initializeInbound(cs) {
|
async _initializeInbound(cs) {
|
||||||
const {ep} = await cs._evalEndpointPrecondition(this);
|
const {ep} = await cs._evalEndpointPrecondition(this);
|
||||||
this.epOther = ep;
|
this.epOther = ep;
|
||||||
@@ -394,20 +476,28 @@ class TaskDial extends Task {
|
|||||||
const {req, srf} = cs;
|
const {req, srf} = cs;
|
||||||
const {getSBC} = srf.locals;
|
const {getSBC} = srf.locals;
|
||||||
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
|
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
|
||||||
const {lookupCarrier} = dbUtils(this.logger, cs.srf);
|
const {lookupCarrier, lookupCarrierByPhoneNumber} = dbUtils(this.logger, cs.srf);
|
||||||
const sbcAddress = this.proxy || getSBC();
|
const sbcAddress = this.proxy || getSBC();
|
||||||
const teamsInfo = {};
|
const teamsInfo = {};
|
||||||
let fqdn;
|
let fqdn;
|
||||||
|
|
||||||
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
||||||
const opts = {
|
this.headers = {
|
||||||
headers: req && req.has('X-CID') ? Object.assign(this.headers, {'X-CID': req.get('X-CID')}) : this.headers,
|
'X-Account-Sid': cs.accountSid,
|
||||||
proxy: `sip:${sbcAddress}`,
|
...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}),
|
||||||
callingNumber: this.callerId || req.callingNumber
|
...(req && req.has('P-Asserted-Identity') && !JAMBONZ_DISABLE_DIAL_PAI_HEADER &&
|
||||||
|
{'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
|
||||||
|
...(req && req.has('X-Voip-Carrier-Sid') && {'X-Voip-Carrier-Sid': req.get('X-Voip-Carrier-Sid')}),
|
||||||
|
// Put headers at the end to make sure opt.headers override all default behavior.
|
||||||
|
...this.headers
|
||||||
};
|
};
|
||||||
opts.headers = {
|
|
||||||
...opts.headers,
|
const opts = {
|
||||||
'X-Account-Sid': cs.accountSid
|
headers: this.headers,
|
||||||
|
proxy: `sip:${sbcAddress}`,
|
||||||
|
callingNumber: this.callerId || req.callingNumber,
|
||||||
|
...(this.callerName && {callingName: this.callerName}),
|
||||||
|
opusFirst: isOpusFirst(this.cs.ep.remote.sdp)
|
||||||
};
|
};
|
||||||
|
|
||||||
const t = this.target.find((t) => t.type === 'teams');
|
const t = this.target.find((t) => t.type === 'teams');
|
||||||
@@ -418,10 +508,14 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ms = await cs.getMS();
|
const ms = await cs.getMS();
|
||||||
this.timerRing = setTimeout(() => {
|
this.timerRing = setTimeout(async() => {
|
||||||
this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`);
|
this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`);
|
||||||
this.timerRing = null;
|
this.timerRing = null;
|
||||||
this._killOutdials();
|
try {
|
||||||
|
await this._killOutdials();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info(err, 'Dial:_attemptCall - error killing outdials');
|
||||||
|
}
|
||||||
this.result = {
|
this.result = {
|
||||||
dialCallStatus: CallStatus.NoAnswer,
|
dialCallStatus: CallStatus.NoAnswer,
|
||||||
dialSipStatus: 487
|
dialSipStatus: 487
|
||||||
@@ -449,12 +543,27 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
if (t.type === 'phone' && t.trunk) {
|
if (t.type === 'phone' && t.trunk) {
|
||||||
const voip_carrier_sid = await lookupCarrier(cs.accountSid, t.trunk);
|
const voip_carrier_sid = await lookupCarrier(cs.accountSid, t.trunk);
|
||||||
this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk})`);
|
this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk}`);
|
||||||
if (voip_carrier_sid) {
|
if (voip_carrier_sid) {
|
||||||
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* trunk isn't specified,
|
||||||
|
* check if number matches any existing numbers
|
||||||
|
* */
|
||||||
|
if (t.type === 'phone' && !t.trunk) {
|
||||||
|
const str = this.callerId || req.callingNumber || '';
|
||||||
|
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||||
|
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
|
||||||
|
if (voip_carrier_sid) {
|
||||||
|
this.logger.info(
|
||||||
|
`Dial:_attemptCalls: selected voip_carrier_sid ${voip_carrier_sid} for callingNumber: ${callingNumber}`);
|
||||||
|
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.killed) return;
|
if (this.killed) return;
|
||||||
|
|
||||||
const sd = placeCall({
|
const sd = placeCall({
|
||||||
@@ -468,7 +577,9 @@ class TaskDial extends Task {
|
|||||||
callInfo: cs.callInfo,
|
callInfo: cs.callInfo,
|
||||||
accountInfo: cs.accountInfo,
|
accountInfo: cs.accountInfo,
|
||||||
rootSpan: cs.rootSpan,
|
rootSpan: cs.rootSpan,
|
||||||
startSpan: this.startSpan.bind(this)
|
startSpan: this.startSpan.bind(this),
|
||||||
|
dialTask: this,
|
||||||
|
onHoldMusic: this.cs.onHoldMusic
|
||||||
});
|
});
|
||||||
this.dials.set(sd.callSid, sd);
|
this.dials.set(sd.callSid, sd);
|
||||||
|
|
||||||
@@ -484,7 +595,8 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('callStatusChange', (obj) => {
|
.on('callStatusChange', (obj) => {
|
||||||
if (this.results.dialCallStatus !== CallStatus.Completed) {
|
if (this.results.dialCallStatus !== CallStatus.Completed &&
|
||||||
|
this.results.dialCallStatus !== CallStatus.NoAnswer) {
|
||||||
Object.assign(this.results, {
|
Object.assign(this.results, {
|
||||||
dialCallStatus: obj.callStatus,
|
dialCallStatus: obj.callStatus,
|
||||||
dialSipStatus: obj.sipStatus,
|
dialSipStatus: obj.sipStatus,
|
||||||
@@ -537,11 +649,7 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('reinvite', (req, res) => {
|
.on('reinvite', (req, res) => {
|
||||||
try {
|
this._onReinvite(req, res);
|
||||||
cs.handleReinviteAfterMediaReleased(req, res);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(err, 'Error in dial einvite from B leg');
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.on('refer', (callInfo, req, res) => {
|
.on('refer', (callInfo, req, res) => {
|
||||||
|
|
||||||
@@ -577,6 +685,56 @@ class TaskDial extends Task {
|
|||||||
this._killOutdials(); // NB: order is important
|
this._killOutdials(); // NB: order is important
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _onReinvite(req, res) {
|
||||||
|
try {
|
||||||
|
let isHandled = false;
|
||||||
|
if (this.isOnHoldEnabled) {
|
||||||
|
if (isOnhold(req.body)) {
|
||||||
|
this.logger.debug('Dial: _onReinvite receive hold Request');
|
||||||
|
if (!this.epOther && !this.ep) {
|
||||||
|
this.logger.debug(`Dial: _onReinvite receive hold Request,
|
||||||
|
media already released, reconnect media server`);
|
||||||
|
// update caller leg for new SDP from callee.
|
||||||
|
await this.cs.handleReinviteAfterMediaReleased(req, res);
|
||||||
|
// Freeswitch media is released, reconnect
|
||||||
|
await this.reAnchorMedia(this.cs, this.sd);
|
||||||
|
this.isOutgoingLegHold = true;
|
||||||
|
} else {
|
||||||
|
this.logger.debug('Dial: _onReinvite receive hold Request, update SDP');
|
||||||
|
const newSdp = await this.ep.modify(req.body);
|
||||||
|
res.send(200, {body: newSdp});
|
||||||
|
}
|
||||||
|
isHandled = true;
|
||||||
|
// Media already connected, ask for onHoldHook
|
||||||
|
this._onHoldHook(req);
|
||||||
|
} else if (!isOnhold(req.body)) {
|
||||||
|
this.logger.debug('Dial: _onReinvite receive unhold Request');
|
||||||
|
if (this.epOther && this.ep && this.isOutgoingLegHold && this.canReleaseMedia) {
|
||||||
|
this.logger.debug('Dial: _onReinvite receive unhold Request, release media');
|
||||||
|
// Offhold, time to release media
|
||||||
|
const newSdp = await this.ep.modify(req.body);
|
||||||
|
await res.send(200, {body: newSdp});
|
||||||
|
await this._releaseMedia(this.cs, this.sd);
|
||||||
|
this.isOutgoingLegHold = false;
|
||||||
|
} else {
|
||||||
|
this.logger.debug('Dial: _onReinvite receive unhold Request, update media server');
|
||||||
|
const newSdp = await this.ep.modify(req.body);
|
||||||
|
res.send(200, {body: newSdp});
|
||||||
|
}
|
||||||
|
if (this._onHoldSession) {
|
||||||
|
this._onHoldSession.kill();
|
||||||
|
}
|
||||||
|
isHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isHandled) {
|
||||||
|
this.cs.handleReinviteAfterMediaReleased(req, res);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err, 'Error in dial einvite from B leg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onMaxCallDuration(cs) {
|
_onMaxCallDuration(cs) {
|
||||||
this.logger.info(`Dial:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`);
|
this.logger.info(`Dial:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`);
|
||||||
this.ep && this.ep.unbridge();
|
this.ep && this.ep.unbridge();
|
||||||
@@ -627,10 +785,22 @@ class TaskDial extends Task {
|
|||||||
dialCallSid: sd.callSid,
|
dialCallSid: sd.callSid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.dubTasks) {
|
||||||
|
for (const dub of this.dubTasks) {
|
||||||
|
try {
|
||||||
|
await dub.exec(cs, {ep: sd.ep});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.logger.error({err}, 'Dial:_selectSingleDial - error executing dubTask');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
||||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
||||||
|
if (cs.sipRequestWithinDialogHook) this._initSipIndialogRequestListener(cs, this.dlg);
|
||||||
|
|
||||||
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep2: this.epOther, ep:this.ep});
|
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep: this.epOther, ep2:this.ep});
|
||||||
if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther});
|
if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther});
|
||||||
if (this.startAmd) {
|
if (this.startAmd) {
|
||||||
try {
|
try {
|
||||||
@@ -640,6 +810,18 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* boost audio signal if requested */
|
||||||
|
if (this.boostAudioSignal) {
|
||||||
|
try {
|
||||||
|
const db = parseDecibels(this.boostAudioSignal);
|
||||||
|
this.logger.info(`Dial: boosting audio signal by ${db} dB`);
|
||||||
|
const args = [this.ep.uuid, 'setGain', db];
|
||||||
|
await this.ep.api('uuid_dub', args);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err}, 'Dial:_selectSingleDial - Error boosting audio signal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* if we can release the media back to the SBC, do so now */
|
/* if we can release the media back to the SBC, do so now */
|
||||||
if (this.canReleaseMedia) setTimeout(this._releaseMedia.bind(this, cs, sd), 200);
|
if (this.canReleaseMedia) setTimeout(this._releaseMedia.bind(this, cs, sd), 200);
|
||||||
}
|
}
|
||||||
@@ -662,9 +844,11 @@ class TaskDial extends Task {
|
|||||||
assert(cs.ep && sd.ep);
|
assert(cs.ep && sd.ep);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Wait until we got new SDP from B leg to ofter to A Leg
|
||||||
const aLegSdp = cs.ep.remote.sdp;
|
const aLegSdp = cs.ep.remote.sdp;
|
||||||
|
await sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp);
|
||||||
const bLegSdp = sd.dlg.remote.sdp;
|
const bLegSdp = sd.dlg.remote.sdp;
|
||||||
await Promise.all[sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp), cs.releaseMediaToSBC(bLegSdp)];
|
await cs.releaseMediaToSBC(bLegSdp);
|
||||||
this.epOther = null;
|
this.epOther = null;
|
||||||
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
|
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -680,10 +864,41 @@ class TaskDial extends Task {
|
|||||||
this.epOther = cs.ep;
|
this.epOther = cs.ep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle RE-INVITE hold from caller leg.
|
||||||
async handleReinviteAfterMediaReleased(req, res) {
|
async handleReinviteAfterMediaReleased(req, res) {
|
||||||
const sdp = await this.dlg.modify(req.body);
|
let isHandled = false;
|
||||||
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
|
if (this.isOnHoldEnabled) {
|
||||||
res.send(200, {body: sdp});
|
if (isOnhold(req.body)) {
|
||||||
|
if (!this.epOther && !this.ep) {
|
||||||
|
// update callee leg for new SDP from caller.
|
||||||
|
const sdp = await this.dlg.modify(req.body);
|
||||||
|
res.send(200, {body: sdp});
|
||||||
|
// Onhold but media is already released, reconnect
|
||||||
|
await this.reAnchorMedia(this.cs, this.sd);
|
||||||
|
isHandled = true;
|
||||||
|
this.isIncomingLegHold = true;
|
||||||
|
}
|
||||||
|
this._onHoldHook(req);
|
||||||
|
} else if (!isOnhold(req.body)) {
|
||||||
|
if (this.epOther && this.ep && this.isIncomingLegHold && this.canReleaseMedia) {
|
||||||
|
// Offhold, time to release media
|
||||||
|
const newSdp = await this.epOther.modify(req.body);
|
||||||
|
await res.send(200, {body: newSdp});
|
||||||
|
await this._releaseMedia(this.cs, this.sd);
|
||||||
|
isHandled = true;
|
||||||
|
}
|
||||||
|
this.isIncomingLegHold = false;
|
||||||
|
if (this._onHoldSession) {
|
||||||
|
this._onHoldSession.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHandled) {
|
||||||
|
const sdp = await this.dlg.modify(req.body);
|
||||||
|
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
|
||||||
|
res.send(200, {body: sdp});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAmdEvent(cs, evt) {
|
_onAmdEvent(cs, evt) {
|
||||||
@@ -694,6 +909,54 @@ class TaskDial extends Task {
|
|||||||
this.logger.error({err}, 'Dial:_onAmdEvent - error calling actionHook');
|
this.logger.error({err}, 'Dial:_onAmdEvent - error calling actionHook');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _onHoldHook(req, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) {
|
||||||
|
if (this.data.onHoldHook) {
|
||||||
|
// send silence for keep Voice quality
|
||||||
|
await this.epOther.play('silence_stream://500');
|
||||||
|
let allowedTasks;
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
const b3 = this.getTracingPropagation();
|
||||||
|
const httpHeaders = b3 && {b3};
|
||||||
|
const json = await this.cs.application.requestor.
|
||||||
|
request('verb:hook', this.data.onHoldHook, {
|
||||||
|
...this.cs.callInfo.toJSON(),
|
||||||
|
hold_detail: {
|
||||||
|
from: req.get('From'),
|
||||||
|
to: req.get('To')
|
||||||
|
}
|
||||||
|
}, httpHeaders);
|
||||||
|
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||||
|
allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||||
|
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(`DialTask:_onHoldHook: executing ${tasks.length} tasks`);
|
||||||
|
if (tasks.length) {
|
||||||
|
this._onHoldSession = new ConfirmCallSession({
|
||||||
|
logger: this.logger,
|
||||||
|
application: this.cs.application,
|
||||||
|
dlg: this.isIncomingLegHold ? this.dlg : this.cs.dlg,
|
||||||
|
ep: this.isIncomingLegHold ? this.ep : this.cs.ep,
|
||||||
|
callInfo: this.cs.callInfo,
|
||||||
|
accountInfo: this.cs.accountInfo,
|
||||||
|
tasks,
|
||||||
|
rootSpan: this.cs.rootSpan
|
||||||
|
});
|
||||||
|
await this._onHoldSession.exec();
|
||||||
|
this._onHoldSession = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.info(error, 'DialTask:_onHoldHook: failed retrieving waitHook');
|
||||||
|
this._onHoldSession = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (allowedTasks && allowedTasks.length > 0 && !this.killed && this.isOnHold);
|
||||||
|
this.logger.info('Finish onHoldHook');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TaskDial;
|
module.exports = TaskDial;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const {TaskName, TaskPreconditions} = require('../../utils/constants');
|
|||||||
const Intent = require('./intent');
|
const Intent = require('./intent');
|
||||||
const DigitBuffer = require('./digit-buffer');
|
const DigitBuffer = require('./digit-buffer');
|
||||||
const Transcription = require('./transcription');
|
const Transcription = require('./transcription');
|
||||||
const normalizeJambones = require('../../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
|
||||||
class Dialogflow extends Task {
|
class Dialogflow extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
@@ -58,6 +58,13 @@ class Dialogflow extends Task {
|
|||||||
this.vendor = this.data.tts.vendor || 'default';
|
this.vendor = this.data.tts.vendor || 'default';
|
||||||
this.language = this.data.tts.language || 'default';
|
this.language = this.data.tts.language || 'default';
|
||||||
this.voice = this.data.tts.voice || 'default';
|
this.voice = this.data.tts.voice || 'default';
|
||||||
|
this.speechSynthesisLabel = this.data.tts.label;
|
||||||
|
|
||||||
|
// fallback tts
|
||||||
|
this.fallbackVendor = this.data.tts.fallbackVendor || 'default';
|
||||||
|
this.fallbackLanguage = this.data.tts.fallbackLanguage || 'default';
|
||||||
|
this.fallbackVoice = this.data.tts.fallbackLanguage || 'default';
|
||||||
|
this.fallbackLabel = this.data.tts.fallbackLabel;
|
||||||
}
|
}
|
||||||
this.bargein = this.data.bargein;
|
this.bargein = this.data.bargein;
|
||||||
}
|
}
|
||||||
@@ -118,8 +125,15 @@ class Dialogflow extends Task {
|
|||||||
this.vendor = cs.speechSynthesisVendor;
|
this.vendor = cs.speechSynthesisVendor;
|
||||||
this.language = cs.speechSynthesisLanguage;
|
this.language = cs.speechSynthesisLanguage;
|
||||||
this.voice = cs.speechSynthesisVoice;
|
this.voice = cs.speechSynthesisVoice;
|
||||||
|
this.speechSynthesisLabel = cs.speechSynthesisLabel;
|
||||||
}
|
}
|
||||||
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
|
if (this.fallbackVendor === 'default') {
|
||||||
|
this.fallbackVendor = cs.fallbackSpeechSynthesisVendor;
|
||||||
|
this.fallbackLanguage = cs.fallbackSpeechSynthesisLanguage;
|
||||||
|
this.fallbackVoice = cs.fallbackSpeechSynthesisVoice;
|
||||||
|
this.fallbackLabel = cs.fallbackSpeechSynthesisLabel;
|
||||||
|
}
|
||||||
|
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts', this.speechSynthesisLabel);
|
||||||
|
|
||||||
this.ep.addCustomEventListener('dialogflow::intent', this._onIntent.bind(this, ep, cs));
|
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::transcription', this._onTranscription.bind(this, ep, cs));
|
||||||
@@ -221,18 +235,8 @@ class Dialogflow extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const obj = {
|
const {filePath} = await this._fallbackSynthAudio(cs, intent, stats, synthAudio);
|
||||||
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(stats, obj);
|
|
||||||
if (filePath) cs.trackTmpFile(filePath);
|
if (filePath) cs.trackTmpFile(filePath);
|
||||||
if (!this.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);
|
|
||||||
|
|
||||||
if (this.playInProgress) {
|
if (this.playInProgress) {
|
||||||
await ep.api('uuid_break', ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
await ep.api('uuid_break', ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||||
@@ -276,6 +280,46 @@ class Dialogflow extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _fallbackSynthAudio(cs, intent, stats, synthAudio) {
|
||||||
|
try {
|
||||||
|
const obj = {
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
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');
|
||||||
|
|
||||||
|
return await synthAudio(stats, obj);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.info({error}, 'Failed to synthesize audio from primary vendor');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.fallbackVendor) {
|
||||||
|
const credentials = cs.getSpeechCredentials(this.fallbackVendor, 'tts', this.fallbackLabel);
|
||||||
|
const obj = {
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
text: intent.fulfillmentText,
|
||||||
|
vendor: this.fallbackVendor,
|
||||||
|
language: this.fallbackLanguage,
|
||||||
|
voice: this.fallbackVoice,
|
||||||
|
salt: cs.callSid,
|
||||||
|
credentials
|
||||||
|
};
|
||||||
|
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via fallback tts');
|
||||||
|
return await synthAudio(stats, obj);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err}, 'Failed to synthesize audio from falllback vendor');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A transcription - either interim or final - has been returned.
|
* 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 doing barge-in based on hotword detection, check for the hotword or phrase.
|
||||||
|
|||||||
144
lib/tasks/dub.js
Normal file
144
lib/tasks/dub.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
const {TaskName} = require('../utils/constants');
|
||||||
|
const TtsTask = require('./tts-task');
|
||||||
|
const assert = require('assert');
|
||||||
|
const parseDecibels = require('../utils/parse-decibels');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dub task: add or remove additional audio tracks into the call
|
||||||
|
*/
|
||||||
|
class TaskDub extends TtsTask {
|
||||||
|
constructor(logger, opts, parentTask) {
|
||||||
|
super(logger, opts, parentTask);
|
||||||
|
|
||||||
|
this.logger.debug({opts: this.data}, 'TaskDub constructor');
|
||||||
|
['action', 'track', 'play', 'say', 'loop'].forEach((prop) => {
|
||||||
|
this[prop] = this.data[prop];
|
||||||
|
});
|
||||||
|
this.gain = parseDecibels(this.data.gain);
|
||||||
|
|
||||||
|
assert.ok(this.action, 'TaskDub: action is required');
|
||||||
|
assert.ok(this.track, 'TaskDub: track is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() { return TaskName.Dub; }
|
||||||
|
|
||||||
|
async exec(cs, {ep}) {
|
||||||
|
super.exec(cs);
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (this.action) {
|
||||||
|
case 'addTrack':
|
||||||
|
await this._addTrack(cs, ep);
|
||||||
|
break;
|
||||||
|
case 'removeTrack':
|
||||||
|
await this._removeTrack(cs, ep);
|
||||||
|
break;
|
||||||
|
case 'silenceTrack':
|
||||||
|
await this._silenceTrack(cs, ep);
|
||||||
|
break;
|
||||||
|
case 'playOnTrack':
|
||||||
|
await this._playOnTrack(cs, ep);
|
||||||
|
break;
|
||||||
|
case 'sayOnTrack':
|
||||||
|
await this._sayOnTrack(cs, ep);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`TaskDub: unsupported action ${this.action}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(err, 'Error executing dub task');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _addTrack(cs, ep) {
|
||||||
|
this.logger.info(`adding track: ${this.track}`);
|
||||||
|
await ep.dub({
|
||||||
|
action: 'addTrack',
|
||||||
|
track: this.track
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.play) await this._playOnTrack(cs, ep);
|
||||||
|
else if (this.say) await this._sayOnTrack(cs, ep);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _removeTrack(_cs, ep) {
|
||||||
|
this.logger.info(`removing track: ${this.track}`);
|
||||||
|
await ep.dub({
|
||||||
|
action: 'removeTrack',
|
||||||
|
track: this.track
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _silenceTrack(_cs, ep) {
|
||||||
|
this.logger.info(`silencing track: ${this.track}`);
|
||||||
|
await ep.dub({
|
||||||
|
action: 'silenceTrack',
|
||||||
|
track: this.track
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _playOnTrack(_cs, ep) {
|
||||||
|
this.logger.info(`playing on track: ${this.track}`);
|
||||||
|
await ep.dub({
|
||||||
|
action: 'playOnTrack',
|
||||||
|
track: this.track,
|
||||||
|
play: this.play,
|
||||||
|
loop: this.loop ? 'loop' : 'once',
|
||||||
|
gain: this.gain
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _sayOnTrack(cs, ep) {
|
||||||
|
const text = this.say.text || this.say;
|
||||||
|
this.synthesizer = this.say.synthesizer || {};
|
||||||
|
|
||||||
|
if (Object.keys(this.synthesizer).length) {
|
||||||
|
this.logger.info({synthesizer: this.synthesizer},
|
||||||
|
`saying on track ${this.track}: ${text} with synthesizer options`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.logger.info(`saying on track ${this.track}: ${text}`);
|
||||||
|
}
|
||||||
|
this.synthesizer = this.synthesizer || {};
|
||||||
|
|
||||||
|
this.text = [text];
|
||||||
|
|
||||||
|
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
|
||||||
|
this.synthesizer.vendor :
|
||||||
|
cs.speechSynthesisVendor;
|
||||||
|
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
|
||||||
|
this.synthesizer.language :
|
||||||
|
cs.speechSynthesisLanguage ;
|
||||||
|
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
||||||
|
this.synthesizer.voice :
|
||||||
|
cs.speechSynthesisVoice;
|
||||||
|
const label = this.synthesizer.label && this.synthesizer.label !== 'default' ?
|
||||||
|
this.synthesizer.label :
|
||||||
|
cs.speechSynthesisLabel;
|
||||||
|
|
||||||
|
const disableTtsStreaming = false;
|
||||||
|
const filepath = await this._synthesizeWithSpecificVendor(cs, ep, {
|
||||||
|
vendor, language, voice, label, disableTtsStreaming
|
||||||
|
});
|
||||||
|
assert.ok(filepath.length === 1, 'TaskDub: no filepath returned from synthesizer');
|
||||||
|
|
||||||
|
const path = filepath[0];
|
||||||
|
if (!path.startsWith('say:{')) {
|
||||||
|
/* we have a local file of mp3 or r8 of synthesized speech audio to play */
|
||||||
|
this.logger.info(`playing synthesized speech from file on track ${this.track}: ${path}`);
|
||||||
|
this.play = path;
|
||||||
|
await this._playOnTrack(cs, ep);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.logger.info(`doing actual text to speech file on track ${this.track}: ${path}`);
|
||||||
|
await ep.dub({
|
||||||
|
action: 'sayOnTrack',
|
||||||
|
track: this.track,
|
||||||
|
say: path,
|
||||||
|
gain: this.gain
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TaskDub;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||||
const normalizeJambones = require('../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
const makeTask = require('./make_task');
|
const makeTask = require('./make_task');
|
||||||
const {TaskName, TaskPreconditions, QueueResults, KillReason} = require('../utils/constants');
|
const {TaskName, TaskPreconditions, QueueResults, KillReason} = require('../utils/constants');
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
@@ -18,6 +18,7 @@ class TaskEnqueue extends Task {
|
|||||||
this.preconditions = TaskPreconditions.Endpoint;
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
|
||||||
this.queueName = this.data.name;
|
this.queueName = this.data.name;
|
||||||
|
this.priority = this.data.priority;
|
||||||
this.waitHook = this.data.waitHook;
|
this.waitHook = this.data.waitHook;
|
||||||
|
|
||||||
this.emitter = new Emitter();
|
this.emitter = new Emitter();
|
||||||
@@ -70,12 +71,22 @@ class TaskEnqueue extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _addToQueue(cs, dlg) {
|
async _addToQueue(cs, dlg) {
|
||||||
const {pushBack} = cs.srf.locals.dbHelpers;
|
const {addToSortedSet, sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||||
const url = getUrl(cs);
|
const url = getUrl(cs);
|
||||||
this.waitStartTime = Date.now();
|
this.waitStartTime = Date.now();
|
||||||
this.logger.debug({queue: this.queueName, url}, 'pushing url onto queue');
|
this.logger.debug({queue: this.queueName, url}, 'pushing url onto queue');
|
||||||
const members = await pushBack(this.queueName, url);
|
if (this.priority < 0) {
|
||||||
this.logger.info(`TaskEnqueue:_addToQueue: added to queue, length now ${members}`);
|
this.logger.warn(`priority ${this.priority} is invalid, need to be non-negative integer,
|
||||||
|
999 will be used for priority`);
|
||||||
|
}
|
||||||
|
let members = await addToSortedSet(this.queueName, url, this.priority);
|
||||||
|
if (members === 1) {
|
||||||
|
this.logger.info('TaskEnqueue:_addToQueue: added to queue');
|
||||||
|
} else {
|
||||||
|
this.logger.info('TaskEnqueue:_addToQueue: failed to add to queue');
|
||||||
|
}
|
||||||
|
members = await sortedSetLength(this.queueName);
|
||||||
|
|
||||||
this.notifyUrl = url;
|
this.notifyUrl = url;
|
||||||
|
|
||||||
/* invoke account-level webhook for queue event notifications */
|
/* invoke account-level webhook for queue event notifications */
|
||||||
@@ -90,9 +101,9 @@ class TaskEnqueue extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _removeFromQueue(cs) {
|
async _removeFromQueue(cs) {
|
||||||
const {removeFromList, lengthOfList} = cs.srf.locals.dbHelpers;
|
const {retrieveByPatternSortedSet, sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||||
await removeFromList(this.queueName, getUrl(cs));
|
await retrieveByPatternSortedSet(this.queueName, `*${getUrl(cs)}`);
|
||||||
return await lengthOfList(this.queueName);
|
return await sortedSetLength(this.queueName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async performAction() {
|
async performAction() {
|
||||||
@@ -279,13 +290,13 @@ class TaskEnqueue extends Task {
|
|||||||
this.emitter.emit('dequeue', opts);
|
this.emitter.emit('dequeue', opts);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {lengthOfList} = cs.srf.locals.dbHelpers;
|
const {sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||||
const members = await lengthOfList(this.queueName);
|
const members = await sortedSetLength(this.queueName);
|
||||||
this.dequeued = true;
|
this.dequeued = true;
|
||||||
cs.performQueueWebhook({
|
cs.performQueueWebhook({
|
||||||
event: 'leave',
|
event: 'leave',
|
||||||
queue: this.data.name,
|
queue: this.data.name,
|
||||||
length: Math.max(members - 1, 0),
|
length: Math.max(members, 0),
|
||||||
leaveReason: 'dequeued',
|
leaveReason: 'dequeued',
|
||||||
leaveTime: Date.now(),
|
leaveTime: Date.now(),
|
||||||
dequeuer: opts.dequeuer
|
dequeuer: opts.dequeuer
|
||||||
@@ -300,8 +311,9 @@ class TaskEnqueue extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave]) {
|
async _playHook(cs, dlg, hook,
|
||||||
const {lengthOfList, getListPosition} = cs.srf.locals.dbHelpers;
|
allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave, TaskName.Tag]) {
|
||||||
|
const {sortedSetLength, sortedSetPositionByPattern} = cs.srf.locals.dbHelpers;
|
||||||
const b3 = this.getTracingPropagation();
|
const b3 = this.getTracingPropagation();
|
||||||
const httpHeaders = b3 && {b3};
|
const httpHeaders = b3 && {b3};
|
||||||
|
|
||||||
@@ -313,13 +325,20 @@ class TaskEnqueue extends Task {
|
|||||||
queueTime: getElapsedTime(this.waitStartTime)
|
queueTime: getElapsedTime(this.waitStartTime)
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const queueSize = await lengthOfList(this.queueName);
|
const queueSize = await sortedSetLength(this.queueName);
|
||||||
const queuePosition = await getListPosition(this.queueName, this.notifyUrl);
|
const queuePosition = await sortedSetPositionByPattern(this.queueName, `*${this.notifyUrl}`);
|
||||||
Object.assign(params, {queueSize, queuePosition});
|
Object.assign(params, {
|
||||||
|
queueSize,
|
||||||
|
queuePosition: queuePosition.length ? queuePosition[0] : 0,
|
||||||
|
callSid: this.cs.callSid,
|
||||||
|
callId: this.cs.callId,
|
||||||
|
customerData: this.cs.callInfo.customerData
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
|
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
|
||||||
}
|
}
|
||||||
const json = await cs.application.requestor.request('verb:hook', hook, params, httpHeaders);
|
const json = await cs.application.requestor.request('verb:hook', hook, params, httpHeaders);
|
||||||
|
this.logger.debug({json}, 'TaskEnqueue:_playHook: received response from waitHook');
|
||||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||||
|
|
||||||
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
const allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ class TaskHangup extends Task {
|
|||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
try {
|
try {
|
||||||
await dlg.destroy({headers: this.headers});
|
await dlg.destroy({headers: this.headers});
|
||||||
|
cs._callReleased();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err, 'TaskHangup:exec - Error hanging up call');
|
this.logger.error(err, 'TaskHangup:exec - Error hanging up call');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
const normalizeJambones = require('../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
|
||||||
class Lex extends Task {
|
class Lex extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
@@ -25,6 +25,13 @@ class Lex extends Task {
|
|||||||
this.vendor = this.data.tts.vendor || 'default';
|
this.vendor = this.data.tts.vendor || 'default';
|
||||||
this.language = this.data.tts.language || 'default';
|
this.language = this.data.tts.language || 'default';
|
||||||
this.voice = this.data.tts.voice || 'default';
|
this.voice = this.data.tts.voice || 'default';
|
||||||
|
this.speechCredentialLabel = this.data.tts.label || 'default';
|
||||||
|
|
||||||
|
// fallback tts
|
||||||
|
this.fallbackVendor = this.data.tts.fallbackVendor || 'default';
|
||||||
|
this.fallbackLanguage = this.data.tts.fallbackLanguage || 'default';
|
||||||
|
this.fallbackVoice = this.data.tts.fallbackLanguage || 'default';
|
||||||
|
this.fallbackLabel = this.data.tts.fallbackLabel || 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.botName = `${this.bot}:${this.alias}:${this.region}`;
|
this.botName = `${this.bot}:${this.alias}:${this.region}`;
|
||||||
@@ -102,8 +109,16 @@ class Lex extends Task {
|
|||||||
this.vendor = cs.speechSynthesisVendor;
|
this.vendor = cs.speechSynthesisVendor;
|
||||||
this.language = cs.speechSynthesisLanguage;
|
this.language = cs.speechSynthesisLanguage;
|
||||||
this.voice = cs.speechSynthesisVoice;
|
this.voice = cs.speechSynthesisVoice;
|
||||||
|
this.speechCredentialLabel = cs.speechSynthesisLabel;
|
||||||
}
|
}
|
||||||
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
|
if (this.fallbackVendor === 'default') {
|
||||||
|
this.fallbackVendor = cs.fallbackSpeechSynthesisVendor;
|
||||||
|
this.fallbackLanguage = cs.fallbackSpeechSynthesisLanguage;
|
||||||
|
this.fallbackVoice = cs.fallbackSpeechSynthesisVoice;
|
||||||
|
this.fallbackLabel = cs.fallbackSpeechSynthesisLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts', this.speechCredentialLabel);
|
||||||
|
|
||||||
this.ep.addCustomEventListener('lex::intent', this._onIntent.bind(this, ep, cs));
|
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::transcription', this._onTranscription.bind(this, ep, cs));
|
||||||
@@ -168,6 +183,41 @@ class Lex extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _fallbackSynthAudio(cs, msg, stats, synthAudio) {
|
||||||
|
try {
|
||||||
|
const {filePath} = await synthAudio(stats, {
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
text: msg,
|
||||||
|
vendor: this.vendor,
|
||||||
|
language: this.language,
|
||||||
|
voice: this.voice,
|
||||||
|
salt: cs.callSid,
|
||||||
|
credentials: this.ttsCredentials
|
||||||
|
});
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.info({error}, 'failed to synth audio from primary vendor');
|
||||||
|
if (this.fallbackVendor) {
|
||||||
|
try {
|
||||||
|
const credential = cs.getSpeechCredentials(this.fallbackVendor, 'tts', this.fallbackLabel);
|
||||||
|
const {filePath} = await synthAudio(stats, {
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
text: msg,
|
||||||
|
vendor: this.fallbackVendor,
|
||||||
|
language: this.fallbackLanguage,
|
||||||
|
voice: this.fallbackVoice,
|
||||||
|
salt: cs.callSid,
|
||||||
|
credentials: credential
|
||||||
|
});
|
||||||
|
return filePath;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err}, 'failed to synth audio from fallback vendor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {*} evt - event data
|
* @param {*} evt - event data
|
||||||
*/
|
*/
|
||||||
@@ -187,15 +237,7 @@ class Lex extends Task {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
|
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
|
||||||
// eslint-disable-next-line no-unused-vars
|
const filePath = await this._fallbackSynthAudio(cs, msg, stats, synthAudio);
|
||||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
|
||||||
text: msg,
|
|
||||||
vendor: this.vendor,
|
|
||||||
language: this.language,
|
|
||||||
voice: this.voice,
|
|
||||||
salt: cs.callSid,
|
|
||||||
credentials: this.ttsCredentials
|
|
||||||
});
|
|
||||||
if (filePath) cs.trackTmpFile(filePath);
|
if (filePath) cs.trackTmpFile(filePath);
|
||||||
|
|
||||||
if (this.events.includes('start-play')) {
|
if (this.events.includes('start-play')) {
|
||||||
|
|||||||
@@ -2,15 +2,22 @@ const Task = require('./task');
|
|||||||
const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants');
|
const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants');
|
||||||
const makeTask = require('./make_task');
|
const makeTask = require('./make_task');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
|
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
|
||||||
|
const DTMF_SPAN_NAME = 'dtmf';
|
||||||
|
|
||||||
class TaskListen extends Task {
|
class TaskListen extends Task {
|
||||||
constructor(logger, opts, parentTask) {
|
constructor(logger, opts, parentTask) {
|
||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* use bidirectionalAudio.enabled
|
||||||
|
*/
|
||||||
|
this.disableBidirectionalAudio = opts.disableBidirectionalAudio;
|
||||||
this.preconditions = TaskPreconditions.Endpoint;
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
|
||||||
[
|
[
|
||||||
'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep',
|
'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep',
|
||||||
'sampleRate', 'timeout', 'transcribe', 'wsAuth'
|
'sampleRate', 'timeout', 'transcribe', 'wsAuth', 'disableBidirectionalAudio'
|
||||||
].forEach((k) => this[k] = this.data[k]);
|
].forEach((k) => this[k] = this.data[k]);
|
||||||
|
|
||||||
this.mixType = this.mixType || 'mono';
|
this.mixType = this.mixType || 'mono';
|
||||||
@@ -20,12 +27,27 @@ class TaskListen extends Task {
|
|||||||
this.nested = parentTask instanceof Task;
|
this.nested = parentTask instanceof Task;
|
||||||
|
|
||||||
this.results = {};
|
this.results = {};
|
||||||
|
this.playAudioQueue = [];
|
||||||
|
this.isPlayingAudioFromQueue = false;
|
||||||
|
this.bidirectionalAudio = {
|
||||||
|
enabled: this.disableBidirectionalAudio === true ? false : true,
|
||||||
|
...(this.data['bidirectionalAudio']),
|
||||||
|
};
|
||||||
|
|
||||||
|
// From drachtio-version 3.0.40, forkAudioStart will send empty bugname, metadata together with
|
||||||
|
// bidirectionalAudio params that cause old version of freeswitch missunderstand between bugname and
|
||||||
|
// bidirectionalAudio params
|
||||||
|
this._bugname = 'audio_fork';
|
||||||
|
|
||||||
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
|
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Listen; }
|
get name() { return TaskName.Listen; }
|
||||||
|
|
||||||
|
set bugname(name) { this._bugname = name; }
|
||||||
|
|
||||||
|
set ignoreCustomerData(val) { this._ignoreCustomerData = val; }
|
||||||
|
|
||||||
async exec(cs, {ep}) {
|
async exec(cs, {ep}) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
@@ -58,10 +80,12 @@ class TaskListen extends Task {
|
|||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`);
|
this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`);
|
||||||
this._clearTimer();
|
this._clearTimer();
|
||||||
|
this.playAudioQueue = [];
|
||||||
if (this.ep && this.ep.connected) {
|
if (this.ep && this.ep.connected) {
|
||||||
this.logger.debug('TaskListen:kill closing websocket');
|
this.logger.debug('TaskListen:kill closing websocket');
|
||||||
try {
|
try {
|
||||||
await this.ep.forkAudioStop();
|
const args = this._bugname ? [this._bugname] : [];
|
||||||
|
await this.ep.forkAudioStop(...args);
|
||||||
this.logger.debug('TaskListen:kill successfully closed websocket');
|
this.logger.debug('TaskListen:kill successfully closed websocket');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info(err, 'TaskListen:kill');
|
this.logger.info(err, 'TaskListen:kill');
|
||||||
@@ -81,13 +105,16 @@ class TaskListen extends Task {
|
|||||||
|
|
||||||
async updateListen(status) {
|
async updateListen(status) {
|
||||||
if (!this.killed && this.ep && this.ep.connected) {
|
if (!this.killed && this.ep && this.ep.connected) {
|
||||||
|
const args = this._bugname ? [this._bugname] : [];
|
||||||
this.logger.info(`TaskListen:updateListen status ${status}`);
|
this.logger.info(`TaskListen:updateListen status ${status}`);
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ListenStatus.Pause:
|
case ListenStatus.Pause:
|
||||||
await this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
await this.ep.forkAudioPause(...args)
|
||||||
|
.catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
||||||
break;
|
break;
|
||||||
case ListenStatus.Resume:
|
case ListenStatus.Resume:
|
||||||
await this.ep.forkAudioResume().catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
|
await this.ep.forkAudioResume(...args)
|
||||||
|
.catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,13 +127,15 @@ class TaskListen extends Task {
|
|||||||
|
|
||||||
async _startListening(cs, ep) {
|
async _startListening(cs, ep) {
|
||||||
this._initListeners(ep);
|
this._initListeners(ep);
|
||||||
|
const ci = this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON();
|
||||||
|
if (this._ignoreCustomerData) {
|
||||||
|
delete ci.customerData;
|
||||||
|
}
|
||||||
const metadata = Object.assign(
|
const metadata = Object.assign(
|
||||||
{sampleRate: this.sampleRate, mixType: this.mixType},
|
{sampleRate: this.sampleRate, mixType: this.mixType},
|
||||||
this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON(),
|
ci,
|
||||||
this.metadata);
|
this.metadata);
|
||||||
if (this.hook.auth) {
|
if (this.hook.auth) {
|
||||||
this.logger.debug({username: this.hook.auth.username, password: this.hook.auth.password},
|
|
||||||
'TaskListen:_startListening basic auth');
|
|
||||||
await this.ep.set({
|
await this.ep.set({
|
||||||
'MOD_AUDIO_BASIC_AUTH_USERNAME': this.hook.auth.username,
|
'MOD_AUDIO_BASIC_AUTH_USERNAME': this.hook.auth.username,
|
||||||
'MOD_AUDIO_BASIC_AUTH_PASSWORD': this.hook.auth.password
|
'MOD_AUDIO_BASIC_AUTH_PASSWORD': this.hook.auth.password
|
||||||
@@ -116,7 +145,9 @@ class TaskListen extends Task {
|
|||||||
wsUrl: this.hook.url,
|
wsUrl: this.hook.url,
|
||||||
mixType: this.mixType,
|
mixType: this.mixType,
|
||||||
sampling: this.sampleRate,
|
sampling: this.sampleRate,
|
||||||
metadata
|
...(this._bugname && {bugname: this._bugname}),
|
||||||
|
metadata,
|
||||||
|
bidirectionalAudio: this.bidirectionalAudio || {}
|
||||||
});
|
});
|
||||||
this.recordStartTime = moment();
|
this.recordStartTime = moment();
|
||||||
if (this.maxLength) {
|
if (this.maxLength) {
|
||||||
@@ -136,7 +167,9 @@ class TaskListen extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* support bi-directional audio */
|
/* support bi-directional audio */
|
||||||
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
if (this.bidirectionalAudio.enabled) {
|
||||||
|
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
||||||
|
}
|
||||||
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
|
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
|
||||||
ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep));
|
ep.addCustomEventListener(ListenEvents.Disconnect, this._onDisconnect.bind(this, ep));
|
||||||
}
|
}
|
||||||
@@ -155,12 +188,25 @@ class TaskListen extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onDtmf(ep, evt) {
|
_onDtmf(ep, evt) {
|
||||||
this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${evt.dtmf}`);
|
const {dtmf, duration} = evt;
|
||||||
|
this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${dtmf}`);
|
||||||
if (this.passDtmf && this.ep?.connected) {
|
if (this.passDtmf && this.ep?.connected) {
|
||||||
const obj = {event: 'dtmf', dtmf: evt.dtmf, duration: evt.duration};
|
const obj = {event: 'dtmf', dtmf, duration};
|
||||||
this.ep.forkAudioSendText(obj)
|
const args = this._bugname ? [this._bugname, obj] : [obj];
|
||||||
|
this.ep.forkAudioSendText(...args)
|
||||||
.catch((err) => this.logger.info({err}, 'TaskListen:_onDtmf error sending dtmf'));
|
.catch((err) => this.logger.info({err}, 'TaskListen:_onDtmf error sending dtmf'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* add a child span for the dtmf event */
|
||||||
|
const msDuration = Math.floor((duration / 8000) * 1000);
|
||||||
|
const {span} = this.startChildSpan(`${DTMF_SPAN_NAME}:${dtmf}`);
|
||||||
|
span.setAttributes({
|
||||||
|
channel: 1,
|
||||||
|
dtmf,
|
||||||
|
duration: `${msDuration}ms`
|
||||||
|
});
|
||||||
|
span.end();
|
||||||
|
|
||||||
if (evt.dtmf === this.finishOnKey) {
|
if (evt.dtmf === this.finishOnKey) {
|
||||||
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
|
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
|
||||||
this.results.digits = evt.dtmf;
|
this.results.digits = evt.dtmf;
|
||||||
@@ -182,16 +228,44 @@ class TaskListen extends Task {
|
|||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onPlayAudio(ep, evt) {
|
async _playAudio(ep, evt, logger) {
|
||||||
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
|
|
||||||
try {
|
try {
|
||||||
const results = await ep.play(evt.file);
|
const results = await ep.play(evt.file);
|
||||||
this.logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
||||||
ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)});
|
const obj = {
|
||||||
|
type: 'playDone',
|
||||||
|
data: {
|
||||||
|
id: evt.id,
|
||||||
|
...results
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const args = this._bugname ? [this._bugname, obj] : [obj];
|
||||||
|
ep.forkAudioSendText(...args);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({err}, 'Error playing file');
|
||||||
}
|
}
|
||||||
catch (err) {
|
}
|
||||||
this.logger.error({err}, 'Error playing file');
|
|
||||||
|
async _onPlayAudio(ep, evt) {
|
||||||
|
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
|
||||||
|
if (!evt.queuePlay) {
|
||||||
|
this.playAudioQueue = [];
|
||||||
|
this._playAudio(ep, evt, this.logger);
|
||||||
|
this.isPlayingAudioFromQueue = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.playAudioQueue.length <= MAX_PLAY_AUDIO_QUEUE_SIZE) {
|
||||||
|
this.playAudioQueue.push(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPlayingAudioFromQueue) return;
|
||||||
|
|
||||||
|
this.isPlayingAudioFromQueue = true;
|
||||||
|
while (this.playAudioQueue.length > 0) {
|
||||||
|
await this._playAudio(ep, this.playAudioQueue.shift(), this.logger);
|
||||||
|
}
|
||||||
|
this.isPlayingAudioFromQueue = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onKillAudio(ep) {
|
_onKillAudio(ep) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const Task = require('./task');
|
const { validateVerb } = require('@jambonz/verb-specifications');
|
||||||
const {TaskName} = require('../utils/constants');
|
const {TaskName} = require('../utils/constants');
|
||||||
const errBadInstruction = new Error('malformed jambonz application payload');
|
const errBadInstruction = new Error('malformed jambonz application payload');
|
||||||
|
|
||||||
@@ -12,8 +12,11 @@ function makeTask(logger, obj, parent) {
|
|||||||
if (typeof data !== 'object') {
|
if (typeof data !== 'object') {
|
||||||
throw errBadInstruction;
|
throw errBadInstruction;
|
||||||
}
|
}
|
||||||
Task.validate(name, data);
|
validateVerb(name, data, logger);
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
case TaskName.Answer:
|
||||||
|
const TaskAnswer = require('./answer');
|
||||||
|
return new TaskAnswer(logger, data, parent);
|
||||||
case TaskName.SipDecline:
|
case TaskName.SipDecline:
|
||||||
const TaskSipDecline = require('./sip_decline');
|
const TaskSipDecline = require('./sip_decline');
|
||||||
return new TaskSipDecline(logger, data, parent);
|
return new TaskSipDecline(logger, data, parent);
|
||||||
@@ -41,6 +44,9 @@ function makeTask(logger, obj, parent) {
|
|||||||
case TaskName.Dtmf:
|
case TaskName.Dtmf:
|
||||||
const TaskDtmf = require('./dtmf');
|
const TaskDtmf = require('./dtmf');
|
||||||
return new TaskDtmf(logger, data, parent);
|
return new TaskDtmf(logger, data, parent);
|
||||||
|
case TaskName.Dub:
|
||||||
|
const TaskDub = require('./dub');
|
||||||
|
return new TaskDub(logger, data, parent);
|
||||||
case TaskName.Enqueue:
|
case TaskName.Enqueue:
|
||||||
const TaskEnqueue = require('./enqueue');
|
const TaskEnqueue = require('./enqueue');
|
||||||
return new TaskEnqueue(logger, data, parent);
|
return new TaskEnqueue(logger, data, parent);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const Task = require('./task');
|
|||||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const uuidv4 = require('uuid-random');
|
const uuidv4 = require('uuid-random');
|
||||||
|
const {K8S} = require('../config');
|
||||||
class TaskMessage extends Task {
|
class TaskMessage extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
@@ -42,7 +42,7 @@ class TaskMessage extends Task {
|
|||||||
}
|
}
|
||||||
if (gw) {
|
if (gw) {
|
||||||
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
|
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
|
||||||
url = process.env.K8S ? 'http://smpp' : getSmpp();
|
url = K8S ? 'http://smpp' : getSmpp();
|
||||||
relativeUrl = '/sms';
|
relativeUrl = '/sms';
|
||||||
payload = {
|
payload = {
|
||||||
...payload,
|
...payload,
|
||||||
|
|||||||
@@ -22,7 +22,22 @@ class TaskPlay extends Task {
|
|||||||
async exec(cs, {ep}) {
|
async exec(cs, {ep}) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
|
let timeout;
|
||||||
|
let playbackSeconds = 0;
|
||||||
|
let playbackMilliseconds = 0;
|
||||||
|
let completed = !(this.timeoutSecs > 0 || this.loop);
|
||||||
|
if (this.timeoutSecs > 0) {
|
||||||
|
timeout = setTimeout(async() => {
|
||||||
|
completed = true;
|
||||||
|
try {
|
||||||
|
await this.kill(cs);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info(err, 'Error killing audio on timeoutSecs');
|
||||||
|
}
|
||||||
|
}, this.timeoutSecs * 1000);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
|
this.notifyStatus({event: 'start-playback'});
|
||||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
|
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
|
||||||
if (cs.isInConference) {
|
if (cs.isInConference) {
|
||||||
const {memberId, confName, confUuid} = cs;
|
const {memberId, confName, confUuid} = cs;
|
||||||
@@ -34,14 +49,24 @@ class TaskPlay extends Task {
|
|||||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
|
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const file = (this.timeoutSecs >= 0 || this.seekOffset >= 0) ?
|
let file = this.url;
|
||||||
{file: this.url, seekOffset: this.seekOffset, timeoutSecs: this.timeoutSecs} : this.url;
|
if (this.seekOffset >= 0) {
|
||||||
|
file = {file: this.url, seekOffset: this.seekOffset};
|
||||||
|
this.seekOffset = -1;
|
||||||
|
}
|
||||||
const result = await ep.play(file);
|
const result = await ep.play(file);
|
||||||
await this.performAction(Object.assign(result, {reason: 'playCompleted'}),
|
playbackSeconds += parseInt(result.playbackSeconds);
|
||||||
!(this.parentTask || cs.isConfirmCallSession));
|
playbackMilliseconds += parseInt(result.playbackMilliseconds);
|
||||||
|
if (this.killed || !this.loop || completed) {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
await this.performAction(
|
||||||
|
Object.assign(result, {reason: 'playCompleted', playbackSeconds, playbackMilliseconds}),
|
||||||
|
!(this.parentTask || cs.isConfirmCallSession));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
|
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
|
||||||
}
|
}
|
||||||
this.emit('playDone');
|
this.emit('playDone');
|
||||||
@@ -56,7 +81,8 @@ class TaskPlay extends Task {
|
|||||||
this.killPlayToConfMember(this.ep, memberId, confName);
|
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
this.notifyStatus({event: 'kill-playback'});
|
||||||
|
this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName} = require('../utils/constants');
|
const {TaskName} = require('../utils/constants');
|
||||||
const makeTask = require('./make_task');
|
const makeTask = require('./make_task');
|
||||||
const normalizeJambones = require('../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages an outdial made via REST API
|
* Manages an outdial made via REST API
|
||||||
@@ -11,10 +11,12 @@ class TaskRestDial extends Task {
|
|||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
|
|
||||||
this.from = this.data.from;
|
this.from = this.data.from;
|
||||||
|
this.callerName = this.data.callerName;
|
||||||
this.fromHost = this.data.fromHost;
|
this.fromHost = this.data.fromHost;
|
||||||
this.to = this.data.to;
|
this.to = this.data.to;
|
||||||
this.call_hook = this.data.call_hook;
|
this.call_hook = this.data.call_hook;
|
||||||
this.timeout = this.data.timeout || 60;
|
this.timeout = this.data.timeout || 60;
|
||||||
|
this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
|
||||||
|
|
||||||
this.on('connect', this._onConnect.bind(this));
|
this.on('connect', this._onConnect.bind(this));
|
||||||
this.on('callStatus', this._onCallStatus.bind(this));
|
this.on('callStatus', this._onCallStatus.bind(this));
|
||||||
@@ -22,37 +24,53 @@ class TaskRestDial extends Task {
|
|||||||
|
|
||||||
get name() { return TaskName.RestDial; }
|
get name() { return TaskName.RestDial; }
|
||||||
|
|
||||||
|
set appJson(app_json) {
|
||||||
|
this.app_json = app_json;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* INVITE has just been sent at this point
|
* INVITE has just been sent at this point
|
||||||
*/
|
*/
|
||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
this.req = cs.req;
|
this.cs = cs;
|
||||||
|
this.canCancel = true;
|
||||||
|
|
||||||
|
if (this.data.amd) {
|
||||||
|
this.startAmd = cs.startAmd;
|
||||||
|
this.stopAmd = cs.stopAmd;
|
||||||
|
this.on('amd', this._onAmdEvent.bind(this, cs));
|
||||||
|
}
|
||||||
|
|
||||||
this._setCallTimer();
|
this._setCallTimer();
|
||||||
await this.awaitTaskDone();
|
await this.awaitTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
turnOffAmd() {
|
||||||
|
if (this.callSession.ep && this.callSession.ep.amd) this.stopAmd(this.callSession.ep, this);
|
||||||
|
}
|
||||||
|
|
||||||
kill(cs) {
|
kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
this._clearCallTimer();
|
this._clearCallTimer();
|
||||||
if (this.req) {
|
if (this.canCancel) {
|
||||||
this.req.cancel();
|
this.canCancel = false;
|
||||||
this.req = null;
|
cs?.req?.cancel();
|
||||||
}
|
}
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onConnect(dlg) {
|
async _onConnect(dlg) {
|
||||||
this.req = null;
|
this.canCancel = false;
|
||||||
const cs = this.callSession;
|
const cs = this.callSession;
|
||||||
cs.setDialog(dlg);
|
cs.setDialog(dlg);
|
||||||
|
this.logger.debug('TaskRestDial:_onConnect - call connected');
|
||||||
|
if (this.sipRequestWithinDialogHook) this._initSipRequestWithinDialogHandler(cs, dlg);
|
||||||
try {
|
try {
|
||||||
const b3 = this.getTracingPropagation();
|
const b3 = this.getTracingPropagation();
|
||||||
const httpHeaders = b3 && {b3};
|
const httpHeaders = b3 && {b3};
|
||||||
const params = {
|
const params = {
|
||||||
...cs.callInfo,
|
...(cs.callInfo.toJSON()),
|
||||||
defaults: {
|
defaults: {
|
||||||
synthesizer: {
|
synthesizer: {
|
||||||
vendor: cs.speechSynthesisVendor,
|
vendor: cs.speechSynthesisVendor,
|
||||||
@@ -65,7 +83,21 @@ class TaskRestDial extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
|
if (this.startAmd) {
|
||||||
|
try {
|
||||||
|
this.startAmd(this.callSession, this.callSession.ep, this, this.data.amd);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err}, 'Rest:dial:Call established - Error calling startAmd');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tasks;
|
||||||
|
if (this.app_json) {
|
||||||
|
this.logger.debug('TaskRestDial: using app_json from task data');
|
||||||
|
tasks = JSON.parse(this.app_json);
|
||||||
|
} else {
|
||||||
|
this.logger.debug({call_hook: this.call_hook}, 'TaskRestDial: retrieving application');
|
||||||
|
tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
|
||||||
|
}
|
||||||
if (tasks && Array.isArray(tasks)) {
|
if (tasks && Array.isArray(tasks)) {
|
||||||
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
||||||
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||||
@@ -79,7 +111,7 @@ class TaskRestDial extends Task {
|
|||||||
_onCallStatus(status) {
|
_onCallStatus(status) {
|
||||||
this.logger.debug(`CallStatus: ${status}`);
|
this.logger.debug(`CallStatus: ${status}`);
|
||||||
if (status >= 200) {
|
if (status >= 200) {
|
||||||
this.req = null;
|
this.canCancel = false;
|
||||||
this._clearCallTimer();
|
this._clearCallTimer();
|
||||||
if (status !== 200) this.notifyTaskDone();
|
if (status !== 200) this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
@@ -97,7 +129,29 @@ class TaskRestDial extends Task {
|
|||||||
_onCallTimeout() {
|
_onCallTimeout() {
|
||||||
this.logger.debug('TaskRestDial: timeout expired without answer, killing task');
|
this.logger.debug('TaskRestDial: timeout expired without answer, killing task');
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this.kill();
|
if (this.canCancel) {
|
||||||
|
this.canCancel = false;
|
||||||
|
this.cs?.req?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAmdEvent(cs, evt) {
|
||||||
|
this.logger.info({evt}, 'Rest:dial:_onAmdEvent');
|
||||||
|
const {actionHook} = this.data.amd;
|
||||||
|
this.performHook(cs, actionHook, evt)
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.error({err}, 'Rest:dial:_onAmdEvent - error calling actionHook');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_initSipRequestWithinDialogHandler(cs, dlg) {
|
||||||
|
cs.sipRequestWithinDialogHook = this.sipRequestWithinDialogHook;
|
||||||
|
dlg.on('info', this._onRequestWithinDialog.bind(this, cs));
|
||||||
|
dlg.on('message', this._onRequestWithinDialog.bind(this, cs));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onRequestWithinDialog(cs, req, res) {
|
||||||
|
cs._onRequestWithinDialog(req, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
363
lib/tasks/say.js
363
lib/tasks/say.js
@@ -1,101 +1,37 @@
|
|||||||
const Task = require('./task');
|
const TtsTask = require('./tts-task');
|
||||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
|
const pollySSMLSplit = require('polly-ssml-split');
|
||||||
|
|
||||||
const breakLengthyTextIfNeeded = (logger, text) => {
|
const breakLengthyTextIfNeeded = (logger, text) => {
|
||||||
const chunkSize = 1000;
|
const chunkSize = 1000;
|
||||||
if (text.length <= chunkSize) return [text];
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
const isSSML = text.startsWith('<speak>');
|
const isSSML = text.startsWith('<speak>');
|
||||||
let startPos = 0;
|
if (text.length <= chunkSize || !isSSML) return [text];
|
||||||
let charPos = isSSML ? 7 : 0; // skip <speak>
|
const options = {
|
||||||
let tag;
|
// MIN length
|
||||||
//logger.debug({isSSML}, `breakLengthyTextIfNeeded: handling text of length ${text.length}`);
|
softLimit: 100,
|
||||||
while (startPos + charPos < text.length) {
|
// MAX length, exclude 15 characters <speak></speak>
|
||||||
if (isSSML && !tag && text[startPos + charPos] === '<') {
|
hardLimit: chunkSize - 15,
|
||||||
const tagStartPos = ++charPos;
|
// Set of extra split characters (Optional property)
|
||||||
while (startPos + charPos < text.length) {
|
extraSplitChars: ',;!?',
|
||||||
if (text[startPos + charPos] === '>') {
|
};
|
||||||
if (text[startPos + charPos - 1] === '\\') tag = null;
|
pollySSMLSplit.configure(options);
|
||||||
else if (!tag) tag = text.substring(startPos + tagStartPos, startPos + charPos - 1);
|
try {
|
||||||
break;
|
return pollySSMLSplit.split(text);
|
||||||
}
|
} catch (err) {
|
||||||
if (!tag) {
|
logger.info({err}, 'Error spliting SSML long text');
|
||||||
const c = text[startPos + charPos];
|
return [text];
|
||||||
if (c === ' ') {
|
|
||||||
tag = text.substring(startPos + tagStartPos, startPos + charPos);
|
|
||||||
//logger.debug(`breakLengthyTextIfNeeded: enter tag ${tag} (space)`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
charPos++;
|
|
||||||
}
|
|
||||||
if (tag) {
|
|
||||||
//search for end of tag
|
|
||||||
//logger.debug(`breakLengthyTextIfNeeded: searching forward for </${tag}>`);
|
|
||||||
const e1 = text.indexOf(`</${tag}>`, startPos + charPos);
|
|
||||||
const e2 = text.indexOf('/>', startPos + charPos);
|
|
||||||
const tagEndPos = e1 === -1 ? e2 : e2 === -1 ? e1 : Math.min(e1, e2);
|
|
||||||
if (tagEndPos === -1) {
|
|
||||||
//logger.debug(`breakLengthyTextIfNeeded: exit tag ${tag} not found, exiting`);
|
|
||||||
} else {
|
|
||||||
//logger.debug(`breakLengthyTextIfNeeded: exit tag ${tag} found at ${tagEndPos}`);
|
|
||||||
charPos = tagEndPos + 1;
|
|
||||||
}
|
|
||||||
tag = null;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (charPos < chunkSize) {
|
|
||||||
charPos++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// start looking for a good break point
|
|
||||||
let chunkIt = false;
|
|
||||||
const a = text[startPos + charPos];
|
|
||||||
const b = text[startPos + charPos + 1];
|
|
||||||
if (/[\.!\?]/.test(a) && /\s/.test(b)) {
|
|
||||||
//logger.debug('breakLengthyTextIfNeeded: breaking at sentence end');
|
|
||||||
chunkIt = true;
|
|
||||||
}
|
|
||||||
if (chunkIt) {
|
|
||||||
charPos++;
|
|
||||||
const chunk = text.substr(startPos, charPos);
|
|
||||||
if (isSSML) {
|
|
||||||
result.push(0 === startPos ? `${chunk}</speak>` : `<speak>${chunk}</speak>`);
|
|
||||||
}
|
|
||||||
else result.push(chunk);
|
|
||||||
charPos = 0;
|
|
||||||
startPos += chunk.length;
|
|
||||||
|
|
||||||
//logger.debug({chunk: result[result.length - 1]},
|
|
||||||
// `breakLengthyTextIfNeeded: chunked; new starting pos ${startPos}`);
|
|
||||||
|
|
||||||
}
|
|
||||||
else charPos++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// final chunk
|
|
||||||
if (startPos < text.length) {
|
|
||||||
const chunk = text.substr(startPos);
|
|
||||||
if (isSSML) {
|
|
||||||
result.push(0 === startPos ? `${chunk}</speak>` : `<speak>${chunk}`);
|
|
||||||
}
|
|
||||||
else result.push(chunk);
|
|
||||||
|
|
||||||
//logger.debug({chunk: result[result.length - 1]},
|
|
||||||
// `breakLengthyTextIfNeeded: final chunk; starting pos ${startPos} length ${chunk.length}`);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class TaskSay extends Task {
|
const parseTextFromSayString = (text) => {
|
||||||
|
const closingBraceIndex = text.indexOf('}');
|
||||||
|
if (closingBraceIndex === -1) return text;
|
||||||
|
return text.slice(closingBraceIndex + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
class TaskSay extends TtsTask {
|
||||||
constructor(logger, opts, parentTask) {
|
constructor(logger, opts, parentTask) {
|
||||||
super(logger, opts);
|
super(logger, opts, parentTask);
|
||||||
this.preconditions = TaskPreconditions.Endpoint;
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
|
||||||
this.text = (Array.isArray(this.data.text) ? this.data.text : [this.data.text])
|
this.text = (Array.isArray(this.data.text) ? this.data.text : [this.data.text])
|
||||||
@@ -103,8 +39,7 @@ class TaskSay extends Task {
|
|||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
this.loop = this.data.loop || 1;
|
this.loop = this.data.loop || 1;
|
||||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
this.isHandledByPrimaryProvider = true;
|
||||||
this.synthesizer = this.data.synthesizer || {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Say; }
|
get name() { return TaskName.Say; }
|
||||||
@@ -118,12 +53,14 @@ class TaskSay extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async exec(cs, {ep}) {
|
async exec(cs, {ep}) {
|
||||||
await super.exec(cs);
|
const {srf, accountSid:account_sid} = cs;
|
||||||
|
const {writeAlerts, AlertType} = srf.locals;
|
||||||
|
const {addFileToCache} = srf.locals.dbHelpers;
|
||||||
|
const engine = this.synthesizer.engine || 'standard';
|
||||||
|
|
||||||
|
await super.exec(cs);
|
||||||
|
this.ep = ep;
|
||||||
|
|
||||||
const {srf} = cs;
|
|
||||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
|
||||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
|
||||||
const {synthAudio} = srf.locals.dbHelpers;
|
|
||||||
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
|
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
|
||||||
this.synthesizer.vendor :
|
this.synthesizer.vendor :
|
||||||
cs.speechSynthesisVendor;
|
cs.speechSynthesisVendor;
|
||||||
@@ -133,112 +70,174 @@ class TaskSay extends Task {
|
|||||||
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
||||||
this.synthesizer.voice :
|
this.synthesizer.voice :
|
||||||
cs.speechSynthesisVoice;
|
cs.speechSynthesisVoice;
|
||||||
const engine = this.synthesizer.engine || 'standard';
|
const label = this.synthesizer.label && this.synthesizer.label !== 'default' ?
|
||||||
const salt = cs.callSid;
|
this.synthesizer.label :
|
||||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
cs.speechSynthesisLabel;
|
||||||
|
|
||||||
this.logger.info({vendor, language, voice}, 'TaskSay:exec');
|
const fallbackVendor = this.synthesizer.fallbackVendor && this.synthesizer.fallbackVendor !== 'default' ?
|
||||||
this.ep = ep;
|
this.synthesizer.fallbackVendor :
|
||||||
|
cs.fallbackSpeechSynthesisVendor;
|
||||||
|
const fallbackLanguage = this.synthesizer.fallbackLanguage && this.synthesizer.fallbackLanguage !== 'default' ?
|
||||||
|
this.synthesizer.fallbackLanguage :
|
||||||
|
cs.fallbackSpeechSynthesisLanguage ;
|
||||||
|
const fallbackVoice = this.synthesizer.fallbackVoice && this.synthesizer.fallbackVoice !== 'default' ?
|
||||||
|
this.synthesizer.fallbackVoice :
|
||||||
|
cs.fallbackSpeechSynthesisVoice;
|
||||||
|
const fallbackLabel = this.synthesizer.fallbackLabel && this.synthesizer.fallbackLabel !== 'default' ?
|
||||||
|
this.synthesizer.fallbackLabel :
|
||||||
|
cs.fallbackSpeechSynthesisLabel;
|
||||||
|
|
||||||
|
let filepath;
|
||||||
try {
|
try {
|
||||||
if (!credentials) {
|
filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label});
|
||||||
writeAlerts({
|
} catch (error) {
|
||||||
account_sid: cs.accountSid,
|
if (fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||||
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
this.isHandledByPrimaryProvider = false;
|
||||||
vendor
|
this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`);
|
||||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
filepath = await this._synthesizeWithSpecificVendor(cs, ep,
|
||||||
this.notifyError(`No speech credentials have been provisioned for ${vendor}`);
|
{
|
||||||
throw new Error('no provisioned speech credentials for TTS');
|
vendor: fallbackVendor,
|
||||||
}
|
language: fallbackLanguage,
|
||||||
// synthesize all of the text elements
|
voice: fallbackVoice,
|
||||||
let lastUpdated = false;
|
label: fallbackLabel
|
||||||
|
|
||||||
/* produce an audio segment from the provided text */
|
|
||||||
const generateAudio = async(text) => {
|
|
||||||
if (this.killed) return;
|
|
||||||
if (text.startsWith('silence_stream://')) return text;
|
|
||||||
|
|
||||||
/* otel: trace time for tts */
|
|
||||||
const {span} = this.startChildSpan('tts-generation', {
|
|
||||||
'tts.vendor': vendor,
|
|
||||||
'tts.language': language,
|
|
||||||
'tts.voice': voice
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
if (vendor === 'microsoft' && this.synthesizer.azureServiceEndpoint) {
|
|
||||||
credentials.use_custom_tts = true;
|
|
||||||
credentials.custom_tts_endpoint = this.synthesizer.azureServiceEndpoint;
|
|
||||||
}
|
|
||||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
|
||||||
text,
|
|
||||||
vendor,
|
|
||||||
language,
|
|
||||||
voice,
|
|
||||||
engine,
|
|
||||||
salt,
|
|
||||||
credentials
|
|
||||||
});
|
});
|
||||||
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
|
} else {
|
||||||
if (filePath) cs.trackTmpFile(filePath);
|
throw error;
|
||||||
if (!servedFromCache && !lastUpdated) {
|
}
|
||||||
lastUpdated = true;
|
}
|
||||||
updateSpeechCredentialLastUsed(credentials.speech_credential_sid)
|
this.notifyStatus({event: 'start-playback'});
|
||||||
.catch(() => {/*already logged error */});
|
|
||||||
}
|
while (!this.killed && (this.loop === 'forever' || this.loop--) && ep?.connected) {
|
||||||
span.setAttributes({'tts.cached': servedFromCache});
|
let segment = 0;
|
||||||
span.end();
|
while (!this.killed && segment < filepath.length) {
|
||||||
return filePath;
|
if (cs.isInConference) {
|
||||||
} catch (err) {
|
const {memberId, confName, confUuid} = cs;
|
||||||
this.logger.info({err}, 'Error synthesizing tts');
|
await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]);
|
||||||
span.end();
|
}
|
||||||
writeAlerts({
|
else {
|
||||||
account_sid: cs.accountSid,
|
if (filepath[segment].startsWith('say:{')) {
|
||||||
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
|
||||||
vendor,
|
if (arr) this.logger.debug(`Say:exec sending streaming tts request: ${arr[1].substring(0, 64)}..`);
|
||||||
detail: err.message
|
}
|
||||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
else this.logger.debug(`Say:exec sending ${filepath[segment].substring(0, 64)}`);
|
||||||
this.notifyError(err.message || err);
|
ep.once('playback-start', (evt) => {
|
||||||
return;
|
this.logger.debug({evt}, 'got playback-start');
|
||||||
}
|
if (this.otelSpan) {
|
||||||
};
|
this._addStreamingTtsAttributes(this.otelSpan, evt);
|
||||||
|
this.otelSpan.end();
|
||||||
const arr = this.text.map((t) => generateAudio(t));
|
this.otelSpan = null;
|
||||||
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
|
if (evt.variable_tts_cache_filename) cs.trackTmpFile(evt.variable_tts_cache_filename);
|
||||||
this.logger.debug({filepath}, 'synthesized files for tts');
|
}
|
||||||
|
});
|
||||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
|
ep.once('playback-stop', (evt) => {
|
||||||
let segment = 0;
|
this.logger.debug({evt}, 'got playback-stop');
|
||||||
while (!this.killed && segment < filepath.length) {
|
if (evt.variable_tts_error) {
|
||||||
if (cs.isInConference) {
|
writeAlerts({
|
||||||
const {memberId, confName, confUuid} = cs;
|
account_sid,
|
||||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
|
alert_type: AlertType.TTS_FAILURE,
|
||||||
}
|
vendor,
|
||||||
else {
|
detail: evt.variable_tts_error
|
||||||
this.logger.debug(`Say:exec sending command to play file ${filepath[segment]}`);
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
||||||
await ep.play(filepath[segment]);
|
}
|
||||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
if (evt.variable_tts_cache_filename) {
|
||||||
}
|
const text = parseTextFromSayString(this.text[segment]);
|
||||||
segment++;
|
addFileToCache(evt.variable_tts_cache_filename, {
|
||||||
}
|
account_sid,
|
||||||
|
vendor,
|
||||||
|
language,
|
||||||
|
voice,
|
||||||
|
engine,
|
||||||
|
text
|
||||||
|
}).catch((err) => this.logger.info({err}, 'Error adding file to cache'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await ep.play(filepath[segment]);
|
||||||
|
if (filepath[segment].startsWith('say:{')) {
|
||||||
|
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
|
||||||
|
if (arr) this.logger.debug(`Say:exec complete playing streaming tts request: ${arr[1].substring(0, 64)}..`);
|
||||||
|
}
|
||||||
|
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
||||||
|
}
|
||||||
|
segment++;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
this.logger.info(err, 'TaskSay:exec error');
|
|
||||||
}
|
}
|
||||||
this.emit('playDone');
|
this.emit('playDone');
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill(cs) {
|
async kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
if (this.ep.connected) {
|
if (this.ep?.connected) {
|
||||||
this.logger.debug('TaskSay:kill - killing audio');
|
this.logger.debug('TaskSay:kill - killing audio');
|
||||||
if (cs.isInConference) {
|
if (cs.isInConference) {
|
||||||
const {memberId, confName} = cs;
|
const {memberId, confName} = cs;
|
||||||
this.killPlayToConfMember(this.ep, memberId, confName);
|
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
this.notifyStatus({event: 'kill-playback'});
|
||||||
this.ep.api('uuid_break', this.ep.uuid);
|
this.ep.api('uuid_break', this.ep.uuid);
|
||||||
}
|
}
|
||||||
|
this.ep.removeAllListeners('playback-start');
|
||||||
|
this.ep.removeAllListeners('playback-stop');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_addStreamingTtsAttributes(span, evt) {
|
||||||
|
const attrs = {'tts.cached': false};
|
||||||
|
for (const [key, value] of Object.entries(evt)) {
|
||||||
|
if (key.startsWith('variable_tts_')) {
|
||||||
|
let newKey = key.substring('variable_tts_'.length)
|
||||||
|
.replace('whisper_', 'whisper.')
|
||||||
|
.replace('deepgram_', 'deepgram.')
|
||||||
|
.replace('playht_', 'playht.')
|
||||||
|
.replace('rimelabs_', 'rimelabs.')
|
||||||
|
.replace('elevenlabs_', 'elevenlabs.');
|
||||||
|
if (spanMapping[newKey]) newKey = spanMapping[newKey];
|
||||||
|
attrs[newKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete attrs['cache_filename']; //no value in adding this to the span
|
||||||
|
span.setAttributes(attrs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const spanMapping = {
|
||||||
|
// IMPORTANT!!! JAMBONZ WEBAPP WILL SHOW TEXT PERFECTLY IF THE SPAN NAME IS SMALLER OR EQUAL 25 CHARACTERS.
|
||||||
|
// EX: whisper.ratelim_reqs has length 20 <= 25 which is perfect
|
||||||
|
// Elevenlabs
|
||||||
|
'elevenlabs.reported_latency_ms': 'elevenlabs.latency_ms',
|
||||||
|
'elevenlabs.request_id': 'elevenlabs.req_id',
|
||||||
|
'elevenlabs.history_item_id': 'elevenlabs.item_id',
|
||||||
|
'elevenlabs.optimize_streaming_latency': 'elevenlabs.optimization',
|
||||||
|
'elevenlabs.name_lookup_time_ms': 'name_lookup_ms',
|
||||||
|
'elevenlabs.connect_time_ms': 'connect_ms',
|
||||||
|
'elevenlabs.final_response_time_ms': 'final_response_ms',
|
||||||
|
// Whisper
|
||||||
|
'whisper.reported_latency_ms': 'whisper.latency_ms',
|
||||||
|
'whisper.request_id': 'whisper.req_id',
|
||||||
|
'whisper.reported_organization': 'whisper.organization',
|
||||||
|
'whisper.reported_ratelimit_requests': 'whisper.ratelimit',
|
||||||
|
'whisper.reported_ratelimit_remaining_requests': 'whisper.ratelimit_remain',
|
||||||
|
'whisper.reported_ratelimit_reset_requests': 'whisper.ratelimit_reset',
|
||||||
|
'whisper.name_lookup_time_ms': 'name_lookup_ms',
|
||||||
|
'whisper.connect_time_ms': 'connect_ms',
|
||||||
|
'whisper.final_response_time_ms': 'final_response_ms',
|
||||||
|
// Deepgram
|
||||||
|
'deepgram.request_id': 'deepgram.req_id',
|
||||||
|
'deepgram.reported_model_name': 'deepgram.model_name',
|
||||||
|
'deepgram.reported_model_uuid': 'deepgram.model_uuid',
|
||||||
|
'deepgram.reported_char_count': 'deepgram.char_count',
|
||||||
|
'deepgram.name_lookup_time_ms': 'name_lookup_ms',
|
||||||
|
'deepgram.connect_time_ms': 'connect_ms',
|
||||||
|
'deepgram.final_response_time_ms': 'final_response_ms',
|
||||||
|
// Playht
|
||||||
|
'playht.request_id': 'playht.req_id',
|
||||||
|
'playht.name_lookup_time_ms': 'name_lookup_ms',
|
||||||
|
'playht.connect_time_ms': 'connect_ms',
|
||||||
|
'playht.final_response_time_ms': 'final_response_ms',
|
||||||
|
// Rimelabs
|
||||||
|
'rimelabs.name_lookup_time_ms': 'name_lookup_ms',
|
||||||
|
'rimelabs.connect_time_ms': 'connect_ms',
|
||||||
|
'rimelabs.final_response_time_ms': 'final_response_ms',
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = TaskSay;
|
module.exports = TaskSay;
|
||||||
|
|||||||
@@ -47,7 +47,17 @@ class TaskSipRefer extends Task {
|
|||||||
|
|
||||||
/* if we fail, fall through to next verb. If success, we should get BYE from far end */
|
/* if we fail, fall through to next verb. If success, we should get BYE from far end */
|
||||||
if (this.referStatus === 202) {
|
if (this.referStatus === 202) {
|
||||||
|
this._notifyTimer = setTimeout(() => {
|
||||||
|
this.logger.info('TaskSipRefer:exec - no NOTIFY received in 15 secs, exiting');
|
||||||
|
this.performAction({refer_status: this.referStatus})
|
||||||
|
.catch((err) => this.logger.error(err, 'TaskSipRefer:exec - error performing action'));
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}, 15000);
|
||||||
await this.awaitTaskDone();
|
await this.awaitTaskDone();
|
||||||
|
if (this._notifyTimer) {
|
||||||
|
clearTimeout(this._notifyTimer);
|
||||||
|
this._notifyTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await this.performAction({refer_status: this.referStatus});
|
await this.performAction({refer_status: this.referStatus});
|
||||||
@@ -71,10 +81,10 @@ class TaskSipRefer extends Task {
|
|||||||
const contentType = req.get('Content-Type');
|
const contentType = req.get('Content-Type');
|
||||||
this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`);
|
this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`);
|
||||||
|
|
||||||
if (contentType === 'message/sipfrag') {
|
if (contentType?.includes('message/sipfrag')) {
|
||||||
const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body);
|
const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
const status = arr[1];
|
const status = typeof arr[1] === 'string' ? parseInt(arr[1], 10) : arr[1];
|
||||||
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
|
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
|
||||||
if (this.eventHook) {
|
if (this.eventHook) {
|
||||||
const b3 = this.getTracingPropagation();
|
const b3 = this.getTracingPropagation();
|
||||||
|
|||||||
@@ -1,558 +0,0 @@
|
|||||||
{
|
|
||||||
"sip:decline": {
|
|
||||||
"properties": {
|
|
||||||
"status": "number",
|
|
||||||
"reason": "string",
|
|
||||||
"headers": "object"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"status"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sip:request": {
|
|
||||||
"properties": {
|
|
||||||
"method": "string",
|
|
||||||
"body": "string",
|
|
||||||
"headers": "object",
|
|
||||||
"actionHook": "object|string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"method"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sip:refer": {
|
|
||||||
"properties": {
|
|
||||||
"referTo": "string",
|
|
||||||
"referredBy": "string",
|
|
||||||
"headers": "object",
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"eventHook": "object|string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"referTo"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"properties": {
|
|
||||||
"synthesizer": "#synthesizer",
|
|
||||||
"recognizer": "#recognizer",
|
|
||||||
"bargeIn": "#bargeIn",
|
|
||||||
"record": "#recordOptions",
|
|
||||||
"amd": "#amd"
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
},
|
|
||||||
"bargeIn": {
|
|
||||||
"properties": {
|
|
||||||
"enable": "boolean",
|
|
||||||
"sticky": "boolean",
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"input": "array",
|
|
||||||
"finishOnKey": "string",
|
|
||||||
"numDigits": "number",
|
|
||||||
"minDigits": "number",
|
|
||||||
"maxDigits": "number",
|
|
||||||
"interDigitTimeout": "number",
|
|
||||||
"dtmfBargein": "boolean",
|
|
||||||
"minBargeinWordCount": "number"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"enable"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dequeue": {
|
|
||||||
"properties": {
|
|
||||||
"name": "string",
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"timeout": "number",
|
|
||||||
"beep": "boolean"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"enqueue": {
|
|
||||||
"properties": {
|
|
||||||
"name": "string",
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"waitHook": "object|string",
|
|
||||||
"_": "object"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"leave": {
|
|
||||||
"properties": {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hangup": {
|
|
||||||
"properties": {
|
|
||||||
"headers": "object"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"play": {
|
|
||||||
"properties": {
|
|
||||||
"url": "string|array",
|
|
||||||
"loop": "number|string",
|
|
||||||
"earlyMedia": "boolean",
|
|
||||||
"seekOffset": "number|string",
|
|
||||||
"timeoutSecs": "number|string",
|
|
||||||
"actionHook": "object|string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"url"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"say": {
|
|
||||||
"properties": {
|
|
||||||
"text": "string|array",
|
|
||||||
"loop": "number|string",
|
|
||||||
"synthesizer": "#synthesizer",
|
|
||||||
"earlyMedia": "boolean"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"text"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"gather": {
|
|
||||||
"properties": {
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"finishOnKey": "string",
|
|
||||||
"input": "array",
|
|
||||||
"numDigits": "number",
|
|
||||||
"minDigits": "number",
|
|
||||||
"maxDigits": "number",
|
|
||||||
"interDigitTimeout": "number",
|
|
||||||
"partialResultHook": "object|string",
|
|
||||||
"speechTimeout": "number",
|
|
||||||
"listenDuringPrompt": "boolean",
|
|
||||||
"dtmfBargein": "boolean",
|
|
||||||
"bargein": "boolean",
|
|
||||||
"minBargeinWordCount": "number",
|
|
||||||
"timeout": "number",
|
|
||||||
"recognizer": "#recognizer",
|
|
||||||
"play": "#play",
|
|
||||||
"say": "#say"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"conference": {
|
|
||||||
"properties": {
|
|
||||||
"name": "string",
|
|
||||||
"beep": "boolean",
|
|
||||||
"startConferenceOnEnter": "boolean",
|
|
||||||
"endConferenceOnExit": "boolean",
|
|
||||||
"maxParticipants": "number",
|
|
||||||
"joinMuted": "boolean",
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"waitHook": "object|string",
|
|
||||||
"statusEvents": "array",
|
|
||||||
"statusHook": "object|string",
|
|
||||||
"enterHook": "object|string",
|
|
||||||
"record": "#record"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dial": {
|
|
||||||
"properties": {
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"answerOnBridge": "boolean",
|
|
||||||
"callerId": "string",
|
|
||||||
"confirmHook": "object|string",
|
|
||||||
"referHook": "object|string",
|
|
||||||
"dialMusic": "string",
|
|
||||||
"dtmfCapture": "object",
|
|
||||||
"dtmfHook": "object|string",
|
|
||||||
"headers": "object",
|
|
||||||
"listen": "#listen",
|
|
||||||
"target": ["#target"],
|
|
||||||
"timeLimit": "number",
|
|
||||||
"timeout": "number",
|
|
||||||
"proxy": "string",
|
|
||||||
"transcribe": "#transcribe",
|
|
||||||
"amd": "#amd"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"target"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dialogflow": {
|
|
||||||
"properties": {
|
|
||||||
"credentials": "object|string",
|
|
||||||
"project": "string",
|
|
||||||
"environment": "string",
|
|
||||||
"region": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["europe-west1", "europe-west2", "australia-southeast1", "asia-northeast1"]
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
"auth": "#auth",
|
|
||||||
"finishOnKey": "string",
|
|
||||||
"maxLength": "number",
|
|
||||||
"metadata": "object",
|
|
||||||
"mixType": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["mono", "stereo", "mixed"]
|
|
||||||
},
|
|
||||||
"passDtmf": "boolean",
|
|
||||||
"playBeep": "boolean",
|
|
||||||
"sampleRate": "number",
|
|
||||||
"timeout": "number",
|
|
||||||
"transcribe": "#transcribe",
|
|
||||||
"url": "string",
|
|
||||||
"wsAuth": "#auth",
|
|
||||||
"earlyMedia": "boolean"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"length"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"rasa": {
|
|
||||||
"properties": {
|
|
||||||
"url": "string",
|
|
||||||
"recognizer": "#recognizer",
|
|
||||||
"tts": "#synthesizer",
|
|
||||||
"prompt": "string",
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"eventHook": "object|string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"url"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"record": {
|
|
||||||
"properties": {
|
|
||||||
"path": "string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"path"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"recordOptions": {
|
|
||||||
"properties": {
|
|
||||||
"action": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"]
|
|
||||||
},
|
|
||||||
"recordingID": "string",
|
|
||||||
"siprecServerURL": "string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"action"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"redirect": {
|
|
||||||
"properties": {
|
|
||||||
"actionHook": "object|string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"actionHook"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"rest:dial": {
|
|
||||||
"properties": {
|
|
||||||
"account_sid": "string",
|
|
||||||
"application_sid": "string",
|
|
||||||
"call_hook": "object|string",
|
|
||||||
"call_status_hook": "object|string",
|
|
||||||
"from": "string",
|
|
||||||
"fromHost": "string",
|
|
||||||
"speech_synthesis_vendor": "string",
|
|
||||||
"speech_synthesis_voice": "string",
|
|
||||||
"speech_synthesis_language": "string",
|
|
||||||
"speech_recognizer_vendor": "string",
|
|
||||||
"speech_recognizer_language": "string",
|
|
||||||
"tag": "object",
|
|
||||||
"to": "#target",
|
|
||||||
"headers": "object",
|
|
||||||
"timeout": "number"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"call_hook",
|
|
||||||
"from",
|
|
||||||
"to"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"tag": {
|
|
||||||
"properties": {
|
|
||||||
"data": "object"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"data"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"transcribe": {
|
|
||||||
"properties": {
|
|
||||||
"transcriptionHook": "string",
|
|
||||||
"recognizer": "#recognizer",
|
|
||||||
"earlyMedia": "boolean"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"recognizer"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"target": {
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["phone", "sip", "user", "teams"]
|
|
||||||
},
|
|
||||||
"confirmHook": "object|string",
|
|
||||||
"method": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["GET", "POST"]
|
|
||||||
},
|
|
||||||
"headers": "object",
|
|
||||||
"from": "#dialFrom",
|
|
||||||
"name": "string",
|
|
||||||
"number": "string",
|
|
||||||
"sipUri": "string",
|
|
||||||
"auth": "#auth",
|
|
||||||
"vmail": "boolean",
|
|
||||||
"tenant": "string",
|
|
||||||
"trunk": "string",
|
|
||||||
"overrideTo": "string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"type"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dialFrom": {
|
|
||||||
"properties": {
|
|
||||||
"user": "string",
|
|
||||||
"host": "string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"properties": {
|
|
||||||
"username": "string",
|
|
||||||
"password": "string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"username",
|
|
||||||
"password"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"synthesizer": {
|
|
||||||
"properties": {
|
|
||||||
"vendor": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["google", "aws", "polly", "microsoft", "default"]
|
|
||||||
},
|
|
||||||
"language": "string",
|
|
||||||
"voice": "string",
|
|
||||||
"engine": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["standard", "neural"]
|
|
||||||
},
|
|
||||||
"gender": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["MALE", "FEMALE", "NEUTRAL"]
|
|
||||||
},
|
|
||||||
"azureServiceEndpoint": "string"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"vendor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"recognizer": {
|
|
||||||
"properties": {
|
|
||||||
"vendor": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["google", "aws", "microsoft", "default"]
|
|
||||||
},
|
|
||||||
"language": "string",
|
|
||||||
"vad": "#vad",
|
|
||||||
"hints": "array",
|
|
||||||
"hintsBoost": "number",
|
|
||||||
"altLanguages": "array",
|
|
||||||
"profanityFilter": "boolean",
|
|
||||||
"interim": "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"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"model": "string",
|
|
||||||
"outputFormat": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"simple",
|
|
||||||
"detailed"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"profanityOption": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"masked",
|
|
||||||
"removed",
|
|
||||||
"raw"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"requestSnr": "boolean",
|
|
||||||
"initialSpeechTimeoutMs": "number",
|
|
||||||
"azureServiceEndpoint": "string",
|
|
||||||
"azureSttEndpointId": "string",
|
|
||||||
"asrDtmfTerminationDigit": "string",
|
|
||||||
"asrTimeout": "number",
|
|
||||||
"audioLogging": "boolean"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"vendor"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lexIntent": {
|
|
||||||
"properties": {
|
|
||||||
"name": "string",
|
|
||||||
"slots": "object"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"vad": {
|
|
||||||
"properties": {
|
|
||||||
"enable": "boolean",
|
|
||||||
"voiceMs": "number",
|
|
||||||
"mode": "number"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"enable"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"amd": {
|
|
||||||
"properties": {
|
|
||||||
"actionHook": "object|string",
|
|
||||||
"thresholdWordCount": "number",
|
|
||||||
"timers": "#amdTimers",
|
|
||||||
"recognizer": "#recognizer"
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"actionHook"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"amdTimers": {
|
|
||||||
"properties": {
|
|
||||||
"noSpeechTimeoutMs": "number",
|
|
||||||
"decisionTimeoutMs": "number",
|
|
||||||
"toneTimeoutMs": "number",
|
|
||||||
"greetingCompletionTimeoutMs": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
279
lib/tasks/stt-task.js
Normal file
279
lib/tasks/stt-task.js
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
const Task = require('./task');
|
||||||
|
const assert = require('assert');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { TaskPreconditions, CobaltTranscriptionEvents } = require('../utils/constants');
|
||||||
|
|
||||||
|
class SttTask extends Task {
|
||||||
|
|
||||||
|
constructor(logger, data, parentTask) {
|
||||||
|
super(logger, data);
|
||||||
|
this.parentTask = parentTask;
|
||||||
|
|
||||||
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
|
||||||
|
const {
|
||||||
|
setChannelVarsForStt,
|
||||||
|
normalizeTranscription,
|
||||||
|
setSpeechCredentialsAtRuntime,
|
||||||
|
compileSonioxTranscripts,
|
||||||
|
consolidateTranscripts
|
||||||
|
} = require('../utils/transcription-utils')(logger);
|
||||||
|
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||||
|
this.normalizeTranscription = normalizeTranscription;
|
||||||
|
this.compileSonioxTranscripts = compileSonioxTranscripts;
|
||||||
|
this.consolidateTranscripts = consolidateTranscripts;
|
||||||
|
this.eventHandlers = [];
|
||||||
|
this.isHandledByPrimaryProvider = true;
|
||||||
|
if (this.data.recognizer) {
|
||||||
|
const recognizer = this.data.recognizer;
|
||||||
|
this.vendor = recognizer.vendor;
|
||||||
|
this.language = recognizer.language;
|
||||||
|
this.label = recognizer.label;
|
||||||
|
|
||||||
|
//fallback
|
||||||
|
this.fallbackVendor = recognizer.fallbackVendor || 'default';
|
||||||
|
this.fallbackLanguage = recognizer.fallbackLanguage || 'default';
|
||||||
|
this.fallbackLabel = recognizer.fallbackLabel || 'default';
|
||||||
|
|
||||||
|
/* let credentials be supplied in the recognizer object at runtime */
|
||||||
|
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||||
|
|
||||||
|
if (!Array.isArray(this.data.recognizer.altLanguages)) {
|
||||||
|
this.data.recognizer.altLanguages = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.data.recognizer = {hints: [], altLanguages: []};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buffer for soniox transcripts */
|
||||||
|
this._sonioxTranscripts = [];
|
||||||
|
/*bug name prefix */
|
||||||
|
this.bugname_prefix = '';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(cs, {ep, ep2}) {
|
||||||
|
super.exec(cs);
|
||||||
|
this.ep = ep;
|
||||||
|
this.ep2 = ep2;
|
||||||
|
|
||||||
|
// use session preferences if we don't have specific verb-level settings.
|
||||||
|
if (cs.recognizer) {
|
||||||
|
for (const k in cs.recognizer) {
|
||||||
|
const newValue = this.data.recognizer && this.data.recognizer[k] !== undefined ?
|
||||||
|
this.data.recognizer[k] :
|
||||||
|
cs.recognizer[k];
|
||||||
|
|
||||||
|
if (Array.isArray(newValue)) {
|
||||||
|
this.data.recognizer[k] = [...(this.data.recognizer[k] || []), ...cs.recognizer[k]];
|
||||||
|
} else if (typeof newValue === 'object' && newValue !== null) {
|
||||||
|
this.data.recognizer[k] = { ...(this.data.recognizer[k] || {}), ...cs.recognizer[k] };
|
||||||
|
} else {
|
||||||
|
this.data.recognizer[k] = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('default' === this.vendor || !this.vendor) {
|
||||||
|
this.vendor = cs.speechRecognizerVendor;
|
||||||
|
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor;
|
||||||
|
}
|
||||||
|
if ('default' === this.language || !this.language) {
|
||||||
|
this.language = cs.speechRecognizerLanguage;
|
||||||
|
if (this.data.recognizer) this.data.recognizer.language = this.language;
|
||||||
|
}
|
||||||
|
if ('default' === this.label || !this.label) {
|
||||||
|
this.label = cs.speechRecognizerLabel;
|
||||||
|
if (this.data.recognizer) this.data.recognizer.label = this.label;
|
||||||
|
}
|
||||||
|
// Fallback options
|
||||||
|
if ('default' === this.fallbackVendor || !this.fallbackVendor) {
|
||||||
|
this.fallbackVendor = cs.fallbackSpeechRecognizerVendor;
|
||||||
|
if (this.data.recognizer) this.data.recognizer.fallbackVendor = this.fallbackVendor;
|
||||||
|
}
|
||||||
|
if ('default' === this.fallbackLanguage || !this.fallbackLanguage) {
|
||||||
|
this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage;
|
||||||
|
if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage;
|
||||||
|
}
|
||||||
|
if ('default' === this.fallbackLabel || !this.fallbackLabel) {
|
||||||
|
this.fallbackLabel = cs.fallbackSpeechRecognizerLabel;
|
||||||
|
if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel;
|
||||||
|
}
|
||||||
|
if (!this.data.recognizer.vendor) {
|
||||||
|
this.data.recognizer.vendor = this.vendor;
|
||||||
|
}
|
||||||
|
if (this.vendor === 'cobalt' && !this.data.recognizer.model) {
|
||||||
|
// By default, application saves cobalt model in language
|
||||||
|
this.data.recognizer.model = cs.speechRecognizerLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
// not gather task, such as transcribe
|
||||||
|
(!this.input ||
|
||||||
|
// gather task with speech
|
||||||
|
this.input.includes('speech')) &&
|
||||||
|
!this.sttCredentials) {
|
||||||
|
try {
|
||||||
|
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||||
|
await this._fallback();
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* when using cobalt model is required */
|
||||||
|
if (this.vendor === 'cobalt' && !this.data.recognizer.model) {
|
||||||
|
this.notifyError({ msg: 'ASR error', details:'Cobalt requires a model to be specified'});
|
||||||
|
throw new Error('Cobalt requires a model to be specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cs.hasAltLanguages) {
|
||||||
|
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
|
||||||
|
this.logger.debug({altLanguages: this.altLanguages},
|
||||||
|
'STT:exec - applying altLanguages');
|
||||||
|
}
|
||||||
|
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
||||||
|
this.data.recognizer.punctuation = cs.globalSttPunctuation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCustomEventListener(ep, event, handler) {
|
||||||
|
this.eventHandlers.push({ep, event, handler});
|
||||||
|
ep.addCustomEventListener(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCustomEventListeners() {
|
||||||
|
this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initSpeechCredentials(cs, vendor, label) {
|
||||||
|
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
|
||||||
|
let credentials = cs.getSpeechCredentials(vendor, 'stt', label);
|
||||||
|
|
||||||
|
if (!credentials) {
|
||||||
|
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||||
|
this.logger.info(`ERROR stt using ${vendor} requested but creds not supplied`);
|
||||||
|
writeAlerts({
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||||
|
vendor
|
||||||
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||||
|
// Notify application that STT vender is wrong.
|
||||||
|
this.notifyError({
|
||||||
|
msg: 'ASR error',
|
||||||
|
details: `No speech-to-text service credentials for ${vendor} have been configured`
|
||||||
|
});
|
||||||
|
this.notifyTaskDone();
|
||||||
|
throw new Error(`No speech-to-text service credentials for ${vendor} have been configured`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vendor === 'nuance' && credentials.client_id) {
|
||||||
|
/* get nuance access token */
|
||||||
|
const {client_id, secret} = credentials;
|
||||||
|
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
||||||
|
this.logger.debug({client_id}, `got nuance access token ${servedFromCache ? 'from cache' : ''}`);
|
||||||
|
credentials = {...credentials, access_token};
|
||||||
|
}
|
||||||
|
else if (vendor == 'ibm' && credentials.stt_api_key) {
|
||||||
|
/* get ibm access token */
|
||||||
|
const {stt_api_key, stt_region} = credentials;
|
||||||
|
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
|
||||||
|
this.logger.debug({stt_api_key}, `got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||||
|
credentials = {...credentials, access_token, stt_region};
|
||||||
|
}
|
||||||
|
return credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fallback() {
|
||||||
|
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
|
||||||
|
this.isHandledByPrimaryProvider = false;
|
||||||
|
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
|
||||||
|
this.vendor = this.fallbackVendor;
|
||||||
|
this.language = this.fallbackLanguage;
|
||||||
|
this.label = this.fallbackLabel;
|
||||||
|
this.data.recognizer.vendor = this.vendor;
|
||||||
|
this.data.recognizer.language = this.language;
|
||||||
|
this.data.recognizer.label = this.label;
|
||||||
|
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
async compileHintsForCobalt(ep, hostport, model, token, hints) {
|
||||||
|
const {retrieveKey} = this.cs.srf.locals.dbHelpers;
|
||||||
|
const hash = crypto.createHash('sha1');
|
||||||
|
hash.update(`${model}:${hints}`);
|
||||||
|
const key = `cobalt:${hash.digest('hex')}`;
|
||||||
|
this.context = await retrieveKey(key);
|
||||||
|
if (this.context) {
|
||||||
|
this.logger.debug({model, hints}, 'found cached cobalt context for supplied hints');
|
||||||
|
return this.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug({model, hints}, 'compiling cobalt context for supplied hints');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.cobaltCompileResolver = resolve;
|
||||||
|
ep.addCustomEventListener(CobaltTranscriptionEvents.CompileContext, this._onCompileContext.bind(this, ep, key));
|
||||||
|
ep.api('uuid_cobalt_compile_context', [ep.uuid, hostport, model, token, hints], (err, evt) => {
|
||||||
|
if (err || 0 !== evt.getBody().indexOf('+OK')) {
|
||||||
|
ep.removeCustomEventListener(CobaltTranscriptionEvents.CompileContext);
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCompileContext(ep, key, evt) {
|
||||||
|
const {addKey} = this.cs.srf.locals.dbHelpers;
|
||||||
|
this.logger.debug({evt}, `received cobalt compile context event, will cache under ${key}`);
|
||||||
|
|
||||||
|
this.cobaltCompileResolver(evt.compiled_context);
|
||||||
|
ep.removeCustomEventListener(CobaltTranscriptionEvents.CompileContext);
|
||||||
|
this.cobaltCompileResolver = null;
|
||||||
|
|
||||||
|
//cache the compiled context
|
||||||
|
addKey(key, evt.compiled_context, 3600 * 12)
|
||||||
|
.catch((err) => this.logger.info({err}, `Error caching cobalt context for ${key}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
_doContinuousAsrWithDeepgram(asrTimeout) {
|
||||||
|
/* deepgram has an utterance_end_ms property that simplifies things */
|
||||||
|
assert(this.vendor === 'deepgram');
|
||||||
|
this.logger.debug(`_doContinuousAsrWithDeepgram - setting utterance_end_ms to ${asrTimeout}`);
|
||||||
|
const dgOptions = this.data.recognizer.deepgramOptions = this.data.recognizer.deepgramOptions || {};
|
||||||
|
dgOptions.utteranceEndMs = dgOptions.utteranceEndMs || asrTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onVendorConnect(_cs, _ep) {
|
||||||
|
this.logger.debug(`TaskGather:_on${this.vendor}Connect`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onVendorError(cs, _ep, evt) {
|
||||||
|
this.logger.info({evt}, `${this.name}:_on${this.vendor}Error`);
|
||||||
|
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||||
|
writeAlerts({
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
alert_type: AlertType.STT_FAILURE,
|
||||||
|
message: 'STT failure reported by vendor',
|
||||||
|
detail: evt.error,
|
||||||
|
vendor: this.vendor,
|
||||||
|
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
|
||||||
|
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${evt.error}`});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onVendorConnectFailure(cs, _ep, evt) {
|
||||||
|
const {reason} = evt;
|
||||||
|
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||||
|
this.logger.info({evt}, `${this.name}:_on${this.vendor}ConnectFailure`);
|
||||||
|
writeAlerts({
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
alert_type: AlertType.STT_FAILURE,
|
||||||
|
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
|
||||||
|
vendor: this.vendor,
|
||||||
|
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
|
||||||
|
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SttTask;
|
||||||
@@ -12,7 +12,7 @@ class TaskTag extends Task {
|
|||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
super.exec(cs);
|
super.exec(cs);
|
||||||
cs.callInfo.customerData = this.data;
|
cs.callInfo.customerData = this.data;
|
||||||
//this.logger.debug({callInfo: cs.callInfo.toJSON()}, 'TaskTag:exec set customer data in callInfo');
|
this.logger.debug({customerData: cs.callInfo.customerData}, 'TaskTag:exec set customer data in callInfo');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const uuidv4 = require('uuid-random');
|
const uuidv4 = require('uuid-random');
|
||||||
const debug = require('debug')('jambonz:feature-server');
|
|
||||||
const assert = require('assert');
|
|
||||||
const {TaskPreconditions} = require('../utils/constants');
|
const {TaskPreconditions} = require('../utils/constants');
|
||||||
const normalizeJambones = require('../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
const WsRequestor = require('../utils/ws-requestor');
|
||||||
|
const {TaskName} = require('../utils/constants');
|
||||||
const {trace} = require('@opentelemetry/api');
|
const {trace} = require('@opentelemetry/api');
|
||||||
const specs = new Map();
|
|
||||||
const _specData = require('./specs');
|
|
||||||
for (const key in _specData) {specs.set(key, _specData[key]);}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @classdesc Represents a jambonz verb. This is a superclass that is extended
|
* @classdesc Represents a jambonz verb. This is a superclass that is extended
|
||||||
@@ -21,6 +18,7 @@ class Task extends Emitter {
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.actionHook = this.data.actionHook;
|
this.actionHook = this.data.actionHook;
|
||||||
|
this.id = data.id;
|
||||||
|
|
||||||
this._killInProgress = false;
|
this._killInProgress = false;
|
||||||
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
||||||
@@ -137,28 +135,56 @@ class Task extends Emitter {
|
|||||||
return this.callSession.normalizeUrl(url, method, auth);
|
return this.callSession.normalizeUrl(url, method, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyError(errMsg) {
|
notifyError(obj) {
|
||||||
const params = {error: errMsg, verb: this.name};
|
if (this.cs.requestor instanceof WsRequestor) {
|
||||||
this.cs.requestor.request('jambonz:error', '/error', params)
|
const params = {...obj, verb: this.name, id: this.id};
|
||||||
.catch((err) => this.logger.info({err}, 'Task:notifyError error sending error'));
|
this.cs.requestor.request('jambonz:error', '/error', params)
|
||||||
|
.catch((err) => this.logger.info({err}, 'Task:notifyError error sending error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyStatus(obj) {
|
||||||
|
if (this.cs.notifyEvents && this.cs.requestor instanceof WsRequestor) {
|
||||||
|
const params = {...obj, verb: this.name, id: this.id};
|
||||||
|
this.cs.requestor.request('verb:status', '/status', params)
|
||||||
|
.catch((err) => this.logger.info({err}, 'Task:notifyStatus error sending error'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async performAction(results, expectResponse = true) {
|
async performAction(results, expectResponse = true) {
|
||||||
if (this.actionHook) {
|
if (this.actionHook) {
|
||||||
|
const type = this.name === TaskName.Redirect ? 'session:redirect' : 'verb:hook';
|
||||||
const params = results ? Object.assign(this.cs.callInfo.toJSON(), results) : this.cs.callInfo.toJSON();
|
const params = results ? Object.assign(this.cs.callInfo.toJSON(), results) : this.cs.callInfo.toJSON();
|
||||||
const span = this.startSpan('verb:hook', {'hook.url': this.actionHook});
|
const span = this.startSpan(`${type} (${this.actionHook})`);
|
||||||
const b3 = this.getTracingPropagation('b3', span);
|
const b3 = this.getTracingPropagation('b3', span);
|
||||||
const httpHeaders = b3 && {b3};
|
const httpHeaders = b3 && {b3};
|
||||||
span.setAttributes({'http.body': JSON.stringify(params)});
|
span.setAttributes({'http.body': JSON.stringify(params)});
|
||||||
try {
|
try {
|
||||||
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params, httpHeaders);
|
if (this.id) params.verb_id = this.id;
|
||||||
|
const json = await this.cs.requestor.request(type, this.actionHook, params, httpHeaders);
|
||||||
span.setAttributes({'http.statusCode': 200});
|
span.setAttributes({'http.statusCode': 200});
|
||||||
span.end();
|
const isWsConnection = this.cs.requestor instanceof WsRequestor;
|
||||||
|
if (!isWsConnection || (expectResponse && json && Array.isArray(json) && json.length)) {
|
||||||
|
span.end();
|
||||||
|
} else {
|
||||||
|
/** we use this span to measure application response latency,
|
||||||
|
* and with websocket connections we generally get the application's response
|
||||||
|
* in a subsequent message from the far end, so we terminate the span when the
|
||||||
|
* first new set of verbs arrive after sending a transcript
|
||||||
|
* */
|
||||||
|
this.emit('VerbHookSpanWaitForEnd', {span});
|
||||||
|
|
||||||
|
// If actionHook delay action is configured, and ws application have not responded yet any verb for actionHook
|
||||||
|
// We have to transfer the task to call-session to await on next ws command verbs, and also run action Hook
|
||||||
|
// delay actions
|
||||||
|
if (this.hookDelayActionOpts) {
|
||||||
|
this.emit('ActionHookDelayActionOptions', this.hookDelayActionOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (expectResponse && json && Array.isArray(json)) {
|
if (expectResponse && json && Array.isArray(json)) {
|
||||||
const makeTask = require('./make_task');
|
const makeTask = require('./make_task');
|
||||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||||
if (tasks && tasks.length > 0) {
|
if (tasks && tasks.length > 0) {
|
||||||
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
|
||||||
this.callSession.replaceApplication(tasks);
|
this.callSession.replaceApplication(tasks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,77 +299,6 @@ class Task extends Emitter {
|
|||||||
this.logger.error(err, 'Task:_doRefer error');
|
this.logger.error(err, 'Task:_doRefer error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* validate that the JSON task description is valid
|
|
||||||
* @param {string} name - verb name
|
|
||||||
* @param {object} data - verb properties
|
|
||||||
*/
|
|
||||||
static validate(name, data) {
|
|
||||||
debug(`validating ${name} with data ${JSON.stringify(data)}`);
|
|
||||||
// validate the instruction is supported
|
|
||||||
if (!specs.has(name)) throw new Error(`invalid instruction: ${name}`);
|
|
||||||
|
|
||||||
// check type of each element and make sure required elements are present
|
|
||||||
const specData = specs.get(name);
|
|
||||||
let required = specData.required || [];
|
|
||||||
for (const dKey in data) {
|
|
||||||
if (dKey in specData.properties) {
|
|
||||||
const dVal = data[dKey];
|
|
||||||
const dSpec = specData.properties[dKey];
|
|
||||||
debug(`Task:validate validating property ${dKey} with value ${JSON.stringify(dVal)}`);
|
|
||||||
|
|
||||||
if (typeof dSpec === 'string' && dSpec === 'array') {
|
|
||||||
if (!Array.isArray(dVal)) throw new Error(`${name}: property ${dKey} is not an array`);
|
|
||||||
}
|
|
||||||
else if (typeof dSpec === 'string' && dSpec.includes('|')) {
|
|
||||||
const types = dSpec.split('|').map((t) => t.trim());
|
|
||||||
if (!types.includes(typeof dVal) && !(types.includes('array') && Array.isArray(dVal))) {
|
|
||||||
throw new Error(`${name}: property ${dKey} has invalid data type, must be one of ${types}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (typeof dSpec === 'string' && ['number', 'string', 'object', 'boolean'].includes(dSpec)) {
|
|
||||||
// simple types
|
|
||||||
if (typeof dVal !== specData.properties[dKey]) {
|
|
||||||
throw new Error(`${name}: property ${dKey} has invalid data type`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) {
|
|
||||||
const name = dSpec[0].slice(1);
|
|
||||||
for (const item of dVal) {
|
|
||||||
Task.validate(name, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (typeof dSpec === 'object') {
|
|
||||||
// complex types
|
|
||||||
const type = dSpec.type;
|
|
||||||
assert.ok(['number', 'string', 'object', 'boolean'].includes(type),
|
|
||||||
`invalid or missing type in spec ${JSON.stringify(dSpec)}`);
|
|
||||||
if (type === 'string' && dSpec.enum) {
|
|
||||||
assert.ok(Array.isArray(dSpec.enum), `enum must be an array ${JSON.stringify(dSpec.enum)}`);
|
|
||||||
if (!dSpec.enum.includes(dVal)) throw new Error(`invalid value ${dVal} must be one of ${dSpec.enum}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (typeof dSpec === 'string' && dSpec.startsWith('#')) {
|
|
||||||
// reference to another datatype (i.e. nested type)
|
|
||||||
const name = dSpec.slice(1);
|
|
||||||
//const obj = {};
|
|
||||||
//obj[name] = dVal;
|
|
||||||
Task.validate(name, dVal);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
assert.ok(0, `invalid spec ${JSON.stringify(dSpec)}`);
|
|
||||||
}
|
|
||||||
required = required.filter((item) => item !== dKey);
|
|
||||||
}
|
|
||||||
else if (dKey === '_') {
|
|
||||||
/* no op: allow arbitrary info to be carried here, used by conference e.g in transfer */
|
|
||||||
}
|
|
||||||
else throw new Error(`${name}: unknown property ${dKey}`);
|
|
||||||
}
|
|
||||||
if (required.length > 0) throw new Error(`${name}: missing value for ${required}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Task;
|
module.exports = Task;
|
||||||
|
|||||||
@@ -1,104 +1,104 @@
|
|||||||
const Task = require('./task');
|
const assert = require('assert');
|
||||||
const {
|
const {
|
||||||
TaskName,
|
TaskName,
|
||||||
TaskPreconditions,
|
|
||||||
GoogleTranscriptionEvents,
|
GoogleTranscriptionEvents,
|
||||||
|
NuanceTranscriptionEvents,
|
||||||
|
AwsTranscriptionEvents,
|
||||||
AzureTranscriptionEvents,
|
AzureTranscriptionEvents,
|
||||||
AwsTranscriptionEvents
|
DeepgramTranscriptionEvents,
|
||||||
} = require('../utils/constants');
|
SonioxTranscriptionEvents,
|
||||||
|
CobaltTranscriptionEvents,
|
||||||
|
IbmTranscriptionEvents,
|
||||||
|
NvidiaTranscriptionEvents,
|
||||||
|
JambonzTranscriptionEvents,
|
||||||
|
TranscribeStatus,
|
||||||
|
AssemblyAiTranscriptionEvents
|
||||||
|
} = require('../utils/constants.json');
|
||||||
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
const SttTask = require('./stt-task');
|
||||||
|
|
||||||
class TaskTranscribe extends Task {
|
const STT_LISTEN_SPAN_NAME = 'stt-listen';
|
||||||
|
|
||||||
|
class TaskTranscribe extends SttTask {
|
||||||
constructor(logger, opts, parentTask) {
|
constructor(logger, opts, parentTask) {
|
||||||
super(logger, opts);
|
super(logger, opts, parentTask);
|
||||||
this.preconditions = TaskPreconditions.Endpoint;
|
|
||||||
this.parentTask = parentTask;
|
|
||||||
|
|
||||||
this.transcriptionHook = this.data.transcriptionHook;
|
this.transcriptionHook = this.data.transcriptionHook;
|
||||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||||
|
|
||||||
const recognizer = this.data.recognizer;
|
if (this.data.recognizer) {
|
||||||
this.vendor = recognizer.vendor;
|
this.interim = !!this.data.recognizer.interim;
|
||||||
this.language = recognizer.language;
|
this.separateRecognitionPerChannel = this.data.recognizer.separateRecognitionPerChannel;
|
||||||
this.interim = !!recognizer.interim;
|
}
|
||||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
|
||||||
|
|
||||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
/* for nested transcribe in dial, unless the app explicitly says so we want to transcribe both legs */
|
||||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
if (this.parentTask?.name === TaskName.Dial) {
|
||||||
this.vad = {enable, voiceMs, mode};
|
if (this.data.channel === 1 || this.data.channel === 2) {
|
||||||
|
/* transcribe only the channel specified */
|
||||||
|
this.separateRecognitionPerChannel = false;
|
||||||
|
this.channel = this.data.channel;
|
||||||
|
logger.debug(`TaskTranscribe: transcribing only channel ${this.channel} in the Dial verb`);
|
||||||
|
}
|
||||||
|
else if (this.separateRecognitionPerChannel !== false) {
|
||||||
|
this.separateRecognitionPerChannel = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.channel = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.channel = 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* google-specific options */
|
this.childSpan = [null, null];
|
||||||
this.hints = recognizer.hints || [];
|
|
||||||
this.hintsBoost = recognizer.hintsBoost;
|
|
||||||
this.profanityFilter = recognizer.profanityFilter;
|
|
||||||
this.punctuation = !!recognizer.punctuation;
|
|
||||||
this.enhancedModel = !!recognizer.enhancedModel;
|
|
||||||
this.model = recognizer.model || 'phone_call';
|
|
||||||
this.words = !!recognizer.words;
|
|
||||||
this.singleUtterance = recognizer.singleUtterance || false;
|
|
||||||
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 */
|
// Continuous asr timeout
|
||||||
this.identifyChannels = !!recognizer.identifyChannels;
|
this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ? this.data.recognizer.asrTimeout * 1000 : 0;
|
||||||
this.vocabularyName = recognizer.vocabularyName;
|
if (this.asrTimeout > 0) {
|
||||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
this.isContinuousAsr = true;
|
||||||
this.filterMethod = recognizer.filterMethod;
|
}
|
||||||
|
/* buffer speech for continuous asr */
|
||||||
/* microsoft options */
|
this._bufferedTranscripts = [ [], [] ]; // for channel 1 and 2
|
||||||
this.outputFormat = recognizer.outputFormat || 'simple';
|
this.bugname_prefix = 'transcribe_';
|
||||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
this.paused = false;
|
||||||
this.requestSnr = recognizer.requestSnr || false;
|
|
||||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
|
||||||
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
|
|
||||||
this.azureSttEndpointId = recognizer.azureSttEndpointId;
|
|
||||||
this.azureAudioLogging = recognizer.audioLogging;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Transcribe; }
|
get name() { return TaskName.Transcribe; }
|
||||||
|
|
||||||
|
get transcribing1() {
|
||||||
|
return this.channel === 1 || this.separateRecognitionPerChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
get transcribing2() {
|
||||||
|
return this.channel === 2 || this.separateRecognitionPerChannel && this.ep2;
|
||||||
|
}
|
||||||
|
|
||||||
async exec(cs, {ep, ep2}) {
|
async exec(cs, {ep, ep2}) {
|
||||||
super.exec(cs);
|
await super.exec(cs, {ep, ep2});
|
||||||
|
|
||||||
|
if (this.data.recognizer.vendor === 'nuance') {
|
||||||
|
this.data.recognizer.nuanceOptions = {
|
||||||
|
// by default, nuance STT will recognize only 1st utterance.
|
||||||
|
// enable multiple allow nuance detact all utterances
|
||||||
|
utteranceDetectionMode: 'multiple',
|
||||||
|
...this.data.recognizer.nuanceOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||||
|
|
||||||
if (cs.hasGlobalSttHints) {
|
if (cs.hasGlobalSttHints) {
|
||||||
const {hints, hintsBoost} = cs.globalSttHints;
|
const {hints, hintsBoost} = cs.globalSttHints;
|
||||||
this.hints = this.hints.concat(hints);
|
this.data.recognizer.hints = this.data.recognizer.hints.concat(hints);
|
||||||
if (!this.hintsBoost && hintsBoost) this.hintsBoost = hintsBoost;
|
if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost;
|
||||||
this.logger.debug({hints: this.hints, hintsBoost: this.hintsBoost},
|
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
|
||||||
'Transcribe:exec - applying global `sttHints');
|
'Transcribe:exec - applying global sttHints');
|
||||||
}
|
}
|
||||||
if (cs.hasAltLanguages) {
|
|
||||||
this.altLanguages = this.altLanguages.concat(cs.altLanguages);
|
|
||||||
this.logger.debug({altLanguages: this.altLanguages},
|
|
||||||
'Gather:exec - applying altLanguages');
|
|
||||||
}
|
|
||||||
if (cs.hasGlobalSttPunctuation) {
|
|
||||||
this.punctuation = cs.globalSttPunctuation;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ep = ep;
|
|
||||||
this.ep2 = ep2;
|
|
||||||
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 {
|
try {
|
||||||
if (!this.sttCredentials) {
|
if (this.transcribing1) {
|
||||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
await this._startTranscribing(cs, ep, 1);
|
||||||
this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not 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 provisioned speech credentials for TTS');
|
|
||||||
}
|
}
|
||||||
await this._startTranscribing(cs, ep, 1);
|
if (this.transcribing2) {
|
||||||
if (this.separateRecognitionPerChannel && ep2) {
|
|
||||||
await this._startTranscribing(cs, ep2, 2);
|
await this._startTranscribing(cs, ep2, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,29 +110,31 @@ class TaskTranscribe extends Task {
|
|||||||
this.logger.info(err, 'TaskTranscribe:exec - error');
|
this.logger.info(err, 'TaskTranscribe:exec - error');
|
||||||
this.parentTask && this.parentTask.emit('error', err);
|
this.parentTask && this.parentTask.emit('error', err);
|
||||||
}
|
}
|
||||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
|
this.removeCustomEventListeners();
|
||||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected);
|
}
|
||||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded);
|
|
||||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
async _stopTranscription() {
|
||||||
ep.removeCustomEventListener(AwsTranscriptionEvents.NoAudioDetected);
|
let stopTranscription = false;
|
||||||
ep.removeCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded);
|
if (this.transcribing1 && this.ep?.connected) {
|
||||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
stopTranscription = true;
|
||||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
this.ep.stopTranscription({
|
||||||
|
vendor: this.vendor,
|
||||||
|
bugname: this.bugname
|
||||||
|
})
|
||||||
|
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||||
|
}
|
||||||
|
if (this.transcribing2 && this.ep2.connected) {
|
||||||
|
stopTranscription = true;
|
||||||
|
this.ep2.stopTranscription({vendor: this.vendor, bugname: this.bugname})
|
||||||
|
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return stopTranscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill(cs) {
|
async kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
let stopTranscription = false;
|
const stopTranscription = this._stopTranscription();
|
||||||
if (this.ep?.connected) {
|
|
||||||
stopTranscription = true;
|
|
||||||
this.ep.stopTranscription({vendor: this.vendor})
|
|
||||||
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
|
||||||
}
|
|
||||||
if (this.separateRecognitionPerChannel && this.ep2 && this.ep2.connected) {
|
|
||||||
stopTranscription = true;
|
|
||||||
this.ep2.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
|
// hangup after 1 sec if we don't get a final transcription
|
||||||
if (stopTranscription) this._timer = setTimeout(() => this.notifyTaskDone(), 1500);
|
if (stopTranscription) this._timer = setTimeout(() => this.notifyTaskDone(), 1500);
|
||||||
else this.notifyTaskDone();
|
else this.notifyTaskDone();
|
||||||
@@ -140,183 +142,340 @@ class TaskTranscribe extends Task {
|
|||||||
await this.awaitTaskDone();
|
await this.awaitTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _startTranscribing(cs, ep, channel) {
|
async updateTranscribe(status) {
|
||||||
const opts = {};
|
if (!this.killed && this.ep && this.ep.connected) {
|
||||||
|
this.logger.info(`TaskTranscribe:updateTranscribe status ${status}`);
|
||||||
|
switch (status) {
|
||||||
|
case TranscribeStatus.Pause:
|
||||||
|
this.paused = true;
|
||||||
|
await this._stopTranscription();
|
||||||
|
break;
|
||||||
|
case TranscribeStatus.Resume:
|
||||||
|
this.paused = false;
|
||||||
|
if (this.transcribing1) await this._startTranscribing(this.cs, this.ep, 1);
|
||||||
|
if (this.transcribing2) await this._startTranscribing(this.cs, this.ep2, 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.vad.enable) {
|
async _setSpeechHandlers(cs, ep, channel) {
|
||||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
if (this[`_speechHandlersSet_${channel}`]) return;
|
||||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
this[`_speechHandlersSet_${channel}`] = true;
|
||||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
|
||||||
|
/* some special deepgram logic */
|
||||||
|
if (this.vendor === 'deepgram') {
|
||||||
|
if (this.isContinuousAsr) this._doContinuousAsrWithDeepgram(this.asrTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
|
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.language, this.data.recognizer);
|
||||||
this._onTranscription.bind(this, cs, ep, channel));
|
switch (this.vendor) {
|
||||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
case 'google':
|
||||||
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
|
this.bugname = `${this.bugname_prefix}google_transcribe`;
|
||||||
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
this.addCustomEventListener(ep, GoogleTranscriptionEvents.Transcription,
|
||||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep, channel));
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
this.addCustomEventListener(ep, GoogleTranscriptionEvents.NoAudioDetected,
|
||||||
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
|
this._onNoAudio.bind(this, cs, ep, channel));
|
||||||
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
this.addCustomEventListener(ep, GoogleTranscriptionEvents.MaxDurationExceeded,
|
||||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
|
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
||||||
this._onTranscription.bind(this, cs, ep, channel));
|
break;
|
||||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
|
||||||
|
|
||||||
if (this.vendor === 'google') {
|
case 'aws':
|
||||||
this.bugname = 'google_transcribe';
|
case 'polly':
|
||||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
this.bugname = `${this.bugname_prefix}aws_transcribe`;
|
||||||
[
|
this.addCustomEventListener(ep, AwsTranscriptionEvents.Transcription,
|
||||||
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
//['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
|
this.addCustomEventListener(ep, AwsTranscriptionEvents.NoAudioDetected,
|
||||||
['profanityFilter', 'GOOGLE_SPEECH_PROFANITY_FILTER'],
|
this._onNoAudio.bind(this, cs, ep, channel));
|
||||||
['punctuation', 'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION'],
|
this.addCustomEventListener(ep, AwsTranscriptionEvents.MaxDurationExceeded,
|
||||||
['words', 'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS'],
|
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
||||||
['singleUtterance', 'GOOGLE_SPEECH_SINGLE_UTTERANCE'],
|
break;
|
||||||
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
|
case 'microsoft':
|
||||||
].forEach((arr) => {
|
this.bugname = `${this.bugname_prefix}azure_transcribe`;
|
||||||
if (this[arr[0]]) opts[arr[1]] = true;
|
this.addCustomEventListener(ep, AzureTranscriptionEvents.Transcription,
|
||||||
else if (this[arr[0]] === false) opts[arr[1]] = false;
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
});
|
this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected,
|
||||||
if (this.hints.length > 0) {
|
this._onNoAudio.bind(this, cs, ep, channel));
|
||||||
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
break;
|
||||||
if (typeof this.hintsBoost === 'number') {
|
case 'nuance':
|
||||||
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
|
this.bugname = `${this.bugname_prefix}nuance_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, NuanceTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
break;
|
||||||
|
case 'deepgram':
|
||||||
|
this.bugname = `${this.bugname_prefix}deepgram_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Connect,
|
||||||
|
this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
|
|
||||||
|
/* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */
|
||||||
|
if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'soniox':
|
||||||
|
this.bugname = `${this.bugname_prefix}soniox_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, SonioxTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
break;
|
||||||
|
case 'cobalt':
|
||||||
|
this.bugname = `${this.bugname_prefix}cobalt_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, CobaltTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
|
||||||
|
/* cobalt doesnt have language, it has model, which is required */
|
||||||
|
if (!this.data.recognizer.model) {
|
||||||
|
throw new Error('Cobalt requires a model to be specified');
|
||||||
}
|
}
|
||||||
}
|
this.language = this.data.recognizer.model;
|
||||||
if (this.altLanguages.length > 0) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
|
||||||
else opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
|
|
||||||
if ('unspecified' !== this.interactionType) {
|
|
||||||
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
|
|
||||||
}
|
|
||||||
opts.GOOGLE_SPEECH_MODEL = this.model;
|
|
||||||
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)
|
/* special case: if using hints with cobalt we need to compile them */
|
||||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with google'));
|
this.hostport = opts.COBALT_SERVER_URI;
|
||||||
|
if (this.vendor === 'cobalt' && opts.COBALT_SPEECH_HINTS) {
|
||||||
|
try {
|
||||||
|
const context = await this.compileHintsForCobalt(
|
||||||
|
ep,
|
||||||
|
opts.COBALT_SERVER_URI,
|
||||||
|
this.data.recognizer.model,
|
||||||
|
opts.COBALT_CONTEXT_TOKEN,
|
||||||
|
opts.COBALT_SPEECH_HINTS
|
||||||
|
);
|
||||||
|
if (context) opts.COBALT_COMPILED_CONTEXT_DATA = context;
|
||||||
|
delete opts.COBALT_SPEECH_HINTS;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, 'Error compiling hints for cobalt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ibm':
|
||||||
|
this.bugname = `${this.bugname_prefix}ibm_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, IbmTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, IbmTranscriptionEvents.Connect,
|
||||||
|
this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, IbmTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nvidia':
|
||||||
|
this.bugname = `${this.bugname_prefix}nvidia_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'assemblyai':
|
||||||
|
this.bugname = `${this.bugname_prefix}assemblyai_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep,
|
||||||
|
AssemblyAiTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (this.vendor.startsWith('custom:')) {
|
||||||
|
this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, JambonzTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, JambonzTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, JambonzTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`});
|
||||||
|
this.notifyTaskDone();
|
||||||
|
throw new Error(`Invalid vendor ${this.vendor}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (this.vendor === 'aws') {
|
|
||||||
this.bugname = 'aws_transcribe';
|
|
||||||
[
|
|
||||||
['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) {
|
/* common handler for all stt engine errors */
|
||||||
Object.assign(opts, {
|
this.addCustomEventListener(ep, JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep));
|
||||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
await ep.set(opts)
|
||||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||||
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)
|
async _startTranscribing(cs, ep, channel) {
|
||||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
|
await this._setSpeechHandlers(cs, ep, channel);
|
||||||
}
|
|
||||||
else if (this.vendor === 'microsoft') {
|
|
||||||
this.bugname = 'azure_transcribe';
|
|
||||||
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
|
|
||||||
Object.assign(opts, {
|
|
||||||
'AZURE_SUBSCRIPTION_KEY': api_key,
|
|
||||||
'AZURE_REGION': region
|
|
||||||
});
|
|
||||||
if (this.azureSttEndpointId) {
|
|
||||||
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
|
|
||||||
}
|
|
||||||
else if (use_custom_stt && custom_stt_endpoint) {
|
|
||||||
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
|
|
||||||
}
|
|
||||||
if (this.hints && this.hints.length > 0) {
|
|
||||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
|
||||||
}
|
|
||||||
if (this.altLanguages.length > 0) opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
|
||||||
else opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
|
|
||||||
if (this.azureAudioLogging) opts.AZURE_AUDIO_LOGGING = 1;
|
|
||||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
|
||||||
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
|
||||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
|
||||||
if (this.outputFormat !== 'simple') opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
|
||||||
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
|
|
||||||
|
|
||||||
await ep.set(opts)
|
|
||||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with azure'));
|
|
||||||
}
|
|
||||||
await this._transcribe(ep);
|
await this._transcribe(ep);
|
||||||
|
|
||||||
|
/* start child span for this channel */
|
||||||
|
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||||
|
this.childSpan[channel - 1] = {span, ctx};
|
||||||
}
|
}
|
||||||
|
|
||||||
async _transcribe(ep) {
|
async _transcribe(ep) {
|
||||||
|
this.logger.debug(
|
||||||
|
`TaskTranscribe:_transcribe - starting transcription vendor ${this.vendor} bugname ${this.bugname}`);
|
||||||
await ep.startTranscription({
|
await ep.startTranscription({
|
||||||
vendor: this.vendor,
|
vendor: this.vendor,
|
||||||
interim: this.interim ? true : false,
|
interim: this.interim ? true : false,
|
||||||
locale: this.language,
|
locale: this.language,
|
||||||
channels: /*this.separateRecognitionPerChannel ? 2 : */ 1,
|
channels: 1,
|
||||||
bugname: this.bugname
|
bugname: this.bugname,
|
||||||
|
hostport: this.hostport
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onTranscription(cs, ep, channel, evt, fsEvent) {
|
async _onTranscription(cs, ep, channel, evt, fsEvent) {
|
||||||
// make sure this is not a transcript from answering machine detection
|
// make sure this is not a transcript from answering machine detection
|
||||||
const bugname = fsEvent.getHeader('media-bugname');
|
const bugname = fsEvent.getHeader('media-bugname');
|
||||||
|
const finished = fsEvent.getHeader('transcription-session-finished');
|
||||||
|
const bufferedTranscripts = this._bufferedTranscripts[channel - 1];
|
||||||
if (bugname && this.bugname !== bugname) return;
|
if (bugname && this.bugname !== bugname) return;
|
||||||
|
if (this.paused) {
|
||||||
|
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - paused, ignoring transcript');
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.debug({evt, channel}, 'TaskTranscribe:_onTranscription');
|
if (this.vendor === 'ibm' && evt?.state === 'listening') return;
|
||||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
|
||||||
if ('microsoft' === this.vendor) {
|
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
|
||||||
const nbest = evt.NBest;
|
/* we will only get this when we have set utterance_end_ms */
|
||||||
const language_code = evt.PrimaryLanguage?.Language || this.language;
|
if (bufferedTranscripts.length === 0) {
|
||||||
const alternatives = nbest ? nbest.map((n) => {
|
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts');
|
||||||
return {
|
}
|
||||||
confidence: n.Confidence,
|
else {
|
||||||
transcript: n.Display
|
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram, return buffered transcript');
|
||||||
};
|
evt = this.consolidateTranscripts(bufferedTranscripts, channel, this.language, this.vendor);
|
||||||
}) :
|
evt.is_final = true;
|
||||||
[
|
this._bufferedTranscripts[channel - 1] = [];
|
||||||
{
|
this._resolve(channel, evt);
|
||||||
transcript: evt.DisplayText || evt.Text
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization');
|
||||||
|
|
||||||
|
evt = this.normalizeTranscription(evt, this.vendor, channel, this.language, undefined,
|
||||||
|
this.data.recognizer.punctuation);
|
||||||
|
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription');
|
||||||
|
if (evt.alternatives.length === 0) {
|
||||||
|
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, continue listening');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptyTranscript = false;
|
||||||
|
if (evt.is_final) {
|
||||||
|
if (evt.alternatives.length === 0 || evt.alternatives[0].transcript === '' && !cs.callGone && !this.killed) {
|
||||||
|
emptyTranscript = true;
|
||||||
|
if (finished === 'true' &&
|
||||||
|
['microsoft', 'deepgram'].includes(this.vendor) &&
|
||||||
|
bufferedTranscripts.length === 0) {
|
||||||
|
this.logger.debug({evt}, 'TaskGather:_onTranscription - got empty transcript from old gather, disregarding');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (this.vendor !== 'deepgram') {
|
||||||
|
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (this.isContinuousAsr) {
|
||||||
|
this.logger.info({evt},
|
||||||
|
'TaskGather:_onTranscription - got empty deepgram transcript during continous asr, continue listening');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (this.vendor === 'deepgram' && bufferedTranscripts.length > 0) {
|
||||||
|
this.logger.info({evt},
|
||||||
|
'TaskGather:_onTranscription - got empty transcript from deepgram, return the buffered transcripts');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.isContinuousAsr) {
|
||||||
|
/* append the transcript and start listening again for asrTimeout */
|
||||||
|
const t = evt.alternatives[0].transcript;
|
||||||
|
if (t) {
|
||||||
|
/* remove trailing punctuation */
|
||||||
|
if (/[,;:\.!\?]$/.test(t)) {
|
||||||
|
this.logger.debug('TaskGather:_onTranscription - removing trailing punctuation');
|
||||||
|
evt.alternatives[0].transcript = t.slice(0, -1);
|
||||||
}
|
}
|
||||||
];
|
}
|
||||||
|
this.logger.info({evt}, 'TaskGather:_onTranscription - got transcript during continous asr');
|
||||||
|
bufferedTranscripts.push(evt);
|
||||||
|
this._startAsrTimer(channel);
|
||||||
|
|
||||||
const newEvent = {
|
/* some STT engines will keep listening after a final response, so no need to restart */
|
||||||
is_final: evt.RecognitionStatus === 'Success',
|
if (!['soniox', 'aws', 'microsoft', 'deepgram', 'google']
|
||||||
|
.includes(this.vendor)) this._startTranscribing(cs, ep, channel);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (this.vendor === 'soniox') {
|
||||||
|
/* compile transcripts into one */
|
||||||
|
this._sonioxTranscripts.push(evt.vendor.finalWords);
|
||||||
|
evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language);
|
||||||
|
this._sonioxTranscripts = [];
|
||||||
|
}
|
||||||
|
else if (this.vendor === 'deepgram') {
|
||||||
|
/* compile transcripts into one */
|
||||||
|
if (!emptyTranscript) bufferedTranscripts.push(evt);
|
||||||
|
|
||||||
|
/* deepgram can send an empty and final transcript; only if we have any buffered should we resolve */
|
||||||
|
if (bufferedTranscripts.length === 0) return;
|
||||||
|
evt = this.consolidateTranscripts(bufferedTranscripts, channel, this.language);
|
||||||
|
this._bufferedTranscripts[channel - 1] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* here is where we return a final transcript */
|
||||||
|
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - sending final transcript');
|
||||||
|
this._resolve(channel, evt);
|
||||||
|
/* some STT engines will keep listening after a final response, so no need to restart */
|
||||||
|
if (!['soniox', 'aws', 'microsoft', 'deepgram', 'google']
|
||||||
|
.includes(this.vendor)) this._startTranscribing(cs, ep, channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* interim transcript */
|
||||||
|
|
||||||
|
/* deepgram can send a non-final transcript but with words that are final, so we need to buffer */
|
||||||
|
if (this.vendor === 'deepgram') {
|
||||||
|
const originalEvent = evt.vendor.evt;
|
||||||
|
if (originalEvent.is_final && evt.alternatives[0].transcript !== '') {
|
||||||
|
this.logger.debug({evt}, 'Gather:_onTranscription - buffering a completed (partial) deepgram transcript');
|
||||||
|
bufferedTranscripts.push(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.interim) {
|
||||||
|
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - sending interim transcript');
|
||||||
|
this._resolve(channel, evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _resolve(channel, evt) {
|
||||||
|
/* we've got a transcript, so end the otel child span for this channel */
|
||||||
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
channel,
|
channel,
|
||||||
language_code,
|
'stt.resolve': 'transcript',
|
||||||
alternatives
|
'stt.result': JSON.stringify(evt)
|
||||||
};
|
});
|
||||||
evt = newEvent;
|
this.childSpan[channel - 1].span.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evt.alternatives[0].transcript === '' && !cs.callGone && !this.killed) {
|
|
||||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
|
|
||||||
return this._transcribe(ep);
|
|
||||||
}
|
|
||||||
|
|
||||||
evt.channel_tag = channel;
|
|
||||||
|
|
||||||
if (this.transcriptionHook) {
|
if (this.transcriptionHook) {
|
||||||
const b3 = this.getTracingPropagation();
|
const b3 = this.getTracingPropagation();
|
||||||
const httpHeaders = b3 && {b3};
|
const httpHeaders = b3 && {b3};
|
||||||
this.cs.requestor.request('verb:hook', this.transcriptionHook,
|
try {
|
||||||
Object.assign({speech: evt}, this.cs.callInfo), httpHeaders)
|
const json = await this.cs.requestor.request('verb:hook', this.transcriptionHook, {
|
||||||
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
|
...this.cs.callInfo,
|
||||||
|
...httpHeaders,
|
||||||
|
speech: evt
|
||||||
|
});
|
||||||
|
this.logger.info({json}, 'sent transcriptionHook');
|
||||||
|
if (json && Array.isArray(json) && !this.parentTask) {
|
||||||
|
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.cs.replaceApplication(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info(err, 'TranscribeTask:_onTranscription error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.parentTask) {
|
if (this.parentTask) {
|
||||||
this.parentTask.emit('transcription', evt);
|
this.parentTask.emit('transcription', evt);
|
||||||
@@ -326,16 +485,46 @@ class TaskTranscribe extends Task {
|
|||||||
this._clearTimer();
|
this._clearTimer();
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
/* start another child span for this channel */
|
||||||
|
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||||
|
this.childSpan[channel - 1] = {span, ctx};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onNoAudio(cs, ep, channel) {
|
_onNoAudio(cs, ep, channel) {
|
||||||
this.logger.debug(`TaskTranscribe:_onNoAudio restarting transcription on channel ${channel}`);
|
this.logger.debug(`TaskTranscribe:_onNoAudio on channel ${channel}`);
|
||||||
|
if (this.paused) return;
|
||||||
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
|
channel,
|
||||||
|
'stt.resolve': 'timeout'
|
||||||
|
});
|
||||||
|
this.childSpan[channel - 1].span.end();
|
||||||
|
}
|
||||||
this._transcribe(ep);
|
this._transcribe(ep);
|
||||||
|
|
||||||
|
/* start new child span for this channel */
|
||||||
|
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||||
|
this.childSpan[channel - 1] = {span, ctx};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMaxDurationExceeded(cs, ep, channel) {
|
_onMaxDurationExceeded(cs, ep, channel) {
|
||||||
this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded restarting transcription on channel ${channel}`);
|
this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded on channel ${channel}`);
|
||||||
|
if (this.paused) return;
|
||||||
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
|
channel,
|
||||||
|
'stt.resolve': 'max duration exceeded'
|
||||||
|
});
|
||||||
|
this.childSpan[channel - 1].span.end();
|
||||||
|
}
|
||||||
|
|
||||||
this._transcribe(ep);
|
this._transcribe(ep);
|
||||||
|
|
||||||
|
/* start new child span for this channel */
|
||||||
|
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||||
|
this.childSpan[channel - 1] = {span, ctx};
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearTimer() {
|
_clearTimer() {
|
||||||
@@ -344,6 +533,83 @@ class TaskTranscribe extends Task {
|
|||||||
this._timer = null;
|
this._timer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _onJambonzError(cs, _ep, evt) {
|
||||||
|
if (this.vendor === 'google' && evt.error_code === 0) {
|
||||||
|
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError - ignoring google error code 0');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
||||||
|
if (this.paused) return;
|
||||||
|
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
|
||||||
|
_ep.stopTranscription({
|
||||||
|
vendor: this.vendor,
|
||||||
|
bugname: this.bugname
|
||||||
|
})
|
||||||
|
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
|
||||||
|
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||||
|
try {
|
||||||
|
await this._fallback();
|
||||||
|
let channel = 1;
|
||||||
|
if (this.ep !== _ep) {
|
||||||
|
channel = 2;
|
||||||
|
}
|
||||||
|
this._startTranscribing(cs, _ep, channel);
|
||||||
|
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||||
|
|
||||||
|
if (this.vendor === 'nuance') {
|
||||||
|
const {code, error} = evt;
|
||||||
|
//TODO: fix below, currently _resolve does not send timeout events
|
||||||
|
if (code === 404 && error === 'No speech') return this._resolve('timeout');
|
||||||
|
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
|
||||||
|
}
|
||||||
|
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
||||||
|
writeAlerts({
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
alert_type: AlertType.STT_FAILURE,
|
||||||
|
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
|
||||||
|
vendor: this.vendor,
|
||||||
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
|
||||||
|
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onVendorConnectFailure(cs, _ep, channel, evt) {
|
||||||
|
super._onVendorConnectFailure(cs, _ep, evt);
|
||||||
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
|
channel,
|
||||||
|
'stt.resolve': 'connection failure'
|
||||||
|
});
|
||||||
|
this.childSpan[channel - 1].span.end();
|
||||||
|
}
|
||||||
|
this.notifyTaskDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
_startAsrTimer(channel) {
|
||||||
|
if (this.vendor === 'deepgram') return; // no need
|
||||||
|
assert(this.isContinuousAsr);
|
||||||
|
this._clearAsrTimer(channel);
|
||||||
|
this._asrTimer = setTimeout(() => {
|
||||||
|
this.logger.debug(`TaskTranscribe:_startAsrTimer - asr timer went off for channel: ${channel}`);
|
||||||
|
const evt = this.consolidateTranscripts(
|
||||||
|
this._bufferedTranscripts[channel - 1], channel, this.language, this.vendor);
|
||||||
|
this._bufferedTranscripts[channel - 1] = [];
|
||||||
|
this._resolve(channel, evt);
|
||||||
|
}, this.asrTimeout);
|
||||||
|
this.logger.debug(`TaskTranscribe:_startAsrTimer: set for ${this.asrTimeout}ms for channel ${channel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearAsrTimer(channel) {
|
||||||
|
if (this._asrTimer) clearTimeout(this._asrTimer);
|
||||||
|
this._asrTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TaskTranscribe;
|
module.exports = TaskTranscribe;
|
||||||
|
|||||||
180
lib/tasks/tts-task.js
Normal file
180
lib/tasks/tts-task.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
const Task = require('./task');
|
||||||
|
const { TaskPreconditions } = require('../utils/constants');
|
||||||
|
|
||||||
|
class TtsTask extends Task {
|
||||||
|
|
||||||
|
constructor(logger, data, parentTask) {
|
||||||
|
super(logger, data);
|
||||||
|
this.parentTask = parentTask;
|
||||||
|
|
||||||
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
|
||||||
|
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||||
|
this.synthesizer = this.data.synthesizer || {};
|
||||||
|
this.disableTtsCache = this.data.disableTtsCache;
|
||||||
|
this.options = this.synthesizer.options || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(cs) {
|
||||||
|
super.exec(cs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _synthesizeWithSpecificVendor(cs, ep, {
|
||||||
|
vendor,
|
||||||
|
language,
|
||||||
|
voice,
|
||||||
|
label,
|
||||||
|
disableTtsStreaming,
|
||||||
|
preCache
|
||||||
|
}) {
|
||||||
|
const {srf, accountSid:account_sid} = cs;
|
||||||
|
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||||
|
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||||
|
const {synthAudio} = srf.locals.dbHelpers;
|
||||||
|
const engine = this.synthesizer.engine || 'standard';
|
||||||
|
const salt = cs.callSid;
|
||||||
|
|
||||||
|
let credentials = cs.getSpeechCredentials(vendor, 'tts', label);
|
||||||
|
/* parse Nuance voices into name and model */
|
||||||
|
let model;
|
||||||
|
if (vendor === 'nuance' && voice) {
|
||||||
|
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
|
||||||
|
if (arr) {
|
||||||
|
voice = arr[1];
|
||||||
|
model = arr[2];
|
||||||
|
}
|
||||||
|
} else if (vendor === 'deepgram') {
|
||||||
|
model = voice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* allow for microsoft custom region voice and api_key to be specified as an override */
|
||||||
|
if (vendor === 'microsoft' && this.options.deploymentId) {
|
||||||
|
credentials = credentials || {};
|
||||||
|
credentials.use_custom_tts = true;
|
||||||
|
credentials.custom_tts_endpoint = this.options.deploymentId;
|
||||||
|
credentials.api_key = this.options.apiKey || credentials.apiKey;
|
||||||
|
credentials.region = this.options.region || credentials.region;
|
||||||
|
voice = this.options.voice || voice;
|
||||||
|
} else if (vendor === 'elevenlabs') {
|
||||||
|
credentials = credentials || {};
|
||||||
|
credentials.model_id = this.options.model_id || credentials.model_id;
|
||||||
|
credentials.voice_settings = this.options.voice_settings || {};
|
||||||
|
credentials.optimize_streaming_latency = this.options.optimize_streaming_latency
|
||||||
|
|| credentials.optimize_streaming_latency;
|
||||||
|
voice = this.options.voice_id || voice;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.set({
|
||||||
|
tts_engine: vendor,
|
||||||
|
tts_voice: voice,
|
||||||
|
cache_speech_handles: 1,
|
||||||
|
}).catch((err) => this.logger.info({err}, `${this.name}: Error setting tts_engine on endpoint`));
|
||||||
|
|
||||||
|
if (!preCache) this.logger.info({vendor, language, voice, model}, `${this.name}:exec`);
|
||||||
|
try {
|
||||||
|
if (!credentials) {
|
||||||
|
writeAlerts({
|
||||||
|
account_sid,
|
||||||
|
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
||||||
|
vendor
|
||||||
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
||||||
|
this.notifyError({
|
||||||
|
msg: 'TTS error',
|
||||||
|
details:`No speech credentials provisioned for selected vendor ${vendor}`
|
||||||
|
});
|
||||||
|
throw new Error('no provisioned speech credentials for TTS');
|
||||||
|
}
|
||||||
|
// synthesize all of the text elements
|
||||||
|
let lastUpdated = false;
|
||||||
|
|
||||||
|
/* produce an audio segment from the provided text */
|
||||||
|
const generateAudio = async(text) => {
|
||||||
|
if (this.killed) return;
|
||||||
|
if (text.startsWith('silence_stream://')) return text;
|
||||||
|
|
||||||
|
/* otel: trace time for tts */
|
||||||
|
if (!preCache && !this.parentTask) {
|
||||||
|
const {span} = this.startChildSpan('tts-generation', {
|
||||||
|
'tts.vendor': vendor,
|
||||||
|
'tts.language': language,
|
||||||
|
'tts.voice': voice
|
||||||
|
});
|
||||||
|
this.otelSpan = span;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
|
||||||
|
account_sid,
|
||||||
|
text,
|
||||||
|
vendor,
|
||||||
|
language,
|
||||||
|
voice,
|
||||||
|
engine,
|
||||||
|
model,
|
||||||
|
salt,
|
||||||
|
credentials,
|
||||||
|
options: this.options,
|
||||||
|
disableTtsCache : this.disableTtsCache,
|
||||||
|
disableTtsStreaming,
|
||||||
|
preCache
|
||||||
|
});
|
||||||
|
if (!filePath.startsWith('say:')) {
|
||||||
|
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
|
||||||
|
if (filePath) cs.trackTmpFile(filePath);
|
||||||
|
if (this.otelSpan) {
|
||||||
|
this.otelSpan.setAttributes({'tts.cached': servedFromCache});
|
||||||
|
this.otelSpan.end();
|
||||||
|
this.otelSpan = null;
|
||||||
|
}
|
||||||
|
if (!servedFromCache && !lastUpdated) {
|
||||||
|
lastUpdated = true;
|
||||||
|
updateSpeechCredentialLastUsed(credentials.speech_credential_sid).catch(() => {/* logged error */});
|
||||||
|
}
|
||||||
|
if (!servedFromCache && rtt && !preCache) {
|
||||||
|
this.notifyStatus({
|
||||||
|
event: 'synthesized-audio',
|
||||||
|
vendor,
|
||||||
|
language,
|
||||||
|
characters: text.length,
|
||||||
|
elapsedTime: rtt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.logger.debug('a streaming tts api will be used');
|
||||||
|
const modifiedPath = filePath.replace('say:{', `say:{session-uuid=${ep.uuid},`);
|
||||||
|
return modifiedPath;
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err}, 'Error synthesizing tts');
|
||||||
|
if (this.otelSpan) this.otelSpan.end();
|
||||||
|
writeAlerts({
|
||||||
|
account_sid: cs.accountSid,
|
||||||
|
alert_type: AlertType.TTS_FAILURE,
|
||||||
|
vendor,
|
||||||
|
detail: err.message
|
||||||
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||||
|
this.notifyError({msg: 'TTS error', details: err.message || err});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arr = this.text.map((t) => (this._validateURL(t) ? t : generateAudio(t)));
|
||||||
|
return (await Promise.all(arr)).filter((fp) => fp && fp.length);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info(err, 'TaskSay:exec error');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_validateURL(urlString) {
|
||||||
|
try {
|
||||||
|
new URL(urlString);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TtsTask;
|
||||||
@@ -1,14 +1,22 @@
|
|||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const {readFile} = require('fs');
|
const {readFile} = require('fs');
|
||||||
const {
|
const {
|
||||||
|
TaskName,
|
||||||
GoogleTranscriptionEvents,
|
GoogleTranscriptionEvents,
|
||||||
AwsTranscriptionEvents,
|
AwsTranscriptionEvents,
|
||||||
AzureTranscriptionEvents,
|
AzureTranscriptionEvents,
|
||||||
|
NuanceTranscriptionEvents,
|
||||||
|
NvidiaTranscriptionEvents,
|
||||||
|
IbmTranscriptionEvents,
|
||||||
|
SonioxTranscriptionEvents,
|
||||||
|
CobaltTranscriptionEvents,
|
||||||
|
DeepgramTranscriptionEvents,
|
||||||
|
JambonzTranscriptionEvents,
|
||||||
AmdEvents,
|
AmdEvents,
|
||||||
AvmdEvents
|
AvmdEvents
|
||||||
} = require('./constants');
|
} = require('./constants');
|
||||||
const bugname = 'amd_bug';
|
const bugname = 'amd_bug';
|
||||||
const {VMD_HINTS_FILE} = process.env;
|
const {VMD_HINTS_FILE} = require('../config');
|
||||||
let voicemailHints = [];
|
let voicemailHints = [];
|
||||||
|
|
||||||
const updateHints = async(file, callback) => {
|
const updateHints = async(file, callback) => {
|
||||||
@@ -47,13 +55,19 @@ class Amd extends Emitter {
|
|||||||
this.language = opts.recognizer?.language || cs.speechRecognizerLanguage;
|
this.language = opts.recognizer?.language || cs.speechRecognizerLanguage;
|
||||||
if ('default' === this.language) this.language = cs.speechRecognizerLanguage;
|
if ('default' === this.language) this.language = cs.speechRecognizerLanguage;
|
||||||
|
|
||||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt',
|
||||||
|
opts.recognizer?.label || cs.speechRecognizerLabel);
|
||||||
|
|
||||||
if (!this.sttCredentials) throw new Error(`No speech credentials found for vendor ${this.vendor}`);
|
if (!this.sttCredentials) throw new Error(`No speech credentials found for vendor ${this.vendor}`);
|
||||||
|
|
||||||
this.thresholdWordCount = opts.thresholdWordCount || 9;
|
this.thresholdWordCount = opts.thresholdWordCount || 9;
|
||||||
const {normalizeTranscription} = require('./transcription-utils')(logger);
|
const {normalizeTranscription} = require('./transcription-utils')(logger);
|
||||||
this.normalizeTranscription = normalizeTranscription;
|
this.normalizeTranscription = normalizeTranscription;
|
||||||
|
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
|
||||||
|
this.getNuanceAccessToken = getNuanceAccessToken;
|
||||||
|
this.getIbmAccessToken = getIbmAccessToken;
|
||||||
|
const {setChannelVarsForStt} = require('./transcription-utils')(logger);
|
||||||
|
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
noSpeechTimeoutMs = 5000,
|
noSpeechTimeoutMs = 5000,
|
||||||
@@ -184,7 +198,7 @@ module.exports = (logger) => {
|
|||||||
const {vendor, language} = ep.amd;
|
const {vendor, language} = ep.amd;
|
||||||
ep.startTranscription({
|
ep.startTranscription({
|
||||||
vendor,
|
vendor,
|
||||||
language,
|
locale: language,
|
||||||
interim: true,
|
interim: true,
|
||||||
bugname
|
bugname
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@@ -229,52 +243,96 @@ module.exports = (logger) => {
|
|||||||
|
|
||||||
const startAmd = async(cs, ep, task, opts) => {
|
const startAmd = async(cs, ep, task, opts) => {
|
||||||
const amd = ep.amd = new Amd(logger, cs, opts);
|
const amd = ep.amd = new Amd(logger, cs, opts);
|
||||||
const {vendor, language, sttCredentials} = amd;
|
const {vendor, language} = amd;
|
||||||
const sttOpts = {};
|
let sttCredentials = amd.sttCredentials;
|
||||||
const hints = voicemailHints[language] || [];
|
const hints = voicemailHints[language] || [];
|
||||||
|
|
||||||
|
if (vendor === 'nuance' && sttCredentials.client_id) {
|
||||||
|
/* get nuance access token */
|
||||||
|
const {getNuanceAccessToken} = amd;
|
||||||
|
const {client_id, secret} = sttCredentials;
|
||||||
|
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
||||||
|
logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
|
||||||
|
sttCredentials = {...sttCredentials, access_token};
|
||||||
|
}
|
||||||
|
else if (vendor == 'ibm' && sttCredentials.stt_api_key) {
|
||||||
|
/* get ibm access token */
|
||||||
|
const {getIbmAccessToken} = amd;
|
||||||
|
const {stt_api_key, stt_region} = sttCredentials;
|
||||||
|
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
|
||||||
|
logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||||
|
sttCredentials = {...sttCredentials, access_token, stt_region};
|
||||||
|
}
|
||||||
|
|
||||||
/* set stt options */
|
/* set stt options */
|
||||||
logger.info(`starting amd for vendor ${vendor} and language ${language}`);
|
logger.info(`starting amd for vendor ${vendor} and language ${language}`);
|
||||||
if ('google' === vendor) {
|
const sttOpts = amd.setChannelVarsForStt({name: TaskName.Gather}, sttCredentials, language, {
|
||||||
sttOpts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(sttCredentials.credentials);
|
vendor,
|
||||||
sttOpts.GOOGLE_SPEECH_USE_ENHANCED = true;
|
hints,
|
||||||
sttOpts.GOOGLE_SPEECH_HINTS = hints.join(',');
|
enhancedModel: true,
|
||||||
if (opts.recognizer?.altLanguages) {
|
altLanguages: opts.recognizer?.altLanguages || [],
|
||||||
sttOpts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
|
initialSpeechTimeoutMs: opts.resolveTimeoutMs,
|
||||||
}
|
});
|
||||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
|
||||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, onEndOfUtterance.bind(null, cs, ep, task));
|
|
||||||
}
|
|
||||||
else if (['aws', 'polly'].includes(vendor)) {
|
|
||||||
Object.assign(sttOpts, {
|
|
||||||
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
|
|
||||||
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
|
|
||||||
AWS_REGION: sttCredentials.region
|
|
||||||
});
|
|
||||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
|
||||||
}
|
|
||||||
else if ('microsoft' === vendor) {
|
|
||||||
Object.assign(sttOpts, {
|
|
||||||
'AZURE_SUBSCRIPTION_KEY': sttCredentials.api_key,
|
|
||||||
'AZURE_REGION': sttCredentials.region
|
|
||||||
});
|
|
||||||
sttOpts.AZURE_SPEECH_HINTS = hints.join(',');
|
|
||||||
if (opts.recognizer?.altLanguages) {
|
|
||||||
sttOpts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
|
|
||||||
}
|
|
||||||
sttOpts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = opts.resolveTimeoutMs || 20000;
|
|
||||||
|
|
||||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
|
||||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task));
|
|
||||||
}
|
|
||||||
logger.debug({sttOpts}, 'startAmd: setting channel vars');
|
|
||||||
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
|
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
|
||||||
|
|
||||||
|
amd.transcriptionHandler = onTranscription.bind(null, cs, ep, task);
|
||||||
|
amd.EndOfUtteranceHandler = onEndOfUtterance.bind(null, cs, ep, task);
|
||||||
|
amd.noSpeechHandler = onNoSpeechDetected.bind(null, cs, ep, task);
|
||||||
|
|
||||||
|
switch (vendor) {
|
||||||
|
case 'google':
|
||||||
|
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, amd.EndOfUtteranceHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'aws':
|
||||||
|
case 'polly':
|
||||||
|
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
case 'microsoft':
|
||||||
|
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, amd.noSpeechHandler);
|
||||||
|
break;
|
||||||
|
case 'nuance':
|
||||||
|
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deepgram':
|
||||||
|
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'soniox':
|
||||||
|
amd.bugname = 'soniox_amd_transcribe';
|
||||||
|
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ibm':
|
||||||
|
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nvidia':
|
||||||
|
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cobalt':
|
||||||
|
ep.addCustomEventListener(CobaltTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (vendor.startsWith('custom:')) {
|
||||||
|
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Invalid vendor ${this.vendor}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
amd
|
amd
|
||||||
.on(AmdEvents.NoSpeechDetected, (evt) => {
|
.on(AmdEvents.NoSpeechDetected, (evt) => {
|
||||||
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
|
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.stopTranscription({vendor, bugname});
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping transcription');
|
logger.info({err}, 'Error stopping transcription');
|
||||||
}
|
}
|
||||||
@@ -282,7 +340,7 @@ module.exports = (logger) => {
|
|||||||
.on(AmdEvents.HumanDetected, (evt) => {
|
.on(AmdEvents.HumanDetected, (evt) => {
|
||||||
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
|
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.stopTranscription({vendor, bugname});
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping transcription');
|
logger.info({err}, 'Error stopping transcription');
|
||||||
}
|
}
|
||||||
@@ -293,7 +351,7 @@ module.exports = (logger) => {
|
|||||||
.on(AmdEvents.DecisionTimeout, (evt) => {
|
.on(AmdEvents.DecisionTimeout, (evt) => {
|
||||||
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
|
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.stopTranscription({vendor, bugname});
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping transcription');
|
logger.info({err}, 'Error stopping transcription');
|
||||||
}
|
}
|
||||||
@@ -301,7 +359,7 @@ module.exports = (logger) => {
|
|||||||
.on(AmdEvents.ToneTimeout, (evt) => {
|
.on(AmdEvents.ToneTimeout, (evt) => {
|
||||||
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
|
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.execute('avmd_stop').catch((err) => logger.info(err, 'Error stopping avmd'));
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping avmd');
|
logger.info({err}, 'Error stopping avmd');
|
||||||
}
|
}
|
||||||
@@ -309,7 +367,7 @@ module.exports = (logger) => {
|
|||||||
.on(AmdEvents.MachineStoppedSpeaking, () => {
|
.on(AmdEvents.MachineStoppedSpeaking, () => {
|
||||||
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
|
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.stopTranscription({vendor, bugname});
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping transcription');
|
logger.info({err}, 'Error stopping transcription');
|
||||||
}
|
}
|
||||||
@@ -328,6 +386,19 @@ module.exports = (logger) => {
|
|||||||
if (ep.amd) {
|
if (ep.amd) {
|
||||||
vendor = ep.amd.vendor;
|
vendor = ep.amd.vendor;
|
||||||
ep.amd.stopAllTimers();
|
ep.amd.stopAllTimers();
|
||||||
|
|
||||||
|
ep.removeListener(GoogleTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(GoogleTranscriptionEvents.EndOfUtterance, ep.amd.EndOfUtteranceHandler);
|
||||||
|
ep.removeListener(AwsTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(AzureTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(AzureTranscriptionEvents.NoSpeechDetected, ep.amd.noSpeechHandler);
|
||||||
|
ep.removeListener(NuanceTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(DeepgramTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(SonioxTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(IbmTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(NvidiaTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(JambonzTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
|
||||||
ep.amd = null;
|
ep.amd = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const PORT = process.env.AWS_SNS_PORT || 3010;
|
const {
|
||||||
|
AWS_REGION,
|
||||||
|
AWS_SNS_PORT: PORT,
|
||||||
|
AWS_SNS_TOPIC_ARM,
|
||||||
|
AWS_SNS_PORT_MAX,
|
||||||
|
} = require('../config');
|
||||||
const {LifeCycleEvents} = require('./constants');
|
const {LifeCycleEvents} = require('./constants');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const app = express();
|
const app = express();
|
||||||
const getString = bent('string');
|
const getString = bent('string');
|
||||||
const AWS = require('aws-sdk');
|
const {
|
||||||
const sns = new AWS.SNS({apiVersion: '2010-03-31'});
|
SNSClient,
|
||||||
const autoscaling = new AWS.AutoScaling({apiVersion: '2011-01-01'});
|
SubscribeCommand,
|
||||||
|
UnsubscribeCommand } = require('@aws-sdk/client-sns');
|
||||||
|
const snsClient = new SNSClient({ region: AWS_REGION, apiVersion: '2010-03-31' });
|
||||||
|
const {
|
||||||
|
AutoScalingClient,
|
||||||
|
DescribeAutoScalingGroupsCommand,
|
||||||
|
CompleteLifecycleActionCommand } = require('@aws-sdk/client-auto-scaling');
|
||||||
|
const autoScalingClient = new AutoScalingClient({ region: AWS_REGION, apiVersion: '2011-01-01' });
|
||||||
const {Parser} = require('xml2js');
|
const {Parser} = require('xml2js');
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
const {validatePayload} = require('verify-aws-sns-signature');
|
const {validatePayload} = require('verify-aws-sns-signature');
|
||||||
|
|
||||||
AWS.config.update({region: process.env.AWS_REGION});
|
|
||||||
|
|
||||||
class SnsNotifier extends Emitter {
|
class SnsNotifier extends Emitter {
|
||||||
constructor(logger) {
|
constructor(logger) {
|
||||||
super();
|
super();
|
||||||
@@ -31,8 +41,8 @@ class SnsNotifier extends Emitter {
|
|||||||
|
|
||||||
_handleErrors(logger, app, resolve, reject, e) {
|
_handleErrors(logger, app, resolve, reject, e) {
|
||||||
if (e.code === 'EADDRINUSE' &&
|
if (e.code === 'EADDRINUSE' &&
|
||||||
process.env.AWS_SNS_PORT_MAX &&
|
AWS_SNS_PORT_MAX &&
|
||||||
e.port < process.env.AWS_SNS_PORT_MAX) {
|
e.port < AWS_SNS_PORT_MAX) {
|
||||||
|
|
||||||
logger.info(`SNS lifecycle server failed to bind port on ${e.port}, will try next port`);
|
logger.info(`SNS lifecycle server failed to bind port on ${e.port}, will try next port`);
|
||||||
const server = this._doListen(logger, app, ++e.port, resolve);
|
const server = this._doListen(logger, app, ++e.port, resolve);
|
||||||
@@ -64,7 +74,7 @@ class SnsNotifier extends Emitter {
|
|||||||
subscriptionRequestId: this.subscriptionRequestId
|
subscriptionRequestId: this.subscriptionRequestId
|
||||||
}, 'response from SNS SubscribeURL');
|
}, 'response from SNS SubscribeURL');
|
||||||
const data = await this.describeInstance();
|
const data = await this.describeInstance();
|
||||||
this.lifecycleState = data.AutoScalingInstances[0].LifecycleState;
|
this.lifecycleState = data.AutoScalingGroups[0].Instances[0].LifecycleState;
|
||||||
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -130,51 +140,56 @@ class SnsNotifier extends Emitter {
|
|||||||
|
|
||||||
async subscribe() {
|
async subscribe() {
|
||||||
try {
|
try {
|
||||||
const response = await sns.subscribe({
|
const params = {
|
||||||
Protocol: 'http',
|
Protocol: 'http',
|
||||||
TopicArn: process.env.AWS_SNS_TOPIC_ARM,
|
TopicArn: AWS_SNS_TOPIC_ARM,
|
||||||
Endpoint: this.snsEndpoint
|
Endpoint: this.snsEndpoint
|
||||||
}).promise();
|
};
|
||||||
this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
|
const response = await snsClient.send(new SubscribeCommand(params));
|
||||||
|
this.logger.info({response}, `response to SNS subscribe to ${AWS_SNS_TOPIC_ARM}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
|
this.logger.error({err}, `Error subscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async unsubscribe() {
|
async unsubscribe() {
|
||||||
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
|
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
|
||||||
try {
|
try {
|
||||||
const response = await sns.unsubscribe({
|
const params = {
|
||||||
SubscriptionArn: this.subscriptionArn
|
SubscriptionArn: this.subscriptionArn
|
||||||
}).promise();
|
};
|
||||||
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
|
const response = await snsClient.send(new UnsubscribeCommand(params));
|
||||||
|
this.logger.info({response}, `response to SNS unsubscribe to ${AWS_SNS_TOPIC_ARM}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
|
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
completeScaleIn() {
|
completeScaleIn() {
|
||||||
assert(this.scaleInParams);
|
assert(this.scaleInParams);
|
||||||
autoscaling.completeLifecycleAction(this.scaleInParams, (err, response) => {
|
autoScalingClient.send(new CompleteLifecycleActionCommand(this.scaleInParams))
|
||||||
if (err) return this.logger.error({err}, 'Error completing scale-in');
|
.then((data) => {
|
||||||
this.logger.info({response}, 'Successfully completed scale-in action');
|
return this.logger.info({data}, 'Successfully completed scale-in action');
|
||||||
});
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.error({err}, 'Error completing scale-in');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describeInstance() {
|
describeInstance() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.instanceId) return reject('instance-id unknown');
|
if (!this.instanceId) return reject('instance-id unknown');
|
||||||
autoscaling.describeAutoScalingInstances({
|
autoScalingClient.send(new DescribeAutoScalingGroupsCommand({
|
||||||
InstanceIds: [this.instanceId]
|
InstanceIds: [this.instanceId]
|
||||||
}, (err, data) => {
|
}))
|
||||||
if (err) {
|
.then((data) => {
|
||||||
|
this.logger.info({data}, 'SnsNotifier: describeInstance');
|
||||||
|
return resolve(data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
this.logger.error({err}, 'Error describing instances');
|
this.logger.error({err}, 'Error describing instances');
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
});
|
||||||
this.logger.info({data}, 'SnsNotifier: describeInstance');
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +203,7 @@ module.exports = async function(logger) {
|
|||||||
process.on('SIGHUP', async() => {
|
process.on('SIGHUP', async() => {
|
||||||
try {
|
try {
|
||||||
const data = await notifier.describeInstance();
|
const data = await notifier.describeInstance();
|
||||||
const state = data.AutoScalingInstances[0].LifecycleState;
|
const state = data.AutoScalingGroups[0].Instances[0].LifecycleState;
|
||||||
if (state !== notifier.lifecycleState) {
|
if (state !== notifier.lifecycleState) {
|
||||||
notifier.lifecycleState = state;
|
notifier.lifecycleState = state;
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
|||||||
195
lib/utils/background-task-manager.js
Normal file
195
lib/utils/background-task-manager.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
const makeTask = require('../tasks/make_task');
|
||||||
|
const { JAMBONZ_RECORD_WS_BASE_URL, JAMBONZ_RECORD_WS_USERNAME, JAMBONZ_RECORD_WS_PASSWORD } = require('../config');
|
||||||
|
const Emitter = require('events');
|
||||||
|
|
||||||
|
class BackgroundTaskManager extends Emitter {
|
||||||
|
constructor({cs, logger, rootSpan}) {
|
||||||
|
super();
|
||||||
|
this.tasks = new Map();
|
||||||
|
this.cs = cs;
|
||||||
|
this.logger = logger;
|
||||||
|
this.rootSpan = rootSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
isTaskRunning(type) {
|
||||||
|
return this.tasks.has(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTask(type) {
|
||||||
|
if (this.tasks.has(type)) {
|
||||||
|
return this.tasks.get(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count() {
|
||||||
|
return this.tasks.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
async newTask(type, opts, sticky = false) {
|
||||||
|
this.logger.info({opts}, `initiating Background task ${type}`);
|
||||||
|
if (this.tasks.has(type)) {
|
||||||
|
this.logger.info(`Background task ${type} is running, skipped`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let task;
|
||||||
|
switch (type) {
|
||||||
|
case 'listen':
|
||||||
|
task = await this._initListen(opts);
|
||||||
|
break;
|
||||||
|
case 'bargeIn':
|
||||||
|
task = await this._initBargeIn(opts);
|
||||||
|
break;
|
||||||
|
case 'record':
|
||||||
|
task = await this._initRecord();
|
||||||
|
break;
|
||||||
|
case 'transcribe':
|
||||||
|
task = await this._initTranscribe(opts);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (task) {
|
||||||
|
this.tasks.set(type, task);
|
||||||
|
}
|
||||||
|
if (task && sticky) task.sticky = true;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(type) {
|
||||||
|
const task = this.getTask(type);
|
||||||
|
if (task) {
|
||||||
|
this.logger.info(`stopping background task: ${type}`);
|
||||||
|
task.removeAllListeners();
|
||||||
|
task.span.end();
|
||||||
|
task.kill();
|
||||||
|
// Remove task from managed List
|
||||||
|
this.tasks.delete(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAll() {
|
||||||
|
this.logger.debug('BackgroundTaskManager:stopAll');
|
||||||
|
for (const key of this.tasks.keys()) {
|
||||||
|
this.stop(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate Listen
|
||||||
|
async _initListen(opts, bugname = 'jambonz-background-listen', ignoreCustomerData = false, type = 'listen') {
|
||||||
|
let task;
|
||||||
|
try {
|
||||||
|
const t = normalizeJambones(this.logger, [opts]);
|
||||||
|
task = makeTask(this.logger, t[0]);
|
||||||
|
task.bugname = bugname;
|
||||||
|
task.ignoreCustomerData = ignoreCustomerData;
|
||||||
|
const resources = await this.cs._evaluatePreconditions(task);
|
||||||
|
const {span, ctx} = this.rootSpan.startChildSpan(`background-${type}:${task.summary}`);
|
||||||
|
task.span = span;
|
||||||
|
task.ctx = ctx;
|
||||||
|
task.exec(this.cs, resources)
|
||||||
|
.then(this._taskCompleted.bind(this, type, task))
|
||||||
|
.catch(this._taskError.bind(this, type, task));
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err, opts}, `BackgroundTaskManager:_initListen - Error creating ${bugname} task`);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate Gather
|
||||||
|
async _initBargeIn(opts) {
|
||||||
|
let task;
|
||||||
|
try {
|
||||||
|
const t = normalizeJambones(this.logger, [opts]);
|
||||||
|
task = makeTask(this.logger, t[0]);
|
||||||
|
task
|
||||||
|
.once('dtmf', this._bargeInTaskCompleted.bind(this))
|
||||||
|
.once('vad', this._bargeInTaskCompleted.bind(this))
|
||||||
|
.once('transcription', this._bargeInTaskCompleted.bind(this))
|
||||||
|
.once('timeout', this._bargeInTaskCompleted.bind(this));
|
||||||
|
const resources = await this.cs._evaluatePreconditions(task);
|
||||||
|
const {span, ctx} = this.rootSpan.startChildSpan(`background-bargeIn:${task.summary}`);
|
||||||
|
task.span = span;
|
||||||
|
task.ctx = ctx;
|
||||||
|
task.bugname_prefix = 'background_bargeIn_';
|
||||||
|
task.exec(this.cs, resources)
|
||||||
|
.then(() => {
|
||||||
|
this._taskCompleted('bargeIn', task);
|
||||||
|
if (task.sticky && !this.cs.callGone && !this.cs._stopping) {
|
||||||
|
this.logger.info('BackgroundTaskManager:_initBargeIn: restarting background bargeIn');
|
||||||
|
this.newTask('bargeIn', opts, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch(this._taskError.bind(this, 'bargeIn', task));
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info(err, 'BackgroundTaskManager:_initGather - Error creating bargeIn task');
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate Record
|
||||||
|
async _initRecord() {
|
||||||
|
if (this.cs.accountInfo.account.record_all_calls || this.cs.application.record_all_calls) {
|
||||||
|
if (!JAMBONZ_RECORD_WS_BASE_URL || !this.cs.accountInfo.account.bucket_credential) {
|
||||||
|
this.logger.error('_initRecord: invalid cfg - missing JAMBONZ_RECORD_WS_BASE_URL or bucket config');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const listenOpts = {
|
||||||
|
url: `${JAMBONZ_RECORD_WS_BASE_URL}/record/${this.cs.accountInfo.account.bucket_credential.vendor}`,
|
||||||
|
disableBidirectionalAudio: true,
|
||||||
|
mixType : 'stereo',
|
||||||
|
passDtmf: true
|
||||||
|
};
|
||||||
|
if (JAMBONZ_RECORD_WS_USERNAME && JAMBONZ_RECORD_WS_PASSWORD) {
|
||||||
|
listenOpts.wsAuth = {
|
||||||
|
username: JAMBONZ_RECORD_WS_USERNAME,
|
||||||
|
password: JAMBONZ_RECORD_WS_PASSWORD
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.logger.debug({listenOpts}, '_initRecord: enabling listen');
|
||||||
|
return await this._initListen({verb: 'listen', ...listenOpts}, 'jambonz-session-record', true, 'record');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate Transcribe
|
||||||
|
async _initTranscribe(opts) {
|
||||||
|
let task;
|
||||||
|
try {
|
||||||
|
const t = normalizeJambones(this.logger, [opts]);
|
||||||
|
task = makeTask(this.logger, t[0]);
|
||||||
|
const resources = await this.cs._evaluatePreconditions(task);
|
||||||
|
const {span, ctx} = this.rootSpan.startChildSpan(`background-transcribe:${task.summary}`);
|
||||||
|
task.span = span;
|
||||||
|
task.ctx = ctx;
|
||||||
|
task.bugname_prefix = 'background_transcribe_';
|
||||||
|
task.exec(this.cs, resources)
|
||||||
|
.then(this._taskCompleted.bind(this, 'transcribe', task))
|
||||||
|
.catch(this._taskError.bind(this, 'transcribe', task));
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info(err, 'BackgroundTaskManager:_initTranscribe - Error creating transcribe task');
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskCompleted(type, task) {
|
||||||
|
this.logger.debug({type, task}, `BackgroundTaskManager:_taskCompleted: task completed, sticky: ${task.sticky}`);
|
||||||
|
task.removeAllListeners();
|
||||||
|
task.span.end();
|
||||||
|
this.tasks.delete(type);
|
||||||
|
}
|
||||||
|
_taskError(type, task, error) {
|
||||||
|
this.logger.info({type, task, error}, 'BackgroundTaskManager:_taskError: task Error');
|
||||||
|
task.removeAllListeners();
|
||||||
|
task.span.end();
|
||||||
|
this.tasks.delete(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bargeInTaskCompleted(evt) {
|
||||||
|
this.logger.debug({evt},
|
||||||
|
'BackgroundTaskManager:_bargeInTaskCompleted on event from background bargeIn, emitting bargein-done event');
|
||||||
|
this.emit('bargeIn-done', evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BackgroundTaskManager;
|
||||||
@@ -2,6 +2,7 @@ const assert = require('assert');
|
|||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const timeSeries = require('@jambonz/time-series');
|
const timeSeries = require('@jambonz/time-series');
|
||||||
|
const {NODE_ENV, JAMBONES_TIME_SERIES_HOST} = require('../config');
|
||||||
let alerter ;
|
let alerter ;
|
||||||
|
|
||||||
class BaseRequestor extends Emitter {
|
class BaseRequestor extends Emitter {
|
||||||
@@ -22,9 +23,9 @@ class BaseRequestor extends Emitter {
|
|||||||
|
|
||||||
if (!alerter) {
|
if (!alerter) {
|
||||||
alerter = timeSeries(logger, {
|
alerter = timeSeries(logger, {
|
||||||
host: process.env.JAMBONES_TIME_SERIES_HOST,
|
host: JAMBONES_TIME_SERIES_HOST,
|
||||||
commitSize: 50,
|
commitSize: 50,
|
||||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
commitInterval: 'test' === NODE_ENV ? 7 : 20
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,24 @@ const {context, trace} = require('@opentelemetry/api');
|
|||||||
const {Dialog} = require('drachtio-srf');
|
const {Dialog} = require('drachtio-srf');
|
||||||
class RootSpan {
|
class RootSpan {
|
||||||
constructor(callType, req) {
|
constructor(callType, req) {
|
||||||
let tracer, callSid, linkedSpanId;
|
const {srf} = require('../../');
|
||||||
|
const tracer = srf.locals.otel.tracer;
|
||||||
|
let callSid, accountSid, applicationSid, linkedSpanId;
|
||||||
|
|
||||||
if (req instanceof Dialog) {
|
if (req instanceof Dialog) {
|
||||||
const dlg = req;
|
const dlg = req;
|
||||||
tracer = dlg.srf.locals.otel.tracer;
|
|
||||||
callSid = dlg.callSid;
|
callSid = dlg.callSid;
|
||||||
linkedSpanId = dlg.linkedSpanId;
|
linkedSpanId = dlg.linkedSpanId;
|
||||||
}
|
}
|
||||||
else {
|
else if (req.srf) {
|
||||||
tracer = req.srf.locals.otel.tracer;
|
|
||||||
callSid = req.locals.callSid;
|
callSid = req.locals.callSid;
|
||||||
|
accountSid = req.get('X-Account-Sid'),
|
||||||
|
applicationSid = req.locals.application_sid;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callSid = req.callSid;
|
||||||
|
accountSid = req.accountSid;
|
||||||
|
applicationSid = req.applicationSid;
|
||||||
}
|
}
|
||||||
this._span = tracer.startSpan(callType || 'incoming-call');
|
this._span = tracer.startSpan(callType || 'incoming-call');
|
||||||
if (req instanceof Dialog) {
|
if (req instanceof Dialog) {
|
||||||
@@ -22,13 +29,20 @@ class RootSpan {
|
|||||||
callId: dlg.sip.callId
|
callId: dlg.sip.callId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (req.srf) {
|
||||||
|
this._span.setAttributes({
|
||||||
|
callSid,
|
||||||
|
accountSid,
|
||||||
|
applicationSid,
|
||||||
|
callId: req.get('Call-ID'),
|
||||||
|
externalCallId: req.get('X-CID')
|
||||||
|
});
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
this._span.setAttributes({
|
this._span.setAttributes({
|
||||||
callSid,
|
callSid,
|
||||||
accountSid: req.get('X-Account-Sid'),
|
accountSid,
|
||||||
applicationSid: req.locals.application_sid,
|
applicationSid
|
||||||
callId: req.get('Call-ID'),
|
|
||||||
externalCallId: req.get('X-CID')
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"TaskName": {
|
"TaskName": {
|
||||||
"Cognigy": "cognigy",
|
"Answer": "answer",
|
||||||
"Conference": "conference",
|
"Conference": "conference",
|
||||||
"Config": "config",
|
"Config": "config",
|
||||||
"Dequeue": "dequeue",
|
"Dequeue": "dequeue",
|
||||||
"Dial": "dial",
|
"Dial": "dial",
|
||||||
"Dialogflow": "dialogflow",
|
"Dialogflow": "dialogflow",
|
||||||
"Dtmf": "dtmf",
|
"Dtmf": "dtmf",
|
||||||
|
"Dub": "dub",
|
||||||
"Enqueue": "enqueue",
|
"Enqueue": "enqueue",
|
||||||
"Gather": "gather",
|
"Gather": "gather",
|
||||||
"Hangup": "hangup",
|
"Hangup": "hangup",
|
||||||
@@ -29,7 +30,8 @@
|
|||||||
"Tag": "tag",
|
"Tag": "tag",
|
||||||
"Transcribe": "transcribe"
|
"Transcribe": "transcribe"
|
||||||
},
|
},
|
||||||
"AllowedSipRecVerbs": ["config", "gather", "transcribe", "listen"],
|
"AllowedSipRecVerbs": ["answer", "config", "gather", "transcribe", "listen", "tag"],
|
||||||
|
"AllowedConfirmSessionVerbs": ["config", "gather", "plays", "say", "tag"],
|
||||||
"CallStatus": {
|
"CallStatus": {
|
||||||
"Trying": "trying",
|
"Trying": "trying",
|
||||||
"Ringing": "ringing",
|
"Ringing": "ringing",
|
||||||
@@ -51,6 +53,11 @@
|
|||||||
"Silence": "silence",
|
"Silence": "silence",
|
||||||
"Resume": "resume"
|
"Resume": "resume"
|
||||||
},
|
},
|
||||||
|
"TranscribeStatus": {
|
||||||
|
"Pause": "pause",
|
||||||
|
"Silence": "silence",
|
||||||
|
"Resume": "resume"
|
||||||
|
},
|
||||||
"TaskPreconditions": {
|
"TaskPreconditions": {
|
||||||
"None": "none",
|
"None": "none",
|
||||||
"Endpoint": "endpoint",
|
"Endpoint": "endpoint",
|
||||||
@@ -67,6 +74,40 @@
|
|||||||
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded",
|
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded",
|
||||||
"VadDetected": "google_transcribe::vad_detected"
|
"VadDetected": "google_transcribe::vad_detected"
|
||||||
},
|
},
|
||||||
|
"NuanceTranscriptionEvents": {
|
||||||
|
"Transcription": "nuance_transcribe::transcription",
|
||||||
|
"StartOfSpeech": "nuance_transcribe::start_of_speech",
|
||||||
|
"TranscriptionComplete": "nuance_transcribe::end_of_transcription",
|
||||||
|
"Error": "nuance_transcribe::error",
|
||||||
|
"VadDetected": "nuance_transcribe::vad_detected"
|
||||||
|
},
|
||||||
|
"NvidiaTranscriptionEvents": {
|
||||||
|
"Transcription": "nvidia_transcribe::transcription",
|
||||||
|
"StartOfSpeech": "nvidia_transcribe::start_of_speech",
|
||||||
|
"TranscriptionComplete": "nvidia_transcribe::end_of_transcription",
|
||||||
|
"Error": "nvidia_transcribe::error",
|
||||||
|
"VadDetected": "nvidia_transcribe::vad_detected"
|
||||||
|
},
|
||||||
|
"DeepgramTranscriptionEvents": {
|
||||||
|
"Transcription": "deepgram_transcribe::transcription",
|
||||||
|
"ConnectFailure": "deepgram_transcribe::connect_failed",
|
||||||
|
"Connect": "deepgram_transcribe::connect"
|
||||||
|
},
|
||||||
|
"SonioxTranscriptionEvents": {
|
||||||
|
"Transcription": "soniox_transcribe::transcription",
|
||||||
|
"Error": "soniox_transcribe::error"
|
||||||
|
},
|
||||||
|
"CobaltTranscriptionEvents": {
|
||||||
|
"Transcription": "cobalt_speech::transcription",
|
||||||
|
"CompileContext": "cobalt_speech::compile_context_response",
|
||||||
|
"Error": "cobalt_speech::error"
|
||||||
|
},
|
||||||
|
"IbmTranscriptionEvents": {
|
||||||
|
"Transcription": "ibm_transcribe::transcription",
|
||||||
|
"ConnectFailure": "ibm_transcribe::connect_failed",
|
||||||
|
"Connect": "ibm_transcribe::connect",
|
||||||
|
"Error": "ibm_transcribe::error"
|
||||||
|
},
|
||||||
"AwsTranscriptionEvents": {
|
"AwsTranscriptionEvents": {
|
||||||
"Transcription": "aws_transcribe::transcription",
|
"Transcription": "aws_transcribe::transcription",
|
||||||
"EndOfTranscript": "aws_transcribe::end_of_transcript",
|
"EndOfTranscript": "aws_transcribe::end_of_transcript",
|
||||||
@@ -81,6 +122,18 @@
|
|||||||
"NoSpeechDetected": "azure_transcribe::no_speech_detected",
|
"NoSpeechDetected": "azure_transcribe::no_speech_detected",
|
||||||
"VadDetected": "azure_transcribe::vad_detected"
|
"VadDetected": "azure_transcribe::vad_detected"
|
||||||
},
|
},
|
||||||
|
"JambonzTranscriptionEvents": {
|
||||||
|
"Transcription": "jambonz_transcribe::transcription",
|
||||||
|
"ConnectFailure": "jambonz_transcribe::connect_failed",
|
||||||
|
"Connect": "jambonz_transcribe::connect",
|
||||||
|
"Error": "jambonz_transcribe::error"
|
||||||
|
},
|
||||||
|
"AssemblyAiTranscriptionEvents": {
|
||||||
|
"Transcription": "assemblyai_transcribe::transcription",
|
||||||
|
"Error": "assemblyai_transcribe::error",
|
||||||
|
"ConnectFailure": "assemblyai_transcribe::connect_failed",
|
||||||
|
"Connect": "assemblyai_transcribe::connect"
|
||||||
|
},
|
||||||
"ListenEvents": {
|
"ListenEvents": {
|
||||||
"Connect": "mod_audio_fork::connect",
|
"Connect": "mod_audio_fork::connect",
|
||||||
"ConnectFailure": "mod_audio_fork::connect_failed",
|
"ConnectFailure": "mod_audio_fork::connect_failed",
|
||||||
@@ -118,10 +171,12 @@
|
|||||||
"session:new",
|
"session:new",
|
||||||
"session:reconnect",
|
"session:reconnect",
|
||||||
"session:redirect",
|
"session:redirect",
|
||||||
|
"session:adulting",
|
||||||
"call:status",
|
"call:status",
|
||||||
"queue:status",
|
"queue:status",
|
||||||
"dial:confirm",
|
"dial:confirm",
|
||||||
"verb:hook",
|
"verb:hook",
|
||||||
|
"verb:status",
|
||||||
"jambonz:error"
|
"jambonz:error"
|
||||||
],
|
],
|
||||||
"RecordState": {
|
"RecordState": {
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
const {execSync} = require('child_process');
|
const {execSync} = require('child_process');
|
||||||
|
const {
|
||||||
|
JAMBONES_FREESWITCH,
|
||||||
|
NODE_ENV,
|
||||||
|
JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS,
|
||||||
|
} = require('../config');
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const fsInventory = process.env.JAMBONES_FREESWITCH
|
const fsInventory = JAMBONES_FREESWITCH
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((fs) => {
|
.map((fs) => {
|
||||||
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
|
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
|
||||||
const opts = {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 (arr.length > 4) opts.advertisedAddress = arr[4];
|
||||||
if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
|
if (NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
|
||||||
return opts;
|
return opts;
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearChannels = () => {
|
const clearChannels = () => {
|
||||||
const {logger} = require('../..');
|
const {logger} = require('../..');
|
||||||
const pwd = fsInventory[0].secret;
|
const pwd = fsInventory[0].secret;
|
||||||
const maxDurationMins = process.env.JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS || 180;
|
const maxDurationMins = JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS;
|
||||||
|
|
||||||
const calls = execSync(`/usr/local/freeswitch/bin/fs_cli -p ${pwd} -x "show calls"`, {encoding: 'utf8'})
|
const calls = execSync(`/usr/local/freeswitch/bin/fs_cli -p ${pwd} -x "show calls"`, {encoding: 'utf8'})
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ const {decrypt} = require('./encrypt-decrypt');
|
|||||||
const sqlAccountDetails = `SELECT *
|
const sqlAccountDetails = `SELECT *
|
||||||
FROM accounts account
|
FROM accounts account
|
||||||
WHERE account.account_sid = ?`;
|
WHERE account.account_sid = ?`;
|
||||||
const sqlSpeechCredentials = `SELECT *
|
const sqlSpeechCredentialsForAccount = `SELECT *
|
||||||
FROM speech_credentials
|
FROM speech_credentials
|
||||||
WHERE account_sid = ? `;
|
WHERE account_sid = ? OR (account_sid is NULL AND service_provider_sid =
|
||||||
const sqlSpeechCredentialsForSP = `SELECT *
|
(SELECT service_provider_sid from accounts where account_sid = ?))`;
|
||||||
FROM speech_credentials
|
|
||||||
WHERE service_provider_sid =
|
|
||||||
(SELECT service_provider_sid from accounts where account_sid = ?)`;
|
|
||||||
const sqlQueryAccountCarrierByName = `SELECT voip_carrier_sid
|
const sqlQueryAccountCarrierByName = `SELECT voip_carrier_sid
|
||||||
FROM voip_carriers vc
|
FROM voip_carriers vc
|
||||||
WHERE vc.account_sid = ?
|
WHERE vc.account_sid = ?
|
||||||
@@ -20,6 +17,19 @@ WHERE vc.account_sid IS NULL
|
|||||||
AND vc.service_provider_sid =
|
AND vc.service_provider_sid =
|
||||||
(SELECT service_provider_sid from accounts where account_sid = ?)
|
(SELECT service_provider_sid from accounts where account_sid = ?)
|
||||||
AND vc.name = ?`;
|
AND vc.name = ?`;
|
||||||
|
const sqlQueryAccountPhoneNumber = `SELECT voip_carrier_sid
|
||||||
|
FROM phone_numbers pn
|
||||||
|
WHERE pn.account_sid = ?
|
||||||
|
AND pn.number = ?`;
|
||||||
|
const sqlQuerySPPhoneNumber = `SELECT voip_carrier_sid
|
||||||
|
FROM phone_numbers pn
|
||||||
|
WHERE pn.account_sid IS NULL
|
||||||
|
AND pn.service_provider_sid =
|
||||||
|
(SELECT service_provider_sid from accounts where account_sid = ?)
|
||||||
|
AND pn.number = ?`;
|
||||||
|
const sqlQueryGoogleCustomVoices = `SELECT *
|
||||||
|
FROM google_custom_voices
|
||||||
|
WHERE google_custom_voice_sid = ?`;
|
||||||
|
|
||||||
const speechMapper = (cred) => {
|
const speechMapper = (cred) => {
|
||||||
const {credential, ...obj} = cred;
|
const {credential, ...obj} = cred;
|
||||||
@@ -39,63 +49,109 @@ const speechMapper = (cred) => {
|
|||||||
obj.region = o.region;
|
obj.region = o.region;
|
||||||
obj.use_custom_stt = o.use_custom_stt;
|
obj.use_custom_stt = o.use_custom_stt;
|
||||||
obj.custom_stt_endpoint = o.custom_stt_endpoint;
|
obj.custom_stt_endpoint = o.custom_stt_endpoint;
|
||||||
|
obj.custom_stt_endpoint_url = o.custom_stt_endpoint_url;
|
||||||
obj.use_custom_tts = o.use_custom_tts;
|
obj.use_custom_tts = o.use_custom_tts;
|
||||||
obj.custom_tts_endpoint = o.custom_tts_endpoint;
|
obj.custom_tts_endpoint = o.custom_tts_endpoint;
|
||||||
|
obj.custom_tts_endpoint_url = o.custom_tts_endpoint_url;
|
||||||
}
|
}
|
||||||
else if ('wellsaid' === obj.vendor) {
|
else if ('wellsaid' === obj.vendor) {
|
||||||
const o = JSON.parse(decrypt(credential));
|
const o = JSON.parse(decrypt(credential));
|
||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
}
|
}
|
||||||
|
else if ('nuance' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.client_id = o.client_id;
|
||||||
|
obj.secret = o.secret;
|
||||||
|
obj.nuance_tts_uri = o.nuance_tts_uri;
|
||||||
|
obj.nuance_stt_uri = o.nuance_stt_uri;
|
||||||
|
}
|
||||||
|
else if ('ibm' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.tts_api_key = o.tts_api_key;
|
||||||
|
obj.tts_region = o.tts_region;
|
||||||
|
obj.stt_api_key = o.stt_api_key;
|
||||||
|
obj.stt_region = o.stt_region;
|
||||||
|
}
|
||||||
|
else if ('deepgram' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
obj.deepgram_stt_uri = o.deepgram_stt_uri;
|
||||||
|
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
|
||||||
|
}
|
||||||
|
else if ('soniox' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
}
|
||||||
|
else if ('nvidia' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.riva_server_uri = o.riva_server_uri;
|
||||||
|
}
|
||||||
|
else if ('cobalt' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.cobalt_server_uri = o.cobalt_server_uri;
|
||||||
|
} else if ('elevenlabs' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
obj.model_id = o.model_id;
|
||||||
|
obj.options = o.options;
|
||||||
|
} else if ('playht' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
obj.user_id = o.user_id;
|
||||||
|
obj.voice_engine = o.voice_engine;
|
||||||
|
obj.options = o.options;
|
||||||
|
} else if ('rimelabs' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
obj.model_id = o.model_id;
|
||||||
|
obj.options = o.options;
|
||||||
|
} else if ('assemblyai' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
} else if ('whisper' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
obj.model_id = o.model_id;
|
||||||
|
} else if (obj.vendor.startsWith('custom:')) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.auth_token = o.auth_token;
|
||||||
|
obj.custom_stt_url = o.custom_stt_url;
|
||||||
|
obj.custom_tts_url = o.custom_tts_url;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bucketCredentialDecrypt = (account) => {
|
||||||
|
const { bucket_credential } = account.account;
|
||||||
|
if (!bucket_credential || bucket_credential.vendor) return;
|
||||||
|
account.account.bucket_credential = JSON.parse(decrypt(bucket_credential));
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = (logger, srf) => {
|
module.exports = (logger, srf) => {
|
||||||
const {pool} = srf.locals.dbHelpers;
|
const {pool} = srf.locals.dbHelpers;
|
||||||
const pp = pool.promise();
|
const pp = pool.promise();
|
||||||
|
|
||||||
const lookupAccountDetails = async(account_sid) => {
|
const lookupAccountDetails = async(account_sid) => {
|
||||||
|
|
||||||
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, account_sid);
|
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, [account_sid]);
|
||||||
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
|
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
|
||||||
const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
|
const [r2] = await pp.query(sqlSpeechCredentialsForAccount, [account_sid, account_sid]);
|
||||||
const speech = r2.map(speechMapper);
|
const speech = r2.map(speechMapper);
|
||||||
|
|
||||||
/* search at the service provider level if we don't find it at the account level */
|
const account = r[0];
|
||||||
const haveGoogle = speech.find((s) => s.vendor === 'google');
|
bucketCredentialDecrypt(account);
|
||||||
const haveAws = speech.find((s) => s.vendor === 'aws');
|
|
||||||
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
|
|
||||||
const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid');
|
|
||||||
if (!haveGoogle || !haveAws || !haveMicrosoft) {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
if (!haveMicrosoft) {
|
|
||||||
const ms = r3.find((s) => s.vendor === 'microsoft');
|
|
||||||
if (ms) speech.push(speechMapper(ms));
|
|
||||||
}
|
|
||||||
if (!haveWellsaid) {
|
|
||||||
const wellsaid = r3.find((s) => s.vendor === 'wellsaid');
|
|
||||||
if (wellsaid) speech.push(speechMapper(wellsaid));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...r[0],
|
...account,
|
||||||
speech
|
speech
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSpeechCredentialLastUsed = async(speech_credential_sid) => {
|
const updateSpeechCredentialLastUsed = async(speech_credential_sid) => {
|
||||||
|
if (!speech_credential_sid) return;
|
||||||
const pp = pool.promise();
|
const pp = pool.promise();
|
||||||
const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?';
|
const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?';
|
||||||
try {
|
try {
|
||||||
@@ -117,9 +173,34 @@ module.exports = (logger, srf) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lookupCarrierByPhoneNumber = async(account_sid, phoneNumber) => {
|
||||||
|
const pp = pool.promise();
|
||||||
|
try {
|
||||||
|
const [r] = await pp.query(sqlQueryAccountPhoneNumber, [account_sid, phoneNumber]);
|
||||||
|
if (r.length) return r[0].voip_carrier_sid;
|
||||||
|
const [r2] = await pp.query(sqlQuerySPPhoneNumber, [account_sid, phoneNumber]);
|
||||||
|
if (r2.length) return r2[0].voip_carrier_sid;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({err}, `lookupPhoneNumber: Error ${account_sid}:${phoneNumber}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookupGoogleCustomVoice = async(google_custom_voice_sid) => {
|
||||||
|
const pp = pool.promise();
|
||||||
|
try {
|
||||||
|
const [r] = await pp.query(sqlQueryGoogleCustomVoices, [google_custom_voice_sid]);
|
||||||
|
return r;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({err}, `lookupGoogleCustomVoices: Error ${google_custom_voice_sid}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lookupAccountDetails,
|
lookupAccountDetails,
|
||||||
updateSpeechCredentialLastUsed,
|
updateSpeechCredentialLastUsed,
|
||||||
lookupCarrier
|
lookupCarrier,
|
||||||
|
lookupCarrierByPhoneNumber,
|
||||||
|
lookupGoogleCustomVoice
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
|
const {LEGACY_CRYPTO, ENCRYPTION_SECRET, JWT_SECRET} = require('../config');
|
||||||
|
const algorithm = LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const secretKey = crypto.createHash('sha256')
|
const secretKey = crypto.createHash('sha256')
|
||||||
.update(String(process.env.JWT_SECRET))
|
.update(ENCRYPTION_SECRET || JWT_SECRET)
|
||||||
.digest('base64')
|
.digest('base64')
|
||||||
.substr(0, 32);
|
.substring(0, 32);
|
||||||
|
|
||||||
const encrypt = (text) => {
|
const encrypt = (text) => {
|
||||||
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
|
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
|
||||||
@@ -25,8 +26,8 @@ const decrypt = (data) => {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
||||||
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
const decrypted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||||
return decrpyted.toString();
|
return decrypted.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const httpRoutes = require('../http-routes');
|
const httpRoutes = require('../http-routes');
|
||||||
const PORT = process.env.HTTP_PORT || 3000;
|
const {PORT, HTTP_PORT_MAX} = require('../config');
|
||||||
|
|
||||||
const doListen = (logger, app, port, resolve) => {
|
const doListen = (logger, app, port, resolve) => {
|
||||||
const server = app.listen(port, () => {
|
const server = app.listen(port, () => {
|
||||||
const {srf} = app.locals;
|
const {srf} = app.locals;
|
||||||
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
|
srf.locals.serviceUrl = `http://${srf.locals.ipv4}:${port}`;
|
||||||
|
logger.info(`listening for HTTP requests on port ${port}, serviceUrl is ${srf.locals.serviceUrl}`);
|
||||||
resolve({server, app});
|
resolve({server, app});
|
||||||
});
|
});
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
const handleErrors = (logger, app, resolve, reject, e) => {
|
const handleErrors = (logger, app, resolve, reject, e) => {
|
||||||
if (e.code === 'EADDRINUSE' &&
|
if (e.code === 'EADDRINUSE' &&
|
||||||
process.env.HTTP_PORT_MAX &&
|
HTTP_PORT_MAX &&
|
||||||
e.port < process.env.HTTP_PORT_MAX) {
|
e.port < HTTP_PORT_MAX) {
|
||||||
|
|
||||||
logger.info(`HTTP server failed to bind port on ${e.port}, will try next port`);
|
logger.info(`HTTP server failed to bind port on ${e.port}, will try next port`);
|
||||||
const server = doListen(logger, app, ++e.port, resolve);
|
const server = doListen(logger, app, ++e.port, resolve);
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
const {Client, Pool} = require('undici');
|
const {request, getGlobalDispatcher, setGlobalDispatcher, Dispatcher, ProxyAgent, Client, Pool} = require('undici');
|
||||||
const parseUrl = require('parse-url');
|
const parseUrl = require('parse-url');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const BaseRequestor = require('./base-requestor');
|
const BaseRequestor = require('./base-requestor');
|
||||||
const {HookMsgTypes} = require('./constants.json');
|
const {HookMsgTypes} = require('./constants.json');
|
||||||
const snakeCaseKeys = require('./snakecase-keys');
|
const snakeCaseKeys = require('./snakecase-keys');
|
||||||
const pools = new Map();
|
const pools = new Map();
|
||||||
const HTTP_TIMEOUT = 10000;
|
const {
|
||||||
|
HTTP_POOL,
|
||||||
|
HTTP_POOLSIZE,
|
||||||
|
HTTP_PIPELINING,
|
||||||
|
HTTP_TIMEOUT,
|
||||||
|
HTTP_PROXY_IP,
|
||||||
|
HTTP_PROXY_PORT,
|
||||||
|
HTTP_PROXY_PROTOCOL,
|
||||||
|
NODE_ENV,
|
||||||
|
HTTP_USER_AGENT_HEADER,
|
||||||
|
} = require('../config');
|
||||||
|
|
||||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||||
|
|
||||||
@@ -16,6 +26,15 @@ function basicAuth(username, password) {
|
|||||||
return {Authorization: header};
|
return {Authorization: header};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultDispatcher = HTTP_PROXY_IP ?
|
||||||
|
new ProxyAgent(`${HTTP_PROXY_PROTOCOL}://${HTTP_PROXY_IP}${HTTP_PROXY_PORT ? `:${HTTP_PROXY_PORT}` : ''}`) :
|
||||||
|
getGlobalDispatcher();
|
||||||
|
|
||||||
|
setGlobalDispatcher(new class extends Dispatcher {
|
||||||
|
dispatch(options, handler) {
|
||||||
|
return defaultDispatcher.dispatch(options, handler);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
|
||||||
class HttpRequestor extends BaseRequestor {
|
class HttpRequestor extends BaseRequestor {
|
||||||
constructor(logger, account_sid, hook, secret) {
|
constructor(logger, account_sid, hook, secret) {
|
||||||
@@ -34,15 +53,15 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
this._resource = u.resource;
|
this._resource = u.resource;
|
||||||
this._port = u.port;
|
this._port = u.port;
|
||||||
this._search = u.search;
|
this._search = u.search;
|
||||||
this._usePools = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
|
this._usePools = HTTP_POOL && parseInt(HTTP_POOL);
|
||||||
|
|
||||||
if (this._usePools) {
|
if (this._usePools) {
|
||||||
if (pools.has(this._baseUrl)) {
|
if (pools.has(this._baseUrl)) {
|
||||||
this.client = pools.get(this._baseUrl);
|
this.client = pools.get(this._baseUrl);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const connections = process.env.HTTP_POOLSIZE ? parseInt(process.env.HTTP_POOLSIZE) : 10;
|
const connections = HTTP_POOLSIZE ? parseInt(HTTP_POOLSIZE) : 10;
|
||||||
const pipelining = process.env.HTTP_PIPELINING ? parseInt(process.env.HTTP_PIPELINING) : 1;
|
const pipelining = HTTP_PIPELINING ? parseInt(HTTP_PIPELINING) : 1;
|
||||||
const pool = this.client = new Pool(this._baseUrl, {
|
const pool = this.client = new Pool(this._baseUrl, {
|
||||||
connections,
|
connections,
|
||||||
pipelining
|
pipelining
|
||||||
@@ -55,6 +74,18 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
if (u.port) this.client = new Client(`${u.protocol}://${u.resource}:${u.port}`);
|
if (u.port) this.client = new Client(`${u.protocol}://${u.resource}:${u.port}`);
|
||||||
else this.client = new Client(`${u.protocol}://${u.resource}`);
|
else this.client = new Client(`${u.protocol}://${u.resource}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (NODE_ENV == 'test' && process.env.JAMBONES_HTTP_PROXY_IP) {
|
||||||
|
const defDispatcher =
|
||||||
|
new ProxyAgent(`${process.env.JAMBONES_HTTP_PROXY_PROTOCOL}://${process.env.JAMBONES_HTTP_PROXY_IP}${
|
||||||
|
process.env.JAMBONES_HTTP_PROXY_PORT ? `:${process.env.JAMBONES_HTTP_PROXY_PORT}` : ''}`);
|
||||||
|
|
||||||
|
setGlobalDispatcher(new class extends Dispatcher {
|
||||||
|
dispatch(options, handler) {
|
||||||
|
return defDispatcher.dispatch(options, handler);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get baseUrl() {
|
get baseUrl() {
|
||||||
@@ -86,11 +117,28 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
const url = hook.url || hook;
|
const url = hook.url || hook;
|
||||||
const method = hook.method || 'POST';
|
const method = hook.method || 'POST';
|
||||||
let buf = '';
|
let buf = '';
|
||||||
|
httpHeaders = {
|
||||||
|
...httpHeaders,
|
||||||
|
...(HTTP_USER_AGENT_HEADER && {'user-agent' : HTTP_USER_AGENT_HEADER})
|
||||||
|
};
|
||||||
|
|
||||||
assert.ok(url, 'HttpRequestor:request url was not provided');
|
assert.ok(url, 'HttpRequestor:request url was not provided');
|
||||||
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
|
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
|
||||||
const startAt = process.hrtime();
|
const startAt = process.hrtime();
|
||||||
|
|
||||||
|
/* if we have an absolute url, and it is ws then do a websocket connection */
|
||||||
|
if (this._isAbsoluteUrl(url) && url.startsWith('ws')) {
|
||||||
|
const WsRequestor = require('./ws-requestor');
|
||||||
|
this.logger.debug({hook}, 'HttpRequestor: switching to websocket connection');
|
||||||
|
const h = typeof hook === 'object' ? hook : {url: hook};
|
||||||
|
const requestor = new WsRequestor(this.logger, this.account_sid, h, this.secret);
|
||||||
|
if (type === 'session:redirect') {
|
||||||
|
this.close();
|
||||||
|
this.emit('handover', requestor);
|
||||||
|
}
|
||||||
|
return requestor.request('session:new', hook, params, httpHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
let newClient;
|
let newClient;
|
||||||
try {
|
try {
|
||||||
let client, path, query;
|
let client, path, query;
|
||||||
@@ -121,7 +169,18 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
};
|
};
|
||||||
const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url;
|
const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url;
|
||||||
this.logger.debug({url, absUrl, hdrs}, 'send webhook');
|
this.logger.debug({url, absUrl, hdrs}, 'send webhook');
|
||||||
const {statusCode, headers, body} = await client.request({
|
const {statusCode, headers, body} = HTTP_PROXY_IP ? await request(
|
||||||
|
this.baseUrl,
|
||||||
|
{
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
method,
|
||||||
|
headers: hdrs,
|
||||||
|
...('POST' === method && {body: JSON.stringify(payload)}),
|
||||||
|
timeout: HTTP_TIMEOUT,
|
||||||
|
followRedirects: false
|
||||||
|
}
|
||||||
|
) : await client.request({
|
||||||
path,
|
path,
|
||||||
query,
|
query,
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
const Mrf = require('drachtio-fsmrf');
|
const Mrf = require('drachtio-fsmrf');
|
||||||
const ip = require('ip');
|
const ip = require('ip');
|
||||||
const PORT = process.env.HTTP_PORT || 3000;
|
const {
|
||||||
|
JAMBONES_MYSQL_HOST,
|
||||||
|
JAMBONES_MYSQL_USER,
|
||||||
|
JAMBONES_MYSQL_PASSWORD,
|
||||||
|
JAMBONES_MYSQL_DATABASE,
|
||||||
|
JAMBONES_MYSQL_CONNECTION_LIMIT,
|
||||||
|
JAMBONES_MYSQL_PORT,
|
||||||
|
JAMBONES_FREESWITCH,
|
||||||
|
SMPP_URL,
|
||||||
|
JAMBONES_TIME_SERIES_HOST,
|
||||||
|
JAMBONES_ESL_LISTEN_ADDRESS,
|
||||||
|
PORT,
|
||||||
|
NODE_ENV,
|
||||||
|
} = require('../config');
|
||||||
|
const Registrar = require('@jambonz/mw-registrar');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
|
||||||
function initMS(logger, wrapper, ms) {
|
function initMS(logger, wrapper, ms) {
|
||||||
@@ -42,18 +56,18 @@ function installSrfLocals(srf, logger) {
|
|||||||
let idxStart = 0;
|
let idxStart = 0;
|
||||||
|
|
||||||
(async function() {
|
(async function() {
|
||||||
const fsInventory = process.env.JAMBONES_FREESWITCH
|
const fsInventory = JAMBONES_FREESWITCH
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((fs) => {
|
.map((fs) => {
|
||||||
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
|
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
|
||||||
assert.ok(arr, `Invalid syntax JAMBONES_FREESWITCH: ${process.env.JAMBONES_FREESWITCH}`);
|
assert.ok(arr, `Invalid syntax JAMBONES_FREESWITCH: ${JAMBONES_FREESWITCH}`);
|
||||||
const opts = {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 (arr.length > 4) opts.advertisedAddress = arr[4];
|
||||||
/* NB: originally for testing only, but for now all jambonz deployments
|
/* NB: originally for testing only, but for now all jambonz deployments
|
||||||
have freeswitch installed locally alongside this app
|
have freeswitch installed locally alongside this app
|
||||||
*/
|
*/
|
||||||
if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
|
if (NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
|
||||||
else if (process.env.JAMBONES_ESL_LISTEN_ADDRESS) opts.listenAddress = process.env.JAMBONES_ESL_LISTEN_ADDRESS;
|
else if (JAMBONES_ESL_LISTEN_ADDRESS) opts.listenAddress = JAMBONES_ESL_LISTEN_ADDRESS;
|
||||||
return opts;
|
return opts;
|
||||||
});
|
});
|
||||||
logger.info({fsInventory}, 'freeswitch inventory');
|
logger.info({fsInventory}, 'freeswitch inventory');
|
||||||
@@ -123,22 +137,22 @@ function installSrfLocals(srf, logger) {
|
|||||||
lookupTeamsByAccount,
|
lookupTeamsByAccount,
|
||||||
lookupAccountBySid,
|
lookupAccountBySid,
|
||||||
lookupAccountCapacitiesBySid,
|
lookupAccountCapacitiesBySid,
|
||||||
lookupSmppGateways
|
lookupSmppGateways,
|
||||||
|
lookupClientByAccountAndUsername
|
||||||
} = require('@jambonz/db-helpers')({
|
} = require('@jambonz/db-helpers')({
|
||||||
host: process.env.JAMBONES_MYSQL_HOST,
|
host: JAMBONES_MYSQL_HOST,
|
||||||
user: process.env.JAMBONES_MYSQL_USER,
|
user: JAMBONES_MYSQL_USER,
|
||||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
port: JAMBONES_MYSQL_PORT || 3306,
|
||||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
password: JAMBONES_MYSQL_PASSWORD,
|
||||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
database: JAMBONES_MYSQL_DATABASE,
|
||||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
connectionLimit: JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||||
}, logger, tracer);
|
}, logger);
|
||||||
const {
|
const {
|
||||||
client,
|
client,
|
||||||
updateCallStatus,
|
updateCallStatus,
|
||||||
retrieveCall,
|
retrieveCall,
|
||||||
listCalls,
|
listCalls,
|
||||||
deleteCall,
|
deleteCall,
|
||||||
synthAudio,
|
|
||||||
createHash,
|
createHash,
|
||||||
retrieveHash,
|
retrieveHash,
|
||||||
deleteKey,
|
deleteKey,
|
||||||
@@ -151,19 +165,28 @@ function installSrfLocals(srf, logger) {
|
|||||||
pushBack,
|
pushBack,
|
||||||
popFront,
|
popFront,
|
||||||
removeFromList,
|
removeFromList,
|
||||||
|
getListPosition,
|
||||||
lengthOfList,
|
lengthOfList,
|
||||||
getListPosition
|
addToSortedSet,
|
||||||
} = require('@jambonz/realtimedb-helpers')({
|
retrieveFromSortedSet,
|
||||||
host: process.env.JAMBONES_REDIS_HOST,
|
retrieveByPatternSortedSet,
|
||||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
sortedSetLength,
|
||||||
}, logger, tracer);
|
sortedSetPositionByPattern
|
||||||
|
} = require('@jambonz/realtimedb-helpers')({}, logger, tracer);
|
||||||
|
const registrar = new Registrar(logger, client);
|
||||||
|
const {
|
||||||
|
synthAudio,
|
||||||
|
addFileToCache,
|
||||||
|
getNuanceAccessToken,
|
||||||
|
getIbmAccessToken,
|
||||||
|
} = require('@jambonz/speech-utils')({}, logger);
|
||||||
const {
|
const {
|
||||||
writeAlerts,
|
writeAlerts,
|
||||||
AlertType
|
AlertType
|
||||||
} = require('@jambonz/time-series')(logger, {
|
} = require('@jambonz/time-series')(logger, {
|
||||||
host: process.env.JAMBONES_TIME_SERIES_HOST,
|
host: JAMBONES_TIME_SERIES_HOST,
|
||||||
commitSize: 50,
|
commitSize: 50,
|
||||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
commitInterval: 'test' === NODE_ENV ? 7 : 20
|
||||||
});
|
});
|
||||||
|
|
||||||
let localIp;
|
let localIp;
|
||||||
@@ -176,6 +199,7 @@ function installSrfLocals(srf, logger) {
|
|||||||
srf.locals = {...srf.locals,
|
srf.locals = {...srf.locals,
|
||||||
dbHelpers: {
|
dbHelpers: {
|
||||||
client,
|
client,
|
||||||
|
registrar,
|
||||||
pool,
|
pool,
|
||||||
lookupAppByPhoneNumber,
|
lookupAppByPhoneNumber,
|
||||||
lookupAppByRegex,
|
lookupAppByRegex,
|
||||||
@@ -186,11 +210,13 @@ function installSrfLocals(srf, logger) {
|
|||||||
lookupAccountBySid,
|
lookupAccountBySid,
|
||||||
lookupAccountCapacitiesBySid,
|
lookupAccountCapacitiesBySid,
|
||||||
lookupSmppGateways,
|
lookupSmppGateways,
|
||||||
|
lookupClientByAccountAndUsername,
|
||||||
updateCallStatus,
|
updateCallStatus,
|
||||||
retrieveCall,
|
retrieveCall,
|
||||||
listCalls,
|
listCalls,
|
||||||
deleteCall,
|
deleteCall,
|
||||||
synthAudio,
|
synthAudio,
|
||||||
|
addFileToCache,
|
||||||
createHash,
|
createHash,
|
||||||
retrieveHash,
|
retrieveHash,
|
||||||
deleteKey,
|
deleteKey,
|
||||||
@@ -204,12 +230,19 @@ function installSrfLocals(srf, logger) {
|
|||||||
popFront,
|
popFront,
|
||||||
removeFromList,
|
removeFromList,
|
||||||
lengthOfList,
|
lengthOfList,
|
||||||
getListPosition
|
getListPosition,
|
||||||
|
getNuanceAccessToken,
|
||||||
|
getIbmAccessToken,
|
||||||
|
addToSortedSet,
|
||||||
|
retrieveFromSortedSet,
|
||||||
|
retrieveByPatternSortedSet,
|
||||||
|
sortedSetLength,
|
||||||
|
sortedSetPositionByPattern
|
||||||
},
|
},
|
||||||
parentLogger: logger,
|
parentLogger: logger,
|
||||||
getSBC,
|
getSBC,
|
||||||
getSmpp: () => {
|
getSmpp: () => {
|
||||||
return process.env.SMPP_URL;
|
return SMPP_URL;
|
||||||
},
|
},
|
||||||
lifecycleEmitter,
|
lifecycleEmitter,
|
||||||
getFreeswitch,
|
getFreeswitch,
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
function normalizeJambones(logger, obj) {
|
|
||||||
if (!Array.isArray(obj)) throw new Error('malformed jambonz payload: must be array');
|
|
||||||
const document = [];
|
|
||||||
for (const tdata of obj) {
|
|
||||||
if (typeof tdata !== 'object') throw new Error('malformed jambonz payload: must be array of objects');
|
|
||||||
if ('verb' in tdata) {
|
|
||||||
// {verb: 'say', text: 'foo..bar'..}
|
|
||||||
const name = tdata.verb;
|
|
||||||
const o = {};
|
|
||||||
Object.keys(tdata)
|
|
||||||
.filter((k) => k !== 'verb')
|
|
||||||
.forEach((k) => o[k] = tdata[k]);
|
|
||||||
const o2 = {};
|
|
||||||
o2[name] = o;
|
|
||||||
document.push(o2);
|
|
||||||
}
|
|
||||||
else if (Object.keys(tdata).length === 1) {
|
|
||||||
// {'say': {..}}
|
|
||||||
document.push(tdata);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
logger.info(tdata, 'malformed jambonz payload: missing verb property');
|
|
||||||
throw new Error('malformed jambonz payload: missing verb property');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.debug({document}, `normalizeJambones: returning document with ${document.length} tasks`);
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = normalizeJambones;
|
|
||||||
|
|
||||||
18
lib/utils/parse-decibels.js
Normal file
18
lib/utils/parse-decibels.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const parseDecibels = (db) => {
|
||||||
|
if (!db) return 0;
|
||||||
|
if (typeof db === 'number') {
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
else if (typeof db === 'string') {
|
||||||
|
const match = db.match(/([+-]?\d+(\.\d+)?)\s*db/i);
|
||||||
|
if (match) {
|
||||||
|
return Math.trunc(parseFloat(match[1]));
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = parseDecibels;
|
||||||
@@ -4,7 +4,7 @@ const SipError = require('drachtio-srf').SipError;
|
|||||||
const {TaskPreconditions, CallDirection} = require('../utils/constants');
|
const {TaskPreconditions, CallDirection} = require('../utils/constants');
|
||||||
const CallInfo = require('../session/call-info');
|
const CallInfo = require('../session/call-info');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const normalizeJambones = require('../utils/normalize-jambones');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
const makeTask = require('../tasks/make_task');
|
const makeTask = require('../tasks/make_task');
|
||||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||||
const AdultingCallSession = require('../session/adulting-call-session');
|
const AdultingCallSession = require('../session/adulting-call-session');
|
||||||
@@ -13,9 +13,16 @@ const moment = require('moment');
|
|||||||
const stripCodecs = require('./strip-ancillary-codecs');
|
const stripCodecs = require('./strip-ancillary-codecs');
|
||||||
const RootSpan = require('./call-tracer');
|
const RootSpan = require('./call-tracer');
|
||||||
const uuidv4 = require('uuid-random');
|
const uuidv4 = require('uuid-random');
|
||||||
|
const HttpRequestor = require('./http-requestor');
|
||||||
|
const WsRequestor = require('./ws-requestor');
|
||||||
|
const {makeOpusFirst} = require('./sdp-utils');
|
||||||
|
const {
|
||||||
|
JAMBONES_USE_FREESWITCH_TIMER_FD
|
||||||
|
} = require('../config');
|
||||||
|
|
||||||
class SingleDialer extends Emitter {
|
class SingleDialer extends Emitter {
|
||||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) {
|
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask,
|
||||||
|
onHoldMusic}) {
|
||||||
super();
|
super();
|
||||||
assert(target.type);
|
assert(target.type);
|
||||||
|
|
||||||
@@ -37,6 +44,8 @@ class SingleDialer extends Emitter {
|
|||||||
this.callGone = false;
|
this.callGone = false;
|
||||||
|
|
||||||
this.callSid = uuidv4();
|
this.callSid = uuidv4();
|
||||||
|
this.dialTask = dialTask;
|
||||||
|
this.onHoldMusic = onHoldMusic;
|
||||||
|
|
||||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||||
}
|
}
|
||||||
@@ -45,6 +54,10 @@ class SingleDialer extends Emitter {
|
|||||||
return this.callInfo.callStatus;
|
return this.callInfo.callStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get applicationSid() {
|
||||||
|
return this.application?.application_sid || this.callInfo?.applicationSid;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* can be used for all http requests within this session
|
* can be used for all http requests within this session
|
||||||
*/
|
*/
|
||||||
@@ -71,7 +84,8 @@ class SingleDialer extends Emitter {
|
|||||||
...(this.from.host && {'X-Preferred-From-Host': this.from.host}),
|
...(this.from.host && {'X-Preferred-From-Host': this.from.host}),
|
||||||
'X-Jambonz-Routing': this.target.type,
|
'X-Jambonz-Routing': this.target.type,
|
||||||
'X-Call-Sid': this.callSid,
|
'X-Call-Sid': this.callSid,
|
||||||
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
|
...(this.applicationSid && {'X-Application-Sid': this.applicationSid}),
|
||||||
|
...(this.target.proxy && {'X-SIP-Proxy': this.target.proxy})
|
||||||
};
|
};
|
||||||
if (srf.locals.fsUUID) {
|
if (srf.locals.fsUUID) {
|
||||||
opts.headers = {
|
opts.headers = {
|
||||||
@@ -122,6 +136,7 @@ class SingleDialer extends Emitter {
|
|||||||
this.serviceUrl = srf.locals.serviceUrl;
|
this.serviceUrl = srf.locals.serviceUrl;
|
||||||
|
|
||||||
this.ep = await ms.createEndpoint();
|
this.ep = await ms.createEndpoint();
|
||||||
|
this._configMsEndpoint();
|
||||||
this.logger.debug(`SingleDialer:exec - created endpoint ${this.ep.uuid}`);
|
this.logger.debug(`SingleDialer:exec - created endpoint ${this.ep.uuid}`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,7 +158,7 @@ class SingleDialer extends Emitter {
|
|||||||
|
|
||||||
Object.assign(opts, {
|
Object.assign(opts, {
|
||||||
proxy: `sip:${this.sbcAddress}`,
|
proxy: `sip:${this.sbcAddress}`,
|
||||||
localSdp: this.ep.local.sdp
|
localSdp: opts.opusFirst ? makeOpusFirst(this.ep.local.sdp) : this.ep.local.sdp
|
||||||
});
|
});
|
||||||
if (this.target.auth) opts.auth = this.target.auth;
|
if (this.target.auth) opts.auth = this.target.auth;
|
||||||
inviteSpan = this.startSpan('invite', {
|
inviteSpan = this.startSpan('invite', {
|
||||||
@@ -171,6 +186,7 @@ class SingleDialer extends Emitter {
|
|||||||
* (a) create a logger for this call
|
* (a) create a logger for this call
|
||||||
*/
|
*/
|
||||||
req.srf = srf;
|
req.srf = srf;
|
||||||
|
this.req = req;
|
||||||
this.callInfo = new CallInfo({
|
this.callInfo = new CallInfo({
|
||||||
direction: CallDirection.Outbound,
|
direction: CallDirection.Outbound,
|
||||||
parentCallInfo: this.parentCallInfo,
|
parentCallInfo: this.parentCallInfo,
|
||||||
@@ -179,6 +195,10 @@ class SingleDialer extends Emitter {
|
|||||||
callSid: this.callSid,
|
callSid: this.callSid,
|
||||||
traceId: this.rootSpan.traceId
|
traceId: this.rootSpan.traceId
|
||||||
});
|
});
|
||||||
|
if (this.dialTask && this.dialTask.tag !== null &&
|
||||||
|
typeof this.dialTask.tag === 'object' && !Array.isArray(this.dialTask.tag)) {
|
||||||
|
this.callInfo.customerData = this.dialTask.tag;
|
||||||
|
}
|
||||||
this.logger = srf.locals.parentLogger.child({
|
this.logger = srf.locals.parentLogger.child({
|
||||||
callSid: this.callSid,
|
callSid: this.callSid,
|
||||||
parentCallSid: this.parentCallInfo.callSid,
|
parentCallSid: this.parentCallInfo.callSid,
|
||||||
@@ -243,9 +263,14 @@ class SingleDialer extends Emitter {
|
|||||||
.on('modify', async(req, res) => {
|
.on('modify', async(req, res) => {
|
||||||
try {
|
try {
|
||||||
if (this.ep) {
|
if (this.ep) {
|
||||||
const newSdp = await this.ep.modify(req.body);
|
if (this.dialTask && this.dialTask.isOnHoldEnabled) {
|
||||||
res.send(200, {body: newSdp});
|
this.logger.info('dial is onhold, emit event');
|
||||||
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
this.emit('reinvite', req, res);
|
||||||
|
} else {
|
||||||
|
const newSdp = await this.ep.modify(req.body);
|
||||||
|
res.send(200, {body: newSdp});
|
||||||
|
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.logger.info('SingleDialer:exec: handling reINVITE with released media, emit event');
|
this.logger.info('SingleDialer:exec: handling reINVITE with released media, emit event');
|
||||||
@@ -270,17 +295,17 @@ class SingleDialer extends Emitter {
|
|||||||
if (err.status === 487) status.callStatus = CallStatus.NoAnswer;
|
if (err.status === 487) status.callStatus = CallStatus.NoAnswer;
|
||||||
else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy;
|
else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy;
|
||||||
this.logger.info(`SingleDialer:exec outdial failure ${err.status}`);
|
this.logger.info(`SingleDialer:exec outdial failure ${err.status}`);
|
||||||
inviteSpan.setAttributes({'invite.status_code': err.status});
|
inviteSpan?.setAttributes({'invite.status_code': err.status});
|
||||||
inviteSpan.end();
|
inviteSpan?.end();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.logger.error(err, 'SingleDialer:exec');
|
this.logger.error(err, 'SingleDialer:exec');
|
||||||
status.sipStatus = 500;
|
status.sipStatus = 500;
|
||||||
inviteSpan.setAttributes({
|
inviteSpan?.setAttributes({
|
||||||
'invite.status_code': 500,
|
'invite.status_code': 500,
|
||||||
'invite.err': err.message
|
'invite.err': err.message
|
||||||
});
|
});
|
||||||
inviteSpan.end();
|
inviteSpan?.end();
|
||||||
}
|
}
|
||||||
this.emit('callStatusChange', status);
|
this.emit('callStatusChange', status);
|
||||||
if (this.ep) this.ep.destroy();
|
if (this.ep) this.ep.destroy();
|
||||||
@@ -305,6 +330,16 @@ class SingleDialer extends Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_configMsEndpoint() {
|
||||||
|
const opts = {
|
||||||
|
...(this.onHoldMusic && {holdMusic: `shout://${this.onHoldMusic.replace(/^https?:\/\//, '')}`}),
|
||||||
|
...(JAMBONES_USE_FREESWITCH_TIMER_FD && {timer_name: 'timerfd'})
|
||||||
|
};
|
||||||
|
if (Object.keys(opts).length > 0) {
|
||||||
|
this.ep.set(opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run an application on the call after answer, e.g. call screening.
|
* Run an application on the call after answer, e.g. call screening.
|
||||||
* Once the application completes in some fashion, emit an 'accepted' event
|
* Once the application completes in some fashion, emit an 'accepted' event
|
||||||
@@ -316,10 +351,16 @@ class SingleDialer extends Emitter {
|
|||||||
try {
|
try {
|
||||||
// retrieve set of tasks
|
// retrieve set of tasks
|
||||||
const json = await this.requestor.request('dial:confirm', confirmHook, this.callInfo.toJSON());
|
const json = await this.requestor.request('dial:confirm', confirmHook, this.callInfo.toJSON());
|
||||||
|
if (!json || (Array.isArray(json) && json.length === 0)) {
|
||||||
|
this.logger.info('SingleDialer:_executeApp: no tasks returned from confirm hook');
|
||||||
|
this.emit('accept');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||||
// verify it contains only allowed verbs
|
// verify it contains only allowed verbs
|
||||||
const allowedTasks = tasks.filter((task) => {
|
const allowedTasks = tasks.filter((task) => {
|
||||||
return [
|
return [
|
||||||
|
TaskPreconditions.None,
|
||||||
TaskPreconditions.StableCall,
|
TaskPreconditions.StableCall,
|
||||||
TaskPreconditions.Endpoint
|
TaskPreconditions.Endpoint
|
||||||
].includes(task.preconditions);
|
].includes(task.preconditions);
|
||||||
@@ -367,15 +408,45 @@ class SingleDialer extends Emitter {
|
|||||||
this.dlg.linkedSpanId = this.rootSpan.traceId;
|
this.dlg.linkedSpanId = this.rootSpan.traceId;
|
||||||
const rootSpan = new RootSpan('outbound-call', this.dlg);
|
const rootSpan = new RootSpan('outbound-call', this.dlg);
|
||||||
const newLogger = logger.child({traceId: rootSpan.traceId});
|
const newLogger = logger.child({traceId: rootSpan.traceId});
|
||||||
|
//clone application from parent call with new requestor
|
||||||
|
//parrent application will be closed in case the parent hangup
|
||||||
|
const app = {...application};
|
||||||
|
if ('WS' === app.call_hook?.method ||
|
||||||
|
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
|
||||||
|
if (app.call_hook?.url) app.call_hook.url += '/adulting';
|
||||||
|
const requestor = new WsRequestor(logger, this.accountInfo.account.account_sid,
|
||||||
|
app.call_hook, this.accountInfo.account.webhook_secret);
|
||||||
|
app.requestor = requestor;
|
||||||
|
app.notifier = requestor;
|
||||||
|
app.call_hook.method = 'WS';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
app.requestor = new HttpRequestor(logger, this.accountInfo.account.account_sid,
|
||||||
|
app.call_hook, this.accountInfo.account.webhook_secret);
|
||||||
|
if (app.call_status_hook) app.notifier = new HttpRequestor(logger,
|
||||||
|
this.accountInfo.account.account_sid, app.call_status_hook,
|
||||||
|
this.accountInfo.account.webhook_secret);
|
||||||
|
else app.notifier = {request: () => {}, close: () => {}};
|
||||||
|
}
|
||||||
|
// Replace old application with new application.
|
||||||
|
this.application = app;
|
||||||
const cs = new AdultingCallSession({
|
const cs = new AdultingCallSession({
|
||||||
logger: newLogger,
|
logger: newLogger,
|
||||||
singleDialer: this,
|
singleDialer: this,
|
||||||
application,
|
application: app,
|
||||||
callInfo: this.callInfo,
|
callInfo: this.callInfo,
|
||||||
accountInfo: this.accountInfo,
|
accountInfo: this.accountInfo,
|
||||||
tasks,
|
tasks,
|
||||||
rootSpan
|
rootSpan
|
||||||
});
|
});
|
||||||
|
app.requestor.request('session:adulting', '/adulting', {
|
||||||
|
...cs.callInfo.toJSON(),
|
||||||
|
parentCallInfo: this.parentCallInfo
|
||||||
|
}).catch((err) => {
|
||||||
|
newLogger.error({err}, 'doAdulting: error sending adulting request');
|
||||||
|
});
|
||||||
|
|
||||||
|
cs.req = this.req;
|
||||||
cs.exec().catch((err) => newLogger.error({err}, 'doAdulting: error executing session'));
|
cs.exec().catch((err) => newLogger.error({err}, 'doAdulting: error executing session'));
|
||||||
return cs;
|
return cs;
|
||||||
}
|
}
|
||||||
@@ -396,6 +467,7 @@ class SingleDialer extends Emitter {
|
|||||||
async reAnchorMedia() {
|
async reAnchorMedia() {
|
||||||
assert(this.dlg && this.dlg.connected && !this.ep);
|
assert(this.dlg && this.dlg.connected && !this.ep);
|
||||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||||
|
this._configMsEndpoint();
|
||||||
await this.dlg.modify(this.ep.local.sdp, {
|
await this.dlg.modify(this.ep.local.sdp, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-Reason': 'anchor-media'
|
'X-Reason': 'anchor-media'
|
||||||
@@ -426,11 +498,12 @@ class SingleDialer extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function placeOutdial({
|
function placeOutdial({
|
||||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan
|
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask,
|
||||||
|
onHoldMusic
|
||||||
}) {
|
}) {
|
||||||
const myOpts = deepcopy(opts);
|
const myOpts = deepcopy(opts);
|
||||||
const sd = new SingleDialer({
|
const sd = new SingleDialer({
|
||||||
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan
|
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask, onHoldMusic
|
||||||
});
|
});
|
||||||
sd.exec(srf, ms, myOpts);
|
sd.exec(srf, ms, myOpts);
|
||||||
return sd;
|
return sd;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const timeSeries = require('@jambonz/time-series');
|
const timeSeries = require('@jambonz/time-series');
|
||||||
|
const {
|
||||||
|
NODE_ENV,
|
||||||
|
JAMBONES_TIME_SERIES_HOST
|
||||||
|
} = require('../config');
|
||||||
let alerter ;
|
let alerter ;
|
||||||
|
|
||||||
function isAbsoluteUrl(u) {
|
function isAbsoluteUrl(u) {
|
||||||
@@ -28,9 +32,9 @@ class Requestor {
|
|||||||
|
|
||||||
if (!alerter) {
|
if (!alerter) {
|
||||||
alerter = timeSeries(logger, {
|
alerter = timeSeries(logger, {
|
||||||
host: process.env.JAMBONES_TIME_SERIES_HOST,
|
host: JAMBONES_TIME_SERIES_HOST,
|
||||||
commitSize: 50,
|
commitSize: 50,
|
||||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
commitInterval: 'test' === NODE_ENV ? 7 : 20
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,9 +42,9 @@ class Requestor {
|
|||||||
get Alerter() {
|
get Alerter() {
|
||||||
if (!alerter) {
|
if (!alerter) {
|
||||||
alerter = timeSeries(this.logger, {
|
alerter = timeSeries(this.logger, {
|
||||||
host: process.env.JAMBONES_TIME_SERIES_HOST,
|
host: JAMBONES_TIME_SERIES_HOST,
|
||||||
commitSize: 50,
|
commitSize: 50,
|
||||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
commitInterval: 'test' === NODE_ENV ? 7 : 20
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return alerter;
|
return alerter;
|
||||||
|
|||||||
@@ -4,28 +4,38 @@ const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./constants');
|
|||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const debug = require('debug')('jambonz:feature-server');
|
const debug = require('debug')('jambonz:feature-server');
|
||||||
const noopLogger = {info: () => {}, error: () => {}};
|
const noopLogger = {info: () => {}, error: () => {}};
|
||||||
|
const {
|
||||||
|
JAMBONES_SBCS,
|
||||||
|
K8S,
|
||||||
|
K8S_SBC_SIP_SERVICE_NAME,
|
||||||
|
AWS_SNS_TOPIC_ARM,
|
||||||
|
OPTIONS_PING_INTERVAL,
|
||||||
|
AWS_REGION,
|
||||||
|
NODE_ENV,
|
||||||
|
JAMBONES_CLUSTER_ID,
|
||||||
|
} = require('../config');
|
||||||
|
|
||||||
module.exports = (logger) => {
|
module.exports = (logger) => {
|
||||||
logger = logger || noopLogger;
|
logger = logger || noopLogger;
|
||||||
let idxSbc = 0;
|
let idxSbc = 0;
|
||||||
let sbcs = [];
|
let sbcs = [];
|
||||||
|
|
||||||
if (process.env.JAMBONES_SBCS) {
|
if (JAMBONES_SBCS) {
|
||||||
sbcs = process.env.JAMBONES_SBCS
|
sbcs = JAMBONES_SBCS
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((sbc) => sbc.trim());
|
.map((sbc) => sbc.trim());
|
||||||
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
|
assert.ok(sbcs.length, 'JAMBONES_SBCS env var is empty or misconfigured');
|
||||||
logger.info({sbcs}, 'SBC inventory');
|
logger.info({sbcs}, 'SBC inventory');
|
||||||
}
|
}
|
||||||
else if (process.env.K8S && process.env.K8S_SBC_SIP_SERVICE_NAME) {
|
else if (K8S && K8S_SBC_SIP_SERVICE_NAME) {
|
||||||
sbcs = [`${process.env.K8S_SBC_SIP_SERVICE_NAME}:5060`];
|
sbcs = [`${K8S_SBC_SIP_SERVICE_NAME}:5060`];
|
||||||
logger.info({sbcs}, 'SBC inventory');
|
logger.info({sbcs}, 'SBC inventory');
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen for SNS lifecycle changes
|
// listen for SNS lifecycle changes
|
||||||
let lifecycleEmitter = new Emitter();
|
let lifecycleEmitter = new Emitter();
|
||||||
let dryUpCalls = false;
|
let dryUpCalls = false;
|
||||||
if (process.env.AWS_SNS_TOPIC_ARM && process.env.AWS_REGION) {
|
if (AWS_SNS_TOPIC_ARM && AWS_REGION) {
|
||||||
|
|
||||||
(async function() {
|
(async function() {
|
||||||
try {
|
try {
|
||||||
@@ -75,9 +85,13 @@ module.exports = (logger) => {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
else if (K8S) {
|
||||||
|
lifecycleEmitter.scaleIn = () => process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function pingProxies(srf) {
|
async function pingProxies(srf) {
|
||||||
if (process.env.NODE_ENV === 'test') return;
|
if (NODE_ENV === 'test') return;
|
||||||
|
|
||||||
for (const sbc of sbcs) {
|
for (const sbc of sbcs) {
|
||||||
try {
|
try {
|
||||||
@@ -87,7 +101,8 @@ module.exports = (logger) => {
|
|||||||
method: 'OPTIONS',
|
method: 'OPTIONS',
|
||||||
headers: {
|
headers: {
|
||||||
'X-FS-Status': ms && !dryUpCalls ? 'open' : 'closed',
|
'X-FS-Status': ms && !dryUpCalls ? 'open' : 'closed',
|
||||||
'X-FS-Calls': srf.locals.sessionTracker.count
|
'X-FS-Calls': srf.locals.sessionTracker.count,
|
||||||
|
'X-FS-ServiceUrl': srf.locals.serviceUrl
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
req.on('response', (res) => {
|
req.on('response', (res) => {
|
||||||
@@ -98,7 +113,7 @@ module.exports = (logger) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process.env.K8S) {
|
if (K8S) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
logger.info('disabling OPTIONS pings since we are running as a kubernetes service');
|
logger.info('disabling OPTIONS pings since we are running as a kubernetes service');
|
||||||
const {srf} = require('../..');
|
const {srf} = require('../..');
|
||||||
@@ -119,16 +134,16 @@ module.exports = (logger) => {
|
|||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const {srf} = require('../..');
|
const {srf} = require('../..');
|
||||||
pingProxies(srf);
|
pingProxies(srf);
|
||||||
}, process.env.OPTIONS_PING_INTERVAL || 30000);
|
}, OPTIONS_PING_INTERVAL);
|
||||||
|
|
||||||
// initial ping once we are up
|
// initial ping once we are up
|
||||||
setTimeout(async() => {
|
setTimeout(async() => {
|
||||||
|
|
||||||
// if SBCs are auto-scaling, monitor them as they come and go
|
// if SBCs are auto-scaling, monitor them as they come and go
|
||||||
const {srf} = require('../..');
|
const {srf} = require('../..');
|
||||||
if (!process.env.JAMBONES_SBCS) {
|
if (!JAMBONES_SBCS) {
|
||||||
const {monitorSet} = srf.locals.dbHelpers;
|
const {monitorSet} = srf.locals.dbHelpers;
|
||||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-sip`;
|
const setName = `${(JAMBONES_CLUSTER_ID || 'default')}:active-sip`;
|
||||||
await monitorSet(setName, 10, (members) => {
|
await monitorSet(setName, 10, (members) => {
|
||||||
sbcs = members;
|
sbcs = members;
|
||||||
logger.info(`sbc-pinger: SBC roster has changed, list of active SBCs is now ${sbcs}`);
|
logger.info(`sbc-pinger: SBC roster has changed, list of active SBCs is now ${sbcs}`);
|
||||||
|
|||||||
58
lib/utils/sdp-utils.js
Normal file
58
lib/utils/sdp-utils.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const sdpTransform = require('sdp-transform');
|
||||||
|
|
||||||
|
const isOnhold = (sdp) => {
|
||||||
|
return sdp && (sdp.includes('a=sendonly') || sdp.includes('a=inactive'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeSdpMedia = (sdp1, sdp2) => {
|
||||||
|
const parsedSdp1 = sdpTransform.parse(sdp1);
|
||||||
|
const parsedSdp2 = sdpTransform.parse(sdp2);
|
||||||
|
|
||||||
|
parsedSdp1.media.push(...parsedSdp2.media);
|
||||||
|
return sdpTransform.write(parsedSdp1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCodecPlacement = (parsedSdp, codec) => parsedSdp?.media[0]?.rtp?.findIndex((e) => e.codec === codec);
|
||||||
|
|
||||||
|
const isOpusFirst = (sdp) => {
|
||||||
|
return getCodecPlacement(sdpTransform.parse(sdp), 'opus') === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeOpusFirst = (sdp) => {
|
||||||
|
const parsedSdp = sdpTransform.parse(sdp);
|
||||||
|
// Find the index of the OPUS codec
|
||||||
|
const opusIndex = getCodecPlacement(parsedSdp, 'opus');
|
||||||
|
|
||||||
|
// Move OPUS codec to the beginning
|
||||||
|
if (opusIndex > 0) {
|
||||||
|
const opusEntry = parsedSdp.media[0].rtp.splice(opusIndex, 1)[0];
|
||||||
|
parsedSdp.media[0].rtp.unshift(opusEntry);
|
||||||
|
|
||||||
|
// Also move the corresponding payload type in the "m" line
|
||||||
|
const opusPayloadType = parsedSdp.media[0].payloads.split(' ')[opusIndex];
|
||||||
|
const otherPayloadTypes = parsedSdp.media[0].payloads.split(' ').filter((pt) => pt != opusPayloadType);
|
||||||
|
parsedSdp.media[0].payloads = [opusPayloadType, ...otherPayloadTypes].join(' ');
|
||||||
|
}
|
||||||
|
return sdpTransform.write(parsedSdp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractSdpMedia = (sdp) => {
|
||||||
|
const parsedSdp1 = sdpTransform.parse(sdp);
|
||||||
|
if (parsedSdp1.media.length > 1) {
|
||||||
|
parsedSdp1.media = [parsedSdp1.media[0]];
|
||||||
|
const parsedSdp2 = sdpTransform.parse(sdp);
|
||||||
|
parsedSdp2.media = [parsedSdp2.media[1]];
|
||||||
|
|
||||||
|
return [sdpTransform.write(parsedSdp1), sdpTransform.write(parsedSdp2)];
|
||||||
|
} else {
|
||||||
|
return [sdp, sdp];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isOnhold,
|
||||||
|
mergeSdpMedia,
|
||||||
|
extractSdpMedia,
|
||||||
|
isOpusFirst,
|
||||||
|
makeOpusFirst
|
||||||
|
};
|
||||||
@@ -47,8 +47,16 @@ const parseSiprecPayload = (req, logger) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sdp || !meta) {
|
|
||||||
logger.info({payload: req.payload}, 'invalid SIPREC payload');
|
if (!meta && sdp) {
|
||||||
|
const arr = /^([^]+)(m=[^]+?)(m=[^]+?)$/.exec(sdp);
|
||||||
|
opts.sdp1 = `${arr[1]}${arr[2]}`;
|
||||||
|
opts.sdp2 = `${arr[1]}${arr[3]}\r\n`;
|
||||||
|
opts.sessionId = uuidv4();
|
||||||
|
logger.info({ payload: req.payload }, 'SIPREC payload with no metadata (e.g. Cisco NBR)');
|
||||||
|
resolve(opts);
|
||||||
|
} else if (!sdp || !meta) {
|
||||||
|
logger.info({ payload: req.payload }, 'invalid SIPREC payload');
|
||||||
return reject(new Error('expected multipart SIPREC body'));
|
return reject(new Error('expected multipart SIPREC body'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +97,12 @@ const parseSiprecPayload = (req, logger) => {
|
|||||||
obj[`${prefix}participantstreamassoc`].forEach((ps) => {
|
obj[`${prefix}participantstreamassoc`].forEach((ps) => {
|
||||||
const part = participants[ps.$.participant_id];
|
const part = participants[ps.$.participant_id];
|
||||||
if (part) {
|
if (part) {
|
||||||
part.send = ps[`${prefix}send`][0];
|
if (ps.hasOwnProperty(`${prefix}send`)) {
|
||||||
part.recv = ps[`${prefix}recv`][0];
|
part.send = ps[`${prefix}send`][0];
|
||||||
|
}
|
||||||
|
if (ps.hasOwnProperty(`${prefix}recv`)) {
|
||||||
|
part.recv = ps[`${prefix}recv`][0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -101,9 +113,9 @@ const parseSiprecPayload = (req, logger) => {
|
|||||||
obj[`${prefix}stream`].forEach((s) => {
|
obj[`${prefix}stream`].forEach((s) => {
|
||||||
const streamId = s.$.stream_id;
|
const streamId = s.$.stream_id;
|
||||||
let sender;
|
let sender;
|
||||||
for (const [k, v] of Object.entries(participants)) {
|
for (const v of Object.values(participants)) {
|
||||||
if (v.send === streamId) {
|
if (v.send === streamId) {
|
||||||
sender = k;
|
sender = v;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,9 +125,15 @@ const parseSiprecPayload = (req, logger) => {
|
|||||||
|
|
||||||
sender.label = s[`${prefix}label`][0];
|
sender.label = s[`${prefix}label`][0];
|
||||||
|
|
||||||
if (-1 !== ['1', 'a_leg', 'inbound'].indexOf(sender.label)) {
|
if (-1 !== ['1', 'a_leg', 'inbound', '10'].indexOf(sender.label)) {
|
||||||
opts.caller.aor = sender.aor ;
|
opts.caller.aor = sender.aor;
|
||||||
if (sender.name) opts.caller.name = sender.name;
|
if (sender.name) opts.caller.name = sender.name;
|
||||||
|
// Remap the sdp stream base on sender label
|
||||||
|
if (!opts.sdp1.includes(`a=label:${sender.label}`)) {
|
||||||
|
const tmp = opts.sdp1;
|
||||||
|
opts.sdp1 = opts.sdp2;
|
||||||
|
opts.sdp2 = tmp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
opts.callee.aor = sender.aor ;
|
opts.callee.aor = sender.aor ;
|
||||||
@@ -242,7 +260,8 @@ const createSipRecPayload = (sdp1, sdp2, logger) => {
|
|||||||
.replace(/a=sendonly\r\n/g, '')
|
.replace(/a=sendonly\r\n/g, '')
|
||||||
.replace(/a=direction:both\r\n/g, '');
|
.replace(/a=direction:both\r\n/g, '');
|
||||||
*/
|
*/
|
||||||
return combinedSdp;
|
|
||||||
|
return combinedSdp.replace(/sendrecv/g, 'recvonly');
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { parseSiprecPayload, createSipRecPayload } ;
|
module.exports = { parseSiprecPayload, createSipRecPayload } ;
|
||||||
|
|||||||
@@ -1,33 +1,877 @@
|
|||||||
module.exports = (logger) => {
|
const {
|
||||||
const normalizeTranscription = (evt, vendor, channel) => {
|
TaskName,
|
||||||
if ('aws' === vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
} = require('./constants.json');
|
||||||
if ('microsoft' === vendor) {
|
|
||||||
const nbest = evt.NBest;
|
|
||||||
const language_code = evt.PrimaryLanguage?.Language || this.language;
|
|
||||||
const alternatives = nbest ? nbest.map((n) => {
|
|
||||||
return {
|
|
||||||
confidence: n.Confidence,
|
|
||||||
transcript: n.Display
|
|
||||||
};
|
|
||||||
}) :
|
|
||||||
[
|
|
||||||
{
|
|
||||||
transcript: evt.DisplayText || evt.Text
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const newEvent = {
|
const stickyVars = {
|
||||||
is_final: evt.RecognitionStatus === 'Success',
|
google: [
|
||||||
channel,
|
'GOOGLE_SPEECH_HINTS',
|
||||||
language_code,
|
'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL',
|
||||||
alternatives
|
'GOOGLE_SPEECH_PROFANITY_FILTER',
|
||||||
};
|
'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
|
||||||
evt = newEvent;
|
'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS',
|
||||||
|
'GOOGLE_SPEECH_SINGLE_UTTERANCE',
|
||||||
|
'GOOGLE_SPEECH_SPEAKER_DIARIZATION',
|
||||||
|
'GOOGLE_SPEECH_USE_ENHANCED',
|
||||||
|
'GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES',
|
||||||
|
'GOOGLE_SPEECH_METADATA_INTERACTION_TYPE',
|
||||||
|
'GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE'
|
||||||
|
],
|
||||||
|
microsoft: [
|
||||||
|
'AZURE_SPEECH_HINTS',
|
||||||
|
'AZURE_SERVICE_ENDPOINT_ID',
|
||||||
|
'AZURE_REQUEST_SNR',
|
||||||
|
'AZURE_PROFANITY_OPTION',
|
||||||
|
'AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES',
|
||||||
|
'AZURE_SERVICE_ENDPOINT',
|
||||||
|
'AZURE_INITIAL_SPEECH_TIMEOUT_MS',
|
||||||
|
'AZURE_USE_OUTPUT_FORMAT_DETAILED',
|
||||||
|
'AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS'
|
||||||
|
],
|
||||||
|
deepgram: [
|
||||||
|
'DEEPGRAM_SPEECH_KEYWORDS',
|
||||||
|
'DEEPGRAM_API_KEY',
|
||||||
|
'DEEPGRAM_SPEECH_TIER',
|
||||||
|
'DEEPGRAM_SPEECH_MODEL',
|
||||||
|
'DEEPGRAM_SPEECH_ENABLE_SMART_FORMAT',
|
||||||
|
'DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
|
||||||
|
'DEEPGRAM_SPEECH_PROFANITY_FILTER',
|
||||||
|
'DEEPGRAM_SPEECH_REDACT',
|
||||||
|
'DEEPGRAM_SPEECH_DIARIZE',
|
||||||
|
'DEEPGRAM_SPEECH_NER',
|
||||||
|
'DEEPGRAM_SPEECH_ALTERNATIVES',
|
||||||
|
'DEEPGRAM_SPEECH_NUMERALS',
|
||||||
|
'DEEPGRAM_SPEECH_SEARCH',
|
||||||
|
'DEEPGRAM_SPEECH_REPLACE',
|
||||||
|
'DEEPGRAM_SPEECH_ENDPOINTING',
|
||||||
|
'DEEPGRAM_SPEECH_UTTERANCE_END_MS',
|
||||||
|
'DEEPGRAM_SPEECH_VAD_TURNOFF',
|
||||||
|
'DEEPGRAM_SPEECH_TAG'
|
||||||
|
],
|
||||||
|
aws: [
|
||||||
|
'AWS_VOCABULARY_NAME',
|
||||||
|
'AWS_VOCABULARY_FILTER_METHOD',
|
||||||
|
'AWS_VOCABULARY_FILTER_NAME'
|
||||||
|
],
|
||||||
|
nuance: [
|
||||||
|
'NUANCE_ACCESS_TOKEN',
|
||||||
|
'NUANCE_KRYPTON_ENDPOINT',
|
||||||
|
'NUANCE_TOPIC',
|
||||||
|
'NUANCE_UTTERANCE_DETECTION_MODE',
|
||||||
|
'NUANCE_FILTER_PROFANITY',
|
||||||
|
'NUANCE_INCLUDE_TOKENIZATION',
|
||||||
|
'NUANCE_DISCARD_SPEAKER_ADAPTATION',
|
||||||
|
'NUANCE_SUPPRESS_CALL_RECORDING',
|
||||||
|
'NUANCE_MASK_LOAD_FAILURES',
|
||||||
|
'NUANCE_SUPPRESS_INITIAL_CAPITALIZATION',
|
||||||
|
'NUANCE_ALLOW_ZERO_BASE_LM_WEIGHT',
|
||||||
|
'NUANCE_FILTER_WAKEUP_WORD',
|
||||||
|
'NUANCE_NO_INPUT_TIMEOUT_MS',
|
||||||
|
'NUANCE_RECOGNITION_TIMEOUT_MS',
|
||||||
|
'NUANCE_UTTERANCE_END_SILENCE_MS',
|
||||||
|
'NUANCE_MAX_HYPOTHESES',
|
||||||
|
'NUANCE_SPEECH_DOMAIN',
|
||||||
|
'NUANCE_FORMATTING',
|
||||||
|
'NUANCE_RESOURCES'
|
||||||
|
],
|
||||||
|
ibm: [
|
||||||
|
'IBM_ACCESS_TOKEN',
|
||||||
|
'IBM_SPEECH_REGION',
|
||||||
|
'IBM_SPEECH_INSTANCE_ID',
|
||||||
|
'IBM_SPEECH_MODEL',
|
||||||
|
'IBM_SPEECH_LANGUAGE_CUSTOMIZATION_ID',
|
||||||
|
'IBM_SPEECH_ACOUSTIC_CUSTOMIZATION_ID',
|
||||||
|
'IBM_SPEECH_BASE_MODEL_VERSION',
|
||||||
|
'IBM_SPEECH_WATSON_METADATA',
|
||||||
|
'IBM_SPEECH_WATSON_LEARNING_OPT_OUT'
|
||||||
|
],
|
||||||
|
nvidia: [
|
||||||
|
'NVIDIA_HINTS'
|
||||||
|
],
|
||||||
|
cobalt: [
|
||||||
|
'COBALT_SPEECH_HINTS',
|
||||||
|
'COBALT_COMPILED_CONTEXT_DATA',
|
||||||
|
'COBALT_METADATA'
|
||||||
|
],
|
||||||
|
soniox: [
|
||||||
|
'SONIOX_PROFANITY_FILTER',
|
||||||
|
'SONIOX_MODEL'
|
||||||
|
],
|
||||||
|
assemblyai: [
|
||||||
|
'ASSEMBLYAI_API_KEY',
|
||||||
|
'ASSEMBLYAI_WORD_BOOST'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://developers.deepgram.com/docs/models-languages-overview
|
||||||
|
*/
|
||||||
|
const optimalDeepramModels = {
|
||||||
|
zh: ['base', 'base'],
|
||||||
|
'zh-CN':['base', 'base'],
|
||||||
|
'zh-TW': ['base', 'base'],
|
||||||
|
da: ['enhanced', 'enhanced'],
|
||||||
|
en: ['nova-2-phonecall', 'nova-2'],
|
||||||
|
'en-US': ['nova-2-phonecall', 'nova-2'],
|
||||||
|
'en-AU': ['nova-2', 'nova-2'],
|
||||||
|
'en-GB': ['nova-2', 'nova-2'],
|
||||||
|
'en-IN': ['nova-2', 'nova-2'],
|
||||||
|
'en-NZ': ['nova-2', 'nova-2'],
|
||||||
|
nl: ['nova-2', 'nova-2'],
|
||||||
|
fr: ['nova-2', 'nova-2'],
|
||||||
|
'fr-CA': ['nova-2', 'nova-2'],
|
||||||
|
de: ['nova-2', 'nova-2'],
|
||||||
|
hi: ['nova-2', 'nova-2'],
|
||||||
|
'hi-Latn': ['nova-2', 'nova-2'],
|
||||||
|
id: ['base', 'base'],
|
||||||
|
it: ['nova-2', 'nova-2'],
|
||||||
|
ja: ['enhanced', 'enhanced'],
|
||||||
|
ko: ['nova-2', 'nova-2'],
|
||||||
|
no: ['nova-2', 'nova-2'],
|
||||||
|
pl: ['nova-2', 'nova-2'],
|
||||||
|
pt: ['nova-2', 'nova-2'],
|
||||||
|
'pt-BR': ['nova-2', 'nova-2'],
|
||||||
|
'pt-PT': ['nova-2', 'nova-2'],
|
||||||
|
ru: ['nova-2', 'nova-2'],
|
||||||
|
es: ['nova-2', 'nova-2'],
|
||||||
|
'es-419': ['nova-2', 'nova-2'],
|
||||||
|
'es-LATAM': ['enhanced', 'enhanced'],
|
||||||
|
sv: ['nova-2', 'nova-2'],
|
||||||
|
ta: ['enhanced', 'enhanced'],
|
||||||
|
taq: ['enhanced', 'enhanced'],
|
||||||
|
tr: ['nova-2', 'nova-2'],
|
||||||
|
uk: ['nova-2', 'nova-2']
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectDefaultDeepgramModel = (task, language) => {
|
||||||
|
if (language in optimalDeepramModels) {
|
||||||
|
const [gather, transcribe] = optimalDeepramModels[language];
|
||||||
|
return task.name === TaskName.Gather ? gather : transcribe;
|
||||||
|
}
|
||||||
|
return 'base';
|
||||||
|
};
|
||||||
|
|
||||||
|
const consolidateTranscripts = (bufferedTranscripts, channel, language, vendor) => {
|
||||||
|
if (bufferedTranscripts.length === 1) return bufferedTranscripts[0];
|
||||||
|
let totalConfidence = 0;
|
||||||
|
const finalTranscript = bufferedTranscripts.reduce((acc, evt) => {
|
||||||
|
totalConfidence += evt.alternatives[0].confidence;
|
||||||
|
|
||||||
|
let newTranscript = evt.alternatives[0].transcript;
|
||||||
|
|
||||||
|
// If new transcript consists only of digits, spaces, and a trailing comma or period
|
||||||
|
if (newTranscript.match(/^[\d\s]+[,.]?$/)) {
|
||||||
|
newTranscript = newTranscript.replace(/\s/g, ''); // Remove all spaces
|
||||||
|
if (newTranscript.endsWith(',')) {
|
||||||
|
newTranscript = newTranscript.slice(0, -1); // Remove the trailing comma
|
||||||
|
} else if (newTranscript.endsWith('.')) {
|
||||||
|
newTranscript = newTranscript.slice(0, -1); // Remove the trailing period
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastChar = acc.alternatives[0].transcript.slice(-1);
|
||||||
|
const firstChar = newTranscript.charAt(0);
|
||||||
|
|
||||||
|
if (lastChar.match(/\d/) && firstChar.match(/\d/)) {
|
||||||
|
acc.alternatives[0].transcript += newTranscript;
|
||||||
|
} else {
|
||||||
|
acc.alternatives[0].transcript += ` ${newTranscript}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: true,
|
||||||
|
alternatives: [{
|
||||||
|
transcript: ''
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
finalTranscript.alternatives[0].confidence = bufferedTranscripts.length === 1 ?
|
||||||
|
bufferedTranscripts[0].alternatives[0].confidence :
|
||||||
|
totalConfidence / bufferedTranscripts.length;
|
||||||
|
finalTranscript.alternatives[0].transcript = finalTranscript.alternatives[0].transcript.trim();
|
||||||
|
finalTranscript.vendor = {
|
||||||
|
name: vendor,
|
||||||
|
evt: bufferedTranscripts
|
||||||
|
};
|
||||||
|
return finalTranscript;
|
||||||
|
};
|
||||||
|
|
||||||
|
const compileSonioxTranscripts = (finalWordChunks, channel, language) => {
|
||||||
|
const words = finalWordChunks.flat();
|
||||||
|
const transcript = words.reduce((acc, word) => {
|
||||||
|
if (word.text === '<end>') return acc;
|
||||||
|
if ([',', '.', '?', '!'].includes(word.text)) return `${acc}${word.text}`;
|
||||||
|
return `${acc} ${word.text}`;
|
||||||
|
}, '').trim();
|
||||||
|
const realWords = words.filter((word) => ![',.!?;'].includes(word.text) && word.text !== '<end>');
|
||||||
|
const confidence = realWords.reduce((acc, word) => acc + word.confidence, 0) / realWords.length;
|
||||||
|
const alternatives = [{transcript, confidence}];
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: true,
|
||||||
|
alternatives,
|
||||||
|
vendor: {
|
||||||
|
name: 'soniox',
|
||||||
|
evt: words
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeSoniox = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
|
||||||
|
/* an <end> token indicates the end of an utterance */
|
||||||
|
const endTokenPos = evt.words.map((w) => w.text).indexOf('<end>');
|
||||||
|
const endpointReached = endTokenPos !== -1;
|
||||||
|
const words = endpointReached ? evt.words.slice(0, endTokenPos) : evt.words;
|
||||||
|
|
||||||
|
/* note: we can safely ignore words after the <end> token as they will be returned again */
|
||||||
|
const finalWords = words.filter((word) => word.is_final);
|
||||||
|
const nonFinalWords = words.filter((word) => !word.is_final);
|
||||||
|
|
||||||
|
const is_final = endpointReached && finalWords.length > 0;
|
||||||
|
const transcript = words.reduce((acc, word) => {
|
||||||
|
if ([',', '.', '?', '!'].includes(word.text)) return `${acc}${word.text}`;
|
||||||
|
else return `${acc} ${word.text}`;
|
||||||
|
}, '').trim();
|
||||||
|
const realWords = words.filter((word) => ![',.!?;'].includes(word.text) && word.text !== '<end>');
|
||||||
|
const confidence = realWords.reduce((acc, word) => acc + word.confidence, 0) / realWords.length;
|
||||||
|
const alternatives = [{transcript, confidence}];
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final,
|
||||||
|
alternatives,
|
||||||
|
vendor: {
|
||||||
|
name: 'soniox',
|
||||||
|
endpointReached,
|
||||||
|
evt: copy,
|
||||||
|
finalWords,
|
||||||
|
nonFinalWords
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeDeepgram = (evt, channel, language, shortUtterance) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
const alternatives = (evt.channel?.alternatives || [])
|
||||||
|
.map((alt) => ({
|
||||||
|
confidence: alt.confidence,
|
||||||
|
transcript: alt.transcript,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* note difference between is_final and speech_final in Deepgram:
|
||||||
|
* https://developers.deepgram.com/docs/understand-endpointing-interim-results
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: shortUtterance ? evt.is_final : evt.speech_final,
|
||||||
|
alternatives: alternatives.length ? [alternatives[0]] : [],
|
||||||
|
vendor: {
|
||||||
|
name: 'deepgram',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeNvidia = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
const alternatives = (evt.alternatives || [])
|
||||||
|
.map((alt) => ({
|
||||||
|
confidence: alt.confidence,
|
||||||
|
transcript: alt.transcript,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.is_final,
|
||||||
|
alternatives,
|
||||||
|
vendor: {
|
||||||
|
name: 'nvidia',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeIbm = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
//const idx = evt.result_index;
|
||||||
|
const result = evt.results[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: result.final,
|
||||||
|
alternatives: result.alternatives,
|
||||||
|
vendor: {
|
||||||
|
name: 'ibm',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeGoogle = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.is_final,
|
||||||
|
alternatives: [evt.alternatives[0]],
|
||||||
|
vendor: {
|
||||||
|
name: 'google',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCobalt = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
const alternatives = (evt.alternatives || [])
|
||||||
|
.map((alt) => ({
|
||||||
|
confidence: alt.confidence,
|
||||||
|
transcript: alt.transcript_formatted,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.is_final,
|
||||||
|
alternatives,
|
||||||
|
vendor: {
|
||||||
|
name: 'cobalt',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCustom = (evt, channel, language, vendor) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.is_final,
|
||||||
|
alternatives: [evt.alternatives[0]],
|
||||||
|
vendor: {
|
||||||
|
name: vendor,
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeNuance = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.is_final,
|
||||||
|
alternatives: [evt.alternatives[0]],
|
||||||
|
vendor: {
|
||||||
|
name: 'nuance',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeMicrosoft = (evt, channel, language, punctuation = true) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
const nbest = evt.NBest;
|
||||||
|
const language_code = evt.PrimaryLanguage?.Language || language;
|
||||||
|
const alternatives = nbest ? nbest.map((n) => {
|
||||||
|
return {
|
||||||
|
confidence: n.Confidence,
|
||||||
|
// remove all puntuation if needed
|
||||||
|
transcript: punctuation ? n.Display : n.Display.replace(/\p{P}/gu, '')
|
||||||
|
};
|
||||||
|
}) :
|
||||||
|
[
|
||||||
|
{
|
||||||
|
transcript: punctuation ? evt.DisplayText || evt.Text : (evt.DisplayText || evt.Text).replace(/\p{P}/gu, '')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
language_code,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.RecognitionStatus === 'Success',
|
||||||
|
alternatives: [alternatives[0]],
|
||||||
|
vendor: {
|
||||||
|
name: 'microsoft',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAws = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt[0].is_final,
|
||||||
|
alternatives: evt[0].alternatives,
|
||||||
|
vendor: {
|
||||||
|
name: 'aws',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAssemblyAi = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.message_type === 'FinalTranscript',
|
||||||
|
alternatives: [
|
||||||
|
{
|
||||||
|
confidence: evt.confidence,
|
||||||
|
transcript: evt.text,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
vendor: {
|
||||||
|
name: 'ASSEMBLYAI',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (logger) => {
|
||||||
|
const normalizeTranscription = (evt, vendor, channel, language, shortUtterance, punctuation) => {
|
||||||
|
|
||||||
|
//logger.debug({ evt, vendor, channel, language }, 'normalizeTranscription');
|
||||||
|
switch (vendor) {
|
||||||
|
case 'deepgram':
|
||||||
|
return normalizeDeepgram(evt, channel, language, shortUtterance);
|
||||||
|
case 'microsoft':
|
||||||
|
return normalizeMicrosoft(evt, channel, language, punctuation);
|
||||||
|
case 'google':
|
||||||
|
return normalizeGoogle(evt, channel, language);
|
||||||
|
case 'aws':
|
||||||
|
return normalizeAws(evt, channel, language);
|
||||||
|
case 'nuance':
|
||||||
|
return normalizeNuance(evt, channel, language);
|
||||||
|
case 'ibm':
|
||||||
|
return normalizeIbm(evt, channel, language);
|
||||||
|
case 'nvidia':
|
||||||
|
return normalizeNvidia(evt, channel, language);
|
||||||
|
case 'soniox':
|
||||||
|
return normalizeSoniox(evt, channel, language);
|
||||||
|
case 'cobalt':
|
||||||
|
return normalizeCobalt(evt, channel, language);
|
||||||
|
case 'assemblyai':
|
||||||
|
return normalizeAssemblyAi(evt, channel, language, shortUtterance);
|
||||||
|
default:
|
||||||
|
if (vendor.startsWith('custom:')) {
|
||||||
|
return normalizeCustom(evt, channel, language, vendor);
|
||||||
|
}
|
||||||
|
logger.error(`Unknown vendor ${vendor}`);
|
||||||
|
return evt;
|
||||||
}
|
}
|
||||||
evt.channel_tag = channel;
|
|
||||||
//logger.debug({evt}, 'normalized transcription');
|
|
||||||
return evt;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {normalizeTranscription};
|
const setChannelVarsForStt = (task, sttCredentials, language, rOpts = {}) => {
|
||||||
|
let opts = {};
|
||||||
|
const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {};
|
||||||
|
const vad = {enable, voiceMs, mode};
|
||||||
|
const vendor = rOpts.vendor;
|
||||||
|
|
||||||
|
/* voice activity detection works across vendors */
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(vad.enable && {START_RECOGNIZING_ON_VAD: 1}),
|
||||||
|
...(vad.enable && vad.voiceMs && {RECOGNIZER_VAD_VOICE_MS: vad.voiceMs}),
|
||||||
|
...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('google' === vendor) {
|
||||||
|
const useV2 = rOpts.googleOptions?.serviceVersion === 'v2';
|
||||||
|
const model = task.name === TaskName.Gather ?
|
||||||
|
(useV2 ? 'telephony_short' : 'command_and_search') :
|
||||||
|
(useV2 ? 'long' : 'latest_long');
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
|
||||||
|
...(rOpts.separateRecognitionPerChannel && {GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
|
||||||
|
...(rOpts.separateRecognitionPerChanne === false && {GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 0}),
|
||||||
|
...(rOpts.profanityFilter && {GOOGLE_SPEECH_PROFANITY_FILTER: 1}),
|
||||||
|
...(rOpts.punctuation && {GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1}),
|
||||||
|
...(rOpts.words && {GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 1}),
|
||||||
|
...(rOpts.singleUtterance && {GOOGLE_SPEECH_SINGLE_UTTERANCE: 1}),
|
||||||
|
...(rOpts.diarization && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 1}),
|
||||||
|
...(rOpts.diarization && rOpts.diarizationMinSpeakers > 0 &&
|
||||||
|
{GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT: rOpts.diarizationMinSpeakers}),
|
||||||
|
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
|
||||||
|
{GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}),
|
||||||
|
...(rOpts.enhancedModel !== false && {GOOGLE_SPEECH_USE_ENHANCED: 1}),
|
||||||
|
...(rOpts.profanityFilter === false && {GOOGLE_SPEECH_PROFANITY_FILTER: 0}),
|
||||||
|
...(rOpts.punctuation === false && {GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 0}),
|
||||||
|
...(rOpts.words == false && {GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 0}),
|
||||||
|
...(rOpts.diarization === false && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 0}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||||
|
{GOOGLE_SPEECH_HINTS: rOpts.hints.join(',')}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||||
|
{GOOGLE_SPEECH_HINTS: JSON.stringify(rOpts.hints)}),
|
||||||
|
...(typeof rOpts.hintsBoost === 'number' && {GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
|
||||||
|
// When altLanguages is emptylist, we have to send value to freeswitch to clear the previous settings
|
||||||
|
...(rOpts.altLanguages &&
|
||||||
|
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
|
||||||
|
...(rOpts.interactionType &&
|
||||||
|
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
|
||||||
|
...{GOOGLE_SPEECH_MODEL: rOpts.model || model},
|
||||||
|
...(rOpts.naicsCode > 0 && {GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
|
||||||
|
GOOGLE_SPEECH_METADATA_RECORDING_DEVICE_TYPE: 'phone_line',
|
||||||
|
...(useV2 && {
|
||||||
|
GOOGLE_SPEECH_RECOGNIZER_PARENT: `projects/${sttCredentials.credentials.project_id}/locations/global`,
|
||||||
|
GOOGLE_SPEECH_CLOUD_SERVICES_VERSION: 'v2',
|
||||||
|
...(rOpts.googleOptions?.speechStartTimeoutMs && {
|
||||||
|
GOOGLE_SPEECH_START_TIMEOUT_MS: rOpts.googleOptions.speechStartTimeoutMs
|
||||||
|
}),
|
||||||
|
...(rOpts.googleOptions?.speechEndTimeoutMs && {
|
||||||
|
GOOGLE_SPEECH_END_TIMEOUT_MS: rOpts.googleOptions.speechEndTimeoutMs
|
||||||
|
}),
|
||||||
|
...(rOpts.googleOptions?.transcriptNormalization && {
|
||||||
|
GOOGLE_SPEECH_TRANSCRIPTION_NORMALIZATION: JSON.stringify(rOpts.googleOptions.transcriptNormalization)
|
||||||
|
}),
|
||||||
|
...(rOpts.googleOptions?.enableVoiceActivityEvents && {
|
||||||
|
GOOGLE_SPEECH_ENABLE_VOICE_ACTIVITY_EVENTS: rOpts.googleOptions.enableVoiceActivityEvents
|
||||||
|
}),
|
||||||
|
...(rOpts.sgoogleOptions?.recognizerId) && {GOOGLE_SPEECH_RECOGNIZER_ID: rOpts.googleOptions.recognizerId},
|
||||||
|
...(rOpts.googleOptions?.enableVoiceActivityEvents && {
|
||||||
|
GOOGLE_SPEECH_ENABLE_VOICE_ACTIVITY_EVENTS: rOpts.googleOptions.enableVoiceActivityEvents
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (['aws', 'polly'].includes(vendor)) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}),
|
||||||
|
...(rOpts.vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: rOpts.vocabularyFilterName}),
|
||||||
|
...(rOpts.filterMethod && {AWS_VOCABULARY_FILTER_METHOD: rOpts.filterMethod}),
|
||||||
|
...(sttCredentials && {
|
||||||
|
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
|
||||||
|
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
|
||||||
|
AWS_REGION: sttCredentials.region
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('microsoft' === vendor) {
|
||||||
|
const {azureOptions = {}} = rOpts;
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||||
|
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.trim()).join(',')}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||||
|
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.phrase).join(',')}),
|
||||||
|
// When altLanguages is emptylist, we have to send value to freeswitch to clear the previous settings
|
||||||
|
...(rOpts.altLanguages &&
|
||||||
|
{AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
|
||||||
|
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
||||||
|
...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}),
|
||||||
|
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint_url &&
|
||||||
|
{AZURE_SERVICE_ENDPOINT: sttCredentials.custom_stt_endpoint_url}),
|
||||||
|
...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}),
|
||||||
|
...(rOpts.initialSpeechTimeoutMs > 0 &&
|
||||||
|
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
|
||||||
|
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
||||||
|
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
||||||
|
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
|
||||||
|
...(azureOptions.speechSegmentationSilenceTimeoutMs &&
|
||||||
|
{AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}),
|
||||||
|
...(azureOptions.languageIdMode &&
|
||||||
|
{AZURE_LANGUAGE_ID_MODE: azureOptions.languageIdMode}),
|
||||||
|
...(sttCredentials && {
|
||||||
|
...(sttCredentials.api_key && {AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key}),
|
||||||
|
...(sttCredentials.region && {AZURE_REGION: sttCredentials.region}),
|
||||||
|
}),
|
||||||
|
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint &&
|
||||||
|
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint}),
|
||||||
|
//azureSttEndpointId overrides sttCredentials.custom_stt_endpoint
|
||||||
|
...(rOpts.azureSttEndpointId &&
|
||||||
|
{AZURE_SERVICE_ENDPOINT_ID: rOpts.azureSttEndpointId}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('nuance' === vendor) {
|
||||||
|
/**
|
||||||
|
* Note: all nuance options are in recognizer.nuanceOptions, should migrate
|
||||||
|
* other vendor settings to similar nested structure
|
||||||
|
*/
|
||||||
|
const {nuanceOptions = {}} = rOpts;
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(sttCredentials.access_token) && {NUANCE_ACCESS_TOKEN: sttCredentials.access_token},
|
||||||
|
...(sttCredentials.nuance_stt_uri) && {NUANCE_KRYPTON_ENDPOINT: sttCredentials.nuance_stt_uri},
|
||||||
|
...(nuanceOptions.topic) && {NUANCE_TOPIC: nuanceOptions.topic},
|
||||||
|
...(nuanceOptions.utteranceDetectionMode) &&
|
||||||
|
{NUANCE_UTTERANCE_DETECTION_MODE: nuanceOptions.utteranceDetectionMode},
|
||||||
|
...(nuanceOptions.punctuation || rOpts.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation},
|
||||||
|
...(nuanceOptions.profanityFilter) &&
|
||||||
|
{NUANCE_FILTER_PROFANITY: nuanceOptions.profanityFilter},
|
||||||
|
...(nuanceOptions.includeTokenization) &&
|
||||||
|
{NUANCE_INCLUDE_TOKENIZATION: nuanceOptions.includeTokenization},
|
||||||
|
...(nuanceOptions.discardSpeakerAdaptation) &&
|
||||||
|
{NUANCE_DISCARD_SPEAKER_ADAPTATION: nuanceOptions.discardSpeakerAdaptation},
|
||||||
|
...(nuanceOptions.suppressCallRecording) &&
|
||||||
|
{NUANCE_SUPPRESS_CALL_RECORDING: nuanceOptions.suppressCallRecording},
|
||||||
|
...(nuanceOptions.maskLoadFailures) &&
|
||||||
|
{NUANCE_MASK_LOAD_FAILURES: nuanceOptions.maskLoadFailures},
|
||||||
|
...(nuanceOptions.suppressInitialCapitalization) &&
|
||||||
|
{NUANCE_SUPPRESS_INITIAL_CAPITALIZATION: nuanceOptions.suppressInitialCapitalization},
|
||||||
|
...(nuanceOptions.allowZeroBaseLmWeight)
|
||||||
|
&& {NUANCE_ALLOW_ZERO_BASE_LM_WEIGHT: nuanceOptions.allowZeroBaseLmWeight},
|
||||||
|
...(nuanceOptions.filterWakeupWord) &&
|
||||||
|
{NUANCE_FILTER_WAKEUP_WORD: nuanceOptions.filterWakeupWord},
|
||||||
|
...(nuanceOptions.resultType) &&
|
||||||
|
{NUANCE_RESULT_TYPE: nuanceOptions.resultType || rOpts.interim ? 'partial' : 'final'},
|
||||||
|
...(nuanceOptions.noInputTimeoutMs) &&
|
||||||
|
{NUANCE_NO_INPUT_TIMEOUT_MS: nuanceOptions.noInputTimeoutMs},
|
||||||
|
...(nuanceOptions.recognitionTimeoutMs) &&
|
||||||
|
{NUANCE_RECOGNITION_TIMEOUT_MS: nuanceOptions.recognitionTimeoutMs},
|
||||||
|
...(nuanceOptions.utteranceEndSilenceMs) &&
|
||||||
|
{NUANCE_UTTERANCE_END_SILENCE_MS: nuanceOptions.utteranceEndSilenceMs},
|
||||||
|
...(nuanceOptions.maxHypotheses) &&
|
||||||
|
{NUANCE_MAX_HYPOTHESES: nuanceOptions.maxHypotheses},
|
||||||
|
...(nuanceOptions.speechDomain) &&
|
||||||
|
{NUANCE_SPEECH_DOMAIN: nuanceOptions.speechDomain},
|
||||||
|
...(nuanceOptions.formatting) &&
|
||||||
|
{NUANCE_FORMATTING: nuanceOptions.formatting},
|
||||||
|
...(nuanceOptions.resources) &&
|
||||||
|
{NUANCE_RESOURCES: JSON.stringify(nuanceOptions.resources)},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('deepgram' === vendor) {
|
||||||
|
let {model} = rOpts;
|
||||||
|
const {deepgramOptions = {}} = rOpts;
|
||||||
|
const deepgramUri = deepgramOptions.deepgramSttUri || sttCredentials.deepgram_stt_uri;
|
||||||
|
const useTls = deepgramOptions.deepgramSttUseTls || sttCredentials.deepgram_stt_use_tls;
|
||||||
|
|
||||||
|
/* default to a sensible model if not supplied */
|
||||||
|
if (!model) {
|
||||||
|
model = selectDefaultDeepgramModel(task, language);
|
||||||
|
}
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
DEEPGRAM_SPEECH_MODEL: model,
|
||||||
|
...(deepgramUri && {DEEPGRAM_URI: deepgramUri}),
|
||||||
|
...(deepgramUri && useTls && {DEEPGRAM_USE_TLS: 1}),
|
||||||
|
...(sttCredentials.api_key) &&
|
||||||
|
{DEEPGRAM_API_KEY: sttCredentials.api_key},
|
||||||
|
...(deepgramOptions.tier) &&
|
||||||
|
{DEEPGRAM_SPEECH_TIER: deepgramOptions.tier},
|
||||||
|
...(deepgramOptions.punctuate) &&
|
||||||
|
{DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1},
|
||||||
|
...(deepgramOptions.smartFormatting) &&
|
||||||
|
{DEEPGRAM_SPEECH_ENABLE_SMART_FORMAT: 1},
|
||||||
|
...(deepgramOptions.profanityFilter) &&
|
||||||
|
{DEEPGRAM_SPEECH_PROFANITY_FILTER: 1},
|
||||||
|
...(deepgramOptions.redact) &&
|
||||||
|
{DEEPGRAM_SPEECH_REDACT: deepgramOptions.redact},
|
||||||
|
...(deepgramOptions.diarize) &&
|
||||||
|
{DEEPGRAM_SPEECH_DIARIZE: 1},
|
||||||
|
...(deepgramOptions.diarizeVersion) &&
|
||||||
|
{DEEPGRAM_SPEECH_DIARIZE_VERSION: deepgramOptions.diarizeVersion},
|
||||||
|
...(deepgramOptions.ner) &&
|
||||||
|
{DEEPGRAM_SPEECH_NER: 1},
|
||||||
|
...(deepgramOptions.alternatives) &&
|
||||||
|
{DEEPGRAM_SPEECH_ALTERNATIVES: deepgramOptions.alternatives},
|
||||||
|
...(deepgramOptions.numerals) &&
|
||||||
|
{DEEPGRAM_SPEECH_NUMERALS: deepgramOptions.numerals},
|
||||||
|
...(deepgramOptions.search) &&
|
||||||
|
{DEEPGRAM_SPEECH_SEARCH: deepgramOptions.search.join(',')},
|
||||||
|
...(deepgramOptions.replace) &&
|
||||||
|
{DEEPGRAM_SPEECH_REPLACE: deepgramOptions.replace.join(',')},
|
||||||
|
...(rOpts.hints && rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||||
|
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.trim()).join(',')}),
|
||||||
|
...(rOpts.hints && rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||||
|
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.phrase).join(',')}),
|
||||||
|
...(deepgramOptions.keywords) &&
|
||||||
|
{DEEPGRAM_SPEECH_KEYWORDS: deepgramOptions.keywords.join(',')},
|
||||||
|
...('endpointing' in deepgramOptions) &&
|
||||||
|
{DEEPGRAM_SPEECH_ENDPOINTING: deepgramOptions.endpointing === false ? 'false' : deepgramOptions.endpointing,
|
||||||
|
// default DEEPGRAM_SPEECH_UTTERANCE_END_MS is 1000, will be override by user settings later if there is.
|
||||||
|
DEEPGRAM_SPEECH_UTTERANCE_END_MS: 1000},
|
||||||
|
...(deepgramOptions.utteranceEndMs) &&
|
||||||
|
{DEEPGRAM_SPEECH_UTTERANCE_END_MS: deepgramOptions.utteranceEndMs},
|
||||||
|
...(deepgramOptions.vadTurnoff) &&
|
||||||
|
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
|
||||||
|
...(deepgramOptions.tag) &&
|
||||||
|
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('soniox' === vendor) {
|
||||||
|
const {sonioxOptions = {}} = rOpts;
|
||||||
|
const {storage = {}} = sonioxOptions;
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(sttCredentials.api_key) &&
|
||||||
|
{SONIOX_API_KEY: sttCredentials.api_key},
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||||
|
{SONIOX_HINTS: rOpts.hints.join(',')}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||||
|
{SONIOX_HINTS: JSON.stringify(rOpts.hints)}),
|
||||||
|
...(typeof rOpts.hintsBoost === 'number' &&
|
||||||
|
{SONIOX_HINTS_BOOST: rOpts.hintsBoost}),
|
||||||
|
...(sonioxOptions.model) &&
|
||||||
|
{SONIOX_MODEL: sonioxOptions.model},
|
||||||
|
...((sonioxOptions.profanityFilter || rOpts.profanityFilter) && {SONIOX_PROFANITY_FILTER: 1}),
|
||||||
|
...(storage?.id && {SONIOX_STORAGE_ID: storage.id}),
|
||||||
|
...(storage?.id && storage?.title && {SONIOX_STORAGE_TITLE: storage.title}),
|
||||||
|
...(storage?.id && storage?.disableStoreAudio && {SONIOX_STORAGE_DISABLE_AUDIO: 1}),
|
||||||
|
...(storage?.id && storage?.disableStoreTranscript && {SONIOX_STORAGE_DISABLE_TRANSCRIPT: 1}),
|
||||||
|
...(storage?.id && storage?.disableSearch && {SONIOX_STORAGE_DISABLE_SEARCH: 1})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('ibm' === vendor) {
|
||||||
|
const {ibmOptions = {}} = rOpts;
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(sttCredentials.access_token) &&
|
||||||
|
{IBM_ACCESS_TOKEN: sttCredentials.access_token},
|
||||||
|
...(sttCredentials.stt_region) &&
|
||||||
|
{IBM_SPEECH_REGION: sttCredentials.stt_region},
|
||||||
|
...(sttCredentials.instance_id) &&
|
||||||
|
{IBM_SPEECH_INSTANCE_ID: sttCredentials.instance_id},
|
||||||
|
...(ibmOptions.model) &&
|
||||||
|
{IBM_SPEECH_MODEL: ibmOptions.model},
|
||||||
|
...(ibmOptions.language_customization_id) &&
|
||||||
|
{IBM_SPEECH_LANGUAGE_CUSTOMIZATION_ID: ibmOptions.language_customization_id},
|
||||||
|
...(ibmOptions.acoustic_customization_id) &&
|
||||||
|
{IBM_SPEECH_ACOUSTIC_CUSTOMIZATION_ID: ibmOptions.acoustic_customization_id},
|
||||||
|
...(ibmOptions.baseModelVersion) &&
|
||||||
|
{IBM_SPEECH_BASE_MODEL_VERSION: ibmOptions.baseModelVersion},
|
||||||
|
...(ibmOptions.watsonMetadata) &&
|
||||||
|
{IBM_SPEECH_WATSON_METADATA: ibmOptions.watsonMetadata},
|
||||||
|
...(ibmOptions.watsonLearningOptOut) &&
|
||||||
|
{IBM_SPEECH_WATSON_LEARNING_OPT_OUT: ibmOptions.watsonLearningOptOut}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('nvidia' === vendor) {
|
||||||
|
const {nvidiaOptions = {}} = rOpts;
|
||||||
|
const rivaUri = nvidiaOptions.rivaUri || sttCredentials.riva_server_uri;
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...((nvidiaOptions.profanityFilter || rOpts.profanityFilter) && {NVIDIA_PROFANITY_FILTER: 1}),
|
||||||
|
...(!(nvidiaOptions.profanityFilter || rOpts.profanityFilter) && {NVIDIA_PROFANITY_FILTER: 0}),
|
||||||
|
...((nvidiaOptions.punctuation || rOpts.punctuation) && {NVIDIA_PUNCTUATION: 1}),
|
||||||
|
...(!(nvidiaOptions.punctuation || rOpts.punctuation) && {NVIDIA_PUNCTUATION: 0}),
|
||||||
|
...((rOpts.words || nvidiaOptions.wordTimeOffsets) && {NVIDIA_WORD_TIME_OFFSETS: 1}),
|
||||||
|
...(!(rOpts.words || nvidiaOptions.wordTimeOffsets) && {NVIDIA_WORD_TIME_OFFSETS: 0}),
|
||||||
|
...(nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: nvidiaOptions.maxAlternatives}),
|
||||||
|
...(!nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: 1}),
|
||||||
|
...(rOpts.model && {NVIDIA_MODEL: rOpts.model}),
|
||||||
|
...(rivaUri && {NVIDIA_RIVA_URI: rivaUri}),
|
||||||
|
...(nvidiaOptions.verbatimTranscripts && {NVIDIA_VERBATIM_TRANSCRIPTS: 1}),
|
||||||
|
...(rOpts.diarization && {NVIDIA_SPEAKER_DIARIZATION: 1}),
|
||||||
|
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
|
||||||
|
{NVIDIA_DIARIZATION_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}),
|
||||||
|
...(rOpts.separateRecognitionPerChannel && {NVIDIA_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||||
|
{NVIDIA_HINTS: rOpts.hints.join(',')}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||||
|
{NVIDIA_HINTS: JSON.stringify(rOpts.hints)}),
|
||||||
|
...(typeof rOpts.hintsBoost === 'number' &&
|
||||||
|
{NVIDIA_HINTS_BOOST: rOpts.hintsBoost}),
|
||||||
|
...(nvidiaOptions.customConfiguration &&
|
||||||
|
{NVIDIA_CUSTOM_CONFIGURATION: JSON.stringify(nvidiaOptions.customConfiguration)}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('cobalt' === vendor) {
|
||||||
|
const {cobaltOptions = {}} = rOpts;
|
||||||
|
const cobaltUri = cobaltOptions.serverUri || sttCredentials.cobalt_server_uri;
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(rOpts.words && {COBALT_WORD_TIME_OFFSETS: 1}),
|
||||||
|
...(!rOpts.words && {COBALT_WORD_TIME_OFFSETS: 0}),
|
||||||
|
...(rOpts.model && {COBALT_MODEL: rOpts.model}),
|
||||||
|
...(cobaltUri && {COBALT_SERVER_URI: cobaltUri}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||||
|
{COBALT_SPEECH_HINTS: rOpts.hints.join(',')}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||||
|
{COBALT_SPEECH_HINTS: JSON.stringify(rOpts.hints)}),
|
||||||
|
...(rOpts.hints?.length > 0 &&
|
||||||
|
{COBALT_CONTEXT_TOKEN: cobaltOptions.contextToken || 'unk:default'}),
|
||||||
|
...(cobaltOptions.metadata && {COBALT_METADATA: cobaltOptions.metadata}),
|
||||||
|
...(cobaltOptions.enableConfusionNetwork && {COBALT_ENABLE_CONFUSION_NETWORK: 1}),
|
||||||
|
...(cobaltOptions.compiledContextData && {COBALT_COMPILED_CONTEXT_DATA: cobaltOptions.compiledContextData}),
|
||||||
|
};
|
||||||
|
} else if ('assemblyai' === vendor) {
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(sttCredentials.api_key) &&
|
||||||
|
{ASSEMBLYAI_API_KEY: sttCredentials.api_key},
|
||||||
|
...(rOpts.hints?.length > 0 &&
|
||||||
|
{ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (vendor.startsWith('custom:')) {
|
||||||
|
let {options = {}} = rOpts;
|
||||||
|
const {auth_token, custom_stt_url} = sttCredentials;
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||||
|
{hints: rOpts.hints}),
|
||||||
|
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||||
|
{hints: JSON.stringify(rOpts.hints)}),
|
||||||
|
...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost})
|
||||||
|
};
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
...(auth_token && {JAMBONZ_STT_API_KEY: auth_token}),
|
||||||
|
JAMBONZ_STT_URL: custom_stt_url,
|
||||||
|
...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
(stickyVars[vendor] || []).forEach((key) => {
|
||||||
|
if (!opts[key]) opts[key] = '';
|
||||||
|
});
|
||||||
|
return opts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSpeechCredentialsAtRuntime = (recognizer) => {
|
||||||
|
if (!recognizer) return;
|
||||||
|
if (recognizer.vendor === 'nuance') {
|
||||||
|
const {clientId, secret, kryptonEndpoint} = recognizer.nuanceOptions || {};
|
||||||
|
if (clientId && secret) return {client_id: clientId, secret};
|
||||||
|
if (kryptonEndpoint) return {nuance_stt_uri: kryptonEndpoint};
|
||||||
|
}
|
||||||
|
else if (recognizer.vendor === 'nvidia') {
|
||||||
|
const {rivaUri} = recognizer.nvidiaOptions || {};
|
||||||
|
if (rivaUri) return {riva_uri: rivaUri};
|
||||||
|
}
|
||||||
|
else if (recognizer.vendor === 'deepgram') {
|
||||||
|
const {apiKey} = recognizer.deepgramOptions || {};
|
||||||
|
if (apiKey) return {api_key: apiKey};
|
||||||
|
}
|
||||||
|
else if (recognizer.vendor === 'soniox') {
|
||||||
|
const {apiKey} = recognizer.sonioxOptions || {};
|
||||||
|
if (apiKey) return {api_key: apiKey};
|
||||||
|
}
|
||||||
|
else if (recognizer.vendor === 'cobalt') {
|
||||||
|
const {serverUri} = recognizer.cobaltOptions || {};
|
||||||
|
if (serverUri) return {cobalt_server_uri: serverUri};
|
||||||
|
}
|
||||||
|
else if (recognizer.vendor === 'ibm') {
|
||||||
|
const {ttsApiKey, ttsRegion, sttApiKey, sttRegion, instanceId} = recognizer.ibmOptions || {};
|
||||||
|
if (ttsApiKey || sttApiKey) return {
|
||||||
|
tts_api_key: ttsApiKey,
|
||||||
|
tts_region: ttsRegion,
|
||||||
|
stt_api_key: sttApiKey,
|
||||||
|
stt_region: sttRegion,
|
||||||
|
instance_id: instanceId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
normalizeTranscription,
|
||||||
|
setChannelVarsForStt,
|
||||||
|
setSpeechCredentialsAtRuntime,
|
||||||
|
compileSonioxTranscripts,
|
||||||
|
consolidateTranscripts
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ const short = require('short-uuid');
|
|||||||
const {HookMsgTypes} = require('./constants.json');
|
const {HookMsgTypes} = require('./constants.json');
|
||||||
const Websocket = require('ws');
|
const Websocket = require('ws');
|
||||||
const snakeCaseKeys = require('./snakecase-keys');
|
const snakeCaseKeys = require('./snakecase-keys');
|
||||||
const HttpRequestor = require('./http-requestor');
|
const {
|
||||||
const MAX_RECONNECTS = 5;
|
RESPONSE_TIMEOUT_MS,
|
||||||
const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
|
JAMBONES_WS_PING_INTERVAL_MS,
|
||||||
|
MAX_RECONNECTS,
|
||||||
|
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
|
||||||
|
JAMBONES_WS_MAX_PAYLOAD,
|
||||||
|
HTTP_USER_AGENT_HEADER
|
||||||
|
} = require('../config');
|
||||||
|
|
||||||
class WsRequestor extends BaseRequestor {
|
class WsRequestor extends BaseRequestor {
|
||||||
constructor(logger, account_sid, hook, secret) {
|
constructor(logger, account_sid, hook, secret) {
|
||||||
@@ -39,13 +44,14 @@ class WsRequestor extends BaseRequestor {
|
|||||||
async request(type, hook, params, httpHeaders = {}) {
|
async request(type, hook, params, httpHeaders = {}) {
|
||||||
assert(HookMsgTypes.includes(type));
|
assert(HookMsgTypes.includes(type));
|
||||||
const url = hook.url || hook;
|
const url = hook.url || hook;
|
||||||
|
const wantsAck = !['call:status', 'verb:status', 'jambonz:error'].includes(type);
|
||||||
|
|
||||||
if (this.maliciousClient) {
|
if (this.maliciousClient) {
|
||||||
this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client');
|
this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.closedGracefully) {
|
if (this.closedGracefully) {
|
||||||
this.logger.debug(`WsRequestor:request - discarding ${type} because we closed the socket`);
|
this.logger.debug(`WsRequestor:request - discarding ${type} because socket was closed gracefully`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +59,10 @@ class WsRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
/* if we have an absolute url, and it is http then do a standard webhook */
|
/* if we have an absolute url, and it is http then do a standard webhook */
|
||||||
if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
|
if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
|
||||||
|
const HttpRequestor = require('./http-requestor');
|
||||||
this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)');
|
this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)');
|
||||||
const requestor = new HttpRequestor(this.logger, this.account_sid, {url: hook}, this.secret);
|
const h = typeof hook === 'object' ? hook : {url: hook};
|
||||||
|
const requestor = new HttpRequestor(this.logger, this.account_sid, h, this.secret);
|
||||||
if (type === 'session:redirect') {
|
if (type === 'session:redirect') {
|
||||||
this.close();
|
this.close();
|
||||||
this.emit('handover', requestor);
|
this.emit('handover', requestor);
|
||||||
@@ -67,11 +75,19 @@ class WsRequestor extends BaseRequestor {
|
|||||||
if (this.connectInProgress) {
|
if (this.connectInProgress) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
|
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
|
||||||
this.queuedMsg.push({type, hook, params, httpHeaders});
|
if (wantsAck) {
|
||||||
|
const p = new Promise((resolve, reject) => {
|
||||||
|
this.queuedMsg.push({type, hook, params, httpHeaders, promise: {resolve, reject}});
|
||||||
|
});
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.queuedMsg.push({type, hook, params, httpHeaders});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.connectInProgress = true;
|
this.connectInProgress = true;
|
||||||
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection`);
|
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection for ${type}`);
|
||||||
if (this.connections >= MAX_RECONNECTS) {
|
if (this.connections >= MAX_RECONNECTS) {
|
||||||
return Promise.reject(`max attempts connecting to ${this.url}`);
|
return Promise.reject(`max attempts connecting to ${this.url}`);
|
||||||
}
|
}
|
||||||
@@ -95,21 +111,29 @@ class WsRequestor extends BaseRequestor {
|
|||||||
assert.ok(url, 'WsRequestor:request url was not provided');
|
assert.ok(url, 'WsRequestor:request url was not provided');
|
||||||
|
|
||||||
const msgid = short.generate();
|
const msgid = short.generate();
|
||||||
|
// save initial msgid in case we need to reconnect during initial session:new
|
||||||
|
if (type === 'session:new') this._initMsgId = msgid;
|
||||||
|
|
||||||
const b3 = httpHeaders?.b3 ? {b3: httpHeaders.b3} : {};
|
const b3 = httpHeaders?.b3 ? {b3: httpHeaders.b3} : {};
|
||||||
const obj = {
|
const obj = {
|
||||||
type,
|
type,
|
||||||
msgid,
|
msgid,
|
||||||
call_sid: this.call_sid,
|
call_sid: this.call_sid,
|
||||||
hook: type === 'verb:hook' ? url : undefined,
|
hook: ['verb:hook', 'session:redirect'].includes(type) ? url : undefined,
|
||||||
data: {...payload},
|
data: {...payload},
|
||||||
...b3
|
...b3
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendQueuedMsgs = () => {
|
const sendQueuedMsgs = () => {
|
||||||
if (this.queuedMsg.length > 0) {
|
if (this.queuedMsg.length > 0) {
|
||||||
for (const {type, hook, params, httpHeaders} of this.queuedMsg) {
|
for (const {type, hook, params, httpHeaders, promise} of this.queuedMsg) {
|
||||||
this.logger.debug(`WsRequestor:request - preparing queued ${type} for sending`);
|
this.logger.debug(`WsRequestor:request - preparing queued ${type} for sending`);
|
||||||
setImmediate(this.request.bind(this, type, hook, params, httpHeaders));
|
if (promise) {
|
||||||
|
this.request(type, hook, params, httpHeaders)
|
||||||
|
.then((res) => promise.resolve(res))
|
||||||
|
.catch((err) => promise.reject(err));
|
||||||
|
}
|
||||||
|
else setImmediate(this.request.bind(this, type, hook, params, httpHeaders));
|
||||||
}
|
}
|
||||||
this.queuedMsg.length = 0;
|
this.queuedMsg.length = 0;
|
||||||
}
|
}
|
||||||
@@ -117,9 +141,19 @@ class WsRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
//this.logger.debug({obj}, `websocket: sending (${url})`);
|
//this.logger.debug({obj}, `websocket: sending (${url})`);
|
||||||
|
|
||||||
|
/* special case: reconnecting before we received ack to session:new */
|
||||||
|
let reconnectingWithoutAck = false;
|
||||||
|
if (type === 'session:reconnect' && this._initMsgId) {
|
||||||
|
reconnectingWithoutAck = true;
|
||||||
|
const obj = this.messagesInFlight.get(this._initMsgId);
|
||||||
|
this.messagesInFlight.delete(this._initMsgId);
|
||||||
|
this.messagesInFlight.set(msgid, obj);
|
||||||
|
this._initMsgId = msgid;
|
||||||
|
}
|
||||||
|
|
||||||
/* simple notifications */
|
/* simple notifications */
|
||||||
if (['call:status', 'jambonz:error', 'session:reconnect'].includes(type)) {
|
if (!wantsAck || reconnectingWithoutAck) {
|
||||||
this.ws.send(JSON.stringify(obj), () => {
|
this.ws?.send(JSON.stringify(obj), () => {
|
||||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||||
sendQueuedMsgs();
|
sendQueuedMsgs();
|
||||||
});
|
});
|
||||||
@@ -130,7 +164,7 @@ class WsRequestor extends BaseRequestor {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
/* give the far end a reasonable amount of time to ack our message */
|
/* give the far end a reasonable amount of time to ack our message */
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
const {failure} = this.messagesInFlight.get(msgid);
|
const {failure} = this.messagesInFlight.get(msgid) || {};
|
||||||
failure && failure(`timeout from far end for msgid ${msgid}`);
|
failure && failure(`timeout from far end for msgid ${msgid}`);
|
||||||
this.messagesInFlight.delete(msgid);
|
this.messagesInFlight.delete(msgid);
|
||||||
}, RESPONSE_TIMEOUT_MS);
|
}, RESPONSE_TIMEOUT_MS);
|
||||||
@@ -160,22 +194,24 @@ class WsRequestor extends BaseRequestor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_stopPingTimer() {
|
||||||
|
if (this._pingTimer) {
|
||||||
|
clearInterval(this._pingTimer);
|
||||||
|
this._pingTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.closedGracefully = true;
|
this.closedGracefully = true;
|
||||||
this.logger.debug('WsRequestor:close closing socket');
|
this.logger.debug('WsRequestor:close closing socket');
|
||||||
|
this._stopPingTimer();
|
||||||
try {
|
try {
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close();
|
this.ws.close(1000);
|
||||||
this.ws.removeAllListeners();
|
this.ws.removeAllListeners();
|
||||||
|
this.ws = null;
|
||||||
}
|
}
|
||||||
|
this._clearPendingMessages();
|
||||||
for (const [msgid, obj] of this.messagesInFlight) {
|
|
||||||
const {timer} = obj;
|
|
||||||
clearTimeout(timer);
|
|
||||||
obj.failure(`abandoning msgid ${msgid} since we have closed the socket`);
|
|
||||||
}
|
|
||||||
this.messagesInFlight.clear();
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err}, 'WsRequestor: Error closing socket');
|
this.logger.info({err}, 'WsRequestor: Error closing socket');
|
||||||
}
|
}
|
||||||
@@ -183,15 +219,19 @@ class WsRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
_connect() {
|
_connect() {
|
||||||
assert(!this.ws);
|
assert(!this.ws);
|
||||||
|
this._stopPingTimer();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const handshakeTimeout = process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS ?
|
const handshakeTimeout = JAMBONES_WS_HANDSHAKE_TIMEOUT_MS ?
|
||||||
parseInt(process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS) :
|
parseInt(JAMBONES_WS_HANDSHAKE_TIMEOUT_MS) :
|
||||||
1500;
|
1500;
|
||||||
let opts = {
|
let opts = {
|
||||||
followRedirects: true,
|
followRedirects: true,
|
||||||
maxRedirects: 2,
|
maxRedirects: 2,
|
||||||
handshakeTimeout,
|
handshakeTimeout,
|
||||||
maxPayload: process.env.JAMBONES_WS_MAX_PAYLOAD ? parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024,
|
maxPayload: JAMBONES_WS_MAX_PAYLOAD ? parseInt(JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024,
|
||||||
|
headers: {
|
||||||
|
...(HTTP_USER_AGENT_HEADER && {'user-agent' : HTTP_USER_AGENT_HEADER})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
|
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};
|
||||||
|
|
||||||
@@ -211,7 +251,6 @@ class WsRequestor extends BaseRequestor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setHandlers(ws) {
|
_setHandlers(ws) {
|
||||||
this.logger.debug('WsRequestor:_setHandlers');
|
|
||||||
ws
|
ws
|
||||||
.once('open', this._onOpen.bind(this, ws))
|
.once('open', this._onOpen.bind(this, ws))
|
||||||
.once('close', this._onClose.bind(this))
|
.once('close', this._onClose.bind(this))
|
||||||
@@ -220,6 +259,15 @@ class WsRequestor extends BaseRequestor {
|
|||||||
.on('error', this._onError.bind(this));
|
.on('error', this._onError.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_clearPendingMessages() {
|
||||||
|
for (const [msgid, obj] of this.messagesInFlight) {
|
||||||
|
const {timer} = obj;
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (!this._initMsgId) obj.failure(`abandoning msgid ${msgid} since socket is closed`);
|
||||||
|
}
|
||||||
|
this.messagesInFlight.clear();
|
||||||
|
}
|
||||||
|
|
||||||
_onError(err) {
|
_onError(err) {
|
||||||
if (this.connections > 0) {
|
if (this.connections > 0) {
|
||||||
this.logger.info({url: this.url, err}, 'WsRequestor:_onError');
|
this.logger.info({url: this.url, err}, 'WsRequestor:_onError');
|
||||||
@@ -235,10 +283,15 @@ class WsRequestor extends BaseRequestor {
|
|||||||
this.connectInProgress = false;
|
this.connectInProgress = false;
|
||||||
this.connections++;
|
this.connections++;
|
||||||
this.emit('ready', ws);
|
this.emit('ready', ws);
|
||||||
|
|
||||||
|
if (JAMBONES_WS_PING_INTERVAL_MS > 15000) {
|
||||||
|
this._pingTimer = setInterval(() => this.ws?.ping(), JAMBONES_WS_PING_INTERVAL_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClose(code) {
|
_onClose(code) {
|
||||||
this.logger.info(`WsRequestor(${this.id}) - closed from far end ${code}`);
|
this.logger.info(`WsRequestor(${this.id}) - closed from far end ${code}`);
|
||||||
|
this._stopPingTimer();
|
||||||
if (this.connections > 0 && code !== 1000) {
|
if (this.connections > 0 && code !== 1000) {
|
||||||
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
||||||
this.emit('socket-closed');
|
this.emit('socket-closed');
|
||||||
@@ -257,12 +310,15 @@ class WsRequestor extends BaseRequestor {
|
|||||||
}, 'WsRequestor - unexpected response');
|
}, 'WsRequestor - unexpected response');
|
||||||
this.emit('connection-failure');
|
this.emit('connection-failure');
|
||||||
this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`));
|
this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`));
|
||||||
|
this.connections++;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSocketClosed() {
|
_onSocketClosed() {
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.emit('connection-dropped');
|
this.emit('connection-dropped');
|
||||||
|
this._stopPingTimer();
|
||||||
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) {
|
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) {
|
||||||
|
if (!this._initMsgId) this._clearPendingMessages();
|
||||||
this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`);
|
this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
@@ -270,7 +326,9 @@ class WsRequestor extends BaseRequestor {
|
|||||||
'WsRequestor:_onSocketClosed time to reconnect');
|
'WsRequestor:_onSocketClosed time to reconnect');
|
||||||
if (!this.ws && !this.connectInProgress) {
|
if (!this.ws && !this.connectInProgress) {
|
||||||
this.connectInProgress = true;
|
this.connectInProgress = true;
|
||||||
this._connect().catch((err) => this.connectInProgress = false);
|
return this._connect()
|
||||||
|
.catch((err) => this.logger.error('WsRequestor:_onSocketClosed There is error while reconnect', err))
|
||||||
|
.finally(() => this.connectInProgress = false);
|
||||||
}
|
}
|
||||||
}, this.backoffMs);
|
}, this.backoffMs);
|
||||||
this.backoffMs = this.backoffMs < 2000 ? this.backoffMs * 2 : (this.backoffMs + 2000);
|
this.backoffMs = this.backoffMs < 2000 ? this.backoffMs * 2 : (this.backoffMs + 2000);
|
||||||
@@ -288,7 +346,9 @@ class WsRequestor extends BaseRequestor {
|
|||||||
/* messages must be JSON format */
|
/* messages must be JSON format */
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(content);
|
const obj = JSON.parse(content);
|
||||||
const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
|
//const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
|
||||||
|
const {type, msgid, command, queueCommand = false, data} = obj;
|
||||||
|
const call_sid = obj.callSid || this.call_sid;
|
||||||
|
|
||||||
//this.logger.debug({obj}, 'WsRequestor:request websocket: received');
|
//this.logger.debug({obj}, 'WsRequestor:request websocket: received');
|
||||||
assert.ok(type, 'type property not supplied');
|
assert.ok(type, 'type property not supplied');
|
||||||
@@ -314,12 +374,13 @@ class WsRequestor extends BaseRequestor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_recvAck(msgid, data) {
|
_recvAck(msgid, data) {
|
||||||
|
this._initMsgId = null;
|
||||||
const obj = this.messagesInFlight.get(msgid);
|
const obj = this.messagesInFlight.get(msgid);
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
|
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
|
//this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
|
||||||
this.messagesInFlight.delete(msgid);
|
this.messagesInFlight.delete(msgid);
|
||||||
const {success} = obj;
|
const {success} = obj;
|
||||||
success && success(data);
|
success && success(data);
|
||||||
|
|||||||
13316
package-lock.json
generated
13316
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
79
package.json
79
package.json
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "jambonz-feature-server",
|
"name": "jambonz-feature-server",
|
||||||
"version": "v0.7.8",
|
"version": "0.9.0",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.16.0"
|
"node": ">= 18.x"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"sip",
|
"sip",
|
||||||
@@ -19,54 +19,63 @@
|
|||||||
"bugs": {},
|
"bugs": {},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node app",
|
"start": "node app",
|
||||||
"test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=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=error 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/ ",
|
"test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 JAMBONES_TTS_TRIM_SILENCE=1 ENCRYPTION_SECRET=foobar 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=error ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:JambonzR0ck$: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",
|
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||||
"jslint": "eslint app.js lib"
|
"jslint": "eslint app.js tracer.js lib",
|
||||||
|
"jslint:fix": "eslint app.js tracer.js lib --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jambonz/db-helpers": "^0.7.3",
|
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||||
|
"@aws-sdk/client-sns": "^3.549.0",
|
||||||
|
"@jambonz/db-helpers": "^0.9.3",
|
||||||
"@jambonz/http-health-check": "^0.0.1",
|
"@jambonz/http-health-check": "^0.0.1",
|
||||||
"@jambonz/realtimedb-helpers": "^0.5.9",
|
"@jambonz/mw-registrar": "^0.2.7",
|
||||||
"@jambonz/stats-collector": "^0.1.6",
|
"@jambonz/realtimedb-helpers": "^0.8.8",
|
||||||
"@jambonz/time-series": "^0.2.5",
|
"@jambonz/speech-utils": "^0.0.51",
|
||||||
"@opentelemetry/api": "^1.1.0",
|
"@jambonz/stats-collector": "^0.1.9",
|
||||||
"@opentelemetry/exporter-jaeger": "^1.3.1",
|
"@jambonz/time-series": "^0.2.8",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
|
"@jambonz/verb-specifications": "^0.0.69",
|
||||||
"@opentelemetry/exporter-zipkin": "^1.3.1",
|
"@opentelemetry/api": "^1.8.0",
|
||||||
"@opentelemetry/instrumentation": "^0.27.0",
|
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||||
"@opentelemetry/resources": "^1.3.1",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
||||||
"@opentelemetry/sdk-trace-base": "^1.3.1",
|
"@opentelemetry/exporter-zipkin": "^1.23.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^1.3.1",
|
"@opentelemetry/instrumentation": "^0.50.0",
|
||||||
"@opentelemetry/semantic-conventions": "^1.3.1",
|
"@opentelemetry/resources": "^1.23.0",
|
||||||
"aws-sdk": "^2.1152.0",
|
"@opentelemetry/sdk-trace-base": "^1.23.0",
|
||||||
|
"@opentelemetry/sdk-trace-node": "^1.23.0",
|
||||||
|
"@opentelemetry/semantic-conventions": "^1.23.0",
|
||||||
"bent": "^7.3.12",
|
"bent": "^7.3.12",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"deepcopy": "^2.1.0",
|
"deepcopy": "^2.1.0",
|
||||||
"drachtio-fsmrf": "^3.0.16",
|
"drachtio-fsmrf": "^3.0.40",
|
||||||
"drachtio-srf": "^4.5.20",
|
"drachtio-srf": "^4.5.31",
|
||||||
"express": "^4.18.2",
|
"express": "^4.19.2",
|
||||||
"ip": "^1.1.8",
|
"express-validator": "^7.0.1",
|
||||||
"moment": "^2.29.4",
|
"ip": "^2.0.1",
|
||||||
"parse-url": "^8.1.0",
|
"moment": "^2.30.1",
|
||||||
"pino": "^6.14.0",
|
"parse-url": "^9.2.0",
|
||||||
"sdp-transform": "^2.14.1",
|
"pino": "^8.20.0",
|
||||||
"short-uuid": "^4.2.0",
|
"polly-ssml-split": "^0.1.0",
|
||||||
|
"proxyquire": "^2.1.3",
|
||||||
|
"sdp-transform": "^2.14.2",
|
||||||
|
"short-uuid": "^4.2.2",
|
||||||
|
"sinon": "^17.0.1",
|
||||||
"to-snake-case": "^1.0.0",
|
"to-snake-case": "^1.0.0",
|
||||||
"undici": "^5.11.0",
|
"undici": "^6.11.1",
|
||||||
"uuid-random": "^1.3.2",
|
"uuid-random": "^1.3.2",
|
||||||
"verify-aws-sns-signature": "^0.1.0",
|
"verify-aws-sns-signature": "^0.1.0",
|
||||||
"ws": "^8.8.0",
|
"ws": "^8.16.0",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"clear-module": "^4.1.2",
|
"clear-module": "^4.1.2",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-plugin-promise": "^4.3.1",
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"tape": "^5.5.3"
|
"tape": "^5.7.5"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"bufferutil": "^4.0.6",
|
"bufferutil": "^4.0.8",
|
||||||
"utf-8-validate": "^5.0.8"
|
"utf-8-validate": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
test/config-test.js
Normal file
102
test/config-test.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const bent = require('bent');
|
||||||
|
const getJSON = bent('json')
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook} = require('./utils')
|
||||||
|
|
||||||
|
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('\'config: listen\'', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const from = "config_listen_success";
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"listen": {
|
||||||
|
"enable": true,
|
||||||
|
"url": `ws://172.38.0.60:3000/${from}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "pause",
|
||||||
|
"length": 5
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
|
||||||
|
t.pass('config: successfully started background listen');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'config: listen - stop\'', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const from = "config_listen_success";
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"listen": {
|
||||||
|
"enable": true,
|
||||||
|
"url": `ws://172.38.0.60:3000/${from}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "pause",
|
||||||
|
"length": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"listen": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "pause",
|
||||||
|
"length": 3
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
|
||||||
|
t.pass('config: successfully started then stopped background listen');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
86
test/configuration/squid.conf
Normal file
86
test/configuration/squid.conf
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#
|
||||||
|
# Recommended minimum configuration:
|
||||||
|
#
|
||||||
|
|
||||||
|
# Example rule allowing access from your local networks.
|
||||||
|
# Adapt to list your (internal) IP networks from where browsing
|
||||||
|
# should be allowed
|
||||||
|
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
|
||||||
|
acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
|
||||||
|
acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
|
||||||
|
acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
|
||||||
|
acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN)
|
||||||
|
acl localnet src 172.38.0.0/12 # RFC 1918 local private network (LAN)
|
||||||
|
acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
|
||||||
|
acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||||
|
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||||
|
|
||||||
|
acl SSL_ports port 443
|
||||||
|
acl Safe_ports port 80 # http
|
||||||
|
acl Safe_ports port 21 # ftp
|
||||||
|
acl Safe_ports port 443 # https
|
||||||
|
acl Safe_ports port 70 # gopher
|
||||||
|
acl Safe_ports port 210 # wais
|
||||||
|
acl Safe_ports port 1025-65535 # unregistered ports
|
||||||
|
acl Safe_ports port 280 # http-mgmt
|
||||||
|
acl Safe_ports port 488 # gss-http
|
||||||
|
acl Safe_ports port 591 # filemaker
|
||||||
|
acl Safe_ports port 777 # multiling http
|
||||||
|
|
||||||
|
#
|
||||||
|
# Recommended minimum Access Permission configuration:
|
||||||
|
#
|
||||||
|
# Deny requests to certain unsafe ports
|
||||||
|
http_access allow !Safe_ports
|
||||||
|
|
||||||
|
# Deny CONNECT to other than secure SSL ports
|
||||||
|
http_access allow CONNECT !SSL_ports
|
||||||
|
|
||||||
|
# Only allow cachemgr access from localhost
|
||||||
|
http_access allow localhost manager
|
||||||
|
http_access allow manager
|
||||||
|
|
||||||
|
# This default configuration only allows localhost requests because a more
|
||||||
|
# permissive Squid installation could introduce new attack vectors into the
|
||||||
|
# network by proxying external TCP connections to unprotected services.
|
||||||
|
http_access allow localhost
|
||||||
|
|
||||||
|
# The two deny rules below are unnecessary in this default configuration
|
||||||
|
# because they are followed by a "deny all" rule. However, they may become
|
||||||
|
# critically important when you start allowing external requests below them.
|
||||||
|
|
||||||
|
# Protect web applications running on the same server as Squid. They often
|
||||||
|
# assume that only local users can access them at "localhost" ports.
|
||||||
|
http_access allow to_localhost
|
||||||
|
|
||||||
|
# Protect cloud servers that provide local users with sensitive info about
|
||||||
|
# their server via certain well-known link-local (a.k.a. APIPA) addresses.
|
||||||
|
# http_access deny to_linklocal
|
||||||
|
|
||||||
|
#
|
||||||
|
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
|
||||||
|
#
|
||||||
|
|
||||||
|
# For example, to allow access from your local networks, you may uncomment the
|
||||||
|
# following rule (and/or add rules that match your definition of "local"):
|
||||||
|
# http_access allow localnet
|
||||||
|
|
||||||
|
# And finally deny all other access to this proxy
|
||||||
|
http_access allow all
|
||||||
|
|
||||||
|
# Squid normally listens to port 3128
|
||||||
|
http_port 3128
|
||||||
|
|
||||||
|
# Uncomment and adjust the following to add a disk cache directory.
|
||||||
|
#cache_dir ufs /usr/local/var/cache/squid 100 16 256
|
||||||
|
|
||||||
|
# Leave coredumps in the first cache dir
|
||||||
|
coredump_dir /usr/local/var/cache/squid
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add any of your own refresh_pattern entries above these.
|
||||||
|
#
|
||||||
|
refresh_pattern ^ftp: 1440 20% 10080
|
||||||
|
refresh_pattern ^gopher: 1440 0% 1440
|
||||||
|
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
|
||||||
|
refresh_pattern . 0 20% 4320
|
||||||
@@ -37,7 +37,7 @@ test('test create-call timeout', async(t) => {
|
|||||||
'account_sid':account_sid,
|
'account_sid':account_sid,
|
||||||
'timeout': 1,
|
'timeout': 1,
|
||||||
"call_hook": {
|
"call_hook": {
|
||||||
"url": "https://public-apps.jambonz.us/hello-world",
|
"url": "https://public-apps.jambonz.cloud/hello-world",
|
||||||
"method": "POST"
|
"method": "POST"
|
||||||
},
|
},
|
||||||
"from": "15083718299",
|
"from": "15083718299",
|
||||||
@@ -88,17 +88,133 @@ test('test create-call call-hook basic authentication', async(t) => {
|
|||||||
|
|
||||||
let verbs = [
|
let verbs = [
|
||||||
{
|
{
|
||||||
"verb": "say",
|
"verb": "pause",
|
||||||
"text": "hello"
|
"length": 1
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
//THEN
|
//THEN
|
||||||
await p;
|
await p;
|
||||||
|
|
||||||
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}`)
|
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}`)
|
||||||
t.ok(obj.headers.Authorization = 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
t.ok(obj.headers.Authorization = 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
|
||||||
'create-call: call-hook contains basic authentication header');
|
'create-call: call-hook contains basic authentication header');
|
||||||
|
t.ok(obj.headers['user-agent'] = 'jambonz',
|
||||||
|
'create-call: call-hook contains user-agent header');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test create-call amd', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
let from = 'create-call-amd';
|
||||||
|
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
|
||||||
|
|
||||||
|
// Give UAS app time to come up
|
||||||
|
const p = sippUac('uas.xml', '172.38.0.10', from);
|
||||||
|
await waitFor(1000);
|
||||||
|
|
||||||
|
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||||
|
post('v1/createCall', {
|
||||||
|
'account_sid':account_sid,
|
||||||
|
"call_hook": {
|
||||||
|
"url": "http://127.0.0.1:3100/",
|
||||||
|
"method": "POST",
|
||||||
|
"username": "username",
|
||||||
|
"password": "password"
|
||||||
|
},
|
||||||
|
"from": from,
|
||||||
|
"to": {
|
||||||
|
"type": "phone",
|
||||||
|
"number": "15583084809"
|
||||||
|
},
|
||||||
|
"amd": {
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
},
|
||||||
|
"speech_recognizer_vendor": "google",
|
||||||
|
"speech_recognizer_language": "en"
|
||||||
|
});
|
||||||
|
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "pause",
|
||||||
|
"length": 7
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
//THEN
|
||||||
|
await p;
|
||||||
|
|
||||||
|
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`)
|
||||||
|
t.ok(obj.body.type = 'amd_no_speech_detected',
|
||||||
|
'create-call: AMD detected');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test create-call app_json', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
let from = 'create-call-app-json';
|
||||||
|
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
|
||||||
|
|
||||||
|
// Give UAS app time to come up
|
||||||
|
const p = sippUac('uas.xml', '172.38.0.10', from);
|
||||||
|
await waitFor(1000);
|
||||||
|
|
||||||
|
const app_json = `[
|
||||||
|
{
|
||||||
|
"verb": "pause",
|
||||||
|
"length": 7
|
||||||
|
}
|
||||||
|
]`;
|
||||||
|
|
||||||
|
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||||
|
post('v1/createCall', {
|
||||||
|
'account_sid':account_sid,
|
||||||
|
"call_hook": {
|
||||||
|
"url": "http://127.0.0.1:3100/",
|
||||||
|
"method": "POST",
|
||||||
|
"username": "username",
|
||||||
|
"password": "password"
|
||||||
|
},
|
||||||
|
app_json,
|
||||||
|
"from": from,
|
||||||
|
"to": {
|
||||||
|
"type": "phone",
|
||||||
|
"number": "15583084809"
|
||||||
|
},
|
||||||
|
"amd": {
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
},
|
||||||
|
"speech_recognizer_vendor": "google",
|
||||||
|
"speech_recognizer_language": "en"
|
||||||
|
});
|
||||||
|
|
||||||
|
//THEN
|
||||||
|
await p;
|
||||||
|
|
||||||
disconnect();
|
disconnect();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`error received: ${err}`);
|
console.log(`error received: ${err}`);
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ const test = require('tape') ;
|
|||||||
const exec = require('child_process').exec ;
|
const exec = require('child_process').exec ;
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const {encrypt} = require('../lib/utils/encrypt-decrypt');
|
const {encrypt} = require('../lib/utils/encrypt-decrypt');
|
||||||
|
const {
|
||||||
|
GCP_JSON_KEY,
|
||||||
|
AWS_ACCESS_KEY_ID,
|
||||||
|
AWS_SECRET_ACCESS_KEY,
|
||||||
|
AWS_REGION,
|
||||||
|
MICROSOFT_REGION,
|
||||||
|
MICROSOFT_API_KEY,
|
||||||
|
} = require('../lib/config');
|
||||||
|
|
||||||
test('creating jambones_test database', (t) => {
|
test('creating jambones_test database', (t) => {
|
||||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__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) => {
|
||||||
@@ -18,24 +26,38 @@ test('creating schema', (t) => {
|
|||||||
if (err) return t.end(err);
|
if (err) return t.end(err);
|
||||||
t.pass('schema and test data successfully created');
|
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 sql = [];
|
||||||
const google_credential = encrypt(process.env.GCP_JSON_KEY);
|
if (GCP_JSON_KEY) {
|
||||||
|
const google_credential = encrypt(GCP_JSON_KEY);
|
||||||
|
t.pass('adding google credentials');
|
||||||
|
sql.push(`UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';`);
|
||||||
|
}
|
||||||
|
if (AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) {
|
||||||
const aws_credential = encrypt(JSON.stringify({
|
const aws_credential = encrypt(JSON.stringify({
|
||||||
access_key_id: process.env.AWS_ACCESS_KEY_ID,
|
access_key_id: AWS_ACCESS_KEY_ID,
|
||||||
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY
|
secret_access_key: AWS_SECRET_ACCESS_KEY,
|
||||||
|
aws_region: AWS_REGION
|
||||||
}));
|
}));
|
||||||
const cmd = `
|
t.pass('adding aws credentials');
|
||||||
UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';
|
sql.push(`UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';`);
|
||||||
UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';
|
}
|
||||||
`;
|
if (MICROSOFT_REGION && MICROSOFT_API_KEY) {
|
||||||
|
const microsoft_credential = encrypt(JSON.stringify({
|
||||||
|
region: MICROSOFT_REGION,
|
||||||
|
api_key: MICROSOFT_API_KEY
|
||||||
|
}));
|
||||||
|
t.pass('adding microsoft credentials');
|
||||||
|
sql.push(`UPDATE speech_credentials SET credential='${microsoft_credential}' WHERE vendor='microsoft';`);
|
||||||
|
}
|
||||||
|
if (sql.length > 0) {
|
||||||
const path = `${__dirname}/.creds.sql`;
|
const path = `${__dirname}/.creds.sql`;
|
||||||
fs.writeFileSync(path, cmd);
|
const cmd = sql.join('\n');
|
||||||
|
fs.writeFileSync(path, sql.join('\n'));
|
||||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${path}`, (err, stdout, stderr) => {
|
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(stdout);
|
||||||
console.log(stderr);
|
console.log(stderr);
|
||||||
if (err) return t.end(err);
|
if (err) return t.end(err);
|
||||||
fs.unlinkSync(path)
|
fs.unlinkSync(path)
|
||||||
fs.writeFileSync(`${__dirname}/credentials/gcp.json`, process.env.GCP_JSON_KEY);
|
|
||||||
t.pass('set account-level speech credentials');
|
t.pass('set account-level speech credentials');
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|||||||
9
test/data/bad/bad-say-ssml.json
Normal file
9
test/data/bad/bad-say-ssml.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"say": {
|
||||||
|
"text": "<speak>I already told you <emphasis level=\"strong\">I already told you I already told you I already told you I already told you! I already told you I already told you I already told you I already told you? I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you told I already told you I already told you told I already told you I already told you. I already told you <break time=\"3s\"/> I really like that person!</emphasis> this is another long text.</speak>",
|
||||||
|
"synthesizer": {
|
||||||
|
"vendor": "google",
|
||||||
|
"language": "en-US"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
test/data/good/say-ssml.json
Normal file
9
test/data/good/say-ssml.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"say": {
|
||||||
|
"text": "<speak>I already told you I already told you I already told you I already told you I already told you! I already told you I already told you I already told you I already told you? I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you I already told you told I already told you I already told you told I already told you I already told you. I already told you <break time=\"3s\"/> I <emphasis level=\"strong\">really like that person!</emphasis> this is another long text.</speak>",
|
||||||
|
"synthesizer": {
|
||||||
|
"vendor": "google",
|
||||||
|
"language": "en-US"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
|||||||
/* SQLEditor (MySQL (2))*/
|
/* SQLEditor (MySQL (2))*/
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS=0;
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS account_static_ips;
|
DROP TABLE IF EXISTS account_static_ips;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS account_limits;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS account_products;
|
DROP TABLE IF EXISTS account_products;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS account_subscriptions;
|
DROP TABLE IF EXISTS account_subscriptions;
|
||||||
@@ -12,12 +13,22 @@ DROP TABLE IF EXISTS beta_invite_codes;
|
|||||||
|
|
||||||
DROP TABLE IF EXISTS call_routes;
|
DROP TABLE IF EXISTS call_routes;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS clients;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS dns_records;
|
DROP TABLE IF EXISTS dns_records;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS lcr;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS lcr_routes;
|
DROP TABLE IF EXISTS lcr_routes;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS password_settings;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS user_permissions;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS permissions;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS predefined_sip_gateways;
|
DROP TABLE IF EXISTS predefined_sip_gateways;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS predefined_smpp_gateways;
|
DROP TABLE IF EXISTS predefined_smpp_gateways;
|
||||||
@@ -36,12 +47,16 @@ DROP TABLE IF EXISTS sbc_addresses;
|
|||||||
|
|
||||||
DROP TABLE IF EXISTS ms_teams_tenants;
|
DROP TABLE IF EXISTS ms_teams_tenants;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS service_provider_limits;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS signup_history;
|
DROP TABLE IF EXISTS signup_history;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS smpp_addresses;
|
DROP TABLE IF EXISTS smpp_addresses;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS speech_credentials;
|
DROP TABLE IF EXISTS speech_credentials;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS system_information;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS smpp_gateways;
|
DROP TABLE IF EXISTS smpp_gateways;
|
||||||
@@ -69,6 +84,15 @@ private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
|
|||||||
PRIMARY KEY (account_static_ip_sid)
|
PRIMARY KEY (account_static_ip_sid)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE account_limits
|
||||||
|
(
|
||||||
|
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
|
account_sid CHAR(36) NOT NULL,
|
||||||
|
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (account_limits_sid)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE account_subscriptions
|
CREATE TABLE account_subscriptions
|
||||||
(
|
(
|
||||||
account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
|
account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
@@ -105,6 +129,16 @@ application_sid CHAR(36) NOT NULL,
|
|||||||
PRIMARY KEY (call_route_sid)
|
PRIMARY KEY (call_route_sid)
|
||||||
) COMMENT='a regex-based pattern match for call routing';
|
) COMMENT='a regex-based pattern match for call routing';
|
||||||
|
|
||||||
|
CREATE TABLE clients
|
||||||
|
(
|
||||||
|
client_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
|
account_sid CHAR(36) NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
username VARCHAR(64),
|
||||||
|
password VARCHAR(1024),
|
||||||
|
PRIMARY KEY (client_sid)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE dns_records
|
CREATE TABLE dns_records
|
||||||
(
|
(
|
||||||
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
|
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
@@ -117,11 +151,38 @@ PRIMARY KEY (dns_record_sid)
|
|||||||
CREATE TABLE lcr_routes
|
CREATE TABLE lcr_routes
|
||||||
(
|
(
|
||||||
lcr_route_sid CHAR(36),
|
lcr_route_sid CHAR(36),
|
||||||
|
lcr_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',
|
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
|
||||||
description VARCHAR(1024),
|
description VARCHAR(1024),
|
||||||
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
|
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
|
||||||
PRIMARY KEY (lcr_route_sid)
|
PRIMARY KEY (lcr_route_sid)
|
||||||
) COMMENT='Least cost routing table';
|
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches';
|
||||||
|
|
||||||
|
CREATE TABLE lcr
|
||||||
|
(
|
||||||
|
lcr_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
|
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
|
||||||
|
service_provider_sid CHAR(36),
|
||||||
|
account_sid CHAR(36),
|
||||||
|
PRIMARY KEY (lcr_sid)
|
||||||
|
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.';
|
||||||
|
|
||||||
|
CREATE TABLE password_settings
|
||||||
|
(
|
||||||
|
min_password_length INTEGER NOT NULL DEFAULT 8,
|
||||||
|
require_digit BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
require_special_character BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE permissions
|
||||||
|
(
|
||||||
|
permission_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
|
name VARCHAR(32) NOT NULL UNIQUE ,
|
||||||
|
description VARCHAR(255),
|
||||||
|
PRIMARY KEY (permission_sid)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE predefined_carriers
|
CREATE TABLE predefined_carriers
|
||||||
(
|
(
|
||||||
@@ -214,7 +275,10 @@ CREATE TABLE sbc_addresses
|
|||||||
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
ipv4 VARCHAR(255) NOT NULL,
|
ipv4 VARCHAR(255) NOT NULL,
|
||||||
port INTEGER NOT NULL DEFAULT 5060,
|
port INTEGER NOT NULL DEFAULT 5060,
|
||||||
|
tls_port INTEGER,
|
||||||
|
wss_port INTEGER,
|
||||||
service_provider_sid CHAR(36),
|
service_provider_sid CHAR(36),
|
||||||
|
last_updated DATETIME,
|
||||||
PRIMARY KEY (sbc_address_sid)
|
PRIMARY KEY (sbc_address_sid)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -228,6 +292,15 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
|
|||||||
PRIMARY KEY (ms_teams_tenant_sid)
|
PRIMARY KEY (ms_teams_tenant_sid)
|
||||||
) COMMENT='A Microsoft Teams customer tenant';
|
) COMMENT='A Microsoft Teams customer tenant';
|
||||||
|
|
||||||
|
CREATE TABLE service_provider_limits
|
||||||
|
(
|
||||||
|
service_provider_limits_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
|
service_provider_sid CHAR(36) NOT NULL,
|
||||||
|
category ENUM('api_rate','voice_call_session', 'device','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (service_provider_limits_sid)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE signup_history
|
CREATE TABLE signup_history
|
||||||
(
|
(
|
||||||
email VARCHAR(255) NOT NULL,
|
email VARCHAR(255) NOT NULL,
|
||||||
@@ -261,9 +334,17 @@ last_tested DATETIME,
|
|||||||
tts_tested_ok BOOLEAN,
|
tts_tested_ok BOOLEAN,
|
||||||
stt_tested_ok BOOLEAN,
|
stt_tested_ok BOOLEAN,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
label VARCHAR(64),
|
||||||
PRIMARY KEY (speech_credential_sid)
|
PRIMARY KEY (speech_credential_sid)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE system_information
|
||||||
|
(
|
||||||
|
domain_name VARCHAR(255),
|
||||||
|
sip_domain_name VARCHAR(255),
|
||||||
|
monitoring_domain_name VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE users
|
CREATE TABLE users
|
||||||
(
|
(
|
||||||
user_sid CHAR(36) NOT NULL UNIQUE ,
|
user_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
@@ -283,6 +364,7 @@ email_activation_code VARCHAR(16),
|
|||||||
email_validated BOOLEAN NOT NULL DEFAULT false,
|
email_validated BOOLEAN NOT NULL DEFAULT false,
|
||||||
phone_validated BOOLEAN NOT NULL DEFAULT false,
|
phone_validated BOOLEAN NOT NULL DEFAULT false,
|
||||||
email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
|
email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
PRIMARY KEY (user_sid)
|
PRIMARY KEY (user_sid)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -310,9 +392,21 @@ smpp_password VARCHAR(64),
|
|||||||
smpp_enquire_link_interval INTEGER DEFAULT 0,
|
smpp_enquire_link_interval INTEGER DEFAULT 0,
|
||||||
smpp_inbound_system_id VARCHAR(255),
|
smpp_inbound_system_id VARCHAR(255),
|
||||||
smpp_inbound_password VARCHAR(64),
|
smpp_inbound_password VARCHAR(64),
|
||||||
|
register_from_user VARCHAR(128),
|
||||||
|
register_from_domain VARCHAR(255),
|
||||||
|
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
register_status VARCHAR(4096),
|
||||||
PRIMARY KEY (voip_carrier_sid)
|
PRIMARY KEY (voip_carrier_sid)
|
||||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||||
|
|
||||||
|
CREATE TABLE user_permissions
|
||||||
|
(
|
||||||
|
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
|
user_sid CHAR(36) NOT NULL,
|
||||||
|
permission_sid CHAR(36) NOT NULL,
|
||||||
|
PRIMARY KEY (user_permissions_sid)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE smpp_gateways
|
CREATE TABLE smpp_gateways
|
||||||
(
|
(
|
||||||
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
|
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
@@ -330,7 +424,7 @@ PRIMARY KEY (smpp_gateway_sid)
|
|||||||
CREATE TABLE phone_numbers
|
CREATE TABLE phone_numbers
|
||||||
(
|
(
|
||||||
phone_number_sid CHAR(36) UNIQUE ,
|
phone_number_sid CHAR(36) UNIQUE ,
|
||||||
number VARCHAR(32) NOT NULL UNIQUE ,
|
number VARCHAR(132) NOT NULL,
|
||||||
voip_carrier_sid CHAR(36),
|
voip_carrier_sid CHAR(36),
|
||||||
account_sid CHAR(36),
|
account_sid CHAR(36),
|
||||||
application_sid CHAR(36),
|
application_sid CHAR(36),
|
||||||
@@ -348,6 +442,7 @@ inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound ca
|
|||||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
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,
|
voip_carrier_sid CHAR(36) NOT NULL,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
|
||||||
PRIMARY KEY (sip_gateway_sid)
|
PRIMARY KEY (sip_gateway_sid)
|
||||||
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
||||||
|
|
||||||
@@ -380,12 +475,16 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
|
|||||||
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
|
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',
|
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 ',
|
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
||||||
|
app_json TEXT,
|
||||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||||
speech_synthesis_voice VARCHAR(64),
|
speech_synthesis_voice VARCHAR(64),
|
||||||
|
speech_synthesis_label VARCHAR(64),
|
||||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||||
|
speech_recognizer_label VARCHAR(64),
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||||
PRIMARY KEY (application_sid)
|
PRIMARY KEY (application_sid)
|
||||||
) COMMENT='A defined set of behaviors to be applied to phone calls ';
|
) COMMENT='A defined set of behaviors to be applied to phone calls ';
|
||||||
|
|
||||||
@@ -418,6 +517,14 @@ disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
|
|||||||
trial_end_date DATETIME,
|
trial_end_date DATETIME,
|
||||||
deactivated_reason VARCHAR(255),
|
deactivated_reason VARCHAR(255),
|
||||||
device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
|
device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
|
||||||
|
subspace_client_id VARCHAR(255),
|
||||||
|
subspace_client_secret VARCHAR(255),
|
||||||
|
subspace_sip_teleport_id VARCHAR(255),
|
||||||
|
subspace_sip_teleport_destinations VARCHAR(255),
|
||||||
|
siprec_hook_sid CHAR(36),
|
||||||
|
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
|
||||||
|
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
|
||||||
PRIMARY KEY (account_sid)
|
PRIMARY KEY (account_sid)
|
||||||
) COMMENT='An enterprise that uses the platform for comm services';
|
) COMMENT='An enterprise that uses the platform for comm services';
|
||||||
|
|
||||||
@@ -425,19 +532,34 @@ CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_
|
|||||||
CREATE INDEX account_sid_idx ON account_static_ips (account_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);
|
ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
|
CREATE INDEX account_sid_idx ON account_limits (account_sid);
|
||||||
|
ALTER TABLE account_limits ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid) ON DELETE CASCADE;
|
||||||
|
|
||||||
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
|
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
|
||||||
CREATE INDEX account_sid_idx ON account_subscriptions (account_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);
|
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
|
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
|
||||||
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
|
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 account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_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);
|
CREATE INDEX client_sid_idx ON clients (client_sid);
|
||||||
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
|
ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
|
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
|
||||||
|
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
|
CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid);
|
||||||
|
ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid);
|
||||||
|
|
||||||
|
CREATE INDEX lcr_sid_idx ON lcr (lcr_sid);
|
||||||
|
ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid);
|
||||||
|
|
||||||
|
CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid);
|
||||||
|
CREATE INDEX account_sid_idx ON lcr (account_sid);
|
||||||
|
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
|
||||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_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_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);
|
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
|
||||||
@@ -456,14 +578,14 @@ ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REF
|
|||||||
|
|
||||||
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
|
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
|
||||||
CREATE INDEX account_sid_idx ON account_offers (account_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);
|
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
CREATE INDEX product_sid_idx ON account_offers (product_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);
|
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 api_key_sid_idx ON api_keys (api_key_sid);
|
||||||
CREATE INDEX account_sid_idx ON api_keys (account_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);
|
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_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);
|
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||||
@@ -477,59 +599,68 @@ ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_
|
|||||||
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_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 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 account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_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 tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
|
||||||
|
CREATE INDEX service_provider_sid_idx ON service_provider_limits (service_provider_sid);
|
||||||
|
ALTER TABLE service_provider_limits ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid) ON DELETE CASCADE;
|
||||||
|
|
||||||
CREATE INDEX email_idx ON signup_history (email);
|
CREATE INDEX email_idx ON signup_history (email);
|
||||||
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
|
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
|
||||||
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_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);
|
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (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 speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
|
||||||
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_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);
|
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||||
|
|
||||||
CREATE INDEX account_sid_idx ON speech_credentials (account_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);
|
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
CREATE INDEX user_sid_idx ON users (user_sid);
|
CREATE INDEX user_sid_idx ON users (user_sid);
|
||||||
CREATE INDEX email_idx ON users (email);
|
CREATE INDEX email_idx ON users (email);
|
||||||
CREATE INDEX phone_idx ON users (phone);
|
CREATE INDEX phone_idx ON users (phone);
|
||||||
CREATE INDEX account_sid_idx ON users (account_sid);
|
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);
|
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
CREATE INDEX service_provider_sid_idx ON users (service_provider_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);
|
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||||
|
|
||||||
CREATE INDEX email_activation_code_idx ON users (email_activation_code);
|
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 voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
|
||||||
CREATE INDEX account_sid_idx ON voip_carriers (account_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);
|
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_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 service_provider_sid_idxfk_7 (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);
|
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
|
||||||
|
|
||||||
|
CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid);
|
||||||
|
CREATE INDEX user_sid_idx ON user_permissions (user_sid);
|
||||||
|
ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid);
|
||||||
|
|
||||||
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_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);
|
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);
|
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid);
|
||||||
|
|
||||||
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
|
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
|
||||||
CREATE INDEX number_idx ON phone_numbers (number);
|
CREATE INDEX number_idx ON phone_numbers (number);
|
||||||
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
|
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 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 account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
|
||||||
|
|
||||||
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_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);
|
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);
|
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||||
|
|
||||||
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
||||||
|
|
||||||
@@ -545,10 +676,10 @@ CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
|
|||||||
|
|
||||||
CREATE INDEX application_sid_idx ON applications (application_sid);
|
CREATE INDEX application_sid_idx ON applications (application_sid);
|
||||||
CREATE INDEX service_provider_sid_idx ON applications (service_provider_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);
|
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||||
|
|
||||||
CREATE INDEX account_sid_idx ON applications (account_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 account_sid_idxfk_12 (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_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||||
|
|
||||||
@@ -564,7 +695,7 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis
|
|||||||
CREATE INDEX account_sid_idx ON accounts (account_sid);
|
CREATE INDEX account_sid_idx ON accounts (account_sid);
|
||||||
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
|
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
|
||||||
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
|
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 service_provider_sid_idxfk_10 (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 registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
|
||||||
|
|
||||||
@@ -572,4 +703,5 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
|
|||||||
|
|
||||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS=0;
|
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
217
test/dial-tests.js
Normal file
217
test/dial-tests.js
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const bent = require('bent');
|
||||||
|
const getJSON = bent('json')
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook} = require('./utils')
|
||||||
|
|
||||||
|
const sleepFor = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
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('\'dial-phone\'', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// wait for fs connected to drachtio server.
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const from = "dial_success";
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "dial",
|
||||||
|
"callerId": from,
|
||||||
|
"callerName": "test_callerName",
|
||||||
|
"actionHook": "/actionHook",
|
||||||
|
"timeLimit": 5,
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"type": "phone",
|
||||||
|
"number": "15083084809"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||||
|
await sleepFor(1000);
|
||||||
|
|
||||||
|
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
|
||||||
|
let post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||||
|
post('v1/createCall', {
|
||||||
|
'account_sid':account_sid,
|
||||||
|
"call_hook": {
|
||||||
|
"url": "http://127.0.0.1:3100/",
|
||||||
|
"method": "POST",
|
||||||
|
},
|
||||||
|
"from": from,
|
||||||
|
"callerName": "Tom",
|
||||||
|
"to": {
|
||||||
|
"type": "phone",
|
||||||
|
"number": "15583084808"
|
||||||
|
}});
|
||||||
|
|
||||||
|
await p;
|
||||||
|
|
||||||
|
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj.body.from === from,
|
||||||
|
'dial: succeeds actionHook');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('\'dial-sip\'', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// wait for fs connected to drachtio server.
|
||||||
|
await sleepFor(1000);
|
||||||
|
// GIVEN
|
||||||
|
const from = "dial_sip";
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "dial",
|
||||||
|
"callerId": from,
|
||||||
|
"actionHook": "/actionHook",
|
||||||
|
"dtmfCapture":["*2", "*3"],
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"type": "sip",
|
||||||
|
"sipUri": "sip:15083084809@jambonz.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||||
|
|
||||||
|
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
|
||||||
|
|
||||||
|
let post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||||
|
post('v1/createCall', {
|
||||||
|
'account_sid':account_sid,
|
||||||
|
"call_hook": {
|
||||||
|
"url": "http://127.0.0.1:3100/",
|
||||||
|
"method": "POST",
|
||||||
|
},
|
||||||
|
"from": from,
|
||||||
|
"to": {
|
||||||
|
"type": "phone",
|
||||||
|
"number": "15583084808"
|
||||||
|
}});
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}`);
|
||||||
|
const callSid = obj.body.call_sid;
|
||||||
|
|
||||||
|
post = bent('http://127.0.0.1:3000/', 'POST', 202);
|
||||||
|
await post(`v1/updateCall/${callSid}`, {
|
||||||
|
"call_status": "completed"
|
||||||
|
});
|
||||||
|
|
||||||
|
await p;
|
||||||
|
|
||||||
|
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj.body.from === from,
|
||||||
|
'dial: succeeds actionHook');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'dial-user\'', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// wait for fs connected to drachtio server.
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
// GIVEN
|
||||||
|
const from = "dial_user";
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "dial",
|
||||||
|
"callerId": from,
|
||||||
|
"actionHook": "/actionHook",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"type": "user",
|
||||||
|
"name": "user110@jambonz.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||||
|
|
||||||
|
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
|
||||||
|
|
||||||
|
let post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||||
|
post('v1/createCall', {
|
||||||
|
'account_sid':account_sid,
|
||||||
|
"call_hook": {
|
||||||
|
"url": "http://127.0.0.1:3100/",
|
||||||
|
"method": "POST",
|
||||||
|
},
|
||||||
|
"from": from,
|
||||||
|
"to": {
|
||||||
|
"type": "phone",
|
||||||
|
"number": "15583084808"
|
||||||
|
}});
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}`);
|
||||||
|
const callSid = obj.body.call_sid;
|
||||||
|
|
||||||
|
post = bent('http://127.0.0.1:3000/', 'POST', 202);
|
||||||
|
await post(`v1/updateCall/${callSid}`, {
|
||||||
|
"call_status": "completed"
|
||||||
|
});
|
||||||
|
|
||||||
|
await p;
|
||||||
|
|
||||||
|
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj.body.from === from,
|
||||||
|
'dial: succeeds actionHook');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -42,9 +42,9 @@ services:
|
|||||||
ipv4_address: 172.38.0.7
|
ipv4_address: 172.38.0.7
|
||||||
|
|
||||||
drachtio:
|
drachtio:
|
||||||
image: drachtio/drachtio-server:latest
|
image: drachtio/drachtio-server:0.8.25-rc8
|
||||||
restart: always
|
restart: always
|
||||||
command: drachtio --contact "sip:*;transport=udp,tcp" --address 0.0.0.0 --port 9022
|
command: drachtio --contact "sip:*;transport=udp" --mtu 4096 --address 0.0.0.0 --port 9022
|
||||||
ports:
|
ports:
|
||||||
- "9060:9022/tcp"
|
- "9060:9022/tcp"
|
||||||
networks:
|
networks:
|
||||||
@@ -57,7 +57,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
freeswitch:
|
freeswitch:
|
||||||
image: drachtio/drachtio-freeswitch-mrf:v1.10.1-full
|
image: drachtio/drachtio-freeswitch-mrf:0.7.3
|
||||||
restart: always
|
restart: always
|
||||||
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
|
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
|
||||||
environment:
|
environment:
|
||||||
@@ -68,7 +68,7 @@ services:
|
|||||||
- /tmp:/tmp
|
- /tmp:/tmp
|
||||||
- ./credentials:/opt/credentials
|
- ./credentials:/opt/credentials
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'fs_cli' ,'-x', '"sofia status"']
|
test: ['CMD', 'fs_cli' ,'-p', 'JambonzR0ck$$', '-x', '"sofia status"']
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 15
|
retries: 15
|
||||||
networks:
|
networks:
|
||||||
@@ -92,3 +92,13 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
fs:
|
fs:
|
||||||
ipv4_address: 172.38.0.90
|
ipv4_address: 172.38.0.90
|
||||||
|
|
||||||
|
squid:
|
||||||
|
image: ubuntu/squid:edge
|
||||||
|
ports:
|
||||||
|
- "3128:3128"
|
||||||
|
volumes:
|
||||||
|
- ./configuration/squid.conf:/etc/squid/squid.conf
|
||||||
|
networks:
|
||||||
|
fs:
|
||||||
|
ipv4_address: 172.38.0.91
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ const bent = require('bent');
|
|||||||
const getJSON = bent('json')
|
const getJSON = bent('json')
|
||||||
const clearModule = require('clear-module');
|
const clearModule = require('clear-module');
|
||||||
const {provisionCallHook} = require('./utils')
|
const {provisionCallHook} = require('./utils')
|
||||||
|
const {
|
||||||
|
GCP_JSON_KEY,
|
||||||
|
AWS_ACCESS_KEY_ID,
|
||||||
|
AWS_SECRET_ACCESS_KEY,
|
||||||
|
SONIOX_API_KEY,
|
||||||
|
DEEPGRAM_API_KEY,
|
||||||
|
MICROSOFT_REGION,
|
||||||
|
MICROSOFT_API_KEY,
|
||||||
|
} = require('../lib/config');
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) => {
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||||
@@ -17,7 +26,11 @@ function connect(connectable) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('\'gather\' and \'transcribe\' tests', async(t) => {
|
test('\'gather\' test - google', async(t) => {
|
||||||
|
if (!GCP_JSON_KEY) {
|
||||||
|
t.pass('skipping google tests');
|
||||||
|
return t.end();
|
||||||
|
}
|
||||||
clearModule.all();
|
clearModule.all();
|
||||||
const {srf, disconnect} = require('../app');
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
@@ -37,12 +50,261 @@ test('\'gather\' and \'transcribe\' tests', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
t.ok(obj.body.speech.alternatives[0].transcript = 'I\'d like to speak to customer support',
|
//console.log(JSON.stringify(obj));
|
||||||
'gather: succeeds when using account credentials');
|
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||||
|
'gather: succeeds when using google credentials');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'gather\' test - default (google)', async(t) => {
|
||||||
|
if (!GCP_JSON_KEY) {
|
||||||
|
t.pass('skipping google tests');
|
||||||
|
return t.end();
|
||||||
|
}
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "gather",
|
||||||
|
"input": ["speech"],
|
||||||
|
"timeout": 10,
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let from = "gather_success";
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
//console.log(JSON.stringify(obj));
|
||||||
|
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase() === 'i\'d like to speak to customer support',
|
||||||
|
'gather: succeeds when using default (google) credentials');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'config\' test - reset to app defaults', async(t) => {
|
||||||
|
if (!GCP_JSON_KEY) {
|
||||||
|
t.pass('skipping config tests');
|
||||||
|
return t.end();
|
||||||
|
}
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"recognizer": {
|
||||||
|
"vendor": "google",
|
||||||
|
"language": "fr-FR"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"reset": ['recognizer'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "gather",
|
||||||
|
"input": ["speech"],
|
||||||
|
"timeout": 10,
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let from = "gather_success";
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
//console.log(JSON.stringify(obj));
|
||||||
|
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase() === 'i\'d like to speak to customer support',
|
||||||
|
'config: resets recognizer to app defaults');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'gather\' test - microsoft', async(t) => {
|
||||||
|
if (!MICROSOFT_REGION || !MICROSOFT_API_KEY) {
|
||||||
|
t.pass('skipping microsoft tests');
|
||||||
|
return t.end();
|
||||||
|
}
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "gather",
|
||||||
|
"input": ["speech"],
|
||||||
|
"recognizer": {
|
||||||
|
"vendor": "microsoft",
|
||||||
|
"hints": ["customer support", "sales", "human resources", "HR"]
|
||||||
|
},
|
||||||
|
"timeout": 10,
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let from = "gather_success";
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
//console.log(JSON.stringify(obj));
|
||||||
|
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||||
|
'gather: succeeds when using microsoft credentials');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'gather\' test - aws', async(t) => {
|
||||||
|
if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) {
|
||||||
|
t.pass('skipping aws tests');
|
||||||
|
return t.end();
|
||||||
|
}
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "gather",
|
||||||
|
"input": ["speech"],
|
||||||
|
"recognizer": {
|
||||||
|
"vendor": "aws",
|
||||||
|
"hints": ["customer support", "sales", "human resources", "HR"]
|
||||||
|
},
|
||||||
|
"timeout": 10,
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let from = "gather_success";
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
//console.log(JSON.stringify(obj));
|
||||||
|
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||||
|
'gather: succeeds when using aws credentials');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'gather\' test - deepgram', async(t) => {
|
||||||
|
if (!DEEPGRAM_API_KEY ) {
|
||||||
|
t.pass('skipping deepgram tests');
|
||||||
|
return t.end();
|
||||||
|
}
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "gather",
|
||||||
|
"input": ["speech"],
|
||||||
|
"recognizer": {
|
||||||
|
"vendor": "deepgram",
|
||||||
|
"hints": ["customer support", "sales", "human resources", "HR"],
|
||||||
|
"deepgramOptions": {
|
||||||
|
"apiKey": DEEPGRAM_API_KEY
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 10,
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let from = "gather_success";
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
//console.log(JSON.stringify(obj));
|
||||||
|
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().includes('like to speak to customer support'),
|
||||||
|
'gather: succeeds when using deepgram credentials');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'gather\' test - soniox', async(t) => {
|
||||||
|
if (!SONIOX_API_KEY ) {
|
||||||
|
t.pass('skipping soniox tests');
|
||||||
|
return t.end();
|
||||||
|
}
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "gather",
|
||||||
|
"input": ["speech"],
|
||||||
|
"recognizer": {
|
||||||
|
"vendor": "deepgram",
|
||||||
|
"hints": ["customer support", "sales", "human resources", "HR"],
|
||||||
|
"deepgramOptions": {
|
||||||
|
"apiKey": SONIOX_API_KEY
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 10,
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let from = "gather_success";
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
console.log(JSON.stringify(obj));
|
||||||
|
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||||
|
'gather: succeeds when using soniox credentials');
|
||||||
|
|
||||||
disconnect();
|
disconnect();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
53
test/hangup-test.js
Normal file
53
test/hangup-test.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook, provisionCustomHook} = require('./utils')
|
||||||
|
const bent = require('bent');
|
||||||
|
const getJSON = bent('json')
|
||||||
|
|
||||||
|
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('\'hangup\' custom headers', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
url: 'https://example.com/example.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "hangup",
|
||||||
|
"headers": {
|
||||||
|
"X-Reason" : "maximum call duration exceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'hangup_custom_headers';
|
||||||
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
|
t.pass('play: succeeds when using single link');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
72
test/http-proxy-test.js
Normal file
72
test/http-proxy-test.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook, provisionCustomHook} = require('./utils')
|
||||||
|
const bent = require('bent');
|
||||||
|
const getJSON = bent('json')
|
||||||
|
|
||||||
|
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('\'HTTP proxy\' test Info', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
process.env.JAMBONES_HTTP_PROXY_IP = "127.0.0.1";
|
||||||
|
process.env.JAMBONES_HTTP_PROXY_PROTOCOL = "http";
|
||||||
|
process.env.JAMBONES_HTTP_PROXY_PORT = 3128;
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'config',
|
||||||
|
sipRequestWithinDialogHook: '/customHook'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
url: 'silence_stream://5000',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const waitHookVerbs = [
|
||||||
|
{
|
||||||
|
verb: 'hangup'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'http_proxy_info';
|
||||||
|
await provisionCustomHook(from, waitHookVerbs)
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-info-received-bye.xml', '172.38.0.10', from, "16174000015");
|
||||||
|
t.pass('sip Info: success send Info');
|
||||||
|
|
||||||
|
// Make sure that sipRequestWithinDialogHook is called and success
|
||||||
|
const json = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
|
||||||
|
t.pass(json.body.sip_method === 'INFO', 'sipRequestWithinDialogHook contains sip_method')
|
||||||
|
t.pass(json.body.sip_body === 'hello jambonz\r\n', 'sipRequestWithinDialogHook contains sip_method')
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
} finally {
|
||||||
|
process.env.JAMBONES_HTTP_PROXY_IP = null;
|
||||||
|
process.env.JAMBONES_HTTP_PROXY_PROTOCOL = null;
|
||||||
|
process.env.JAMBONES_HTTP_PROXY_PORT = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
65
test/in-dialog-test.js
Normal file
65
test/in-dialog-test.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook, provisionCustomHook} = require('./utils')
|
||||||
|
const bent = require('bent');
|
||||||
|
const getJSON = bent('json')
|
||||||
|
|
||||||
|
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('\'sip Indialog\' test Info', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'config',
|
||||||
|
sipRequestWithinDialogHook: '/customHook'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
url: 'silence_stream://5000',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const waitHookVerbs = [
|
||||||
|
{
|
||||||
|
verb: 'hangup'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'sip_indialog_info';
|
||||||
|
await provisionCustomHook(from, waitHookVerbs)
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-info-received-bye.xml', '172.38.0.10', from);
|
||||||
|
t.pass('sip Info: success send Info');
|
||||||
|
|
||||||
|
// Make sure that sipRequestWithinDialogHook is called and success
|
||||||
|
const json = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
|
||||||
|
t.pass(json.body.sip_method === 'INFO', 'sipRequestWithinDialogHook contains sip_method')
|
||||||
|
t.pass(json.body.sip_body === 'hello jambonz\r\n', 'sipRequestWithinDialogHook contains sip_method')
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,12 +1,23 @@
|
|||||||
|
require('./ws-requestor-unit-test');
|
||||||
require('./unit-tests');
|
require('./unit-tests');
|
||||||
require('./docker_start');
|
require('./docker_start');
|
||||||
require('./create-test-db');
|
require('./create-test-db');
|
||||||
require('./account-validation-tests');
|
require('./account-validation-tests');
|
||||||
|
require('./dial-tests');
|
||||||
require('./webhooks-tests');
|
require('./webhooks-tests');
|
||||||
require('./say-tests');
|
require('./say-tests');
|
||||||
require('./gather-tests');
|
require('./gather-tests');
|
||||||
|
require('./transcribe-tests');
|
||||||
require('./sip-request-tests');
|
require('./sip-request-tests');
|
||||||
require('./create-call-test');
|
require('./create-call-test');
|
||||||
require('./play-tests');
|
require('./play-tests');
|
||||||
|
require('./sip-refer-tests');
|
||||||
|
require('./listen-tests');
|
||||||
|
require('./config-test');
|
||||||
|
require('./queue-test');
|
||||||
|
require('./in-dialog-test');
|
||||||
|
require('./hangup-test');
|
||||||
|
require('./sdp-utils-test');
|
||||||
|
require('./http-proxy-test');
|
||||||
require('./remove-test-db');
|
require('./remove-test-db');
|
||||||
require('./docker_stop');
|
require('./docker_stop');
|
||||||
149
test/listen-tests.js
Normal file
149
test/listen-tests.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const bent = require('bent');
|
||||||
|
const getJSON = bent('json')
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook} = require('./utils')
|
||||||
|
|
||||||
|
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('\'listen-success\'', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const from = "listen_success";
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "listen",
|
||||||
|
"url": `ws://172.38.0.60:3000/${from}`,
|
||||||
|
"mixType" : "mono",
|
||||||
|
"actionHook": "/actionHook",
|
||||||
|
"playBeep": true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
|
||||||
|
t.ok(38000 <= obj.count, 'listen: success incoming call audio');
|
||||||
|
|
||||||
|
obj = await getJSON(`http://127.0.0.1:3100/ws_metadata/${from}`);
|
||||||
|
t.ok(obj.metadata.from === from && obj.metadata.sampleRate === 8000, 'listen: success metadata');
|
||||||
|
|
||||||
|
obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj.body.from === from,
|
||||||
|
'listen: succeeds actionHook');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip('\'listen-maxLength\'', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
let from = "listen_timeout";
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "listen",
|
||||||
|
"url": `ws://172.38.0.60:3000/${from}`,
|
||||||
|
"mixType" : "stereo",
|
||||||
|
"timeout": 2,
|
||||||
|
"maxLength": 2
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/ws_packet_count/${from}`);
|
||||||
|
t.ok(30000 <= obj.count, 'listen: success maxLength incoming call audio');
|
||||||
|
|
||||||
|
obj = await getJSON(`http://127.0.0.1:3100/ws_metadata/${from}`);
|
||||||
|
t.ok(obj.metadata.from === from && obj.metadata.sampleRate === 8000, 'listen: success maxLength metadata');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'listen-pause-resume\'', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
let from = "listen_timeout";
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "listen",
|
||||||
|
"url": `ws://172.38.0.60:3000/${from}`,
|
||||||
|
"mixType" : "mixed"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const p = sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
|
||||||
|
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}`);
|
||||||
|
const callSid = obj.body.call_sid;
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
// Pause listen
|
||||||
|
let post = bent('http://127.0.0.1:3000/', 'POST', 202);
|
||||||
|
await post(`v1/updateCall/${callSid}`, {
|
||||||
|
"listen_status": "pause"
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
|
||||||
|
// Resume listen
|
||||||
|
post = bent('http://127.0.0.1:3000/', 'POST', 202);
|
||||||
|
await post(`v1/updateCall/${callSid}`, {
|
||||||
|
"listen_status": "resume"
|
||||||
|
});
|
||||||
|
|
||||||
|
// turn off the call
|
||||||
|
post = bent('http://127.0.0.1:3000/', 'POST', 202);
|
||||||
|
await post(`v1/updateCall/${callSid}`, {
|
||||||
|
"call_status": "completed"
|
||||||
|
});
|
||||||
|
|
||||||
|
await p;
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -33,7 +33,7 @@ test('\'play\' tests single link in plain text', async(t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const from = 'play_single_link';
|
const from = 'play_single_link';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
@@ -62,7 +62,7 @@ test('\'play\' tests multi links in array', async(t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const from = 'play_multi_links_in_array';
|
const from = 'play_multi_links_in_array';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
@@ -100,8 +100,8 @@ test('\'play\' tests single link in conference', async(t) => {
|
|||||||
waitHook: `/customHook`
|
waitHook: `/customHook`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
provisionCustomHook(from, waitHookVerbs)
|
await provisionCustomHook(from, waitHookVerbs)
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
||||||
@@ -141,8 +141,8 @@ test('\'play\' tests multi links in array in conference', async(t) => {
|
|||||||
waitHook: `/customHook`
|
waitHook: `/customHook`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
provisionCustomHook(from, waitHookVerbs)
|
await provisionCustomHook(from, waitHookVerbs)
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
||||||
@@ -178,17 +178,73 @@ test('\'play\' tests with seekOffset and actionHook', async(t) => {
|
|||||||
const waitHookVerbs = [];
|
const waitHookVerbs = [];
|
||||||
|
|
||||||
const from = 'play_action_hook';
|
const from = 'play_action_hook';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
provisionCustomHook(from, waitHookVerbs)
|
await provisionCustomHook(from, waitHookVerbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
t.pass('play: succeeds');
|
t.pass('play: succeeds');
|
||||||
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`);
|
||||||
t.ok(obj.body.reason === "playCompleted", "play: actionHook success received")
|
const seconds = parseInt(obj.body.playback_seconds);
|
||||||
t.ok(obj.body.playback_seconds === "2", "playback_seconds: actionHook success received")
|
const milliseconds = parseInt(obj.body.playback_milliseconds);
|
||||||
t.ok(obj.body.playback_milliseconds === "2048", "playback_milliseconds: actionHook success received")
|
const lastOffsetPos = parseInt(obj.body.playback_last_offset_pos);
|
||||||
t.ok(obj.body.playback_last_offset_pos === "16000", "playback_last_offset_pos: actionHook success received")
|
console.log({obj}, 'lastRequest');
|
||||||
|
t.ok(obj.body.reason === "playCompleted", "play: actionHook success received");
|
||||||
|
t.ok(seconds === 2, "playback_seconds: actionHook success received");
|
||||||
|
t.ok(milliseconds === 2048, "playback_milliseconds: actionHook success received");
|
||||||
|
t.ok(lastOffsetPos > 15500 && lastOffsetPos < 16500, "playback_last_offset_pos: actionHook success received")
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'play\' tests with earlymedia', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
url: 'silence_stream://5000',
|
||||||
|
earlyMedia: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'play_early_media';
|
||||||
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-invite-expect-183-cancel.xml', '172.38.0.10', from);
|
||||||
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_callStatus`);
|
||||||
|
t.ok(obj.body.sip_status === 487, "play: actionHook success received");
|
||||||
|
t.ok(obj.body.sip_reason === 'Request Terminated', "play: actionHook success received");
|
||||||
|
t.ok(obj.body.call_termination_by === 'caller', "play: actionHook success received");
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'play\' tests with initial app_json', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
const from = 'play_initial_app_json';
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from, "16174000007");
|
||||||
|
t.pass('application can use app_json for initial instructions');
|
||||||
disconnect();
|
disconnect();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`error received: ${err}`);
|
console.log(`error received: ${err}`);
|
||||||
|
|||||||
127
test/queue-test.js
Normal file
127
test/queue-test.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook, provisionActionHook, provisionAnyHook} = require('./utils');
|
||||||
|
const bent = require('bent');
|
||||||
|
const getJSON = bent('json');
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
||||||
|
|
||||||
|
test('\'enqueue-dequeue\' tests', async(t) => {
|
||||||
|
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'enqueue',
|
||||||
|
name: 'support',
|
||||||
|
actionHook: '/actionHook'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const verbs2 = [
|
||||||
|
{
|
||||||
|
verb: 'dequeue',
|
||||||
|
name: 'support'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const actionVerbs = [
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
url: 'silence_stream://1000',
|
||||||
|
earlyMedia: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'enqueue_success';
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
await provisionActionHook(from, actionVerbs)
|
||||||
|
|
||||||
|
const from2 = 'dequeue_success';
|
||||||
|
await provisionCallHook(from2, verbs2);
|
||||||
|
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const p1 = sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
|
|
||||||
|
await sleepFor(1000);
|
||||||
|
|
||||||
|
const p2 = sippUac('uac-success-send-bye.xml', '172.38.0.11', from2);
|
||||||
|
await Promise.all([p1, p2]);
|
||||||
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj.body.queue_result === 'bridged');
|
||||||
|
t.pass('enqueue-dequeue: succeeds connect');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\leave\' tests', async(t) => {
|
||||||
|
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'enqueue',
|
||||||
|
name: 'support1',
|
||||||
|
waitHook: '/anyHook/enqueue_success_leave',
|
||||||
|
actionHook: '/actionHook'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const anyHookVerbs = [
|
||||||
|
{
|
||||||
|
verb: 'leave'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const actionVerbs = [
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
url: 'silence_stream://1000',
|
||||||
|
earlyMedia: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'enqueue_success_leave';
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
await provisionAnyHook(from, anyHookVerbs);
|
||||||
|
await provisionActionHook(from, actionVerbs)
|
||||||
|
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/enqueue_success_leave`);
|
||||||
|
t.ok(obj.body.queue_position === 0);
|
||||||
|
const obj1 = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj1.body.queue_result === 'leave');
|
||||||
|
t.pass('enqueue-dequeue: succeeds connect');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -5,7 +5,6 @@ test('dropping jambones_test database', (t) => {
|
|||||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__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);
|
if (err) return t.end(err);
|
||||||
t.pass('database successfully dropped');
|
t.pass('database successfully dropped');
|
||||||
fs.unlinkSync(`${__dirname}/credentials/gcp.json`);
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ test('\'say\' tests', async(t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const from = 'say_test_success';
|
const from = 'say_test_success';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
@@ -43,3 +43,124 @@ test('\'say\' tests', async(t) => {
|
|||||||
t.error(err);
|
t.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('\'config\' reset synthesizer tests', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"synthesizer": {
|
||||||
|
"vendor": "microsft",
|
||||||
|
"voice": "foobar"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"reset": 'synthesizer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verb: 'say',
|
||||||
|
text: 'hello'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'say_test_success';
|
||||||
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
|
t.pass('say: succeeds when using using account credentials');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Say verb array test', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"synthesizer": {
|
||||||
|
"vendor": "microsft",
|
||||||
|
"voice": "foobar"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"reset": 'synthesizer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verb: 'say',
|
||||||
|
text: ['hello', 'https://samplelib.com/lib/preview/mp3/sample-3s.mp3']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'say_test_success';
|
||||||
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
|
t.pass('say: succeeds when using using account credentials');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {MICROSOFT_CUSTOM_API_KEY, MICROSOFT_DEPLOYMENT_ID, MICROSOFT_CUSTOM_REGION, MICROSOFT_CUSTOM_VOICE} = process.env;
|
||||||
|
if (MICROSOFT_CUSTOM_API_KEY && MICROSOFT_DEPLOYMENT_ID && MICROSOFT_CUSTOM_REGION && MICROSOFT_CUSTOM_VOICE) {
|
||||||
|
test('\'say\' tests - microsoft custom voice', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'say',
|
||||||
|
text: 'hello',
|
||||||
|
synthesizer: {
|
||||||
|
vendor: 'microsoft',
|
||||||
|
voice: MICROSOFT_CUSTOM_VOICE,
|
||||||
|
options: {
|
||||||
|
deploymentId: MICROSOFT_DEPLOYMENT_ID,
|
||||||
|
apiKey: MICROSOFT_CUSTOM_API_KEY,
|
||||||
|
region: MICROSOFT_CUSTOM_REGION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'say_test_success';
|
||||||
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
|
t.pass('say: succeeds when using microsoft custom voice');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
99
test/scenarios/uac-gather-account-creds-success-send-bye.xml
Normal file
99
test/scenarios/uac-gather-account-creds-success-send-bye.xml
Normal 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:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: <sip:[to]@[remote_ip]:[remote_port]>
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 INVITE
|
||||||
|
Contact: sip:[from]@[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:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 ACK
|
||||||
|
Contact: sip:[from]@[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>
|
||||||
|
|
||||||
|
<!-- Pause briefly -->
|
||||||
|
<pause milliseconds="3000"/>
|
||||||
|
|
||||||
|
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
|
||||||
|
<send retrans="500">
|
||||||
|
<![CDATA[
|
||||||
|
|
||||||
|
BYE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 3 BYE
|
||||||
|
Contact: sip:[from]@[local_ip]:[local_port]
|
||||||
|
Max-Forwards: 70
|
||||||
|
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
|
||||||
|
<recv response="200" crlf="true">
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
</scenario>
|
||||||
|
|
||||||
86
test/scenarios/uac-invite-expect-183-cancel.xml
Normal file
86
test/scenarios/uac-invite-expect-183-cancel.xml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?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:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: <sip:[to]@[remote_ip]:[remote_port]>
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 INVITE
|
||||||
|
Contact: sip:[from]@[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" rtd="true">
|
||||||
|
<action>
|
||||||
|
<ereg regexp=";branch=[^;]*" search_in="hdr" header="Via" check_it="false" assign_to="1"/>
|
||||||
|
</action>
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<send>
|
||||||
|
<![CDATA[
|
||||||
|
CANCEL sip:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port][$1]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: [to] <sip:[to]@[remote_ip]:[remote_port]>
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: [cseq] CANCEL
|
||||||
|
Contact: sip:[from]@[local_ip]:[local_port]
|
||||||
|
Max-Forwards: 70
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
|
||||||
|
<recv response="200" rtd="true">
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<recv response="487" 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:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port][$1]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 ACK
|
||||||
|
Contact: sip:[from]@[local_ip]:[local_port]
|
||||||
|
Max-Forwards: 70
|
||||||
|
Subject: uac-say
|
||||||
|
Content-Length: 0
|
||||||
|
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
|
||||||
|
</scenario>
|
||||||
95
test/scenarios/uac-refer-no-notify.xml
Normal file
95
test/scenarios/uac-refer-no-notify.xml
Normal 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:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: <sip:[to]@[remote_ip]:[remote_port]>
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 INVITE
|
||||||
|
Contact: sip:[from]@[local_ip]:[local_port]
|
||||||
|
Max-Forwards: 70
|
||||||
|
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
|
||||||
|
Subject: uac-refer-no-notify.xml
|
||||||
|
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:[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]>[peer_tag_param]
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 ACK
|
||||||
|
Max-Forwards: 70
|
||||||
|
Subject: REFER test with no NOT
|
||||||
|
Content-Length: 0
|
||||||
|
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
|
||||||
|
<!-- receive re-invite -->
|
||||||
|
<recv request="REFER" crlf="true"/>
|
||||||
|
|
||||||
|
<send>
|
||||||
|
<![CDATA[
|
||||||
|
|
||||||
|
SIP/2.0 202 Accepted
|
||||||
|
[last_Via:]
|
||||||
|
[last_From:]
|
||||||
|
[last_To:]
|
||||||
|
[last_Call-ID:]
|
||||||
|
[last_CSeq:]
|
||||||
|
Contact: sip:sipp@[local_ip]:[local_port]
|
||||||
|
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>
|
||||||
|
|
||||||
|
</scenario>
|
||||||
115
test/scenarios/uac-refer-with-notify.xml
Normal file
115
test/scenarios/uac-refer-with-notify.xml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?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:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[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:[from]@[local_ip]:[local_port]
|
||||||
|
Max-Forwards: 70
|
||||||
|
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
|
||||||
|
Subject: uac-refer-with-notify.xml
|
||||||
|
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:[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:16174000000@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 ACK
|
||||||
|
Max-Forwards: 70
|
||||||
|
Subject: uac-refer-with-notify.xml
|
||||||
|
Content-Length: 0
|
||||||
|
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
|
||||||
|
<!-- receive re-invite -->
|
||||||
|
<recv request="REFER" crlf="true"/>
|
||||||
|
|
||||||
|
<send>
|
||||||
|
<![CDATA[
|
||||||
|
|
||||||
|
SIP/2.0 202 Accepted
|
||||||
|
[last_Via:]
|
||||||
|
[last_From:]
|
||||||
|
[last_To:]
|
||||||
|
[last_Call-ID:]
|
||||||
|
[last_CSeq:]
|
||||||
|
Contact: sip:sipp@[local_ip]:[local_port]
|
||||||
|
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>
|
||||||
|
|
||||||
|
<send retrans="500">
|
||||||
|
<![CDATA[
|
||||||
|
|
||||||
|
NOTIFY 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:16174000000@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 2 NOTIFY
|
||||||
|
Contact: sip:sipp@[local_ip]:[local_port]
|
||||||
|
Max-Forwards: 70
|
||||||
|
Subject: uac-refer-with-notify.xml
|
||||||
|
Content-Type: message/sipfrag;version=2.0
|
||||||
|
Content-Length: 16
|
||||||
|
|
||||||
|
SIP/2.0 200 OK
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
<recv response="200"</recv>
|
||||||
|
|
||||||
|
</scenario>
|
||||||
114
test/scenarios/uac-success-info-received-bye.xml
Normal file
114
test/scenarios/uac-success-info-received-bye.xml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?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:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: <sip:[to]@[remote_ip]:[remote_port]>
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 INVITE
|
||||||
|
Contact: sip:[from]@[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:[to]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 1 ACK
|
||||||
|
Contact: sip:[from]@[local_ip]:[local_port]
|
||||||
|
Max-Forwards: 70
|
||||||
|
Subject: uac-say
|
||||||
|
Content-Length: 0
|
||||||
|
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
|
||||||
|
<pause milliseconds="2000"/>
|
||||||
|
|
||||||
|
<!-- Send an INFO message -->
|
||||||
|
<send>
|
||||||
|
<![CDATA[
|
||||||
|
INFO sip:[service]@[remote_ip]:[remote_port] SIP/2.0
|
||||||
|
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||||
|
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
|
||||||
|
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
|
||||||
|
Call-ID: [call_id]
|
||||||
|
CSeq: 2 INFO
|
||||||
|
Contact: sip:[from]@[local_ip]:[local_port]
|
||||||
|
Max-Forwards: 70
|
||||||
|
Subject: Performance Test
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Length: [len]
|
||||||
|
|
||||||
|
hello jambonz
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
|
||||||
|
<!-- Receive 200 OK -->
|
||||||
|
<recv response="200">
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
164
test/scenarios/uas-dial.xml
Normal file
164
test/scenarios/uas-dial.xml
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||||
|
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||||
|
|
||||||
|
<!-- This program is free software; you can redistribute it and/or -->
|
||||||
|
<!-- modify it under the terms of the GNU General Public License as -->
|
||||||
|
<!-- published by the Free Software Foundation; either version 2 of the -->
|
||||||
|
<!-- License, or (at your option) any later version. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- This program is distributed in the hope that it will be useful, -->
|
||||||
|
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
|
||||||
|
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
|
||||||
|
<!-- GNU General Public License for more details. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- You should have received a copy of the GNU General Public License -->
|
||||||
|
<!-- along with this program; if not, write to the -->
|
||||||
|
<!-- Free Software Foundation, Inc., -->
|
||||||
|
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- Sipp default 'uas' scenario. -->
|
||||||
|
<!-- -->
|
||||||
|
|
||||||
|
<scenario name="Basic UAS responder">
|
||||||
|
<!-- 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 request="INVITE" crlf="true">
|
||||||
|
<action>
|
||||||
|
<ereg regexp=".*" search_in="hdr" header="Subject:" assign_to="1" />
|
||||||
|
</action>
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<!-- The '[last_*]' keyword is replaced automatically by the -->
|
||||||
|
<!-- specified header if it was present in the last message received -->
|
||||||
|
<!-- (except if it was a retransmission). If the header was not -->
|
||||||
|
<!-- present or if no message has been received, the '[last_*]' -->
|
||||||
|
<!-- keyword is discarded, and all bytes until the end of the line -->
|
||||||
|
<!-- are also discarded. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- If the specified header was present several times in the -->
|
||||||
|
<!-- message, all occurrences are concatenated (CRLF separated) -->
|
||||||
|
<!-- to be used in place of the '[last_*]' keyword. -->
|
||||||
|
|
||||||
|
<send>
|
||||||
|
<![CDATA[
|
||||||
|
|
||||||
|
SIP/2.0 180 Ringing
|
||||||
|
[last_Via:]
|
||||||
|
[last_From:]
|
||||||
|
[last_To:];tag=[pid]SIPpTag01[call_number]
|
||||||
|
[last_Call-ID:]
|
||||||
|
[last_CSeq:]
|
||||||
|
[last_Record-Route:]
|
||||||
|
Subject:[$1]
|
||||||
|
Content-Length: 0
|
||||||
|
|
||||||
|
]]>
|
||||||
|
</send>
|
||||||
|
|
||||||
|
<send>
|
||||||
|
<![CDATA[
|
||||||
|
|
||||||
|
SIP/2.0 200 OK
|
||||||
|
[last_Via:]
|
||||||
|
[last_From:]
|
||||||
|
[last_To:];tag=[pid]SIPpTag01[call_number]
|
||||||
|
[last_Call-ID:]
|
||||||
|
[last_CSeq:]
|
||||||
|
[last_Record-Route:]
|
||||||
|
Subject:[$1]
|
||||||
|
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
|
||||||
|
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 request="ACK"
|
||||||
|
rtd="true"
|
||||||
|
crlf="true">
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<recv request="INFO" optional="true" next="1">
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<recv request="INVITE" crlf="true">
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<send>
|
||||||
|
<![CDATA[
|
||||||
|
|
||||||
|
SIP/2.0 200 OK
|
||||||
|
[last_Via:]
|
||||||
|
[last_From:]
|
||||||
|
[last_To:];tag=[pid]SIPpTag01[call_number]
|
||||||
|
[last_Call-ID:]
|
||||||
|
[last_CSeq:]
|
||||||
|
[last_Record-Route:]
|
||||||
|
Subject:[$1]
|
||||||
|
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
|
||||||
|
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 request="ACK"
|
||||||
|
rtd="true"
|
||||||
|
crlf="true">
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<recv request="BYE">
|
||||||
|
</recv>
|
||||||
|
|
||||||
|
<send next="2">
|
||||||
|
<![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>
|
||||||
|
|
||||||
|
<label id="1"/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<label id="2"/>
|
||||||
|
|
||||||
|
</scenario>
|
||||||
|
|
||||||
26
test/sdp-utils-test.js
Normal file
26
test/sdp-utils-test.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const {makeOpusFirst, isOpusFirst} = require('../lib/utils/sdp-utils');
|
||||||
|
const sdpTransform = require('sdp-transform');
|
||||||
|
|
||||||
|
test('test opus first', (t) => {
|
||||||
|
const sdp = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:111 opus/48000/2\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
|
||||||
|
const opusSdp = makeOpusFirst(sdp);
|
||||||
|
const parsedSdp = sdpTransform.parse(opusSdp);
|
||||||
|
const opusIndex = parsedSdp.media[0].rtp.findIndex((entry) => entry.codec === 'opus');
|
||||||
|
t.ok(opusIndex === 0, 'succesffuly move opus to be first offer')
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('test is opus first', (t) => {
|
||||||
|
|
||||||
|
const sdp = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
|
||||||
|
t.ok(isOpusFirst(sdp), "opus is first offer");
|
||||||
|
|
||||||
|
const sdp2 = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:111 opus/48000/2\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
|
||||||
|
t.ok(!isOpusFirst(sdp2), "opus is not first offer")
|
||||||
|
|
||||||
|
const sdp3 = 'v=0\r\no=xhoaluu2 1314 1504 IN IP4 192.168.1.4\r\ns=Talk\r\nc=IN IP4 192.168.1.4\r\nt=0 0\r\na=ice-pwd:397d063ea23fdc05164e3ee4\r\na=ice-ufrag:16c449a3\r\na=rtcp-xr:rcvr-rtt=all:10000 stat-summary=loss,dup,jitt,TTL voip-metrics\r\na=group:BUNDLE as\r\na=record:off\r\nm=audio 56542 RTP/AVPF 0 8\r\nc=IN IP4 14.226.233.151\r\na=rtcp-mux\r\na=mid:as\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=rtcp:63076 IN IP4 192.168.1.4\r\na=candidate:1 1 UDP 2130706303 192.168.1.4 56542 typ host\r\na=candidate:1 2 UDP 2130706302 192.168.1.4 63076 typ host\r\na=candidate:2 1 UDP 2130706431 2001:ee0:d744:dcf0:c1d3:d73f:7a93:dc9f 56542 typ host\r\na=candidate:2 2 UDP 2130706430 2001:ee0:d744:dcf0:c1d3:d73f:7a93:dc9f 63076 typ host\r\na=candidate:3 1 UDP 2130706431 2001:ee0:d744:dcf0:15:6be3:8e6b:b736 56542 typ host\r\na=candidate:3 2 UDP 2130706430 2001:ee0:d744:dcf0:15:6be3:8e6b:b736 63076 typ host\r\na=candidate:4 1 UDP 1694498687 14.226.233.151 56542 typ srflx raddr 192.168.1.4 rport 56542\r\na=rtcp-fb:* trr-int 1000\r\na=rtcp-fb:* ccm tmmbr';
|
||||||
|
t.ok(!isOpusFirst(sdp2), "opus is not first offer")
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
100
test/sip-refer-tests.js
Normal file
100
test/sip-refer-tests.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook, provisionCustomHook, provisionActionHook} = require('./utils')
|
||||||
|
const bent = require('bent');
|
||||||
|
const getJSON = bent('json')
|
||||||
|
|
||||||
|
const sleepFor = async(ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
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('\'refer\' tests w/202 and NOTIFY', {timeout: 25000}, async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'say',
|
||||||
|
text: 'silence_stream://100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verb: 'sip:refer',
|
||||||
|
referTo: '123456',
|
||||||
|
actionHook: '/actionHook'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const noVerbs = [];
|
||||||
|
|
||||||
|
const from = 'refer_with_notify';
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
await provisionActionHook(from, noVerbs)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-refer-with-notify.xml', '172.38.0.10', from);
|
||||||
|
t.pass('refer: successfully received 202 Accepted');
|
||||||
|
await sleepFor(1000);
|
||||||
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj.body.final_referred_call_status === 200, 'refer: successfully received NOTIFY with 200 OK');
|
||||||
|
//console.log(`obj: ${JSON.stringify(obj)}`);
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\'refer\' tests w/202 but no NOTIFY', {timeout: 25000}, async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'say',
|
||||||
|
text: 'silence_stream://100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verb: 'sip:refer',
|
||||||
|
referTo: '123456',
|
||||||
|
actionHook: '/actionHook'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const noVerbs = [];
|
||||||
|
|
||||||
|
const from = 'refer_no_notify';
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
await provisionActionHook(from, noVerbs)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-refer-no-notify.xml', '172.38.0.10', from);
|
||||||
|
t.pass('refer: successfully received 202 Accepted w/o NOTIFY');
|
||||||
|
await sleepFor(17000);
|
||||||
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
console.log(`obj: ${JSON.stringify(obj)}`);
|
||||||
|
t.ok(obj.body.refer_status === 202, 'refer: successfully timed out and reported 202');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user