mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7c56d8ea0 | ||
|
|
9cfe990bb8 | ||
|
|
6c7d2c9074 | ||
|
|
73e35c84c5 | ||
|
|
86d50d94cb | ||
|
|
b8f4ad6b27 | ||
|
|
ad3ec926ee | ||
|
|
66bd9a442c | ||
|
|
fa81d179a1 | ||
|
|
fab8a391b7 | ||
|
|
89288acf6e | ||
|
|
23cd4408a5 | ||
|
|
ce4618523c | ||
|
|
0eb8097e32 | ||
|
|
8851b3fac0 | ||
|
|
e080118b6a | ||
|
|
75c27e3f80 | ||
|
|
843980c7f6 | ||
|
|
f9990da468 | ||
|
|
e8d5655abb | ||
|
|
e908f5830c | ||
|
|
5c7bac91a8 | ||
|
|
de250c8d58 | ||
|
|
84d83a0a48 | ||
|
|
b5bede7a08 | ||
|
|
6e779f6744 | ||
|
|
77b9ca4cba | ||
|
|
0451b6982c | ||
|
|
71adc577e9 | ||
|
|
e8b32103fe | ||
|
|
57d8d0a02c | ||
|
|
a41760fa9f | ||
|
|
c6bae80a03 | ||
|
|
4cddbd83a1 | ||
|
|
6275aac341 | ||
|
|
52de41c9bc | ||
|
|
ed71abd675 | ||
|
|
2d2b98dab5 | ||
|
|
7553e2b617 | ||
|
|
b921cab867 | ||
|
|
48e1a72ef3 | ||
|
|
4337a55a27 | ||
|
|
6041b1d595 | ||
|
|
d33d0aa519 | ||
|
|
ffe9cb23eb | ||
|
|
dbbc894832 | ||
|
|
82c16380f5 | ||
|
|
c0fab2880b | ||
|
|
ce2fa392a4 | ||
|
|
3b47162d13 | ||
|
|
b765232d4f | ||
|
|
2436bea6ea | ||
|
|
f67abddbd4 | ||
|
|
39fcb17dec | ||
|
|
80418aa7e5 | ||
|
|
b21d10eb3e | ||
|
|
7875eb51b9 | ||
|
|
e2c1383723 | ||
|
|
40de2c5945 | ||
|
|
3a299bc3ca | ||
|
|
70c9407742 | ||
|
|
dba66d58fc | ||
|
|
0ff3d22faf | ||
|
|
187a428a75 | ||
|
|
a4792a521f | ||
|
|
3ac9693735 | ||
|
|
3ad54a0e72 | ||
|
|
bd8fb2f9db | ||
|
|
32b317ae68 | ||
|
|
40e8d08727 | ||
|
|
256ca440a0 | ||
|
|
68d73345ef | ||
|
|
54dd72ff66 | ||
|
|
832a4e8032 | ||
|
|
33c3b99e2e | ||
|
|
8b2a2e196e | ||
|
|
556717a9a4 | ||
|
|
f2c635268f | ||
|
|
c8999a5929 | ||
|
|
7e046ac7f3 | ||
|
|
997ff05f3c | ||
|
|
55d8fdef1c | ||
|
|
7d355f2fac | ||
|
|
c6b8ec1b28 | ||
|
|
10159a0ba6 | ||
|
|
7a558c7349 | ||
|
|
4dbe7af9db | ||
|
|
4ec34faa29 | ||
|
|
e2d6086f9f | ||
|
|
0e056ad296 | ||
|
|
1d69457ddc | ||
|
|
dcfe6cc05d | ||
|
|
a474c2d4cc | ||
|
|
0f244cf6d5 | ||
|
|
92d9468570 | ||
|
|
1b143f6aae | ||
|
|
43344ae14b | ||
|
|
e6168d0a3c | ||
|
|
f725d5f0a1 | ||
|
|
1e9f388f51 | ||
|
|
72d2877ddf | ||
|
|
f15c339a2a | ||
|
|
30ba84d57b | ||
|
|
a4e767e1e4 | ||
|
|
7b805130bb | ||
|
|
a1c302f85c | ||
|
|
bf9ae3b5ce | ||
|
|
4c9af253a3 | ||
|
|
936a9244ba | ||
|
|
0522ae408c | ||
|
|
9c788cdedc | ||
|
|
cbc5e2d6f7 | ||
|
|
f4d6fd14b8 | ||
|
|
b190334839 | ||
|
|
209a58ff51 | ||
|
|
f8720bab9f | ||
|
|
77363d54d1 | ||
|
|
ad483ba0b7 | ||
|
|
02c9a951d4 | ||
|
|
d5f5e3a86f | ||
|
|
62cea3a9e9 | ||
|
|
6d3bfd527e | ||
|
|
9002bacf8f | ||
|
|
92473454d6 | ||
|
|
1c2280af88 | ||
|
|
7d16bdd774 | ||
|
|
79e1bc8d12 | ||
|
|
9d24ef6238 | ||
|
|
042ad9f629 | ||
|
|
7351f0ad68 | ||
|
|
de7b74f898 | ||
|
|
d361f1aeb1 | ||
|
|
f3d002cfca | ||
|
|
3121c2a197 | ||
|
|
b7bdf300c6 | ||
|
|
c96159268e | ||
|
|
8e200251ca | ||
|
|
898f3aec4a | ||
|
|
6f85752352 | ||
|
|
fe7cc9ad58 | ||
|
|
1ffdfebdb2 | ||
|
|
dcf1895920 | ||
|
|
c509b9d277 | ||
|
|
eff8474997 | ||
|
|
b4237beeeb | ||
|
|
0406e42c19 | ||
|
|
533cd2f47d | ||
|
|
742884cc72 | ||
|
|
9fccfa2a73 | ||
|
|
3356b7302a | ||
|
|
9f533ed17c | ||
|
|
a0797a3a4c | ||
|
|
0b33ef0c2c | ||
|
|
71ecf453f8 | ||
|
|
494f1cf784 | ||
|
|
da74e2526a | ||
|
|
e35a03c7ad | ||
|
|
46fb9b8875 | ||
|
|
f9df2b3028 | ||
|
|
32ff023b14 | ||
|
|
f3d3afee73 | ||
|
|
3c8cbd97c5 | ||
|
|
eba9c98412 | ||
|
|
c2065ef787 | ||
|
|
307787526d | ||
|
|
3141646dfd | ||
|
|
cac6e2117d | ||
|
|
6d34d6f886 | ||
|
|
964afc1660 | ||
|
|
d09dca47b9 | ||
|
|
f3ec847474 | ||
|
|
cf7ce675f5 | ||
|
|
34895daf4f | ||
|
|
b06032b5f0 | ||
|
|
3486ff958c | ||
|
|
f79f96b884 | ||
|
|
2aa3d40268 | ||
|
|
148fc49f06 | ||
|
|
02806a109c | ||
|
|
077c791e37 | ||
|
|
4b70c6458a | ||
|
|
aadb0b15f2 | ||
|
|
3997f57365 | ||
|
|
c97874ed1f | ||
|
|
1dcc92a177 | ||
|
|
105aa16ffe | ||
|
|
a574045f8a | ||
|
|
af3d03bef9 | ||
|
|
5b1b50c3a3 | ||
|
|
ba431aeb35 | ||
|
|
36607b505f | ||
|
|
616a0b364d | ||
|
|
1b764b31e6 | ||
|
|
009396becc | ||
|
|
84305e30cc | ||
|
|
9c7f8b4e7b | ||
|
|
b2dce18c7a | ||
|
|
8f93b69af0 | ||
|
|
127b690ae2 | ||
|
|
3ad19eca3c | ||
|
|
efe7e22109 | ||
|
|
7a67ed704c |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -7,11 +7,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
- run: npm install
|
||||
- run: npm run jslint
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- run: npm test
|
||||
- run: npm run test:encrypt-decrypt
|
||||
|
||||
|
||||
|
||||
60
.github/workflows/docker-publish-dbcreate.yml
vendored
60
.github/workflows/docker-publish-dbcreate.yml
vendored
@@ -2,16 +2,10 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: db-create
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
push:
|
||||
@@ -20,32 +14,42 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile.db-create --tag $IMAGE_NAME
|
||||
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
IMAGE_ID=jambonz/db-create
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
- name: Login to Docker Hub
|
||||
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: .
|
||||
file: ./Dockerfile.db-create
|
||||
push: true
|
||||
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
|
||||
build-args: |
|
||||
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
|
||||
GITHUB_REF=$GITHUB_REF
|
||||
|
||||
59
.github/workflows/docker-publish.yml
vendored
59
.github/workflows/docker-publish.yml
vendored
@@ -2,16 +2,10 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: api-server
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
push:
|
||||
@@ -20,32 +14,41 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
||||
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
IMAGE_ID=jambonz/api-server
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
- name: Login to Docker Hub
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:18.14.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
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Drachtio Communications Services, LLC
|
||||
Copyright (c) 2018-2024 FirstFive8, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -33,6 +33,8 @@ Configuration is provided via environment variables:
|
||||
|K8S| service running as kubernetes service |no|
|
||||
|K8S_FEATURE_SERVER_SERVICE_NAME| feature server name(required for K8S) |no|
|
||||
|K8S_FEATURE_SERVER_SERVICE_PORT| feature server port(required for K8S) |no|
|
||||
|JAMBONZ_RECORD_WS_USERNAME| recording websocket username|no|
|
||||
|JAMBONZ_RECORD_WS_PASSWORD| recording websocket password|no|
|
||||
|
||||
#### Database dependency
|
||||
A mysql database is used to store long-lived objects such as Accounts, Applications, etc. To create the database schema, use or review the scripts in the 'db' folder, particularly:
|
||||
|
||||
95
app.js
95
app.js
@@ -3,16 +3,23 @@ const logger = require('./lib/logger');
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const helmet = require('helmet');
|
||||
const nocache = require('nocache');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const cors = require('cors');
|
||||
const passport = require('passport');
|
||||
const routes = require('./lib/routes');
|
||||
const Registrar = require('@jambonz/mw-registrar');
|
||||
|
||||
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.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST 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_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
|
||||
assert.ok(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET, 'missing ENCRYPTION_SECRET env var');
|
||||
assert.ok(process.env.JWT_SECRET, 'missing JWT_SECRET env var');
|
||||
@@ -29,21 +36,27 @@ const {
|
||||
logger, process.env.JAMBONES_TIME_SERIES_HOST
|
||||
);
|
||||
const {
|
||||
client,
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey,
|
||||
listConferences
|
||||
} = require('./lib/helpers/realtimedb-helpers');
|
||||
const {
|
||||
getTtsVoices
|
||||
} = require('@jambonz/speech-utils')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
getTtsVoices,
|
||||
getTtsSize,
|
||||
purgeTtsCache,
|
||||
getAwsAuthToken,
|
||||
getVerbioAccessToken,
|
||||
synthAudio
|
||||
} = require('@jambonz/speech-utils')({}, logger);
|
||||
const {
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid,
|
||||
@@ -51,7 +64,8 @@ const {
|
||||
lookupAppByPhoneNumber,
|
||||
lookupCarrierBySid,
|
||||
lookupSipGatewayBySid,
|
||||
lookupSmppGatewayBySid
|
||||
lookupSmppGatewayBySid,
|
||||
lookupClientByAccountAndUsername
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
@@ -62,22 +76,33 @@ const {
|
||||
}, logger);
|
||||
const PORT = process.env.HTTP_PORT || 3000;
|
||||
const authStrategy = require('./lib/auth')(logger, retrieveKey);
|
||||
const {delayLoginMiddleware} = require('./lib/middleware');
|
||||
const Websocket = require('ws');
|
||||
|
||||
passport.use(authStrategy);
|
||||
|
||||
app.locals = app.locals || {};
|
||||
app.locals = {
|
||||
...app.locals,
|
||||
registrar: new Registrar(logger, client),
|
||||
logger,
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
listConferences,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
incrKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
getTtsVoices,
|
||||
getTtsSize,
|
||||
getAwsAuthToken,
|
||||
getVerbioAccessToken,
|
||||
purgeTtsCache,
|
||||
synthAudio,
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid,
|
||||
lookupAccountByPhoneNumber,
|
||||
@@ -85,6 +110,7 @@ app.locals = {
|
||||
lookupCarrierBySid,
|
||||
lookupSipGatewayBySid,
|
||||
lookupSmppGatewayBySid,
|
||||
lookupClientByAccountAndUsername,
|
||||
queryCdrs,
|
||||
queryCdrsSP,
|
||||
queryAlerts,
|
||||
@@ -108,6 +134,12 @@ const limiter = rateLimit({
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
});
|
||||
|
||||
// Setup websocket for recording audio
|
||||
const recordWsServer = require('./lib/record');
|
||||
const wsServer = new Websocket.Server({ noServer: true });
|
||||
wsServer.setMaxListeners(0);
|
||||
wsServer.on('connection', recordWsServer.bind(null, logger));
|
||||
|
||||
if (process.env.JAMBONES_TRUST_PROXY) {
|
||||
const proxyCount = parseInt(process.env.JAMBONES_TRUST_PROXY);
|
||||
if (!isNaN(proxyCount) && proxyCount > 0) {
|
||||
@@ -122,9 +154,11 @@ if (process.env.JAMBONES_TRUST_PROXY) {
|
||||
app.use(limiter);
|
||||
app.use(helmet());
|
||||
app.use(helmet.hidePoweredBy());
|
||||
app.use(nocache());
|
||||
app.use(passport.initialize());
|
||||
app.use(cors());
|
||||
app.use(express.urlencoded({extended: true}));
|
||||
app.use(delayLoginMiddleware);
|
||||
app.use(unless(['/stripe'], express.json()));
|
||||
app.use('/v1', unless(
|
||||
[
|
||||
@@ -146,7 +180,52 @@ app.use((err, req, res, next) => {
|
||||
});
|
||||
});
|
||||
logger.info(`listening for HTTP traffic on port ${PORT}`);
|
||||
app.listen(PORT);
|
||||
const server = app.listen(PORT);
|
||||
|
||||
|
||||
const isValidWsKey = (hdr) => {
|
||||
const username = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME;
|
||||
const password = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD;
|
||||
if (username && password) {
|
||||
if (!hdr) {
|
||||
// auth header is missing
|
||||
return false;
|
||||
}
|
||||
const token = Buffer.from(`${username}:${password}`).toString('base64');
|
||||
const arr = /^Basic (.*)$/.exec(hdr);
|
||||
if (!Array.isArray(arr)) {
|
||||
// malformed auth header
|
||||
return false;
|
||||
}
|
||||
return arr[1] === token;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
logger.debug({
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
}, 'received upgrade request');
|
||||
|
||||
/* verify the path starts with /transcribe */
|
||||
if (!request.url.includes('/record/')) {
|
||||
logger.info(`unhandled path: ${request.url}`);
|
||||
return socket.write('HTTP/1.1 404 Not Found \r\n\r\n', () => socket.destroy());
|
||||
}
|
||||
|
||||
/* verify the api key */
|
||||
if (!isValidWsKey(request.headers['authorization'])) {
|
||||
logger.info(`invalid auth header: ${request.headers['authorization'] || 'authorization header missing'}`);
|
||||
return socket.write('HTTP/1.1 403 Forbidden \r\n\r\n', () => socket.destroy());
|
||||
}
|
||||
|
||||
/* complete the upgrade */
|
||||
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||
logger.debug(`upgraded to websocket, url: ${request.url}`);
|
||||
wsServer.emit('connection', ws, request.url);
|
||||
});
|
||||
});
|
||||
|
||||
// purge old calls from active call set every 10 mins
|
||||
async function purge() {
|
||||
|
||||
@@ -14,8 +14,12 @@ DROP TABLE IF EXISTS beta_invite_codes;
|
||||
|
||||
DROP TABLE IF EXISTS call_routes;
|
||||
|
||||
DROP TABLE IF EXISTS clients;
|
||||
|
||||
DROP TABLE IF EXISTS dns_records;
|
||||
|
||||
DROP TABLE IF EXISTS lcr;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_routes;
|
||||
@@ -50,8 +54,12 @@ DROP TABLE IF EXISTS signup_history;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS google_custom_voices;
|
||||
|
||||
DROP TABLE IF EXISTS speech_credentials;
|
||||
|
||||
DROP TABLE IF EXISTS system_information;
|
||||
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_gateways;
|
||||
@@ -124,6 +132,19 @@ application_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (call_route_sid)
|
||||
) 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),
|
||||
allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1,
|
||||
allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1,
|
||||
allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (client_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE dns_records
|
||||
(
|
||||
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -136,11 +157,23 @@ PRIMARY KEY (dns_record_sid)
|
||||
CREATE TABLE lcr_routes
|
||||
(
|
||||
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',
|
||||
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)
|
||||
) COMMENT='Least cost routing table';
|
||||
) COMMENT='An ordered list of digit patterns in an LCR table. The pat';
|
||||
|
||||
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 ';
|
||||
|
||||
CREATE TABLE password_settings
|
||||
(
|
||||
@@ -248,7 +281,10 @@ CREATE TABLE sbc_addresses
|
||||
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(255) NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 5060,
|
||||
tls_port INTEGER,
|
||||
wss_port INTEGER,
|
||||
service_provider_sid CHAR(36),
|
||||
last_updated DATETIME,
|
||||
PRIMARY KEY (sbc_address_sid)
|
||||
);
|
||||
|
||||
@@ -304,9 +340,31 @@ last_tested DATETIME,
|
||||
tts_tested_ok BOOLEAN,
|
||||
stt_tested_ok BOOLEAN,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
label VARCHAR(64),
|
||||
PRIMARY KEY (speech_credential_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE google_custom_voices
|
||||
(
|
||||
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
speech_credential_sid CHAR(36) NOT NULL,
|
||||
model VARCHAR(512) NOT NULL,
|
||||
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
|
||||
name VARCHAR(64) NOT NULL,
|
||||
voice_cloning_key MEDIUMTEXT,
|
||||
use_voice_cloning_key BOOLEAN DEFAULT false,
|
||||
PRIMARY KEY (google_custom_voice_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE system_information
|
||||
(
|
||||
domain_name VARCHAR(255),
|
||||
sip_domain_name VARCHAR(255),
|
||||
monitoring_domain_name VARCHAR(255),
|
||||
private_network_cidr VARCHAR(8192),
|
||||
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
user_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -357,6 +415,8 @@ 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),
|
||||
dtmf_type ENUM('rfc2833','tones','info') NOT NULL DEFAULT 'rfc2833',
|
||||
PRIMARY KEY (voip_carrier_sid)
|
||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
|
||||
@@ -385,7 +445,7 @@ PRIMARY KEY (smpp_gateway_sid)
|
||||
CREATE TABLE phone_numbers
|
||||
(
|
||||
phone_number_sid CHAR(36) UNIQUE ,
|
||||
number VARCHAR(132) NOT NULL UNIQUE ,
|
||||
number VARCHAR(132) NOT NULL,
|
||||
voip_carrier_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
application_sid CHAR(36),
|
||||
@@ -398,11 +458,15 @@ CREATE TABLE sip_gateways
|
||||
sip_gateway_sid CHAR(36),
|
||||
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
|
||||
netmask INTEGER NOT NULL DEFAULT 32,
|
||||
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
|
||||
port INTEGER COMMENT 'sip signaling port',
|
||||
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
|
||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
send_options_ping BOOLEAN NOT NULL DEFAULT 0,
|
||||
use_sips_scheme BOOLEAN NOT NULL DEFAULT 0,
|
||||
pad_crypto BOOLEAN NOT NULL DEFAULT 0,
|
||||
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
|
||||
PRIMARY KEY (sip_gateway_sid)
|
||||
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
||||
|
||||
@@ -438,10 +502,21 @@ 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_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(64),
|
||||
speech_synthesis_voice VARCHAR(256),
|
||||
speech_synthesis_label VARCHAR(64),
|
||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||
speech_recognizer_label VARCHAR(64),
|
||||
use_for_fallback_speech BOOLEAN DEFAULT false,
|
||||
fallback_speech_synthesis_vendor VARCHAR(64),
|
||||
fallback_speech_synthesis_language VARCHAR(12),
|
||||
fallback_speech_synthesis_voice VARCHAR(256),
|
||||
fallback_speech_synthesis_label VARCHAR(64),
|
||||
fallback_speech_recognizer_vendor VARCHAR(64),
|
||||
fallback_speech_recognizer_language VARCHAR(64),
|
||||
fallback_speech_recognizer_label VARCHAR(64),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (application_sid)
|
||||
) COMMENT='A defined set of behaviors to be applied to phone calls ';
|
||||
|
||||
@@ -479,6 +554,10 @@ 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',
|
||||
enable_debug_log BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (account_sid)
|
||||
) COMMENT='An enterprise that uses the platform for comm services';
|
||||
|
||||
@@ -499,9 +578,20 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERE
|
||||
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX client_sid_idx ON clients (client_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_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
|
||||
@@ -555,8 +645,6 @@ CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
|
||||
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_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 service_provider_sid_idx ON speech_credentials (service_provider_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
@@ -564,6 +652,10 @@ ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (ser
|
||||
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid);
|
||||
CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid);
|
||||
ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX user_sid_idx ON users (user_sid);
|
||||
CREATE INDEX email_idx ON users (email);
|
||||
CREATE INDEX phone_idx ON users (phone);
|
||||
@@ -593,6 +685,8 @@ CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
|
||||
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
|
||||
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
CREATE 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 number_idx ON phone_numbers (number);
|
||||
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
|
||||
@@ -648,4 +742,4 @@ ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (devic
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
641
db/jambones.sqs
641
db/jambones.sqs
File diff suppressed because one or more lines are too long
@@ -22,17 +22,25 @@ values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9
|
||||
|
||||
-- create one service provider and one account
|
||||
insert into service_providers (service_provider_sid, name, root_domain)
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.us');
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.cloud');
|
||||
|
||||
insert into accounts (account_sid, service_provider_sid, name, webhook_secret)
|
||||
values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'wh_secret_cJqgtMDPzDhhnjmaJH6Mtk');
|
||||
|
||||
-- create account level api key
|
||||
insert into api_keys (api_key_sid, token, service_provider_sid)
|
||||
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fa', '38700987-c7a4-4685-a5bb-af378f9734da', '9351f46a-678c-43f5-b8a6-d4eb58d131af');
|
||||
|
||||
-- create SP level api key
|
||||
insert into api_keys (api_key_sid, token, account_sid)
|
||||
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fs', '38700987-c7a4-4685-a5bb-af378f9734ds', '2708b1b3-2736-40ea-b502-c53d8396247f');
|
||||
|
||||
-- create two applications
|
||||
insert into webhooks(webhook_sid, url, method)
|
||||
values
|
||||
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
|
||||
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
|
||||
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
|
||||
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.cloud/call-status', 'POST'),
|
||||
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.cloud/hello-world', 'POST'),
|
||||
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.cloud/dial-time', 'POST');
|
||||
|
||||
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
|
||||
VALUES
|
||||
@@ -79,6 +87,7 @@ VALUES
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('973e7824-0cf3-4645-88e4-d2460ddb8577', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '168.86.128.0', 18, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
|
||||
@@ -24,10 +24,9 @@ values ('09e92f3c-9d73-4303-b63f-3668574862ce', '1cf2f4f4-64c4-4249-9a3e-5bb4cb5
|
||||
-- create two applications
|
||||
insert into webhooks(webhook_sid, url, method)
|
||||
values
|
||||
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
|
||||
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
|
||||
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
|
||||
|
||||
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.cloud/call-status', 'POST'),
|
||||
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.cloud/hello-world', 'POST'),
|
||||
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.cloud/dial-time', 'POST');
|
||||
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
|
||||
VALUES
|
||||
('7087fe50-8acb-4f3b-b820-97b573723aab', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'hello world', 'd31568d0-b193-4a05-8ff6-778369bc6efe', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'),
|
||||
@@ -68,14 +67,12 @@ VALUES
|
||||
('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0),
|
||||
('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0),
|
||||
('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0),
|
||||
('48b108e3-1ce7-4f18-a4cb-e41e63688bdf', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.193', 30, 5060, 1, 0),
|
||||
('d9131a69-fe44-4c2a-ba82-4adc81f628dd', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.194', 30, 5060, 1, 0),
|
||||
('34a6a311-4bd6-49ca-aa77-edd3cb92c6e1', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.195', 30, 5060, 1, 0),
|
||||
('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0),
|
||||
('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0),
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('973e7824-0cf3-4645-88e4-d2460ddb8577', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '168.86.128.0', 18, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
|
||||
@@ -53,6 +53,7 @@ VALUES
|
||||
('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0),
|
||||
('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0),
|
||||
('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0),
|
||||
('973e7824-0cf3-4645-88e4-d2460ddb8577', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '168.86.128.0', 18, 5060, 1, 0),
|
||||
('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '<your-domain>.pstn.twilio.com', 32, 5060, 0, 1);
|
||||
|
||||
-- voxbone gateways
|
||||
|
||||
@@ -87,6 +87,128 @@ const sql = {
|
||||
'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)',
|
||||
'ALTER TABLE `users` ADD COLUMN `is_active` BOOLEAN NOT NULL default true',
|
||||
],
|
||||
'8003': [
|
||||
'SET FOREIGN_KEY_CHECKS=0',
|
||||
'ALTER TABLE `voip_carriers` ADD COLUMN `register_status` VARCHAR(4096)',
|
||||
'ALTER TABLE `sbc_addresses` ADD COLUMN `last_updated` DATETIME',
|
||||
'ALTER TABLE `sbc_addresses` ADD COLUMN `tls_port` INTEGER',
|
||||
'ALTER TABLE `sbc_addresses` ADD COLUMN `wss_port` INTEGER',
|
||||
`CREATE TABLE system_information
|
||||
(
|
||||
domain_name VARCHAR(255),
|
||||
sip_domain_name VARCHAR(255),
|
||||
monitoring_domain_name VARCHAR(255)
|
||||
)`,
|
||||
'DROP TABLE IF EXISTS `lcr_routes`',
|
||||
'DROP TABLE IF EXISTS `lcr_carrier_set_entry`',
|
||||
`CREATE TABLE lcr_routes
|
||||
(
|
||||
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',
|
||||
description VARCHAR(1024),
|
||||
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
|
||||
PRIMARY KEY (lcr_route_sid)
|
||||
)`,
|
||||
`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)
|
||||
)`,
|
||||
`CREATE TABLE lcr_carrier_set_entry
|
||||
(
|
||||
lcr_carrier_set_entry_sid CHAR(36),
|
||||
workload INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
|
||||
lcr_route_sid CHAR(36) NOT NULL,
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
|
||||
PRIMARY KEY (lcr_carrier_set_entry_sid)
|
||||
)`,
|
||||
'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)',
|
||||
'ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid)',
|
||||
'ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_3 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid)',
|
||||
'SET FOREIGN_KEY_CHECKS=1',
|
||||
],
|
||||
'8004': [
|
||||
'alter table accounts add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
|
||||
'alter table accounts add column bucket_credential VARCHAR(8192)',
|
||||
'alter table accounts add column record_format VARCHAR(16) NOT NULL DEFAULT \'mp3\'',
|
||||
'alter table applications add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
|
||||
'alter table phone_numbers DROP INDEX number',
|
||||
'create unique index phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid)',
|
||||
`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 INDEX client_sid_idx ON clients (client_sid)',
|
||||
'ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid)',
|
||||
'ALTER TABLE sip_gateways ADD COLUMN protocol ENUM(\'udp\',\'tcp\',\'tls\', \'tls/srtp\') DEFAULT \'udp\''
|
||||
],
|
||||
'8005': [
|
||||
'DROP INDEX speech_credentials_idx_1 ON speech_credentials',
|
||||
'ALTER TABLE speech_credentials ADD COLUMN label VARCHAR(64)',
|
||||
'ALTER TABLE applications ADD COLUMN speech_synthesis_label VARCHAR(64)',
|
||||
'ALTER TABLE applications ADD COLUMN speech_recognizer_label VARCHAR(64)',
|
||||
'ALTER TABLE applications ADD COLUMN use_for_fallback_speech BOOLEAN DEFAULT false',
|
||||
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_vendor VARCHAR(64)',
|
||||
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_language VARCHAR(12)',
|
||||
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_voice VARCHAR(64)',
|
||||
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_label VARCHAR(64)',
|
||||
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_vendor VARCHAR(64)',
|
||||
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_language VARCHAR(64)',
|
||||
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_label VARCHAR(64)',
|
||||
'ALTER TABLE sip_gateways ADD COLUMN pad_crypto BOOLEAN NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE sip_gateways MODIFY port INTEGER',
|
||||
`CREATE TABLE google_custom_voices
|
||||
(
|
||||
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
speech_credential_sid CHAR(36) NOT NULL,
|
||||
model VARCHAR(512) NOT NULL,
|
||||
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
|
||||
name VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (google_custom_voice_sid)
|
||||
)
|
||||
`,
|
||||
'CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid)',
|
||||
'CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid)',
|
||||
'ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE',
|
||||
'ALTER TABLE clients ADD COLUMN allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1',
|
||||
'ALTER TABLE clients ADD COLUMN allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1',
|
||||
'ALTER TABLE clients ADD COLUMN allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1',
|
||||
],
|
||||
9000: [
|
||||
'ALTER TABLE sip_gateways ADD COLUMN send_options_ping BOOLEAN NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(256)',
|
||||
'ALTER TABLE applications MODIFY COLUMN fallback_speech_synthesis_voice VARCHAR(256)',
|
||||
'ALTER TABLE sip_gateways ADD COLUMN use_sips_scheme BOOLEAN NOT NULL DEFAULT 0',
|
||||
],
|
||||
9002: [
|
||||
'ALTER TABLE system_information ADD COLUMN private_network_cidr VARCHAR(8192)',
|
||||
'ALTER TABLE system_information ADD COLUMN log_level ENUM(\'info\', \'debug\') NOT NULL DEFAULT \'info\'',
|
||||
'ALTER TABLE accounts ADD COLUMN enable_debug_log BOOLEAN NOT NULL DEFAULT false',
|
||||
'ALTER TABLE google_custom_voices ADD COLUMN use_voice_cloning_key BOOLEAN DEFAULT false',
|
||||
'ALTER TABLE google_custom_voices ADD COLUMN voice_cloning_key MEDIUMTEXT',
|
||||
],
|
||||
9003: [
|
||||
'ALTER TABLE google_custom_voices ADD COLUMN voice_cloning_key MEDIUMTEXT',
|
||||
'ALTER TABLE google_custom_voices ADD COLUMN use_voice_cloning_key BOOLEAN DEFAULT false',
|
||||
'ALTER TABLE voip_carriers ADD COLUMN dtmf_type ENUM(\'rfc2833\',\'tones\',\'info\') NOT NULL DEFAULT \'rfc2833\'',
|
||||
]
|
||||
};
|
||||
|
||||
@@ -116,6 +238,12 @@ const doIt = async() => {
|
||||
if (val < 7006) upgrades.push(...sql['7006']);
|
||||
if (val < 7007) upgrades.push(...sql['7007']);
|
||||
if (val < 8000) upgrades.push(...sql['8000']);
|
||||
if (val < 8003) upgrades.push(...sql['8003']);
|
||||
if (val < 8004) upgrades.push(...sql['8004']);
|
||||
if (val < 8005) upgrades.push(...sql['8005']);
|
||||
if (val < 9000) upgrades.push(...sql['9000']);
|
||||
if (val < 9002) upgrades.push(...sql['9002']);
|
||||
if (val < 9003) upgrades.push(...sql['9003']);
|
||||
|
||||
// perform all upgrades
|
||||
logger.info({upgrades}, 'applying schema upgrades..');
|
||||
|
||||
@@ -2,7 +2,7 @@ SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
-- create one service provider
|
||||
insert into service_providers (service_provider_sid, name, description, root_domain)
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.us', 'jambonz.us service provider', 'sip.yakeeda.com');
|
||||
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.cloud', 'jambonz.cloud service provider', 'sip.yakeeda.com');
|
||||
insert into api_keys (api_key_sid, token)
|
||||
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
|
||||
|
||||
@@ -19,8 +19,8 @@ insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound
|
||||
values ('46b727eb-c7dc-44fa-b063-96e48d408e4a', '5145b436-2f38-4029-8d4c-fd8c67831c7a', '3.3.3.3', 5060, 1, 1, 1);
|
||||
|
||||
-- create the test application and test phone number
|
||||
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.us/testCall', 'POST');
|
||||
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.us/callStatus', 'POST');
|
||||
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.cloud/testCall', 'POST');
|
||||
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.cloud/callStatus', 'POST');
|
||||
insert into applications (application_sid, name, service_provider_sid, call_hook_sid, call_status_hook_sid,
|
||||
speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
|
||||
values ('7a489343-02ed-471e-8df0-fc5e1b98ce8f', 'Test application', '2708b1b3-2736-40ea-b502-c53d8396247f',
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
const logger = require('../logger');
|
||||
|
||||
const {
|
||||
client,
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey,
|
||||
client: redisClient,
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
listConferences
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
module.exports = {
|
||||
client,
|
||||
retrieveCall,
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
redisClient,
|
||||
incrKey,
|
||||
listConferences
|
||||
};
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
const opts = Object.assign({
|
||||
timestamp: () => {
|
||||
return `, "time": "${new Date().toISOString()}"`;
|
||||
}
|
||||
}, {
|
||||
const opts = {
|
||||
level: process.env.JAMBONES_LOGLEVEL || 'info'
|
||||
});
|
||||
|
||||
const logger = require('pino')(opts);
|
||||
};
|
||||
const pino = require('pino');
|
||||
const logger = pino(opts, pino.destination(1, {sync: false}));
|
||||
|
||||
module.exports = logger;
|
||||
|
||||
32
lib/middleware.js
Normal file
32
lib/middleware.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const logger = require('./logger');
|
||||
|
||||
function delayLoginMiddleware(req, res, next) {
|
||||
if (req.path.includes('/login') || req.path.includes('/signin')) {
|
||||
const min = 200;
|
||||
const max = 1000;
|
||||
/* Random delay between 200 - 1000ms */
|
||||
const sendStatusDelay = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
||||
/* the res.json take longer, we decrease the max delay slightly to 0-800ms */
|
||||
const jsonDelay = Math.floor(Math.random() * 800);
|
||||
logger.debug(`delayLoginMiddleware: sendStatus ${sendStatusDelay} - json ${jsonDelay}`);
|
||||
const sendStatus = res.sendStatus;
|
||||
const json = res.json;
|
||||
|
||||
res.sendStatus = function(status) {
|
||||
setTimeout(() => {
|
||||
sendStatus.call(res, status);
|
||||
}, sendStatusDelay);
|
||||
};
|
||||
res.json = function(body) {
|
||||
setTimeout(() => {
|
||||
json.call(res, body);
|
||||
}, jsonDelay);
|
||||
};
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
delayLoginMiddleware
|
||||
};
|
||||
@@ -4,7 +4,7 @@ const {getMysqlConnection} = require('../db');
|
||||
const {promisePool} = require('../db');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const {encrypt} = require('../utils/encrypt-decrypt');
|
||||
const {encrypt, decrypt} = require('../utils/encrypt-decrypt');
|
||||
|
||||
const retrieveSql = `SELECT * from accounts acc
|
||||
LEFT JOIN webhooks AS rh
|
||||
@@ -34,7 +34,7 @@ AND effective_end_date IS NULL
|
||||
AND pending=0`;
|
||||
|
||||
const updatePaymentInfoSql = `UPDATE account_subscriptions
|
||||
SET last4 = ?, exp_month = ?, exp_year = ?, card_type = ?
|
||||
SET last4 = ?, stripe_payment_method_id=?, exp_month = ?, exp_year = ?, card_type = ?
|
||||
WHERE account_sid = ?
|
||||
AND effective_end_date IS NULL`;
|
||||
|
||||
@@ -55,6 +55,17 @@ WHERE account_sid = ?
|
||||
AND effective_end_date IS NULL
|
||||
AND pending = 0`;
|
||||
|
||||
const extractBucketCredential = (obj) => {
|
||||
try {
|
||||
const {bucket_credential} = obj;
|
||||
if (bucket_credential) {
|
||||
obj.bucket_credential = JSON.parse(decrypt(bucket_credential));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while decrypting data', error);
|
||||
}
|
||||
};
|
||||
|
||||
function transmogrifyResults(results) {
|
||||
return results.map((row) => {
|
||||
const obj = row.acc;
|
||||
@@ -75,6 +86,8 @@ function transmogrifyResults(results) {
|
||||
else obj.queue_event_hook = null;
|
||||
delete obj.queue_event_hook_sid;
|
||||
|
||||
extractBucketCredential(obj);
|
||||
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
@@ -190,17 +203,18 @@ class Account extends Model {
|
||||
debug(r3, 'Account.activateSubscription - replaced old subscription');
|
||||
|
||||
/* update account.plan to paid, if it isnt already */
|
||||
/* update account.is_active to 1, if account is deactivated */
|
||||
await promisePool.execute(
|
||||
'UPDATE accounts SET plan_type = \'paid\' WHERE account_sid = ?',
|
||||
'UPDATE accounts SET plan_type = \'paid\', is_active = 1 WHERE account_sid = ?',
|
||||
[account_sid]);
|
||||
return true;
|
||||
}
|
||||
|
||||
static async updatePaymentInfo(logger, account_sid, pm) {
|
||||
const {card} = pm;
|
||||
const {id, card} = pm;
|
||||
const last4_encrypted = encrypt(card.last4);
|
||||
await promisePool.execute(updatePaymentInfoSql,
|
||||
[last4_encrypted, card.exp_month, card.exp_year, card.brand, account_sid]);
|
||||
[last4_encrypted, id, card.exp_month, card.exp_year, card.brand, account_sid]);
|
||||
}
|
||||
|
||||
static async provisionPendingSubscription(logger, account_sid, products, payment_method, subscription_id) {
|
||||
@@ -238,7 +252,6 @@ class Account extends Model {
|
||||
}));
|
||||
return account_subscription_sid;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Account.table = 'accounts';
|
||||
@@ -318,6 +331,18 @@ Account.fields = [
|
||||
name: 'siprec_hook_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'record_all_calls',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'record_format',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'bucket_credential',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = Account;
|
||||
|
||||
@@ -37,19 +37,24 @@ class Application extends Model {
|
||||
}
|
||||
|
||||
/**
|
||||
* list all applications - for all service providers, for one service provider, or for one account
|
||||
* list all applications - for all service providers, for one service provider, or for one account,
|
||||
* or by an optional name
|
||||
*/
|
||||
static retrieveAll(service_provider_sid, account_sid) {
|
||||
let sql = retrieveSql;
|
||||
static retrieveAll(service_provider_sid, account_sid, name) {
|
||||
let sql = retrieveSql + ' WHERE 1 = 1';
|
||||
const args = [];
|
||||
if (account_sid) {
|
||||
sql = `${sql} WHERE app.account_sid = ?`;
|
||||
sql = `${sql} AND app.account_sid = ?`;
|
||||
args.push(account_sid);
|
||||
}
|
||||
else if (service_provider_sid) {
|
||||
sql = `${sql} WHERE account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)`;
|
||||
sql = `${sql} AND account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)`;
|
||||
args.push(service_provider_sid);
|
||||
}
|
||||
if (name) {
|
||||
sql = `${sql} AND app.name = ?`;
|
||||
args.push(name);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
@@ -120,6 +125,10 @@ Application.fields = [
|
||||
{
|
||||
name: 'messaging_hook_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'record_all_calls',
|
||||
type: 'number',
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
70
lib/models/client.js
Normal file
70
lib/models/client.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
|
||||
class Client extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAllByAccountSid(account_sid) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE account_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, account_sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveAllByServiceProviderSid(service_provider_sid) {
|
||||
const sql = `SELECT c.client_sid, c.account_sid, c.is_active, c.username, c.password
|
||||
FROM ${this.table} AS c LEFT JOIN accounts AS acc ON c.account_sid = acc.account_sid
|
||||
LEFT JOIN service_providers AS sp ON sp.service_provider_sid = acc.service_provider_sid
|
||||
WHERE sp.service_provider_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, service_provider_sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveByAccountSidAndUserName(account_sid, username) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE account_sid = ? AND username = ?`;
|
||||
const [rows] = await promisePool.query(sql, [account_sid, username]);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
Client.table = 'clients';
|
||||
Client.fields = [
|
||||
{
|
||||
name: 'client_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'account_sid',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'allow_direct_app_calling',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'allow_direct_queue_calling',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'allow_direct_user_calling',
|
||||
type: 'number'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = Client;
|
||||
61
lib/models/google-custom-voice.js
Normal file
61
lib/models/google-custom-voice.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
|
||||
class GoogleCustomVoice extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAllBySpeechCredentialSid(speech_credential_sid) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE speech_credential_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, speech_credential_sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async deleteAllBySpeechCredentialSid(speech_credential_sid) {
|
||||
const sql = `DELETE FROM ${this.table} WHERE speech_credential_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, speech_credential_sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveAllByLabel(service_provider_sid, account_sid, label) {
|
||||
let sql;
|
||||
if (account_sid) {
|
||||
sql = `SELECT gcv.* FROM ${this.table} gcv
|
||||
LEFT JOIN speech_credentials sc ON gcv.speech_credential_sid = sc.speech_credential_sid
|
||||
WHERE sc.account_sid = ? OR (sc.account_sid is NULL && sc.service_provider_sid = ?)
|
||||
${label ? 'AND label = ?' : 'AND label is NULL'}`;
|
||||
} else {
|
||||
sql = `SELECT gcv.* FROM ${this.table} gcv
|
||||
LEFT JOIN speech_credentials sc ON gcv.speech_credential_sid = sc.speech_credential_sid
|
||||
WHERE sc.service_provider_sid = ? ${label ? 'AND label = ?' : 'AND label is NULL'}`;
|
||||
}
|
||||
const [rows] = await promisePool.query(sql, [...(account_sid ?
|
||||
[account_sid, service_provider_sid] : [service_provider_sid]), label]);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
GoogleCustomVoice.table = 'google_custom_voices';
|
||||
GoogleCustomVoice.fields = [
|
||||
{
|
||||
name: 'google_custom_voice_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'model',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reported_usage',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = GoogleCustomVoice;
|
||||
47
lib/models/lcr-carrier-set-entry.js
Normal file
47
lib/models/lcr-carrier-set-entry.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
|
||||
class LcrCarrierSetEntry extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAllByLcrRouteSid(sid) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE lcr_route_sid = ? ORDER BY priority`;
|
||||
const [rows] = await promisePool.query(sql, sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async deleteByLcrRouteSid(sid) {
|
||||
const sql = `DELETE FROM ${this.table} WHERE lcr_route_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, sid);
|
||||
return rows.affectedRows;
|
||||
}
|
||||
}
|
||||
|
||||
LcrCarrierSetEntry.table = 'lcr_carrier_set_entry';
|
||||
LcrCarrierSetEntry.fields = [
|
||||
{
|
||||
name: 'lcr_carrier_set_entry_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'workload',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'lcr_route_sid',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'voip_carrier_sid',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: 'number'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = LcrCarrierSetEntry;
|
||||
54
lib/models/lcr-route.js
Normal file
54
lib/models/lcr-route.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
|
||||
|
||||
class LcrRoutes extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAllByLcrSid(sid) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE lcr_sid = ? ORDER BY priority`;
|
||||
const [rows] = await promisePool.query(sql, sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async deleteByLcrSid(sid) {
|
||||
const sql = `DELETE FROM ${this.table} WHERE lcr_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, sid);
|
||||
return rows.affectedRows;
|
||||
}
|
||||
|
||||
static async countAllByLcrSid(sid) {
|
||||
const sql = `SELECT COUNT(*) AS count FROM ${this.table} WHERE lcr_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, sid);
|
||||
return rows.length ? rows[0].count : 0;
|
||||
}
|
||||
}
|
||||
|
||||
LcrRoutes.table = 'lcr_routes';
|
||||
LcrRoutes.fields = [
|
||||
{
|
||||
name: 'lcr_route_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'lcr_sid',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'regex',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: 'number'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = LcrRoutes;
|
||||
54
lib/models/lcr.js
Normal file
54
lib/models/lcr.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
|
||||
class Lcr extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAllByAccountSid(account_sid) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE account_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, account_sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveAllByServiceProviderSid(sid) {
|
||||
const sql = `SELECT * FROM ${this.table} WHERE service_provider_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, sid);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async releaseDefaultEntry(sid) {
|
||||
const sql = `UPDATE ${this.table} SET default_carrier_set_entry_sid = null WHERE lcr_sid = ?`;
|
||||
const [rows] = await promisePool.query(sql, sid);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
Lcr.table = 'lcr';
|
||||
Lcr.fields = [
|
||||
{
|
||||
name: 'lcr_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'account_sid',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'service_provider_sid',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'default_carrier_set_entry_sid',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = Lcr;
|
||||
@@ -1,6 +1,7 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const sql = 'SELECT * from phone_numbers WHERE account_sid = ?';
|
||||
const sqlRetrieveAll = 'SELECT * from phone_numbers WHERE account_sid = ? ORDER BY number';
|
||||
const sqlRetrieveOne = 'SELECT * from phone_numbers WHERE phone_number_sid = ? AND account_sid = ? ORDER BY number';
|
||||
const sqlSP = `SELECT *
|
||||
FROM phone_numbers
|
||||
WHERE account_sid IN
|
||||
@@ -8,7 +9,7 @@ WHERE account_sid IN
|
||||
SELECT account_sid
|
||||
FROM accounts
|
||||
WHERE service_provider_sid = ?
|
||||
)`;
|
||||
) ORDER BY number`;
|
||||
|
||||
class PhoneNumber extends Model {
|
||||
constructor() {
|
||||
@@ -16,8 +17,8 @@ class PhoneNumber extends Model {
|
||||
}
|
||||
|
||||
static async retrieveAll(account_sid) {
|
||||
if (!account_sid) return super.retrieveAll();
|
||||
const [rows] = await promisePool.query(sql, account_sid);
|
||||
if (!account_sid) return await super.retrieveAll();
|
||||
const [rows] = await promisePool.query(sqlRetrieveAll, account_sid);
|
||||
return rows;
|
||||
}
|
||||
static async retrieveAllForSP(service_provider_sid) {
|
||||
@@ -30,7 +31,7 @@ class PhoneNumber extends Model {
|
||||
*/
|
||||
static async retrieve(sid, account_sid) {
|
||||
if (!account_sid) return super.retrieve(sid);
|
||||
const [rows] = await promisePool.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid]);
|
||||
const [rows] = await promisePool.query(sqlRetrieveOne, [sid, account_sid]);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ ServiceProvider.fields = [
|
||||
{
|
||||
name: 'ms_teams_fqdn',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'lcr_sid',
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
@@ -51,6 +51,10 @@ SipGateway.fields = [
|
||||
name: 'is_active',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'pad_crypto',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'account_sid',
|
||||
type: 'string'
|
||||
@@ -58,6 +62,10 @@ SipGateway.fields = [
|
||||
{
|
||||
name: 'application_sid',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const retrieveSql = 'SELECT * from speech_credentials WHERE account_sid = ?';
|
||||
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ?';
|
||||
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ? and account_sid is null';
|
||||
|
||||
class SpeechCredential extends Model {
|
||||
constructor() {
|
||||
@@ -20,6 +20,22 @@ class SpeechCredential extends Model {
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async getSpeechCredentialsByVendorAndLabel(service_provider_sid, account_sid, vendor, label) {
|
||||
let sql;
|
||||
let rows = [];
|
||||
if (account_sid) {
|
||||
sql = `SELECT * FROM speech_credentials WHERE account_sid = ? AND vendor = ?
|
||||
AND label ${label ? '= ?' : 'is NULL'}`;
|
||||
[rows] = await promisePool.query(sql, [account_sid, vendor, label]);
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
sql = `SELECT * FROM speech_credentials WHERE service_provider_sid = ? AND vendor = ?
|
||||
AND label ${label ? '= ?' : 'is NULL'}`;
|
||||
[rows] = await promisePool.query(sql, [service_provider_sid, vendor, label]);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async disableStt(account_sid) {
|
||||
await promisePool.execute('UPDATE speech_credentials SET use_for_stt = 0 WHERE account_sid = ?', [account_sid]);
|
||||
}
|
||||
@@ -86,6 +102,10 @@ SpeechCredential.fields = [
|
||||
{
|
||||
name: 'last_tested',
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
name: 'label',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
42
lib/models/system-information.js
Normal file
42
lib/models/system-information.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const Model = require('./model');
|
||||
const { promisePool } = require('../db');
|
||||
class SystemInformation extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async add(body) {
|
||||
let [sysInfo] = await this.retrieveAll();
|
||||
if (sysInfo) {
|
||||
const sql = `UPDATE ${this.table} SET ?`;
|
||||
await promisePool.query(sql, body);
|
||||
} else {
|
||||
const sql = `INSERT INTO ${this.table} SET ?`;
|
||||
await promisePool.query(sql, body);
|
||||
}
|
||||
[sysInfo] = await this.retrieveAll();
|
||||
return sysInfo;
|
||||
}
|
||||
}
|
||||
|
||||
SystemInformation.table = 'system_information';
|
||||
SystemInformation.fields = [
|
||||
{
|
||||
name: 'domain_name',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'sip_domain_name',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'monitoring_domain_name',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'private_network_cidr',
|
||||
type: 'string',
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = SystemInformation;
|
||||
@@ -11,10 +11,16 @@ class VoipCarrier extends Model {
|
||||
static async retrieveAll(account_sid) {
|
||||
if (!account_sid) return super.retrieveAll();
|
||||
const [rows] = await promisePool.query(retrieveSql, account_sid);
|
||||
if (rows) {
|
||||
rows.map((r) => r.register_status = JSON.parse(r.register_status || '{}'));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
static async retrieveAllForSP(service_provider_sid) {
|
||||
const [rows] = await promisePool.query(retrieveSqlForSP, service_provider_sid);
|
||||
if (rows) {
|
||||
rows.map((r) => r.register_status = JSON.parse(r.register_status || '{}'));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -55,6 +61,10 @@ VoipCarrier.fields = [
|
||||
name: 'requires_register',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'register_use_tls',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'register_username',
|
||||
type: 'string'
|
||||
@@ -122,6 +132,14 @@ VoipCarrier.fields = [
|
||||
{
|
||||
name: 'register_public_ip_in_contact',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
name: 'register_status',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'dtmf_type',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
71
lib/record/azure-storage.js
Normal file
71
lib/record/azure-storage.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const { Writable } = require('stream');
|
||||
const { BlobServiceClient } = require('@azure/storage-blob');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const streamBuffers = require('stream-buffers');
|
||||
|
||||
class AzureStorageUploadStream extends Writable {
|
||||
constructor(logger, opts) {
|
||||
super(opts);
|
||||
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
|
||||
this.blockBlobClient = blobServiceClient.getContainerClient(opts.bucketName).getBlockBlobClient(opts.Key);
|
||||
|
||||
this.metadata = opts.metadata;
|
||||
this.blocks = [];
|
||||
|
||||
this.bufferSize = 2 * 1024 * 1024; // Buffer size set to 2MB
|
||||
this.buffer = new streamBuffers.WritableStreamBuffer({
|
||||
initialSize: this.bufferSize,
|
||||
incrementAmount: this.bufferSize
|
||||
});
|
||||
}
|
||||
|
||||
async _write(chunk, encoding, callback) {
|
||||
this.buffer.write(chunk, encoding);
|
||||
|
||||
if (this.buffer.size() >= this.bufferSize) {
|
||||
const blockID = uuidv4().replace(/-/g, '');
|
||||
this.blocks.push(blockID);
|
||||
|
||||
try {
|
||||
const dataToWrite = this.buffer.getContents();
|
||||
await this.blockBlobClient.stageBlock(blockID, dataToWrite, dataToWrite.length);
|
||||
callback();
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
async _final(callback) {
|
||||
// Write any remaining data in buffer
|
||||
if (this.buffer.size() > 0) {
|
||||
const remainingData = this.buffer.getContents();
|
||||
const blockID = uuidv4().replace(/-/g, '');
|
||||
this.blocks.push(blockID);
|
||||
|
||||
try {
|
||||
await this.blockBlobClient.stageBlock(blockID, remainingData, remainingData.length);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.blockBlobClient.commitBlockList(this.blocks);
|
||||
// remove all null/undefined props
|
||||
const filteredObj = Object.entries(this.metadata).reduce((acc, [key, val]) => {
|
||||
if (val !== undefined && val !== null) acc[key] = val;
|
||||
return acc;
|
||||
}, {});
|
||||
await this.blockBlobClient.setMetadata(filteredObj);
|
||||
callback();
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AzureStorageUploadStream;
|
||||
61
lib/record/encoder.js
Normal file
61
lib/record/encoder.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const { Transform } = require('stream');
|
||||
const lamejs = require('@jambonz/lamejs');
|
||||
|
||||
class PCMToMP3Encoder extends Transform {
|
||||
constructor(options, logger) {
|
||||
super(options);
|
||||
|
||||
const channels = options.channels || 1;
|
||||
const sampleRate = options.sampleRate || 8000;
|
||||
const bitRate = options.bitRate || 128;
|
||||
|
||||
this.encoder = new lamejs.Mp3Encoder(channels, sampleRate, bitRate);
|
||||
this.channels = channels;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
try {
|
||||
// Convert chunk buffer into Int16Array for lamejs
|
||||
const samples = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
|
||||
|
||||
// Split input samples into left and right channel arrays if stereo
|
||||
let leftChannel, rightChannel;
|
||||
if (this.channels === 2) {
|
||||
leftChannel = new Int16Array(samples.length / 2);
|
||||
rightChannel = new Int16Array(samples.length / 2);
|
||||
|
||||
for (let i = 0; i < samples.length; i += 2) {
|
||||
leftChannel[i / 2] = samples[i];
|
||||
rightChannel[i / 2] = samples[i + 1];
|
||||
}
|
||||
} else {
|
||||
leftChannel = samples;
|
||||
}
|
||||
|
||||
// Encode the input data
|
||||
const mp3Data = this.encoder.encodeBuffer(leftChannel, rightChannel);
|
||||
|
||||
if (mp3Data.length > 0) {
|
||||
this.push(Buffer.from(mp3Data));
|
||||
}
|
||||
callback();
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
{ err },
|
||||
'Error while mp3 transform');
|
||||
}
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
// Finalize encoding and flush the internal buffers
|
||||
const mp3Data = this.encoder.flush();
|
||||
|
||||
if (mp3Data.length > 0) {
|
||||
this.push(Buffer.from(mp3Data));
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PCMToMP3Encoder;
|
||||
62
lib/record/google-storage.js
Normal file
62
lib/record/google-storage.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const { Storage } = require('@google-cloud/storage');
|
||||
const { Writable } = require('stream');
|
||||
const streamBuffers = require('stream-buffers');
|
||||
|
||||
class GoogleStorageUploadStream extends Writable {
|
||||
|
||||
constructor(logger, opts) {
|
||||
super(opts);
|
||||
this.logger = logger;
|
||||
this.metadata = opts.metadata;
|
||||
|
||||
const storage = new Storage(opts.bucketCredential);
|
||||
this.gcsFile = storage.bucket(opts.bucketName).file(opts.Key);
|
||||
this.writeStream = this.gcsFile.createWriteStream();
|
||||
|
||||
this.bufferSize = 2 * 1024 * 1024; // Buffer size set to 2MB
|
||||
this.buffer = new streamBuffers.WritableStreamBuffer({
|
||||
initialSize: this.bufferSize,
|
||||
incrementAmount: this.bufferSize
|
||||
});
|
||||
|
||||
this.writeStream.on('error', (err) => this.logger.error(err));
|
||||
this.writeStream.on('finish', () => {
|
||||
this.logger.info('Google storage Upload completed.');
|
||||
this._addMetadata();
|
||||
});
|
||||
}
|
||||
|
||||
_write(chunk, encoding, callback) {
|
||||
this.buffer.write(chunk, encoding);
|
||||
|
||||
// Write to GCS when buffer reaches desired size
|
||||
if (this.buffer.size() >= this.bufferSize) {
|
||||
const dataToWrite = this.buffer.getContents();
|
||||
this.writeStream.write(dataToWrite, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
_final(callback) {
|
||||
// Write any remaining data in the buffer to GCS
|
||||
if (this.buffer.size() > 0) {
|
||||
const remainingData = this.buffer.getContents();
|
||||
this.writeStream.write(remainingData);
|
||||
}
|
||||
|
||||
this.writeStream.end();
|
||||
this.writeStream.once('finish', callback);
|
||||
}
|
||||
|
||||
async _addMetadata() {
|
||||
try {
|
||||
await this.gcsFile.setMetadata({metadata: this.metadata});
|
||||
this.logger.info('Google storage Upload and metadata setting completed.');
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Google storage An error occurred while setting metadata');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleStorageUploadStream;
|
||||
6
lib/record/index.js
Normal file
6
lib/record/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
async function record(logger, socket) {
|
||||
return require('./upload')(logger, socket);
|
||||
}
|
||||
|
||||
module.exports = record;
|
||||
103
lib/record/s3-multipart-upload-stream.js
Normal file
103
lib/record/s3-multipart-upload-stream.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const { Writable } = require('stream');
|
||||
const {
|
||||
S3Client,
|
||||
CreateMultipartUploadCommand,
|
||||
UploadPartCommand,
|
||||
CompleteMultipartUploadCommand,
|
||||
} = require('@aws-sdk/client-s3');
|
||||
|
||||
class S3MultipartUploadStream extends Writable {
|
||||
constructor(logger, opts) {
|
||||
super(opts);
|
||||
this.logger = logger;
|
||||
this.bucketName = opts.bucketName;
|
||||
this.objectKey = opts.Key;
|
||||
this.uploadId = null;
|
||||
this.partNumber = 1;
|
||||
this.multipartETags = [];
|
||||
this.buffer = Buffer.alloc(0);
|
||||
this.minPartSize = 5 * 1024 * 1024; // 5 MB
|
||||
this.s3 = new S3Client(opts.bucketCredential);
|
||||
this.metadata = opts.metadata;
|
||||
}
|
||||
|
||||
async _initMultipartUpload() {
|
||||
const command = new CreateMultipartUploadCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: this.objectKey,
|
||||
Metadata: this.metadata
|
||||
});
|
||||
const response = await this.s3.send(command);
|
||||
return response.UploadId;
|
||||
}
|
||||
|
||||
async _uploadBuffer() {
|
||||
const uploadPartCommand = new UploadPartCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: this.objectKey,
|
||||
PartNumber: this.partNumber,
|
||||
UploadId: this.uploadId,
|
||||
Body: this.buffer,
|
||||
});
|
||||
|
||||
const uploadPartResponse = await this.s3.send(uploadPartCommand);
|
||||
this.multipartETags.push({
|
||||
ETag: uploadPartResponse.ETag,
|
||||
PartNumber: this.partNumber,
|
||||
});
|
||||
this.partNumber += 1;
|
||||
}
|
||||
|
||||
async _write(chunk, encoding, callback) {
|
||||
try {
|
||||
if (!this.uploadId) {
|
||||
this.uploadId = await this._initMultipartUpload();
|
||||
}
|
||||
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
|
||||
if (this.buffer.length >= this.minPartSize) {
|
||||
await this._uploadBuffer();
|
||||
this.buffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
async _finalize(err) {
|
||||
try {
|
||||
if (this.buffer.length > 0) {
|
||||
await this._uploadBuffer();
|
||||
}
|
||||
|
||||
const completeMultipartUploadCommand = new CompleteMultipartUploadCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: this.objectKey,
|
||||
MultipartUpload: {
|
||||
Parts: this.multipartETags.sort((a, b) => a.PartNumber - b.PartNumber),
|
||||
},
|
||||
UploadId: this.uploadId,
|
||||
});
|
||||
|
||||
await this.s3.send(completeMultipartUploadCommand);
|
||||
this.logger.info('Finished upload to S3');
|
||||
} catch (error) {
|
||||
this.logger.error('Error completing multipart upload:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async _final(callback) {
|
||||
try {
|
||||
await this._finalize();
|
||||
callback(null);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = S3MultipartUploadStream;
|
||||
97
lib/record/upload.js
Normal file
97
lib/record/upload.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const Account = require('../models/account');
|
||||
const Websocket = require('ws');
|
||||
const PCMToMP3Encoder = require('./encoder');
|
||||
const wav = require('wav');
|
||||
const { getUploader } = require('./utils');
|
||||
const { pipeline } = require('stream');
|
||||
|
||||
async function upload(logger, socket) {
|
||||
socket._recvInitialMetadata = false;
|
||||
socket.on('message', async function(data, isBinary) {
|
||||
try {
|
||||
if (!isBinary && !socket._recvInitialMetadata) {
|
||||
socket._recvInitialMetadata = true;
|
||||
logger.debug(`initial metadata: ${data}`);
|
||||
const obj = JSON.parse(data.toString());
|
||||
logger.info({ obj }, 'received JSON message from jambonz');
|
||||
const { sampleRate, accountSid, callSid, direction, from, to,
|
||||
callId, applicationSid, originatingSipIp, originatingSipTrunkName } = obj;
|
||||
const account = await Account.retrieve(accountSid);
|
||||
if (account && account.length && account[0].bucket_credential) {
|
||||
const obj = account[0].bucket_credential;
|
||||
// add tags to metadata
|
||||
const metadata = {
|
||||
accountSid,
|
||||
callSid,
|
||||
direction,
|
||||
from,
|
||||
to,
|
||||
callId,
|
||||
applicationSid,
|
||||
originatingSipIp,
|
||||
originatingSipTrunkName,
|
||||
sampleRate: `${sampleRate}`
|
||||
};
|
||||
if (obj.tags && obj.tags.length) {
|
||||
obj.tags.forEach((tag) => {
|
||||
metadata[tag.Key] = tag.Value;
|
||||
});
|
||||
}
|
||||
// create S3 path
|
||||
const day = new Date();
|
||||
let key = `${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
|
||||
key += `/${day.getDate().toString().padStart(2, '0')}/${callSid}.${account[0].record_format}`;
|
||||
|
||||
// Uploader
|
||||
const uploadStream = getUploader(key, metadata, obj, logger);
|
||||
if (!uploadStream) {
|
||||
logger.info('There is no available record uploader, close the socket.');
|
||||
socket.close();
|
||||
}
|
||||
|
||||
/**encoder */
|
||||
let encoder;
|
||||
if (account[0].record_format === 'wav') {
|
||||
encoder = new wav.Writer({ channels: 2, sampleRate, bitDepth: 16 });
|
||||
} else {
|
||||
// default is mp3
|
||||
encoder = new PCMToMP3Encoder({
|
||||
channels: 2,
|
||||
sampleRate: sampleRate,
|
||||
bitrate: 128
|
||||
}, logger);
|
||||
}
|
||||
|
||||
/* start streaming data */
|
||||
pipeline(
|
||||
Websocket.createWebSocketStream(socket),
|
||||
encoder,
|
||||
uploadStream,
|
||||
(error) => {
|
||||
if (error) {
|
||||
logger.error({ error }, 'pipeline error, cannot upload data to storage');
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
logger.info(`account ${accountSid} does not have any bucket credential, close the socket`);
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err, data }, 'error parsing message during connection');
|
||||
}
|
||||
});
|
||||
socket.on('error', function(err) {
|
||||
logger.error({ err }, 'record upload: error');
|
||||
});
|
||||
socket.on('close', (data) => {
|
||||
logger.info({ data }, 'record upload: close');
|
||||
});
|
||||
socket.on('end', function(err) {
|
||||
logger.error({ err }, 'record upload: socket closed from jambonz');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = upload;
|
||||
58
lib/record/utils.js
Normal file
58
lib/record/utils.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const AzureStorageUploadStream = require('./azure-storage');
|
||||
const GoogleStorageUploadStream = require('./google-storage');
|
||||
const S3MultipartUploadStream = require('./s3-multipart-upload-stream');
|
||||
|
||||
const getUploader = (key, metadata, bucket_credential, logger) => {
|
||||
const uploaderOpts = {
|
||||
bucketName: bucket_credential.name,
|
||||
Key: key,
|
||||
metadata
|
||||
};
|
||||
try {
|
||||
switch (bucket_credential.vendor) {
|
||||
case 'aws_s3':
|
||||
uploaderOpts.bucketCredential = {
|
||||
credentials: {
|
||||
accessKeyId: bucket_credential.access_key_id,
|
||||
secretAccessKey: bucket_credential.secret_access_key,
|
||||
},
|
||||
region: bucket_credential.region || 'us-east-1'
|
||||
};
|
||||
return new S3MultipartUploadStream(logger, uploaderOpts);
|
||||
case 's3_compatible':
|
||||
uploaderOpts.bucketCredential = {
|
||||
endpoint: bucket_credential.endpoint,
|
||||
credentials: {
|
||||
accessKeyId: bucket_credential.access_key_id,
|
||||
secretAccessKey: bucket_credential.secret_access_key,
|
||||
},
|
||||
region: 'us-east-1',
|
||||
forcePathStyle: true
|
||||
};
|
||||
return new S3MultipartUploadStream(logger, uploaderOpts);
|
||||
case 'google':
|
||||
const serviceKey = JSON.parse(bucket_credential.service_key);
|
||||
uploaderOpts.bucketCredential = {
|
||||
projectId: serviceKey.project_id,
|
||||
credentials: {
|
||||
client_email: serviceKey.client_email,
|
||||
private_key: serviceKey.private_key
|
||||
}
|
||||
};
|
||||
return new GoogleStorageUploadStream(logger, uploaderOpts);
|
||||
case 'azure':
|
||||
uploaderOpts.connection_string = bucket_credential.connection_string;
|
||||
return new AzureStorageUploadStream(logger, uploaderOpts);
|
||||
default:
|
||||
logger.error(`unknown bucket vendor: ${bucket_credential.vendor}`);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error creating uploader, vendor: ${bucket_credential.vendor}, reason: ${err.message}`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getUploader
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
const router = require('express').Router();
|
||||
const assert = require('assert');
|
||||
const request = require('request');
|
||||
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const Account = require('../../models/account');
|
||||
@@ -18,10 +19,13 @@ const {
|
||||
parseCallSid,
|
||||
enableSubspace,
|
||||
disableSubspace,
|
||||
parseVoipCarrierSid
|
||||
parseVoipCarrierSid,
|
||||
hasValue,
|
||||
} = require('./utils');
|
||||
const short = require('short-uuid');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const { encrypt, obscureBucketCredentialsSensitiveData, isObscureKey } = require('../../utils/encrypt-decrypt');
|
||||
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
|
||||
const translator = short();
|
||||
|
||||
let idx = 0;
|
||||
@@ -38,21 +42,15 @@ const getFsUrl = async(logger, retrieveSet, setName) => {
|
||||
logger.info('No available feature servers to handle createCall API request');
|
||||
return ;
|
||||
}
|
||||
const ip = stripPort(fs[idx++ % fs.length]);
|
||||
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
|
||||
return `http://${ip}:3000/v1/createCall`;
|
||||
const f = fs[idx++ % fs.length];
|
||||
logger.debug({fs}, `feature servers available for createCall API request, selecting ${f}`);
|
||||
return `${f}/v1/createCall`;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
|
||||
}
|
||||
};
|
||||
|
||||
const stripPort = (hostport) => {
|
||||
const arr = /^(.*):(.*)$/.exec(hostport);
|
||||
if (arr) return arr[1];
|
||||
return hostport;
|
||||
};
|
||||
|
||||
const validateUpdateForCarrier = async(req, account_sid) => {
|
||||
const validateRequest = async(req, account_sid) => {
|
||||
try {
|
||||
if (req.user.hasScope('admin')) {
|
||||
return;
|
||||
@@ -63,7 +61,7 @@ const validateUpdateForCarrier = async(req, account_sid) => {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions to update account');
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
@@ -75,7 +73,7 @@ const validateUpdateForCarrier = async(req, account_sid) => {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions to update account');
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -89,10 +87,12 @@ router.use('/:sid/Charges', hasAccountPermissions, require('./charges'));
|
||||
router.use('/:sid/SipRealms', hasAccountPermissions, require('./sip-realm'));
|
||||
router.use('/:sid/PredefinedCarriers', hasAccountPermissions, require('./add-from-predefined-carrier'));
|
||||
router.use('/:sid/Limits', hasAccountPermissions, require('./limits'));
|
||||
router.use('/:sid/TtsCache', hasAccountPermissions, require('./tts-cache'));
|
||||
router.get('/:sid/Applications', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const results = await Application.retrieveAll(null, account_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
@@ -103,6 +103,7 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const results = await VoipCarrier.retrieveAll(account_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
@@ -115,7 +116,7 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
|
||||
try {
|
||||
const sid = parseVoipCarrierSid(req);
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateUpdateForCarrier(req, account_sid);
|
||||
await validateRequest(req, account_sid);
|
||||
|
||||
const rowsAffected = await VoipCarrier.update(sid, req.body);
|
||||
if (rowsAffected === 0) {
|
||||
@@ -133,6 +134,8 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
|
||||
const payload = req.body;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
|
||||
logger.debug({payload}, 'POST /:sid/VoipCarriers');
|
||||
const uuid = await VoipCarrier.make({
|
||||
account_sid,
|
||||
@@ -144,6 +147,88 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid/RegisteredSipUsers', async(req, res) => {
|
||||
const {logger, registrar} = req.app.locals;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const result = await Account.retrieve(account_sid);
|
||||
if (!result || result.length === 0) {
|
||||
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
|
||||
}
|
||||
if (!result[0].sip_realm) {
|
||||
throw new DbErrorBadRequest('account does not have sip_realm configuration');
|
||||
}
|
||||
const users = await registrar.getRegisteredUsersForRealm(result[0].sip_realm);
|
||||
res.status(200).json(users.map((u) => `${u}@${result[0].sip_realm}`));
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:sid/RegisteredSipUsers', async(req, res) => {
|
||||
const {logger, registrar} = req.app.locals;
|
||||
const users = req.body;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const result = await Account.retrieve(account_sid);
|
||||
if (!result || result.length === 0) {
|
||||
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
|
||||
}
|
||||
if (!result[0].sip_realm) {
|
||||
throw new DbErrorBadRequest('account does not have sip_realm configuration');
|
||||
}
|
||||
if (!users || !Array.isArray(users) || users.length === 0) {
|
||||
return res.status(200).json(await registrar.getRegisteredUsersDetailsForRealm(result[0].sip_realm));
|
||||
}
|
||||
const ret = [];
|
||||
for (const u of users) {
|
||||
const user = await registrar.query(`${u}@${result[0].sip_realm}`) || {
|
||||
name: u,
|
||||
contact: null,
|
||||
expiryTime: 0,
|
||||
protocol: null
|
||||
};
|
||||
ret.push({
|
||||
name: u,
|
||||
...user,
|
||||
registered_status: user.expiryTime > 0 ? 'active' : 'inactive',
|
||||
});
|
||||
}
|
||||
res.status(200).json(ret);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid/RegisteredSipUsers/:client', async(req, res) => {
|
||||
const {logger, registrar, lookupClientByAccountAndUsername} = req.app.locals;
|
||||
const client = req.params.client;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const result = await Account.retrieve(account_sid);
|
||||
if (!result || result.length === 0) {
|
||||
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
|
||||
}
|
||||
const user = await registrar.query(`${client}@${result[0].sip_realm}`);
|
||||
const [clientDb] = await lookupClientByAccountAndUsername(account_sid, client);
|
||||
res.status(200).json({
|
||||
name: client,
|
||||
contact: user ? user.contact : null,
|
||||
expiryTime: user ? user.expiryTime : 0,
|
||||
protocol: user ? user.protocol : null,
|
||||
allow_direct_app_calling: clientDb ? clientDb.allow_direct_app_calling : 0,
|
||||
allow_direct_queue_calling: clientDb ? clientDb.allow_direct_queue_calling : 0,
|
||||
allow_direct_user_calling: clientDb ? clientDb.allow_direct_user_calling : 0,
|
||||
registered_status: user ? 'active' : 'inactive'
|
||||
});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
function coerceNumbers(callInfo) {
|
||||
if (Array.isArray(callInfo)) {
|
||||
return callInfo.map((ci) => {
|
||||
@@ -174,11 +259,16 @@ function validateUpdateCall(opts) {
|
||||
'child_call_hook',
|
||||
'call_status',
|
||||
'listen_status',
|
||||
'transcribe_status',
|
||||
'conf_hold_status',
|
||||
'conf_mute_status',
|
||||
'mute_status',
|
||||
'sip_request',
|
||||
'record'
|
||||
'record',
|
||||
'tag',
|
||||
'dtmf',
|
||||
'conferenceParticipantAction',
|
||||
'dub'
|
||||
]
|
||||
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
|
||||
|
||||
@@ -214,15 +304,34 @@ function validateUpdateCall(opts) {
|
||||
throw new DbErrorBadRequest('invalid conf_mute_status');
|
||||
}
|
||||
if (opts.sip_request &&
|
||||
(!opts.sip_request.method && !opts.sip_request.content_type || !opts.sip_request.content_type)) {
|
||||
throw new DbErrorBadRequest('sip_request requires content_type and content properties');
|
||||
(!opts.sip_request.method || !opts.sip_request.content_type || !opts.sip_request.content)) {
|
||||
throw new DbErrorBadRequest('sip_request requires method, content_type and content properties');
|
||||
}
|
||||
if (opts.record && !opts.record.action) {
|
||||
throw new DbErrorBadRequest('record requires action property');
|
||||
}
|
||||
if (opts.dtmf && !opts.dtmf.digit) {
|
||||
throw new DbErrorBadRequest('invalid dtmf');
|
||||
}
|
||||
if ('startCallRecording' === opts.record?.action && !opts.record.siprecServerURL) {
|
||||
throw new DbErrorBadRequest('record requires siprecServerURL property when starting recording');
|
||||
}
|
||||
if (opts.tag && (typeof opts.tag !== 'object' || Array.isArray(opts.tag) || opts.tag === null)) {
|
||||
throw new DbErrorBadRequest('invalid tag data');
|
||||
}
|
||||
if (opts.conferenceParticipantAction) {
|
||||
if (!['tag', 'untag', 'coach', 'uncoach', 'mute', 'unmute', 'hold', 'unhold']
|
||||
.includes(opts.conferenceParticipantAction.action)) {
|
||||
throw new DbErrorBadRequest(
|
||||
`conferenceParticipantAction invalid action property ${opts.conferenceParticipantAction.action}`);
|
||||
}
|
||||
if ('tag' == opts.conferenceParticipantAction.action && !opts.conferenceParticipantAction.tag) {
|
||||
throw new DbErrorBadRequest('conferenceParticipantAction requires tag property when action is \'tag\'');
|
||||
}
|
||||
if ('coach' == opts.conferenceParticipantAction.action && !opts.conferenceParticipantAction.tag) {
|
||||
throw new DbErrorBadRequest('conferenceParticipantAction requires tag property when action is \'coach\'');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateTo(to) {
|
||||
@@ -259,6 +368,7 @@ async function validateCreateCall(logger, sid, req) {
|
||||
const application = await lookupAppBySid(obj.application_sid);
|
||||
Object.assign(obj, {
|
||||
call_hook: application.call_hook,
|
||||
app_json: application.app_json,
|
||||
call_status_hook: application.call_status_hook,
|
||||
speech_synthesis_vendor: application.speech_synthesis_vendor,
|
||||
speech_synthesis_language: application.speech_synthesis_language,
|
||||
@@ -343,7 +453,7 @@ async function validateCreateMessage(logger, sid, req) {
|
||||
async function validateAdd(req) {
|
||||
/* account-level token can not be used to add accounts */
|
||||
if (req.user.hasAccountAuth) {
|
||||
throw new DbErrorUnprocessableRequest('insufficient permissions to create accounts');
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
if (req.user.hasServiceProviderAuth && req.user.service_provider_sid) {
|
||||
/* service providers can only create accounts under themselves */
|
||||
@@ -365,7 +475,7 @@ async function validateAdd(req) {
|
||||
|
||||
async function validateUpdate(req, sid) {
|
||||
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
|
||||
throw new DbErrorUnprocessableRequest('insufficient privileges to update this account');
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
if (req.user.hasAccountAuth && req.body.sip_realm) {
|
||||
throw new DbErrorBadRequest('use POST /Accounts/:sid/sip_realm/:realm to set or change the sip realm');
|
||||
@@ -377,7 +487,7 @@ async function validateUpdate(req, sid) {
|
||||
throw new DbErrorBadRequest(`account not found for sid ${sid}`);
|
||||
}
|
||||
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot update account from different service provider');
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
if (req.user.hasScope('admin')) {
|
||||
@@ -397,7 +507,7 @@ async function validateDelete(req, sid) {
|
||||
if (req.user.service_provider_sid && !req.user.hasScope('admin')) {
|
||||
const result = await Account.retrieve(sid);
|
||||
if (result[0].service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete account from different service provider');
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,10 +554,14 @@ router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const results = await Account.retrieve(account_sid, service_provider_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
return res.status(200).json(results[0]);
|
||||
const [result] = await Account.retrieve(account_sid, service_provider_sid) || [];
|
||||
if (!result) return res.status(404).end();
|
||||
|
||||
result.bucket_credential = obscureBucketCredentialsSensitiveData(result.bucket_credential);
|
||||
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -458,6 +572,7 @@ router.get('/:sid/WebhookSecret', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const results = await Account.retrieve(account_sid, service_provider_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
@@ -528,6 +643,72 @@ router.delete('/:sid/SubspaceTeleport', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
function encryptBucketCredential(obj, storedCredentials = {}) {
|
||||
if (!hasValue(obj?.bucket_credential)) return;
|
||||
const {
|
||||
vendor,
|
||||
region,
|
||||
name,
|
||||
access_key_id,
|
||||
tags,
|
||||
endpoint
|
||||
} = obj.bucket_credential;
|
||||
let {
|
||||
secret_access_key,
|
||||
service_key,
|
||||
connection_string
|
||||
} = obj.bucket_credential;
|
||||
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
assert(access_key_id, 'invalid aws S3 bucket credential: access_key_id is required');
|
||||
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
|
||||
assert(name, 'invalid aws bucket name: name is required');
|
||||
assert(region, 'invalid aws bucket region: region is required');
|
||||
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
|
||||
secret_access_key = storedCredentials.secret_access_key;
|
||||
}
|
||||
const awsData = JSON.stringify({vendor, region, name, access_key_id,
|
||||
secret_access_key, tags});
|
||||
obj.bucket_credential = encrypt(awsData);
|
||||
break;
|
||||
case 's3_compatible':
|
||||
assert(access_key_id, 'invalid aws S3 bucket credential: access_key_id is required');
|
||||
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
|
||||
assert(name, 'invalid aws bucket name: name is required');
|
||||
assert(endpoint, 'invalid endpoint uri: endpoint is required');
|
||||
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
|
||||
secret_access_key = storedCredentials.secret_access_key;
|
||||
}
|
||||
const s3Data = JSON.stringify({vendor, endpoint, name, access_key_id,
|
||||
secret_access_key, tags});
|
||||
obj.bucket_credential = encrypt(s3Data);
|
||||
break;
|
||||
case 'google':
|
||||
assert(service_key, 'invalid google cloud storage credential: service_key is required');
|
||||
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
|
||||
service_key = storedCredentials.service_key;
|
||||
}
|
||||
const googleData = JSON.stringify({vendor, name, service_key, tags});
|
||||
obj.bucket_credential = encrypt(googleData);
|
||||
break;
|
||||
case 'azure':
|
||||
assert(name, 'invalid azure container name: name is required');
|
||||
assert(connection_string, 'invalid azure cloud storage credential: connection_string is required');
|
||||
if (isObscureKey(obj.bucket_credential) && hasValue(storedCredentials)) {
|
||||
connection_string = storedCredentials.connection_string;
|
||||
}
|
||||
const azureData = JSON.stringify({vendor, name, connection_string, tags});
|
||||
obj.bucket_credential = encrypt(azureData);
|
||||
break;
|
||||
case 'none':
|
||||
obj.bucket_credential = null;
|
||||
break;
|
||||
default:
|
||||
throw new DbErrorBadRequest(`unknown storage vendor: ${vendor}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update
|
||||
*/
|
||||
@@ -535,6 +716,7 @@ router.put('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const sid = parseAccountSid(req);
|
||||
await validateRequest(req, sid);
|
||||
|
||||
// create webhooks if provided
|
||||
const obj = Object.assign({}, req.body);
|
||||
@@ -573,6 +755,18 @@ router.put('/:sid', async(req, res) => {
|
||||
delete obj.registration_hook;
|
||||
delete obj.queue_event_hook;
|
||||
|
||||
let storedBucketCredentials = {};
|
||||
if (isObscureKey(obj?.bucket_credential)) {
|
||||
const [account] = await Account.retrieve(sid) || [];
|
||||
/* to avoid overwriting valid credentials with the obscured secret,
|
||||
* that the frontend might send, we pass the stored account bucket credentials
|
||||
* in the case it is a obscured key, we replace it with the stored one
|
||||
*/
|
||||
storedBucketCredentials = account.bucket_credential;
|
||||
}
|
||||
|
||||
encryptBucketCredential(obj, storedBucketCredentials);
|
||||
|
||||
const rowsAffected = await Account.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
@@ -601,6 +795,7 @@ router.delete('/:sid', async(req, res) => {
|
||||
|
||||
try {
|
||||
const sid = parseAccountSid(req);
|
||||
await validateRequest(req, sid);
|
||||
await validateDelete(req, sid);
|
||||
|
||||
const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid);
|
||||
@@ -664,6 +859,51 @@ account_subscriptions WHERE account_sid = ?)
|
||||
}
|
||||
});
|
||||
|
||||
/* Test Bucket credential Keys */
|
||||
router.post('/:sid/BucketCredentialTest', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
/* if the req.body bucket credentials contain an obscured key, replace with stored account.bucket_credential */
|
||||
if (isObscureKey(req.body)) {
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const [account] = await Account.retrieve(account_sid, service_provider_sid) || [];
|
||||
if (!account) return res.status(404).end();
|
||||
req.body = account.bucket_credential;
|
||||
}
|
||||
const {vendor, name, region, access_key_id, secret_access_key, service_key, connection_string, endpoint} = req.body;
|
||||
const ret = {
|
||||
status: 'not tested'
|
||||
};
|
||||
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
await testS3Storage(logger, {vendor, name, region, access_key_id, secret_access_key});
|
||||
ret.status = 'ok';
|
||||
break;
|
||||
case 's3_compatible':
|
||||
await testS3Storage(logger, {vendor, name, endpoint, access_key_id, secret_access_key});
|
||||
ret.status = 'ok';
|
||||
break;
|
||||
case 'google':
|
||||
await testGoogleStorage(logger, {vendor, name, service_key});
|
||||
ret.status = 'ok';
|
||||
break;
|
||||
case 'azure':
|
||||
await testAzureStorage(logger, {vendor, name, connection_string});
|
||||
ret.status = 'ok';
|
||||
break;
|
||||
default:
|
||||
throw new DbErrorBadRequest(`Does not support test for ${vendor}`);
|
||||
}
|
||||
return res.status(200).json(ret);
|
||||
}
|
||||
catch (err) {
|
||||
return res.status(200).json({status: 'failed', reason: err.message});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve account level api keys
|
||||
*/
|
||||
@@ -672,6 +912,8 @@ router.get('/:sid/ApiKeys', async(req, res) => {
|
||||
|
||||
try {
|
||||
const sid = parseAccountSid(req);
|
||||
await validateRequest(req, sid);
|
||||
|
||||
const results = await ApiKey.retrieveAll(sid);
|
||||
res.status(200).json(results);
|
||||
updateLastUsed(logger, sid, req).catch((err) => {});
|
||||
@@ -685,7 +927,7 @@ router.get('/:sid/ApiKeys', async(req, res) => {
|
||||
*/
|
||||
router.post('/:sid/Calls', async(req, res) => {
|
||||
const {retrieveSet, logger} = req.app.locals;
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
|
||||
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
|
||||
|
||||
if (!serviceUrl) {
|
||||
@@ -694,6 +936,8 @@ router.post('/:sid/Calls', async(req, res) => {
|
||||
|
||||
try {
|
||||
const sid = parseAccountSid(req);
|
||||
await validateRequest(req, sid);
|
||||
|
||||
await validateCreateCall(logger, sid, req);
|
||||
updateLastUsed(logger, sid, req).catch((err) => {});
|
||||
request({
|
||||
@@ -724,10 +968,17 @@ router.post('/:sid/Calls', async(req, res) => {
|
||||
*/
|
||||
router.get('/:sid/Calls', async(req, res) => {
|
||||
const {logger, listCalls} = req.app.locals;
|
||||
|
||||
const {direction, from, to, callStatus} = req.query || {};
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
const calls = await listCalls(accountSid);
|
||||
await validateRequest(req, accountSid);
|
||||
const calls = await listCalls({
|
||||
accountSid,
|
||||
direction,
|
||||
from,
|
||||
to,
|
||||
callStatus
|
||||
});
|
||||
logger.debug(`retrieved ${calls.length} calls for account sid ${accountSid}`);
|
||||
res.status(200).json(coerceNumbers(snakeCase(calls)));
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
@@ -744,6 +995,7 @@ router.get('/:sid/Calls/:callSid', async(req, res) => {
|
||||
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const callSid = parseCallSid(req);
|
||||
const callInfo = await retrieveCall(accountSid, callSid);
|
||||
if (callInfo) {
|
||||
@@ -768,6 +1020,7 @@ router.delete('/:sid/Calls/:callSid', async(req, res) => {
|
||||
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const callSid = parseCallSid(req);
|
||||
const result = await deleteCall(accountSid, callSid);
|
||||
if (result) {
|
||||
@@ -792,6 +1045,7 @@ const updateCall = async(req, res) => {
|
||||
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const callSid = parseCallSid(req);
|
||||
validateUpdateCall(req.body);
|
||||
const call = await retrieveCall(accountSid, callSid);
|
||||
@@ -832,7 +1086,9 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||
await validateRequest(req, account_sid);
|
||||
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
|
||||
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
|
||||
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
|
||||
await validateCreateMessage(logger, account_sid, req);
|
||||
@@ -865,4 +1121,39 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve info for a group of queues under an account
|
||||
*/
|
||||
router.get('/:sid/Queues', async(req, res) => {
|
||||
const {logger, listSortedSets} = req.app.locals;
|
||||
const { search } = req.query || {};
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const queues = search ? await listSortedSets(accountSid, search) : await listSortedSets(accountSid);
|
||||
logger.debug(`retrieved ${queues.length} queues for account sid ${accountSid}`);
|
||||
res.status(200).json(queues);
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve info for a list of conferences under an account
|
||||
*/
|
||||
router.get('/:sid/Conferences', async(req, res) => {
|
||||
const {logger, listConferences} = req.app.locals;
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const conferences = await listConferences(accountSid);
|
||||
logger.debug(`retrieved ${conferences.length} queues for account sid ${accountSid}`);
|
||||
res.status(200).json(conferences.map((c) => c.split(':').pop()));
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const router = require('express').Router();
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
|
||||
const Application = require('../../models/application');
|
||||
const Account = require('../../models/account');
|
||||
const Webhook = require('../../models/webhook');
|
||||
@@ -13,6 +13,36 @@ const preconditions = {
|
||||
'update': validateUpdate
|
||||
};
|
||||
|
||||
const validateRequest = async(req, account_sid) => {
|
||||
try {
|
||||
if (req.user.hasScope('admin')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
if (account_sid === req.user.account_sid) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
const [r] = await promisePool.execute(
|
||||
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]
|
||||
);
|
||||
|
||||
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/* only user-level tokens can add applications */
|
||||
async function validateAdd(req) {
|
||||
if (req.user.account_sid) {
|
||||
@@ -23,7 +53,7 @@ async function validateAdd(req) {
|
||||
if (!req.body.account_sid) throw new DbErrorBadRequest('missing required field: \'account_sid\'');
|
||||
const result = await Account.retrieve(req.body.account_sid, req.user.service_provider_sid);
|
||||
if (result.length === 0) {
|
||||
throw new DbErrorBadRequest('insufficient privileges to create an application under the specified account');
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
|
||||
@@ -35,12 +65,26 @@ async function validateAdd(req) {
|
||||
}
|
||||
|
||||
async function validateUpdate(req, sid) {
|
||||
if (req.user.account_sid) {
|
||||
const app = await Application.retrieve(sid);
|
||||
if (!app || !app.length || app[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
|
||||
const app = await Application.retrieve(sid);
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (!app || 0 === app.length || app[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
const [r] = await promisePool.execute(
|
||||
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [app[0].account_sid]
|
||||
);
|
||||
|
||||
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
|
||||
|
||||
if (req.body.call_hook && typeof req.body.call_hook !== 'object') {
|
||||
throw new DbErrorBadRequest('\'call_hook\' must be an object when updating an application');
|
||||
}
|
||||
@@ -50,13 +94,24 @@ async function validateUpdate(req, sid) {
|
||||
}
|
||||
|
||||
async function validateDelete(req, sid) {
|
||||
const result = await Application.retrieve(sid);
|
||||
if (req.user.hasAccountAuth) {
|
||||
const result = await Application.retrieve(sid);
|
||||
if (!result || 0 === result.length) throw new DbErrorBadRequest('application does not exist');
|
||||
if (result[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete application owned by a different account');
|
||||
throw new DbErrorUnprocessableRequest('insufficient permissions');
|
||||
}
|
||||
}
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
const [r] = await promisePool.execute(
|
||||
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [result[0].account_sid]
|
||||
);
|
||||
|
||||
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
const assignedPhoneNumbers = await Application.getForeignKeyReferences('phone_numbers.application_sid', sid);
|
||||
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete application with phone numbers');
|
||||
}
|
||||
@@ -101,7 +156,8 @@ router.get('/', async(req, res) => {
|
||||
try {
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const results = await Application.retrieveAll(service_provider_sid, account_sid);
|
||||
const name = req.query.name;
|
||||
const results = await Application.retrieveAll(service_provider_sid, account_sid, name);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -117,6 +173,7 @@ router.get('/:sid', async(req, res) => {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const results = await Application.retrieve(application_sid, service_provider_sid, account_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
await validateRequest(req, results[0].account_sid);
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
88
lib/routes/api/clients.js
Normal file
88
lib/routes/api/clients.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const router = require('express').Router();
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
const Client = require('../../models/client');
|
||||
const Account = require('../../models/account');
|
||||
const { DbErrorBadRequest, DbErrorForbidden } = require('../../utils/errors');
|
||||
const { encrypt, decrypt, obscureKey } = require('../../utils/encrypt-decrypt');
|
||||
|
||||
const commonCheck = async(req) => {
|
||||
if (req.user.hasAccountAuth) {
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
} else if (req.user.hasServiceProviderAuth && req.body.account_sid) {
|
||||
const accounts = await Account.retrieve(req.body.account_sid, req.user.service_provider_sid);
|
||||
if (accounts.length === 0) {
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.password) {
|
||||
req.body.password = encrypt(req.body.password);
|
||||
}
|
||||
};
|
||||
|
||||
const validateAdd = async(req) => {
|
||||
await commonCheck(req);
|
||||
|
||||
const clients = await Client.retrieveByAccountSidAndUserName(req.body.account_sid, req.body.username);
|
||||
if (clients.length) {
|
||||
throw new DbErrorBadRequest('the client\'s username already exists');
|
||||
}
|
||||
};
|
||||
|
||||
const validateUpdate = async(req, sid) => {
|
||||
await commonCheck(req);
|
||||
|
||||
const clients = await Client.retrieveByAccountSidAndUserName(req.body.account_sid, req.body.username);
|
||||
if (clients.length && clients[0].client_sid !== sid) {
|
||||
throw new DbErrorBadRequest('the client\'s username already exists');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const preconditions = {
|
||||
add: validateAdd,
|
||||
update: validateUpdate,
|
||||
};
|
||||
|
||||
decorate(router, Client, ['add', 'update', 'delete'], preconditions);
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = req.user.hasAdminAuth ?
|
||||
await Client.retrieveAll() : req.user.hasAccountAuth ?
|
||||
await Client.retrieveAllByAccountSid(req.user.hasAccountAuth ? req.user.account_sid : null) :
|
||||
await Client.retrieveAllByServiceProviderSid(req.user.service_provider_sid);
|
||||
const ret = results.map((c) => {
|
||||
c.password = obscureKey(decrypt(c.password), 1);
|
||||
return c;
|
||||
});
|
||||
res.status(200).json(ret);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await Client.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const client = results[0];
|
||||
client.password = obscureKey(decrypt(client.password), 1);
|
||||
if (req.user.hasAccountAuth && client.account_sid !== req.user.account_sid) {
|
||||
return res.sendStatus(404);
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
const accounts = await Account.retrieve(client.account_sid, req.user.service_provider_sid);
|
||||
if (!accounts.length) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
}
|
||||
return res.status(200).json(client);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -6,6 +6,7 @@ const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
const {cacheClient} = require('../../helpers');
|
||||
const sysError = require('../error');
|
||||
const assert = require('assert');
|
||||
const sql = `SELECT * from users user
|
||||
LEFT JOIN accounts AS acc
|
||||
ON acc.account_sid = user.account_sid
|
||||
@@ -26,7 +27,8 @@ function createOauthEmailText(provider) {
|
||||
}
|
||||
|
||||
function createResetEmailText(link) {
|
||||
const baseUrl = 'http://localhost:3001';
|
||||
assert(process.env.JAMBONZ_BASE_URL, 'process.env.JAMBONZ_BASE_URL is missing');
|
||||
const baseUrl = process.env.JAMBONZ_BASE_URL;
|
||||
|
||||
return `Hi there!
|
||||
|
||||
@@ -46,7 +48,7 @@ function createResetEmailText(link) {
|
||||
router.post('/', async(req, res) => {
|
||||
const {logger, addKey} = req.app.locals;
|
||||
const {email} = req.body;
|
||||
const {user_sid} = req.user;
|
||||
|
||||
let obj;
|
||||
try {
|
||||
if (!email || !validateEmail(email)) {
|
||||
@@ -55,11 +57,16 @@ router.post('/', async(req, res) => {
|
||||
|
||||
const [r] = await promisePool.query({sql, nestTables: true}, email);
|
||||
if (0 === r.length) {
|
||||
return res.status(400).json({error: 'email does not exist'});
|
||||
logger.info('user not found');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
}
|
||||
obj = r[0];
|
||||
if (!obj.acc.is_active) {
|
||||
return res.status(400).json({error: 'you may not reset the password of an inactive account'});
|
||||
if (!obj.user.is_active) {
|
||||
logger.info(obj.user.name, 'user is inactive');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
} else if (obj.acc.account_sid !== null && !obj.acc.is_active) {
|
||||
logger.info(obj.acc.account_sid, 'account is inactive');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
}
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
@@ -81,7 +88,7 @@ router.post('/', async(req, res) => {
|
||||
emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link));
|
||||
}
|
||||
|
||||
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
|
||||
const redisKey = cacheClient.generateRedisKey('jwt', obj.user.user_sid, 'v2');
|
||||
await cacheClient.delete(redisKey);
|
||||
});
|
||||
|
||||
|
||||
137
lib/routes/api/google-custom-voices.js
Normal file
137
lib/routes/api/google-custom-voices.js
Normal file
@@ -0,0 +1,137 @@
|
||||
const router = require('express').Router();
|
||||
const GoogleCustomVoice = require('../../models/google-custom-voice');
|
||||
const SpeechCredential = require('../../models/speech-credential');
|
||||
const decorate = require('./decorate');
|
||||
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
|
||||
const sysError = require('../error');
|
||||
const multer = require('multer');
|
||||
const upload = multer({ dest: '/tmp/csv/' });
|
||||
const fs = require('fs');
|
||||
|
||||
const validateCredentialPermission = async(req) => {
|
||||
const credential = await SpeechCredential.retrieve(req.body.speech_credential_sid);
|
||||
if (!credential || credential.length === 0) {
|
||||
throw new DbErrorBadRequest('Invalid speech_credential_sid');
|
||||
}
|
||||
const cred = credential[0];
|
||||
|
||||
if (req.user.hasServiceProviderAuth && cred.service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
if (req.user.hasAccountAuth && cred.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
};
|
||||
|
||||
const validateAdd = async(req) => {
|
||||
if (!req.body.speech_credential_sid) {
|
||||
throw new DbErrorBadRequest('missing speech_credential_sid');
|
||||
}
|
||||
|
||||
await validateCredentialPermission(req);
|
||||
};
|
||||
|
||||
const validateUpdate = async(req) => {
|
||||
if (req.body.speech_credential_sid) {
|
||||
await validateCredentialPermission(req);
|
||||
}
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
add: validateAdd,
|
||||
update: validateUpdate,
|
||||
};
|
||||
|
||||
decorate(router, GoogleCustomVoice, ['add', 'retrieve', 'update', 'delete'], preconditions);
|
||||
|
||||
const voiceCloningKeySubString = (voice_cloning_key) => {
|
||||
return voice_cloning_key ? voice_cloning_key.substring(0, 100) + '...' : undefined;
|
||||
};
|
||||
|
||||
router.get('/: sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const {sid} = req.params;
|
||||
const account_sid = req.user.account_sid;
|
||||
const service_provider_sid = req.user.service_provider_sid;
|
||||
|
||||
const google_voice = await GoogleCustomVoice.retrieve(sid);
|
||||
google_voice.voice_cloning_key = voiceCloningKeySubString(google_voice.voice_cloning_key);
|
||||
if (!google_voice) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
if (req.user.hasScope('service_provider') && google_voice.service_provider_sid !== service_provider_sid ||
|
||||
req.user.hasScope('account') && google_voice.account_sid !== account_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
|
||||
return res.status(200).json(google_voice);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const account_sid = req.user.account_sid || req.query.account_sid;
|
||||
const service_provider_sid = req.user.service_provider_sid || req.query.service_provider_sid;
|
||||
const speech_credential_sid = req.query.speech_credential_sid;
|
||||
const label = req.query.label;
|
||||
try {
|
||||
let results = [];
|
||||
if (speech_credential_sid) {
|
||||
const [cred] = await SpeechCredential.retrieve(speech_credential_sid);
|
||||
if (!cred) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
if (account_sid && cred.account_sid && cred.account_sid !== account_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
if (service_provider_sid && cred.service_provider_sid && cred.service_provider_sid !== service_provider_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
results = await GoogleCustomVoice.retrieveAllBySpeechCredentialSid(speech_credential_sid);
|
||||
} else {
|
||||
if (!account_sid && !service_provider_sid) {
|
||||
throw new DbErrorBadRequest('missing account_sid or service_provider_sid in query parameters');
|
||||
}
|
||||
results = await GoogleCustomVoice.retrieveAllByLabel(service_provider_sid, account_sid, label);
|
||||
}
|
||||
res.status(200).json(results.map((r) => {
|
||||
r.voice_cloning_key = voiceCloningKeySubString(r.voice_cloning_key);
|
||||
return r;
|
||||
}));
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:sid/VoiceCloningKey', upload.single('file'), async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const {sid} = req.params;
|
||||
const account_sid = req.user.account_sid;
|
||||
const service_provider_sid = req.user.service_provider_sid;
|
||||
try {
|
||||
const google_voice = await GoogleCustomVoice.retrieve(sid);
|
||||
if (!google_voice) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
if (req.user.hasScope('service_provider') && google_voice.service_provider_sid !== service_provider_sid ||
|
||||
req.user.hasScope('account') && google_voice.account_sid !== account_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
|
||||
const voice_cloning_key = Buffer.from(fs.readFileSync(req.file.path)).toString();
|
||||
await GoogleCustomVoice.update(sid, {
|
||||
voice_cloning_key
|
||||
});
|
||||
fs.unlinkSync(req.file.path);
|
||||
return res.sendStatus(204);
|
||||
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -16,6 +16,8 @@ const isAdminScope = (req, res, next) => {
|
||||
// };
|
||||
|
||||
api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes'));
|
||||
api.use('/SystemInformation', isAdminScope, require('./system-information'));
|
||||
api.use('/TtsCache', isAdminScope, require('./tts-cache'));
|
||||
api.use('/ServiceProviders', require('./service-providers'));
|
||||
api.use('/VoipCarriers', require('./voip-carriers'));
|
||||
api.use('/Webhooks', require('./webhooks'));
|
||||
@@ -45,6 +47,13 @@ api.use('/Invoices', require('./invoices'));
|
||||
api.use('/InviteCodes', require('./invite-codes'));
|
||||
api.use('/PredefinedCarriers', require('./predefined-carriers'));
|
||||
api.use('/PasswordSettings', require('./password-settings'));
|
||||
// Least Cost Routing
|
||||
api.use('/Lcrs', require('./lcrs'));
|
||||
api.use('/LcrRoutes', require('./lcr-routes'));
|
||||
api.use('/LcrCarrierSetEntries', require('./lcr-carrier-set-entries'));
|
||||
api.use('/Clients', require('./clients'));
|
||||
// Google Custom Voices
|
||||
api.use('/GoogleCustomVoices', require('./google-custom-voices'));
|
||||
|
||||
// messaging
|
||||
api.use('/Smpps', require('./smpps')); // our smpp server info
|
||||
|
||||
65
lib/routes/api/lcr-carrier-set-entries.js
Normal file
65
lib/routes/api/lcr-carrier-set-entries.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const router = require('express').Router();
|
||||
const LcrCarrierSetEntry = require('../../models/lcr-carrier-set-entry');
|
||||
const LcrRoute = require('../../models/lcr-route');
|
||||
const decorate = require('./decorate');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const sysError = require('../error');
|
||||
|
||||
const validateAdd = async(req) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
if (!req.body.lcr_route_sid) {
|
||||
throw new DbErrorBadRequest('missing lcr_route_sid');
|
||||
}
|
||||
// check lcr_route_sid is exist
|
||||
const lcrRoute = await LcrRoute.retrieve(req.body.lcr_route_sid);
|
||||
if (lcrRoute.length === 0) {
|
||||
throw new DbErrorBadRequest('unknown lcr_route_sid');
|
||||
}
|
||||
// check voip_carrier_sid is exist
|
||||
if (!req.body.voip_carrier_sid) {
|
||||
throw new DbErrorBadRequest('missing voip_carrier_sid');
|
||||
}
|
||||
const carrier = await lookupCarrierBySid(req.body.voip_carrier_sid);
|
||||
if (!carrier) {
|
||||
throw new DbErrorBadRequest('unknown voip_carrier_sid');
|
||||
}
|
||||
};
|
||||
|
||||
const validateUpdate = async(req) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
if (req.body.lcr_route_sid) {
|
||||
const lcrRoute = await LcrRoute.retrieve(req.body.lcr_route_sid);
|
||||
if (lcrRoute.length === 0) {
|
||||
throw new DbErrorBadRequest('unknown lcr_route_sid');
|
||||
}
|
||||
}
|
||||
|
||||
// check voip_carrier_sid is exist
|
||||
if (req.body.voip_carrier_sid) {
|
||||
const carrier = await lookupCarrierBySid(req.body.voip_carrier_sid);
|
||||
if (!carrier) {
|
||||
throw new DbErrorBadRequest('unknown voip_carrier_sid');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
add: validateAdd,
|
||||
update: validateUpdate,
|
||||
};
|
||||
|
||||
decorate(router, LcrCarrierSetEntry, ['add', 'retrieve', 'update', 'delete'], preconditions);
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const lcr_route_sid = req.query.lcr_route_sid;
|
||||
try {
|
||||
const results = await LcrCarrierSetEntry.retrieveAllByLcrRouteSid(lcr_route_sid);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
96
lib/routes/api/lcr-routes.js
Normal file
96
lib/routes/api/lcr-routes.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const router = require('express').Router();
|
||||
const LcrRoute = require('../../models/lcr-route');
|
||||
const Lcr = require('../../models/lcr');
|
||||
const LcrCarrierSetEntry = require('../../models/lcr-carrier-set-entry');
|
||||
const decorate = require('./decorate');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const sysError = require('../error');
|
||||
|
||||
const validateAdd = async(req) => {
|
||||
// check if lcr sid is available
|
||||
if (!req.body.lcr_sid) {
|
||||
throw new DbErrorBadRequest('missing parameter lcr_sid');
|
||||
}
|
||||
|
||||
const lcr = await Lcr.retrieve(req.body.lcr_sid);
|
||||
if (lcr.length === 0) {
|
||||
throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
}
|
||||
};
|
||||
|
||||
const validateUpdate = async(req) => {
|
||||
if (req.body.lcr_sid) {
|
||||
const lcr = await Lcr.retrieve(req.body.lcr_sid);
|
||||
if (lcr.length === 0) {
|
||||
throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateDelete = async(req, sid) => {
|
||||
// delete all lcr carrier set entries
|
||||
await LcrCarrierSetEntry.deleteByLcrRouteSid(sid);
|
||||
};
|
||||
|
||||
const checkUserScope = async(req, lcr_sid) => {
|
||||
if (!lcr_sid) {
|
||||
throw new DbErrorBadRequest('missing lcr_sid');
|
||||
}
|
||||
|
||||
if (req.user.hasAdminAuth) return;
|
||||
|
||||
const lcrList = await Lcr.retrieve(lcr_sid);
|
||||
if (lcrList.length === 0) throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
const lcr = lcrList[0];
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (!lcr.account_sid || lcr.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
}
|
||||
}
|
||||
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
if (!lcr.service_provider_sid || lcr.service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
add: validateAdd,
|
||||
update: validateUpdate,
|
||||
delete: validateDelete,
|
||||
};
|
||||
|
||||
decorate(router, LcrRoute, ['add', 'update', 'delete'], preconditions);
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const lcr_sid = req.query.lcr_sid;
|
||||
try {
|
||||
await checkUserScope(req, lcr_sid);
|
||||
const results = await LcrRoute.retrieveAllByLcrSid(lcr_sid);
|
||||
for (const r of results) {
|
||||
r.lcr_carrier_set_entries = await LcrCarrierSetEntry.retrieveAllByLcrRouteSid(r.lcr_route_sid);
|
||||
}
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const lcr_route_sid = req.params.sid;
|
||||
try {
|
||||
const results = await LcrRoute.retrieve(lcr_route_sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const route = results[0];
|
||||
route.lcr_carrier_set_entries = await LcrCarrierSetEntry.retrieveAllByLcrRouteSid(route.lcr_route_sid);
|
||||
res.status(200).json(route);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
225
lib/routes/api/lcrs.js
Normal file
225
lib/routes/api/lcrs.js
Normal file
@@ -0,0 +1,225 @@
|
||||
const router = require('express').Router();
|
||||
const Lcr = require('../../models/lcr');
|
||||
const LcrCarrierSetEntry = require('../../models/lcr-carrier-set-entry');
|
||||
const LcrRoutes = require('../../models/lcr-route');
|
||||
const decorate = require('./decorate');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const sysError = require('../error');
|
||||
const ServiceProvider = require('../../models/service-provider');
|
||||
|
||||
const validateAssociatedTarget = async(req, sid) => {
|
||||
const {lookupAccountBySid} = req.app.locals;
|
||||
if (req.body.account_sid) {
|
||||
// Add only for account
|
||||
req.body.service_provider_sid = null;
|
||||
const account = await lookupAccountBySid(req.body.account_sid);
|
||||
if (!account) throw new DbErrorBadRequest('unknown account_sid');
|
||||
const lcr = await Lcr.retrieveAllByAccountSid(req.body.account_sid);
|
||||
if (lcr.length > 0 && (!sid || sid !== lcr[0].lcr_sid)) {
|
||||
throw new DbErrorBadRequest(`Account: ${account.name} already has an active call routing table.`);
|
||||
}
|
||||
} else if (req.body.service_provider_sid) {
|
||||
const serviceProviders = await ServiceProvider.retrieve(req.body.service_provider_sid);
|
||||
if (serviceProviders.length === 0) throw new DbErrorBadRequest('unknown service_provider_sid');
|
||||
const serviceProvider = serviceProviders[0];
|
||||
const lcr = await Lcr.retrieveAllByServiceProviderSid(req.body.service_provider_sid);
|
||||
if (lcr.length > 0 && (!sid || sid !== lcr[0].lcr_sid)) {
|
||||
throw new DbErrorBadRequest(`Service Provider: ${serviceProvider.name} already
|
||||
has an active call routing table.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateAdd = async(req) => {
|
||||
if (req.user.hasAccountAuth) {
|
||||
// Account just create LCR for himself
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
// SP just can create LCR for himself
|
||||
req.body.service_provider_sid = req.user.service_provider_sid;
|
||||
req.body.account_sid = null;
|
||||
}
|
||||
|
||||
await validateAssociatedTarget(req);
|
||||
// check if lcr_carrier_set_entry is available
|
||||
if (req.body.lcr_carrier_set_entry) {
|
||||
const e = await LcrCarrierSetEntry.retrieve(req.body.lcr_carrier_set_entry);
|
||||
if (e.length === 0) throw new DbErrorBadRequest('unknown lcr_carrier_set_entry');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const validateUserPermissionForExistingEntity = async(req, sid) => {
|
||||
const r = await Lcr.retrieve(sid);
|
||||
if (r.length === 0) {
|
||||
throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
}
|
||||
const lcr = r[0];
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (lcr.account_sid != req.user.account_sid) {
|
||||
throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
}
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
if (lcr.service_provider_sid != req.user.service_provider_sid) {
|
||||
throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateUpdate = async(req, sid) => {
|
||||
await validateUserPermissionForExistingEntity(req, sid);
|
||||
await validateAssociatedTarget(req, sid);
|
||||
};
|
||||
|
||||
const validateDelete = async(req, sid) => {
|
||||
if (req.user.hasAccountAuth) {
|
||||
/* can only delete Lcr for the user's account */
|
||||
const r = await Lcr.retrieve(sid);
|
||||
const lcr = r.length > 0 ? r[0] : null;
|
||||
if (!lcr || (req.user.account_sid && lcr.account_sid != req.user.account_sid)) {
|
||||
throw new DbErrorBadRequest('unknown lcr_sid');
|
||||
}
|
||||
}
|
||||
await Lcr.releaseDefaultEntry(sid);
|
||||
// fetch lcr route
|
||||
const lcr_routes = await LcrRoutes.retrieveAllByLcrSid(sid);
|
||||
// delete all lcr carrier set entries
|
||||
for (const e of lcr_routes) {
|
||||
await LcrCarrierSetEntry.deleteByLcrRouteSid(e.lcr_route_sid);
|
||||
}
|
||||
|
||||
// delete all lcr routes
|
||||
await LcrRoutes.deleteByLcrSid(sid);
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
add: validateAdd,
|
||||
update: validateUpdate,
|
||||
delete: validateDelete
|
||||
};
|
||||
|
||||
decorate(router, Lcr, ['add', 'update', 'delete'], preconditions);
|
||||
|
||||
const validateLcrBatchAdd = async(lcr_sid, body, lookupCarrierBySid) => {
|
||||
for (const lcr_route of body) {
|
||||
lcr_route.lcr_sid = lcr_sid;
|
||||
if (!lcr_route.lcr_carrier_set_entries || lcr_route.lcr_carrier_set_entries.length === 0) {
|
||||
throw new DbErrorBadRequest('Lcr Route batch process require lcr_carrier_set_entries');
|
||||
}
|
||||
for (const entry of lcr_route.lcr_carrier_set_entries) {
|
||||
// check voip_carrier_sid is exist
|
||||
if (!entry.voip_carrier_sid) {
|
||||
throw new DbErrorBadRequest('One of lcr_carrier_set_entries is missing voip_carrier_sid');
|
||||
}
|
||||
const carrier = await lookupCarrierBySid(entry.voip_carrier_sid);
|
||||
if (!carrier) {
|
||||
throw new DbErrorBadRequest('unknown voip_carrier_sid');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const addNewLcrRoute = async(lcr_route) => {
|
||||
const lcr_sid = lcr_route.lcr_sid;
|
||||
const lcr_carrier_set_entries = lcr_route.lcr_carrier_set_entries;
|
||||
delete lcr_route.lcr_carrier_set_entries;
|
||||
const lcr_route_sid = await LcrRoutes.make(lcr_route);
|
||||
for (const entry of lcr_carrier_set_entries) {
|
||||
entry.lcr_route_sid = lcr_route_sid;
|
||||
const lcr_carrier_set_entry_sid = await LcrCarrierSetEntry.make(entry);
|
||||
if (lcr_route.priority === 9999) {
|
||||
// this is default lcr set entry
|
||||
const [lcr] = await Lcr.retrieve(lcr_sid);
|
||||
if (lcr) {
|
||||
lcr.default_carrier_set_entry_sid = lcr_carrier_set_entry_sid;
|
||||
delete lcr.lcr_sid;
|
||||
await Lcr.update(lcr_sid, lcr);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
router.post('/:sid/Routes', async(req, res) => {
|
||||
const results = await Lcr.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const {logger, lookupCarrierBySid} = req.app.locals;
|
||||
try {
|
||||
const body = req.body;
|
||||
await validateLcrBatchAdd(req.params.sid, body, lookupCarrierBySid);
|
||||
for (const lcr_route of body) {
|
||||
await addNewLcrRoute(lcr_route, lookupCarrierBySid);
|
||||
}
|
||||
res.sendStatus(204);
|
||||
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:sid/Routes', async(req, res) => {
|
||||
const results = await Lcr.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const {logger, lookupCarrierBySid} = req.app.locals;
|
||||
try {
|
||||
const body = req.body;
|
||||
await validateLcrBatchAdd(req.params.sid, body, lookupCarrierBySid);
|
||||
for (const lcr_route of body) {
|
||||
if (lcr_route.lcr_route_sid) {
|
||||
const lcr_route_sid = lcr_route.lcr_route_sid;
|
||||
delete lcr_route.lcr_route_sid;
|
||||
const lcr_carrier_set_entries = lcr_route.lcr_carrier_set_entries;
|
||||
delete lcr_route.lcr_carrier_set_entries;
|
||||
await LcrRoutes.update(lcr_route_sid, lcr_route);
|
||||
for (const entry of lcr_carrier_set_entries) {
|
||||
const lcr_carrier_set_entry_sid = entry.lcr_carrier_set_entry_sid;
|
||||
delete entry.lcr_carrier_set_entry_sid;
|
||||
await LcrCarrierSetEntry.update(lcr_carrier_set_entry_sid, entry);
|
||||
}
|
||||
} else {
|
||||
// Route is not available yet, let create it now
|
||||
await addNewLcrRoute(lcr_route, lookupCarrierBySid);
|
||||
}
|
||||
}
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = req.user.hasAdminAuth ?
|
||||
await Lcr.retrieveAll() : req.user.hasAccountAuth ?
|
||||
await Lcr.retrieveAllByAccountSid(req.user.hasAccountAuth ? req.user.account_sid : null) :
|
||||
await Lcr.retrieveAllByServiceProviderSid(req.user.service_provider_sid);
|
||||
|
||||
for (const lcr of results) {
|
||||
lcr.number_routes = await LcrRoutes.countAllByLcrSid(lcr.lcr_sid);
|
||||
}
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await Lcr.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const lcr = results[0];
|
||||
if (req.user.hasAccountAuth && lcr.account_sid !== req.user.account_sid) {
|
||||
return res.sendStatus(404);
|
||||
} else if (req.user.hasServiceProviderAuth && lcr.service_provider_sid !== req.user.service_provider_sid) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
lcr.number_routes = await LcrRoutes.countAllByLcrSid(lcr.lcr_sid);
|
||||
return res.status(200).json(lcr);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -13,11 +13,9 @@ WHERE up.permission_sid = p.permission_sid
|
||||
AND up.user_sid = ?
|
||||
`;
|
||||
const retrieveSql = 'SELECT * from users where name = ?';
|
||||
const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL';
|
||||
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {logger, incrKey, retrieveKey} = req.app.locals;
|
||||
const {username, password} = req.body;
|
||||
if (!username || !password) {
|
||||
logger.info('Bad POST to /login is missing username or password');
|
||||
@@ -31,15 +29,30 @@ router.post('/', async(req, res) => {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
logger.info({r}, 'successfully retrieved user account');
|
||||
const isCorrect = await verifyPassword(r[0].hashed_password, password);
|
||||
if (!isCorrect) return res.sendStatus(403);
|
||||
const force_change = !!r[0].force_change;
|
||||
const [t] = await promisePool.query(tokenSql);
|
||||
if (t.length === 0) {
|
||||
logger.error('Database has no admin token provisioned...run reset_admin_password');
|
||||
return res.sendStatus(500);
|
||||
|
||||
const maxLoginAttempts = process.env.LOGIN_ATTEMPTS_MAX_RETRIES || 6;
|
||||
const loginAttempsBlocked = await retrieveKey(`login:${r[0].user_sid}`) >= maxLoginAttempts;
|
||||
|
||||
if (loginAttempsBlocked) {
|
||||
logger.info(`User ${r[0].user_sid} was blocked due to excessive login attempts with incorrect credentials.`);
|
||||
return res.status(403)
|
||||
.json({error: 'Maximum login attempts reached. Please try again later or reset your password.'});
|
||||
}
|
||||
|
||||
const isCorrect = await verifyPassword(r[0].hashed_password, password);
|
||||
if (!isCorrect) {
|
||||
const attempTime = process.env.LOGIN_ATTEMPTS_TIME || 1800;
|
||||
const newAttempt = await incrKey(`login:${r[0].user_sid}`, attempTime)
|
||||
.catch((err) => logger.error({err}, 'Error adding logging attempt to redis'));
|
||||
if (newAttempt >= maxLoginAttempts) {
|
||||
logger.info(`User ${r[0].user_sid} is now blocked due to excessive login attempts with incorrect credentials.`);
|
||||
return res.status(403)
|
||||
.json({error: `Maximum login attempts reached. Please try again in ${attempTime} seconds.`});
|
||||
}
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
const force_change = !!r[0].force_change;
|
||||
|
||||
const [p] = await promisePool.query(retrievePemissionsSql, r[0].user_sid);
|
||||
const permissions = p.map((x) => x.name);
|
||||
const obj = {user_sid: r[0].user_sid, scope: 'admin', force_change, permissions};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const router = require('express').Router();
|
||||
const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
|
||||
const PhoneNumber = require('../../models/phone-number');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const Account = require('../../models/account');
|
||||
const decorate = require('./decorate');
|
||||
const {promisePool} = require('../../db');
|
||||
const {e164} = require('../../utils/phone-number-utils');
|
||||
const preconditions = {
|
||||
'add': validateAdd,
|
||||
@@ -21,6 +23,10 @@ async function validateAdd(req) {
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
}
|
||||
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
req.body.service_provider_sid = req.user.service_provider_sid;
|
||||
}
|
||||
|
||||
if (!req.body.number) throw new DbErrorBadRequest('number is required');
|
||||
const formattedNumber = e164(req.body.number);
|
||||
req.body.number = formattedNumber;
|
||||
@@ -42,11 +48,11 @@ async function checkInUse(req, sid) {
|
||||
const phoneNumber = await PhoneNumber.retrieve(sid);
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account');
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
if (!req.user.hasAccountAuth && phoneNumber.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +64,23 @@ async function validateUpdate(req, sid) {
|
||||
const phoneNumber = await PhoneNumber.retrieve(sid);
|
||||
if (req.user.hasAccountAuth) {
|
||||
if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('cannot operate on a phone number that belongs to another account');
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
let service_provider_sid;
|
||||
|
||||
if (!phoneNumber[0].service_provider_sid) {
|
||||
const [r] = await Account.retrieve(phoneNumber[0].account_sid);
|
||||
service_provider_sid = r.service_provider_sid;
|
||||
} else {
|
||||
service_provider_sid = phoneNumber[0].service_provider_sid;
|
||||
}
|
||||
|
||||
if (phoneNumber && phoneNumber.length && service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
// TODO: if we are assigning to an account, verify it exists
|
||||
|
||||
// TODO: if we are assigning to an application, verify it is associated to the same account
|
||||
@@ -75,7 +94,9 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
|
||||
const results = req.user.hasServiceProviderAuth ?
|
||||
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid) :
|
||||
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -90,6 +111,18 @@ router.get('/:sid', async(req, res) => {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const results = await PhoneNumber.retrieve(sid, account_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
|
||||
if (req.user.hasServiceProviderAuth && results.length === 1) {
|
||||
const account_sid = results[0].account_sid;
|
||||
const [r] = await promisePool.execute(
|
||||
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]);
|
||||
if (r.length === 1 && r[0].service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
}
|
||||
if (req.user.hasAccountAuth && results.length > 1) {
|
||||
return res.status(200).json(results.filter((r) => r.phone_number_sid === sid)[0]);
|
||||
}
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
@@ -2,6 +2,17 @@ const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {getHomerApiKey, getHomerSipTrace, getHomerPcap} = require('../../utils/homer-utils');
|
||||
const {getJaegerTrace} = require('../../utils/jaeger-utils');
|
||||
const Account = require('../../models/account');
|
||||
const {
|
||||
getS3Object,
|
||||
getGoogleStorageObject,
|
||||
getAzureStorageObject,
|
||||
deleteS3Object,
|
||||
deleteGoogleStorageObject,
|
||||
deleteAzureStorageObject
|
||||
} = require('../../utils/storage-utils');
|
||||
|
||||
const parseAccountSid = (url) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(url);
|
||||
if (arr) return arr[1];
|
||||
@@ -18,7 +29,7 @@ router.get('/', async(req, res) => {
|
||||
logger.debug({opts: req.query}, 'GET /RecentCalls');
|
||||
const account_sid = parseAccountSid(req.originalUrl);
|
||||
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
|
||||
const {page, count, trunk, direction, days, answered, start, end} = req.query || {};
|
||||
const {page, count, trunk, direction, days, answered, start, end, filter} = req.query || {};
|
||||
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
|
||||
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
|
||||
|
||||
@@ -33,6 +44,7 @@ router.get('/', async(req, res) => {
|
||||
answered,
|
||||
start: days ? undefined : start,
|
||||
end: days ? undefined : end,
|
||||
filter
|
||||
});
|
||||
res.status(200).json(data);
|
||||
}
|
||||
@@ -47,6 +59,7 @@ router.get('/', async(req, res) => {
|
||||
answered,
|
||||
start: days ? undefined : start,
|
||||
end: days ? undefined : end,
|
||||
filter
|
||||
});
|
||||
res.status(200).json(data);
|
||||
}
|
||||
@@ -72,12 +85,12 @@ router.get('/:call_id', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:call_id/pcap', async(req, res) => {
|
||||
router.get('/:call_id/:method/pcap', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
try {
|
||||
const token = await getHomerApiKey(logger);
|
||||
if (!token) return res.sendStatus(400, {msg: 'getHomerApiKey: Failed to get Homer API token; check server config'});
|
||||
const stream = await getHomerPcap(logger, token, [req.params.call_id]);
|
||||
const stream = await getHomerPcap(logger, token, [req.params.call_id], req.params.method);
|
||||
if (!stream) {
|
||||
logger.info(`getHomerApiKey: unable to get sip traces from Homer for ${req.params.call_id}`);
|
||||
return res.sendStatus(404);
|
||||
@@ -93,4 +106,100 @@ router.get('/:call_id/pcap', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/trace/:trace_id', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const {trace_id} = req.params;
|
||||
try {
|
||||
const obj = await getJaegerTrace(logger, trace_id);
|
||||
if (!obj) {
|
||||
logger.info(`/RecentCalls: unable to get spans from jaeger for ${trace_id}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.status(200).json(obj.result);
|
||||
} catch (err) {
|
||||
logger.error({err}, `/RecentCalls error retrieving jaeger trace ${trace_id}`);
|
||||
res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const {call_sid, year, month, day, format} = req.params;
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req.originalUrl);
|
||||
const r = await Account.retrieve(account_sid);
|
||||
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
|
||||
const {bucket_credential} = r[0];
|
||||
const getOptions = {
|
||||
...bucket_credential,
|
||||
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
|
||||
};
|
||||
let stream;
|
||||
switch (bucket_credential.vendor) {
|
||||
case 'aws_s3':
|
||||
case 's3_compatible':
|
||||
stream = await getS3Object(logger, getOptions);
|
||||
break;
|
||||
case 'google':
|
||||
stream = await getGoogleStorageObject(logger, getOptions);
|
||||
break;
|
||||
case 'azure':
|
||||
stream = await getAzureStorageObject(logger, getOptions);
|
||||
break;
|
||||
default:
|
||||
logger.error(`There is no handler for fetching record from ${bucket_credential.vendor}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
res.set({
|
||||
'Content-Type': `audio/${format || 'mp3'}`
|
||||
});
|
||||
if (stream) {
|
||||
stream.pipe(res);
|
||||
} else {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err}, ` error retrieving recording ${call_sid}`);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const {call_sid, year, month, day, format} = req.params;
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req.originalUrl);
|
||||
const r = await Account.retrieve(account_sid);
|
||||
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
|
||||
const {bucket_credential} = r[0];
|
||||
|
||||
const deleteOptions = {
|
||||
...bucket_credential,
|
||||
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
|
||||
};
|
||||
|
||||
switch (bucket_credential.vendor) {
|
||||
case 'aws_s3':
|
||||
case 's3_compatible':
|
||||
await deleteS3Object(logger, deleteOptions);
|
||||
break;
|
||||
case 'google':
|
||||
await deleteGoogleStorageObject(logger, deleteOptions);
|
||||
break;
|
||||
case 'azure':
|
||||
await deleteAzureStorageObject(logger, deleteOptions);
|
||||
break;
|
||||
default:
|
||||
logger.error(`There is no handler for deleting record from ${bucket_credential.vendor}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
logger.error({err}, ` error deleting recording ${call_sid}`);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -3,7 +3,7 @@ const debug = require('debug')('jambonz:api-server');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {promisePool} = require('../../db');
|
||||
const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils');
|
||||
const {validateEmail} = require('../../utils/email-utils');
|
||||
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
|
||||
const {cacheClient} = require('../../helpers');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const short = require('short-uuid');
|
||||
@@ -12,12 +12,14 @@ const jwt = require('jsonwebtoken');
|
||||
const {setupFreeTrial, createTestCdrs, createTestAlerts} = require('./utils');
|
||||
const {generateHashedPassword} = require('../../utils/password-utils');
|
||||
const sysError = require('../error');
|
||||
|
||||
const insertUserSql = `INSERT into users
|
||||
(user_sid, account_sid, name, email, provider, provider_userid, email_validated)
|
||||
values (?, ?, ?, ?, ?, ?, 1)`;
|
||||
const insertUserLocalSql = `INSERT into users
|
||||
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider, hashed_password)
|
||||
values (?, ?, ?, ?, ?, 0, 'local', ?)`;
|
||||
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider,
|
||||
hashed_password, service_provider_sid)
|
||||
values (?, ?, ?, ?, ?, 0, 'local', ?, ?)`;
|
||||
const insertAccountSql = `INSERT into accounts
|
||||
(account_sid, service_provider_sid, name, is_active, webhook_secret, trial_end_date)
|
||||
values (?, ?, ?, ?, ?, CURDATE() + INTERVAL 21 DAY)`;
|
||||
@@ -35,8 +37,19 @@ const insertSignupHistorySql = `INSERT into signup_history
|
||||
(email, name)
|
||||
values (?, ?)`;
|
||||
|
||||
const slackEmail = `Hi there and welcome to jambonz!
|
||||
|
||||
We are excited to have you on board. Feel free to join the community on Slack at https://joinslack.jambonz.org,
|
||||
where you can connect with other jambonz users, ask questions, share your experiences, and learn from others.
|
||||
|
||||
Hope to see you there!
|
||||
|
||||
Best,
|
||||
|
||||
DaveH and the jambonz team`;
|
||||
|
||||
const addLocalUser = async(logger, user_sid, account_sid,
|
||||
name, email, email_activation_code, passwordHash) => {
|
||||
name, email, email_activation_code, passwordHash, service_provider_sid) => {
|
||||
const [r] = await promisePool.execute(insertUserLocalSql,
|
||||
[
|
||||
user_sid,
|
||||
@@ -44,7 +57,8 @@ const addLocalUser = async(logger, user_sid, account_sid,
|
||||
name,
|
||||
email,
|
||||
email_activation_code,
|
||||
passwordHash
|
||||
passwordHash,
|
||||
service_provider_sid
|
||||
]);
|
||||
debug({r}, 'Result from adding user');
|
||||
};
|
||||
@@ -145,7 +159,7 @@ router.post('/', async(req, res) => {
|
||||
const user = await doGithubAuth(logger, req.body);
|
||||
logger.info({user}, 'retrieved user details from github');
|
||||
Object.assign(userProfile, {
|
||||
name: user.name,
|
||||
name: user.email,
|
||||
email: user.email,
|
||||
email_validated: user.email_validated,
|
||||
avatar_url: user.avatar_url,
|
||||
@@ -157,7 +171,7 @@ router.post('/', async(req, res) => {
|
||||
const user = await doGoogleAuth(logger, req.body);
|
||||
logger.info({user}, 'retrieved user details from google');
|
||||
Object.assign(userProfile, {
|
||||
name: user.name || user.email,
|
||||
name: user.email || user.email,
|
||||
email: user.email,
|
||||
email_validated: user.verified_email,
|
||||
picture: user.picture,
|
||||
@@ -170,7 +184,7 @@ router.post('/', async(req, res) => {
|
||||
logger.info({user}, 'retrieved user details for local provider');
|
||||
debug({user}, 'retrieved user details for local provider');
|
||||
Object.assign(userProfile, {
|
||||
name: user.name,
|
||||
name: user.email,
|
||||
email: user.email,
|
||||
provider: 'local',
|
||||
email_activation_code: user.email_activation_code
|
||||
@@ -280,7 +294,8 @@ router.post('/', async(req, res) => {
|
||||
const passwordHash = await generateHashedPassword(req.body.password);
|
||||
debug(`hashed password: ${passwordHash}`);
|
||||
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
|
||||
userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash);
|
||||
userProfile.name, userProfile.email, userProfile.email_activation_code,
|
||||
passwordHash, req.body.service_provider_sid);
|
||||
debug('added local user');
|
||||
}
|
||||
else {
|
||||
@@ -293,17 +308,25 @@ router.post('/', async(req, res) => {
|
||||
const callStatusSid = uuid();
|
||||
const helloWordSid = uuid();
|
||||
const dialTimeSid = uuid();
|
||||
const echoSid = uuid();
|
||||
|
||||
/* 3 webhooks */
|
||||
await promisePool.execute(insertWebookSql, [callStatusSid, 'https://public-apps.jambonz.us/call-status', 'POST']);
|
||||
await promisePool.execute(insertWebookSql, [helloWordSid, 'https://public-apps.jambonz.us/hello-world', 'POST']);
|
||||
await promisePool.execute(insertWebookSql, [dialTimeSid, 'https://public-apps.jambonz.us/dial-time', 'POST']);
|
||||
/* 4 webhooks */
|
||||
await promisePool.execute(insertWebookSql,
|
||||
[callStatusSid, 'https://public-apps.jambonz.cloud/call-status', 'POST']);
|
||||
await promisePool.execute(insertWebookSql,
|
||||
[helloWordSid, 'https://public-apps.jambonz.cloud/hello-world', 'POST']);
|
||||
await promisePool.execute(insertWebookSql,
|
||||
[dialTimeSid, 'https://public-apps.jambonz.cloud/dial-time', 'POST']);
|
||||
await promisePool.execute(insertWebookSql,
|
||||
[echoSid, 'https://public-apps.jambonz.cloud/echo', 'POST']);
|
||||
|
||||
/* 2 applications */
|
||||
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'hello world',
|
||||
helloWordSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
|
||||
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'dial time clock',
|
||||
dialTimeSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
|
||||
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'simple echo test',
|
||||
echoSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
|
||||
|
||||
Object.assign(userProfile, {
|
||||
pristine: true,
|
||||
@@ -313,6 +336,15 @@ router.post('/', async(req, res) => {
|
||||
tutorial_completion: 0,
|
||||
scope: 'read-write'
|
||||
});
|
||||
|
||||
// send invite to Slack
|
||||
if (process.env.SEND_SLACK_INVITE_ON_SIGNUP) {
|
||||
try {
|
||||
emailSimpleText(logger, userProfile.email, 'Welcome to jambonz!', slackEmail);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error sending slack invite');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (user_sid) {
|
||||
/* add a new user for existing account */
|
||||
@@ -327,7 +359,7 @@ router.post('/', async(req, res) => {
|
||||
|
||||
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
|
||||
userProfile.name, userProfile.email, userProfile.email_activation_code,
|
||||
passwordHash);
|
||||
passwordHash, req.body.service_provider_sid);
|
||||
|
||||
/* note: we deactivate the old user once the new email is validated */
|
||||
}
|
||||
@@ -349,6 +381,8 @@ router.post('/', async(req, res) => {
|
||||
const token = jwt.sign({
|
||||
user_sid: userProfile.user_sid,
|
||||
account_sid: userProfile.account_sid,
|
||||
service_provider_sid: req.body.service_provider_sid,
|
||||
scope: 'account',
|
||||
email: userProfile.email,
|
||||
name: userProfile.name
|
||||
}, process.env.JWT_SECRET, { expiresIn });
|
||||
|
||||
@@ -2,25 +2,46 @@ const router = require('express').Router();
|
||||
const Sbc = require('../../models/sbc');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
//const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
//const {promisePool} = require('../../db');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {promisePool} = require('../../db');
|
||||
|
||||
decorate(router, Sbc, ['add', 'delete']);
|
||||
const validate = (req, res) => {
|
||||
if (req.user.hasScope('admin')) return;
|
||||
res.status(403).json({
|
||||
status: 'fail',
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
'add': validate,
|
||||
'delete': validate
|
||||
};
|
||||
|
||||
decorate(router, Sbc, ['add', 'delete'], preconditions);
|
||||
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const service_provider_sid = req.query.service_provider_sid;
|
||||
/*
|
||||
let service_provider_sid = req.query.service_provider_sid;
|
||||
|
||||
if (req.user.hasAccountAuth) {
|
||||
const [r] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', req.user.account_sid);
|
||||
if (0 === r.length) throw new Error('invalid account_sid');
|
||||
if (0 === r.length) throw new DbErrorBadRequest('invalid account_sid');
|
||||
|
||||
service_provider_sid = r[0].service_provider_sid;
|
||||
}
|
||||
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
|
||||
*/
|
||||
const results = await Sbc.retrieveAll(service_provider_sid);
|
||||
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
service_provider_sid = req.user.service_provider_sid;
|
||||
}
|
||||
|
||||
/** generally, we have a global set of SBCs that all accounts use.
|
||||
* However, we can have a set of SBCs that are specific for use by a service provider.
|
||||
*/
|
||||
let results = await Sbc.retrieveAll(service_provider_sid);
|
||||
if (results.length === 0) results = await Sbc.retrieveAll();
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const {promisePool} = require('../../db');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
|
||||
const {DbErrorForbidden} = require('../../utils/errors');
|
||||
const Webhook = require('../../models/webhook');
|
||||
const ServiceProvider = require('../../models/service-provider');
|
||||
const Account = require('../../models/account');
|
||||
@@ -8,7 +8,11 @@ const VoipCarrier = require('../../models/voip-carrier');
|
||||
const Application = require('../../models/application');
|
||||
const PhoneNumber = require('../../models/phone-number');
|
||||
const ApiKey = require('../../models/api-key');
|
||||
const {hasServiceProviderPermissions, parseServiceProviderSid, parseVoipCarrierSid} = require('./utils');
|
||||
const {
|
||||
hasServiceProviderPermissions,
|
||||
parseServiceProviderSid,
|
||||
parseVoipCarrierSid,
|
||||
} = require('./utils');
|
||||
const sysError = require('../error');
|
||||
const decorate = require('./decorate');
|
||||
const preconditions = {
|
||||
@@ -42,18 +46,11 @@ async function validateRetrieve(req) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
if (service_provider_sid === req.user.service_provider_sid) return ;
|
||||
if (req.user.hasScope('service_provider') || req.user.hasScope('account')) {
|
||||
if (service_provider_sid === req.user.service_provider_sid) return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
/* allow account users to retrieve service provider data from parent SP */
|
||||
const sid = req.user.account_sid;
|
||||
const [r] = await promisePool.execute('SELECT service_provider_sid from accounts WHERE account_sid = ?', [sid]);
|
||||
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions to update service provider');
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -84,14 +81,10 @@ async function noActiveAccountsOrUsers(req, sid) {
|
||||
}
|
||||
const activeAccounts = await ServiceProvider.getForeignKeyReferences('accounts.service_provider_sid', sid);
|
||||
const activeUsers = await ServiceProvider.getForeignKeyReferences('users.service_provider_sid', sid);
|
||||
if (activeAccounts > 0 && activeUsers > 0) throw new DbErrorUnprocessableRequest(
|
||||
'cannot delete service provider with active accounts or users'
|
||||
);
|
||||
if (activeAccounts > 0 && activeUsers > 0) throw new DbErrorForbidden('insufficient privileges');
|
||||
|
||||
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
|
||||
if (activeUsers > 0) throw new DbErrorUnprocessableRequest(
|
||||
'cannot delete service provider with active service provider level users'
|
||||
);
|
||||
if (activeAccounts > 0) throw new DbErrorForbidden('insufficient privileges');
|
||||
if (activeUsers > 0) throw new DbErrorForbidden('insufficient privileges');
|
||||
|
||||
/* ok we can delete -- no active accounts. remove carriers and speech credentials */
|
||||
await promisePool.execute('DELETE from speech_credentials WHERE service_provider_sid = ?', [sid]);
|
||||
@@ -122,6 +115,7 @@ router.get('/:sid/Accounts', async(req, res) => {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid/Applications', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
@@ -250,6 +244,7 @@ router.get('/', async(req, res) => {
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
await validateRetrieve(req);
|
||||
const sid = parseServiceProviderSid(req);
|
||||
const results = await ServiceProvider.retrieve(sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
|
||||
@@ -6,8 +6,14 @@ const {verifyPassword} = require('../../utils/password-utils');
|
||||
const {cacheClient} = require('../../helpers');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const sysError = require('../error');
|
||||
const retrievePermissionsSql = `
|
||||
SELECT p.name
|
||||
FROM permissions p, user_permissions up
|
||||
WHERE up.permission_sid = p.permission_sid
|
||||
AND up.user_sid = ?
|
||||
`;
|
||||
|
||||
const validateRequest = async(req) => {
|
||||
const validateRequest = (req) => {
|
||||
const {email, password} = req.body || {};
|
||||
|
||||
/* check required properties are there */
|
||||
@@ -53,6 +59,7 @@ router.post('/', async(req, res) => {
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
account_sid: user.account_sid,
|
||||
service_provider_sid: a[0].service_provider_sid,
|
||||
force_change: !!user.force_change,
|
||||
provider: user.provider,
|
||||
provider_userid: user.provider_userid,
|
||||
@@ -65,12 +72,22 @@ router.post('/', async(req, res) => {
|
||||
pristine: false
|
||||
});
|
||||
|
||||
const [p] = await promisePool.query(retrievePermissionsSql, user.user_sid);
|
||||
const permissions = p.map((x) => x.name);
|
||||
|
||||
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
|
||||
// generate a json web token for this session
|
||||
const token = jwt.sign({
|
||||
const payload = {
|
||||
scope: 'account',
|
||||
permissions,
|
||||
user_sid: userProfile.user_sid,
|
||||
account_sid: userProfile.account_sid
|
||||
}, process.env.JWT_SECRET, { expiresIn });
|
||||
account_sid: userProfile.account_sid,
|
||||
service_provider_sid: userProfile.service_provider_sid
|
||||
};
|
||||
const token = jwt.sign(payload,
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn }
|
||||
);
|
||||
|
||||
logger.debug({
|
||||
user_sid: userProfile.user_sid,
|
||||
|
||||
@@ -1,11 +1,47 @@
|
||||
const router = require('express').Router();
|
||||
const SipGateway = require('../../models/sip-gateway');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
|
||||
//const {parseSipGatewaySid} = require('./utils');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
|
||||
const checkUserScope = async(req, voip_carrier_sid) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
if (!voip_carrier_sid) {
|
||||
throw new DbErrorBadRequest('missing voip_carrier_sid');
|
||||
}
|
||||
|
||||
if (req.user.hasAdminAuth) return;
|
||||
if (req.user.hasAccountAuth) {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
|
||||
if ((!carrier.service_provider_sid || carrier.service_provider_sid === req.user.service_provider_sid) &&
|
||||
(!carrier.account_sid || carrier.account_sid === req.user.account_sid)) {
|
||||
|
||||
if (req.method !== 'GET' && !carrier.account_sid) {
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) {
|
||||
throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
}
|
||||
|
||||
if (carrier.service_provider_sid === req.user.service_provider_sid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
};
|
||||
|
||||
const validate = async(req, sid) => {
|
||||
const {lookupCarrierBySid, lookupSipGatewayBySid} = req.app.locals;
|
||||
const {lookupSipGatewayBySid} = req.app.locals;
|
||||
const {netmask} = req.body;
|
||||
let voip_carrier_sid;
|
||||
|
||||
if (sid) {
|
||||
@@ -17,13 +53,13 @@ const validate = async(req, sid) => {
|
||||
voip_carrier_sid = req.body.voip_carrier_sid;
|
||||
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
|
||||
}
|
||||
if (req.hasAccountAuth) {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
if (carrier.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account');
|
||||
}
|
||||
if (netmask &&
|
||||
process.env.JAMBONZ_MIN_GATEWAY_NETMASK &&
|
||||
parseInt(netmask) < process.env.JAMBONZ_MIN_GATEWAY_NETMASK) {
|
||||
throw new DbErrorBadRequest(
|
||||
`netmask required to have value equal or greater than ${process.env.JAMBONZ_MIN_GATEWAY_NETMASK}`);
|
||||
}
|
||||
await checkUserScope(req, voip_carrier_sid);
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
@@ -39,6 +75,7 @@ router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const voip_carrier_sid = req.query.voip_carrier_sid;
|
||||
try {
|
||||
await checkUserScope(req, voip_carrier_sid);
|
||||
if (!voip_carrier_sid) {
|
||||
logger.info('GET /SipGateways missing voip_carrier_sid param');
|
||||
return res.status(400).json({message: 'missing voip_carrier_sid query param'});
|
||||
|
||||
@@ -31,6 +31,7 @@ router.post('/:sip_realm', async(req, res) => {
|
||||
const [sbcs] = await promisePool.query('SELECT ipv4 from sbc_addresses');
|
||||
if (sbcs.length === 0) throw new Error('no SBC addresses provisioned in the database!');
|
||||
const ips = sbcs.map((s) => s.ipv4);
|
||||
const uniqueIps = [...new Set(ips)];
|
||||
|
||||
/* retrieve existing dns records */
|
||||
const [old_recs] = await promisePool.query('SELECT record_id from dns_records WHERE account_sid = ?',
|
||||
@@ -48,7 +49,7 @@ router.post('/:sip_realm', async(req, res) => {
|
||||
}
|
||||
|
||||
/* add the dns records */
|
||||
const records = await createDnsRecords(logger, domain, subdomain, ips);
|
||||
const records = await createDnsRecords(logger, domain, subdomain, uniqueIps);
|
||||
if (!records) throw new Error(`failure updating dns records for ${sip_realm}`);
|
||||
const values = records.map((r) => {
|
||||
return `('${uuid()}', '${account_sid}', '${r.type}', ${r.id})`;
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
const router = require('express').Router();
|
||||
const SmppGateway = require('../../models/smpp-gateway');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
|
||||
const checkUserScope = async(req, voip_carrier_sid) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
if (!voip_carrier_sid) {
|
||||
throw new DbErrorBadRequest('missing voip_carrier_sid');
|
||||
}
|
||||
|
||||
if (req.user.hasAdminAuth) return;
|
||||
|
||||
if (req.user.hasAccountAuth) {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
|
||||
if ((!carrier.service_provider_sid || carrier.service_provider_sid === req.user.service_provider_sid) &&
|
||||
(!carrier.account_sid || carrier.account_sid === req.user.account_sid)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
if (carrier.service_provider_sid === req.user.service_provider_sid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
};
|
||||
|
||||
const validate = async(req, sid) => {
|
||||
const {lookupCarrierBySid, lookupSmppGatewayBySid} = req.app.locals;
|
||||
const {lookupSmppGatewayBySid} = req.app.locals;
|
||||
let voip_carrier_sid;
|
||||
|
||||
if (sid) {
|
||||
@@ -17,13 +44,8 @@ const validate = async(req, sid) => {
|
||||
voip_carrier_sid = req.body.voip_carrier_sid;
|
||||
if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid');
|
||||
}
|
||||
if (req.hasAccountAuth) {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
if (carrier.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account');
|
||||
}
|
||||
}
|
||||
|
||||
await checkUserScope(req, voip_carrier_sid);
|
||||
};
|
||||
|
||||
const preconditions = {
|
||||
@@ -39,6 +61,7 @@ router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const voip_carrier_sid = req.query.voip_carrier_sid;
|
||||
try {
|
||||
await checkUserScope(req, voip_carrier_sid);
|
||||
if (!voip_carrier_sid) {
|
||||
logger.info('GET /SmppGateways missing voip_carrier_sid param');
|
||||
return res.status(400).json({message: 'missing voip_carrier_sid query param'});
|
||||
|
||||
@@ -14,20 +14,14 @@ const getFsUrl = async(logger, retrieveSet, setName, provider) => {
|
||||
logger.info('No available feature servers to handle createCall API request');
|
||||
return ;
|
||||
}
|
||||
const ip = stripPort(fs[idx++ % fs.length]);
|
||||
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
|
||||
return `http://${ip}:3000/v1/messaging/${provider}`;
|
||||
const f = fs[idx++ % fs.length];
|
||||
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
|
||||
return `${f}/v1/messaging/${provider}`;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
|
||||
}
|
||||
};
|
||||
|
||||
const stripPort = (hostport) => {
|
||||
const arr = /^(.*):(.*)$/.exec(hostport);
|
||||
if (arr) return arr[1];
|
||||
return hostport;
|
||||
};
|
||||
|
||||
const doSendResponse = async(res, respondFn, body) => {
|
||||
if (typeof respondFn === 'number') res.sendStatus(respondFn);
|
||||
else if (typeof respondFn !== 'function') res.sendStatus(200);
|
||||
@@ -44,7 +38,7 @@ router.post('/:provider', async(req, res) => {
|
||||
lookupAppByPhoneNumber,
|
||||
logger
|
||||
} = req.app.locals;
|
||||
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
|
||||
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:fs-service-url`;
|
||||
logger.debug({path: req.path, body: req.body}, 'incomingSMS from carrier');
|
||||
|
||||
// search for provider module
|
||||
|
||||
@@ -5,7 +5,16 @@ const SpeechCredential = require('../../models/speech-credential');
|
||||
const sysError = require('../error');
|
||||
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
|
||||
const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = require('./utils');
|
||||
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {decryptCredential, testWhisper, testDeepgramTTS,
|
||||
getLanguagesAndVoicesForVendor,
|
||||
testPlayHT,
|
||||
testRimelabs,
|
||||
testVerbioTts,
|
||||
testVerbioStt,
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt} = require('../../utils/speech-utils');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {
|
||||
testGoogleTts,
|
||||
testGoogleStt,
|
||||
@@ -19,18 +28,86 @@ const {
|
||||
testDeepgramStt,
|
||||
testSonioxStt,
|
||||
testIbmTts,
|
||||
testIbmStt
|
||||
testIbmStt,
|
||||
testElevenlabs,
|
||||
testAssemblyStt
|
||||
} = require('../../utils/speech-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
|
||||
const obscureKey = (key) => {
|
||||
const key_spoiler_length = 6;
|
||||
const key_spoiler_char = 'X';
|
||||
const validateAdd = async(req) => {
|
||||
const account_sid = parseAccountSid(req);
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
|
||||
if (key.length <= key_spoiler_length) {
|
||||
return key;
|
||||
if (service_provider_sid) {
|
||||
if (req.user.hasServiceProviderAuth && service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
if (req.user.hasAccountAuth && service_provider_sid !== req.user.service_provider_sid &&
|
||||
req.body.account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
}
|
||||
|
||||
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
|
||||
if (account_sid) {
|
||||
if (req.user.hasAccountAuth && account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
|
||||
const [r] = await promisePool.execute(
|
||||
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [account_sid]
|
||||
);
|
||||
|
||||
if (req.user.hasServiceProviderAuth && r[0].service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const validateRetrieveUpdateDelete = async(req, speech_credentials) => {
|
||||
if (req.user.hasServiceProviderAuth && speech_credentials[0].service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
|
||||
if (req.user.hasAccountAuth && speech_credentials[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const validateRetrieveList = async(req) => {
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
|
||||
if (service_provider_sid) {
|
||||
if ((req.user.hasServiceProviderAuth || req.user.hasAccountAuth) &&
|
||||
service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const validateTest = async(req, speech_credentials) => {
|
||||
if (req.user.hasAdminAuth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.user.hasAdminAuth && speech_credentials.service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
|
||||
if (speech_credentials.service_provider_sid === req.user.service_provider_sid) {
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.hasAccountAuth && (!speech_credentials.account_sid ||
|
||||
speech_credentials.account_sid === req.user.account_sid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('Insufficient privileges');
|
||||
}
|
||||
};
|
||||
|
||||
const encryptCredential = (obj) => {
|
||||
@@ -41,15 +118,23 @@ const encryptCredential = (obj) => {
|
||||
secret_access_key,
|
||||
aws_region,
|
||||
api_key,
|
||||
role_arn,
|
||||
region,
|
||||
client_id,
|
||||
client_secret,
|
||||
secret,
|
||||
nuance_tts_uri,
|
||||
nuance_stt_uri,
|
||||
speechmatics_stt_uri,
|
||||
deepgram_stt_uri,
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
use_custom_stt,
|
||||
custom_stt_endpoint,
|
||||
custom_stt_endpoint_url,
|
||||
tts_api_key,
|
||||
tts_region,
|
||||
stt_api_key,
|
||||
@@ -58,7 +143,14 @@ const encryptCredential = (obj) => {
|
||||
instance_id,
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
auth_token = ''
|
||||
custom_tts_streaming_url,
|
||||
auth_token = '',
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
user_id,
|
||||
voice_engine,
|
||||
engine_version,
|
||||
options
|
||||
} = obj;
|
||||
|
||||
switch (vendor) {
|
||||
@@ -74,22 +166,33 @@ const encryptCredential = (obj) => {
|
||||
return encrypt(service_key);
|
||||
|
||||
case 'aws':
|
||||
assert(access_key_id, 'invalid aws speech credential: access_key_id is required');
|
||||
assert(secret_access_key, 'invalid aws speech credential: secret_access_key is required');
|
||||
assert(aws_region, 'invalid aws speech credential: aws_region is required');
|
||||
const awsData = JSON.stringify({aws_region, access_key_id, secret_access_key});
|
||||
// AWS polly can work for 3 types of credentials:
|
||||
// 1/ access_key_id and secret_access_key
|
||||
// 2/ RoleArn Assume role
|
||||
// 3/ RoleArn assigned to instance profile where will run this application
|
||||
const awsData = JSON.stringify(
|
||||
{
|
||||
aws_region,
|
||||
...(access_key_id && {access_key_id}),
|
||||
...(secret_access_key && {secret_access_key}),
|
||||
...(role_arn && {role_arn}),
|
||||
});
|
||||
return encrypt(awsData);
|
||||
|
||||
case 'microsoft':
|
||||
assert(region, 'invalid azure speech credential: region is required');
|
||||
assert(api_key, 'invalid azure speech credential: api_key is required');
|
||||
if (!custom_tts_endpoint_url && !custom_stt_endpoint_url) {
|
||||
assert(region, 'invalid azure speech credential: region is required');
|
||||
assert(api_key, 'invalid azure speech credential: api_key is required');
|
||||
}
|
||||
const azureData = JSON.stringify({
|
||||
region,
|
||||
api_key,
|
||||
...(region && {region}),
|
||||
...(api_key && {api_key}),
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
use_custom_stt,
|
||||
custom_stt_endpoint
|
||||
custom_stt_endpoint,
|
||||
custom_stt_endpoint_url
|
||||
});
|
||||
return encrypt(azureData);
|
||||
|
||||
@@ -106,8 +209,11 @@ const encryptCredential = (obj) => {
|
||||
return encrypt(nuanceData);
|
||||
|
||||
case 'deepgram':
|
||||
assert(api_key, 'invalid deepgram speech credential: api_key is required');
|
||||
const deepgramData = JSON.stringify({api_key});
|
||||
// API key is optional if onprem
|
||||
if (!deepgram_stt_uri || !deepgram_tts_uri) {
|
||||
assert(api_key, 'invalid deepgram speech credential: api_key is required');
|
||||
}
|
||||
const deepgramData = JSON.stringify({api_key, deepgram_stt_uri, deepgram_stt_use_tls, deepgram_tts_uri});
|
||||
return encrypt(deepgramData);
|
||||
|
||||
case 'ibm':
|
||||
@@ -124,9 +230,68 @@ const encryptCredential = (obj) => {
|
||||
const sonioxData = JSON.stringify({api_key});
|
||||
return encrypt(sonioxData);
|
||||
|
||||
case 'cobalt':
|
||||
assert(cobalt_server_uri, 'invalid cobalt speech credential: cobalt_server_uri is required');
|
||||
const cobaltData = JSON.stringify({cobalt_server_uri});
|
||||
return encrypt(cobaltData);
|
||||
|
||||
case 'elevenlabs':
|
||||
assert(api_key, 'invalid elevenLabs speech credential: api_key is required');
|
||||
assert(model_id, 'invalid elevenLabs speech credential: model_id is required');
|
||||
const elevenlabsData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(elevenlabsData);
|
||||
|
||||
case 'speechmatics':
|
||||
assert(api_key, 'invalid speechmatics speech credential: api_key is required');
|
||||
assert(speechmatics_stt_uri, 'invalid speechmatics speech credential: speechmatics_stt_uri is required');
|
||||
const speechmaticsData = JSON.stringify({api_key, speechmatics_stt_uri, options});
|
||||
return encrypt(speechmaticsData);
|
||||
|
||||
case 'playht':
|
||||
assert(api_key, 'invalid playht speech credential: api_key is required');
|
||||
assert(user_id, 'invalid playht speech credential: user_id is required');
|
||||
assert(voice_engine, 'invalid voice_engine speech credential: voice_engine is required');
|
||||
const playhtData = JSON.stringify({api_key, user_id, voice_engine, options});
|
||||
return encrypt(playhtData);
|
||||
|
||||
case 'cartesia':
|
||||
assert(api_key, 'invalid cartesia speech credential: api_key is required');
|
||||
assert(model_id, 'invalid cartesia speech credential: model_id is required');
|
||||
const cartesiaData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(cartesiaData);
|
||||
|
||||
case 'rimelabs':
|
||||
assert(api_key, 'invalid rimelabs speech credential: api_key is required');
|
||||
assert(model_id, 'invalid rimelabs speech credential: model_id is required');
|
||||
const rimelabsData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(rimelabsData);
|
||||
|
||||
case 'assemblyai':
|
||||
assert(api_key, 'invalid assemblyai speech credential: api_key is required');
|
||||
const assemblyaiData = JSON.stringify({api_key});
|
||||
return encrypt(assemblyaiData);
|
||||
|
||||
case 'voxist':
|
||||
assert(api_key, 'invalid voxist speech credential: api_key is required');
|
||||
const voxistData = JSON.stringify({api_key});
|
||||
return encrypt(voxistData);
|
||||
|
||||
case 'whisper':
|
||||
assert(api_key, 'invalid whisper speech credential: api_key is required');
|
||||
assert(model_id, 'invalid whisper speech credential: model_id is required');
|
||||
const whisperData = JSON.stringify({api_key, model_id});
|
||||
return encrypt(whisperData);
|
||||
|
||||
case 'verbio':
|
||||
assert(engine_version, 'invalid verbio speech credential: client_id is required');
|
||||
assert(client_id, 'invalid verbio speech credential: client_id is required');
|
||||
assert(client_secret, 'invalid verbio speech credential: secret is required');
|
||||
const verbioData = JSON.stringify({client_id, client_secret, engine_version});
|
||||
return encrypt(verbioData);
|
||||
|
||||
default:
|
||||
if (vendor.startsWith('custom:')) {
|
||||
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
|
||||
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url, custom_tts_streaming_url});
|
||||
return encrypt(customData);
|
||||
}
|
||||
else assert(false, `invalid or missing vendor: ${vendor}`);
|
||||
@@ -141,11 +306,14 @@ router.post('/', async(req, res) => {
|
||||
use_for_stt,
|
||||
use_for_tts,
|
||||
vendor,
|
||||
label
|
||||
} = req.body;
|
||||
const account_sid = req.user.account_sid || req.body.account_sid;
|
||||
const service_provider_sid = req.user.service_provider_sid ||
|
||||
req.body.service_provider_sid || parseServiceProviderSid(req);
|
||||
|
||||
await validateAdd(req);
|
||||
|
||||
if (!account_sid) {
|
||||
if (!req.user.hasServiceProviderAuth && !req.user.hasAdminAuth) {
|
||||
logger.error('POST /SpeechCredentials invalid credentials');
|
||||
@@ -153,11 +321,21 @@ router.post('/', async(req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if vendor and label is already used for account or SP
|
||||
if (label) {
|
||||
const existingSpeech = await SpeechCredential.getSpeechCredentialsByVendorAndLabel(
|
||||
service_provider_sid, account_sid, vendor, label);
|
||||
if (existingSpeech.length > 0) {
|
||||
throw new DbErrorUnprocessableRequest(`Label ${label} is already in use for another speech credential`);
|
||||
}
|
||||
}
|
||||
|
||||
const encrypted_credential = encryptCredential(req.body);
|
||||
const uuid = await SpeechCredential.make({
|
||||
account_sid,
|
||||
service_provider_sid,
|
||||
vendor,
|
||||
label,
|
||||
use_for_tts,
|
||||
use_for_stt,
|
||||
credential: encrypted_credential
|
||||
@@ -175,8 +353,11 @@ router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
try {
|
||||
const account_sid = parseAccountSid(req) || req.user.account_sid;
|
||||
const service_provider_sid = parseServiceProviderSid(req) || req.user.service_provider_sid;
|
||||
const account_sid = parseAccountSid(req) ? parseAccountSid(req) : req.user.account_sid;
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
|
||||
await validateRetrieveList(req);
|
||||
|
||||
const credsAccount = account_sid ? await SpeechCredential.retrieveAll(account_sid) : [];
|
||||
const credsSP = service_provider_sid ?
|
||||
await SpeechCredential.retrieveAllForSP(service_provider_sid) :
|
||||
@@ -190,65 +371,16 @@ router.get('/', async(req, res) => {
|
||||
|
||||
res.status(200).json(creds.map((c) => {
|
||||
const {credential, ...obj} = c;
|
||||
if ('google' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
const key_header = '-----BEGIN PRIVATE KEY-----\n';
|
||||
const obscured = {
|
||||
...o,
|
||||
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
|
||||
};
|
||||
obj.service_key = obscured;
|
||||
}
|
||||
else if ('aws' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.access_key_id = o.access_key_id;
|
||||
obj.secret_access_key = obscureKey(o.secret_access_key);
|
||||
obj.aws_region = o.aws_region;
|
||||
logger.info({obj, o}, 'retrieving aws speech credential');
|
||||
}
|
||||
else if ('microsoft' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
obj.region = o.region;
|
||||
obj.use_custom_tts = o.use_custom_tts;
|
||||
obj.custom_tts_endpoint = o.custom_tts_endpoint;
|
||||
obj.use_custom_stt = o.use_custom_stt;
|
||||
obj.custom_stt_endpoint = o.custom_stt_endpoint;
|
||||
logger.info({obj, o}, 'retrieving azure speech credential');
|
||||
}
|
||||
else if ('wellsaid' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
}
|
||||
else if ('nuance' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.client_id = o.client_id;
|
||||
obj.secret = o.secret ? obscureKey(o.secret) : null;
|
||||
}
|
||||
else if ('deepgram' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
}
|
||||
else if ('ibm' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.tts_api_key = obscureKey(o.tts_api_key);
|
||||
obj.tts_region = o.tts_region;
|
||||
obj.stt_api_key = obscureKey(o.stt_api_key);
|
||||
obj.stt_region = o.stt_region;
|
||||
obj.instance_id = o.instance_id;
|
||||
} else if ('nvidia' == obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.riva_server_uri = o.riva_server_uri;
|
||||
}
|
||||
else if ('soniox' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
}
|
||||
else if (obj.vendor.startsWith('custom:')) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.auth_token = obscureKey(o.auth_token);
|
||||
obj.custom_stt_url = o.custom_stt_url;
|
||||
obj.custom_tts_url = o.custom_tts_url;
|
||||
|
||||
decryptCredential(obj, credential, logger);
|
||||
|
||||
if (req.user.hasAccountAuth && obj.account_sid === null) {
|
||||
delete obj.api_key;
|
||||
delete obj.secret_access_key;
|
||||
delete obj.secret;
|
||||
delete obj.auth_token;
|
||||
delete obj.stt_api_key;
|
||||
delete obj.tts_api_key;
|
||||
}
|
||||
return obj;
|
||||
}));
|
||||
@@ -266,67 +398,21 @@ router.get('/:sid', async(req, res) => {
|
||||
const sid = parseSpeechCredentialSid(req);
|
||||
const cred = await SpeechCredential.retrieve(sid);
|
||||
if (0 === cred.length) return res.sendStatus(404);
|
||||
|
||||
await validateRetrieveUpdateDelete(req, cred);
|
||||
|
||||
const {credential, ...obj} = cred[0];
|
||||
if ('google' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
const key_header = '-----BEGIN PRIVATE KEY-----\n';
|
||||
const obscured = {
|
||||
...o,
|
||||
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
|
||||
};
|
||||
obj.service_key = JSON.stringify(obscured);
|
||||
}
|
||||
else if ('aws' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.access_key_id = o.access_key_id;
|
||||
obj.secret_access_key = obscureKey(o.secret_access_key);
|
||||
obj.aws_region = o.aws_region;
|
||||
}
|
||||
else if ('microsoft' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
obj.region = o.region;
|
||||
obj.use_custom_tts = o.use_custom_tts;
|
||||
obj.custom_tts_endpoint = o.custom_tts_endpoint;
|
||||
obj.use_custom_stt = o.use_custom_stt;
|
||||
obj.custom_stt_endpoint = o.custom_stt_endpoint;
|
||||
}
|
||||
else if ('wellsaid' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
}
|
||||
else if ('nuance' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.client_id = o.client_id;
|
||||
obj.secret = o.secret ? obscureKey(o.secret) : null;
|
||||
obj.nuance_tts_uri = o.nuance_tts_uri;
|
||||
obj.nuance_stt_uri = o.nuance_stt_uri;
|
||||
}
|
||||
else if ('deepgram' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
}
|
||||
else if ('ibm' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.tts_api_key = obscureKey(o.tts_api_key);
|
||||
obj.tts_region = o.tts_region;
|
||||
obj.stt_api_key = obscureKey(o.stt_api_key);
|
||||
obj.stt_region = o.stt_region;
|
||||
obj.instance_id = o.instance_id;
|
||||
} else if ('nvidia' == obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.riva_server_uri = o.riva_server_uri;
|
||||
}
|
||||
else if ('soniox' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = obscureKey(o.api_key);
|
||||
}
|
||||
else if (obj.vendor.startsWith('custom:')) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.auth_token = obscureKey(o.auth_token);
|
||||
obj.custom_stt_url = o.custom_stt_url;
|
||||
obj.custom_tts_url = o.custom_tts_url;
|
||||
decryptCredential(obj, credential, logger);
|
||||
|
||||
if (req.user.hasAccountAuth && obj.account_sid === null) {
|
||||
delete obj.api_key;
|
||||
delete obj.secret_access_key;
|
||||
delete obj.secret;
|
||||
delete obj.auth_token;
|
||||
delete obj.stt_api_key;
|
||||
delete obj.tts_api_key;
|
||||
}
|
||||
|
||||
res.status(200).json(obj);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -340,6 +426,8 @@ router.delete('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const sid = parseSpeechCredentialSid(req);
|
||||
const cred = await SpeechCredential.retrieve(sid);
|
||||
await validateRetrieveUpdateDelete(req, cred);
|
||||
const count = await SpeechCredential.remove(sid);
|
||||
if (0 === count) return res.sendStatus(404);
|
||||
res.sendStatus(204);
|
||||
@@ -372,14 +460,30 @@ router.put('/:sid', async(req, res) => {
|
||||
/* update the credential if provided */
|
||||
try {
|
||||
const cred = await SpeechCredential.retrieve(sid);
|
||||
|
||||
await validateRetrieveUpdateDelete(req, cred);
|
||||
|
||||
if (1 === cred.length) {
|
||||
const {credential, vendor} = cred[0];
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
const {
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
use_custom_stt,
|
||||
custom_stt_endpoint
|
||||
custom_stt_endpoint,
|
||||
custom_stt_endpoint_url,
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
custom_tts_streaming_url,
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
voice_engine,
|
||||
options,
|
||||
deepgram_stt_uri,
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
engine_version
|
||||
} = req.body;
|
||||
|
||||
const newCred = {
|
||||
@@ -389,13 +493,26 @@ router.put('/:sid', async(req, res) => {
|
||||
aws_region,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
use_custom_stt,
|
||||
custom_stt_endpoint,
|
||||
custom_stt_endpoint_url,
|
||||
stt_region,
|
||||
tts_region,
|
||||
riva_server_uri,
|
||||
nuance_stt_uri,
|
||||
nuance_tts_uri
|
||||
nuance_tts_uri,
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
custom_tts_streaming_url,
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
voice_engine,
|
||||
options,
|
||||
deepgram_stt_uri,
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
engine_version
|
||||
};
|
||||
logger.info({o, newCred}, 'updating speech credential with this new credential');
|
||||
obj.credential = encryptCredential(newCred);
|
||||
@@ -424,12 +541,15 @@ router.put('/:sid', async(req, res) => {
|
||||
* Test a credential
|
||||
*/
|
||||
router.get('/:sid/test', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {logger, synthAudio, getVerbioAccessToken} = req.app.locals;
|
||||
try {
|
||||
const sid = parseSpeechCredentialSid(req);
|
||||
const creds = await SpeechCredential.retrieve(sid);
|
||||
|
||||
if (!creds || 0 === creds.length) return res.sendStatus(404);
|
||||
|
||||
await validateTest(req, creds[0]);
|
||||
|
||||
const cred = creds[0];
|
||||
const credential = JSON.parse(decrypt(cred.credential));
|
||||
const results = {
|
||||
@@ -469,12 +589,13 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'aws') {
|
||||
const {getTtsVoices, getAwsAuthToken} = req.app.locals;
|
||||
if (cred.use_for_tts) {
|
||||
const {getTtsVoices} = req.app.locals;
|
||||
try {
|
||||
await testAwsTts(logger, getTtsVoices, {
|
||||
accessKeyId: credential.access_key_id,
|
||||
secretAccessKey: credential.secret_access_key,
|
||||
roleArn: credential.role_arn,
|
||||
region: credential.aws_region || process.env.AWS_REGION
|
||||
});
|
||||
results.tts.status = 'ok';
|
||||
@@ -486,9 +607,10 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testAwsStt(logger, {
|
||||
await testAwsStt(logger, getAwsAuthToken, {
|
||||
accessKeyId: credential.access_key_id,
|
||||
secretAccessKey: credential.secret_access_key,
|
||||
roleArn: credential.role_arn,
|
||||
region: credential.aws_region || process.env.AWS_REGION
|
||||
});
|
||||
results.stt.status = 'ok';
|
||||
@@ -505,18 +627,22 @@ router.get('/:sid/test', async(req, res) => {
|
||||
region,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
use_custom_stt,
|
||||
custom_stt_endpoint
|
||||
custom_stt_endpoint,
|
||||
custom_stt_endpoint_url
|
||||
} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testMicrosoftTts(logger, {
|
||||
await testMicrosoftTts(logger, synthAudio, {
|
||||
api_key,
|
||||
region,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
use_custom_stt,
|
||||
custom_stt_endpoint
|
||||
custom_stt_endpoint,
|
||||
custom_stt_endpoint_url
|
||||
});
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
@@ -527,7 +653,7 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testMicrosoftStt(logger, {api_key, region});
|
||||
await testMicrosoftStt(logger, {api_key, region, use_custom_stt, custom_stt_endpoint_url});
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
@@ -586,10 +712,19 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'deepgram') {
|
||||
} else if (cred.vendor === 'deepgram') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt) {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testDeepgramTTS(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
if (cred.use_for_stt && api_key) {
|
||||
try {
|
||||
await testDeepgramStt(logger, {api_key});
|
||||
results.stt.status = 'ok';
|
||||
@@ -645,6 +780,129 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'elevenlabs') {
|
||||
const {api_key, model_id} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testElevenlabs(logger, {api_key, model_id});
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'speechmatics') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testSpeechmaticsStt(logger, {api_key});
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'playht') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testPlayHT(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
// if error is from bent, let get the body
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
results.tts = {status: 'fail', reason};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'cartesia') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testCartesia(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
// if error is from bent, let get the body
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
results.tts = {status: 'fail', reason};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'rimelabs') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testRimelabs(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'assemblyai') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testAssemblyStt(logger, {api_key});
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'voxist') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testVoxistStt(logger, {api_key});
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'whisper') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testWhisper(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'verbio') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testVerbioTts(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testVerbioStt(logger, getVerbioAccessToken, credential);
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(results);
|
||||
@@ -654,4 +912,34 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch speech voices and languages
|
||||
*/
|
||||
|
||||
router.get('/speech/supportedLanguagesAndVoices', async(req, res) => {
|
||||
const {logger, getTtsVoices} = req.app.locals;
|
||||
try {
|
||||
const {vendor, label, create_new} = req.query;
|
||||
if (!vendor) {
|
||||
throw new DbErrorBadRequest('vendor is required');
|
||||
}
|
||||
const account_sid = req.user.account_sid || req.body.account_sid;
|
||||
const service_provider_sid = req.user.service_provider_sid ||
|
||||
req.body.service_provider_sid || parseServiceProviderSid(req);
|
||||
|
||||
const credentials = create_new ? null : await SpeechCredential.getSpeechCredentialsByVendorAndLabel(
|
||||
service_provider_sid, account_sid, vendor, label);
|
||||
const tmp = credentials && credentials.length > 0 ? credentials[0] : null;
|
||||
const cred = tmp ? JSON.parse(decrypt(tmp.credential)) : null;
|
||||
try {
|
||||
const data = await getLanguagesAndVoicesForVendor(logger, vendor, cred, getTtsVoices);
|
||||
res.status(200).json(data);
|
||||
} catch (err) {
|
||||
throw new DbErrorUnprocessableRequest(err.message);
|
||||
}
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
14
lib/routes/api/system-information.js
Normal file
14
lib/routes/api/system-information.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const router = require('express').Router();
|
||||
const SystemInformation = require('../../models/system-information');
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const sysInfo = await SystemInformation.add(req.body);
|
||||
res.status(201).json(sysInfo);
|
||||
});
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const [sysInfo] = await SystemInformation.retrieveAll();
|
||||
res.status(200).json(sysInfo || {});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
137
lib/routes/api/tts-cache.js
Normal file
137
lib/routes/api/tts-cache.js
Normal file
@@ -0,0 +1,137 @@
|
||||
const router = require('express').Router();
|
||||
const {
|
||||
parseAccountSid
|
||||
} = require('./utils');
|
||||
const SpeechCredential = require('../../models/speech-credential');
|
||||
const fs = require('fs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const Account = require('../../models/account');
|
||||
const sysError = require('../error');
|
||||
const { getSpeechCredential, decryptCredential } = require('../../utils/speech-utils');
|
||||
const PCMToMP3Encoder = require('../../record/encoder');
|
||||
const { pipeline } = require('stream');
|
||||
|
||||
router.delete('/', async(req, res) => {
|
||||
const {purgeTtsCache} = req.app.locals;
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (account_sid) {
|
||||
await purgeTtsCache({account_sid});
|
||||
} else {
|
||||
await purgeTtsCache();
|
||||
}
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const {getTtsSize} = req.app.locals;
|
||||
const account_sid = parseAccountSid(req);
|
||||
let size = 0;
|
||||
if (account_sid) {
|
||||
size = await getTtsSize(`tts:${account_sid}:*`);
|
||||
} else {
|
||||
size = await getTtsSize();
|
||||
}
|
||||
res.status(200).json({size});
|
||||
});
|
||||
|
||||
router.post('/Synthesize', async(req, res) => {
|
||||
const {logger, synthAudio} = req.app.locals;
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
const body = req.body;
|
||||
const encodingMp3 = req.body.encodingMp3 || false;
|
||||
if (!body.speech_credential_sid || !body.text || !body.language || !body.voice) {
|
||||
throw new DbErrorBadRequest('speech_credential_sid, text, language, voice are all required');
|
||||
}
|
||||
|
||||
const result = await Account.retrieve(accountSid);
|
||||
if (!result || result.length === 0 || !result[0].is_active) {
|
||||
throw new DbErrorBadRequest(`Account not found for sid ${accountSid}`);
|
||||
}
|
||||
const credentials = await SpeechCredential.retrieve(body.speech_credential_sid);
|
||||
if (!credentials || credentials.length === 0) {
|
||||
throw new
|
||||
DbErrorBadRequest(`There is no available speech credential for ${body.speech_credential_sid}`);
|
||||
}
|
||||
const {credential, ...obj} = credentials[0];
|
||||
|
||||
decryptCredential(obj, credential, logger, false);
|
||||
const cred = getSpeechCredential(obj, logger);
|
||||
|
||||
const { text, language, engine = 'standard' } = body;
|
||||
const salt = uuidv4();
|
||||
/* parse Nuance voices into name and model */
|
||||
let voice = body.voice;
|
||||
let model;
|
||||
if (cred.vendor === 'nuance' && voice) {
|
||||
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
|
||||
if (arr) {
|
||||
voice = arr[1];
|
||||
model = arr[2];
|
||||
}
|
||||
} else if (cred.vendor === 'deepgram') {
|
||||
model = voice;
|
||||
}
|
||||
const stats = {
|
||||
histogram: () => {},
|
||||
increment: () => {},
|
||||
};
|
||||
const { filePath } = await synthAudio(stats, {
|
||||
account_sid: accountSid,
|
||||
text,
|
||||
vendor: cred.vendor,
|
||||
language,
|
||||
voice,
|
||||
engine,
|
||||
model,
|
||||
salt,
|
||||
credentials: cred,
|
||||
disableTtsCache: false,
|
||||
disableTtsStreaming: true
|
||||
});
|
||||
|
||||
let contentType = 'audio/mpeg';
|
||||
|
||||
let readStream = fs.createReadStream(filePath);
|
||||
if (['nuance', 'nvidia'].includes(cred.vendor) ||
|
||||
(
|
||||
process.env.JAMBONES_TTS_TRIM_SILENCE &&
|
||||
['microsoft', 'azure'].includes(cred.vendor)
|
||||
)
|
||||
) {
|
||||
if (encodingMp3) {
|
||||
readStream = readStream
|
||||
.pipe(new PCMToMP3Encoder({
|
||||
channels: 1,
|
||||
sampleRate: 8000,
|
||||
bitRate: 128
|
||||
}, logger));
|
||||
} else {
|
||||
contentType = 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': contentType,
|
||||
});
|
||||
|
||||
pipeline(readStream, res, (err) => {
|
||||
if (err) {
|
||||
logger.error('ttscache/Synthesize failed:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).end('Server error');
|
||||
}
|
||||
}
|
||||
|
||||
fs.unlink(filePath, (unlinkErr) => {
|
||||
if (unlinkErr) throw unlinkErr;
|
||||
logger.info(`${filePath} was deleted`);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,9 +1,9 @@
|
||||
const router = require('express').Router();
|
||||
const User = require('../../models/user');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
|
||||
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
const {validatePasswordSettings} = require('./utils');
|
||||
const {validatePasswordSettings, parseUserSid} = require('./utils');
|
||||
const {decrypt} = require('../../utils/encrypt-decrypt');
|
||||
const {cacheClient} = require('../../helpers');
|
||||
const sysError = require('../error');
|
||||
@@ -99,6 +99,48 @@ const validateRequest = async(user_sid, req) => {
|
||||
return user;
|
||||
};
|
||||
|
||||
const getActiveAdminUsers = (users) => {
|
||||
return users.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
|
||||
};
|
||||
|
||||
const ensureUserActionIsAllowed = (req, user) => {
|
||||
if (req.user.hasAdminAuth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.hasAccountAuth && req.user.account_sid === user.account_sid) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
};
|
||||
|
||||
const ensureUserDeletionIsAllowed = (req, activeAdminUsers, user) => {
|
||||
try {
|
||||
if (req.user.hasAdminAuth && activeAdminUsers.length === 1 && activeAdminUsers[0].user_sid === user[0].user_sid) {
|
||||
throw new BadRequestError('cannot delete this admin user - there are no other active admin users');
|
||||
}
|
||||
|
||||
ensureUserActionIsAllowed(req, user[0]);
|
||||
return;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const ensureUserRetrievalIsAllowed = (req, user) => {
|
||||
try {
|
||||
ensureUserActionIsAllowed(req, user);
|
||||
return;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
@@ -256,22 +298,20 @@ router.get('/me', async(req, res) => {
|
||||
|
||||
router.get('/:user_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid} = req.params;
|
||||
|
||||
try {
|
||||
const user_sid = parseUserSid(req);
|
||||
const [user] = await User.retrieve(user_sid);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {hashed_password, ...rest} = user;
|
||||
if (!user) throw new Error('failure retrieving user');
|
||||
|
||||
if (req.user.hasAdminAuth ||
|
||||
req.user.hasAccountAuth && req.user.account_sid === user.account_sid ||
|
||||
req.user.hasServiceProviderAuth && req.user.service_provider_sid === user.service_provider_sid) {
|
||||
res.status(200).json(rest);
|
||||
} else {
|
||||
res.sendStatus(403);
|
||||
if (!user) {
|
||||
throw new Error('failure retrieving user');
|
||||
}
|
||||
|
||||
ensureUserRetrievalIsAllowed(req, user);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { hashed_password, ...rest } = user;
|
||||
return res.status(200).json(rest);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -298,8 +338,8 @@ router.put('/:user_sid', async(req, res) => {
|
||||
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
||||
|
||||
if (!hasAdminAuth &&
|
||||
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
|
||||
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
|
||||
!(hasAccountAuth && user[0] && req.user.account_sid === user[0].account_sid) &&
|
||||
!(hasServiceProviderAuth && user[0] && req.user.service_provider_sid === user[0].service_provider_sid) &&
|
||||
(req.user.user_sid && req.user.user_sid !== user_sid)) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
@@ -413,12 +453,12 @@ router.post('/', async(req, res) => {
|
||||
|
||||
if (name) {
|
||||
logger.debug({payload}, 'user with this username already exists');
|
||||
return res.status(422).json({msg: 'user with this username already exists'});
|
||||
return res.status(422).json({msg: 'invalid username or email'});
|
||||
}
|
||||
|
||||
if (email) {
|
||||
logger.debug({payload}, 'user with this email already exists');
|
||||
return res.status(422).json({msg: 'user with this email already exists'});
|
||||
return res.status(422).json({msg: 'invalid username or email'});
|
||||
}
|
||||
|
||||
if (req.user.hasAdminAuth) {
|
||||
@@ -449,28 +489,21 @@ router.post('/', async(req, res) => {
|
||||
|
||||
router.delete('/:user_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {user_sid} = req.params;
|
||||
const allUsers = await User.retrieveAll();
|
||||
const activeAdminUsers = allUsers.filter((e) => !e.account_sid && !e.service_provider_sid && e.is_active);
|
||||
const user = await User.retrieve(user_sid);
|
||||
|
||||
try {
|
||||
if (req.user.hasAdminAuth && activeAdminUsers.length === 1) {
|
||||
throw new Error('cannot delete this admin user - there are no other active admin users');
|
||||
}
|
||||
const user_sid = parseUserSid(req);
|
||||
const allUsers = await User.retrieveAll();
|
||||
const activeAdminUsers = getActiveAdminUsers(allUsers);
|
||||
const user = allUsers.filter((user) => user.user_sid === user_sid);
|
||||
|
||||
if (req.user.hasAdminAuth ||
|
||||
(req.user.hasAccountAuth && req.user.account_sid === user[0].account_sid) ||
|
||||
(req.user.hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid)) {
|
||||
await User.remove(user_sid);
|
||||
/* invalidate the jwt of the deleted user */
|
||||
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
|
||||
await cacheClient.delete(redisKey);
|
||||
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
|
||||
await User.remove(user_sid);
|
||||
|
||||
return res.sendStatus(204);
|
||||
} else {
|
||||
throw new DbErrorBadRequest('invalid request');
|
||||
}
|
||||
/* invalidate the jwt of the deleted user */
|
||||
const redisKey = cacheClient.generateRedisKey('jwt', user_sid, 'v2');
|
||||
await cacheClient.delete(redisKey);
|
||||
|
||||
return res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -218,20 +218,67 @@ const parseWebhookSid = (req) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasAccountPermissions = (req, res, next) => {
|
||||
const parseSipGatewaySid = (req) => {
|
||||
try {
|
||||
return validateSid('SipGateways', req);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const parseUserSid = (req) => {
|
||||
try {
|
||||
return validateSid('Users', req);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const parseLcrSid = (req) => {
|
||||
try {
|
||||
return validateSid('Lcrs', req);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const hasAccountPermissions = async(req, res, next) => {
|
||||
try {
|
||||
if (req.user.hasScope('admin')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
return next();
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (service_provider_sid) {
|
||||
if (service_provider_sid === req.user.service_provider_sid) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
if (account_sid) {
|
||||
const [r] = await Account.retrieve(account_sid);
|
||||
if (r && r.service_provider_sid === req.user.service_provider_sid) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
const account_sid = parseAccountSid(req);
|
||||
if (account_sid === req.user.account_sid) {
|
||||
return next();
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
const [r] = await Account.retrieve(account_sid);
|
||||
|
||||
if (account_sid) {
|
||||
if (r && r.account_sid === req.user.account_sid) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
if (service_provider_sid) {
|
||||
if (r && r.service_provider_sid === req.user.service_provider_sid) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +440,22 @@ const validatePasswordSettings = async(password) => {
|
||||
return;
|
||||
};
|
||||
|
||||
function hasValue(data) {
|
||||
if (typeof data === 'string') {
|
||||
return data && data.length > 0;
|
||||
} else if (Array.isArray(data)) {
|
||||
return data && data.length > 0;
|
||||
} else if (typeof data === 'object') {
|
||||
return data && Object.keys(data).length > 0;
|
||||
} else if (typeof data === 'number') {
|
||||
return data !== null;
|
||||
} else if (typeof data === 'boolean') {
|
||||
return data !== null;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setupFreeTrial,
|
||||
createTestCdrs,
|
||||
@@ -405,10 +468,14 @@ module.exports = {
|
||||
parseSpeechCredentialSid,
|
||||
parseVoipCarrierSid,
|
||||
parseWebhookSid,
|
||||
parseSipGatewaySid,
|
||||
parseUserSid,
|
||||
parseLcrSid,
|
||||
hasAccountPermissions,
|
||||
hasServiceProviderPermissions,
|
||||
checkLimits,
|
||||
enableSubspace,
|
||||
disableSubspace,
|
||||
validatePasswordSettings
|
||||
validatePasswordSettings,
|
||||
hasValue,
|
||||
};
|
||||
|
||||
@@ -74,7 +74,14 @@ decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions);
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
const results = await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
|
||||
const results = req.user.hasAdminAuth ?
|
||||
await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
|
||||
await VoipCarrier.retrieveAllForSP(req.user.service_provider_sid);
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
return res.status(200).json(results.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
|
||||
}
|
||||
|
||||
res.status(200).json(results);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -89,6 +96,20 @@ router.get('/:sid', async(req, res) => {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const results = await VoipCarrier.retrieve(sid, account_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
const ret = results[0];
|
||||
ret.register_status = JSON.parse(ret.register_status || '{}');
|
||||
|
||||
if (req.user.hasServiceProviderAuth && results.length === 1) {
|
||||
if (results.length === 1 && results[0].service_provider_sid !== req.user.service_provider_sid) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
}
|
||||
if (req.user.hasAccountAuth && results.length === 1) {
|
||||
if (results.length === 1 && results[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
@@ -2,17 +2,37 @@ const router = require('express').Router();
|
||||
const Webhook = require('../../models/webhook');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
const {DbErrorForbidden} = require('../../utils/errors');
|
||||
const { parseWebhookSid } = require('./utils');
|
||||
const {promisePool} = require('../../db');
|
||||
|
||||
decorate(router, Webhook, ['add']);
|
||||
|
||||
/* retrieve */
|
||||
router.get('/:sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
try {
|
||||
const sid = parseWebhookSid(req);
|
||||
const results = await Webhook.retrieve(sid);
|
||||
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
|
||||
if (req.user.hasAccountAuth) {
|
||||
/* can only update carriers for the user's account */
|
||||
if (results[0].account_sid !== req.user.account_sid) {
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
}
|
||||
}
|
||||
if (req.user.hasServiceProviderAuth) {
|
||||
const [r] = await promisePool.execute(
|
||||
'SELECT service_provider_sid from accounts WHERE account_sid = ?', [results[0].account_sid]
|
||||
);
|
||||
if (r.length === 1 && r[0].service_provider_sid === req.user.service_provider_sid) {
|
||||
return;
|
||||
}
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
}
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
@@ -61,8 +61,7 @@ router.post('/', express.raw({type: 'application/json'}), async(req, res) => {
|
||||
}
|
||||
|
||||
/* process event */
|
||||
logger.info(`received webhook: ${evt.type}`);
|
||||
if (evt.type.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
|
||||
if (evt?.type?.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
|
||||
else {
|
||||
logger.debug(evt, 'unhandled stripe webook');
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -89,7 +89,12 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
|
||||
const str = await res.text();
|
||||
return JSON.parse(str);
|
||||
}
|
||||
logger.error({res}, 'Error creating records');
|
||||
let body;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch (err) {
|
||||
}
|
||||
logger.error({headers: res.headers, body}, `Error creating records, status ${res.statusCode}`);
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error retrieving domains');
|
||||
}
|
||||
|
||||
@@ -47,13 +47,15 @@ const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
|
||||
};
|
||||
|
||||
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
|
||||
const mg = mailgun.client({
|
||||
username: 'api',
|
||||
key: process.env.MAILGUN_API_KEY
|
||||
});
|
||||
if (!process.env.MAILGUN_API_KEY) throw new Error('MAILGUN_API_KEY env variable is not defined!');
|
||||
if (!process.env.MAILGUN_DOMAIN) throw new Error('MAILGUN_DOMAIN env variable is not defined!');
|
||||
|
||||
const mg = mailgun.client({
|
||||
username: 'api',
|
||||
key: process.env.MAILGUN_API_KEY,
|
||||
...(process.env.MAILGUN_URL && {url: process.env.MAILGUN_URL})
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {
|
||||
from,
|
||||
|
||||
@@ -17,13 +17,106 @@ const encrypt = (text) => {
|
||||
};
|
||||
|
||||
const decrypt = (data) => {
|
||||
const hash = JSON.parse(data);
|
||||
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
||||
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||
return decrpyted.toString();
|
||||
try {
|
||||
const hash = JSON.parse(data);
|
||||
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
||||
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||
return decrpyted.toString();
|
||||
} catch (error) {
|
||||
console.error('Error while decrypting data', error);
|
||||
return '{}';
|
||||
}
|
||||
};
|
||||
|
||||
const obscureKey = (key, key_spoiler_length = 6) => {
|
||||
const key_spoiler_char = 'X';
|
||||
|
||||
if (!key || key.length <= key_spoiler_length) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
|
||||
};
|
||||
|
||||
function isObscureKey(bucketCredentials) {
|
||||
if (!bucketCredentials) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
vendor,
|
||||
secret_access_key = '',
|
||||
service_key = '',
|
||||
connection_string = ''
|
||||
} = bucketCredentials || {};
|
||||
let pattern;
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
case 's3_compatible':
|
||||
pattern = /^([A-Za-z0-9]{4,6}X+$)/;
|
||||
return pattern.test(secret_access_key);
|
||||
case 'azure':
|
||||
pattern = /^https:[A-Za-z0-9\/.:?=&_-]+$/;
|
||||
return pattern.test(connection_string);
|
||||
|
||||
case 'google': {
|
||||
pattern = /^([A-Za-z0-9]{4,6}X+$)/;
|
||||
let {private_key} = JSON.parse(service_key);
|
||||
const key_header = '-----BEGIN PRIVATE KEY-----\n';
|
||||
private_key = private_key.slice(key_header.length, private_key.length);
|
||||
return pattern.test(private_key || '');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.log('Error in isObscureKey', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* obscure sensitive data in bucket credentials
|
||||
* an obscured key contains of 6 'spoiled' characters of the key followed by 'X' characters
|
||||
* '123456XXXXXXXXXXXXXXXXXXXXXXXX'
|
||||
* @param {*} obj
|
||||
* @returns
|
||||
*/
|
||||
function obscureBucketCredentialsSensitiveData(obj) {
|
||||
if (!obj) return obj;
|
||||
const {vendor, service_key, connection_string, secret_access_key} = obj;
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
case 's3_compatible':
|
||||
obj.secret_access_key = obscureKey(secret_access_key);
|
||||
break;
|
||||
case 'google':
|
||||
const o = JSON.parse(service_key);
|
||||
let private_key = o.private_key;
|
||||
if (!isObscureKey(obj)) {
|
||||
const key_header = '-----BEGIN PRIVATE KEY-----\n';
|
||||
private_key = o.private_key.slice(key_header.length, o.private_key.length);
|
||||
private_key = `${key_header}${obscureKey(private_key)}`;
|
||||
}
|
||||
const obscured = {
|
||||
...o,
|
||||
private_key
|
||||
};
|
||||
obj.service_key = JSON.stringify(obscured);
|
||||
break;
|
||||
case 'azure':
|
||||
obj.connection_string = obscureKey(connection_string);
|
||||
break;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
encrypt,
|
||||
decrypt
|
||||
decrypt,
|
||||
obscureKey,
|
||||
isObscureKey,
|
||||
obscureBucketCredentialsSensitiveData,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"trial": [
|
||||
{
|
||||
"category": "voice_call_session",
|
||||
"quantity": 20
|
||||
"quantity": 5
|
||||
},
|
||||
{
|
||||
"category": "device",
|
||||
|
||||
@@ -39,11 +39,17 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
|
||||
const obj = await postJSON('/api/v3/call/transaction', {
|
||||
param: {
|
||||
transaction: {
|
||||
call: true
|
||||
call: true,
|
||||
registration: true,
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: [callId]
|
||||
},
|
||||
'1_registration': {
|
||||
callid: [callId]
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -58,7 +64,7 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getHomerPcap = async(logger, apiKey, callIds) => {
|
||||
const getHomerPcap = async(logger, apiKey, callIds, method) => {
|
||||
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
|
||||
logger.debug('getHomerPcap: Homer integration not installed');
|
||||
}
|
||||
@@ -67,12 +73,23 @@ const getHomerPcap = async(logger, apiKey, callIds) => {
|
||||
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
|
||||
param: {
|
||||
transaction: {
|
||||
call: true
|
||||
call: method === 'invite',
|
||||
registration: method === 'register',
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: callIds
|
||||
}
|
||||
...(method === 'invite' && {
|
||||
'1_call': {
|
||||
callid: callIds
|
||||
}
|
||||
})
|
||||
,
|
||||
...(method === 'register' && {
|
||||
'1_registration': {
|
||||
callid: callIds
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
|
||||
19
lib/utils/jaeger-utils.js
Normal file
19
lib/utils/jaeger-utils.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const bent = require('bent');
|
||||
const getJSON = bent(process.env.JAEGER_BASE_URL || 'http://127.0.0.1', 'GET', 'json', 200);
|
||||
|
||||
const getJaegerTrace = async(logger, traceId) => {
|
||||
if (!process.env.JAEGER_BASE_URL) {
|
||||
logger.debug('getJaegerTrace: jaeger integration not installed');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await getJSON(`/api/v3/traces/${traceId}`);
|
||||
} catch (err) {
|
||||
const url = `${process.env.JAEGER_BASE_URL}/api/traces/${traceId}`;
|
||||
logger.error({err, traceId}, `getJaegerTrace: Error retrieving spans from ${url}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getJaegerTrace
|
||||
};
|
||||
1
lib/utils/jambonz-sample.text
Normal file
1
lib/utils/jambonz-sample.text
Normal file
@@ -0,0 +1 @@
|
||||
Hello From Jambonz. This file was created because Record all call bucket credential test.
|
||||
22
lib/utils/speech-data/stt-assemblyai.js
Normal file
22
lib/utils/speech-data/stt-assemblyai.js
Normal file
@@ -0,0 +1,22 @@
|
||||
module.exports = [
|
||||
{ name: 'Global English', value: 'en' },
|
||||
{ name: 'Australian English', value: 'en_au' },
|
||||
{ name: 'British English', value: 'en_uk' },
|
||||
{ name: 'US English', value: 'en_us' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'Hindi', value: 'hi' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Chinese', value: 'zh' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Ukrainian', value: 'uk' },
|
||||
{ name: 'Vietnamese', value: 'vi' },
|
||||
];
|
||||
10
lib/utils/speech-data/stt-aws.js
Normal file
10
lib/utils/speech-data/stt-aws.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = [
|
||||
{ name: 'Australian English', value: 'en-AU' },
|
||||
{ name: 'British English', value: 'en-GB' },
|
||||
{ name: 'US English', value: 'en-US' },
|
||||
{ name: 'French', value: 'fr-FR' },
|
||||
{ name: 'Canadian French', value: 'fr-CA' },
|
||||
{ name: 'German', value: 'de-DE' },
|
||||
{ name: 'Italian', value: 'it-IT' },
|
||||
{ name: 'US Spanish', value: 'es-US' },
|
||||
];
|
||||
26
lib/utils/speech-data/stt-cobalt.js
Normal file
26
lib/utils/speech-data/stt-cobalt.js
Normal file
@@ -0,0 +1,26 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'English US',
|
||||
value: 'en_US-8khz',
|
||||
},
|
||||
{
|
||||
name: 'English UK',
|
||||
value: 'en_UK-8khz',
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
value: 'es_xx-8khz',
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
value: 'fr_fr-8khz',
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
value: 'ru_ru-8khz',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
value: 'pt_br-8khz',
|
||||
},
|
||||
];
|
||||
138
lib/utils/speech-data/stt-deepgram.js
Normal file
138
lib/utils/speech-data/stt-deepgram.js
Normal file
@@ -0,0 +1,138 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Chinese - general',
|
||||
value: 'zh',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (China)',
|
||||
value: 'zh-CN',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Taiwan)',
|
||||
value: 'zh-TW',
|
||||
},
|
||||
{
|
||||
name: 'Dutch - general',
|
||||
value: 'nl',
|
||||
},
|
||||
{
|
||||
name: 'English - general',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
name: 'English (Australia)',
|
||||
value: 'en-AU',
|
||||
},
|
||||
{
|
||||
name: 'English (United Kingdom)',
|
||||
value: 'en-GB',
|
||||
},
|
||||
{
|
||||
name: 'English (India)',
|
||||
value: 'en-IN',
|
||||
},
|
||||
{
|
||||
name: 'English (New Zealand)',
|
||||
value: 'en-NZ',
|
||||
},
|
||||
{
|
||||
name: 'English (United States)',
|
||||
value: 'en-US',
|
||||
},
|
||||
{
|
||||
name: 'French - general',
|
||||
value: 'fr',
|
||||
},
|
||||
{
|
||||
name: 'French (Canada)',
|
||||
value: 'fr-CA',
|
||||
},
|
||||
{
|
||||
name: 'German - general',
|
||||
value: 'de',
|
||||
},
|
||||
{
|
||||
name: 'Hindi - general',
|
||||
value: 'hi',
|
||||
},
|
||||
{
|
||||
name: 'Hindi (Roman Script)',
|
||||
value: 'hi-Latin',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian - general',
|
||||
value: 'in',
|
||||
},
|
||||
{
|
||||
name: 'Italian - general',
|
||||
value: 'it',
|
||||
},
|
||||
{
|
||||
name: 'Japanese - general',
|
||||
value: 'ja',
|
||||
},
|
||||
{
|
||||
name: 'Korean - general',
|
||||
value: 'ko',
|
||||
},
|
||||
{
|
||||
name: 'Norwegian - general',
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
name: 'Polish - general',
|
||||
value: 'pl',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese - general',
|
||||
value: 'pt',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Brazil)',
|
||||
value: 'pt-BR',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Portugal)',
|
||||
value: 'pt-PT',
|
||||
},
|
||||
{
|
||||
name: 'Russian - general',
|
||||
value: 'ru',
|
||||
},
|
||||
{
|
||||
name: 'Spanish - general',
|
||||
value: 'es',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Latin America)',
|
||||
value: 'es-419',
|
||||
},
|
||||
{
|
||||
name: 'Swedish - general',
|
||||
value: 'sv',
|
||||
},
|
||||
{
|
||||
name: 'Turkish - general',
|
||||
value: 'tr',
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian - general',
|
||||
value: 'uk',
|
||||
},
|
||||
{
|
||||
name: 'Flemish - general',
|
||||
value: 'nl-BE',
|
||||
},
|
||||
{
|
||||
name: 'Danish - general',
|
||||
value: 'da',
|
||||
},
|
||||
{
|
||||
name: 'Tamil - general',
|
||||
value: 'ta',
|
||||
},
|
||||
{
|
||||
name: 'Tamasheq - general',
|
||||
value: 'taq',
|
||||
},
|
||||
];
|
||||
130
lib/utils/speech-data/stt-google.js
Normal file
130
lib/utils/speech-data/stt-google.js
Normal file
@@ -0,0 +1,130 @@
|
||||
module.exports = [
|
||||
{ name: 'Afrikaans (South Africa)', value: 'af-ZA' },
|
||||
{ name: 'Albanian (Albania)', value: 'sq-AL' },
|
||||
{ name: 'Amharic (Ethiopia)', value: 'am-ET' },
|
||||
{ name: 'Arabic (Algeria)', value: 'ar-DZ' },
|
||||
{ name: 'Arabic (Bahrain)', value: 'ar-BH' },
|
||||
{ name: 'Arabic (Egypt)', value: 'ar-EG' },
|
||||
{ name: 'Arabic (Iraq)', value: 'ar-IQ' },
|
||||
{ name: 'Arabic (Israel)', value: 'ar-IL' },
|
||||
{ name: 'Arabic (Jordan)', value: 'ar-JO' },
|
||||
{ name: 'Arabic (Kuwait)', value: 'ar-KW' },
|
||||
{ name: 'Arabic (Lebanon)', value: 'ar-LB' },
|
||||
{ name: 'Arabic (Morocco)', value: 'ar-MA' },
|
||||
{ name: 'Arabic (Oman)', value: 'ar-OM' },
|
||||
{ name: 'Arabic (Qatar)', value: 'ar-QA' },
|
||||
{ name: 'Arabic (Saudi Arabia)', value: 'ar-SA' },
|
||||
{ name: 'Arabic (State of Palestine)', value: 'ar-PS' },
|
||||
{ name: 'Arabic (Tunisia)', value: 'ar-TN' },
|
||||
{ name: 'Arabic (United Arab Emirates)', value: 'ar-AE' },
|
||||
{ name: 'Armenian (Armenia)', value: 'hy-AM' },
|
||||
{ name: 'Azerbaijani (Azerbaijan)', value: 'az-AZ' },
|
||||
{ name: 'Basque (Spain)', value: 'eu-ES' },
|
||||
{ name: 'Bengali (Bangladesh)', value: 'bn-BD' },
|
||||
{ name: 'Bengali (India)', value: 'bn-IN' },
|
||||
{ name: 'Bulgarian (Bulgaria)', value: 'bg-BG' },
|
||||
{ name: 'Burmese (Myanmar)', value: 'my-MM' },
|
||||
{ name: 'Catalan (Spain)', value: 'ca-ES' },
|
||||
{ name: 'Chinese, Cantonese (Traditional, Hong Kong)', value: 'yue-Hant-HK' },
|
||||
{ name: 'Chinese, Mandarin (Simplified, China)', value: 'zh' },
|
||||
{ name: 'Chinese, Mandarin (Simplified, Hong Kong)', value: 'zh-HK' },
|
||||
{ name: 'Chinese, Mandarin (Simplified, Taiwan)', value: 'zh-TW' },
|
||||
{ name: 'Croatian (Croatia)', value: 'hr-HR' },
|
||||
{ name: 'Czech (Czech Republic)', value: 'cs-CZ' },
|
||||
{ name: 'Danish (Denmark)', value: 'da-DK' },
|
||||
{ name: 'Dutch (Belgium)', value: 'nl-BE' },
|
||||
{ name: 'Dutch (Netherlands)', value: 'nl-NL' },
|
||||
{ name: 'English (Australia)', value: 'en-AU' },
|
||||
{ name: 'English (Canada)', value: 'en-CA' },
|
||||
{ name: 'English (Ghana)', value: 'en-GH' },
|
||||
{ name: 'English (India)', value: 'en-IN' },
|
||||
{ name: 'English (Ireland)', value: 'en-IE' },
|
||||
{ name: 'English (Kenya)', value: 'en-KE' },
|
||||
{ name: 'English (New Zealand)', value: 'en-NZ' },
|
||||
{ name: 'English (Nigeria)', value: 'en-NG' },
|
||||
{ name: 'English (Philippines)', value: 'en-PH' },
|
||||
{ name: 'English (Singapore)', value: 'en-SG' },
|
||||
{ name: 'English (South Africa)', value: 'en-ZA' },
|
||||
{ name: 'English (Tanzania)', value: 'en-TZ' },
|
||||
{ name: 'English (United Kingdom)', value: 'en-GB' },
|
||||
{ name: 'English (United States)', value: 'en-US' },
|
||||
{ name: 'Estonian (Estonia)', value: 'et-EE' },
|
||||
{ name: 'Filipino (Philippines)', value: 'fil-PH' },
|
||||
{ name: 'Finnish (Finland)', value: 'fi-FI' },
|
||||
{ name: 'French (Canada)', value: 'fr-CA' },
|
||||
{ name: 'French (France)', value: 'fr-FR' },
|
||||
{ name: 'Galician (Spain)', value: 'gl-ES' },
|
||||
{ name: 'Georgian (Georgia)', value: 'ka-GE' },
|
||||
{ name: 'German (Germany)', value: 'de-DE' },
|
||||
{ name: 'Greek (Greece)', value: 'el-GR' },
|
||||
{ name: 'Gujarati (India)', value: 'gu-IN' },
|
||||
{ name: 'Hebrew (Israel)', value: 'he-IL' },
|
||||
{ name: 'Hindi (India)', value: 'hi-IN' },
|
||||
{ name: 'Hungarian (Hungary)', value: 'hu-HU' },
|
||||
{ name: 'Icelandic (Iceland)', value: 'is-IS' },
|
||||
{ name: 'Indonesian (Indonesia)', value: 'id-ID' },
|
||||
{ name: 'Italian (Italy)', value: 'it-IT' },
|
||||
{ name: 'Japanese (Japan)', value: 'ja-JP' },
|
||||
{ name: 'Javanese (Indonesia)', value: 'jv-ID' },
|
||||
{ name: 'Kannada (India)', value: 'kn-IN' },
|
||||
{ name: 'Khmer (Cambodia)', value: 'km-KH' },
|
||||
{ name: 'Korean (South Korea)', value: 'ko-KR' },
|
||||
{ name: 'Lao (Laos)', value: 'lo-LA' },
|
||||
{ name: 'Latvian (Latvia)', value: 'lv-LV' },
|
||||
{ name: 'Lithuanian (Lithuania)', value: 'lt-LT' },
|
||||
{ name: 'Macedonian (North Macedonia)', value: 'mk-MK' },
|
||||
{ name: 'Malay (Malaysia)', value: 'ms-MY' },
|
||||
{ name: 'Malayalam (India)', value: 'ml-IN' },
|
||||
{ name: 'Marathi (India)', value: 'mr-IN' },
|
||||
{ name: 'Mongolian (Mongolia)', value: 'mn-MN' },
|
||||
{ name: 'Nepali (Nepal)', value: 'ne-NP' },
|
||||
{ name: 'Norwegian Bokmål (Norway)', value: 'nb-NO' },
|
||||
{ name: 'Persian (Iran)', value: 'fa-IR' },
|
||||
{ name: 'Polish (Poland)', value: 'pl-PL' },
|
||||
{ name: 'Portuguese (Brazil)', value: 'pt-BR' },
|
||||
{ name: 'Portuguese (Portugal)', value: 'pt-PT' },
|
||||
{ name: 'Punjabi (Gurmukhi, India)', value: 'pa-guru-IN' },
|
||||
{ name: 'Romanian (Romania)', value: 'ro-RO' },
|
||||
{ name: 'Russian (Russia)', value: 'ru-RU' },
|
||||
{ name: 'Serbian (Serbia)', value: 'sr-RS' },
|
||||
{ name: 'Sinhala (Sri Lanka)', value: 'si-LK' },
|
||||
{ name: 'Slovak (Slovakia)', value: 'sk-SK' },
|
||||
{ name: 'Slovenian (Slovenia)', value: 'sl-SI' },
|
||||
{ name: 'Spanish (Argentina)', value: 'es-AR' },
|
||||
{ name: 'Spanish (Bolivia)', value: 'es-BO' },
|
||||
{ name: 'Spanish (Chile)', value: 'es-CL' },
|
||||
{ name: 'Spanish (Colombia)', value: 'es-CO' },
|
||||
{ name: 'Spanish (Costa Rica)', value: 'es-CR' },
|
||||
{ name: 'Spanish (Dominican Republic)', value: 'es-DO' },
|
||||
{ name: 'Spanish (Ecuador)', value: 'es-EC' },
|
||||
{ name: 'Spanish (El Salvador)', value: 'es-SV' },
|
||||
{ name: 'Spanish (Guatemala)', value: 'es-GT' },
|
||||
{ name: 'Spanish (Honduras)', value: 'es-HN' },
|
||||
{ name: 'Spanish (Mexico)', value: 'es-MX' },
|
||||
{ name: 'Spanish (Nicaragua)', value: 'es-NI' },
|
||||
{ name: 'Spanish (Panama)', value: 'es-PA' },
|
||||
{ name: 'Spanish (Paraguay)', value: 'es-PY' },
|
||||
{ name: 'Spanish (Peru)', value: 'es-PE' },
|
||||
{ name: 'Spanish (Puerto Rico)', value: 'es-PR' },
|
||||
{ name: 'Spanish (Spain)', value: 'es-ES' },
|
||||
{ name: 'Spanish (United States)', value: 'es-US' },
|
||||
{ name: 'Spanish (Uruguay)', value: 'es-UY' },
|
||||
{ name: 'Spanish (Venezuela)', value: 'es-VE' },
|
||||
{ name: 'Sundanese (Indonesia)', value: 'su-ID' },
|
||||
{ name: 'Swahili (Kenya)', value: 'sw-KE' },
|
||||
{ name: 'Swahili (Tanzania)', value: 'sw-TZ' },
|
||||
{ name: 'Swedish (Sweden)', value: 'sv-SE' },
|
||||
{ name: 'Tamil (India)', value: 'ta-IN' },
|
||||
{ name: 'Tamil (Malaysia)', value: 'ta-MY' },
|
||||
{ name: 'Tamil (Singapore)', value: 'ta-SG' },
|
||||
{ name: 'Tamil (Sri Lanka)', value: 'ta-LK' },
|
||||
{ name: 'Telugu (India)', value: 'te-IN' },
|
||||
{ name: 'Thai (Thailand)', value: 'th-TH' },
|
||||
{ name: 'Turkish (Turkey)', value: 'tr-TR' },
|
||||
{ name: 'Ukrainian (Ukraine)', value: 'uk-UA' },
|
||||
{ name: 'Urdu (India)', value: 'ur-IN' },
|
||||
{ name: 'Urdu (Pakistan)', value: 'ur-PK' },
|
||||
{ name: 'Uzbek (Uzbekistan)', value: 'uz-UZ' },
|
||||
{ name: 'Vietnamese (Vietnam)', value: 'vi-VN' },
|
||||
{ name: 'Zulu (South Africa)', value: 'zu-ZA' },
|
||||
];
|
||||
82
lib/utils/speech-data/stt-ibm.js
Normal file
82
lib/utils/speech-data/stt-ibm.js
Normal file
@@ -0,0 +1,82 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Arabic (Modern Standard)',
|
||||
value: 'ar-MS_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Mandarin)',
|
||||
value: 'zh-CN_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Czech ',
|
||||
value: 'cs-CZ_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Dutch (Belgian)',
|
||||
value: 'nl-BE_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Dutch (Netherlands)',
|
||||
value: 'nl-NL_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'English (all supported dialects)',
|
||||
value: 'en-WW_Medical_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'English (Australian)',
|
||||
value: 'en-AU_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'English (Indian)',
|
||||
value: 'en-IN_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'English (United Kingdom)',
|
||||
value: 'en-GB_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'English (United States)',
|
||||
value: 'en-US_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'French (Canadian)',
|
||||
value: 'fr-CA_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'French (France)',
|
||||
value: 'fr-FR_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
value: 'de-DE_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Hindi (Indian)',
|
||||
value: 'hi-IN_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Italian',
|
||||
value: 'it-IT_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
value: 'ko-KR_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Brazilian)',
|
||||
value: 'pt-BR_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Mexican)',
|
||||
value: 'es-LA_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Castilian)',
|
||||
value: 'es-ES_Telephony',
|
||||
},
|
||||
{
|
||||
name: 'Swedish ',
|
||||
value: 'sv-SE_Telephony',
|
||||
},
|
||||
];
|
||||
490
lib/utils/speech-data/stt-microsoft.js
Normal file
490
lib/utils/speech-data/stt-microsoft.js
Normal file
@@ -0,0 +1,490 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Afrikaans (South Africa)',
|
||||
value: 'af-ZA',
|
||||
},
|
||||
{
|
||||
name: 'Amharic (Ethiopia)',
|
||||
value: 'am-ET',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Algeria)',
|
||||
value: 'ar-DZ',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Bahrain)',
|
||||
value: 'ar-BH',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Egypt)',
|
||||
value: 'ar-EG',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Iraq)',
|
||||
value: 'ar-IQ',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Israel)',
|
||||
value: 'ar-IL',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Jordan)',
|
||||
value: 'ar-JO',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Kuwait)',
|
||||
value: 'ar-KW',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Lebanon)',
|
||||
value: 'ar-LB',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Libya)',
|
||||
value: 'ar-LY',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Morocco)',
|
||||
value: 'ar-MA',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Oman)',
|
||||
value: 'ar-OM',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Qatar)',
|
||||
value: 'ar-QA',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Saudi Arabia)',
|
||||
value: 'ar-SA',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Palestinian Authority)',
|
||||
value: 'ar-PS',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Syria)',
|
||||
value: 'ar-SY',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Tunisia)',
|
||||
value: 'ar-TN',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (United Arab Emirates)',
|
||||
value: 'ar-AE',
|
||||
},
|
||||
{
|
||||
name: 'Arabic (Yemen)',
|
||||
value: 'ar-YE',
|
||||
},
|
||||
{
|
||||
name: 'Bulgarian (Bulgaria)',
|
||||
value: 'bg-BG',
|
||||
},
|
||||
{
|
||||
name: 'Bengali (India)',
|
||||
value: 'bn-IN',
|
||||
},
|
||||
{
|
||||
name: 'Catalan (Spain)',
|
||||
value: 'ca-ES',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Cantonese, Traditional)',
|
||||
value: 'zh-HK',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Mandarin, Simplified)',
|
||||
value: 'zh-CN',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Taiwanese Mandarin)',
|
||||
value: 'zh-TW',
|
||||
},
|
||||
{
|
||||
name: 'Croatian (Croatia)',
|
||||
value: 'hr-HR',
|
||||
},
|
||||
{
|
||||
name: 'Czech (Czech)',
|
||||
value: 'cs-CZ',
|
||||
},
|
||||
{
|
||||
name: 'Danish (Denmark)',
|
||||
value: 'da-DK',
|
||||
},
|
||||
{
|
||||
name: 'Dutch (Netherlands)',
|
||||
value: 'nl-NL',
|
||||
},
|
||||
{
|
||||
name: 'Dutch (Belgium)',
|
||||
value: 'nl-BE',
|
||||
},
|
||||
{
|
||||
name: 'English (Australia)',
|
||||
value: 'en-AU',
|
||||
},
|
||||
{
|
||||
name: 'English (Canada)',
|
||||
value: 'en-CA',
|
||||
},
|
||||
{
|
||||
name: 'English (Ghana)',
|
||||
value: 'en-GH',
|
||||
},
|
||||
{
|
||||
name: 'English (Hong Kong)',
|
||||
value: 'en-HK',
|
||||
},
|
||||
{
|
||||
name: 'English (India)',
|
||||
value: 'en-IN',
|
||||
},
|
||||
{
|
||||
name: 'English (Ireland)',
|
||||
value: 'en-IE',
|
||||
},
|
||||
{
|
||||
name: 'English (Kenya)',
|
||||
value: 'en-KE',
|
||||
},
|
||||
{
|
||||
name: 'English (New Zealand)',
|
||||
value: 'en-NZ',
|
||||
},
|
||||
{
|
||||
name: 'English (Nigeria)',
|
||||
value: 'en-NG',
|
||||
},
|
||||
{
|
||||
name: 'English (Philippines)',
|
||||
value: 'en-PH',
|
||||
},
|
||||
{
|
||||
name: 'English (Singapore)',
|
||||
value: 'en-SG',
|
||||
},
|
||||
{
|
||||
name: 'English (South Africa)',
|
||||
value: 'en-ZA',
|
||||
},
|
||||
{
|
||||
name: 'English (Tanzania)',
|
||||
value: 'en-TZ',
|
||||
},
|
||||
{
|
||||
name: 'English (United Kingdom)',
|
||||
value: 'en-GB',
|
||||
},
|
||||
{
|
||||
name: 'English (United States)',
|
||||
value: 'en-US',
|
||||
},
|
||||
{
|
||||
name: 'Estonian(Estonia)',
|
||||
value: 'et-EE',
|
||||
},
|
||||
{
|
||||
name: 'Filipino (Philippines)',
|
||||
value: 'fil-PH',
|
||||
},
|
||||
{
|
||||
name: 'Finnish (Finland)',
|
||||
value: 'fi-FI',
|
||||
},
|
||||
{
|
||||
name: 'French (Belgium)',
|
||||
value: 'fr-BE',
|
||||
},
|
||||
{
|
||||
name: 'French (Canada)',
|
||||
value: 'fr-CA',
|
||||
},
|
||||
{
|
||||
name: 'French (France)',
|
||||
value: 'fr-FR',
|
||||
},
|
||||
{
|
||||
name: 'French (Switzerland)',
|
||||
value: 'fr-CH',
|
||||
},
|
||||
{
|
||||
name: 'German (Austria)',
|
||||
value: 'de-AT',
|
||||
},
|
||||
{
|
||||
name: 'German (Switzerland)',
|
||||
value: 'de-CH',
|
||||
},
|
||||
{
|
||||
name: 'German (Germany)',
|
||||
value: 'de-DE',
|
||||
},
|
||||
{
|
||||
name: 'Greek (Greece)',
|
||||
value: 'el-GR',
|
||||
},
|
||||
{
|
||||
name: 'Gujarati (Indian)',
|
||||
value: 'gu-IN',
|
||||
},
|
||||
{
|
||||
name: 'Hebrew (Israel)',
|
||||
value: 'he-IL',
|
||||
},
|
||||
{
|
||||
name: 'Hindi (India)',
|
||||
value: 'hi-IN',
|
||||
},
|
||||
{
|
||||
name: 'Hungarian (Hungary)',
|
||||
value: 'hu-HU',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian (Indonesia)',
|
||||
value: 'id-ID',
|
||||
},
|
||||
{
|
||||
name: 'Icelandic (Iceland)',
|
||||
value: 'is-IS',
|
||||
},
|
||||
{
|
||||
name: 'Irish (Ireland)',
|
||||
value: 'ga-IE',
|
||||
},
|
||||
{
|
||||
name: 'Italian (Italy)',
|
||||
value: 'it-IT',
|
||||
},
|
||||
{
|
||||
name: 'Japanese (Japan)',
|
||||
value: 'ja-JP',
|
||||
},
|
||||
{
|
||||
name: 'Javanese (Indonesia)',
|
||||
value: 'jv-ID',
|
||||
},
|
||||
{
|
||||
name: 'Kannada (India)',
|
||||
value: 'kn-IN',
|
||||
},
|
||||
{
|
||||
name: 'Khmer (Cambodia)',
|
||||
value: 'km-KH',
|
||||
},
|
||||
{
|
||||
name: 'Korean (Korea)',
|
||||
value: 'ko-KR',
|
||||
},
|
||||
{
|
||||
name: 'Latvian (Latvia)',
|
||||
value: 'lv-LV',
|
||||
},
|
||||
{
|
||||
name: 'Lao (Laos)',
|
||||
value: 'lo-LA',
|
||||
},
|
||||
{
|
||||
name: 'Lithuanian (Lithuania)',
|
||||
value: 'lt-LT',
|
||||
},
|
||||
{
|
||||
name: 'Malay (Malaysia)',
|
||||
value: 'ms-MY',
|
||||
},
|
||||
{
|
||||
name: 'Macedonian (North Macedonia)',
|
||||
value: 'mk-MK',
|
||||
},
|
||||
{
|
||||
name: 'Maltese (Malta)',
|
||||
value: 'mt-MT',
|
||||
},
|
||||
{
|
||||
name: 'Marathi (India)',
|
||||
value: 'mr-IN',
|
||||
},
|
||||
{
|
||||
name: 'Burmese (Myanmar)',
|
||||
value: 'my-MM',
|
||||
},
|
||||
{
|
||||
name: 'Norwegian (Bokmål, Norway)',
|
||||
value: 'nb-NO',
|
||||
},
|
||||
{
|
||||
name: 'Persian (Iran)',
|
||||
value: 'fa-IR',
|
||||
},
|
||||
{
|
||||
name: 'Polish (Poland)',
|
||||
value: 'pl-PL',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Brazil)',
|
||||
value: 'pt-BR',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Portugal)',
|
||||
value: 'pt-PT',
|
||||
},
|
||||
{
|
||||
name: 'Romanian (Romania)',
|
||||
value: 'ro-RO',
|
||||
},
|
||||
{
|
||||
name: 'Russian (Russia)',
|
||||
value: 'ru-RU',
|
||||
},
|
||||
{
|
||||
name: 'Slovak (Slovakia)',
|
||||
value: 'sk-SK',
|
||||
},
|
||||
{
|
||||
name: 'Slovenian (Slovenia)',
|
||||
value: 'sl-SI',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Argentina)',
|
||||
value: 'es-AR',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Bolivia)',
|
||||
value: 'es-BO',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Chile)',
|
||||
value: 'es-CL',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Colombia)',
|
||||
value: 'es-CO',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Costa Rica)',
|
||||
value: 'es-CR',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Cuba)',
|
||||
value: 'es-CU',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Dominican Republic)',
|
||||
value: 'es-DO',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Ecuador)',
|
||||
value: 'es-EC',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (El Salvador)',
|
||||
value: 'es-SV',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Equatorial Guinea)',
|
||||
value: 'es-GQ',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Guatemala)',
|
||||
value: 'es-GT',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Honduras)',
|
||||
value: 'es-HN',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Mexico)',
|
||||
value: 'es-MX',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Nicaragua)',
|
||||
value: 'es-NI',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Panama)',
|
||||
value: 'es-PA',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Paraguay)',
|
||||
value: 'es-PY',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Peru)',
|
||||
value: 'es-PE',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Puerto Rico)',
|
||||
value: 'es-PR',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Spain)',
|
||||
value: 'es-ES',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Uruguay)',
|
||||
value: 'es-UY',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (USA)',
|
||||
value: 'es-US',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Venezuela)',
|
||||
value: 'es-VE',
|
||||
},
|
||||
{
|
||||
name: 'Swahili (Kenya)',
|
||||
value: 'sw-KE',
|
||||
},
|
||||
{
|
||||
name: 'Swahili (Tanzania)',
|
||||
value: 'sw-TZ',
|
||||
},
|
||||
{
|
||||
name: 'Sinhala (Sri Lanka)',
|
||||
value: 'si-LK',
|
||||
},
|
||||
{
|
||||
name: 'Swedish (Sweden)',
|
||||
value: 'sv-SE',
|
||||
},
|
||||
{
|
||||
name: 'Serbian (Serbia)',
|
||||
value: 'sr-RS',
|
||||
},
|
||||
{
|
||||
name: 'Tamil (India)',
|
||||
value: 'ta-IN',
|
||||
},
|
||||
{
|
||||
name: 'Telugu (India)',
|
||||
value: 'te-IN',
|
||||
},
|
||||
{
|
||||
name: 'Thai (Thailand)',
|
||||
value: 'th-TH',
|
||||
},
|
||||
{
|
||||
name: 'Turkish (Turkey)',
|
||||
value: 'tr-TR',
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian (Ukraine)',
|
||||
value: 'uk-UA',
|
||||
},
|
||||
{
|
||||
name: 'Uzbek (Uzbekistan)',
|
||||
value: 'uz-UZ',
|
||||
},
|
||||
{
|
||||
name: 'Zulu (South Africa)',
|
||||
value: 'zu-ZA',
|
||||
},
|
||||
{
|
||||
name: 'Vietnamese (Vietnam)',
|
||||
value: 'vi-VN',
|
||||
},
|
||||
];
|
||||
207
lib/utils/speech-data/stt-nuance.js
Normal file
207
lib/utils/speech-data/stt-nuance.js
Normal file
@@ -0,0 +1,207 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Arabic (Worldwide)',
|
||||
value: 'ar-WW',
|
||||
valueMix: 'ara-XWW',
|
||||
},
|
||||
{
|
||||
name: 'Catalan (Spain)',
|
||||
value: 'ca-ES',
|
||||
valueMix: 'cat-ESP',
|
||||
},
|
||||
{
|
||||
name: 'Croatian (Croatia)',
|
||||
value: 'hr-HR',
|
||||
valueMix: 'hrv-HRV',
|
||||
},
|
||||
{
|
||||
name: 'Czech (Czech Republic)',
|
||||
value: 'cs-CZ',
|
||||
valueMix: 'ces-CZE',
|
||||
},
|
||||
{
|
||||
name: 'Danish (Denmark)',
|
||||
value: 'da-DK',
|
||||
valueMix: 'dan-DNK',
|
||||
},
|
||||
{
|
||||
name: 'Dutch (Netherlands)',
|
||||
value: 'nl-NL',
|
||||
valueMix: 'nld-NLD',
|
||||
},
|
||||
{
|
||||
name: 'English (Australia)',
|
||||
value: 'en-AU',
|
||||
valueMix: 'eng-AUS',
|
||||
},
|
||||
{
|
||||
name: 'English (United States)',
|
||||
value: 'en-US',
|
||||
valueMix: 'eng-USA',
|
||||
},
|
||||
{
|
||||
name: 'English (India)',
|
||||
value: 'en-IN',
|
||||
valueMix: 'eng-IND',
|
||||
},
|
||||
{
|
||||
name: 'English (United Kingdom)',
|
||||
value: 'en-GB',
|
||||
valueMix: 'eng-GBR',
|
||||
},
|
||||
{
|
||||
name: 'Finnish (Finland)',
|
||||
value: 'fi-FI',
|
||||
valueMix: 'fin-FIN',
|
||||
},
|
||||
{
|
||||
name: 'French (Canada)',
|
||||
value: 'fr-CA',
|
||||
valueMix: 'fra-CAN',
|
||||
},
|
||||
{
|
||||
name: 'French (France)',
|
||||
value: 'fr-FR',
|
||||
valueMix: 'fra-FRA',
|
||||
},
|
||||
{
|
||||
name: 'German (Germany)',
|
||||
value: 'de-DE',
|
||||
valueMix: 'deu-DEU',
|
||||
},
|
||||
{
|
||||
name: 'Greek (Greece)',
|
||||
value: 'el-GR',
|
||||
valueMix: 'ell-GRC',
|
||||
},
|
||||
{
|
||||
name: 'Hebrew (Israel)',
|
||||
value: 'he-IL',
|
||||
valueMix: 'heb-ISR',
|
||||
},
|
||||
{
|
||||
name: 'Hindi (India)',
|
||||
value: 'hi-IN',
|
||||
valueMix: 'hin-IND',
|
||||
},
|
||||
{
|
||||
name: 'Hungarian (Hungary)',
|
||||
value: 'hu-HU',
|
||||
valueMix: 'hun-HUN',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian (Indonesia)',
|
||||
value: 'id-ID',
|
||||
valueMix: 'ind-IDN',
|
||||
},
|
||||
{
|
||||
name: 'Italian (Italy)',
|
||||
value: 'it-IT',
|
||||
valueMix: 'ita-ITA',
|
||||
},
|
||||
{
|
||||
name: 'Japanese (Japan)',
|
||||
value: 'ja-JP',
|
||||
valueMix: 'jpn-JPN',
|
||||
},
|
||||
{
|
||||
name: 'Korean (South Korea)',
|
||||
value: 'ko-KR',
|
||||
valueMix: 'kor-KOR',
|
||||
},
|
||||
{
|
||||
name: 'Malay (Malaysia)',
|
||||
value: 'ms-MY',
|
||||
valueMix: 'zlm-MYS',
|
||||
},
|
||||
{
|
||||
name: 'Norwegian (Norway)',
|
||||
value: 'no-NO',
|
||||
valueMix: 'nor-NOR',
|
||||
},
|
||||
{
|
||||
name: 'Polish (Poland)',
|
||||
value: 'pl-PL',
|
||||
valueMix: 'pol-POL',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Brazil)',
|
||||
value: 'pt-BR',
|
||||
valueMix: 'por-BRA',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Portugal)',
|
||||
value: 'pt-PT',
|
||||
valueMix: 'por-PRT',
|
||||
},
|
||||
{
|
||||
name: 'Romanian (Romania)',
|
||||
value: 'ro-RO',
|
||||
valueMix: 'ron-ROU',
|
||||
},
|
||||
{
|
||||
name: 'Russian (Russia)',
|
||||
value: 'ru-RU',
|
||||
valueMix: 'rus-RUS',
|
||||
},
|
||||
{
|
||||
name: 'Shanghainese (China)',
|
||||
value: 'zh-WU',
|
||||
valueMix: 'wuu-CHN',
|
||||
},
|
||||
{
|
||||
name: 'Mandarin (China)',
|
||||
value: 'zh-CN',
|
||||
valueMix: 'cmn-CHN',
|
||||
},
|
||||
{
|
||||
name: 'Slovak (Slovakia)',
|
||||
value: 'sk-SK',
|
||||
valueMix: 'slk-SVK',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Spain)',
|
||||
value: 'es-ES',
|
||||
valueMix: 'spa-ESP',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Latin America)',
|
||||
value: 'es-US',
|
||||
valueMix: 'spa-XLA',
|
||||
},
|
||||
{
|
||||
name: 'Swedish (Sweden)',
|
||||
value: 'sv-SE',
|
||||
valueMix: 'swe-SWE',
|
||||
},
|
||||
{
|
||||
name: 'Thai (Thailand)',
|
||||
value: 'th-TH',
|
||||
valueMix: 'tha-THA',
|
||||
},
|
||||
{
|
||||
name: 'Cantonese (Hong Kong)',
|
||||
value: 'cn-HK',
|
||||
valueMix: 'yue-CHS',
|
||||
},
|
||||
{
|
||||
name: 'Mandarin (Taiwan)',
|
||||
value: 'zh-TW',
|
||||
valueMix: 'cmn-TWN',
|
||||
},
|
||||
{
|
||||
name: 'Turkish (Turkey)',
|
||||
value: 'tr-TR',
|
||||
valueMix: 'tur-TUR',
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian (Ukraine)',
|
||||
value: 'uk-UA',
|
||||
valueMix: 'ukr-UKR',
|
||||
},
|
||||
{
|
||||
name: 'Vietnamese (Vietnam)',
|
||||
value: 'vi-VN',
|
||||
valueMix: 'vie-VNM',
|
||||
},
|
||||
];
|
||||
58
lib/utils/speech-data/stt-nvidia.js
Normal file
58
lib/utils/speech-data/stt-nvidia.js
Normal file
@@ -0,0 +1,58 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Arabic',
|
||||
value: 'ar-AR',
|
||||
},
|
||||
{
|
||||
name: 'English',
|
||||
value: 'en-US',
|
||||
},
|
||||
{
|
||||
name: 'English - GB',
|
||||
value: 'en-GB',
|
||||
},
|
||||
{
|
||||
name: 'Spanish - US',
|
||||
value: 'es-US',
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
value: 'es-ES',
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
value: 'de-DE',
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
value: 'fr-FR',
|
||||
},
|
||||
{
|
||||
name: 'Hindi',
|
||||
value: 'hi-IN',
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
value: 'ru-RU',
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
value: 'ko-KR',
|
||||
},
|
||||
{
|
||||
name: 'Brazilian-Portuguese',
|
||||
value: 'pt-BR',
|
||||
},
|
||||
{
|
||||
name: 'Japanese',
|
||||
value: 'ja-JP',
|
||||
},
|
||||
{
|
||||
name: 'Italian',
|
||||
value: 'it-IT',
|
||||
},
|
||||
{
|
||||
name: 'Mandarin',
|
||||
value: 'zh-CN',
|
||||
},
|
||||
];
|
||||
6
lib/utils/speech-data/stt-soniox.js
Normal file
6
lib/utils/speech-data/stt-soniox.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'English (United States)',
|
||||
value: 'en-US',
|
||||
},
|
||||
];
|
||||
218
lib/utils/speech-data/stt-speechmatics.js
Normal file
218
lib/utils/speech-data/stt-speechmatics.js
Normal file
@@ -0,0 +1,218 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Automatic',
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
name: 'Arabic',
|
||||
value: 'ar',
|
||||
},
|
||||
{
|
||||
name: 'Bashkir',
|
||||
value: 'ba',
|
||||
},
|
||||
{
|
||||
name: 'Basque',
|
||||
value: 'eu',
|
||||
},
|
||||
{
|
||||
name: 'Belarusian',
|
||||
value: 'be',
|
||||
},
|
||||
{
|
||||
name: 'Bulgarian',
|
||||
value: 'bg',
|
||||
},
|
||||
{
|
||||
name: 'Cantonese',
|
||||
value: 'yue',
|
||||
},
|
||||
{
|
||||
name: 'Catalan',
|
||||
value: 'ca',
|
||||
},
|
||||
{
|
||||
name: 'Croatian',
|
||||
value: 'hr',
|
||||
},
|
||||
{
|
||||
name: 'Czech',
|
||||
value: 'cs',
|
||||
},
|
||||
{
|
||||
name: 'Danish',
|
||||
value: 'da',
|
||||
},
|
||||
{
|
||||
name: 'Dutch',
|
||||
value: 'nl',
|
||||
},
|
||||
{
|
||||
name: 'English',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
name: 'Esperanto',
|
||||
value: 'eo',
|
||||
},
|
||||
{
|
||||
name: 'Estonian',
|
||||
value: 'et',
|
||||
},
|
||||
{
|
||||
name: 'Finnish',
|
||||
value: 'fi',
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
value: 'fr',
|
||||
},
|
||||
{
|
||||
name: 'Galician',
|
||||
value: 'gl',
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
value: 'de',
|
||||
},
|
||||
{
|
||||
name: 'Greek',
|
||||
value: 'el',
|
||||
},
|
||||
{
|
||||
name: 'Hebrew',
|
||||
value: 'he',
|
||||
},
|
||||
{
|
||||
name: 'Hindi',
|
||||
value: 'hi',
|
||||
},
|
||||
{
|
||||
name: 'Hungarian',
|
||||
value: 'hu',
|
||||
},
|
||||
{
|
||||
name: 'Irish',
|
||||
value: 'ga',
|
||||
},
|
||||
{
|
||||
name: 'Interlingua',
|
||||
value: 'ia',
|
||||
},
|
||||
{
|
||||
name: 'Italian',
|
||||
value: 'it',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian',
|
||||
value: 'id',
|
||||
},
|
||||
{
|
||||
name: 'Japanese',
|
||||
value: 'ja',
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
value: 'ko',
|
||||
},
|
||||
{
|
||||
name: 'Latvian',
|
||||
value: 'lv',
|
||||
},
|
||||
{
|
||||
name: 'Lithuanian',
|
||||
value: 'lt',
|
||||
},
|
||||
{
|
||||
name: 'Maltese',
|
||||
value: 'mt',
|
||||
},
|
||||
{
|
||||
name: 'Malay',
|
||||
value: 'ms',
|
||||
},
|
||||
{
|
||||
name: 'Mandarin',
|
||||
value: 'cmn',
|
||||
},
|
||||
{
|
||||
name: 'Marathi',
|
||||
value: 'mr',
|
||||
},
|
||||
{
|
||||
name: 'Mongolian',
|
||||
value: 'mn',
|
||||
},
|
||||
{
|
||||
name: 'Norwegian',
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
name: 'Persian',
|
||||
value: 'fa',
|
||||
},
|
||||
{
|
||||
name: 'Polish',
|
||||
value: 'pl',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
value: 'pt',
|
||||
},
|
||||
{
|
||||
name: 'Romanian',
|
||||
value: 'ro',
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
value: 'ru',
|
||||
},
|
||||
{
|
||||
name: 'Slovakian',
|
||||
value: 'sk',
|
||||
},
|
||||
{
|
||||
name: 'Slovenian',
|
||||
value: 'sl',
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
value: 'es',
|
||||
},
|
||||
{
|
||||
name: 'Spanish & English bilingual',
|
||||
value: 'es',
|
||||
},
|
||||
{
|
||||
name: 'Swedish',
|
||||
value: 'sv',
|
||||
},
|
||||
{
|
||||
name: 'Tamil',
|
||||
value: 'ta',
|
||||
},
|
||||
{
|
||||
name: 'Thai',
|
||||
value: 'th',
|
||||
},
|
||||
{
|
||||
name: 'Turkish',
|
||||
value: 'tr',
|
||||
},
|
||||
{
|
||||
name: 'Uyghur',
|
||||
value: 'ug',
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian',
|
||||
value: 'uk',
|
||||
},
|
||||
{
|
||||
name: 'Vietnamese',
|
||||
value: 'vi',
|
||||
},
|
||||
{
|
||||
name: 'Welsh',
|
||||
value: 'cy',
|
||||
},
|
||||
];
|
||||
14
lib/utils/speech-data/stt-verbio.js
Normal file
14
lib/utils/speech-data/stt-verbio.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = [
|
||||
{ name: 'US English', value: 'en-US' },
|
||||
{ name: 'British English', value: 'en-GB' },
|
||||
{ name: 'LATAM Spanish', value: 'en-USes-419' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Catalan', value: 'ca-ES', version: 'v2' },
|
||||
{ name: 'Brazilian Portuguese', value: 'pt-BR' },
|
||||
{ name: 'French', value: 'fr', version: 'v1' },
|
||||
{ name: 'Canadian French', value: 'fr-CA', version: 'v1' },
|
||||
{ name: 'German', value: 'de', version: 'v1' },
|
||||
{ name: 'Italian', value: 'it', version: 'v1' },
|
||||
{ name: 'Turkish', value: 'tr', version: 'v1' },
|
||||
{ name: 'Japanese', value: 'ja', version: 'v1' },
|
||||
];
|
||||
8
lib/utils/speech-data/stt-voxist.js
Normal file
8
lib/utils/speech-data/stt-voxist.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = [
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Spanish', value: 'sp' },
|
||||
];
|
||||
213
lib/utils/speech-data/tts-aws.js
Normal file
213
lib/utils/speech-data/tts-aws.js
Normal file
@@ -0,0 +1,213 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'arb',
|
||||
name: 'Arabic',
|
||||
voices: [{ value: 'Zeina', name: 'Zeina (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'cmn-CN',
|
||||
name: 'Chinese, Mandarin',
|
||||
voices: [{ value: 'Zhiyu', name: 'Zhiyu (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'da-DK',
|
||||
name: 'Danish',
|
||||
voices: [
|
||||
{ value: 'Naja', name: 'Naja (Female)' },
|
||||
{ value: 'Mads', name: 'Mads (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'nl-NL',
|
||||
name: 'Dutch',
|
||||
voices: [
|
||||
{ value: 'Lotte', name: 'Lotte (Female)' },
|
||||
{ value: 'Ruben', name: 'Ruben (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-AU',
|
||||
name: 'English (Australian)',
|
||||
voices: [
|
||||
{ value: 'Nicole', name: 'Nicole (Female)' },
|
||||
{ value: 'Russell', name: 'Russell (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-GB',
|
||||
name: 'English (British)',
|
||||
voices: [
|
||||
{ value: 'Amy', name: 'Amy (Female)' },
|
||||
{ value: 'Emma', name: 'Emma (Female)' },
|
||||
{ value: 'Brian', name: 'Brian (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-IN',
|
||||
name: 'English (Indian)',
|
||||
voices: [
|
||||
{ value: 'Aditi', name: 'Aditi (Female)' },
|
||||
{ value: 'Raveena', name: 'Raveena (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'English (US)',
|
||||
voices: [
|
||||
{ value: 'Joanna', name: 'Joanna (Female)' },
|
||||
{ value: 'Kendra', name: 'Kendra (Female)' },
|
||||
{ value: 'Kimberly', name: 'Kimberly (Female)' },
|
||||
{ value: 'Ivy', name: 'Ivy (Female child)' },
|
||||
{ value: 'Salli', name: 'Salli (Female)' },
|
||||
{ value: 'Joey', name: 'Joey (Male)' },
|
||||
{ value: 'Matthew', name: 'Matthew (Male)' },
|
||||
{ value: 'Justin', name: 'Justin (Male child)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-GB-WLS',
|
||||
name: 'English (Welsh)',
|
||||
voices: [{ value: 'Geraint', name: 'Geraint (Male)' }],
|
||||
},
|
||||
{
|
||||
value: 'fr-FR',
|
||||
name: 'French',
|
||||
voices: [
|
||||
{ value: 'Celine', name: 'Céline (Female)' },
|
||||
{ value: 'Lea', name: 'Léa (Female)' },
|
||||
{ value: 'Mathieu', name: 'Mathieu (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fr-CA',
|
||||
name: 'French (Canadian)',
|
||||
voices: [{ value: 'Chantal', name: 'Chantal (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'de-DE',
|
||||
name: 'German',
|
||||
voices: [
|
||||
{ value: 'Marlene', name: 'Marlene (Female)' },
|
||||
{ value: 'Vicki', name: 'Vicki (Female)' },
|
||||
{ value: 'Hans', name: 'Hans (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'hi-IN',
|
||||
name: 'Hindi',
|
||||
voices: [{ value: 'Aditi', name: 'Aditi (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'is-IS',
|
||||
name: 'Icelandic',
|
||||
voices: [
|
||||
{ value: 'Dora', name: 'Dóra (Female)' },
|
||||
{ value: 'Karl', name: 'Karl (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'it-IT',
|
||||
name: 'Italian',
|
||||
voices: [
|
||||
{ value: 'Carla', name: 'Carla (Female)' },
|
||||
{ value: 'Bianca', name: 'Bianca (Female)' },
|
||||
{ value: 'Giorgio', name: 'Giorgio (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ja-JP',
|
||||
name: 'Japanese',
|
||||
voices: [
|
||||
{ value: 'Mizuki', name: 'Mizuki (Female)' },
|
||||
{ value: 'Takumi', name: 'Takumi (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ko-KR',
|
||||
name: 'Korean',
|
||||
voices: [{ value: 'Seoyeon', name: 'Seoyeon (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'nb-NO',
|
||||
name: 'Norwegian',
|
||||
voices: [{ value: 'Liv', name: 'Liv (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'pl-PL',
|
||||
name: 'Polish',
|
||||
voices: [
|
||||
{ value: 'Ewa', name: 'Ewa (Female)' },
|
||||
{ value: 'Maja', name: 'Maja (Female)' },
|
||||
{ value: 'Jacek', name: 'Jacek (Male)' },
|
||||
{ value: 'Jan', name: 'Jan (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'pt-BR',
|
||||
name: 'Portuguese (Brazilian)',
|
||||
voices: [
|
||||
{ value: 'Camila', name: 'Camila (Female)' },
|
||||
{ value: 'Vitoria', name: 'Vitória (Female)' },
|
||||
{ value: 'Ricardo', name: 'Ricardo (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'pt-PT',
|
||||
name: 'Portuguese (European)',
|
||||
voices: [
|
||||
{ value: 'Ines', name: 'Inês (Female)' },
|
||||
{ value: 'Cristiano', name: 'Cristiano (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ro-RO',
|
||||
name: 'Romanian',
|
||||
voices: [{ value: 'Carmen', name: 'Carmen (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'ru-RU',
|
||||
name: 'Russian',
|
||||
voices: [
|
||||
{ value: 'Tatyana', name: 'Tatyana (Female)' },
|
||||
{ value: 'Maxim', name: 'Maxim (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es-ES',
|
||||
name: 'Spanish (European)',
|
||||
voices: [
|
||||
{ value: 'Conchita', name: 'Conchita (Female)' },
|
||||
{ value: 'Lucia', name: 'Lucia (Female)' },
|
||||
{ value: 'Enrique', name: 'Enrique (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es-MX',
|
||||
name: 'Spanish (Mexican)',
|
||||
voices: [{ value: 'Mia', name: 'Mia (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'es-US',
|
||||
name: 'Spanish (US)',
|
||||
voices: [
|
||||
{ value: 'Lupe', name: 'Lupe (Female)' },
|
||||
{ value: 'Penelope', name: 'Penélope (Female)' },
|
||||
{ value: 'Miguel', name: 'Miguel (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'sv-SE',
|
||||
name: 'Swedish',
|
||||
voices: [{ value: 'Astrid', name: 'Astrid (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'tr-TR',
|
||||
name: 'Turkish',
|
||||
voices: [{ value: 'Filiz', name: 'Filiz (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'cy-GB',
|
||||
name: 'Welsh',
|
||||
voices: [{ value: 'Gwyneth', name: 'Gwyneth (Female)' }],
|
||||
},
|
||||
];
|
||||
301
lib/utils/speech-data/tts-cartesia.js
Normal file
301
lib/utils/speech-data/tts-cartesia.js
Normal file
@@ -0,0 +1,301 @@
|
||||
/* eslint-disable max-len */
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en',
|
||||
name: 'English',
|
||||
voices: [
|
||||
{
|
||||
value: '79f8b5fb-2cc8-479a-80df-29f7a7cf1a3e',
|
||||
name: 'Nonfiction Man - This voice is smooth, confident, and resonant, perfect for narrating educational content',
|
||||
},
|
||||
{
|
||||
value: 'e00d0e4c-a5c8-443f-a8a3-473eb9a62355',
|
||||
name: 'Friendly Sidekick - This voice is friendly and supportive, designed for voicing characters in games and videos',
|
||||
},
|
||||
{
|
||||
value: '3b554273-4299-48b9-9aaf-eefd438e3941',
|
||||
name: 'Indian Lady - This voice is young, rich, and curious, perfect for a narrator or fictional character',
|
||||
},
|
||||
{
|
||||
value: '71a7ad14-091c-4e8e-a314-022ece01c121',
|
||||
name: 'British Reading Lady - This is a calm and elegant voice with a British accent, perfect for storytelling and narration',
|
||||
},
|
||||
{
|
||||
value: '4d2fd738-3b3d-4368-957a-bb4805275bd9',
|
||||
name: 'British Narration Lady - This is a neutral voice with a British accent, perfect for narrations ',
|
||||
},
|
||||
{
|
||||
value: '15a9cd88-84b0-4a8b-95f2-5d583b54c72e',
|
||||
name: 'Reading Lady - This voice is monotone and deliberate, perfect for a slower-paced and more serious reading voice',
|
||||
},
|
||||
{
|
||||
value: 'd46abd1d-2d02-43e8-819f-51fb652c1c61',
|
||||
name: 'Newsman - This voice is neutral and educational, perfect for a news anchor',
|
||||
},
|
||||
{
|
||||
value: '2ee87190-8f84-4925-97da-e52547f9462c',
|
||||
name: 'Child - This voice is young and full, perfect for a child',
|
||||
},
|
||||
{
|
||||
value: 'cd17ff2d-5ea4-4695-be8f-42193949b946',
|
||||
name: 'Meditation Lady - This voice is calm, soothing, and relaxing, perfect for meditation',
|
||||
},
|
||||
{
|
||||
value: '5345cf08-6f37-424d-a5d9-8ae1101b9377',
|
||||
name: 'Maria - This voice is laid back, natural, and conversational, like you\'re catching up with a good friend',
|
||||
},
|
||||
{
|
||||
value: '41534e16-2966-4c6b-9670-111411def906',
|
||||
name: '1920\'s Radioman - This voice is energetic and confident, great for an entertainer or radio host',
|
||||
},
|
||||
{
|
||||
value: 'bf991597-6c13-47e4-8411-91ec2de5c466',
|
||||
name: 'Newslady - This voice is authoritative and educational, perfect for a news anchor',
|
||||
},
|
||||
{
|
||||
value: '00a77add-48d5-4ef6-8157-71e5437b282d',
|
||||
name: 'Calm Lady - This voice is calm and nurturing, perfect for a narrator',
|
||||
},
|
||||
{
|
||||
value: '156fb8d2-335b-4950-9cb3-a2d33befec77',
|
||||
name: 'Helpful Woman - This voice is friendly and conversational, designed for customer support agents and casual conversations',
|
||||
},
|
||||
{
|
||||
value: '36b42fcb-60c5-4bec-b077-cb1a00a92ec6',
|
||||
name: 'Pilot over Intercom - This voice sounds like a British Pilot character speaking over an Intercom',
|
||||
},
|
||||
{
|
||||
value: 'f146dcec-e481-45be-8ad2-96e1e40e7f32',
|
||||
name: 'Reading Man - Male with calm narrational voice.',
|
||||
},
|
||||
{
|
||||
value: '34575e71-908f-4ab6-ab54-b08c95d6597d',
|
||||
name: 'New York Man - This voice is compelling and husky, with a New York accent, perfect for sales pitches and motivational content',
|
||||
},
|
||||
{
|
||||
value: 'a0e99841-438c-4a64-b679-ae501e7d6091',
|
||||
name: 'Barbershop Man - This voice is smooth and relaxing, perfect for a casual conversation',
|
||||
},
|
||||
{
|
||||
value: '638efaaa-4d0c-442e-b701-3fae16aad012',
|
||||
name: 'Indian Man - This voice is smooth with an Indian accent, perfect for a narrator',
|
||||
},
|
||||
{
|
||||
value: '41f3c367-e0a8-4a85-89e0-c27bae9c9b6d',
|
||||
name: 'Australian Customer Support Man - This voice is warm with an Australian accent, perfect for customer support agents',
|
||||
},
|
||||
{
|
||||
value: '421b3369-f63f-4b03-8980-37a44df1d4e8',
|
||||
name: 'Friendly Australian Man - This voice is rich and deep, with an Australian accent, perfect for casual conversations with a friend',
|
||||
},
|
||||
{
|
||||
value: 'b043dea0-a007-4bbe-a708-769dc0d0c569',
|
||||
name: 'Wise Man - This is a deep and deliberate voice, suited for educational content and conversations',
|
||||
},
|
||||
{
|
||||
value: '69267136-1bdc-412f-ad78-0caad210fb40',
|
||||
name: 'Friendly Reading Man - This voice is energetic and friendly, like having your friend read his favorite book to you',
|
||||
},
|
||||
{
|
||||
value: 'a167e0f3-df7e-4d52-a9c3-f949145efdab',
|
||||
name: 'Customer Support Man - This voice is clear and calm, perfect for a call center',
|
||||
},
|
||||
{
|
||||
value: '4f8651b0-bbbd-46ac-8b37-5168c5923303',
|
||||
name: 'Kentucky Woman - This voice is energetic and upbeat, with a slight Kentucky accent, perfect for speeches and rallies',
|
||||
},
|
||||
{
|
||||
value: 'daf747c6-6bc2-4083-bd59-aa94dce23f5d',
|
||||
name: 'Middle Eastern Woman - This voice is clear with a Middle Eastern Accent, perfect for a narrator',
|
||||
},
|
||||
{
|
||||
value: '694f9389-aac1-45b6-b726-9d9369183238',
|
||||
name: 'Sarah - This voice is natural and expressive with an American accent, perfect for a wide range of conversational use cases including customer support, sales, reception, and more.',
|
||||
},
|
||||
{
|
||||
value: '794f9389-aac1-45b6-b726-9d9369183238',
|
||||
name: 'Sarah Curious - This voice is similar to Sarah, but has improved emphasis for questions.',
|
||||
},
|
||||
{
|
||||
value: '21b81c14-f85b-436d-aff5-43f2e788ecf8',
|
||||
name: 'Laidback Woman - This voice is laid back and husky, with a slight Californian accent',
|
||||
},
|
||||
{
|
||||
value: 'a3520a8f-226a-428d-9fcd-b0a4711a6829',
|
||||
name: 'Reflective Woman - This voice is even, full, and reflective, perfect for a young narrator for an audiobook or movie',
|
||||
},
|
||||
{
|
||||
value: '829ccd10-f8b3-43cd-b8a0-4aeaa81f3b30',
|
||||
name: 'Customer Support Lady - This voice is polite and helpful, perfect for customer support agents',
|
||||
},
|
||||
{
|
||||
value: '79a125e8-cd45-4c13-8a67-188112f4dd22',
|
||||
name: 'British Lady - This voice is elegant with a slight British accent, perfect for storytelling and narrating',
|
||||
},
|
||||
{
|
||||
value: 'c8605446-247c-4d39-acd4-8f4c28aa363c',
|
||||
name: 'Wise Lady - This voice is wise and authoritative, perfect for a confident narrator',
|
||||
},
|
||||
{
|
||||
value: '8985388c-1332-4ce7-8d55-789628aa3df4',
|
||||
name: 'Australian Narrator Lady - This voice is even and neutral, with an Australian accent, designed for narrating content and stories',
|
||||
},
|
||||
{
|
||||
value: 'ff1bb1a9-c582-4570-9670-5f46169d0fc8',
|
||||
name: 'Indian Customer Support Lady - This voice is clear and polite, with an Indian accent, suitable for customer support agents',
|
||||
},
|
||||
{
|
||||
value: '820a3788-2b37-4d21-847a-b65d8a68c99a',
|
||||
name: 'Salesman - This voice is smooth and persuasive, perfect for sales pitches and phone conversations',
|
||||
},
|
||||
{
|
||||
value: 'f114a467-c40a-4db8-964d-aaba89cd08fa',
|
||||
name: 'Yogaman - This voice is calm, soothing, and stable, perfect for a yoga instructor',
|
||||
},
|
||||
{
|
||||
value: 'c45bc5ec-dc68-4feb-8829-6e6b2748095d',
|
||||
name: 'Movieman - This voice is deep, resonant, and assertive, perfect for a movie narrator',
|
||||
},
|
||||
{
|
||||
value: '87748186-23bb-4158-a1eb-332911b0b708',
|
||||
name: 'Wizardman - This voice is wise and mysterious, perfect for a Wizard character',
|
||||
},
|
||||
{
|
||||
value: '043cfc81-d69f-4bee-ae1e-7862cb358650',
|
||||
name: 'Australian Woman - This voice is deliberate and confident, with a slight Australian accent, perfect for inspiring characters in videos and stories',
|
||||
},
|
||||
{
|
||||
value: '5619d38c-cf51-4d8e-9575-48f61a280413',
|
||||
name: 'Announcer Man - This voice is deep and inviting, perfect for entertainment and broadcasting content',
|
||||
},
|
||||
{
|
||||
value: '42b39f37-515f-4eee-8546-73e841679c1d',
|
||||
name: 'Wise Guide Man - This voice is deep and deliberate, perfect for inspiring and guiding characters in games and videos',
|
||||
},
|
||||
{
|
||||
value: '565510e8-6b45-45de-8758-13588fbaec73',
|
||||
name: 'Midwestern Man - This voice is neutral and smooth, with a slight midwestern accent, perfect for narrations',
|
||||
},
|
||||
{
|
||||
value: '726d5ae5-055f-4c3d-8355-d9677de68937',
|
||||
name: 'Kentucky Man - This voice is laidback and smooth, with a Kentucky accent, perfect for a casual conversation',
|
||||
},
|
||||
{
|
||||
value: '63ff761f-c1e8-414b-b969-d1833d1c870c',
|
||||
name: 'Confident British Man - This voice is disciplined with a British accent, perfect for a commanding character or narrator',
|
||||
},
|
||||
{
|
||||
value: '98a34ef2-2140-4c28-9c71-663dc4dd7022',
|
||||
name: 'Southern Man - This voice is warm with a Southern accent, perfect for a narrator',
|
||||
},
|
||||
{
|
||||
value: '95856005-0332-41b0-935f-352e296aa0df',
|
||||
name: 'Classy British Man - This voice is light and smooth with a British accent, perfect for casual conversation',
|
||||
},
|
||||
{
|
||||
value: 'ee7ea9f8-c0c1-498c-9279-764d6b56d189',
|
||||
name: 'Polite Man - This voice is polite and conversational, with a slight accent, designed for customer support and casual conversations',
|
||||
},
|
||||
{
|
||||
value: '40104aff-a015-4da1-9912-af950fbec99e',
|
||||
name: 'Alabama Male - This voice has a strong Southern Accent, perfect for conversations and instructional videos',
|
||||
},
|
||||
{
|
||||
value: '13524ffb-a918-499a-ae97-c98c7c4408c4',
|
||||
name: 'Australian Male - This voice is smooth and disciplined, with an Australian Accent, suited for narrating educational content',
|
||||
},
|
||||
{
|
||||
value: '1001d611-b1a8-46bd-a5ca-551b23505334',
|
||||
name: 'Anime Girl - This voice is expressive and has a high pitch, suitable for anime or gaming characters',
|
||||
},
|
||||
{
|
||||
value: 'e3827ec5-697a-4b7c-9704-1a23041bbc51',
|
||||
name: 'Sweet Lady - This voice is sweet and passionate, perfect for a character in a game or book',
|
||||
},
|
||||
{
|
||||
value: 'c2ac25f9-ecc4-4f56-9095-651354df60c0',
|
||||
name: 'Commercial Lady - This voice is inviting, enthusiastic, and relatable, perfect for a commercial or advertisement',
|
||||
},
|
||||
{
|
||||
value: '573e3144-a684-4e72-ac2b-9b2063a50b53',
|
||||
name: 'Teacher Lady - This voice is neutral and clear, perfect for narrating educational content',
|
||||
},
|
||||
{
|
||||
value: '8f091740-3df1-4795-8bd9-dc62d88e5131',
|
||||
name: 'Princess - This voice is light, freindly and has a flourish, perfect for character work in videos and games',
|
||||
},
|
||||
{
|
||||
value: '7360f116-6306-4e9a-b487-1235f35a0f21',
|
||||
name: 'Commercial Man - This voice is upbeat and enthusiastic, perfect for commercials and advertisements',
|
||||
},
|
||||
{
|
||||
value: '03496517-369a-4db1-8236-3d3ae459ddf7',
|
||||
name: 'ASMR Lady - This voice is calming and soft, perfect for guided meditations and soothing content',
|
||||
},
|
||||
{
|
||||
value: '248be419-c632-4f23-adf1-5324ed7dbf1d',
|
||||
name: 'Professional Woman - This voice is neutral and calm, perfect for a call center',
|
||||
},
|
||||
{
|
||||
value: 'bd9120b6-7761-47a6-a446-77ca49132781',
|
||||
name: 'Tutorial Man - This voice is inviting and calming, perfect for tutorials',
|
||||
},
|
||||
{
|
||||
value: '34bde396-9fde-4ebf-ad03-e3a1d1155205',
|
||||
name: 'New York Woman - This voice commands authority, with a New York accent, perfect for a commanding narrator or character',
|
||||
},
|
||||
{
|
||||
value: '11af83e2-23eb-452f-956e-7fee218ccb5c',
|
||||
name: 'Midwestern Woman - This voice is neutral and deliberate, with a midwestern accent, suitable for news broadcasts and narration',
|
||||
},
|
||||
{
|
||||
value: 'ed81fd13-2016-4a49-8fe3-c0d2761695fc',
|
||||
name: 'Sportsman - This voice is energetic and enthusiastic, perfect for a sports broadcaster',
|
||||
},
|
||||
{
|
||||
value: '996a8b96-4804-46f0-8e05-3fd4ef1a87cd',
|
||||
name: 'Storyteller Lady - This voice is neutral and smooth, with a slight Canadian accent, perfect for narrations',
|
||||
},
|
||||
{
|
||||
value: 'fb26447f-308b-471e-8b00-8e9f04284eb5',
|
||||
name: 'Doctor Mischief - This is an expressive character voice, suited to whimsical characters for games and educational content',
|
||||
},
|
||||
{
|
||||
value: '50d6beb4-80ea-4802-8387-6c948fe84208',
|
||||
name: 'The Merchant - This voice is playful and quirky, designed for character work in games and videos',
|
||||
},
|
||||
{
|
||||
value: 'e13cae5c-ec59-4f71-b0a6-266df3c9bb8e',
|
||||
name: 'Madame Mischief - This voice is mischeivious and playful, suitable for voicing characters for kids content and games',
|
||||
},
|
||||
{
|
||||
value: '5c42302c-194b-4d0c-ba1a-8cb485c84ab9',
|
||||
name: 'Female Nurse - This voice is clear and firm, perfect for nurse characters and instructional videos',
|
||||
},
|
||||
{
|
||||
value: 'f9836c6e-a0bd-460e-9d3c-f7299fa60f94',
|
||||
name: 'Southern Woman - This voice is friendly and inviting, with a slight Southern Accent, perfect for conversations and phone calls',
|
||||
},
|
||||
{
|
||||
value: 'a01c369f-6d2d-4185-bc20-b32c225eab70',
|
||||
name: 'British Customer Support Lady - This voice is friendly and polite, with a British accent, perfect for phone conversations',
|
||||
},
|
||||
{
|
||||
value: 'b7d50908-b17c-442d-ad8d-810c63997ed9',
|
||||
name: 'California Girl - This voice is enthusiastic and friendly, perfect for a casual conversation between friends',
|
||||
},
|
||||
{
|
||||
value: 'f785af04-229c-4a7c-b71b-f3194c7f08bb',
|
||||
name: 'John - This voice is natural and empathetic with an American accent, perfect for use cases like demos and customer support calls.',
|
||||
},
|
||||
{
|
||||
value: '729651dc-c6c3-4ee5-97fa-350da1f88600',
|
||||
name: 'Pleasant Man - A pleasant male voice that\'s good for use cases like demos and customer support calls',
|
||||
},
|
||||
{
|
||||
value: '91b4cf29-5166-44eb-8054-30d40ecc8081',
|
||||
name: 'Anna - This voice is natural and expressive with an American accent, perfect for use cases like interviews and customer support calls.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
28
lib/utils/speech-data/tts-deepgram.js
Normal file
28
lib/utils/speech-data/tts-deepgram.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const TtsDeepgramLanguagesVoiceRaw = require('./tts-model-deepgram');
|
||||
|
||||
const languagesVoices = [];
|
||||
|
||||
TtsDeepgramLanguagesVoiceRaw.forEach((data) => {
|
||||
const lang = languagesVoices.find((l) => {
|
||||
return l.value === data.locale;
|
||||
});
|
||||
|
||||
if (!lang) {
|
||||
languagesVoices.push({
|
||||
value: data.locale,
|
||||
name: data.localeName,
|
||||
voices: TtsDeepgramLanguagesVoiceRaw
|
||||
.filter((d) => {
|
||||
return d.locale === data.locale;
|
||||
})
|
||||
.map((d) => {
|
||||
return {
|
||||
value: d.value,
|
||||
name: `${d.name}`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = languagesVoices;
|
||||
192
lib/utils/speech-data/tts-elevenlabs.js
Normal file
192
lib/utils/speech-data/tts-elevenlabs.js
Normal file
@@ -0,0 +1,192 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'ar',
|
||||
name: 'Arabic',
|
||||
voices: [
|
||||
{
|
||||
value: 'pNInz6obpgDQGcFmaJgB',
|
||||
name: 'Adam - american, deep, middle aged, male, narration',
|
||||
},
|
||||
{
|
||||
value: 'ErXwobaYiN019PkySvjV',
|
||||
name: 'Antoni - american, well-rounded, young, male, narration',
|
||||
},
|
||||
{
|
||||
value: 'VR6AewLTigWG4xSOukaG',
|
||||
name: 'Arnold - american, crisp, middle aged, male, narration',
|
||||
},
|
||||
{
|
||||
value: 'EXAVITQu4vr4xnSDxMaL',
|
||||
name: 'Bella - american, soft, young, female, narration',
|
||||
},
|
||||
{
|
||||
value: 'N2lVS1w4EtoT3dr4eOWO',
|
||||
name: 'Callum - american, hoarse, middle aged, male, video games',
|
||||
},
|
||||
{
|
||||
value: 'IKne3meq5aSn9XLyUdCD',
|
||||
name: 'Charlie - australian, casual, middle aged, male, conversational',
|
||||
},
|
||||
{
|
||||
value: 'XB0fDUnXU5powFXDhCwa',
|
||||
name: 'Charlotte - english-swedish, seductive, middle aged, female, video games',
|
||||
},
|
||||
{
|
||||
value: '2EiwWnXFnvU5JabPnv8n',
|
||||
name: 'Clyde - american, war veteran, middle aged, male, video games',
|
||||
},
|
||||
{
|
||||
value: 'onwK4e9ZLuTAKqWW03F9',
|
||||
name: 'Daniel - british, deep, middle aged, male, news presenter',
|
||||
},
|
||||
{
|
||||
value: 'CYw3kZ02Hs0563khs1Fj',
|
||||
name: 'Dave - british-essex, conversational, young, male, video games',
|
||||
},
|
||||
{
|
||||
value: 'AZnzlk1XvdvUeBnXmlld',
|
||||
name: 'Domi - american, strong, young, female, narration',
|
||||
},
|
||||
{
|
||||
value: 'ThT5KcBeYPX3keUQqHPh',
|
||||
name: "Dorothy - british, pleasant, young, female, children's stories",
|
||||
},
|
||||
{
|
||||
value: 'MF3mGyEYCl7XYWbV9V6O',
|
||||
name: 'Elli - american, emotional, young, female, narration',
|
||||
},
|
||||
{
|
||||
value: 'LcfcDJNUP1GQjkzn1xUU',
|
||||
name: 'Emily - american, calm, young, female, meditation',
|
||||
},
|
||||
{
|
||||
value: 'g5CIjZEefAph4nQFvHAz',
|
||||
name: 'Ethan - american, undefined, young, male, ASMR',
|
||||
},
|
||||
{
|
||||
value: 'D38z5RcWu1voky8WS1ja',
|
||||
name: 'Fin - irish, sailor, old, male, video games',
|
||||
},
|
||||
{
|
||||
value: 'jsCqWAovK2LkecY7zXl4',
|
||||
name: 'Freya - american, undefined, young, female, undefined',
|
||||
},
|
||||
{
|
||||
value: 'jBpfuIE2acCO8z3wKNLl',
|
||||
name: 'Gigi - american, childlish, young, female, animation',
|
||||
},
|
||||
{
|
||||
value: 'zcAOhNBS3c14rBihAFp1',
|
||||
name: 'Giovanni - english-italian, foreigner, young, male, audiobook',
|
||||
},
|
||||
{
|
||||
value: 'z9fAnlkpzviPz146aGWa',
|
||||
name: 'Glinda - american, witch, middle aged, female, video games',
|
||||
},
|
||||
{
|
||||
value: 'oWAxZDx7w5VEj9dCyTzz',
|
||||
name: 'Grace - american-southern, undefined, young, female, audiobook ',
|
||||
},
|
||||
{
|
||||
value: 'SOYHLrjzK2X1ezoPC6cr',
|
||||
name: 'Harry - american, anxious, young, male, video games',
|
||||
},
|
||||
{
|
||||
value: 'ZQe5CZNOzWyzPSCn5a3c',
|
||||
name: 'James - australian, calm , old, male, news',
|
||||
},
|
||||
{
|
||||
value: 'bVMeCyTHy58xNoL34h3p',
|
||||
name: 'Jeremy - american-irish, excited, young, male, narration',
|
||||
},
|
||||
{
|
||||
value: 't0jbNlBVZ17f02VDIeMI',
|
||||
name: 'Jessie - american, raspy , old, male, video games',
|
||||
},
|
||||
{
|
||||
value: 'Zlb1dXrM653N07WRdFW3',
|
||||
name: 'Joseph - british, undefined, middle aged, male, news',
|
||||
},
|
||||
{
|
||||
value: 'TxGEqnHWrfWFTfGW9XjX',
|
||||
name: 'Josh - american, deep, young, male, narration',
|
||||
},
|
||||
{
|
||||
value: 'TX3LPaxmHKxFdv7VOQHJ',
|
||||
name: 'Liam - american, undefined, young, male, narration',
|
||||
},
|
||||
{
|
||||
value: 'XrExE9yKIg1WjnnlVkGX',
|
||||
name: 'Matilda - american, warm, young, female, audiobook',
|
||||
},
|
||||
{
|
||||
value: 'Yko7PKHZNXotIFUBG7I9',
|
||||
name: 'Matthew - british, undefined, middle aged, male, audiobook',
|
||||
},
|
||||
{
|
||||
value: 'flq6f7yk4E4fJM5XTYuZ',
|
||||
name: 'Michael - american, undefined, old, male, audiobook',
|
||||
},
|
||||
{
|
||||
value: 'zrHiDhphv9ZnVXBqCLjz',
|
||||
name: 'Mimi - english-swedish, childish, young, female, animation',
|
||||
},
|
||||
{
|
||||
value: 'piTKgcLEGmPE4e6mEKli',
|
||||
name: 'Nicole - american, whisper, young, female, audiobook',
|
||||
},
|
||||
{
|
||||
value: 'ODq5zmih8GrVes37Dizd',
|
||||
name: 'Patrick - american, shouty, middle aged, male, video games',
|
||||
},
|
||||
{
|
||||
value: '21m00Tcm4TlvDq8ikWAM',
|
||||
name: 'Rachel - american, calm, young, female, narration',
|
||||
},
|
||||
{
|
||||
value: 'wViXBPUzp2ZZixB1xQuM',
|
||||
name: 'Ryan - american, soldier, middle aged, male, audiobook',
|
||||
},
|
||||
{
|
||||
value: 'yoZ06aMxZJJ28mfd3POQ',
|
||||
name: 'Sam - american, raspy, young, male, narration',
|
||||
},
|
||||
{
|
||||
value: 'pMsXgVXv3BLzUgSXRplE',
|
||||
name: 'Serena - american, pleasant, middle aged, female, interactive',
|
||||
},
|
||||
{
|
||||
value: 'GBv7mTt0atIp3Br8iCZE',
|
||||
name: 'Thomas - american, calm, young, male, meditation',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ value: 'bg', name: 'Bulgarian', voices: [] },
|
||||
{ value: 'zh', name: 'Chinese', voices: [] },
|
||||
{ value: 'hr', name: 'Croatian', voices: [] },
|
||||
{ value: 'cs', name: 'Czech', voices: [] },
|
||||
{ value: 'da', name: 'Danish', voices: [] },
|
||||
{ value: 'nl', name: 'Dutch', voices: [] },
|
||||
{ value: 'en', name: 'English', voices: [] },
|
||||
{ value: 'fil', name: 'Filipino', voices: [] },
|
||||
{ value: 'fi', name: 'Finnish', voices: [] },
|
||||
{ value: 'fr', name: 'French', voices: [] },
|
||||
{ value: 'de', name: 'German', voices: [] },
|
||||
{ value: 'el', name: 'Greek', voices: [] },
|
||||
{ value: 'hi', name: 'Hindi', voices: [] },
|
||||
{ value: 'id', name: 'Indonesian', voices: [] },
|
||||
{ value: 'it', name: 'Italian', voices: [] },
|
||||
{ value: 'ja', name: 'Japanese', voices: [] },
|
||||
{ value: 'ko', name: 'Korean', voices: [] },
|
||||
{ value: 'ms', name: 'Malay', voices: [] },
|
||||
{ value: 'pl', name: 'Polish', voices: [] },
|
||||
{ value: 'pt', name: 'Portuguese', voices: [] },
|
||||
{ value: 'ro', name: 'Romanian', voices: [] },
|
||||
{ value: 'ru', name: 'Russian', voices: [] },
|
||||
{ value: 'sk', name: 'Slovak', voices: [] },
|
||||
{ value: 'es', name: 'Spanish', voices: [] },
|
||||
{ value: 'sv', name: 'Swedish', voices: [] },
|
||||
{ value: 'ta', name: 'Tamil', voices: [] },
|
||||
{ value: 'tr', name: 'Turkish', voices: [] },
|
||||
{ value: 'uk', name: 'Ukrainian', voices: [] },
|
||||
];
|
||||
796
lib/utils/speech-data/tts-google.js
Normal file
796
lib/utils/speech-data/tts-google.js
Normal file
@@ -0,0 +1,796 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'ar-XA',
|
||||
name: 'Arabic',
|
||||
voices: [
|
||||
{ value: 'ar-XA-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ar-XA-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'ar-XA-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'ar-XA-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'ar-XA-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ar-XA-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'ar-XA-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'ar-XA-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'af-ZA',
|
||||
name: 'Afrikaans (South Africa)',
|
||||
voices: [{ value: 'af-ZA-Standard-A', name: 'Standard-A (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'bn-IN',
|
||||
name: 'Bengali (India)',
|
||||
voices: [
|
||||
{ value: 'bn-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'bn-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'bn-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'bn-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'bg-BG',
|
||||
name: 'Bulgarian (Bulgaria)',
|
||||
voices: [{ value: 'bg-BG-Standard-A', name: 'Standard-A (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'ca-ES',
|
||||
name: 'Catalan (Spain)',
|
||||
voices: [{ value: 'ca-ES-Standard-A', name: 'Standard-A (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'cs-CZ',
|
||||
name: 'Czech (Czech Republic)',
|
||||
voices: [
|
||||
{ value: 'cs-CZ-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'cs-CZ-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'da-DK',
|
||||
name: 'Danish (Denmark)',
|
||||
voices: [
|
||||
{ value: 'da-DK-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'da-DK-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'da-DK-Neural2-D', name: 'Neural2-D (Female)' },
|
||||
{ value: 'da-DK-Neural2-F', name: 'Neural2-F (Male)' },
|
||||
|
||||
{ value: 'da-DK-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'da-DK-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'da-DK-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'da-DK-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'da-DK-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'da-DK-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'eu-ES',
|
||||
name: 'Basque (Spain)',
|
||||
voices: [{ value: 'eu-ES-Standard-A', name: 'Standard-A (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'nl-NL',
|
||||
name: 'Dutch (Netherlands)',
|
||||
voices: [
|
||||
{ value: 'nl-NL-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'nl-NL-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'nl-NL-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'nl-NL-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'nl-NL-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'nl-NL-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'nl-NL-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'nl-NL-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'nl-NL-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'nl-NL-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-AU',
|
||||
name: 'English (Australia)',
|
||||
voices: [
|
||||
{ value: 'en-AU-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'en-AU-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'en-AU-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'en-AU-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'en-AU-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'en-AU-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'en-AU-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'en-AU-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'en-AU-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'en-AU-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'en-AU-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
{ value: 'en-AU-Neural2-D', name: 'Neural2-D (Male)' },
|
||||
{ value: 'en-AU-Polyglot-1', name: 'Polyglot-1 (Male)' },
|
||||
{ value: 'en-AU-News-E', name: 'News-E (Female)' },
|
||||
{ value: 'en-AU-News-F', name: 'News-F (Female)' },
|
||||
{ value: 'en-AU-News-G', name: 'News-G (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-IN',
|
||||
name: 'English (India)',
|
||||
voices: [
|
||||
{ value: 'en-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'en-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'en-IN-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'en-IN-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'en-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'en-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'en-IN-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'en-IN-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
|
||||
{ value: 'en-IN-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'en-IN-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'en-IN-Neural2-C', name: 'Neural2-C (Male)' },
|
||||
{ value: 'en-IN-Neural2-D', name: 'Neural2-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-GB',
|
||||
name: 'English (UK)',
|
||||
voices: [
|
||||
{ value: 'en-GB-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'en-GB-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'en-GB-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'en-GB-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'en-GB-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'en-GB-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'en-GB-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'en-GB-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'en-GB-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'en-GB-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'en-GB-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
{ value: 'en-GB-Neural2-D', name: 'Neural2-D (Male)' },
|
||||
{ value: 'en-GB-Neural2-F', name: 'Neural2-F (Female)' },
|
||||
{ value: 'en-GB-News-G', name: 'News-G (Female)' },
|
||||
{ value: 'en-GB-News-H', name: 'News-H (Female)' },
|
||||
{ value: 'en-GB-News-I', name: 'News-I (Female)' },
|
||||
{ value: 'en-GB-News-J', name: 'News-J (Male)' },
|
||||
{ value: 'en-GB-News-K', name: 'News-K (Male)' },
|
||||
{ value: 'en-GB-News-L', name: 'News-L (Male)' },
|
||||
{ value: 'en-GB-News-M', name: 'News-M (Male)' },
|
||||
|
||||
{ value: 'en-GB-Studio-B', name: 'Studio-B (Male)' },
|
||||
{ value: 'en-GB-Studio-C', name: 'Studio-C (Female)' },
|
||||
{ value: 'en-GB-Wavenet-F', name: 'Wavenet-F (Female)' },
|
||||
{ value: 'en-GB-Standard-F', name: 'Standard-F (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'English (US)',
|
||||
voices: [
|
||||
{ value: 'en-US-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'en-US-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'en-US-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'en-US-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'en-US-Wavenet-A', name: 'Wavenet-A (Male)' },
|
||||
{ value: 'en-US-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'en-US-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'en-US-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'en-US-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
{ value: 'en-US-Wavenet-F', name: 'Wavenet-F (Female)' },
|
||||
{ value: 'en-US-Neural2-A', name: 'Neural2-A (Male)' },
|
||||
{ value: 'en-US-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
{ value: 'en-US-Neural2-D', name: 'Neural2-D (Male)' },
|
||||
{ value: 'en-US-Neural2-E', name: 'Neural2-E (Female)' },
|
||||
{ value: 'en-US-Neural2-F', name: 'Neural2-F (Female)' },
|
||||
{ value: 'en-US-Neural2-G', name: 'Neural2-G (Female)' },
|
||||
{ value: 'en-US-Neural2-H', name: 'Neural2-H (Female)' },
|
||||
{ value: 'en-US-Neural2-I', name: 'Neural2-I (Male)' },
|
||||
{ value: 'en-US-Neural2-J', name: 'Neural2-J (Male)' },
|
||||
{ value: 'en-US-Studio-M', name: 'Studio-M (Male)' },
|
||||
{ value: 'en-US-Studio-O', name: 'Studio-M (Female)' },
|
||||
{ value: 'en-US-Polyglot-1', name: 'Polyglot-1 (Male)' },
|
||||
{ value: 'en-US-News-K', name: 'News-K (Female)' },
|
||||
{ value: 'en-US-News-L', name: 'News-L (Female)' },
|
||||
{ value: 'en-US-News-M', name: 'News-M (Male)' },
|
||||
{ value: 'en-US-News-N', name: 'News-N (Male)' },
|
||||
|
||||
{ value: 'en-US-Standard-A', name: 'Standard-A (Male)' },
|
||||
{ value: 'en-US-Standard-F', name: 'Standard-F (Female)' },
|
||||
{ value: 'en-US-Standard-G', name: 'Standard-G (Female)' },
|
||||
{ value: 'en-US-Standard-H', name: 'Standard-H (Female)' },
|
||||
{ value: 'en-US-Standard-I', name: 'Standard-I (Male)' },
|
||||
{ value: 'en-US-Standard-J', name: 'Standard-J (Male)' },
|
||||
{ value: 'en-US-Wavenet-G', name: 'Wavenet-G (Female)' },
|
||||
{ value: 'en-US-Wavenet-H', name: 'Wavenet-H (Female)' },
|
||||
{ value: 'en-US-Wavenet-I', name: 'Wavenet-I (Male)' },
|
||||
{ value: 'en-US-Wavenet-J', name: 'Wavenet-J (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fil-PH',
|
||||
name: 'Filipino (Philippines)',
|
||||
voices: [
|
||||
{ value: 'fil-PH-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'fil-PH-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'fil-ph-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'fil-ph-Neural2-D', name: 'Neural2-A (Male)' },
|
||||
|
||||
{ value: 'fil-PH-Standard-B', name: 'Standard-B (Female)' },
|
||||
{ value: 'fil-PH-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'fil-PH-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'fil-PH-Wavenet-B', name: 'Wavenet-B (Female)' },
|
||||
{ value: 'fil-PH-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'fil-PH-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fi-FI',
|
||||
name: 'Finnish (Finland)',
|
||||
voices: [
|
||||
{ value: 'fi-FI-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'fi-FI-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fr-CA',
|
||||
name: 'French (Canada)',
|
||||
voices: [
|
||||
{ value: 'fr-CA-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'fr-CA-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'fr-CA-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'fr-CA-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'fr-CA-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'fr-CA-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'fr-CA-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'fr-CA-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'fr-CA-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'fr-CA-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'fr-CA-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
{ value: 'fr-CA-Neural2-D', name: 'Neural2-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fr-FR',
|
||||
name: 'French (France)',
|
||||
voices: [
|
||||
{ value: 'fr-FR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'fr-FR-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'fr-FR-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'fr-FR-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'fr-FR-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'fr-FR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'fr-FR-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'fr-FR-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'fr-FR-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'fr-FR-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
{ value: 'fr-FR-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'fr-FR-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'fr-FR-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
{ value: 'fr-FR-Neural2-D', name: 'Neural2-D (Male)' },
|
||||
{ value: 'fr-FR-Neural2-E', name: 'Neural2-E (Female)' },
|
||||
{ value: 'fr-FR-Polyglot-1', name: 'Polyglot-1 (Male)' },
|
||||
|
||||
{ value: 'fr-FR-Studio-A', name: 'Studio-A (Female)' },
|
||||
{ value: 'fr-FR-Studio-D', name: 'Studio-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'de-DE',
|
||||
name: 'German (Germany)',
|
||||
voices: [
|
||||
{ value: 'de-DE-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'de-DE-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'de-DE-Standard-E', name: 'Standard-E (Male)' },
|
||||
{ value: 'de-DE-Standard-F', name: 'Standard-F (Female)' },
|
||||
{ value: 'de-DE-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'de-DE-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'de-DE-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'de-DE-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'de-DE-Wavenet-E', name: 'Wavenet-E (Male)' },
|
||||
{ value: 'de-DE-Wavenet-F', name: 'Wavenet-F (Female)' },
|
||||
{ value: 'de-DE-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'de-DE-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
{ value: 'de-DE-Neural2-D', name: 'Neural2-D (Male)' },
|
||||
{ value: 'de-DE-Neural2-F', name: 'Neural2-F (Female)' },
|
||||
{ value: 'de-DE-Polyglot-1', name: 'Polyglot-1 (Male)' },
|
||||
|
||||
{ value: 'de-DE-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'de-DE-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'de-DE-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'de-DE-Studio-B', name: 'Studio-B (Male)' },
|
||||
{ value: 'de-DE-Studio-C', name: 'Studio-C (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'el-GR',
|
||||
name: 'Greek (Greece)',
|
||||
voices: [
|
||||
{ value: 'el-GR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'el-GR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'gl-ES',
|
||||
name: 'Galician (Spain)',
|
||||
voices: [{ value: 'gl-ES-Standard-A', name: 'Standard-A (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'gu-IN',
|
||||
name: 'Gujarati (India)',
|
||||
voices: [
|
||||
{ value: 'gu-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'gu-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'gu-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'gu-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'he-IL',
|
||||
name: 'Hebrew (Israel)',
|
||||
voices: [
|
||||
{ value: 'he-IL-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'he-IL-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'he-IL-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'he-IL-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'he-IL-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'he-IL-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'he-IL-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'he-IL-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'hi-IN',
|
||||
name: 'Hindi (India)',
|
||||
voices: [
|
||||
{ value: 'hi-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'hi-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'hi-IN-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'hi-IN-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'hi-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'hi-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'hi-IN-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'hi-IN-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'hi-IN-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'hi-IN-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'hi-IN-Neural2-C', name: 'Neural2-C (Male)' },
|
||||
{ value: 'hi-IN-Neural2-D', name: 'Neural2-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'hu-HU',
|
||||
name: 'Hungarian (Hungary)',
|
||||
voices: [
|
||||
{ value: 'hu-HU-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'hu-HU-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'is-IS',
|
||||
name: 'Icelandic (Iceland)',
|
||||
voices: [{ value: 'is-IS-Standard-A', name: 'Standard-A (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'id-ID',
|
||||
name: 'Indonesian (Indonesia)',
|
||||
voices: [
|
||||
{ value: 'id-ID-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'id-ID-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'id-ID-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'id-ID-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'id-ID-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'id-ID-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'id-ID-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'id-ID-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'it-IT',
|
||||
name: 'Italian (Italy)',
|
||||
voices: [
|
||||
{ value: 'it-IT-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'it-IT-Standard-B', name: 'Standard-B (Female)' },
|
||||
{ value: 'it-IT-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'it-IT-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'it-IT-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'it-IT-Wavenet-B', name: 'Wavenet-B (Female)' },
|
||||
{ value: 'it-IT-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'it-IT-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'it-IT-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'it-IT-Neural2-C', name: 'Neural2-C (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ja-JP',
|
||||
name: 'Japanese (Japan)',
|
||||
voices: [
|
||||
{ value: 'ja-JP-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ja-JP-Standard-B', name: 'Standard-B (Female)' },
|
||||
{ value: 'ja-JP-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'ja-JP-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'ja-JP-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ja-JP-Wavenet-B', name: 'Wavenet-B (Female)' },
|
||||
{ value: 'ja-JP-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'ja-JP-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'ja-JP-Neural2-B', name: 'Neural2-B (Female)' },
|
||||
{ value: 'ja-JP-Neural2-C', name: 'Neural2-C (Male)' },
|
||||
{ value: 'ja-JP-Neural2-D', name: 'Neural2-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'kn-IN',
|
||||
name: 'Kannada (India)',
|
||||
voices: [
|
||||
{ value: 'kn-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'kn-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'kn-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'kn-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ko-KR',
|
||||
name: 'Korean (South Korea)',
|
||||
voices: [
|
||||
{ value: 'ko-KR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ko-KR-Standard-B', name: 'Standard-B (Female)' },
|
||||
{ value: 'ko-KR-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'ko-KR-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'ko-KR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ko-KR-Wavenet-B', name: 'Wavenet-B (Female)' },
|
||||
{ value: 'ko-KR-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'ko-KR-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'ko-KR-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'ko-KR-Neural2-B', name: 'Neural2-B (Female)' },
|
||||
{ value: 'ko-KR-Neural2-C', name: 'Neural2-C (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'lv-LV',
|
||||
name: 'Latvian (Latvia)',
|
||||
voices: [{ value: 'lv-LV-Standard-A', name: 'Standard-A (Male)' }],
|
||||
},
|
||||
{
|
||||
value: 'lt-LT',
|
||||
name: 'Lithuanian (Lithuania)',
|
||||
voices: [{ value: 'lt-LT-Standard-A', name: 'Standard-A (Male)' }],
|
||||
},
|
||||
{
|
||||
value: 'cmn-CN',
|
||||
name: 'Mandarin Chinese',
|
||||
voices: [
|
||||
{ value: 'cmn-CN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'cmn-CN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'cmn-CN-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'cmn-CN-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'cmn-CN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'cmn-CN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'cmn-CN-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'cmn-CN-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'cmn-TW',
|
||||
name: 'Mandarin Chinese (Traditional)',
|
||||
voices: [
|
||||
{ value: 'cmn-TW-Standard-A-Alpha', name: 'Standard-A-Alpha (Female)' },
|
||||
{ value: 'cmn-TW-Standard-B-Alpha', name: 'Standard-B-Alpha (Male)' },
|
||||
{ value: 'cmn-TW-Standard-C-Alpha', name: 'Standard-C-Alpha (Male)' },
|
||||
{ value: 'cmn-TW-Wavenet-A-Alpha', name: 'Wavenet-A-Alpha (Female)' },
|
||||
{ value: 'cmn-TW-Wavenet-B-Alpha', name: 'Wavenet-B-Alpha (Male)' },
|
||||
{ value: 'cmn-TW-Wavenet-C-Alpha', name: 'Wavenet-C-Alpha (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ms-MY',
|
||||
name: 'Malay (Malaysia)',
|
||||
voices: [
|
||||
{ value: 'ms-MY-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ms-MY-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'ms-MY-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'ms-MY-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'ms-MY-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ms-MY-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'ms-MY-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'ms-MY-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'ml-IN',
|
||||
name: 'Malayalam (India)',
|
||||
voices: [
|
||||
{ value: 'ml-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ml-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'ml-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ml-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'ml-IN-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'ml-IN-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'mr-IN',
|
||||
name: 'Marathi (India)',
|
||||
voices: [
|
||||
{ value: 'mr-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'mr-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'mr-IN-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'mr-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'mr-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'mr-IN-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'nb-NO',
|
||||
name: 'Norwegian (Norway)',
|
||||
voices: [
|
||||
{ value: 'nb-NO-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'nb-NO-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'nb-NO-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'nb-NO-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'nb-no-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'nb-NO-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'nb-NO-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'nb-NO-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'nb-NO-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'nb-no-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'nl-BE',
|
||||
name: 'Dutch (Belgium)',
|
||||
voices: [
|
||||
{ value: 'nl-BE-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'nl-BE-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'nl-BE-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'nl-BE-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'pl-PL',
|
||||
name: 'Polish (Poland)',
|
||||
voices: [
|
||||
{ value: 'pl-PL-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'pl-PL-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'pl-PL-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'pl-PL-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'pl-PL-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'pl-PL-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'pl-PL-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'pl-PL-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'pl-PL-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'pl-PL-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'pa-IN',
|
||||
name: 'Punjabi (India)',
|
||||
voices: [
|
||||
{ value: 'pa-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'pa-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'pa-IN-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'pa-IN-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'pa-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'pa-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'pa-IN-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'pa-IN-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'pt-BR',
|
||||
name: 'Portuguese (Brazil)',
|
||||
voices: [
|
||||
{ value: 'pt-BR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'pt-BR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'pt-BR-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'pt-BR-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'pt-BR-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
|
||||
{ value: 'pt-BR-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'pt-BR-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'pt-BR-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'pt-BR-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'pt-PT',
|
||||
name: 'Portuguese (Portugal)',
|
||||
voices: [
|
||||
{ value: 'pt-PT-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'pt-PT-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'pt-PT-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'pt-PT-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'pt-PT-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'pt-PT-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'pt-PT-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'pt-PT-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'ro-RO',
|
||||
name: 'Romanian (Romania)',
|
||||
voices: [
|
||||
{ value: 'ro-RO-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ro-RO-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'ru-RU',
|
||||
name: 'Russian (Russia)',
|
||||
voices: [
|
||||
{ value: 'ru-RU-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ru-RU-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'ru-RU-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'ru-RU-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'ru-RU-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'ru-RU-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ru-RU-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'ru-RU-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'ru-RU-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'ru-RU-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'sk-SK',
|
||||
name: 'Slovak (Slovakia)',
|
||||
voices: [
|
||||
{ value: 'sk-SK-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'sk-SK-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'sr-RS',
|
||||
name: 'Serbian (Cyrillic)',
|
||||
voices: [{ value: 'sr-RS-Standard-A', name: 'Standard-A (Female)' }],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'es-ES',
|
||||
name: 'Spanish (Spain)',
|
||||
voices: [
|
||||
{ value: 'es-ES-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'es-ES-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'es-ES-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'es-ES-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
{ value: 'es-ES-Neural2-D', name: 'Neural2-D (Female)' },
|
||||
{ value: 'es-ES-Neural2-E', name: 'Neural2-E (Female)' },
|
||||
{ value: 'es-ES-Neural2-F', name: 'Neural2-F (Male)' },
|
||||
{ value: 'es-ES-Polyglot-1', name: 'Polyglot-1 (Male)' },
|
||||
|
||||
{ value: 'es-ES-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'es-ES-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'es-ES-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'es-ES-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'es-ES-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'es-ES-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es-US',
|
||||
name: 'Spanish (US)',
|
||||
voices: [
|
||||
{ value: 'es-US-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'es-US-Neural2-B', name: 'Neural2-B (Male)' },
|
||||
{ value: 'es-US-Neural2-C', name: 'Neural2-C (Male)' },
|
||||
{ value: 'es-US-Studio-B', name: 'Studio-B (Male)' },
|
||||
{ value: 'es-US-Polyglot-1', name: 'Polyglot-1 (Male)' },
|
||||
{ value: 'es-US-News-D', name: 'News-D (Male)' },
|
||||
{ value: 'es-US-News-E', name: 'News-E (Male)' },
|
||||
{ value: 'es-US-News-F', name: 'News-F (Female)' },
|
||||
{ value: 'es-US-News-G', name: 'News-G (Female)' },
|
||||
|
||||
{ value: 'es-US-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'es-US-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'es-US-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'es-US-Studio-B', name: 'Studio-B (Male)' },
|
||||
{ value: 'es-US-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'es-US-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'es-US-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'sv-SE',
|
||||
name: 'Swedish (Sweden)',
|
||||
voices: [
|
||||
{ value: 'sv-SE-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'sv-SE-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
|
||||
{ value: 'sv-SE-Standard-B', name: 'Standard-B (Female)' },
|
||||
{ value: 'sv-SE-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'sv-SE-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'sv-SE-Standard-E', name: 'Standard-E (Male)' },
|
||||
{ value: 'sv-SE-Wavenet-B', name: 'Wavenet-B (Female)' },
|
||||
{ value: 'sv-SE-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'sv-SE-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'sv-SE-Wavenet-E', name: 'Wavenet-E (Male)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'ta-IN',
|
||||
name: 'Tamil (India)',
|
||||
voices: [
|
||||
{ value: 'ta-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ta-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'ta-IN-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'ta-IN-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'ta-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ta-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'ta-IN-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'ta-IN-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'te-IN',
|
||||
name: 'Telugu (India)',
|
||||
voices: [
|
||||
{ value: 'te-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'te-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
value: 'tr-TR',
|
||||
name: 'Turkish (Turkey)',
|
||||
voices: [
|
||||
{ value: 'tr-TR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'tr-TR-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'tr-TR-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'tr-TR-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'tr-TR-Standard-E', name: 'Standard-E (Male)' },
|
||||
{ value: 'tr-TR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'tr-TR-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'tr-TR-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'tr-TR-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'tr-TR-Wavenet-E', name: 'Wavenet-E (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'uk-UA',
|
||||
name: 'Ukrainian (Ukraine)',
|
||||
voices: [
|
||||
{ value: 'uk-UA-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'uk-UA-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'th-TH',
|
||||
name: 'Thai (Thailand)',
|
||||
voices: [
|
||||
{ value: 'th-TH-Neural2-C', name: 'Neural2-C (Female)' },
|
||||
|
||||
{ value: 'th-TH-Standard-A', name: 'Standard-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'vi-VN',
|
||||
name: 'Vietnamese (Vietnam)',
|
||||
voices: [
|
||||
{ value: 'vi-VN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'vi-VN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'vi-VN-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'vi-VN-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'vi-VN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'vi-VN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'vi-VN-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'vi-VN-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'vi-VN-Neural2-A', name: 'Neural2-A (Female)' },
|
||||
{ value: 'vi-VN-Neural2-D', name: 'Neural2-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'yue-HK',
|
||||
name: 'Chinese (Hong Kong)',
|
||||
voices: [
|
||||
{ value: 'yue-HK-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'yue-HK-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'yue-HK-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'yue-HK-Standard-D', name: 'Standard-D (Male)' },
|
||||
],
|
||||
},
|
||||
];
|
||||
167
lib/utils/speech-data/tts-ibm.js
Normal file
167
lib/utils/speech-data/tts-ibm.js
Normal file
@@ -0,0 +1,167 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'de-DE',
|
||||
name: 'German (Germany)',
|
||||
voices: [
|
||||
{ value: 'de-DE_DieterVoice', name: 'Dieter (Male): Standard German' },
|
||||
{
|
||||
value: 'de-DE_DieterV2Voice',
|
||||
name: 'Dieter 2 (Male): Standard German',
|
||||
},
|
||||
{
|
||||
value: 'de-DE_DieterV3Voice',
|
||||
name: 'Dieter 3 (Male): Standard German',
|
||||
},
|
||||
{ value: 'de-DE_ErikaV3Voice', name: 'Erika (Female): Standard German' },
|
||||
{ value: 'de-DE_BirgitVoice', name: 'Brigit (Female): Standard German' },
|
||||
{
|
||||
value: 'de-DE_BirgitV2Voice',
|
||||
name: 'Brigit 2 (Female): Standard German',
|
||||
},
|
||||
{
|
||||
value: 'de-DE_BirgitV3Voice',
|
||||
name: 'Brigit 3 (Female): Standard German',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'English (US)',
|
||||
voices: [
|
||||
{
|
||||
value: 'en-US_MichaelExpressive',
|
||||
name: 'Michael (Male): American English - Expressive',
|
||||
},
|
||||
{ value: 'en-US_MichaelVoice', name: 'Michael (Male): American English' },
|
||||
{
|
||||
value: 'en-US_MichaelV2Voice',
|
||||
name: 'Michael 2 (Male): American English',
|
||||
},
|
||||
{
|
||||
value: 'en-US_MichaelV3Voice',
|
||||
name: 'Michael 3 (Male): American English',
|
||||
},
|
||||
{ value: 'en-US_HenryV3Voice', name: 'Henry (Male): American English' },
|
||||
{ value: 'en-US_EmilyV3Voice', name: 'Emily (Female): American English' },
|
||||
{
|
||||
value: 'en-US_OliviaV3Voice',
|
||||
name: 'Olivia (Female): American English',
|
||||
},
|
||||
{
|
||||
value: 'en-US_AllisonExpressive',
|
||||
name: 'Allison (Female): American English - Expressive',
|
||||
},
|
||||
{
|
||||
value: 'en-US_AllisonVoice',
|
||||
name: 'Allison (Female): American English',
|
||||
},
|
||||
{
|
||||
value: 'en-US_AllisonV2Voice',
|
||||
name: 'Allison 2 (Female): American English',
|
||||
},
|
||||
{
|
||||
value: 'en-US_AllisonV3Voice',
|
||||
name: 'Allison 3 (Female): American English',
|
||||
},
|
||||
{
|
||||
value: 'en-US_LisaExpressive',
|
||||
name: 'Lisa (Female): American English - Expressive',
|
||||
},
|
||||
{ value: 'en-US_LisaVoice', name: 'Lisa (Female): American English' },
|
||||
{ value: 'en-US_LisaV2Voice', name: 'Lisa 2 (Female): American English' },
|
||||
{ value: 'en-US_LisaV3Voice', name: 'Lisa 3 (Female): American English' },
|
||||
{ value: 'en-US_KevinV3Voice', name: 'Kevin (Male): American English' },
|
||||
{
|
||||
value: 'en-US_EmmaExpressive',
|
||||
name: 'Emma (Female): American English - Expressive',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-GB',
|
||||
name: 'English (GB)',
|
||||
voices: [
|
||||
{ value: 'en-GB_JamesV3Voice', name: 'James (Male)' },
|
||||
{ value: 'en-GB_KateVoice', name: 'Kate (Female)' },
|
||||
{ value: 'en-GB_KateV3Voice', name: 'Kate 2 (Female)' },
|
||||
{ value: 'en-GB_CharlotteV3Voice', name: 'Charlotte (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es-US',
|
||||
name: 'Spanish (North America)',
|
||||
voices: [
|
||||
{
|
||||
value: 'es-US_SofiaVoice',
|
||||
name: 'Sofia (Female): North American Spanish',
|
||||
},
|
||||
{
|
||||
value: 'es-US_SofiaV3Voice',
|
||||
name: 'Sofia 2 (Female): North American Spanish',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es-LA',
|
||||
name: 'Spanish (Latin America)',
|
||||
voices: [
|
||||
{
|
||||
value: 'es-LA_SofiaVoice',
|
||||
name: 'Sofia (Female): Latin American Spanish',
|
||||
},
|
||||
{
|
||||
value: 'es-LA_SofiaV3Voice',
|
||||
name: 'Sofia 2 (Female): Latin American Spanish',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es-ES',
|
||||
name: 'Spanish (Castilian)',
|
||||
voices: [
|
||||
{ value: 'es-ES_LauraVoice', name: 'Laura (Female)' },
|
||||
{ value: 'es-ES_LauraV3Voice', name: 'Laura 2 (Female)' },
|
||||
{ value: 'es-ES_EnriqueVoice', name: 'Enrique (Male)' },
|
||||
{ value: 'es-ES_EnriqueV3Voice', name: 'Enrique 2 (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fr-FR',
|
||||
name: 'French (FR)',
|
||||
voices: [
|
||||
{ value: 'fr-FR_NicolasV3Voice', name: 'Nicolas (Male)' },
|
||||
{ value: 'fr-FR_ReneeVoice', name: 'Renee (Female)' },
|
||||
{ value: 'fr-FR_ReneeV3Voice', name: 'Renee 2 (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fr-CA',
|
||||
name: 'French (CA)',
|
||||
voices: [{ value: 'fr-CA_LouiseV3Voice', name: 'Louise (Female)' }],
|
||||
},
|
||||
{
|
||||
value: 'it-IT',
|
||||
name: 'Italian',
|
||||
voices: [
|
||||
{ value: 'it-IT_FrancescaVoice', name: 'Francesca (Female)' },
|
||||
{ value: 'it-IT_FrancescaV2Voice', name: 'Francesca 2 (Female)' },
|
||||
{ value: 'it-IT_FrancescaV3Voice', name: 'Francesca 3 (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'pt-BR',
|
||||
name: 'Portuguese (Brazil)',
|
||||
voices: [
|
||||
{ value: 'pt-BR_IsabelaVoice', name: 'Isabela (Female)' },
|
||||
{ value: 'pt-BR_IsabelaV3Voice', name: 'Isabela 2 (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ja-JP',
|
||||
name: 'Japanese',
|
||||
voices: [
|
||||
{ value: 'ja-JP_EmiVoice', name: 'Emi (Female)' },
|
||||
{ value: 'ja-JP_EmiV3Voice', name: 'Emi 2 (Female)' },
|
||||
],
|
||||
},
|
||||
];
|
||||
152
lib/utils/speech-data/tts-languages-playht.js
Normal file
152
lib/utils/speech-data/tts-languages-playht.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// languages.js
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
name: 'English',
|
||||
value: 'english'
|
||||
},
|
||||
{
|
||||
name: 'Mandarin',
|
||||
value: 'mandarin'
|
||||
},
|
||||
{
|
||||
name: 'Hindi',
|
||||
value: 'hindi'
|
||||
},
|
||||
{
|
||||
name: 'Japanese',
|
||||
value: 'japanese'
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
value: 'korean'
|
||||
},
|
||||
{
|
||||
name: 'Arabic',
|
||||
value: 'arabic'
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
value: 'spanish'
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
value: 'french'
|
||||
},
|
||||
{
|
||||
name: 'Italian',
|
||||
value: 'italian'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
value: 'portuguese'
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
value: 'german'
|
||||
},
|
||||
{
|
||||
name: 'Dutch',
|
||||
value: 'dutch'
|
||||
},
|
||||
{
|
||||
name: 'Swedish',
|
||||
value: 'swedish'
|
||||
},
|
||||
{
|
||||
name: 'Czech',
|
||||
value: 'czech'
|
||||
},
|
||||
{
|
||||
name: 'Polish',
|
||||
value: 'polish'
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
value: 'russian'
|
||||
},
|
||||
{
|
||||
name: 'Bulgarian',
|
||||
value: 'bulgarian'
|
||||
},
|
||||
{
|
||||
name: 'Hebrew',
|
||||
value: 'hebrew'
|
||||
},
|
||||
{
|
||||
name: 'Greek',
|
||||
value: 'greek'
|
||||
},
|
||||
{
|
||||
name: 'Turkish',
|
||||
value: 'turkish'
|
||||
},
|
||||
{
|
||||
name: 'Afrikaans',
|
||||
value: 'afrikaans'
|
||||
},
|
||||
{
|
||||
name: 'Xhosa',
|
||||
value: 'xhosa'
|
||||
},
|
||||
{
|
||||
name: 'Tagalog',
|
||||
value: 'tagalog'
|
||||
},
|
||||
{
|
||||
name: 'Malay',
|
||||
value: 'malay'
|
||||
},
|
||||
{
|
||||
name: 'Indonesian',
|
||||
value: 'indonesian'
|
||||
},
|
||||
{
|
||||
name: 'Bengali',
|
||||
value: 'bengali'
|
||||
},
|
||||
{
|
||||
name: 'Serbian',
|
||||
value: 'serbian'
|
||||
},
|
||||
{
|
||||
name: 'Thai',
|
||||
value: 'thai'
|
||||
},
|
||||
{
|
||||
name: 'Urdu',
|
||||
value: 'urdu'
|
||||
},
|
||||
{
|
||||
name: 'Croatian',
|
||||
value: 'croatian'
|
||||
},
|
||||
{
|
||||
name: 'Hungarian',
|
||||
value: 'hungarian'
|
||||
},
|
||||
{
|
||||
name: 'Danish',
|
||||
value: 'danish'
|
||||
},
|
||||
{
|
||||
name: 'Amharic',
|
||||
value: 'amharic'
|
||||
},
|
||||
{
|
||||
name: 'Albanian',
|
||||
value: 'albanian'
|
||||
},
|
||||
{
|
||||
name: 'Catalan',
|
||||
value: 'catalan'
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian',
|
||||
value: 'ukrainian'
|
||||
},
|
||||
{
|
||||
name: 'Galician',
|
||||
value: 'galician'
|
||||
}
|
||||
];
|
||||
7476
lib/utils/speech-data/tts-microsoft-raw.js
Normal file
7476
lib/utils/speech-data/tts-microsoft-raw.js
Normal file
File diff suppressed because it is too large
Load Diff
28
lib/utils/speech-data/tts-microsoft.js
Normal file
28
lib/utils/speech-data/tts-microsoft.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const TtsAwsLanguagesVoiceRaw = require('./tts-microsoft-raw');
|
||||
|
||||
const languagesVoices = [];
|
||||
|
||||
TtsAwsLanguagesVoiceRaw.forEach((data) => {
|
||||
const lang = languagesVoices.find((l) => {
|
||||
return l.value === data.Locale;
|
||||
});
|
||||
|
||||
if (!lang) {
|
||||
languagesVoices.push({
|
||||
value: data.Locale,
|
||||
name: data.LocaleName,
|
||||
voices: TtsAwsLanguagesVoiceRaw
|
||||
.filter((d) => {
|
||||
return d.Locale === data.Locale;
|
||||
})
|
||||
.map((d) => {
|
||||
return {
|
||||
value: d.ShortName,
|
||||
name: `${d.DisplayName} (${d.Gender})`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = languagesVoices;
|
||||
25
lib/utils/speech-data/tts-model-cartesia.js
Normal file
25
lib/utils/speech-data/tts-model-cartesia.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Sonic',
|
||||
value: 'sonic',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{ name: 'Sonic Preview', value: 'sonic-preview', languages: ['en'] },
|
||||
{
|
||||
name: 'Sonic 2024-12-12',
|
||||
value: 'sonic-2024-12-12',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{
|
||||
name: 'Sonic 2024-10-19',
|
||||
value: 'sonic-2024-10-19',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{ name: 'Sonic English', value: 'sonic-english', languages: ['en'] },
|
||||
{
|
||||
name: 'Sonic Multilingual',
|
||||
value: 'sonic-multilingual',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
];
|
||||
|
||||
68
lib/utils/speech-data/tts-model-deepgram.js
Normal file
68
lib/utils/speech-data/tts-model-deepgram.js
Normal file
@@ -0,0 +1,68 @@
|
||||
module.exports = [
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Asteria English (US) Female',
|
||||
value: 'aura-asteria-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Luna English (US) Female',
|
||||
value: 'aura-luna-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Stella English (US) Female',
|
||||
value: 'aura-stella-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-GB',
|
||||
localeName: 'English (UK)',
|
||||
name: 'Stella English (UK) Female',
|
||||
value: 'aura-athena-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Hera English (US) Female',
|
||||
value: 'aura-hera-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orion English (US) Male',
|
||||
value: 'aura-orion-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Arcas English (US) Male',
|
||||
value: 'aura-arcas-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Perseus English (US) Male',
|
||||
value: 'aura-perseus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-IE',
|
||||
localeName: 'English (Ireland)',
|
||||
name: 'Angus English (Ireland) Male',
|
||||
value: 'aura-angus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orpheus English (US) Male',
|
||||
value: 'aura-orpheus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Zeus English (US) Male',
|
||||
value: 'aura-zeus-en'
|
||||
},
|
||||
];
|
||||
12
lib/utils/speech-data/tts-model-elevenlabs.js
Normal file
12
lib/utils/speech-data/tts-model-elevenlabs.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = [
|
||||
{ name: 'Turbo v2', value: 'eleven_turbo_v2' },
|
||||
{ name: 'Turbo v2.5', value: 'eleven_turbo_v2_5' },
|
||||
{ name: 'Flash v2', value: 'eleven_flash_v2' },
|
||||
{ name: 'Flash v2.5', value: 'eleven_flash_v2_5' },
|
||||
{ name: 'Multilingual v1', value: 'eleven_multilingual_v1' },
|
||||
{ name: 'Multilingual v2', value: 'eleven_multilingual_v2' },
|
||||
{ name: 'Multilingual STS v2', value: 'eleven_multilingual_sts_v2' },
|
||||
{ name: 'English v1', value: 'eleven_monolingual_v1' },
|
||||
{ name: 'English v2', value: 'eleven_english_sts_v2' },
|
||||
];
|
||||
|
||||
7
lib/utils/speech-data/tts-model-playht.js
Normal file
7
lib/utils/speech-data/tts-model-playht.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = [
|
||||
{ name: 'Play3.0', value: 'Play3.0' },
|
||||
{ name: 'PlayHT2.0-turbo', value: 'PlayHT2.0-turbo' },
|
||||
{ name: 'PlayHT2.0', value: 'PlayHT2.0' },
|
||||
{ name: 'PlayHT1.0', value: 'PlayHT1.0' },
|
||||
];
|
||||
|
||||
6
lib/utils/speech-data/tts-model-rimelabs.js
Normal file
6
lib/utils/speech-data/tts-model-rimelabs.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = [
|
||||
{ name: 'Mist', value: 'mist' },
|
||||
{ name: 'Mistv2', value: 'mistv2' },
|
||||
{ name: 'V1', value: 'v1' },
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user