Compare commits

..

1 Commits

Author SHA1 Message Date
Quan HL
cfd1eab7cd fix admin setting issue 2023-05-12 07:17:19 +07:00
154 changed files with 11278 additions and 28797 deletions

View File

@@ -7,22 +7,11 @@ 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

View File

@@ -2,10 +2,16 @@ 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:
@@ -14,42 +20,32 @@ jobs:
if: github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v3
- uses: actions/checkout@v3
- name: prepare tag
id: prepare_tag
- 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
run: |
IMAGE_ID=jambonz/db-create
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$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
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION

View File

@@ -2,8 +2,6 @@ name: Docker
on:
push:
branches:
- main
tags:
- '*'
@@ -20,7 +18,7 @@ jobs:
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=jambonz/api-server
IMAGE_ID=$GITHUB_REPOSITORY
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2024 FirstFive8, Inc.
Copyright (c) 2021 Drachtio Communications Services, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -33,8 +33,6 @@ 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:

127
app.js
View File

@@ -7,20 +7,13 @@ const nocache = require('nocache');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const passport = require('passport');
const {verifyViewOnlyUser} = require('./lib/middleware');
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');
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_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');
@@ -37,28 +30,23 @@ const {
logger, process.env.JAMBONES_TIME_SERIES_HOST
);
const {
client,
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
listQueues,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
incrKey,
listConferences,
getCallCount
incrKey
} = require('./lib/helpers/realtimedb-helpers');
const {
getTtsVoices,
getTtsSize,
purgeTtsCache,
getAwsAuthToken,
getVerbioAccessToken,
synthAudio
} = require('@jambonz/speech-utils')({}, logger);
getTtsVoices
} = require('@jambonz/speech-utils')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
lookupAppBySid,
lookupAccountBySid,
@@ -66,8 +54,7 @@ const {
lookupAppByPhoneNumber,
lookupCarrierBySid,
lookupSipGatewayBySid,
lookupSmppGatewayBySid,
lookupClientByAccountAndUsername
lookupSmppGatewayBySid
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
@@ -79,20 +66,17 @@ const {
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,
listQueues,
purgeCalls,
retrieveSet,
addKey,
@@ -100,11 +84,6 @@ app.locals = {
retrieveKey,
deleteKey,
getTtsVoices,
getTtsSize,
getAwsAuthToken,
getVerbioAccessToken,
purgeTtsCache,
synthAudio,
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,
@@ -112,15 +91,13 @@ app.locals = {
lookupCarrierBySid,
lookupSipGatewayBySid,
lookupSmppGatewayBySid,
lookupClientByAccountAndUsername,
queryCdrs,
queryCdrsSP,
queryAlerts,
queryAlertsSP,
writeCdrs,
writeAlerts,
AlertType,
getCallCount
AlertType
};
const unless = (paths, middleware) => {
@@ -130,35 +107,13 @@ const unless = (paths, middleware) => {
};
};
const RATE_LIMIT_BY = process.env.RATE_LIMIT_BY || 'system';
const limiter = rateLimit({
windowMs: (process.env.RATE_LIMIT_WINDOWS_MINS || 5) * 60 * 1000, // 5 minutes
max: process.env.RATE_LIMIT_MAX_PER_WINDOW || 600, // Limit each IP to 600 requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
keyGenerator: (req, res) => {
switch (RATE_LIMIT_BY) {
case 'system':
return '127.0.0.1';
case 'apikey':
// uses shared limit for requests without an authorization header
const token = req.headers.authorization?.split(' ')[1] || '127.0.0.1';
return token;
case 'ip':
return req.headers['x-real-ip'];
default:
return '127.0.0.1';
}
}
});
// 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) {
@@ -191,19 +146,6 @@ app.use('/v1', unless(
'/InviteCodes',
'/PredefinedCarriers'
], passport.authenticate('bearer', {session: false})));
app.use('/v1', unless(
[
'/register',
'/forgot-password',
'/signin',
'/login',
'/messaging',
'/outboundSMS',
'/AccountTest',
'/InviteCodes',
'/PredefinedCarriers',
'/logout'
], verifyViewOnlyUser));
app.use('/', routes);
app.use((err, req, res, next) => {
logger.error(err, 'burped error');
@@ -212,52 +154,7 @@ app.use((err, req, res, next) => {
});
});
logger.info(`listening for HTTP traffic on port ${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);
});
});
app.listen(PORT);
// purge old calls from active call set every 10 mins
async function purge() {

View File

@@ -1,22 +0,0 @@
#!/bin/bash
# This script exports the 'jambones' database (schema and data)
# from the source MySQL server into a file.
# Configuration variables
SOURCE_HOST=
DB_USER=
DB_PASS=
DB_NAME=
EXPORT_FILE="jambones_export.sql"
# Export the database using mysqldump
echo "Exporting database '$DB_NAME' from $SOURCE_HOST..."
mysqldump -h "$SOURCE_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$EXPORT_FILE"
# Check for errors
if [ $? -eq 0 ]; then
echo "Database export successful. Export file created: $EXPORT_FILE"
else
echo "Error exporting database '$DB_NAME'."
exit 1
fi

View File

@@ -1,31 +0,0 @@
#!/bin/bash
# This script imports the SQL dump file into the target MySQL server.
# It first drops the existing 'jambones' database (if it exists),
# recreates it, and then imports the dump file.
# Configuration variables
TARGET_HOST=
DB_USER=
DB_PASS=
DB_NAME=
IMPORT_FILE="jambones_export.sql"
# Drop the existing database (if any) and create a new one
echo "Dropping and recreating database '$DB_NAME' on $TARGET_HOST..."
mysql -h "$TARGET_HOST" -u "$DB_USER" -p"$DB_PASS" -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\`;"
if [ $? -ne 0 ]; then
echo "Error dropping/creating database '$DB_NAME'."
exit 1
fi
# Import the SQL dump into the newly created database
echo "Importing dump file '$IMPORT_FILE' into database '$DB_NAME'..."
mysql -h "$TARGET_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$IMPORT_FILE"
if [ $? -eq 0 ]; then
echo "Database import successful."
else
echo "Error importing the database."
exit 1
fi

View File

@@ -1,5 +1,4 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips;
@@ -14,8 +13,6 @@ 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;
@@ -54,8 +51,6 @@ 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;
@@ -132,19 +127,6 @@ 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 ,
@@ -340,29 +322,14 @@ 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'
monitoring_domain_name VARCHAR(255)
);
CREATE TABLE users
@@ -416,8 +383,6 @@ 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',
outbound_sip_proxy VARCHAR(255),
PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls';
@@ -446,7 +411,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(132) NOT NULL,
number VARCHAR(132) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
@@ -459,14 +424,11 @@ 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 COMMENT 'sip signaling port',
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
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';
@@ -503,22 +465,10 @@ 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(256) DEFAULT 'en-US-Standard-C',
speech_synthesis_label VARCHAR(64),
speech_synthesis_voice 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),
env_vars TEXT,
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 ';
@@ -556,10 +506,6 @@ 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';
@@ -580,9 +526,6 @@ 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);
@@ -647,6 +590,8 @@ 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);
@@ -654,10 +599,6 @@ 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);
@@ -687,8 +628,6 @@ 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);
@@ -744,4 +683,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;

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +0,0 @@
/* remove VIEW_ONLY permission for admin user as it will prevent write operations*/
delete from user_permissions;
delete from permissions;
insert into permissions (permission_sid, name, description)
values
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');

View File

@@ -22,7 +22,7 @@ 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.cloud');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.us');
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');
@@ -38,9 +38,9 @@ values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fs', '38700987-c7a4-4685-a5bb-af378f9
-- create two applications
insert into webhooks(webhook_sid, url, method)
values
('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');
('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');
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

View File

@@ -24,9 +24,10 @@ 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.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');
('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');
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'),
@@ -67,6 +68,9 @@ 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),

View File

@@ -6,7 +6,6 @@ const {readFile} = require('fs/promises');
const {execSync} = require('child_process');
const {version:desiredVersion} = require('../package.json');
const logger = require('pino')();
const fs = require('fs');
logger.info(`upgrade-jambonz-db: desired version ${desiredVersion}`);
@@ -23,20 +22,6 @@ const opts = {
port: process.env.JAMBONES_MYSQL_PORT || 3306,
multipleStatements: true
};
const rejectUnauthorized = process.env.JAMBONES_MYSQL_REJECT_UNAUTHORIZED;
const ssl_ca_file = process.env.JAMBONES_MYSQL_SSL_CA_FILE;
const ssl_cert_file = process.env.JAMBONES_MYSQL_SSL_CERT_FILE;
const ssl_key_file = process.env.JAMBONES_MYSQL_SSL_KEY_FILE;
const sslFilesProvided = Boolean(ssl_ca_file && ssl_cert_file && ssl_key_file);
if (rejectUnauthorized !== undefined || sslFilesProvided) {
opts.ssl = {
...(rejectUnauthorized !== undefined && { rejectUnauthorized: rejectUnauthorized === '0' ? false : true }),
...(ssl_ca_file && { ca: fs.readFileSync(ssl_ca_file) }),
...(ssl_cert_file && { cert: fs.readFileSync(ssl_cert_file) }),
...(ssl_key_file && { key: fs.readFileSync(ssl_key_file) })
};
}
const sql = {
'7006': [
@@ -103,7 +88,7 @@ const sql = {
'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': [
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',
@@ -153,85 +138,6 @@ const sql = {
'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\'',
'ALTER TABLE voip_carriers ADD COLUMN outbound_sip_proxy VARCHAR(255)',
],
9004: [
'ALTER TABLE applications ADD COLUMN env_vars TEXT',
],
9005: [
'UPDATE applications SET speech_synthesis_voice = \'en-US-Standard-C\' WHERE speech_synthesis_voice IS NULL AND speech_synthesis_vendor = \'google\' AND speech_synthesis_language = \'en-US\'',
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(255) DEFAULT \'en-US-Standard-C\''
]
};
@@ -262,11 +168,6 @@ const doIt = async() => {
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..');

View File

@@ -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.cloud', 'jambonz.cloud service provider', 'sip.yakeeda.com');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.us', 'jambonz.us 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.cloud/testCall', 'POST');
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.cloud/callStatus', 'POST');
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 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',

View File

@@ -35,8 +35,8 @@ function makeStrategy(logger) {
debug(err);
logger.info({err}, 'Error checking redis for jwt');
}
const { user_sid, service_provider_sid, account_sid, email,
name, scope, permissions, is_view_only } = decoded;
const { user_sid, service_provider_sid, account_sid, email, name, scope, permissions } = decoded;
const user = {
service_provider_sid,
account_sid,
@@ -45,7 +45,6 @@ function makeStrategy(logger) {
email,
name,
permissions,
is_view_only,
hasScope: (s) => s === scope,
hasAdminAuth: scope === 'admin',
hasServiceProviderAuth: scope === 'service_provider',

View File

@@ -1,11 +1,10 @@
const logger = require('../logger');
const {
client,
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
listQueues,
purgeCalls,
retrieveSet,
addKey,
@@ -13,23 +12,21 @@ const {
deleteKey,
incrKey,
client: redisClient,
listConferences,
getCallCount
} = require('@jambonz/realtimedb-helpers')({}, logger);
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
module.exports = {
client,
retrieveCall,
deleteCall,
listCalls,
listSortedSets,
listQueues,
purgeCalls,
retrieveSet,
addKey,
retrieveKey,
deleteKey,
redisClient,
incrKey,
listConferences,
getCallCount
incrKey
};

View File

@@ -1,7 +1,11 @@
const opts = {
const opts = Object.assign({
timestamp: () => {
return `, "time": "${new Date().toISOString()}"`;
}
}, {
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const pino = require('pino');
const logger = pino(opts);
});
const logger = require('pino')(opts);
module.exports = logger;

View File

@@ -1,5 +1,4 @@
const logger = require('./logger');
const {UserPermissionError} = require('./utils/errors');
function delayLoginMiddleware(req, res, next) {
if (req.path.includes('/login') || req.path.includes('/signin')) {
@@ -28,26 +27,6 @@ function delayLoginMiddleware(req, res, next) {
next();
}
function verifyViewOnlyUser(req, res, next) {
// Skip check for GET requests
if (req.method === 'GET') {
return next();
}
// current user is changing their password which shuould be allowed
if (req.body?.old_password && req.body?.new_password) {
return next();
}
// Check if user is read-only
if (req.user && !!req.user.is_view_only) {
const upError = new UserPermissionError('User has view-only access');
upError.status = 403;
throw upError;
}
next();
}
module.exports = {
delayLoginMiddleware,
verifyViewOnlyUser
delayLoginMiddleware
};

View File

@@ -4,7 +4,7 @@ const {getMysqlConnection} = require('../db');
const {promisePool} = require('../db');
const { v4: uuid } = require('uuid');
const {encrypt, decrypt} = require('../utils/encrypt-decrypt');
const {encrypt} = 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 = ?, stripe_payment_method_id=?, exp_month = ?, exp_year = ?, card_type = ?
SET last4 = ?, exp_month = ?, exp_year = ?, card_type = ?
WHERE account_sid = ?
AND effective_end_date IS NULL`;
@@ -55,17 +55,6 @@ 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;
@@ -86,8 +75,6 @@ function transmogrifyResults(results) {
else obj.queue_event_hook = null;
delete obj.queue_event_hook_sid;
extractBucketCredential(obj);
return obj;
});
}
@@ -203,18 +190,17 @@ 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\', is_active = 1 WHERE account_sid = ?',
'UPDATE accounts SET plan_type = \'paid\' WHERE account_sid = ?',
[account_sid]);
return true;
}
static async updatePaymentInfo(logger, account_sid, pm) {
const {id, card} = pm;
const {card} = pm;
const last4_encrypted = encrypt(card.last4);
await promisePool.execute(updatePaymentInfoSql,
[last4_encrypted, id, card.exp_month, card.exp_year, card.brand, account_sid]);
[last4_encrypted, card.exp_month, card.exp_year, card.brand, account_sid]);
}
static async provisionPendingSubscription(logger, account_sid, products, payment_method, subscription_id) {
@@ -252,6 +238,7 @@ class Account extends Model {
}));
return account_subscription_sid;
}
}
Account.table = 'accounts';
@@ -332,15 +319,7 @@ Account.fields = [
type: 'string',
},
{
name: 'record_all_calls',
type: 'number'
},
{
name: 'record_format',
type: 'string'
},
{
name: 'bucket_credential',
name: 'lcr_sid',
type: 'string'
}
];

View File

@@ -36,98 +36,20 @@ class Application extends Model {
super();
}
static _criteriaBuilder(obj, args) {
let sql = '';
if (obj.account_sid) {
sql += ' AND app.account_sid = ?';
args.push(obj.account_sid);
}
if (obj.service_provider_sid) {
sql += ' AND app.account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)';
args.push(obj.service_provider_sid);
}
if (obj.name) {
sql += ' AND app.name LIKE ?';
args.push(`%${obj.name}%`);
}
return sql;
}
static countAll(obj) {
let sql = 'SELECT COUNT(*) AS count FROM applications app WHERE 1 = 1';
const args = [];
sql += Application._criteriaBuilder(obj, args);
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query({sql}, args, (err, results) => {
conn.release();
if (err) return reject(err);
resolve(results[0].count);
});
});
});
}
/**
* list all applications - for all service providers, for one service provider, or for one account,
* or by an optional name
* list all applications - for all service providers, for one service provider, or for one account
*/
static retrieveAll(obj) {
const { page, page_size = 50 } = obj || {};
// If pagination is requested, first get the application IDs
if (page !== null && page !== undefined) {
let idSql = 'SELECT application_sid, name FROM applications app WHERE 1 = 1';
const idArgs = [];
idSql += Application._criteriaBuilder(obj, idArgs);
idSql += ' ORDER BY app.name';
const limit = Number(page_size);
const offset = Number(page > 0 ? (page - 1) : page) * limit;
idSql += ' LIMIT ? OFFSET ?';
idArgs.push(limit);
idArgs.push(offset);
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
// Get paginated application IDs
conn.query(idSql, idArgs, (err, idResults) => {
if (err) {
conn.release();
return reject(err);
}
if (idResults.length === 0) {
conn.release();
return resolve([]);
}
// Get full data for these applications
const appIds = idResults.map((row) => row.application_sid);
const placeholders = appIds.map(() => '?').join(',');
const fullSql = `${retrieveSql}
WHERE app.application_sid IN (${placeholders}) ORDER BY app.name`;
conn.query({sql: fullSql, nestTables: true}, appIds, (err, results) => {
conn.release();
if (err) return reject(err);
const r = transmogrifyResults(results);
resolve(r);
});
});
});
});
}
// No pagination - use original query
let sql = retrieveSql + ' WHERE 1 = 1';
static retrieveAll(service_provider_sid, account_sid) {
let sql = retrieveSql;
const args = [];
sql += Application._criteriaBuilder(obj, args);
sql += ' ORDER BY app.application_sid';
if (account_sid) {
sql = `${sql} WHERE 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 = ?)`;
args.push(service_provider_sid);
}
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
@@ -198,10 +120,6 @@ Application.fields = [
{
name: 'messaging_hook_sid',
type: 'string',
},
{
name: 'record_all_calls',
type: 'number',
}
];

View File

@@ -1,70 +0,0 @@
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;

View File

@@ -1,61 +0,0 @@
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;

View File

@@ -1,45 +0,0 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sqlAll = `
SELECT * from permissions
`;
const sqlByName = `
SELECT * from permissions where name = ?
`;
class Permissions extends Model {
constructor() {
super();
}
static async retrieveAll() {
const [rows] = await promisePool.query(sqlAll);
return rows;
}
static async retrieveByName(name) {
const [rows] = await promisePool.query(sqlByName, [name]);
return rows;
}
}
Permissions.table = 'permissions';
Permissions.fields = [
{
name: 'permission_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'description',
type: 'string',
required: true
}
];
module.exports = Permissions;

View File

@@ -1,7 +1,6 @@
const Model = require('./model');
const {promisePool} = require('../db');
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 sql = 'SELECT * from phone_numbers WHERE account_sid = ?';
const sqlSP = `SELECT *
FROM phone_numbers
WHERE account_sid IN
@@ -9,7 +8,7 @@ WHERE account_sid IN
SELECT account_sid
FROM accounts
WHERE service_provider_sid = ?
) ORDER BY number`;
)`;
class PhoneNumber extends Model {
constructor() {
@@ -17,8 +16,8 @@ class PhoneNumber extends Model {
}
static async retrieveAll(account_sid) {
if (!account_sid) return await super.retrieveAll();
const [rows] = await promisePool.query(sqlRetrieveAll, account_sid);
if (!account_sid) return super.retrieveAll();
const [rows] = await promisePool.query(sql, account_sid);
return rows;
}
static async retrieveAllForSP(service_provider_sid) {
@@ -26,55 +25,12 @@ class PhoneNumber extends Model {
return rows;
}
static _criteriaBuilder(obj, params) {
let sql = '';
if (obj.service_provider_sid) {
sql += ' AND account_sid IN (SELECT account_sid FROM accounts WHERE service_provider_sid = ?)';
params.push(obj.service_provider_sid);
}
if (obj.account_sid) {
sql += ' AND account_sid = ?';
params.push(obj.account_sid);
}
if (obj.filter) {
sql += ' AND number LIKE ?';
params.push(`%${obj.filter}%`);
}
return sql;
}
static async countAll(obj) {
let sql = 'SELECT COUNT(*) AS count FROM phone_numbers WHERE 1 = 1';
const args = [];
sql += PhoneNumber._criteriaBuilder(obj, args);
const [rows] = await promisePool.query(sql, args);
return rows[0].count;
}
static async retrieveAllByCriteria(obj) {
let sql = 'SELECT * FROM phone_numbers WHERE 1=1';
const params = [];
const { page, page_size = 50 } = obj || {};
sql += PhoneNumber._criteriaBuilder(obj, params);
sql += ' ORDER BY number';
if (page !== null && page !== undefined) {
const limit = Number(page_size);
const offset = Number(page > 0 ? (page - 1) : page) * limit;
sql += ' LIMIT ? OFFSET ?';
params.push(limit);
params.push(offset);
}
const [rows] = await promisePool.query(sql, params);
return rows;
}
/**
* retrieve a phone number
*/
static async retrieve(sid, account_sid) {
if (!account_sid) return super.retrieve(sid);
const [rows] = await promisePool.query(sqlRetrieveOne, [sid, account_sid]);
const [rows] = await promisePool.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid]);
return rows;
}
}

View File

@@ -51,10 +51,6 @@ SipGateway.fields = [
name: 'is_active',
type: 'number'
},
{
name: 'pad_crypto',
type: 'number'
},
{
name: 'account_sid',
type: 'string'

View File

@@ -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 = ? and account_sid is null';
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ?';
class SpeechCredential extends Model {
constructor() {
@@ -20,22 +20,6 @@ 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]);
}
@@ -102,10 +86,6 @@ SpeechCredential.fields = [
{
name: 'last_tested',
type: 'date'
},
{
name: 'label',
type: 'string'
}
];

View File

@@ -33,10 +33,6 @@ SystemInformation.fields = [
name: 'monitoring_domain_name',
type: 'string',
},
{
name: 'private_network_cidr',
type: 'string',
},
];
module.exports = SystemInformation;

View File

@@ -1,53 +0,0 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sqlAll = `
SELECT * from user_permissions
`;
const sqlByUserIdPermissionSid = `
SELECT * from user_permissions where user_sid = ? and permission_sid = ?
`;
const sqlByUserId = `
SELECT * from user_permissions where user_sid = ?
`;
class UserPermissions extends Model {
constructor() {
super();
}
static async retrieveAll() {
const [rows] = await promisePool.query(sqlAll);
return rows;
}
static async retrieveByUserIdPermissionSid(user_sid, permission_sid) {
const [rows] = await promisePool.query(sqlByUserIdPermissionSid, [user_sid, permission_sid]);
return rows;
}
static async retrieveByUserId(user_sid) {
const [rows] = await promisePool.query(sqlByUserId, [user_sid]);
return rows;
}
}
UserPermissions.table = 'user_permissions';
UserPermissions.fields = [
{
name: 'user_permissions_sid',
type: 'string',
primaryKey: true
},
{
name: 'user_sid',
type: 'string',
required: true
},
{
name: 'permission_sid',
type: 'string',
required: true
}
];
module.exports = UserPermissions;

View File

@@ -8,57 +8,6 @@ class VoipCarrier extends Model {
constructor() {
super();
}
static _criteriaBuilder(obj, args) {
let sql = '';
if (obj.account_sid) {
// carrier belong to an account when
// 1. account_sid is set
// 2. account_sid is null and service_provider_sid matches the account's service_provider_sid
sql += ` AND (vc.account_sid = ? OR
(vc.account_sid IS NULL AND vc.service_provider_sid IN
(SELECT service_provider_sid FROM accounts WHERE account_sid = ?))
)`;
args.push(obj.account_sid);
args.push(obj.account_sid);
}
if (obj.service_provider_sid) {
sql += ' AND vc.service_provider_sid = ?';
args.push(obj.service_provider_sid);
}
if (obj.name) {
sql += ' AND vc.name LIKE ?';
args.push(`%${obj.name}%`);
}
return sql;
}
static async countAll(obj) {
let sql = 'SELECT COUNT(*) AS count FROM voip_carriers vc WHERE 1 = 1';
const args = [];
sql += VoipCarrier._criteriaBuilder(obj, args);
const [rows] = await promisePool.query(sql, args);
return rows[0].count;
}
static async retrieveByCriteria(obj) {
let sql = 'SELECT * from voip_carriers vc WHERE 1 =1';
const args = [];
sql += VoipCarrier._criteriaBuilder(obj, args);
if (obj.page !== null && obj.page !== undefined) {
const limit = Number(obj.page_size || 50);
const offset = (Number(obj.page) - 1) * limit;
sql += ' LIMIT ? OFFSET ?';
args.push(limit, offset);
}
const [rows] = await promisePool.query(sql, args);
if (rows) {
rows.map((r) => r.register_status = JSON.parse(r.register_status || '{}'));
}
return rows;
}
static async retrieveAll(account_sid) {
if (!account_sid) return super.retrieveAll();
const [rows] = await promisePool.query(retrieveSql, account_sid);
@@ -112,10 +61,6 @@ VoipCarrier.fields = [
name: 'requires_register',
type: 'number'
},
{
name: 'register_use_tls',
type: 'number'
},
{
name: 'register_username',
type: 'string'
@@ -187,14 +132,6 @@ VoipCarrier.fields = [
{
name: 'register_status',
type: 'string'
},
{
name: 'dtmf_type',
type: 'string'
},
{
name: 'sip_proxy',
type: 'string'
}
];

View File

@@ -1,71 +0,0 @@
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;

View File

@@ -1,61 +0,0 @@
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;

View File

@@ -1,62 +0,0 @@
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;

View File

@@ -1,6 +0,0 @@
async function record(logger, socket) {
return require('./upload')(logger, socket);
}
module.exports = record;

View File

@@ -1,103 +0,0 @@
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;

View File

@@ -1,97 +0,0 @@
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;

View File

@@ -1,58 +0,0 @@
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: bucket_credential.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
};

View File

@@ -1,5 +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');
const Application = require('../../models/application');
@@ -18,14 +18,10 @@ const {
parseCallSid,
enableSubspace,
disableSubspace,
parseVoipCarrierSid,
hasValue,
parseVoipCarrierSid
} = require('./utils');
const short = require('short-uuid');
const VoipCarrier = require('../../models/voip-carrier');
const { encrypt, obscureBucketCredentialsSensitiveData,
isObscureKey, decrypt } = require('../../utils/encrypt-decrypt');
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
const translator = short();
let idx = 0;
@@ -42,14 +38,20 @@ const getFsUrl = async(logger, retrieveSet, setName) => {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const f = fs[idx++ % fs.length];
logger.debug({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/createCall`;
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`;
} 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 validateRequest = async(req, account_sid) => {
try {
if (req.user.hasScope('admin')) {
@@ -87,40 +89,13 @@ 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 {page, page_size, name} = req.query || {};
const isPaginationRequest = page !== null && page !== undefined;
let results = [];
let total = 0;
if (isPaginationRequest) {
total = await Application.countAll({account_sid, name});
results = await Application.retrieveAll({
account_sid, name, page, page_size
});
} else {
results = await Application.retrieveAll({account_sid});
}
const ret = results.map((a) => {
if (a.env_vars) {
a.env_vars = JSON.parse(decrypt(a.env_vars));
return a;
} else {
return a;
}
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: ret
} : ret;
res.status(200).json(body);
const results = await Application.retrieveAll(null, account_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
@@ -173,89 +148,6 @@ 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',
proxy: user ? user.proxy : null
});
} catch (err) {
sysError(logger, res, err);
}
});
function coerceNumbers(callInfo) {
if (Array.isArray(callInfo)) {
return callInfo.map((ci) => {
@@ -286,16 +178,11 @@ function validateUpdateCall(opts) {
'child_call_hook',
'call_status',
'listen_status',
'transcribe_status',
'conf_hold_status',
'conf_mute_status',
'mute_status',
'sip_request',
'record',
'tag',
'dtmf',
'conferenceParticipantAction',
'dub'
'record'
]
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
@@ -331,34 +218,15 @@ 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)) {
throw new DbErrorBadRequest('sip_request requires method, content_type and content properties');
(!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');
}
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) {
@@ -383,10 +251,7 @@ async function validateCreateCall(logger, sid, req) {
const {lookupAppBySid} = req.app.locals;
const obj = req.body;
if (req.user.hasServiceProviderAuth ||
req.user.hasAccountAuth && req.user.account_sid !== sid) {
throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
}
if (req.user.account_sid !== sid) throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
obj.account_sid = sid;
if (!obj.from) throw new DbErrorBadRequest('missing from parameter');
@@ -398,7 +263,6 @@ 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,
@@ -585,13 +449,11 @@ router.get('/:sid', async(req, res) => {
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 [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);
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]);
}
catch (err) {
sysError(logger, res, err);
@@ -673,74 +535,6 @@ 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,
...(region && {region})
});
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
*/
@@ -787,18 +581,6 @@ 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();
@@ -891,51 +673,6 @@ 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
*/
@@ -959,7 +696,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')}:fs-service-url`;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
@@ -972,25 +709,24 @@ router.post('/:sid/Calls', async(req, res) => {
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
const response = await fetch(serviceUrl, {
request({
url: serviceUrl,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(Object.assign(req.body, {account_sid: sid}))
json: true,
body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
return res.status(201).json(body);
});
if (!response.ok) {
logger.error(`Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.status !== 201) {
logger.error(`Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
const body = await response.json();
return res.status(201).json(body);
} catch (err) {
sysError(logger, res, err);
}
@@ -1081,54 +817,22 @@ const updateCall = async(req, res) => {
await validateRequest(req, accountSid);
const callSid = parseCallSid(req);
validateUpdateCall(req.body);
updateLastUsed(logger, accountSid, req).catch((err) => {});
const call = await retrieveCall(accountSid, callSid);
if (call) {
const url = `${call.serviceUrl}/${process.env.JAMBONES_API_VERSION || 'v1'}/updateCall/${callSid}`;
logger.debug({call, url, payload: req.body}, `updateCall: retrieved call info for call sid ${callSid}`);
const response = await fetch(url, {
request({
url: url,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req.body)
});
if (!response.ok) {
try {
const text = await response.text();
logger.error(`Error sending updateCall POST to ${url}, status: ${response.status} body: ${text}`);
// Try to parse as JSON if there's content
if (text) {
try {
const body = JSON.parse(text);
return res.status(response.status).json(body);
} catch {
// Not valid JSON
return res.status(response.status).send(text);
}
}
return res.sendStatus(response.status);
} catch (err) {
logger.error({err}, `updateCall: error reading response from ${url}`);
return res.sendStatus(response.status);
}
}
if (response.status === 200) {
// feature server return json for sip_request command
// with 200 OK
const body = await response.json();
return res.status(200).json(body);
} else {
// rest commander returns 202 Accepted for all other commands
return res.sendStatus(response.status);
}
json: true,
body: req.body
}).pipe(res);
}
else {
logger.debug(`updateCall: call not found for call sid ${callSid}`);
res.sendStatus(404);
}
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
@@ -1153,11 +857,9 @@ router.post('/:sid/Messages', async(req, res) => {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
return res.status(480).json({msg: 'no available feature servers at this time'});
}
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
await validateCreateMessage(logger, account_sid, req);
const payload = {
@@ -1167,19 +869,22 @@ router.post('/:sid/Messages', async(req, res) => {
};
logger.debug({payload}, `sending createMessage API request to to ${serviceUrl}`);
updateLastUsed(logger, account_sid, req).catch(() => {});
const response = await fetch(serviceUrl, {
request({
url: serviceUrl,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
json: true,
body: payload
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createMessage POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 200) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createMessage ${serviceUrl}`);
return body ? res.status(response.statusCode).json(body) : res.sendStatus(response.statusCode);
}
res.status(201).json(body);
});
if (!response.ok) {
logger.error(`Error sending createMessage POST to ${serviceUrl}`);
return res.sendStatus(500);
}
const body = await response.json();
return res.status(response.status).json(body);
} catch (err) {
sysError(logger, res, err);
}
@@ -1189,12 +894,12 @@ 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 {logger, listQueues} = 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);
const queues = search ? await listQueues(accountSid, search) : await listQueues(accountSid);
logger.debug(`retrieved ${queues.length} queues for account sid ${accountSid}`);
res.status(200).json(queues);
updateLastUsed(logger, accountSid, req).catch((err) => {});
@@ -1203,40 +908,4 @@ router.get('/:sid/Queues', async(req, res) => {
}
});
/**
* 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);
}
});
/**
* retrieve counts of calls under an account
*/
router.get('/:sid/CallCount', async(req, res) => {
const {logger, getCallCount} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
await validateRequest(req, accountSid);
const count = await getCallCount(accountSid);
count.outbound = Number(count.outbound);
count.inbound = Number(count.inbound);
logger.debug(`retrieved, outbound: ${count.outbound}, inbound: ${count.inbound}, for account sid ${accountSid}`);
res.status(200).json(count);
updateLastUsed(logger, accountSid, req).catch((err) => {});
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -16,7 +16,7 @@ router.get('/', async(req, res) => {
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
const {page, count, alert_type, days, start, end} = req.query || {};
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
if (!count || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (account_sid) {
const data = await queryAlerts({

View File

@@ -1,47 +0,0 @@
const router = require('express').Router();
const sysError = require('../error');
const { fetchAppEnvSchema, validateAppEnvSchema } = require('../../utils/appenv_utils');
const URL = require('url').URL;
const isValidUrl = (s) => {
const protocols = ['https:', 'http:', 'ws:', 'wss:'];
try {
const url = new URL(s);
if (protocols.includes(url.protocol)) {
return true;
}
else {
return false;
}
} catch (err) {
return false;
}
};
/* get appenv schema for endpoint */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const url = req.query.url;
if (!isValidUrl(url)) {
sysError(logger, res, 'Invalid URL');
} else {
try {
const appenv = await fetchAppEnvSchema(logger, url);
if (appenv && validateAppEnvSchema(appenv)) {
return res.status(200).json(appenv);
} else if (appenv) {
return res.status(400).json({
msg: 'Invalid appenv schema',
});
} else {
return res.status(204).end(); //No appenv returned from url, normal scenario
}
}
catch (err) {
sysError(logger, res, err);
}
}
});
module.exports = router;

View File

@@ -7,13 +7,11 @@ const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const { validate } = require('@jambonz/verb-specifications');
const { parseApplicationSid, isInvalidUrl } = require('./utils');
const { parseApplicationSid } = require('./utils');
const preconditions = {
'add': validateAdd,
'update': validateUpdate
};
const { fetchAppEnvSchema, validateAppEnvData } = require('../../utils/appenv_utils');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
const validateRequest = async(req, account_sid) => {
try {
@@ -64,14 +62,6 @@ async function validateAdd(req) {
if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') {
throw new DbErrorBadRequest('\'call_status_hook\' must be an object when adding an application');
}
let urlError = await isInvalidUrl(req.body.call_hook.url);
if (urlError) {
throw new DbErrorBadRequest(`call_hook ${urlError}`);
}
urlError = await isInvalidUrl(req.body.call_status_hook.url);
if (urlError) {
throw new DbErrorBadRequest(`call_status_hook ${urlError}`);
}
}
async function validateUpdate(req, sid) {
@@ -152,16 +142,6 @@ router.post('/', async(req, res) => {
throw new DbErrorBadRequest(err);
}
}
// validate env_vars data if required
if (obj['env_vars']) {
const appenvschema = await fetchAppEnvSchema(logger, req.body.call_hook.url);
const errors = await validateAppEnvData(appenvschema, obj['env_vars']);
if (errors) {
throw new DbErrorBadRequest(errors);
} else {
obj['env_vars'] = encrypt(JSON.stringify(obj['env_vars']));
}
}
const uuid = await Application.make(obj);
res.status(201).json({sid: uuid});
@@ -173,34 +153,11 @@ router.post('/', async(req, res) => {
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const {page, page_size, name} = req.query || {};
const isPaginationRequest = page !== null && page !== undefined;
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;
let results = [];
let total = 0;
if (isPaginationRequest) {
total = await Application.countAll({service_provider_sid, account_sid, name});
}
results = await Application.retrieveAll({
service_provider_sid, account_sid, name, page, page_size
});
const ret = results.map((a) => {
if (a.env_vars) {
a.env_vars = JSON.parse(decrypt(a.env_vars));
return a;
} else {
return a;
}
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: ret
} : ret;
res.status(200).json(body);
const results = await Application.retrieveAll(service_provider_sid, account_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
@@ -216,9 +173,6 @@ router.get('/:sid', async(req, res) => {
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);
if (results[0].env_vars) {
results[0].env_vars = JSON.parse(decrypt(results[0].env_vars));
}
return res.status(200).json(results[0]);
}
catch (err) {
@@ -273,8 +227,6 @@ router.put('/:sid', async(req, res) => {
try {
const sid = parseApplicationSid(req);
await validateUpdate(req, sid);
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
// create webhooks if provided
const obj = Object.assign({}, req.body);
@@ -306,21 +258,6 @@ router.put('/:sid', async(req, res) => {
}
}
// validate env_vars data if required
if (obj['env_vars']) {
const applications = await Application.retrieve(sid, service_provider_sid, account_sid);
const call_hook_url = req.body.call_hook ?
(req.body.call_hook.url || req.body.call_hook) :
applications[0].call_hook.url;
const appenvschema = await fetchAppEnvSchema(logger, call_hook_url);
const errors = await validateAppEnvData(appenvschema, obj['env_vars']);
if (errors) {
throw new DbErrorBadRequest(errors);
} else {
obj['env_vars'] = encrypt(JSON.stringify(obj['env_vars']));
}
}
const rowsAffected = await Application.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();

View File

@@ -1,88 +0,0 @@
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;

View File

@@ -6,7 +6,6 @@ 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
@@ -27,8 +26,7 @@ function createOauthEmailText(provider) {
}
function createResetEmailText(link) {
assert(process.env.JAMBONZ_BASE_URL, 'process.env.JAMBONZ_BASE_URL is missing');
const baseUrl = process.env.JAMBONZ_BASE_URL;
const baseUrl = 'http://localhost:3001';
return `Hi there!
@@ -52,21 +50,20 @@ router.post('/', async(req, res) => {
let obj;
try {
if (!email || !validateEmail(email)) {
logger.info({email}, 'Bad POST to /forgot-password is missing email or invalid email');
return res.status(400).json({error: 'invalid or missing email'});
}
const [r] = await promisePool.query({sql, nestTables: true}, email);
if (0 === r.length) {
logger.info(`user not found: ${email}`);
logger.info('user not found');
return res.status(400).json({error: 'failed to reset your password'});
}
obj = r[0];
if (!obj.user.is_active) {
logger.info({user: obj.user.name, obj}, 'user is inactive');
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({account_sid: obj.acc.account_sid, obj}, 'account is inactive');
logger.info(obj.acc.account_sid, 'account is inactive');
return res.status(400).json({error: 'failed to reset your password'});
}
res.sendStatus(204);

View File

@@ -1,137 +0,0 @@
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;

View File

@@ -17,7 +17,6 @@ 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'));
@@ -39,7 +38,6 @@ api.use('/change-password', require('./change-password'));
api.use('/ActivationCode', require('./activation-code'));
api.use('/Availability', require('./availability'));
api.use('/AccountTest', require('./account-test'));
api.use('/AppEnv', require('./appenv'));
//api.use('/Products', require('./products'));
api.use('/Prices', require('./prices'));
api.use('/StripeCustomerId', require('./stripe-customer-id'));
@@ -52,9 +50,6 @@ api.use('/PasswordSettings', require('./password-settings'));
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

View File

@@ -100,93 +100,6 @@ const preconditions = {
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 {

View File

@@ -13,6 +13,8 @@ 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, incrKey, retrieveKey} = req.app.locals;
@@ -29,9 +31,6 @@ router.post('/', async(req, res) => {
return res.sendStatus(403);
}
logger.info({r}, 'successfully retrieved user account');
if (r[0].provider !== 'local') {
return res.sendStatus(403);
}
const maxLoginAttempts = process.env.LOGIN_ATTEMPTS_MAX_RETRIES || 6;
const loginAttempsBlocked = await retrieveKey(`login:${r[0].user_sid}`) >= maxLoginAttempts;
@@ -55,6 +54,11 @@ router.post('/', async(req, res) => {
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 [p] = await promisePool.query(retrievePemissionsSql, r[0].user_sid);
const permissions = p.map((x) => x.name);
@@ -74,13 +78,9 @@ router.post('/', async(req, res) => {
obj.service_provider_sid = r[0].service_provider_sid;
obj.service_provider_name = service_provider[0].name;
}
// if there is only one permission and it is VIEW_ONLY, then the user is view only
// this is to prevent write operations on the API
const is_view_only = permissions.length === 1 && permissions.includes('VIEW_ONLY');
const payload = {
scope: obj.scope,
permissions,
is_view_only,
...(obj.service_provider_sid && {
service_provider_sid: obj.service_provider_sid,
service_provider_name: obj.service_provider_name
@@ -90,8 +90,7 @@ router.post('/', async(req, res) => {
account_name: obj.account_name,
service_provider_name: obj.service_provider_name
}),
user_sid: obj.user_sid,
name: username
user_sid: obj.user_sid
};
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;

View File

@@ -13,7 +13,6 @@ const preconditions = {
};
const sysError = require('../error');
const { parsePhoneNumberSid } = require('./utils');
const hasWhitespace = (str) => /\s/.test(str);
/* check for required fields when adding */
@@ -29,7 +28,6 @@ async function validateAdd(req) {
}
if (!req.body.number) throw new DbErrorBadRequest('number is required');
if (hasWhitespace(req.body.number)) throw new DbErrorBadRequest('number cannot contain whitespace');
const formattedNumber = e164(req.body.number);
req.body.number = formattedNumber;
} catch (err) {
@@ -42,10 +40,6 @@ async function validateAdd(req) {
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`voip_carrier not found for sid ${req.body.voip_carrier_sid}`);
}
const carrier = result[0];
if (carrier.account_sid && req.body.account_sid && req.body.account_sid !== carrier.account_sid) {
throw new DbErrorBadRequest('voip_carrier_sid does not belong to the account');
}
}
}
@@ -99,35 +93,11 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const {service_provider_sid: query_service_provider_sid,
account_sid: query_account_sid, filter, page, page_size} = req.query;
const isPaginationRequest = page !== null && page !== undefined;
let service_provider_sid = null, account_sid = query_account_sid;
if (req.user.hasAccountAuth) {
account_sid = req.user.account_sid;
} else if (req.user.hasServiceProviderAuth) {
service_provider_sid = req.user.service_provider_sid;
} else {
// admin user can query all phone numbers
service_provider_sid = query_service_provider_sid;
account_sid = query_account_sid;
}
try {
let total = 0;
if (isPaginationRequest) {
total = await PhoneNumber.countAll({service_provider_sid, account_sid, filter});
}
const results = await PhoneNumber.retrieveAllByCriteria({
service_provider_sid, account_sid, filter, page, page_size
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: results,
} : results;
res.status(200).json(body);
const results = req.user.hasAdminAuth ?
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
@@ -150,9 +120,6 @@ router.get('/:sid', async(req, res) => {
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) {

View File

@@ -3,16 +3,6 @@ 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 { CloudWatchLogsClient, FilterLogEventsCommand } = require('@aws-sdk/client-cloudwatch-logs');
const {
getS3Object,
getGoogleStorageObject,
getAzureStorageObject,
deleteS3Object,
deleteGoogleStorageObject,
deleteAzureStorageObject
} = require('../../utils/storage-utils');
const parseAccountSid = (url) => {
const arr = /Accounts\/([^\/]*)/.exec(url);
@@ -30,9 +20,9 @@ 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, filter} = req.query || {};
const {page, count, trunk, direction, days, answered, start, end} = req.query || {};
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
if (!count || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (account_sid) {
const data = await queryCdrs({
@@ -45,7 +35,6 @@ router.get('/', async(req, res) => {
answered,
start: days ? undefined : start,
end: days ? undefined : end,
filter
});
res.status(200).json(data);
}
@@ -60,7 +49,6 @@ router.get('/', async(req, res) => {
answered,
start: days ? undefined : start,
end: days ? undefined : end,
filter
});
res.status(200).json(data);
}
@@ -86,12 +74,12 @@ router.get('/:call_id', async(req, res) => {
}
});
router.get('/:call_id/:method/pcap', async(req, res) => {
router.get('/:call_id/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], req.params.method);
const stream = await getHomerPcap(logger, token, [req.params.call_id]);
if (!stream) {
logger.info(`getHomerApiKey: unable to get sip traces from Homer for ${req.params.call_id}`);
return res.sendStatus(404);
@@ -107,71 +95,6 @@ router.get('/:call_id/:method/pcap', async(req, res) => {
}
});
router.get('/:call_sid/logs', async(req, res) => {
const {logger, queryCdrs} = req.app.locals;
const aws_region = process.env.AWS_REGION;
const {call_sid} = req.params;
const {logGroupName = 'jambonz-feature_server'} = req.query;
const account_sid = parseAccountSid(req.originalUrl);
if (!aws_region) {
return res.status(400).send({msg: 'Logs are only available in AWS environments'});
}
if (!account_sid) {
return res.status(400).send({msg: 'account_sid is required,' +
'please use /Accounts/{account_sid}/RecentCalls/{call_sid}/logs'});
}
try {
//find back the call in CDR to get timestame of the call
// this allow us limit search in cloudwatch logs
const data = await queryCdrs({
account_sid,
filter: call_sid,
page: 0,
page_size: 50
});
if (!data || data.data.length === 0) {
return res.status(404).send({msg: 'Call not found'});
}
const {
attempted_at, //2025-02-24T13:11:51.969Z
terminated_at, //2025-02-24T13:11:56.153Z
sip_callid
} = data.data[0];
const TIMEBUFFER = 60; //60 seconds
const startTime = new Date(attempted_at).getTime() - TIMEBUFFER * 1000;
const endTime = new Date(terminated_at).getTime() + TIMEBUFFER * 1000;
const client = new CloudWatchLogsClient({ region: aws_region });
let params = {
logGroupName,
startTime,
endTime,
filterPattern: `{ ($.callSid = "${call_sid}") || ($.callId = "${sip_callid}") }`
};
const command = new FilterLogEventsCommand(params);
const response = await client.send(command);
// if response have nextToken, we need to fetch all logs
while (response.nextToken) {
params = {
...params,
nextToken: response.nextToken
};
const command = new FilterLogEventsCommand(params);
const response2 = await client.send(command);
response.events = response.events.concat(response2.events);
response.nextToken = response2.nextToken;
}
let logs = [];
if (response.events && response.events.length > 0) {
logs = response.events.map((e) => e.message);
}
res.status(200).json(logs);
} catch (err) {
logger.error({err}, 'Cannot fetch logs from cloudwatch');
res.status(500).send({msg: err.message});
}
});
router.get('/trace/:trace_id', async(req, res) => {
const {logger} = req.app.locals;
const {trace_id} = req.params;
@@ -188,84 +111,4 @@ router.get('/trace/:trace_id', async(req, res) => {
}
});
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;

View File

@@ -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, emailSimpleText} = require('../../utils/email-utils');
const {validateEmail} = require('../../utils/email-utils');
const {cacheClient} = require('../../helpers');
const { v4: uuid } = require('uuid');
const short = require('short-uuid');
@@ -12,14 +12,12 @@ 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, service_provider_sid)
values (?, ?, ?, ?, ?, 0, 'local', ?, ?)`;
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider, hashed_password)
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)`;
@@ -37,19 +35,8 @@ 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, service_provider_sid) => {
name, email, email_activation_code, passwordHash) => {
const [r] = await promisePool.execute(insertUserLocalSql,
[
user_sid,
@@ -57,8 +44,7 @@ const addLocalUser = async(logger, user_sid, account_sid,
name,
email,
email_activation_code,
passwordHash,
service_provider_sid
passwordHash
]);
debug({r}, 'Result from adding user');
};
@@ -159,7 +145,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.email,
name: user.name,
email: user.email,
email_validated: user.email_validated,
avatar_url: user.avatar_url,
@@ -171,7 +157,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.email || user.email,
name: user.name || user.email,
email: user.email,
email_validated: user.verified_email,
picture: user.picture,
@@ -184,7 +170,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.email,
name: user.name,
email: user.email,
provider: 'local',
email_activation_code: user.email_activation_code
@@ -294,8 +280,7 @@ 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, req.body.service_provider_sid);
userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash);
debug('added local user');
}
else {
@@ -308,25 +293,17 @@ router.post('/', async(req, res) => {
const callStatusSid = uuid();
const helloWordSid = uuid();
const dialTimeSid = uuid();
const echoSid = uuid();
/* 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']);
/* 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']);
/* 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,
@@ -336,15 +313,6 @@ 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 */
@@ -359,7 +327,7 @@ router.post('/', async(req, res) => {
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
userProfile.name, userProfile.email, userProfile.email_activation_code,
passwordHash, req.body.service_provider_sid);
passwordHash);
/* note: we deactivate the old user once the new email is validated */
}
@@ -381,8 +349,6 @@ 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 });

View File

@@ -28,13 +28,20 @@ router.get('/', async(req, res) => {
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 DbErrorBadRequest('invalid account_sid');
if (0 === r.length) throw new Error('invalid account_sid');
service_provider_sid = r[0].service_provider_sid;
}
if (req.user.hasServiceProviderAuth) {
service_provider_sid = req.user.service_provider_sid;
const [r] = await promisePool.query(
'SELECT * from service_providers where service_provider_sid = ?',
service_provider_sid);
if (0 === r.length) throw new Error('invalid account_sid');
service_provider_sid = r[0].service_provider_sid;
if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query');
}
/** generally, we have a global set of SBCs that all accounts use.

View File

@@ -149,30 +149,13 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
try {
await validateRetrieve(req);
const service_provider_sid = parseServiceProviderSid(req);
const {account_sid: query_account_sid, name, page, page_size} = req.query || {};
const isPaginationRequest = page !== null && page !== undefined;
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : query_account_sid || null;
let carriers = [];
let total = 0;
if (isPaginationRequest) {
total = await VoipCarrier.countAll({service_provider_sid, account_sid, name});
const carriers = await VoipCarrier.retrieveAllForSP(service_provider_sid);
if (req.user.hasScope('account')) {
return res.status(200).json(carriers.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
}
carriers = await VoipCarrier.retrieveByCriteria({
service_provider_sid,
account_sid,
name,
page,
page_size,
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: carriers,
} : carriers;
res.status(200).json(body);
res.status(200).json(carriers);
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -4,9 +4,7 @@ const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
//const {parseSipGatewaySid} = require('./utils');
const decorate = require('./decorate');
const sysError = require('../error');
const net = require('net');
const hasWhitespace = (str) => /\s/.test(str);
const checkUserScope = async(req, voip_carrier_sid) => {
const {lookupCarrierBySid} = req.app.locals;
if (!voip_carrier_sid) {
@@ -43,7 +41,6 @@ const checkUserScope = async(req, voip_carrier_sid) => {
const validate = async(req, sid) => {
const {lookupSipGatewayBySid} = req.app.locals;
const {netmask, ipv4, inbound, outbound} = req.body;
let voip_carrier_sid;
if (sid) {
@@ -55,21 +52,6 @@ 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 (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}`);
}
if (hasWhitespace(ipv4)) {
throw new DbErrorBadRequest('Gateway must not contain whitespace');
}
if (inbound && !net.isIPv4(ipv4)) {
throw new DbErrorBadRequest('Inbound gateway must be IPv4 address');
}
if (!inbound && outbound && (netmask && netmask != 32)) {
throw new DbErrorBadRequest('For outbound only gateway netmask can only be 32');
}
await checkUserScope(req, voip_carrier_sid);
};

View File

@@ -31,7 +31,6 @@ 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 = ?',
@@ -49,7 +48,7 @@ router.post('/:sip_realm', async(req, res) => {
}
/* add the dns records */
const records = await createDnsRecords(logger, domain, subdomain, uniqueIps);
const records = await createDnsRecords(logger, domain, subdomain, ips);
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})`;

View File

@@ -1,4 +1,5 @@
const router = require('express').Router();
const request = require('request');
const getProvider = require('../../utils/sms-provider');
const { v4: uuidv4 } = require('uuid');
const sysError = require('../error');
@@ -13,14 +14,20 @@ const getFsUrl = async(logger, retrieveSet, setName, provider) => {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const f = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/messaging/${provider}`;
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}`;
} 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);
@@ -37,7 +44,7 @@ router.post('/:provider', async(req, res) => {
lookupAppByPhoneNumber,
logger
} = req.app.locals;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:fs-service-url`;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
logger.debug({path: req.path, body: req.body}, 'incomingSMS from carrier');
// search for provider module
@@ -121,19 +128,25 @@ router.post('/:provider', async(req, res) => {
logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${serviceUrl}`);
const response = await fetch(serviceUrl, {
request({
url: serviceUrl,
method: 'POST',
body: JSON.stringify(payload),
headers: {'Content-Type': 'application/json'},
});
if (!response.ok) {
logger.error({response}, `Error sending incomingSms POST to ${serviceUrl}`);
json: true,
body: payload,
},
async(err, response, body) => {
if (err) {
logger.error(err, `Error sending incomingSms POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (200 === response.statusCode) {
// success
logger.info({body}, 'sending response to provider for incomingSMS');
return doSendResponse(res, respondFn, body);
}
logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${serviceUrl}`);
return res.sendStatus(500);
}
const body = await response.json();
logger.info({body}, 'sending response to provider for incomingSMS');
return doSendResponse(res, respondFn, body);
});
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -5,19 +5,7 @@ const SpeechCredential = require('../../models/speech-credential');
const sysError = require('../error');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = require('./utils');
const {decryptCredential, testWhisper, testDeepgramTTS,
getLanguagesAndVoicesForVendor,
testPlayHT,
testRimelabs,
testVerbioTts,
testVerbioStt,
testSpeechmaticsStt,
testCartesia,
testVoxistStt,
testOpenAiStt,
testInworld,
testResembleTTS} = require('../../utils/speech-utils');
const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors');
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const {
testGoogleTts,
testGoogleStt,
@@ -31,9 +19,7 @@ const {
testDeepgramStt,
testSonioxStt,
testIbmTts,
testIbmStt,
testElevenlabs,
testAssemblyStt
testIbmStt
} = require('../../utils/speech-utils');
const {promisePool} = require('../../db');
@@ -113,6 +99,17 @@ const validateTest = async(req, speech_credentials) => {
}
};
const obscureKey = (key) => {
const key_spoiler_length = 6;
const key_spoiler_char = 'X';
if (key.length <= key_spoiler_length) {
return key;
}
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
};
const encryptCredential = (obj) => {
const {
vendor,
@@ -121,28 +118,15 @@ 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,
playht_tts_uri,
resemble_tts_uri,
resemble_tts_use_tls,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
use_for_stt,
use_for_tts,
custom_stt_endpoint,
custom_stt_endpoint_url,
tts_api_key,
tts_region,
stt_api_key,
@@ -151,18 +135,7 @@ const encryptCredential = (obj) => {
instance_id,
custom_stt_url,
custom_tts_url,
custom_tts_streaming_url,
auth_token = '',
cobalt_server_uri,
// For most vendors, model_id is being used for both TTS and STT, or one of them.
// for Cartesia, model_id is used for TTS only. introduce stt_model_id for STT
model_id,
stt_model_id,
user_id,
voice_engine,
engine_version,
service_version,
options
auth_token = ''
} = obj;
switch (vendor) {
@@ -178,33 +151,22 @@ const encryptCredential = (obj) => {
return encrypt(service_key);
case 'aws':
// 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}),
});
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});
return encrypt(awsData);
case 'microsoft':
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');
}
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 && {region}),
...(api_key && {api_key}),
region,
api_key,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url
custom_stt_endpoint
});
return encrypt(azureData);
@@ -221,28 +183,10 @@ const encryptCredential = (obj) => {
return encrypt(nuanceData);
case 'deepgram':
// 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, model_id});
assert(api_key, 'invalid deepgram speech credential: api_key is required');
const deepgramData = JSON.stringify({api_key});
return encrypt(deepgramData);
case 'resemble':
assert(api_key, 'invalid resemble speech credential: api_key is required');
const resembleData = JSON.stringify({
api_key,
...(resemble_tts_uri && {resemble_tts_uri}),
...(resemble_tts_use_tls && {resemble_tts_use_tls})
});
return encrypt(resembleData);
case 'deepgramriver':
assert(api_key, 'invalid deepgram river speech credential: api_key is required');
const deepgramriverData = JSON.stringify({api_key});
return encrypt(deepgramriverData);
case 'ibm':
const ibmData = JSON.stringify({tts_api_key, tts_region, stt_api_key, stt_region, instance_id});
return encrypt(ibmData);
@@ -257,89 +201,9 @@ 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, playht_tts_uri, options});
return encrypt(playhtData);
case 'cartesia':
assert(api_key, 'invalid cartesia speech credential: api_key is required');
if (use_for_tts) {
assert(model_id, 'invalid cartesia speech credential: model_id is required');
}
if (use_for_stt) {
assert(stt_model_id, 'invalid cartesia speech credential: stt_model_id is required');
}
const cartesiaData = JSON.stringify({
api_key,
...(model_id && {model_id}),
...(stt_model_id && {stt_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 'inworld':
assert(api_key, 'invalid inworld speech credential: api_key is required');
assert(model_id, 'invalid inworld speech credential: model_id is required');
const inworldData = JSON.stringify({api_key, model_id, options});
return encrypt(inworldData);
case 'assemblyai':
assert(api_key, 'invalid assemblyai speech credential: api_key is required');
const assemblyaiData = JSON.stringify({api_key, service_version});
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 'openai':
assert(api_key, 'invalid openai speech credential: api_key is required');
assert(model_id, 'invalid openai speech credential: model_id is required');
const openaiData = JSON.stringify({api_key, model_id});
return encrypt(openaiData);
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, custom_tts_streaming_url});
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
return encrypt(customData);
}
else assert(false, `invalid or missing vendor: ${vendor}`);
@@ -354,7 +218,6 @@ 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 ||
@@ -369,21 +232,11 @@ 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
@@ -420,7 +273,66 @@ router.get('/', async(req, res) => {
res.status(200).json(creds.map((c) => {
const {credential, ...obj} = c;
decryptCredential(obj, credential, logger);
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;
}
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
@@ -450,7 +362,66 @@ router.get('/:sid', async(req, res) => {
await validateRetrieveUpdateDelete(req, cred);
const {credential, ...obj} = cred[0];
decryptCredential(obj, credential, logger);
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;
}
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
@@ -517,27 +488,8 @@ router.put('/:sid', async(req, res) => {
const {
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url,
custom_stt_url,
custom_tts_url,
custom_tts_streaming_url,
cobalt_server_uri,
model_id,
stt_model_id,
voice_engine,
options,
deepgram_stt_uri,
deepgram_stt_use_tls,
deepgram_tts_uri,
playht_tts_uri,
engine_version,
service_version,
speechmatics_stt_uri,
resemble_tts_use_tls,
resemble_tts_uri
custom_stt_endpoint
} = req.body;
const newCred = {
@@ -547,32 +499,13 @@ 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,
custom_stt_url,
custom_tts_url,
custom_tts_streaming_url,
cobalt_server_uri,
model_id,
stt_model_id,
voice_engine,
options,
deepgram_stt_uri,
deepgram_stt_use_tls,
deepgram_tts_uri,
playht_tts_uri,
engine_version,
service_version,
speechmatics_stt_uri,
resemble_tts_uri,
resemble_tts_use_tls
nuance_tts_uri
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -601,7 +534,7 @@ router.put('/:sid', async(req, res) => {
* Test a credential
*/
router.get('/:sid/test', async(req, res) => {
const {logger, synthAudio, getVerbioAccessToken} = req.app.locals;
const logger = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const creds = await SpeechCredential.retrieve(sid);
@@ -649,13 +582,12 @@ 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';
@@ -667,10 +599,9 @@ router.get('/:sid/test', async(req, res) => {
}
if (cred.use_for_stt) {
try {
await testAwsStt(logger, getAwsAuthToken, {
await testAwsStt(logger, {
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';
@@ -687,22 +618,18 @@ 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_url
custom_stt_endpoint
} = credential;
if (cred.use_for_tts) {
try {
await testMicrosoftTts(logger, synthAudio, {
await testMicrosoftTts(logger, {
api_key,
region,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url
custom_stt_endpoint
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
@@ -713,7 +640,7 @@ router.get('/:sid/test', async(req, res) => {
}
if (cred.use_for_stt) {
try {
await testMicrosoftStt(logger, {api_key, region, use_custom_stt, custom_stt_endpoint_url});
await testMicrosoftStt(logger, {api_key, region});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
@@ -772,43 +699,10 @@ router.get('/:sid/test', async(req, res) => {
SpeechCredential.sttTestResult(sid, false);
}
}
} else if (cred.vendor === 'resemble') {
if (cred.use_for_tts) {
try {
await testResembleTTS(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 === 'deepgram') {
const {api_key} = credential;
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';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
else if (cred.vendor === 'deepgramriver') {
else if (cred.vendor === 'deepgram') {
const {api_key} = credential;
if (cred.use_for_stt && api_key) {
if (cred.use_for_stt) {
try {
await testDeepgramStt(logger, {api_key});
results.stt.status = 'ok';
@@ -864,160 +758,6 @@ 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;
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 || cred.use_for_stt) {
try {
// Cartesia does not have API for testing STT, same key is used for both TTS and STT
await testCartesia(logger, synthAudio, credential);
if (cred.use_for_tts) {
results.tts.status = 'ok';
}
if (cred.use_for_stt) {
results.stt.status = 'ok';
}
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
let reason = err.message;
try {
reason = await err.text();
} catch {}
if (cred.use_for_tts) {
results.tts = {status: 'fail', reason};
}
if (cred.use_for_stt) {
results.stt = {status: 'fail', reason};
}
SpeechCredential.ttsTestResult(sid, false);
}
}
} else if (cred.vendor === 'inworld') {
if (cred.use_for_tts) {
try {
await testInworld(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 === '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 === 'openai') {
if (cred.use_for_stt) {
try {
await testOpenAiStt(logger, credential);
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 === '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);
@@ -1027,34 +767,4 @@ 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;

View File

@@ -229,7 +229,7 @@ const updateQuantities = async(req, res) => {
const obj = {
quantity: product.quantity,
};
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price: product.price_id});
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price_id: product.price_id});
});
if (dry_run) {

View File

@@ -1,137 +0,0 @@
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;

View File

@@ -1,7 +1,5 @@
const router = require('express').Router();
const User = require('../../models/user');
const UserPermissions = require('../../models/user-permissions');
const Permissions = require('../../models/permissions');
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
const {promisePool} = require('../../db');
@@ -40,8 +38,7 @@ const validateRequest = async(user_sid, req) => {
email,
email_activation_code,
force_change,
is_active,
is_view_only
is_active
} = payload;
const [r] = await promisePool.query(retrieveSql, user_sid);
@@ -96,8 +93,7 @@ const validateRequest = async(user_sid, req) => {
if (email_activation_code && !email) {
throw new DbErrorBadRequest('email and email_activation_code both required');
}
if (!name && !new_password && !email && !initial_password && !force_change && !is_active &&
is_view_only === undefined)
if (!name && !new_password && !email && !initial_password && !force_change && !is_active)
throw new DbErrorBadRequest('no updates requested');
return user;
@@ -144,35 +140,7 @@ const ensureUserRetrievalIsAllowed = (req, user) => {
throw error;
}
};
async function updateViewOnlyUserPermission(is_view_only, user_sid) {
try {
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
if (!viewOnlyPermission) {
throw new Error('VIEW_ONLY permission not found');
}
const existingPermissions = await UserPermissions.retrieveByUserIdPermissionSid(
user_sid,
viewOnlyPermission.permission_sid
);
if (is_view_only && existingPermissions.length === 0) {
await UserPermissions.make({
user_sid,
permission_sid: viewOnlyPermission.permission_sid,
});
} else if (!is_view_only && existingPermissions.length > 0) {
await UserPermissions.remove(existingPermissions[0].user_permissions_sid);
}
} catch (err) {
throw new Error(`Failed to update user permissions: ${err.message}`);
}
}
async function removeViewOnlyUserPermission(user_id) {
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
if (viewOnlyPermission) {
await UserPermissions.remove(user_id, viewOnlyPermission.permission_sid);
}
}
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
@@ -325,7 +293,6 @@ router.get('/me', async(req, res) => {
res.json(payload);
} catch (err) {
sysError(logger, res, err);
logger.info({err, payload}, 'payload');
}
});
@@ -341,14 +308,7 @@ router.get('/:user_sid', async(req, res) => {
}
ensureUserRetrievalIsAllowed(req, user);
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
const existingPermissions = await UserPermissions.retrieveByUserId(
user_sid
);
logger.debug(`existingPermissions of ${user_sid}: ${JSON.stringify(existingPermissions)}`);
user.is_view_only = existingPermissions.length === 1 &&
existingPermissions[0].permission_sid === viewOnlyPermission.permission_sid;
logger.debug(`User ${user_sid} is view-only user: ${user.is_view_only}`);
// eslint-disable-next-line no-unused-vars
const { hashed_password, ...rest } = user;
return res.status(200).json(rest);
@@ -372,14 +332,14 @@ router.put('/:user_sid', async(req, res) => {
is_active,
force_change,
account_sid,
service_provider_sid,
is_view_only
service_provider_sid
} = req.body;
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
if (!hasAdminAuth &&
!(hasAccountAuth && user[0] && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && user[0] && req.user.service_provider_sid === user[0].service_provider_sid) &&
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}
@@ -467,8 +427,6 @@ router.put('/:user_sid', async(req, res) => {
//TODO: send email with activation code
}
}
// update user permissions
await updateViewOnlyUserPermission(is_view_only, user_sid);
res.sendStatus(204);
} catch (err) {
sysError(logger, res, err);
@@ -485,8 +443,6 @@ router.post('/', async(req, res) => {
};
const allUsers = await User.retrieveAll();
delete payload.initial_password;
const is_view_only = payload.is_view_only;
delete payload.is_view_only;
try {
if (req.body.initial_password) {
@@ -508,7 +464,6 @@ router.post('/', async(req, res) => {
if (req.user.hasAdminAuth) {
logger.debug({payload}, 'POST /users');
const uuid = await User.make(payload);
await updateViewOnlyUserPermission(is_view_only, uuid);
res.status(201).json({user_sid: uuid});
}
else if (req.user.hasAccountAuth) {
@@ -517,7 +472,6 @@ router.post('/', async(req, res) => {
...payload,
account_sid: req.user.account_sid,
});
await updateViewOnlyUserPermission(is_view_only, uuid);
res.status(201).json({user_sid: uuid});
}
else if (req.user.hasServiceProviderAuth) {
@@ -526,7 +480,6 @@ router.post('/', async(req, res) => {
...payload,
service_provider_sid: req.user.service_provider_sid,
});
await updateViewOnlyUserPermission(is_view_only, uuid);
res.status(201).json({user_sid: uuid});
}
} catch (err) {
@@ -544,8 +497,6 @@ router.delete('/:user_sid', async(req, res) => {
const user = allUsers.filter((user) => user.user_sid === user_sid);
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
logger.debug(`Removing view-only permission for user ${user_sid}`);
await removeViewOnlyUserPermission(user_sid);
await User.remove(user_sid);
/* invalidate the jwt of the deleted user */

View File

@@ -1,6 +1,5 @@
const { v4: uuid, validate } = require('uuid');
const URL = require('url').URL;
const isValidHostname = require('is-valid-hostname');
const bent = require('bent');
const Account = require('../../models/account');
const {promisePool} = require('../../db');
const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils');
@@ -12,6 +11,8 @@ values (?, ?)`;
const replaceOldSubscriptionSql = `UPDATE account_subscriptions
SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ?
WHERE account_subscription_sid = ?`;
//const request = require('request');
//require('request-debug')(request);
const setupFreeTrial = async(logger, account_sid, isReturningUser) => {
const sid = uuid();
@@ -286,11 +287,7 @@ const hasAccountPermissions = async(req, res, next) => {
message: 'insufficient privileges'
});
} catch (error) {
// return 400 on errors
res.status(400).json({
status: 'fail',
message: error.message
});
throw error;
}
};
@@ -373,44 +370,35 @@ const checkLimits = async(req, res, next) => {
};
const getSubspaceJWT = async(id, secret) => {
const response = await fetch('https://id.subspace.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
const postJwt = bent('https://id.subspace.com', 'POST', 'json', 200);
const jwt = await postJwt('/oauth/token',
{
client_id: id,
client_secret: secret,
audience: 'https://api.subspace.com/',
grant_type: 'client_credentials',
})
});
if (!response.ok) {
throw new Error(`Failed to get JWT: ${response.status} ${response.statusText}`);
}
const jwt = await response.json();
}
);
return jwt.access_token;
};
const enableSubspace = async(opts) => {
const {subspace_client_id, subspace_client_secret, destination} = opts;
const accessToken = await getSubspaceJWT(subspace_client_id, subspace_client_secret);
const response = await fetch('https://api.subspace.com/v1/sipteleport', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
},
body: JSON.stringify({
const postTeleport = bent('https://api.subspace.com', 'POST', 'json', 200);
const teleport = await postTeleport('/v1/sipteleport',
{
name: 'Jambonz',
destination,
status: 'ENABLED'
})
});
if (!response.ok) {
throw new Error(`Failed to enable teleport: ${response.status} ${response.statusText}`);
}
const teleport = await response.json();
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
}
);
return teleport;
};
@@ -418,15 +406,13 @@ const disableSubspace = async(opts) => {
const {subspace_client_id, subspace_client_secret, subspace_sip_teleport_id} = opts;
const accessToken = await getSubspaceJWT(subspace_client_id, subspace_client_secret);
const relativeUrl = `/v1/sipteleport/${subspace_sip_teleport_id}`;
const response = await fetch(`https://api.subspace.com${relativeUrl}`, {
method: 'DELETE',
headers: {
const deleteTeleport = bent('https://api.subspace.com', 'DELETE', 'json', 200);
await deleteTeleport(relativeUrl, {},
{
Authorization: `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error(`Failed to delete teleport: ${response.status} ${response.statusText}`);
}
);
return;
};
const validatePasswordSettings = async(password) => {
@@ -454,44 +440,6 @@ 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;
}
}
const isInvalidUrl = async(s) => {
const protocols = ['https:', 'http:', 'ws:', 'wss:'];
try {
const url = new URL(s);
if (s.length != s.trim().length) {
return 'URL contains leading/trailing whitespace';
}
else if (!isValidHostname(url.hostname)) {
return `URL has invalid hostname ${url.hostname}`;
}
else if (!protocols.includes(url.protocol)) {
return `URL has missing or invalid protocol ${url.protocol}`;
}
else {
return false;
}
} catch (err) {
return 'URL is invalid';
}
};
module.exports = {
setupFreeTrial,
createTestCdrs,
@@ -512,7 +460,5 @@ module.exports = {
checkLimits,
enableSubspace,
disableSubspace,
validatePasswordSettings,
hasValue,
isInvalidUrl
validatePasswordSettings
};

View File

@@ -73,36 +73,16 @@ decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions);
/* list */
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const {account_sid: query_account_sid, name, page, page_size} = req.query || {};
const isPaginationRequest = page !== null && page !== undefined;
let service_provider_sid = null, account_sid = query_account_sid;
if (req.user.hasAccountAuth) {
account_sid = req.user.account_sid;
} else if (req.user.hasServiceProviderAuth) {
service_provider_sid = req.user.service_provider_sid;
}
try {
let total = 0;
if (isPaginationRequest) {
total = await VoipCarrier.countAll({service_provider_sid, account_sid, name});
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));
}
const carriers = await VoipCarrier.retrieveByCriteria({
service_provider_sid,
account_sid,
name,
page,
page_size,
});
const body = isPaginationRequest ? {
total,
page: Number(page),
page_size: Number(page_size),
data: carriers,
} : carriers;
res.status(200).json(body);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -61,7 +61,8 @@ router.post('/', express.raw({type: 'application/json'}), async(req, res) => {
}
/* process event */
if (evt?.type?.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
logger.info(`received webhook: ${evt.type}`);
if (evt.type.startsWith('invoice.')) handleInvoiceEvents(logger, evt);
else {
logger.debug(evt, 'unhandled stripe webook');
}

View File

@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Jambonz REST API
description: Jambonz REST API specification
title: jambonz REST API
description: jambonz REST API
contact:
email: daveh@drachtio.org
license:
@@ -44,8 +44,6 @@ tags:
description: Least Cost Routing Routes operations
- name: LcrCarrierSetEntries
description: Least Cost Routing Carrier Set Entries operation
- name: GoogleCustomVoices
description: Google Custom voices operation
paths:
/BetaInviteCodes:
post:
@@ -382,35 +380,11 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/login:
post:
tags:
- Authentication
summary: login and retrieve a JWT
operationId: login
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Login'
responses:
200:
description: user logged in
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulLogin'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/logout:
post:
tags:
- Authentication
summary: log out and deactivate the JWT
summary: log out and deactivate jwt
operationId: logoutUser
responses:
204:
@@ -608,9 +582,10 @@ paths:
content:
application/json:
schema:
type: array
type:
array
items:
$ref: '#/components/schemas/UserList'
$ref: '#/components/schemas/Users'
403:
description: unauthorized
500:
@@ -633,13 +608,27 @@ paths:
- Users
summary: retrieve user information
operationId: getUser
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
email:
type: string
is_active:
type: boolean
force_change:
type: boolean
scope:
type: string
permissions:
type: array
responses:
200:
204:
description: user information
content:
application/json:
schema:
$ref: '#/components/schemas/UserProfile'
403:
description: user information
content:
@@ -683,8 +672,6 @@ paths:
type: string
permissions:
type: array
items:
type: string
responses:
204:
description: user updated
@@ -723,8 +710,6 @@ paths:
type: string
permissions:
type: array
items:
type: string
old_password:
type: string
description: existing password, which is to be replaced
@@ -801,7 +786,7 @@ paths:
required: true
schema:
type: string
example: mycorp.sip.jambonz.cloud
example: mycorp.sip.jambonz.us
responses:
200:
description: indicates whether value is already in use
@@ -1011,7 +996,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/AccountTest/{ServiceProviderSid}:
/AccountTest/:ServiceProviderSid:
parameters:
- name: ServiceProviderSid
in: path
@@ -1108,9 +1093,6 @@ paths:
requires_register:
type: boolean
description: wehther this provider requires us to send a REGISTER to them in order to receive calls
register_use_tls:
type: boolean
description: wehther this provider requires us to send a REGISTER use TLS protocol
register_username:
type: string
description: sip username to authenticate with, if registration is required
@@ -1987,7 +1969,7 @@ paths:
tags:
- Service Providers
summary: add a VoiPCarrier to a service provider based on PredefinedCarrier template
operationId: createVoipCarrierFromTemplateBySP
operationId: createVoipCarrierFromTemplate
responses:
201:
description: voip carrier successfully created
@@ -2088,41 +2070,6 @@ paths:
description: credential successfully deleted
404:
description: credential not found
/ServiceProviders/{ServiceProviderSid}/SpeechCredentials/speech/supportedLanguagesAndVoices:
get:
tags:
- Service Providers
summary: get supported languages, voices and models
operationId: supportedLanguagesAndVoices
parameters:
- name: ServiceProviderSid
in: path
required: true
schema:
type: string
format: uuid
- name: vendor
in: query
required: true
schema:
type: string
- name: label
in: query
schema:
type: string
responses:
200:
description: get supported languages, voices and models
content:
application/json:
schema:
$ref: '#/components/schemas/SpeechLanguagesVoices'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/ServiceProviders/{ServiceProviderSid}/SpeechCredentials/{SpeechCredentialSid}/test:
get:
tags:
@@ -2942,7 +2889,7 @@ paths:
tags:
- Accounts
summary: get a specific speech credential
operationId: getSpeechCredentialByAccount
operationId: getSpeechCredential
responses:
200:
description: retrieve speech credentials for a specified account
@@ -2956,7 +2903,7 @@ paths:
tags:
- Accounts
summary: update a speech credential
operationId: updateSpeechCredentialByAccount
operationId: updateSpeechCredential
requestBody:
content:
application/json:
@@ -2977,53 +2924,18 @@ paths:
tags:
- Accounts
summary: delete a speech credential
operationId: deleteSpeechCredentialByAccount
operationId: deleteSpeechCredential
responses:
204:
description: credential successfully deleted
404:
description: credential not found
/Accounts/{AccountSid}/SpeechCredentials/speech/supportedLanguagesAndVoices:
get:
tags:
- Accounts
summary: get supported languages, voices and models
operationId: supportedLanguagesAndVoicesByAccount
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
- name: vendor
in: query
required: true
schema:
type: string
- name: label
in: query
schema:
type: string
responses:
200:
description: get supported languages, voices and models
content:
application/json:
schema:
$ref: '#/components/schemas/SpeechLanguagesVoices'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/SpeechCredentials/{SpeechCredentialSid}/test:
get:
tags:
- Accounts
summary: test a speech credential
operationId: testSpeechCredentialByAccount
operationId: testSpeechCredential
parameters:
- name: AccountSid
in: path
@@ -3134,12 +3046,6 @@ paths:
enum:
- inbound
- outbound
- in: query
name: filter
required: false
schema:
type: string
description: Filter value can be caller ID, callee ID or call Sid
get:
tags:
- Accounts
@@ -3269,7 +3175,7 @@ paths:
tags:
- Service Providers
summary: retrieve pcap for a call
operationId: getRecentCallTraceBySP
operationId: getRecentCallTrace
responses:
200:
description: retrieve sip trace data
@@ -3339,23 +3245,11 @@ paths:
enum:
- inbound
- outbound
- in: query
name: from
required: false
schema:
type: string
description: calling number to retrieve
- in: query
name: to
required: false
schema:
type: string
description: called number to retrieve
get:
tags:
- Service Providers
summary: retrieve recent calls for an account
operationId: listRecentCallsBySP
operationId: listRecentCalls
responses:
200:
description: retrieve recent call records for a specified account
@@ -3456,7 +3350,7 @@ paths:
tags:
- Service Providers
summary: retrieve sip trace detail for a call
operationId: getRecentCallTraceByCallId
operationId: getRecentCallTrace
responses:
200:
description: retrieve sip trace data
@@ -3483,7 +3377,7 @@ paths:
tags:
- Accounts
summary: retrieve pcap for a call
operationId: getRecentCallTraceByAccount
operationId: getRecentCallTrace
responses:
200:
description: retrieve sip trace data
@@ -3669,7 +3563,7 @@ paths:
tags:
- Accounts
summary: retrieve alerts for an account
operationId: listAlertsByAccount
operationId: listAlerts
responses:
200:
description: retrieve alerts for a specified account
@@ -3882,38 +3776,10 @@ paths:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Conferences:
get:
tags:
- Conferences
summary: list conferences
operationId: listConferences
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
responses:
200:
description: list of conferences for a specified account
content:
application/json:
schema:
type: array
items:
type: string
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Calls:
post:
tags:
- Accounts
- Accounts
summary: create a call
operationId: createCall
parameters:
@@ -3972,10 +3838,6 @@ paths:
type: object
description: The customer SIP headers to associate with the call
example: {"X-Custom-Header": "Hello"}
sipRequestWithinDialogHook:
type: string
description: The sip indialog hook to receive session messages
example: '/customHook'
responses:
201:
description: call successfully created
@@ -4180,22 +4042,6 @@ paths:
type: string
siprecServerURL:
type: string
conferenceParticipantAction:
type: object
properties:
action:
type: string
enum:
- tag
- untag
- coach
- uncoach
- mute
- unmute
- hold
- unhold
tag:
type: string
responses:
200:
description: Accepted
@@ -4300,146 +4146,6 @@ paths:
type: string
length:
type: string
/Accounts/{AccountSid}/RegisteredSipUsers:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
get:
tags:
- Accounts
summary: retrieve online sip users for an account
operationId: listRegisteredSipUsers
responses:
200:
description: retrieve online sip users for an account
content:
application/json:
schema:
type: array
items:
type: string
post:
tags:
- Accounts
summary: retrieve online sip users for an account by list of sip username
operationId: listRegisteredSipUsersByUsername
requestBody:
content:
application/json:
schema:
type: array
items:
type: string
responses:
200:
description: retrieve online sip users for an account
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/RegisteredClient'
/Accounts/{AccountSid}/RegisteredSipUsers/{Client}:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
- name: Client
in: path
required: true
style: simple
explode: false
schema:
type: string
get:
tags:
- Accounts
summary: retrieve registered client registration
operationId: getRegisteredClient
responses:
200:
description: registered client found
content:
application/json:
schema:
$ref: '#/components/schemas/RegisteredClient'
/Accounts/{AccountSid}/TtsCache/Synthesize:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
post:
tags:
- Accounts
summary: get TTS from provider
operationId: Synthesize
requestBody:
content:
application/json:
schema:
type: object
properties:
speech_credential_sid:
type: string
description: Speech credential Sid
example: 553b4b6b-8918-4394-a46d-1e3c5a3c717b
text:
type: string
description: the text to convert to audio
example: Hello How are you
language:
type: string
description: language is used in text
example: en-US
voice:
type: string
description: voice ID
example: en-US-Standard-C
encodingMp3:
type: boolean
description: convert audio to mp3.
example: true
required:
- speech_credential_sid
- text
- language
- voice
responses:
200:
description: Audio is created
content:
audio/mpeg:
schema:
type: string
format: binary
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Lcrs:
post:
tags:
@@ -4587,69 +4293,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Lcrs/{LcrSid}/Routes:
parameters:
- name: LcrSid
in: path
required: true
style: simple
explode: false
schema:
type: string
post:
tags:
- Lcrs
summary: Create least cost routing routes and carrier set entries
operationId: createLeastCostRoutingRoutesAndCarrierEntries
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LcrRoutes'
responses:
204:
description: least cost routing routes and carrier set entries created
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
404:
description: least cost routing not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- Lcrs
summary: update least cost routing routes and carrier set entries
operationId: updateLeastCostRoutingRoutesAndCarrierEntries
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LcrRoutes'
responses:
204:
description: least cost routing ruoutes and carrier entries updated
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
404:
description: least cost routing not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/LcrRoutes:
post:
tags:
@@ -4942,173 +4585,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/GoogleCustomVoices:
post:
tags:
- GoogleCustomVoices
summary: create a Google custom voice
operationId: createGoogleCustomVoice
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/GoogleCustomVoice'
required:
- speech_credential_sid
- name
- reported_usage
- model
responses:
201:
description: Least Cost Routing Carrier Set Entry successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- GoogleCustomVoices
parameters:
- in: query
name: service_provider_sid
required: false
schema:
type: string
description: return only the google voice custom operated belong to this service provider
- in: query
name: account_sid
required: false
schema:
type: string
description: return only the google voice custom operated belong to this account_sid
- in: query
name: speech_credential_sid
required: false
schema:
type: string
description: return only the google voice custom operated belong to this speech credential
summary: list google custom voices
operationId: listGoogleCustomVoices
responses:
200:
description: list oflist google custom voices
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/GoogleCustomVoice'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/GoogleCustomVoices/{GoogleCustomVoiceSid}:
parameters:
- name: GoogleCustomVoiceSid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
tags:
- GoogleCustomVoices
summary: delete a google custom voice
operationId: deleteGoogleCustomVoice
responses:
204:
description: google custom voice successfully deleted
404:
description: google custom voice not found
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
msg: a service provider with active accounts can not be deleted
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- GoogleCustomVoices
summary: retrieve google custom voice
operationId: getGoogleCustomVoice
responses:
200:
description: google custom voice found
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleCustomVoice'
404:
description: google custom voice not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- GoogleCustomVoices
summary: update google custom voice
operationId: updateGoogleCustomVoice
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleCustomVoice'
responses:
204:
description: google custom voice updated
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleCustomVoice'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
404:
description: least cost routing carrier set entry not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
components:
securitySchemes:
bearerAuth:
@@ -5116,32 +4592,17 @@ components:
scheme: bearer
bearerFormat: token
schemas:
SuccessfulLogin:
type: object
required:
- username
- password
properties:
token:
type: string
user_sid:
type: string
scope:
type: string
force_change:
type: boolean
Login:
type: object
properties:
username:
user_sid:
type: string
password:
type: string
api_token:
type: string
change_password:
type: boolean
required:
- username
- password
- user_sid
SuccessfulApiKeyAdd:
type: object
required:
@@ -6082,137 +5543,6 @@ components:
- lcr_route_sid
- voip_carrier_sid
- priority
LcrRouteAndCarrierEntries:
allOf:
- $ref: '#/components/schemas/LcrRoute'
- type: object
properties:
lcr_carrier_set_entries:
type: array
items:
$ref: '#/components/schemas/LcrCarrierSetEntry'
LcrRoutes:
type: array
items:
$ref: '#/components/schemas/LcrRouteAndCarrierEntries'
GoogleCustomVoice:
type: object
properties:
speech_credential_sid:
type: string
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
name:
type: string
example: Sally
reported_usage:
type: string
example: REALTIME
model:
type: string
example: projects/12412312/locations/global/models/2134124123-2dbf-43be-9593-12314123
required:
- speech_credential_sid
- name
- reported_usage
- model
RegisteredClient:
type: object
properties:
name:
type: string
example: xhoaluu
contact:
type: string
example: sip:0dluqjt6@od41sl9jfc9m.invalid;transport=ws
expiryTime:
type: number
example: 1698981449173
protocol:
type: string
example: wss
allow_direct_app_calling:
type: number
example: 1
allow_direct_queue_calling:
type: number
example: 1
allow_direct_user_calling:
type: number
example: 1
registered_status:
type: string
enum:
- active
- inactive
required:
- speech_credential_sid
- name
- reported_usage
- model
TtsModel:
type: object
properties:
name:
type: string
example: Turbo v2
value:
type: string
example: eleven_turbo_v2
LanguageVoice:
type: object
properties:
name:
type: string
example: Standard-A (Female)
value:
type: string
example: ar-XA-Standard-A
LanguageVoices:
type: object
properties:
name:
type: string
example: English (US)
value:
type: string
example: en-US
voices:
type: array
items:
$ref: '#/components/schemas/LanguageVoice'
SpeechLanguagesVoices:
type: object
properties:
tts:
type: array
items:
$ref: '#/components/schemas/LanguageVoices'
stt:
type: array
items:
$ref: '#/components/schemas/LanguageVoice'
ttsModel:
type: array
items:
$ref: '#/components/schemas/TtsModel'
UserList:
type: object
properties:
name:
type: string
email:
type: string
is_active:
type: boolean
force_change:
type: boolean
scope:
type: string
permissions:
type: array
items:
type: string
security:
- bearerAuth: []

View File

@@ -1,50 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"patternProperties": {
"^[a-zA-Z0-9_]+$": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"string",
"number",
"boolean"
]
},
"required": {
"type": "boolean"
},
"default": {
"oneOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "boolean"
}
]
},
"enum": {
"type": "array"
},
"obscure": {
"type": "boolean"
}
},
"required": [
"type",
"description"
]
}
}
}

View File

@@ -1,78 +0,0 @@
const Ajv = require('ajv');
const assert = require('assert');
const ajv = new Ajv();
const schemaSchema = require('./appenv_schemaSchema.json');
const validateAppEnvSchema = (schema) => {
const validate = ajv.compile(schemaSchema);
return validate(schema);
};
//Currently this request is not signed with the webhook secret as it is outside an account
const fetchAppEnvSchema = async(logger, url) => {
// Translate WebSocket URLs to HTTP equivalents (case-insensitive)
let fetchUrl = url;
if (url.toLowerCase().startsWith('ws://')) {
fetchUrl = 'http://' + url.substring(5);
} else if (url.toLowerCase().startsWith('wss://')) {
fetchUrl = 'https://' + url.substring(6);
}
try {
const response = await fetch(fetchUrl, {
method: 'OPTIONS',
headers: {
Accept: 'application/json'
}
});
if (!response.ok) {
logger.info(`Failure to fetch app env schema ${response.status} ${response.statusText}`);
return false;
}
const schema = await response.json();
return schema;
}
catch (e) {
logger.info(`Failure to fetch app env schema ${e}`);
return false;
}
};
const validateAppEnvData = async(schema, data) => {
const schemaKeys = Object.keys(schema);
const dataKeys = Object.keys(data);
let errorMsg = false;
// Check for required keys
schemaKeys.forEach((k) => {
if (schema[k].required) {
if (!dataKeys.includes(k)) {
errorMsg = `Missing required value env_vars.${k}`;
console.log(errorMsg);
}
}
});
//Validate the values
dataKeys.forEach((k) => {
if (schemaKeys.includes(k)) {
try {
// Check value is correct type
assert(typeof data[k] == schema[k].type);
// if enum check value is valid
if (schema[k].enum) {
assert(schema[k].enum.includes(data[k]));
}
} catch (error) {
errorMsg = `Invalid value/type for env_vars.${k}`;
}
}
});
return errorMsg;
};
module.exports = {
validateAppEnvSchema,
fetchAppEnvSchema,
validateAppEnvData
};

View File

@@ -1,5 +1,6 @@
if (!process.env.JAMBONES_HOSTING) return;
const bent = require('bent');
const crypto = require('crypto');
const assert = require('assert');
const domains = new Map();
@@ -25,20 +26,17 @@ const createAuthHeaders = () => {
const getDnsDomainId = async(logger, name) => {
checkAsserts();
const headers = createAuthHeaders();
const response = await fetch(`${process.env.DME_BASE_URL}/dns/managed`, {
method: 'GET',
headers
});
if (!response.ok) {
logger.error({response}, 'Error retrieving domains');
return;
}
const result = await response.json();
debug(result, 'getDnsDomainId: all domains');
if (Array.isArray(result.data)) {
const domain = result.data.find((o) => o.name === name);
if (domain) return domain.id;
debug(`getDnsDomainId: failed to find domain ${name}`);
const get = bent(process.env.DME_BASE_URL, 'GET', 'json', headers);
try {
const result = await get('/dns/managed');
debug(result, 'getDnsDomainId: all domains');
if (Array.isArray(result.data)) {
const domain = result.data.find((o) => o.name === name);
if (domain) return domain.id;
debug(`getDnsDomainId: failed to find domain ${name}`);
}
} catch (err) {
logger.error({err}, 'Error retrieving domains');
}
};
@@ -82,20 +80,16 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
];
const headers = createAuthHeaders();
const records = [...a_records, ...srv_records];
const response = await fetch(`${process.env.DME_BASE_URL}/dns/managed/${domainId}/records/createMulti`, {
method: 'POST',
headers,
body: JSON.stringify(records)
});
if (!response.ok) {
logger.error({response}, 'Error creating records');
return;
}
const result = await response.json();
logger.debug({result}, 'createDnsRecords: created records');
if (201 === response.status) {
return result;
const post = bent(process.env.DME_BASE_URL, 'POST', 201, 400, headers);
logger.debug({records}, 'Attemting to create dns records');
const res = await post(`/dns/managed/${domainId}/records/createMulti`,
[...a_records, ...srv_records]);
if (201 === res.statusCode) {
const str = await res.text();
return JSON.parse(str);
}
logger.error({res}, 'Error creating records');
} catch (err) {
logger.error({err}, 'Error retrieving domains');
}
@@ -104,6 +98,7 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
const deleteDnsRecords = async(logger, domain, recIds) => {
checkAsserts();
const headers = createAuthHeaders();
const del = bent(process.env.DME_BASE_URL, 'DELETE', 200, headers);
try {
if (!domains.has(domain)) {
const domainId = await getDnsDomainId(logger, domain);
@@ -112,10 +107,7 @@ const deleteDnsRecords = async(logger, domain, recIds) => {
}
const domainId = domains.get(domain);
const url = `/dns/managed/${domainId}/records?${recIds.map((r) => `ids=${r}`).join('&')}`;
await fetch(`${process.env.DME_BASE_URL}${url}`, {
method: 'DELETE',
headers
});
await del(url);
return true;
} catch (err) {
console.error(err);

View File

@@ -1,6 +1,7 @@
const formData = require('form-data');
const Mailgun = require('mailgun.js');
const mailgun = new Mailgun(formData);
const bent = require('bent');
const validateEmail = (email) => {
// eslint-disable-next-line max-len
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
@@ -18,9 +19,8 @@ const emailSimpleText = async(logger, to, subject, text) => {
};
const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
const response = await fetch(process.env.CUSTOM_EMAIL_VENDOR_URL, {
method: 'POST',
headers: {
try {
const post = bent('POST', {
'Content-Type': 'application/json',
...((process.env.CUSTOM_EMAIL_VENDOR_USERNAME && process.env.CUSTOM_EMAIL_VENDOR_PASSWORD) &&
({
@@ -28,33 +28,31 @@ const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
`${process.env.CUSTOM_EMAIL_VENDOR_USERNAME}:${process.env.CUSTOM_EMAIL_VENDOR_PASSWORD}`
).toString('base64')}`
}))
},
body: JSON.stringify({
});
const res = await post(process.env.CUSTOM_EMAIL_VENDOR_URL, {
from,
to,
subject,
text
})
});
if (!response.ok) {
logger.error({response}, 'Error sending email to custom vendor');
return;
});
logger.debug({
res
}, 'sent email to custom vendor.');
} catch (err) {
logger.info({
err
}, 'Error sending email From Custom email vendor');
}
const res = await response.json();
logger.debug({
res
}, 'sent email to custom vendor.');
};
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
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})
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!');
try {
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {

View File

@@ -17,106 +17,13 @@ const encrypt = (text) => {
};
const decrypt = (data) => {
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 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();
};
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 = /^([A-Za-z0-9:]{4,6}X+$)/;
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,
obscureKey,
isObscureKey,
obscureBucketCredentialsSensitiveData,
decrypt
};

View File

@@ -27,17 +27,11 @@ class DbErrorForbidden extends DbError {
super(msg);
}
}
class UserPermissionError extends Error {
constructor(msg) {
super(msg);
}
}
module.exports = {
BadRequestError,
DbError,
DbErrorBadRequest,
DbErrorUnprocessableRequest,
DbErrorForbidden,
UserPermissionError
DbErrorForbidden
};

View File

@@ -2,7 +2,7 @@
"trial": [
{
"category": "voice_call_session",
"quantity": 5
"quantity": 20
},
{
"category": "device",

View File

@@ -1,11 +1,15 @@
const debug = require('debug')('jambonz:api-server');
const bent = require('bent');
const basicAuth = (apiKey) => {
const header = `Bearer ${apiKey}`;
return {Authorization: header};
};
const { Readable } = require('stream');
const postJSON = bent(process.env.HOMER_BASE_URL || 'http://127.0.0.1', 'POST', 'json', 200, 201);
const postPcap = bent(process.env.HOMER_BASE_URL || 'http://127.0.0.1', 'POST', 200, {
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
});
const SEVEN_DAYS_IN_MS = (1000 * 3600 * 24 * 7);
const HOMER_BASE_URL = process.env.HOMER_BASE_URL || 'http://127.0.0.1';
const getHomerApiKey = async(logger) => {
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
@@ -13,21 +17,11 @@ const getHomerApiKey = async(logger) => {
}
try {
const response = await fetch(`${HOMER_BASE_URL}/api/v3/auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: process.env.HOMER_USERNAME,
password: process.env.HOMER_PASSWORD
})
const obj = await postJSON('/api/v3/auth', {
username: process.env.HOMER_USERNAME,
password: process.env.HOMER_PASSWORD
});
if (!response.ok) {
logger.error({response}, 'Error retrieving apikey');
return;
}
const obj = await response.json();
debug(obj);
logger.debug({obj}, `getHomerApiKey for user ${process.env.HOMER_USERNAME}`);
return obj.token;
} catch (err) {
@@ -42,91 +36,63 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
}
try {
const now = Date.now();
const response = await fetch(`${HOMER_BASE_URL}/api/v3/call/transaction`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...basicAuth(apiKey)
},
body: JSON.stringify({
param: {
transaction: {
call: true,
registration: true,
rest: false
},
orlogic: true,
search: {
'1_call': {
callid: [callId]
},
'1_registration': {
callid: [callId]
}
},
const obj = await postJSON('/api/v3/call/transaction', {
param: {
transaction: {
call: true,
registration: true,
rest: false
},
timestamp: {
from: now - SEVEN_DAYS_IN_MS,
to: now
}
})
});
if (!response.ok) {
logger.error({response}, 'Error retrieving messages');
return;
}
const obj = await response.json();
orlogic: true,
search: {
'1_call': {
callid: [callId]
},
'1_registration': {
callid: [callId]
}
},
},
timestamp: {
from: now - SEVEN_DAYS_IN_MS,
to: now
}
}, basicAuth(apiKey));
return obj;
} catch (err) {
logger.info({err}, `getHomerSipTrace: Error retrieving messages for callid ${callId}`);
}
};
const getHomerPcap = async(logger, apiKey, callIds, method) => {
const getHomerPcap = async(logger, apiKey, callIds) => {
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
logger.debug('getHomerPcap: Homer integration not installed');
}
try {
const now = Date.now();
const response = await fetch(`${HOMER_BASE_URL}/api/v3/export/call/messages/pcap`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...basicAuth(apiKey)
},
body: JSON.stringify({
param: {
transaction: {
call: method === 'invite',
registration: method === 'register',
rest: false
},
orlogic: true,
search: {
...(method === 'invite' && {
'1_call': {
callid: callIds
}
})
,
...(method === 'register' && {
'1_registration': {
callid: callIds
}
})
},
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
param: {
transaction: {
call: true,
registration: true,
rest: false
},
timestamp: {
from: now - SEVEN_DAYS_IN_MS,
to: now
}
})
});
if (!response.ok) {
logger.error({response}, 'Error retrieving messages');
return;
}
return Readable.fromWeb(response.body);
orlogic: true,
search: {
'1_call': {
callid: callIds
},
'1_registration': {
callid: callIds
}
},
},
timestamp: {
from: now - SEVEN_DAYS_IN_MS,
to: now
}
}, basicAuth(apiKey));
return stream;
} catch (err) {
logger.info({err}, `getHomerPcap: Error retrieving messages for callid ${callIds}`);
}

View File

@@ -1,4 +1,5 @@
const JAEGER_BASE_URL = process.env.JAEGER_BASE_URL || 'http://127.0.0.1';
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) {
@@ -6,15 +7,9 @@ const getJaegerTrace = async(logger, traceId) => {
return null;
}
try {
const response = await fetch(`${JAEGER_BASE_URL}/api/v3/traces/${traceId}`);
if (!response.ok) {
logger.error({response}, 'Error retrieving spans');
return;
}
return await response.json();
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}`);
logger.error({err}, `getJaegerTrace: Error retrieving spans for traceId ${traceId}`);
}
};

View File

@@ -1 +0,0 @@
Hello From Jambonz. This file was created because Record all call bucket credential test.

View File

@@ -1,4 +1,7 @@
const assert = require('assert');
const bent = require('bent');
const postJSON = bent('POST', 'json', 200);
const getJSON = bent('GET', 'json', 200);
const {emailSimpleText} = require('./email-utils');
const {DbErrorForbidden} = require('../utils/errors');
@@ -7,26 +10,13 @@ const doGithubAuth = async(logger, payload) => {
try {
/* exchange the code for an access token */
const response = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify({
client_id: payload.oauth2_client_id,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code: payload.oauth2_code,
state: payload.oauth2_state,
redirect_uri: payload.oauth2_redirect_uri
})
const obj = await postJSON('https://github.com/login/oauth/access_token', {
client_id: payload.oauth2_client_id,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code: payload.oauth2_code,
state: payload.oauth2_state,
redirect_uri: payload.oauth2_redirect_uri
});
if (!response.ok) {
logger.error({response}, 'Error retrieving access_token from github');
throw new DbErrorForbidden(await response.text());
}
const obj = await response.json();
if (!obj.access_token) {
logger.error({obj}, 'Error retrieving access_token from github');
if (obj.error === 'bad_verification_code') throw new Error('bad verification code');
@@ -35,31 +25,17 @@ const doGithubAuth = async(logger, payload) => {
logger.debug({obj}, 'got response from github for access_token');
/* use the access token to get basic public info as well as primary email */
const userResponse = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${obj.access_token}`,
Accept: 'application/json',
'User-Agent': 'jambonz 1.0'
}
const userDetails = await getJSON('https://api.github.com/user', null, {
Authorization: `Bearer ${obj.access_token}`,
Accept: 'application/json',
'User-Agent': 'jambonz 1.0'
});
if (!userResponse.ok) {
logger.error({userResponse}, 'Error retrieving user details from github');
throw new DbErrorForbidden(await userResponse.text());
}
const userDetails = await userResponse.json();
const emailsResponse = await fetch('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${obj.access_token}`,
Accept: 'application/json',
'User-Agent': 'jambonz 1.0'
}
const emails = await getJSON('https://api.github.com/user/emails', null, {
Authorization: `Bearer ${obj.access_token}`,
Accept: 'application/json',
'User-Agent': 'jambonz 1.0'
});
if (!emailsResponse.ok) {
logger.error({emailsResponse}, 'Error retrieving emails from github');
throw new DbErrorForbidden(await emailsResponse.text());
}
const emails = await emailsResponse.json();
const primary = emails.find((e) => e.primary);
if (primary) Object.assign(userDetails, {
email: primary.email,
@@ -79,26 +55,14 @@ const doGoogleAuth = async(logger, payload) => {
try {
/* exchange the code for an access token */
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify({
client_id: payload.oauth2_client_id,
client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET,
code: payload.oauth2_code,
state: payload.oauth2_state,
redirect_uri: payload.oauth2_redirect_uri,
grant_type: 'authorization_code'
})
const obj = await postJSON('https://oauth2.googleapis.com/token', {
client_id: payload.oauth2_client_id,
client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET,
code: payload.oauth2_code,
state: payload.oauth2_state,
redirect_uri: payload.oauth2_redirect_uri,
grant_type: 'authorization_code'
});
if (!response.ok) {
logger.error({response}, 'Error retrieving access_token from google');
throw new DbErrorForbidden(await response.text());
}
const obj = await response.json();
if (!obj.access_token) {
logger.error({obj}, 'Error retrieving access_token from github');
if (obj.error === 'bad_verification_code') throw new Error('bad verification code');
@@ -107,18 +71,12 @@ const doGoogleAuth = async(logger, payload) => {
logger.debug({obj}, 'got response from google for access_token');
/* use the access token to get basic public info as well as primary email */
const userDetailsResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
Authorization: `Bearer ${obj.access_token}`,
Accept: 'application/json',
'User-Agent': 'jambonz 1.0'
}
const userDetails = await getJSON('https://www.googleapis.com/oauth2/v2/userinfo', null, {
Authorization: `Bearer ${obj.access_token}`,
Accept: 'application/json',
'User-Agent': 'jambonz 1.0'
});
if (!userDetailsResponse.ok) {
logger.error({userDetailsResponse}, 'Error retrieving user details from google');
throw new DbErrorForbidden(await userDetailsResponse.text());
}
const userDetails = await userDetailsResponse.json();
logger.info({userDetails}, 'retrieved user details from google');
return userDetails;
} catch (err) {

View File

@@ -1,22 +0,0 @@
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' },
];

View File

@@ -1,57 +0,0 @@
module.exports = [
{ name: 'Arabic, Gulf', value: 'ar-AE' },
{ name: 'Arabic, Modern Standard', value: 'ar-SA' },
{ name: 'Afrikaans', value: 'af-ZA' },
{ name: 'Basque', value: 'eu-ES' },
{ name: 'Catalan', value: 'ca-ES' },
{ name: 'Chinese, Simplified', value: 'zh-CN' },
{ name: 'Chinese, Traditional', value: 'zh-TW' },
{ name: 'Chinese (Cantonese), Hong-Kong', value: 'zh-HK' },
{ name: 'Croatian', value: 'hr-HR' },
{ name: 'Czech', value: 'cs-CZ' },
{ name: 'Danish', value: 'da-DK' },
{ name: 'Dutch', value: 'nl-NL' },
{ name: 'Australian English', value: 'en-AU' },
{ name: 'British English', value: 'en-GB' },
{ name: 'US English', value: 'en-US' },
{ name: 'Indian English', value: 'en-IN' },
{ name: 'Irish English', value: 'en-IE' },
{ name: 'New Zealand English', value: 'en-NZ' },
{ name: 'Scottish English', value: 'en-AB' },
{ name: 'South African English', value: 'en-ZA' },
{ name: 'Welsh English', value: 'en-WL' },
{ name: 'Farsi', value: 'fa-IR' },
{ name: 'Finnish', value: 'fi-FI' },
{ name: 'French', value: 'fr-FR' },
{ name: 'Canadian French', value: 'fr-CA' },
{ name: 'Galician', value: 'gl-ES' },
{ name: 'German', value: 'de-DE' },
{ name: 'Swiss German', value: 'de-CH' },
{ name: 'Greek', value: 'el-GR' },
{ name: 'Hindi', value: 'hi-IN' },
{ name: 'Hebrew', value: 'he-IL' },
{ name: 'Italian', value: 'it-IT' },
{ name: 'Indian Hindi', value: 'hi-IN' },
{ name: 'Indonesian', value: 'id-ID' },
{ name: 'Japanese', value: 'ja-JP' },
{ name: 'Korean', value: 'ko-KR' },
{ name: 'Latvian', value: 'lv-LV' },
{ name: 'Malay', value: 'ms-MY' },
{ name: 'Norwegian Bokmål', value: 'no-NO' },
{ name: 'Polish', value: 'pl-PL' },
{ name: 'Portuguese', value: 'pt-PT' },
{ name: 'Brazilian Portuguese', value: 'pt-BR' },
{ name: 'Romanian', value: 'ro-RO' },
{ name: 'Russian', value: 'ru-RU' },
{ name: 'Serbian', value: 'sr-RS' },
{ name: 'Slovak', value: 'sk-SK' },
{ name: 'Somali', value: 'so-SO' },
{ name: 'Spanish', value: 'es-ES' },
{ name: 'US Spanish', value: 'es-US' },
{ name: 'Swedish', value: 'sv-SE' },
{ name: 'Tagalog/Filipino', value: 'tl-PH' },
{ name: 'Thai', value: 'th-TH' },
{ name: 'Ukrainian', value: 'uk-UA' },
{ name: 'Vietnamese', value: 'vi-VN' },
{ name: 'Zulu', value: 'zu-ZA' }
];

View File

@@ -1,26 +0,0 @@
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',
},
];

View File

@@ -1,56 +0,0 @@
module.exports = [
{ name: 'Multilingual', value: 'multi' },
{ name: 'Bulgarian', value: 'bg' },
{ name: 'Catalan', value: 'ca' },
{ name: 'Chinese (Mandarin, Simplified)', value: 'zh' },
{ name: 'Chinese (Mandarin, Simplified - China)', value: 'zh-CN' },
{ name: 'Chinese (Mandarin, Simplified - Hans)', value: 'zh-Hans' },
{ name: 'Chinese (Mandarin, Traditional)', value: 'zh-TW' },
{ name: 'Chinese (Mandarin, Traditional - Hant)', value: 'zh-Hant' },
{ name: 'Chinese (Cantonese, Traditional - Hong Kong)', value: 'zh-HK' },
{ name: 'Czech', value: 'cs' },
{ name: 'Danish', value: 'da' },
{ name: 'Danish (Denmark)', value: 'da-DK' },
{ name: 'Dutch', value: 'nl' },
{ name: 'English', value: 'en' },
{ name: 'English (United States)', value: 'en-US' },
{ name: 'English (Australia)', value: 'en-AU' },
{ name: 'English (United Kingdom)', value: 'en-GB' },
{ name: 'English (New Zealand)', value: 'en-NZ' },
{ name: 'English (India)', value: 'en-IN' },
{ name: 'Estonian', value: 'et' },
{ name: 'Finnish', value: 'fi' },
{ name: 'Flemish', value: 'nl-BE' },
{ name: 'French', value: 'fr' },
{ name: 'French (Canada)', value: 'fr-CA' },
{ name: 'German', value: 'de' },
{ name: 'German (Switzerland)', value: 'de-CH' },
{ name: 'Greek', value: 'el' },
{ name: 'Hindi', value: 'hi' },
{ name: 'Hungarian', value: 'hu' },
{ name: 'Indonesian', value: 'id' },
{ name: 'Italian', value: 'it' },
{ name: 'Japanese', value: 'ja' },
{ name: 'Korean', value: 'ko' },
{ name: 'Korean (South Korea)', value: 'ko-KR' },
{ name: 'Latvian', value: 'lv' },
{ name: 'Lithuanian', value: 'lt' },
{ name: 'Malay', value: 'ms' },
{ name: 'Norwegian', value: 'no' },
{ name: 'Polish', value: 'pl' },
{ name: 'Portuguese', value: 'pt' },
{ name: 'Portuguese (Brazil)', value: 'pt-BR' },
{ name: 'Portuguese (Portugal)', value: 'pt-PT' },
{ name: 'Romanian', value: 'ro' },
{ name: 'Russian', value: 'ru' },
{ name: 'Slovak', value: 'sk' },
{ name: 'Spanish', value: 'es' },
{ name: 'Spanish (Latin America)', value: 'es-419' },
{ name: 'Swedish', value: 'sv' },
{ name: 'Swedish (Sweden)', value: 'sv-SE' },
{ name: 'Thai', value: 'th' },
{ name: 'Thai (Thailand)', value: 'th-TH' },
{ name: 'Turkish', value: 'tr' },
{ name: 'Ukrainian', value: 'uk' },
{ name: 'Vietnamese', value: 'vi' }
];

View File

@@ -1,130 +0,0 @@
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' },
];

View File

@@ -1,82 +0,0 @@
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',
},
];

View File

@@ -1,490 +0,0 @@
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',
},
];

View File

@@ -1,4 +0,0 @@
module.exports = [
{ name: 'Ink-whisper', value: 'ink-whisper' },
];

View File

@@ -1,52 +0,0 @@
module.exports = [
// Nova-3
{ name: 'Nova-3', value: 'nova-3' },
{ name: 'Nova-3 General', value: 'nova-3-general' },
{ name: 'Nova-3 Medical', value: 'nova-3-medical' },
// Nova-2
{ name: 'Nova-2', value: 'nova-2' },
{ name: 'Nova-2 General', value: 'nova-2-general' },
{ name: 'Nova-2 Meeting', value: 'nova-2-meeting' },
{ name: 'Nova-2 Phonecall', value: 'nova-2-phonecall' },
{ name: 'Nova-2 Finance', value: 'nova-2-finance' },
{ name: 'Nova-2 Conversational AI', value: 'nova-2-conversationalai' },
{ name: 'Nova-2 Voicemail', value: 'nova-2-voicemail' },
{ name: 'Nova-2 Video', value: 'nova-2-video' },
{ name: 'Nova-2 Medical', value: 'nova-2-medical' },
{ name: 'Nova-2 Drivethru', value: 'nova-2-drivethru' },
{ name: 'Nova-2 Automotive', value: 'nova-2-automotive' },
{ name: 'Nova-2 ATC', value: 'nova-2-atc' },
// Nova (legacy)
{ name: 'Nova', value: 'nova' },
{ name: 'Nova General', value: 'nova-general' },
{ name: 'Nova Phonecall', value: 'nova-phonecall' },
{ name: 'Nova Medical', value: 'nova-medical' },
// Enhanced (legacy)
{ name: 'Enhanced', value: 'enhanced' },
{ name: 'Enhanced General', value: 'enhanced-general' },
{ name: 'Enhanced Meeting', value: 'enhanced-meeting' },
{ name: 'Enhanced Phonecall', value: 'enhanced-phonecall' },
{ name: 'Enhanced Finance', value: 'enhanced-finance' },
// Base (legacy)
{ name: 'Base', value: 'base' },
{ name: 'Base General', value: 'base-general' },
{ name: 'Base Meeting', value: 'base-meeting' },
{ name: 'Base Phonecall', value: 'base-phonecall' },
{ name: 'Base Finance', value: 'base-finance' },
{ name: 'Base Conversational AI', value: 'base-conversationalai' },
{ name: 'Base Voicemail', value: 'base-voicemail' },
{ name: 'Base Video', value: 'base-video' },
// Whisper
{ name: 'Whisper Tiny', value: 'whisper-tiny' },
{ name: 'Whisper Base', value: 'whisper-base' },
{ name: 'Whisper Small', value: 'whisper-small' },
{ name: 'Whisper Medium', value: 'whisper-medium' },
{ name: 'Whisper Large', value: 'whisper-large' },
{ name: 'Whisper', value: 'whisper' },
];

View File

@@ -1,6 +0,0 @@
module.exports = [
{ name: 'Whisper', value: 'whisper-1' },
{ name: 'GPT 4o Mini Transcribe', value: 'gpt-4o-mini-transcribe' },
{ name: 'GLT 4o Transcribe', value: 'gpt-4o-transcribe' },
];

View File

@@ -1,207 +0,0 @@
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',
},
];

View File

@@ -1,58 +0,0 @@
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',
},
];

View File

@@ -1,59 +0,0 @@
module.exports = [
{ name: 'Afrikaans', value: 'af' },
{ name: 'Arabic', value: 'ar' },
{ name: 'Azerbaijani', value: 'az' },
{ name: 'Belarusian', value: 'be' },
{ name: 'Bulgarian', value: 'bg' },
{ name: 'Bosnian', value: 'bs' },
{ name: 'Catalan', value: 'ca' },
{ name: 'Czech', value: 'cs' },
{ name: 'Welsh', value: 'cy' },
{ name: 'Danish', value: 'da' },
{ name: 'German', value: 'de' },
{ name: 'Greek', value: 'el' },
{ name: 'English', value: 'en' },
{ name: 'Spanish', value: 'es' },
{ name: 'Estonian', value: 'et' },
{ name: 'Persian', value: 'fa' },
{ name: 'Finnish', value: 'fi' },
{ name: 'French', value: 'fr' },
{ name: 'Galician', value: 'gl' },
{ name: 'Hebrew', value: 'he' },
{ name: 'Hindi', value: 'hi' },
{ name: 'Croatian', value: 'hr' },
{ name: 'Hungarian', value: 'hu' },
{ name: 'Armenian', value: 'hy' },
{ name: 'Indonesian', value: 'id' },
{ name: 'Icelandic', value: 'is' },
{ name: 'Italian', value: 'it' },
{ name: 'Japanese', value: 'ja' },
{ name: 'Kazakh', value: 'kk' },
{ name: 'Kannada', value: 'kn' },
{ name: 'Korean', value: 'ko' },
{ name: 'Lithuanian', value: 'lt' },
{ name: 'Latvian', value: 'lv' },
{ name: 'Maori', value: 'mi' },
{ name: 'Macedonian', value: 'mk' },
{ name: 'Marathi', value: 'mr' },
{ name: 'Malay', value: 'ms' },
{ name: 'Nepali', value: 'ne' },
{ name: 'Dutch', value: 'nl' },
{ name: 'Norwegian', value: 'no' },
{ name: 'Polish', value: 'pl' },
{ name: 'Portuguese', value: 'pt' },
{ name: 'Romanian', value: 'ro' },
{ name: 'Russian', value: 'ru' },
{ name: 'Slovak', value: 'sk' },
{ name: 'Slovenian', value: 'sl' },
{ name: 'Serbian', value: 'sr' },
{ name: 'Swedish', value: 'sv' },
{ name: 'Swahili', value: 'sw' },
{ name: 'Tamil', value: 'ta' },
{ name: 'Thai', value: 'th' },
{ name: 'Tagalog', value: 'tl' },
{ name: 'Turkish', value: 'tr' },
{ name: 'Ukrainian', value: 'uk' },
{ name: 'Urdu', value: 'ur' },
{ name: 'Vietnamese', value: 'vi' },
{ name: 'Chinese', value: 'zh' },
];

View File

@@ -1,6 +0,0 @@
module.exports = [
{
name: 'English (United States)',
value: 'en-US',
},
];

View File

@@ -1,218 +0,0 @@
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',
},
];

View File

@@ -1,14 +0,0 @@
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' },
];

View File

@@ -1,8 +0,0 @@
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' },
];

View File

@@ -1,213 +0,0 @@
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)' }],
},
];

View File

@@ -1,301 +0,0 @@
/* 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.',
},
],
},
];

View File

@@ -1,28 +0,0 @@
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;

View File

@@ -1,192 +0,0 @@
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: [] },
];

View File

@@ -1,796 +0,0 @@
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)' },
],
},
];

View File

@@ -1,167 +0,0 @@
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)' },
],
},
];

View File

@@ -1,118 +0,0 @@
module.exports = [
{
value: 'en',
name: 'English',
voices: [
{ name: 'Alex', value: 'Alex' },
{ name: 'Ashley', value: 'Ashley' },
{ name: 'Craig', value: 'Craig' },
{ name: 'Deborah', value: 'Deborah' },
{ name: 'Dennis', value: 'Dennis' },
{ name: 'Edward', value: 'Edward' },
{ name: 'Elizabeth', value: 'Elizabeth' },
{ name: 'Hades', value: 'Hades' },
{ name: 'Julia', value: 'Julia' },
{ name: 'Pixie', value: 'Pixie' },
{ name: 'Mark', value: 'Mark' },
{ name: 'Olivia', value: 'Olivia' },
{ name: 'Priya', value: 'Priya' },
{ name: 'Ronald', value: 'Ronald' },
{ name: 'Sarah', value: 'Sarah' },
{ name: 'Shaun', value: 'Shaun' },
{ name: 'Theodore', value: 'Theodore' },
{ name: 'Timothy', value: 'Timothy' },
{ name: 'Wendy', value: 'Wendy' },
{ name: 'Dominus', value: 'Dominus' },
],
},
{
value: 'zh',
name: 'Chinese',
voices: [
{ name: 'Yichen', value: 'Yichen' },
{ name: 'Xiaoyin', value: 'Xiaoyin' },
{ name: 'Xinyi', value: 'Xinyi' },
{ name: 'Jing', value: 'Jing' },
],
},
{
value: 'nl',
name: 'Dutch',
voices: [
{ name: 'Erik', value: 'Erik' },
{ name: 'Katrien', value: 'Katrien' },
{ name: 'Lennart', value: 'Lennart' },
{ name: 'Lore', value: 'Lore' },
],
},
{
value: 'fr',
name: 'French',
voices: [
{ name: 'Alain', value: 'Alain' },
{ name: 'Hélène', value: 'Hélène' },
{ name: 'Mathieu', value: 'Mathieu' },
{ name: 'Étienne', value: 'Étienne' },
],
},
{
value: 'de',
name: 'German',
voices: [
{ name: 'Johanna', value: 'Johanna' },
{ name: 'Josef', value: 'Josef' },
],
},
{
value: 'it',
name: 'Italian',
voices: [
{ name: 'Gianni', value: 'Gianni' },
{ name: 'Orietta', value: 'Orietta' },
],
},
{
value: 'ja',
name: 'Japanese',
voices: [
{ name: 'Asuka', value: 'Asuka' },
{ name: 'Satoshi', value: 'Satoshi' },
],
},
{
value: 'ko',
name: 'Korean',
voices: [
{ name: 'Hyunwoo', value: 'Hyunwoo' },
{ name: 'Minji', value: 'Minji' },
{ name: 'Seojun', value: 'Seojun' },
{ name: 'Yoona', value: 'Yoona' },
],
},
{
value: 'pl',
name: 'Polish',
voices: [
{ name: 'Szymon', value: 'Szymon' },
{ name: 'Wojciech', value: 'Wojciech' },
],
},
{
value: 'pt',
name: 'Portuguese',
voices: [
{ name: 'Heitor', value: 'Heitor' },
{ name: 'Maitê', value: 'Maitê' },
],
},
{
value: 'es',
name: 'Spanish',
voices: [
{ name: 'Diego', value: 'Diego' },
{ name: 'Lupita', value: 'Lupita' },
{ name: 'Miguel', value: 'Miguel' },
{ name: 'Rafael', value: 'Rafael' },
],
},
];

View File

@@ -1,152 +0,0 @@
// 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'
}
];

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More