mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
101 Commits
fix/micros
...
v0.9.4-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcff3d4b32 | ||
|
|
2dd06df641 | ||
|
|
579a586a03 | ||
|
|
3e1b383284 | ||
|
|
c51b7bab82 | ||
|
|
bb5dba7c20 | ||
|
|
c7e279d0ee | ||
|
|
6700ff35be | ||
|
|
3f2a304830 | ||
|
|
f23c4fbd48 | ||
|
|
0c2f5becdc | ||
|
|
cd6772c10f | ||
|
|
b708f7beb6 | ||
|
|
431cc9e4f4 | ||
|
|
35b10d55d5 | ||
|
|
533e202474 | ||
|
|
e506fc8b66 | ||
|
|
76a2054745 | ||
|
|
be300ebd51 | ||
|
|
27c3664391 | ||
|
|
48e39f37d3 | ||
|
|
2b4b3056e9 | ||
|
|
3cad5219b4 | ||
|
|
30a799030c | ||
|
|
e41caf8887 | ||
|
|
217c11a5e1 | ||
|
|
561de0532f | ||
|
|
ce2ea8bd62 | ||
|
|
c21f5b871f | ||
|
|
9a2e48b538 | ||
|
|
29adbfc6ae | ||
|
|
ffda2398f4 | ||
|
|
b05b32d73e | ||
|
|
b8bf18f8ca | ||
|
|
1e532212f9 | ||
|
|
92347c26bf | ||
|
|
bc51b60e9b | ||
|
|
f0ec0a916f | ||
|
|
c94f14f27d | ||
|
|
06873186ac | ||
|
|
956da4334f | ||
|
|
c144758d44 | ||
|
|
e24f3472ae | ||
|
|
4c935c7fda | ||
|
|
1c55bad04f | ||
|
|
32a2bfcdb5 | ||
|
|
becc1636b7 | ||
|
|
68a9b4226d | ||
|
|
b154b56064 | ||
|
|
556d5c3526 | ||
|
|
2ac0da0d14 | ||
|
|
5a02346c71 | ||
|
|
d872d9ee87 | ||
|
|
d81a0167cf | ||
|
|
c7c56d8ea0 | ||
|
|
9cfe990bb8 | ||
|
|
6c7d2c9074 | ||
|
|
73e35c84c5 | ||
|
|
86d50d94cb | ||
|
|
b8f4ad6b27 | ||
|
|
ad3ec926ee | ||
|
|
66bd9a442c | ||
|
|
fa81d179a1 | ||
|
|
fab8a391b7 | ||
|
|
89288acf6e | ||
|
|
23cd4408a5 | ||
|
|
ce4618523c | ||
|
|
0eb8097e32 | ||
|
|
8851b3fac0 | ||
|
|
e080118b6a | ||
|
|
75c27e3f80 | ||
|
|
843980c7f6 | ||
|
|
f9990da468 | ||
|
|
e8d5655abb | ||
|
|
e908f5830c | ||
|
|
5c7bac91a8 | ||
|
|
de250c8d58 | ||
|
|
84d83a0a48 | ||
|
|
b5bede7a08 | ||
|
|
6e779f6744 | ||
|
|
77b9ca4cba | ||
|
|
0451b6982c | ||
|
|
71adc577e9 | ||
|
|
e8b32103fe | ||
|
|
57d8d0a02c | ||
|
|
a41760fa9f | ||
|
|
c6bae80a03 | ||
|
|
4cddbd83a1 | ||
|
|
6275aac341 | ||
|
|
52de41c9bc | ||
|
|
ed71abd675 | ||
|
|
2d2b98dab5 | ||
|
|
7553e2b617 | ||
|
|
b921cab867 | ||
|
|
48e1a72ef3 | ||
|
|
4337a55a27 | ||
|
|
6041b1d595 | ||
|
|
d33d0aa519 | ||
|
|
ffe9cb23eb | ||
|
|
dbbc894832 | ||
|
|
82c16380f5 |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -7,11 +7,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
- run: npm install
|
||||
- run: npm run jslint
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- run: npm test
|
||||
- run: npm run test:encrypt-decrypt
|
||||
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Drachtio Communications Services, LLC
|
||||
Copyright (c) 2018-2024 FirstFive8, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
38
app.js
38
app.js
@@ -7,6 +7,7 @@ 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');
|
||||
|
||||
@@ -46,13 +47,15 @@ const {
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey
|
||||
incrKey,
|
||||
listConferences
|
||||
} = require('./lib/helpers/realtimedb-helpers');
|
||||
const {
|
||||
getTtsVoices,
|
||||
getTtsSize,
|
||||
purgeTtsCache,
|
||||
getAwsAuthToken,
|
||||
getVerbioAccessToken,
|
||||
synthAudio
|
||||
} = require('@jambonz/speech-utils')({}, logger);
|
||||
const {
|
||||
@@ -88,6 +91,7 @@ app.locals = {
|
||||
deleteCall,
|
||||
listCalls,
|
||||
listSortedSets,
|
||||
listConferences,
|
||||
purgeCalls,
|
||||
retrieveSet,
|
||||
addKey,
|
||||
@@ -97,6 +101,7 @@ app.locals = {
|
||||
getTtsVoices,
|
||||
getTtsSize,
|
||||
getAwsAuthToken,
|
||||
getVerbioAccessToken,
|
||||
purgeTtsCache,
|
||||
synthAudio,
|
||||
lookupAppBySid,
|
||||
@@ -123,11 +128,27 @@ 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
|
||||
@@ -168,6 +189,19 @@ 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');
|
||||
@@ -218,7 +252,7 @@ server.on('upgrade', (request, socket, head) => {
|
||||
|
||||
/* complete the upgrade */
|
||||
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||
logger.info(`upgraded to websocket, url: ${request.url}`);
|
||||
logger.debug(`upgraded to websocket, url: ${request.url}`);
|
||||
wsServer.emit('connection', ws, request.url);
|
||||
});
|
||||
});
|
||||
|
||||
22
db/export_jambonz.sh
Executable file
22
db/export_jambonz.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/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
|
||||
31
db/import_jambonz.sh
Executable file
31
db/import_jambonz.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/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
|
||||
@@ -351,6 +351,8 @@ 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)
|
||||
);
|
||||
|
||||
@@ -358,7 +360,9 @@ CREATE TABLE system_information
|
||||
(
|
||||
domain_name VARCHAR(255),
|
||||
sip_domain_name VARCHAR(255),
|
||||
monitoring_domain_name VARCHAR(255)
|
||||
monitoring_domain_name VARCHAR(255),
|
||||
private_network_cidr VARCHAR(8192),
|
||||
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
@@ -412,6 +416,8 @@ 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';
|
||||
|
||||
@@ -459,6 +465,7 @@ outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when p
|
||||
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)
|
||||
@@ -509,6 +516,7 @@ 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)
|
||||
@@ -551,6 +559,7 @@ 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';
|
||||
|
||||
@@ -735,4 +744,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
11
db/prepare-permissions-test.sql
Normal file
11
db/prepare-permissions-test.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
/* 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');
|
||||
|
||||
@@ -6,6 +6,7 @@ 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}`);
|
||||
|
||||
@@ -22,6 +23,20 @@ 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': [
|
||||
@@ -196,6 +211,23 @@ const sql = {
|
||||
'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',
|
||||
]
|
||||
};
|
||||
|
||||
@@ -229,6 +261,8 @@ const doIt = async() => {
|
||||
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..');
|
||||
|
||||
@@ -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 } = decoded;
|
||||
|
||||
const { user_sid, service_provider_sid, account_sid, email,
|
||||
name, scope, permissions, is_view_only } = decoded;
|
||||
const user = {
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
@@ -45,6 +45,7 @@ function makeStrategy(logger) {
|
||||
email,
|
||||
name,
|
||||
permissions,
|
||||
is_view_only,
|
||||
hasScope: (s) => s === scope,
|
||||
hasAdminAuth: scope === 'admin',
|
||||
hasServiceProviderAuth: scope === 'service_provider',
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
deleteKey,
|
||||
incrKey,
|
||||
client: redisClient,
|
||||
listConferences
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
module.exports = {
|
||||
@@ -27,5 +28,6 @@ module.exports = {
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
redisClient,
|
||||
incrKey
|
||||
incrKey,
|
||||
listConferences
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const logger = require('./logger');
|
||||
const {UserPermissionError} = require('./utils/errors');
|
||||
|
||||
function delayLoginMiddleware(req, res, next) {
|
||||
if (req.path.includes('/login') || req.path.includes('/signin')) {
|
||||
@@ -27,6 +28,26 @@ 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
|
||||
delayLoginMiddleware,
|
||||
verifyViewOnlyUser
|
||||
};
|
||||
|
||||
@@ -56,9 +56,13 @@ AND effective_end_date IS NULL
|
||||
AND pending = 0`;
|
||||
|
||||
const extractBucketCredential = (obj) => {
|
||||
const {bucket_credential} = obj;
|
||||
if (bucket_credential) {
|
||||
obj.bucket_credential = JSON.parse(decrypt(bucket_credential));
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -36,20 +36,98 @@ class Application extends Model {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* list all applications - for all service providers, for one service provider, or for one account
|
||||
*/
|
||||
static retrieveAll(service_provider_sid, account_sid) {
|
||||
let sql = retrieveSql;
|
||||
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 = [];
|
||||
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);
|
||||
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
|
||||
*/
|
||||
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';
|
||||
const args = [];
|
||||
sql += Application._criteriaBuilder(obj, args);
|
||||
sql += ' ORDER BY app.application_sid';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
@@ -13,9 +13,9 @@ class Client extends Model {
|
||||
}
|
||||
|
||||
static async retrieveAllByServiceProviderSid(service_provider_sid) {
|
||||
const sql = `SELECT c.client_sid, c.account_sid, c.is_active, c.username, c.hashed_password
|
||||
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 = accs.service_provider_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;
|
||||
|
||||
45
lib/models/permissions.js
Normal file
45
lib/models/permissions.js
Normal file
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
@@ -26,6 +26,49 @@ 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
|
||||
*/
|
||||
|
||||
@@ -22,13 +22,17 @@ class SpeechCredential extends Model {
|
||||
|
||||
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 = ? ${label ? 'AND label = ?' : ''}`;
|
||||
} else {
|
||||
sql = `SELECT * FROM speech_credentials WHERE service_provider_sid = ? AND vendor = ?
|
||||
${label ? 'AND label = ?' : ''}`;
|
||||
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]);
|
||||
}
|
||||
const [rows] = await promisePool.query(sql, [account_sid ? account_sid : service_provider_sid, vendor, label]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ SystemInformation.fields = [
|
||||
name: 'monitoring_domain_name',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'private_network_cidr',
|
||||
type: 'string',
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = SystemInformation;
|
||||
|
||||
53
lib/models/user-permissions.js
Normal file
53
lib/models/user-permissions.js
Normal file
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
@@ -8,6 +8,57 @@ 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);
|
||||
@@ -136,6 +187,14 @@ VoipCarrier.fields = [
|
||||
{
|
||||
name: 'register_status',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'dtmf_type',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'sip_proxy',
|
||||
type: 'string'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const getUploader = (key, metadata, bucket_credential, logger) => {
|
||||
accessKeyId: bucket_credential.access_key_id,
|
||||
secretAccessKey: bucket_credential.secret_access_key,
|
||||
},
|
||||
region: 'us-east-1',
|
||||
region: bucket_credential.region || 'us-east-1',
|
||||
forcePathStyle: true
|
||||
};
|
||||
return new S3MultipartUploadStream(logger, uploaderOpts);
|
||||
|
||||
@@ -1,6 +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');
|
||||
@@ -19,11 +18,13 @@ const {
|
||||
parseCallSid,
|
||||
enableSubspace,
|
||||
disableSubspace,
|
||||
parseVoipCarrierSid
|
||||
parseVoipCarrierSid,
|
||||
hasValue,
|
||||
} = require('./utils');
|
||||
const short = require('short-uuid');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const { encrypt } = require('../../utils/encrypt-decrypt');
|
||||
const { encrypt, obscureBucketCredentialsSensitiveData,
|
||||
isObscureKey, decrypt } = require('../../utils/encrypt-decrypt');
|
||||
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
|
||||
const translator = short();
|
||||
|
||||
@@ -42,7 +43,7 @@ const getFsUrl = async(logger, retrieveSet, setName) => {
|
||||
return ;
|
||||
}
|
||||
const f = fs[idx++ % fs.length];
|
||||
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
|
||||
logger.debug({fs}, `feature servers available for createCall API request, selecting ${f}`);
|
||||
return `${f}/v1/createCall`;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
|
||||
@@ -92,8 +93,34 @@ router.get('/:sid/Applications', async(req, res) => {
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const results = await Application.retrieveAll(null, account_sid);
|
||||
res.status(200).json(results);
|
||||
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);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -221,7 +248,8 @@ router.get('/:sid/RegisteredSipUsers/:client', async(req, res) => {
|
||||
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'
|
||||
registered_status: user ? 'active' : 'inactive',
|
||||
proxy: user ? user.proxy : null
|
||||
});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -266,7 +294,8 @@ function validateUpdateCall(opts) {
|
||||
'record',
|
||||
'tag',
|
||||
'dtmf',
|
||||
'conferenceParticipantAction'
|
||||
'conferenceParticipantAction',
|
||||
'dub'
|
||||
]
|
||||
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
|
||||
|
||||
@@ -323,10 +352,10 @@ function validateUpdateCall(opts) {
|
||||
throw new DbErrorBadRequest(
|
||||
`conferenceParticipantAction invalid action property ${opts.conferenceParticipantAction.action}`);
|
||||
}
|
||||
if ('tag' == opts.conferenceParticipantAction.action && !opts.tag) {
|
||||
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.tag) {
|
||||
if ('coach' == opts.conferenceParticipantAction.action && !opts.conferenceParticipantAction.tag) {
|
||||
throw new DbErrorBadRequest('conferenceParticipantAction requires tag property when action is \'coach\'');
|
||||
}
|
||||
}
|
||||
@@ -354,7 +383,10 @@ async function validateCreateCall(logger, sid, req) {
|
||||
const {lookupAppBySid} = req.app.locals;
|
||||
const obj = req.body;
|
||||
|
||||
if (req.user.account_sid !== sid) throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
|
||||
if (req.user.hasServiceProviderAuth ||
|
||||
req.user.hasAccountAuth && 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');
|
||||
@@ -554,9 +586,12 @@ router.get('/:sid', async(req, res) => {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const results = await Account.retrieve(account_sid, service_provider_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
return res.status(200).json(results[0]);
|
||||
const [result] = await Account.retrieve(account_sid, service_provider_sid) || [];
|
||||
if (!result) return res.status(404).end();
|
||||
|
||||
result.bucket_credential = obscureBucketCredentialsSensitiveData(result.bucket_credential);
|
||||
|
||||
return res.status(200).json(result);
|
||||
}
|
||||
catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -638,18 +673,20 @@ router.delete('/:sid/SubspaceTeleport', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
function encryptBucketCredential(obj) {
|
||||
if (!obj.bucket_credential) return;
|
||||
function encryptBucketCredential(obj, storedCredentials = {}) {
|
||||
if (!hasValue(obj?.bucket_credential)) return;
|
||||
const {
|
||||
vendor,
|
||||
region,
|
||||
name,
|
||||
access_key_id,
|
||||
secret_access_key,
|
||||
tags,
|
||||
endpoint,
|
||||
} = obj.bucket_credential;
|
||||
let {
|
||||
secret_access_key,
|
||||
service_key,
|
||||
connection_string,
|
||||
endpoint
|
||||
} = obj.bucket_credential;
|
||||
|
||||
switch (vendor) {
|
||||
@@ -658,6 +695,9 @@ function encryptBucketCredential(obj) {
|
||||
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);
|
||||
@@ -667,18 +707,29 @@ function encryptBucketCredential(obj) {
|
||||
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});
|
||||
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;
|
||||
@@ -736,7 +787,17 @@ router.put('/:sid', async(req, res) => {
|
||||
delete obj.registration_hook;
|
||||
delete obj.queue_event_hook;
|
||||
|
||||
encryptBucketCredential(obj);
|
||||
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) {
|
||||
@@ -836,6 +897,13 @@ router.post('/:sid/BucketCredentialTest', async(req, res) => {
|
||||
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'
|
||||
@@ -904,24 +972,25 @@ router.post('/:sid/Calls', async(req, res) => {
|
||||
|
||||
await validateCreateCall(logger, sid, req);
|
||||
updateLastUsed(logger, sid, req).catch((err) => {});
|
||||
request({
|
||||
url: serviceUrl,
|
||||
const response = await fetch(serviceUrl, {
|
||||
method: 'POST',
|
||||
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);
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(Object.assign(req.body, {account_sid: sid}))
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1012,22 +1081,54 @@ 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}`);
|
||||
request({
|
||||
url: url,
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: req.body
|
||||
}).pipe(res);
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -1054,7 +1155,9 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
|
||||
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
|
||||
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
|
||||
if (!serviceUrl) {
|
||||
return res.status(480).json({msg: 'no available feature servers at this time'});
|
||||
}
|
||||
await validateCreateMessage(logger, account_sid, req);
|
||||
|
||||
const payload = {
|
||||
@@ -1064,22 +1167,19 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
};
|
||||
logger.debug({payload}, `sending createMessage API request to to ${serviceUrl}`);
|
||||
updateLastUsed(logger, account_sid, req).catch(() => {});
|
||||
request({
|
||||
url: serviceUrl,
|
||||
const response = await fetch(serviceUrl, {
|
||||
method: 'POST',
|
||||
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);
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
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);
|
||||
}
|
||||
@@ -1103,5 +1203,21 @@ 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);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -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 < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
|
||||
if (!count || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
|
||||
|
||||
if (account_sid) {
|
||||
const data = await queryAlerts({
|
||||
|
||||
47
lib/routes/api/appenv.js
Normal file
47
lib/routes/api/appenv.js
Normal file
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
@@ -7,11 +7,13 @@ const {promisePool} = require('../../db');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
const { validate } = require('@jambonz/verb-specifications');
|
||||
const { parseApplicationSid } = require('./utils');
|
||||
const { parseApplicationSid, isInvalidUrl } = 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 {
|
||||
@@ -62,6 +64,14 @@ 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) {
|
||||
@@ -142,6 +152,16 @@ 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});
|
||||
@@ -153,11 +173,34 @@ 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;
|
||||
const results = await Application.retrieveAll(service_provider_sid, account_sid);
|
||||
res.status(200).json(results);
|
||||
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);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -173,6 +216,9 @@ 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) {
|
||||
@@ -227,6 +273,8 @@ 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);
|
||||
@@ -258,6 +306,21 @@ 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();
|
||||
|
||||
@@ -4,6 +4,9 @@ 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);
|
||||
@@ -41,6 +44,33 @@ const preconditions = {
|
||||
|
||||
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;
|
||||
@@ -67,7 +97,37 @@ router.get('/', async(req, res) => {
|
||||
}
|
||||
results = await GoogleCustomVoice.retrieveAllByLabel(service_provider_sid, account_sid, label);
|
||||
}
|
||||
res.status(200).json(results);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ 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'));
|
||||
|
||||
@@ -29,6 +29,9 @@ 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;
|
||||
@@ -71,9 +74,13 @@ 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
|
||||
@@ -83,7 +90,8 @@ router.post('/', async(req, res) => {
|
||||
account_name: obj.account_name,
|
||||
service_provider_name: obj.service_provider_name
|
||||
}),
|
||||
user_sid: obj.user_sid
|
||||
user_sid: obj.user_sid,
|
||||
name: username
|
||||
};
|
||||
|
||||
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
|
||||
|
||||
@@ -40,6 +40,10 @@ 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,11 +97,35 @@ 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 {
|
||||
const results = req.user.hasServiceProviderAuth ?
|
||||
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid) :
|
||||
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
|
||||
res.status(200).json(results);
|
||||
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);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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,
|
||||
@@ -31,7 +32,7 @@ router.get('/', async(req, res) => {
|
||||
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
|
||||
const {page, count, trunk, direction, days, answered, start, end, filter} = req.query || {};
|
||||
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
|
||||
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
|
||||
if (!count || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
|
||||
|
||||
if (account_sid) {
|
||||
const data = await queryCdrs({
|
||||
@@ -106,6 +107,71 @@ 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;
|
||||
|
||||
@@ -3,7 +3,7 @@ const debug = require('debug')('jambonz:api-server');
|
||||
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const {promisePool} = require('../../db');
|
||||
const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils');
|
||||
const {validateEmail} = require('../../utils/email-utils');
|
||||
const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
|
||||
const {cacheClient} = require('../../helpers');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const short = require('short-uuid');
|
||||
@@ -12,6 +12,7 @@ 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)`;
|
||||
@@ -36,6 +37,17 @@ 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) => {
|
||||
const [r] = await promisePool.execute(insertUserLocalSql,
|
||||
@@ -324,6 +336,15 @@ router.post('/', async(req, res) => {
|
||||
tutorial_completion: 0,
|
||||
scope: 'read-write'
|
||||
});
|
||||
|
||||
// send invite to Slack
|
||||
if (process.env.SEND_SLACK_INVITE_ON_SIGNUP) {
|
||||
try {
|
||||
emailSimpleText(logger, userProfile.email, 'Welcome to jambonz!', slackEmail);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error sending slack invite');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (user_sid) {
|
||||
/* add a new user for existing account */
|
||||
|
||||
@@ -149,13 +149,30 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
|
||||
try {
|
||||
await validateRetrieve(req);
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
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));
|
||||
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});
|
||||
}
|
||||
carriers = await VoipCarrier.retrieveByCriteria({
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
name,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
|
||||
res.status(200).json(carriers);
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: carriers,
|
||||
} : carriers;
|
||||
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -4,6 +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 checkUserScope = async(req, voip_carrier_sid) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
@@ -41,6 +42,7 @@ 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) {
|
||||
@@ -52,6 +54,18 @@ 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 (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);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const router = require('express').Router();
|
||||
const request = require('request');
|
||||
const getProvider = require('../../utils/sms-provider');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const sysError = require('../error');
|
||||
@@ -122,25 +121,19 @@ router.post('/:provider', async(req, res) => {
|
||||
|
||||
logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${serviceUrl}`);
|
||||
|
||||
request({
|
||||
url: serviceUrl,
|
||||
const response = await fetch(serviceUrl, {
|
||||
method: 'POST',
|
||||
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);
|
||||
body: JSON.stringify(payload),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error({response}, `Error sending incomingSms POST to ${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);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@ const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = req
|
||||
const {decryptCredential, testWhisper, testDeepgramTTS,
|
||||
getLanguagesAndVoicesForVendor,
|
||||
testPlayHT,
|
||||
testRimelabs} = require('../../utils/speech-utils');
|
||||
testRimelabs,
|
||||
testVerbioTts,
|
||||
testVerbioStt,
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt,
|
||||
testOpenAiStt} = require('../../utils/speech-utils');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {
|
||||
testGoogleTts,
|
||||
@@ -116,11 +122,15 @@ const encryptCredential = (obj) => {
|
||||
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,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
@@ -135,11 +145,13 @@ const encryptCredential = (obj) => {
|
||||
instance_id,
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
custom_tts_streaming_url,
|
||||
auth_token = '',
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
user_id,
|
||||
voice_engine,
|
||||
engine_version,
|
||||
options
|
||||
} = obj;
|
||||
|
||||
@@ -200,10 +212,11 @@ const encryptCredential = (obj) => {
|
||||
|
||||
case 'deepgram':
|
||||
// API key is optional if onprem
|
||||
if (!deepgram_stt_uri) {
|
||||
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});
|
||||
const deepgramData = JSON.stringify({api_key, deepgram_stt_uri,
|
||||
deepgram_stt_use_tls, deepgram_tts_uri, model_id});
|
||||
return encrypt(deepgramData);
|
||||
|
||||
case 'ibm':
|
||||
@@ -231,13 +244,25 @@ const encryptCredential = (obj) => {
|
||||
const elevenlabsData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(elevenlabsData);
|
||||
|
||||
case 'speechmatics':
|
||||
assert(api_key, 'invalid speechmatics speech credential: api_key is required');
|
||||
assert(speechmatics_stt_uri, 'invalid speechmatics speech credential: speechmatics_stt_uri is required');
|
||||
const speechmaticsData = JSON.stringify({api_key, speechmatics_stt_uri, options});
|
||||
return encrypt(speechmaticsData);
|
||||
|
||||
case 'playht':
|
||||
assert(api_key, 'invalid playht speech credential: api_key is required');
|
||||
assert(user_id, 'invalid playht speech credential: user_id is required');
|
||||
assert(voice_engine, 'invalid voice_engine speech credential: voice_engine is required');
|
||||
const playhtData = JSON.stringify({api_key, user_id, voice_engine, options});
|
||||
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');
|
||||
assert(model_id, 'invalid cartesia speech credential: model_id is required');
|
||||
const cartesiaData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(cartesiaData);
|
||||
|
||||
case 'rimelabs':
|
||||
assert(api_key, 'invalid rimelabs speech credential: api_key is required');
|
||||
assert(model_id, 'invalid rimelabs speech credential: model_id is required');
|
||||
@@ -249,15 +274,33 @@ const encryptCredential = (obj) => {
|
||||
const assemblyaiData = JSON.stringify({api_key});
|
||||
return encrypt(assemblyaiData);
|
||||
|
||||
case 'voxist':
|
||||
assert(api_key, 'invalid voxist speech credential: api_key is required');
|
||||
const voxistData = JSON.stringify({api_key});
|
||||
return encrypt(voxistData);
|
||||
|
||||
case 'whisper':
|
||||
assert(api_key, 'invalid whisper speech credential: api_key is required');
|
||||
assert(model_id, 'invalid whisper speech credential: model_id is required');
|
||||
const whisperData = JSON.stringify({api_key, model_id});
|
||||
return encrypt(whisperData);
|
||||
|
||||
case '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});
|
||||
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url, custom_tts_streaming_url});
|
||||
return encrypt(customData);
|
||||
}
|
||||
else assert(false, `invalid or missing vendor: ${vendor}`);
|
||||
@@ -441,12 +484,17 @@ router.put('/:sid', async(req, res) => {
|
||||
custom_stt_endpoint_url,
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
custom_tts_streaming_url,
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
voice_engine,
|
||||
options,
|
||||
deepgram_stt_uri,
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
playht_tts_uri,
|
||||
engine_version,
|
||||
speechmatics_stt_uri
|
||||
} = req.body;
|
||||
|
||||
const newCred = {
|
||||
@@ -467,12 +515,17 @@ router.put('/:sid', async(req, res) => {
|
||||
nuance_tts_uri,
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
custom_tts_streaming_url,
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
voice_engine,
|
||||
options,
|
||||
deepgram_stt_uri,
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
playht_tts_uri,
|
||||
engine_version,
|
||||
speechmatics_stt_uri
|
||||
};
|
||||
logger.info({o, newCred}, 'updating speech credential with this new credential');
|
||||
obj.credential = encryptCredential(newCred);
|
||||
@@ -501,7 +554,7 @@ router.put('/:sid', async(req, res) => {
|
||||
* Test a credential
|
||||
*/
|
||||
router.get('/:sid/test', async(req, res) => {
|
||||
const {logger, synthAudio} = req.app.locals;
|
||||
const {logger, synthAudio, getVerbioAccessToken} = req.app.locals;
|
||||
try {
|
||||
const sid = parseSpeechCredentialSid(req);
|
||||
const creds = await SpeechCredential.retrieve(sid);
|
||||
@@ -613,7 +666,7 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testMicrosoftStt(logger, {api_key, region});
|
||||
await testMicrosoftStt(logger, {api_key, region, use_custom_stt, custom_stt_endpoint_url});
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
@@ -672,8 +725,7 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'deepgram') {
|
||||
} else if (cred.vendor === 'deepgram') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
@@ -753,6 +805,18 @@ router.get('/:sid/test', async(req, res) => {
|
||||
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 {
|
||||
@@ -761,7 +825,21 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
// if error is from bent, let get the body
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
results.tts = {status: 'fail', reason};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'cartesia') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testCartesia(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
@@ -792,6 +870,18 @@ router.get('/:sid/test', async(req, res) => {
|
||||
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 {
|
||||
@@ -803,6 +893,38 @@ router.get('/:sid/test', async(req, res) => {
|
||||
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);
|
||||
@@ -819,7 +941,7 @@ router.get('/:sid/test', async(req, res) => {
|
||||
router.get('/speech/supportedLanguagesAndVoices', async(req, res) => {
|
||||
const {logger, getTtsVoices} = req.app.locals;
|
||||
try {
|
||||
const {vendor, label} = req.query;
|
||||
const {vendor, label, create_new} = req.query;
|
||||
if (!vendor) {
|
||||
throw new DbErrorBadRequest('vendor is required');
|
||||
}
|
||||
@@ -827,7 +949,7 @@ router.get('/speech/supportedLanguagesAndVoices', async(req, res) => {
|
||||
const service_provider_sid = req.user.service_provider_sid ||
|
||||
req.body.service_provider_sid || parseServiceProviderSid(req);
|
||||
|
||||
const credentials = await SpeechCredential.getSpeechCredentialsByVendorAndLabel(
|
||||
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;
|
||||
|
||||
@@ -229,7 +229,7 @@ const updateQuantities = async(req, res) => {
|
||||
const obj = {
|
||||
quantity: product.quantity,
|
||||
};
|
||||
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price_id: product.price_id});
|
||||
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price: product.price_id});
|
||||
});
|
||||
|
||||
if (dry_run) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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');
|
||||
@@ -38,7 +40,8 @@ const validateRequest = async(user_sid, req) => {
|
||||
email,
|
||||
email_activation_code,
|
||||
force_change,
|
||||
is_active
|
||||
is_active,
|
||||
is_view_only
|
||||
} = payload;
|
||||
|
||||
const [r] = await promisePool.query(retrieveSql, user_sid);
|
||||
@@ -93,7 +96,8 @@ 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)
|
||||
if (!name && !new_password && !email && !initial_password && !force_change && !is_active &&
|
||||
is_view_only === undefined)
|
||||
throw new DbErrorBadRequest('no updates requested');
|
||||
|
||||
return user;
|
||||
@@ -140,7 +144,35 @@ 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;
|
||||
|
||||
@@ -308,7 +340,14 @@ 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);
|
||||
@@ -332,9 +371,9 @@ router.put('/:user_sid', async(req, res) => {
|
||||
is_active,
|
||||
force_change,
|
||||
account_sid,
|
||||
service_provider_sid
|
||||
service_provider_sid,
|
||||
is_view_only
|
||||
} = req.body;
|
||||
|
||||
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
||||
|
||||
if (!hasAdminAuth &&
|
||||
@@ -427,6 +466,8 @@ 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);
|
||||
@@ -443,6 +484,8 @@ 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) {
|
||||
@@ -464,6 +507,7 @@ 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) {
|
||||
@@ -472,6 +516,7 @@ 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) {
|
||||
@@ -480,6 +525,7 @@ 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) {
|
||||
@@ -497,6 +543,8 @@ 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 */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { v4: uuid, validate } = require('uuid');
|
||||
const bent = require('bent');
|
||||
const URL = require('url').URL;
|
||||
const isValidHostname = require('is-valid-hostname');
|
||||
const Account = require('../../models/account');
|
||||
const {promisePool} = require('../../db');
|
||||
const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils');
|
||||
@@ -11,8 +12,6 @@ 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();
|
||||
@@ -370,35 +369,44 @@ const checkLimits = async(req, res, next) => {
|
||||
};
|
||||
|
||||
const getSubspaceJWT = async(id, secret) => {
|
||||
const postJwt = bent('https://id.subspace.com', 'POST', 'json', 200);
|
||||
const jwt = await postJwt('/oauth/token',
|
||||
{
|
||||
const response = await fetch('https://id.subspace.com/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
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 postTeleport = bent('https://api.subspace.com', 'POST', 'json', 200);
|
||||
|
||||
const teleport = await postTeleport('/v1/sipteleport',
|
||||
{
|
||||
const response = await fetch('https://api.subspace.com/v1/sipteleport', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'Jambonz',
|
||||
destination,
|
||||
status: 'ENABLED'
|
||||
},
|
||||
{
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
);
|
||||
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to enable teleport: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const teleport = await response.json();
|
||||
return teleport;
|
||||
};
|
||||
|
||||
@@ -406,13 +414,15 @@ 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 deleteTeleport = bent('https://api.subspace.com', 'DELETE', 'json', 200);
|
||||
await deleteTeleport(relativeUrl, {},
|
||||
{
|
||||
const response = await fetch(`https://api.subspace.com${relativeUrl}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
);
|
||||
return;
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete teleport: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
const validatePasswordSettings = async(password) => {
|
||||
@@ -440,6 +450,44 @@ 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,
|
||||
@@ -460,5 +508,7 @@ module.exports = {
|
||||
checkLimits,
|
||||
enableSubspace,
|
||||
disableSubspace,
|
||||
validatePasswordSettings
|
||||
validatePasswordSettings,
|
||||
hasValue,
|
||||
isInvalidUrl
|
||||
};
|
||||
|
||||
@@ -73,16 +73,36 @@ 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 {
|
||||
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));
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await VoipCarrier.countAll({service_provider_sid, account_sid, name});
|
||||
}
|
||||
|
||||
res.status(200).json(results);
|
||||
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);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -3882,6 +3882,34 @@ 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:
|
||||
|
||||
50
lib/utils/appenv_schemaSchema.json
Normal file
50
lib/utils/appenv_schemaSchema.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
78
lib/utils/appenv_utils.js
Normal file
78
lib/utils/appenv_utils.js
Normal file
@@ -0,0 +1,78 @@
|
||||
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
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
if (!process.env.JAMBONES_HOSTING) return;
|
||||
|
||||
const bent = require('bent');
|
||||
const crypto = require('crypto');
|
||||
const assert = require('assert');
|
||||
const domains = new Map();
|
||||
@@ -26,17 +25,20 @@ const createAuthHeaders = () => {
|
||||
const getDnsDomainId = async(logger, name) => {
|
||||
checkAsserts();
|
||||
const headers = createAuthHeaders();
|
||||
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');
|
||||
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}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,21 +82,20 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
|
||||
];
|
||||
const headers = createAuthHeaders();
|
||||
const records = [...a_records, ...srv_records];
|
||||
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);
|
||||
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;
|
||||
}
|
||||
let body;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch (err) {
|
||||
const result = await response.json();
|
||||
logger.debug({result}, 'createDnsRecords: created records');
|
||||
if (201 === response.status) {
|
||||
return result;
|
||||
}
|
||||
logger.error({headers: res.headers, body}, `Error creating records, status ${res.statusCode}`);
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error retrieving domains');
|
||||
}
|
||||
@@ -103,7 +104,6 @@ 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,7 +112,10 @@ const deleteDnsRecords = async(logger, domain, recIds) => {
|
||||
}
|
||||
const domainId = domains.get(domain);
|
||||
const url = `/dns/managed/${domainId}/records?${recIds.map((r) => `ids=${r}`).join('&')}`;
|
||||
await del(url);
|
||||
await fetch(`${process.env.DME_BASE_URL}${url}`, {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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,}))$/;
|
||||
@@ -19,8 +18,9 @@ const emailSimpleText = async(logger, to, subject, text) => {
|
||||
};
|
||||
|
||||
const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
|
||||
try {
|
||||
const post = bent('POST', {
|
||||
const response = await fetch(process.env.CUSTOM_EMAIL_VENDOR_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...((process.env.CUSTOM_EMAIL_VENDOR_USERNAME && process.env.CUSTOM_EMAIL_VENDOR_PASSWORD) &&
|
||||
({
|
||||
@@ -28,22 +28,22 @@ const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
|
||||
`${process.env.CUSTOM_EMAIL_VENDOR_USERNAME}:${process.env.CUSTOM_EMAIL_VENDOR_PASSWORD}`
|
||||
).toString('base64')}`
|
||||
}))
|
||||
});
|
||||
|
||||
const res = await post(process.env.CUSTOM_EMAIL_VENDOR_URL, {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text
|
||||
});
|
||||
logger.debug({
|
||||
res
|
||||
}, 'sent email to custom vendor.');
|
||||
} catch (err) {
|
||||
logger.info({
|
||||
err
|
||||
}, 'Error sending email From Custom email vendor');
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error sending email to custom vendor');
|
||||
return;
|
||||
}
|
||||
const res = await response.json();
|
||||
logger.debug({
|
||||
res
|
||||
}, 'sent email to custom vendor.');
|
||||
};
|
||||
|
||||
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
|
||||
|
||||
@@ -17,10 +17,15 @@ const encrypt = (text) => {
|
||||
};
|
||||
|
||||
const decrypt = (data) => {
|
||||
const hash = JSON.parse(data);
|
||||
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
||||
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||
return decrpyted.toString();
|
||||
try {
|
||||
const hash = JSON.parse(data);
|
||||
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
||||
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||
return decrpyted.toString();
|
||||
} catch (error) {
|
||||
console.error('Error while decrypting data', error);
|
||||
return '{}';
|
||||
}
|
||||
};
|
||||
|
||||
const obscureKey = (key, key_spoiler_length = 6) => {
|
||||
@@ -33,8 +38,85 @@ const obscureKey = (key, key_spoiler_length = 6) => {
|
||||
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
|
||||
obscureKey,
|
||||
isObscureKey,
|
||||
obscureBucketCredentialsSensitiveData,
|
||||
};
|
||||
|
||||
@@ -27,11 +27,17 @@ class DbErrorForbidden extends DbError {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
class UserPermissionError extends Error {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BadRequestError,
|
||||
DbError,
|
||||
DbErrorBadRequest,
|
||||
DbErrorUnprocessableRequest,
|
||||
DbErrorForbidden
|
||||
DbErrorForbidden,
|
||||
UserPermissionError
|
||||
};
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
const bent = require('bent');
|
||||
const basicAuth = (apiKey) => {
|
||||
const header = `Bearer ${apiKey}`;
|
||||
return {Authorization: header};
|
||||
};
|
||||
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 { Readable } = require('stream');
|
||||
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) {
|
||||
@@ -17,11 +13,21 @@ const getHomerApiKey = async(logger) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const obj = await postJSON('/api/v3/auth', {
|
||||
username: process.env.HOMER_USERNAME,
|
||||
password: process.env.HOMER_PASSWORD
|
||||
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
|
||||
})
|
||||
});
|
||||
debug(obj);
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving apikey');
|
||||
return;
|
||||
}
|
||||
const obj = await response.json();
|
||||
logger.debug({obj}, `getHomerApiKey for user ${process.env.HOMER_USERNAME}`);
|
||||
return obj.token;
|
||||
} catch (err) {
|
||||
@@ -36,28 +42,40 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
|
||||
}
|
||||
try {
|
||||
const now = Date.now();
|
||||
const obj = await postJSON('/api/v3/call/transaction', {
|
||||
param: {
|
||||
transaction: {
|
||||
call: true,
|
||||
registration: true,
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: [callId]
|
||||
},
|
||||
'1_registration': {
|
||||
callid: [callId]
|
||||
}
|
||||
},
|
||||
const response = await fetch(`${HOMER_BASE_URL}/api/v3/call/transaction`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...basicAuth(apiKey)
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
}, basicAuth(apiKey));
|
||||
body: JSON.stringify({
|
||||
param: {
|
||||
transaction: {
|
||||
call: true,
|
||||
registration: true,
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: [callId]
|
||||
},
|
||||
'1_registration': {
|
||||
callid: [callId]
|
||||
}
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving messages');
|
||||
return;
|
||||
}
|
||||
const obj = await response.json();
|
||||
return obj;
|
||||
} catch (err) {
|
||||
logger.info({err}, `getHomerSipTrace: Error retrieving messages for callid ${callId}`);
|
||||
@@ -70,34 +88,45 @@ const getHomerPcap = async(logger, apiKey, callIds, method) => {
|
||||
}
|
||||
try {
|
||||
const now = Date.now();
|
||||
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
|
||||
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 response = await fetch(`${HOMER_BASE_URL}/api/v3/export/call/messages/pcap`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...basicAuth(apiKey)
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
}, basicAuth(apiKey));
|
||||
return stream;
|
||||
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
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving messages');
|
||||
return;
|
||||
}
|
||||
return Readable.fromWeb(response.body);
|
||||
} catch (err) {
|
||||
logger.info({err}, `getHomerPcap: Error retrieving messages for callid ${callIds}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const bent = require('bent');
|
||||
const getJSON = bent(process.env.JAEGER_BASE_URL || 'http://127.0.0.1', 'GET', 'json', 200);
|
||||
const JAEGER_BASE_URL = process.env.JAEGER_BASE_URL || 'http://127.0.0.1';
|
||||
|
||||
const getJaegerTrace = async(logger, traceId) => {
|
||||
if (!process.env.JAEGER_BASE_URL) {
|
||||
@@ -7,7 +6,12 @@ const getJaegerTrace = async(logger, traceId) => {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await getJSON(`/api/v3/traces/${traceId}`);
|
||||
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();
|
||||
} catch (err) {
|
||||
const url = `${process.env.JAEGER_BASE_URL}/api/traces/${traceId}`;
|
||||
logger.error({err, traceId}, `getJaegerTrace: Error retrieving spans from ${url}`);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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');
|
||||
|
||||
@@ -10,13 +7,26 @@ const doGithubAuth = async(logger, payload) => {
|
||||
|
||||
try {
|
||||
/* exchange the code for an access token */
|
||||
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
|
||||
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
|
||||
})
|
||||
});
|
||||
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');
|
||||
@@ -25,17 +35,31 @@ 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 userDetails = await getJSON('https://api.github.com/user', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
const userResponse = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
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 emails = await getJSON('https://api.github.com/user/emails', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
const emailsResponse = await fetch('https://api.github.com/user/emails', {
|
||||
headers: {
|
||||
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,
|
||||
@@ -55,14 +79,26 @@ const doGoogleAuth = async(logger, payload) => {
|
||||
|
||||
try {
|
||||
/* exchange the code for an access token */
|
||||
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'
|
||||
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'
|
||||
})
|
||||
});
|
||||
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');
|
||||
@@ -71,12 +107,18 @@ 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 userDetails = await getJSON('https://www.googleapis.com/oauth2/v2/userinfo', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
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'
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,10 +1,57 @@
|
||||
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' }
|
||||
];
|
||||
|
||||
@@ -1,138 +1,56 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Chinese - general',
|
||||
value: 'zh',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (China)',
|
||||
value: 'zh-CN',
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Taiwan)',
|
||||
value: 'zh-TW',
|
||||
},
|
||||
{
|
||||
name: 'Dutch - general',
|
||||
value: 'nl',
|
||||
},
|
||||
{
|
||||
name: 'English - general',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
name: 'English (Australia)',
|
||||
value: 'en-AU',
|
||||
},
|
||||
{
|
||||
name: 'English (United Kingdom)',
|
||||
value: 'en-GB',
|
||||
},
|
||||
{
|
||||
name: 'English (India)',
|
||||
value: 'en-IN',
|
||||
},
|
||||
{
|
||||
name: 'English (New Zealand)',
|
||||
value: 'en-NZ',
|
||||
},
|
||||
{
|
||||
name: 'English (United States)',
|
||||
value: 'en-US',
|
||||
},
|
||||
{
|
||||
name: 'French - general',
|
||||
value: 'fr',
|
||||
},
|
||||
{
|
||||
name: 'French (Canada)',
|
||||
value: 'fr-CA',
|
||||
},
|
||||
{
|
||||
name: 'German - general',
|
||||
value: 'de',
|
||||
},
|
||||
{
|
||||
name: 'Hindi - general',
|
||||
value: 'hi',
|
||||
},
|
||||
{
|
||||
name: 'Hindi (Roman Script)',
|
||||
value: 'hi-Latin',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian - general',
|
||||
value: 'in',
|
||||
},
|
||||
{
|
||||
name: 'Italian - general',
|
||||
value: 'it',
|
||||
},
|
||||
{
|
||||
name: 'Japanese - general',
|
||||
value: 'ja',
|
||||
},
|
||||
{
|
||||
name: 'Korean - general',
|
||||
value: 'ko',
|
||||
},
|
||||
{
|
||||
name: 'Norwegian - general',
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
name: 'Polish - general',
|
||||
value: 'pl',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese - general',
|
||||
value: 'pt',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Brazil)',
|
||||
value: 'pt-BR',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (Portugal)',
|
||||
value: 'pt-PT',
|
||||
},
|
||||
{
|
||||
name: 'Russian - general',
|
||||
value: 'ru',
|
||||
},
|
||||
{
|
||||
name: 'Spanish - general',
|
||||
value: 'es',
|
||||
},
|
||||
{
|
||||
name: 'Spanish (Latin America)',
|
||||
value: 'es-419',
|
||||
},
|
||||
{
|
||||
name: 'Swedish - general',
|
||||
value: 'sv',
|
||||
},
|
||||
{
|
||||
name: 'Turkish - general',
|
||||
value: 'tr',
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian - general',
|
||||
value: 'uk',
|
||||
},
|
||||
{
|
||||
name: 'Flemish - general',
|
||||
value: 'nl-BE',
|
||||
},
|
||||
{
|
||||
name: 'Danish - general',
|
||||
value: 'da',
|
||||
},
|
||||
{
|
||||
name: 'Tamil - general',
|
||||
value: 'ta',
|
||||
},
|
||||
{
|
||||
name: 'Tamasheq - general',
|
||||
value: 'taq',
|
||||
},
|
||||
{ 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' }
|
||||
];
|
||||
|
||||
52
lib/utils/speech-data/stt-model-deepgram.js
Normal file
52
lib/utils/speech-data/stt-model-deepgram.js
Normal file
@@ -0,0 +1,52 @@
|
||||
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' },
|
||||
];
|
||||
|
||||
6
lib/utils/speech-data/stt-model-openai.js
Normal file
6
lib/utils/speech-data/stt-model-openai.js
Normal file
@@ -0,0 +1,6 @@
|
||||
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' },
|
||||
];
|
||||
|
||||
59
lib/utils/speech-data/stt-openai.js
Normal file
59
lib/utils/speech-data/stt-openai.js
Normal file
@@ -0,0 +1,59 @@
|
||||
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' },
|
||||
];
|
||||
218
lib/utils/speech-data/stt-speechmatics.js
Normal file
218
lib/utils/speech-data/stt-speechmatics.js
Normal file
@@ -0,0 +1,218 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Automatic',
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
name: 'Arabic',
|
||||
value: 'ar',
|
||||
},
|
||||
{
|
||||
name: 'Bashkir',
|
||||
value: 'ba',
|
||||
},
|
||||
{
|
||||
name: 'Basque',
|
||||
value: 'eu',
|
||||
},
|
||||
{
|
||||
name: 'Belarusian',
|
||||
value: 'be',
|
||||
},
|
||||
{
|
||||
name: 'Bulgarian',
|
||||
value: 'bg',
|
||||
},
|
||||
{
|
||||
name: 'Cantonese',
|
||||
value: 'yue',
|
||||
},
|
||||
{
|
||||
name: 'Catalan',
|
||||
value: 'ca',
|
||||
},
|
||||
{
|
||||
name: 'Croatian',
|
||||
value: 'hr',
|
||||
},
|
||||
{
|
||||
name: 'Czech',
|
||||
value: 'cs',
|
||||
},
|
||||
{
|
||||
name: 'Danish',
|
||||
value: 'da',
|
||||
},
|
||||
{
|
||||
name: 'Dutch',
|
||||
value: 'nl',
|
||||
},
|
||||
{
|
||||
name: 'English',
|
||||
value: 'en',
|
||||
},
|
||||
{
|
||||
name: 'Esperanto',
|
||||
value: 'eo',
|
||||
},
|
||||
{
|
||||
name: 'Estonian',
|
||||
value: 'et',
|
||||
},
|
||||
{
|
||||
name: 'Finnish',
|
||||
value: 'fi',
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
value: 'fr',
|
||||
},
|
||||
{
|
||||
name: 'Galician',
|
||||
value: 'gl',
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
value: 'de',
|
||||
},
|
||||
{
|
||||
name: 'Greek',
|
||||
value: 'el',
|
||||
},
|
||||
{
|
||||
name: 'Hebrew',
|
||||
value: 'he',
|
||||
},
|
||||
{
|
||||
name: 'Hindi',
|
||||
value: 'hi',
|
||||
},
|
||||
{
|
||||
name: 'Hungarian',
|
||||
value: 'hu',
|
||||
},
|
||||
{
|
||||
name: 'Irish',
|
||||
value: 'ga',
|
||||
},
|
||||
{
|
||||
name: 'Interlingua',
|
||||
value: 'ia',
|
||||
},
|
||||
{
|
||||
name: 'Italian',
|
||||
value: 'it',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian',
|
||||
value: 'id',
|
||||
},
|
||||
{
|
||||
name: 'Japanese',
|
||||
value: 'ja',
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
value: 'ko',
|
||||
},
|
||||
{
|
||||
name: 'Latvian',
|
||||
value: 'lv',
|
||||
},
|
||||
{
|
||||
name: 'Lithuanian',
|
||||
value: 'lt',
|
||||
},
|
||||
{
|
||||
name: 'Maltese',
|
||||
value: 'mt',
|
||||
},
|
||||
{
|
||||
name: 'Malay',
|
||||
value: 'ms',
|
||||
},
|
||||
{
|
||||
name: 'Mandarin',
|
||||
value: 'cmn',
|
||||
},
|
||||
{
|
||||
name: 'Marathi',
|
||||
value: 'mr',
|
||||
},
|
||||
{
|
||||
name: 'Mongolian',
|
||||
value: 'mn',
|
||||
},
|
||||
{
|
||||
name: 'Norwegian',
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
name: 'Persian',
|
||||
value: 'fa',
|
||||
},
|
||||
{
|
||||
name: 'Polish',
|
||||
value: 'pl',
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
value: 'pt',
|
||||
},
|
||||
{
|
||||
name: 'Romanian',
|
||||
value: 'ro',
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
value: 'ru',
|
||||
},
|
||||
{
|
||||
name: 'Slovakian',
|
||||
value: 'sk',
|
||||
},
|
||||
{
|
||||
name: 'Slovenian',
|
||||
value: 'sl',
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
value: 'es',
|
||||
},
|
||||
{
|
||||
name: 'Spanish & English bilingual',
|
||||
value: 'es',
|
||||
},
|
||||
{
|
||||
name: 'Swedish',
|
||||
value: 'sv',
|
||||
},
|
||||
{
|
||||
name: 'Tamil',
|
||||
value: 'ta',
|
||||
},
|
||||
{
|
||||
name: 'Thai',
|
||||
value: 'th',
|
||||
},
|
||||
{
|
||||
name: 'Turkish',
|
||||
value: 'tr',
|
||||
},
|
||||
{
|
||||
name: 'Uyghur',
|
||||
value: 'ug',
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian',
|
||||
value: 'uk',
|
||||
},
|
||||
{
|
||||
name: 'Vietnamese',
|
||||
value: 'vi',
|
||||
},
|
||||
{
|
||||
name: 'Welsh',
|
||||
value: 'cy',
|
||||
},
|
||||
];
|
||||
14
lib/utils/speech-data/stt-verbio.js
Normal file
14
lib/utils/speech-data/stt-verbio.js
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = [
|
||||
{ name: 'US English', value: 'en-US' },
|
||||
{ name: 'British English', value: 'en-GB' },
|
||||
{ name: 'LATAM Spanish', value: 'en-USes-419' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Catalan', value: 'ca-ES', version: 'v2' },
|
||||
{ name: 'Brazilian Portuguese', value: 'pt-BR' },
|
||||
{ name: 'French', value: 'fr', version: 'v1' },
|
||||
{ name: 'Canadian French', value: 'fr-CA', version: 'v1' },
|
||||
{ name: 'German', value: 'de', version: 'v1' },
|
||||
{ name: 'Italian', value: 'it', version: 'v1' },
|
||||
{ name: 'Turkish', value: 'tr', version: 'v1' },
|
||||
{ name: 'Japanese', value: 'ja', version: 'v1' },
|
||||
];
|
||||
8
lib/utils/speech-data/stt-voxist.js
Normal file
8
lib/utils/speech-data/stt-voxist.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = [
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Spanish', value: 'sp' },
|
||||
];
|
||||
301
lib/utils/speech-data/tts-cartesia.js
Normal file
301
lib/utils/speech-data/tts-cartesia.js
Normal file
@@ -0,0 +1,301 @@
|
||||
/* eslint-disable max-len */
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en',
|
||||
name: 'English',
|
||||
voices: [
|
||||
{
|
||||
value: '79f8b5fb-2cc8-479a-80df-29f7a7cf1a3e',
|
||||
name: 'Nonfiction Man - This voice is smooth, confident, and resonant, perfect for narrating educational content',
|
||||
},
|
||||
{
|
||||
value: 'e00d0e4c-a5c8-443f-a8a3-473eb9a62355',
|
||||
name: 'Friendly Sidekick - This voice is friendly and supportive, designed for voicing characters in games and videos',
|
||||
},
|
||||
{
|
||||
value: '3b554273-4299-48b9-9aaf-eefd438e3941',
|
||||
name: 'Indian Lady - This voice is young, rich, and curious, perfect for a narrator or fictional character',
|
||||
},
|
||||
{
|
||||
value: '71a7ad14-091c-4e8e-a314-022ece01c121',
|
||||
name: 'British Reading Lady - This is a calm and elegant voice with a British accent, perfect for storytelling and narration',
|
||||
},
|
||||
{
|
||||
value: '4d2fd738-3b3d-4368-957a-bb4805275bd9',
|
||||
name: 'British Narration Lady - This is a neutral voice with a British accent, perfect for narrations ',
|
||||
},
|
||||
{
|
||||
value: '15a9cd88-84b0-4a8b-95f2-5d583b54c72e',
|
||||
name: 'Reading Lady - This voice is monotone and deliberate, perfect for a slower-paced and more serious reading voice',
|
||||
},
|
||||
{
|
||||
value: 'd46abd1d-2d02-43e8-819f-51fb652c1c61',
|
||||
name: 'Newsman - This voice is neutral and educational, perfect for a news anchor',
|
||||
},
|
||||
{
|
||||
value: '2ee87190-8f84-4925-97da-e52547f9462c',
|
||||
name: 'Child - This voice is young and full, perfect for a child',
|
||||
},
|
||||
{
|
||||
value: 'cd17ff2d-5ea4-4695-be8f-42193949b946',
|
||||
name: 'Meditation Lady - This voice is calm, soothing, and relaxing, perfect for meditation',
|
||||
},
|
||||
{
|
||||
value: '5345cf08-6f37-424d-a5d9-8ae1101b9377',
|
||||
name: 'Maria - This voice is laid back, natural, and conversational, like you\'re catching up with a good friend',
|
||||
},
|
||||
{
|
||||
value: '41534e16-2966-4c6b-9670-111411def906',
|
||||
name: '1920\'s Radioman - This voice is energetic and confident, great for an entertainer or radio host',
|
||||
},
|
||||
{
|
||||
value: 'bf991597-6c13-47e4-8411-91ec2de5c466',
|
||||
name: 'Newslady - This voice is authoritative and educational, perfect for a news anchor',
|
||||
},
|
||||
{
|
||||
value: '00a77add-48d5-4ef6-8157-71e5437b282d',
|
||||
name: 'Calm Lady - This voice is calm and nurturing, perfect for a narrator',
|
||||
},
|
||||
{
|
||||
value: '156fb8d2-335b-4950-9cb3-a2d33befec77',
|
||||
name: 'Helpful Woman - This voice is friendly and conversational, designed for customer support agents and casual conversations',
|
||||
},
|
||||
{
|
||||
value: '36b42fcb-60c5-4bec-b077-cb1a00a92ec6',
|
||||
name: 'Pilot over Intercom - This voice sounds like a British Pilot character speaking over an Intercom',
|
||||
},
|
||||
{
|
||||
value: 'f146dcec-e481-45be-8ad2-96e1e40e7f32',
|
||||
name: 'Reading Man - Male with calm narrational voice.',
|
||||
},
|
||||
{
|
||||
value: '34575e71-908f-4ab6-ab54-b08c95d6597d',
|
||||
name: 'New York Man - This voice is compelling and husky, with a New York accent, perfect for sales pitches and motivational content',
|
||||
},
|
||||
{
|
||||
value: 'a0e99841-438c-4a64-b679-ae501e7d6091',
|
||||
name: 'Barbershop Man - This voice is smooth and relaxing, perfect for a casual conversation',
|
||||
},
|
||||
{
|
||||
value: '638efaaa-4d0c-442e-b701-3fae16aad012',
|
||||
name: 'Indian Man - This voice is smooth with an Indian accent, perfect for a narrator',
|
||||
},
|
||||
{
|
||||
value: '41f3c367-e0a8-4a85-89e0-c27bae9c9b6d',
|
||||
name: 'Australian Customer Support Man - This voice is warm with an Australian accent, perfect for customer support agents',
|
||||
},
|
||||
{
|
||||
value: '421b3369-f63f-4b03-8980-37a44df1d4e8',
|
||||
name: 'Friendly Australian Man - This voice is rich and deep, with an Australian accent, perfect for casual conversations with a friend',
|
||||
},
|
||||
{
|
||||
value: 'b043dea0-a007-4bbe-a708-769dc0d0c569',
|
||||
name: 'Wise Man - This is a deep and deliberate voice, suited for educational content and conversations',
|
||||
},
|
||||
{
|
||||
value: '69267136-1bdc-412f-ad78-0caad210fb40',
|
||||
name: 'Friendly Reading Man - This voice is energetic and friendly, like having your friend read his favorite book to you',
|
||||
},
|
||||
{
|
||||
value: 'a167e0f3-df7e-4d52-a9c3-f949145efdab',
|
||||
name: 'Customer Support Man - This voice is clear and calm, perfect for a call center',
|
||||
},
|
||||
{
|
||||
value: '4f8651b0-bbbd-46ac-8b37-5168c5923303',
|
||||
name: 'Kentucky Woman - This voice is energetic and upbeat, with a slight Kentucky accent, perfect for speeches and rallies',
|
||||
},
|
||||
{
|
||||
value: 'daf747c6-6bc2-4083-bd59-aa94dce23f5d',
|
||||
name: 'Middle Eastern Woman - This voice is clear with a Middle Eastern Accent, perfect for a narrator',
|
||||
},
|
||||
{
|
||||
value: '694f9389-aac1-45b6-b726-9d9369183238',
|
||||
name: 'Sarah - This voice is natural and expressive with an American accent, perfect for a wide range of conversational use cases including customer support, sales, reception, and more.',
|
||||
},
|
||||
{
|
||||
value: '794f9389-aac1-45b6-b726-9d9369183238',
|
||||
name: 'Sarah Curious - This voice is similar to Sarah, but has improved emphasis for questions.',
|
||||
},
|
||||
{
|
||||
value: '21b81c14-f85b-436d-aff5-43f2e788ecf8',
|
||||
name: 'Laidback Woman - This voice is laid back and husky, with a slight Californian accent',
|
||||
},
|
||||
{
|
||||
value: 'a3520a8f-226a-428d-9fcd-b0a4711a6829',
|
||||
name: 'Reflective Woman - This voice is even, full, and reflective, perfect for a young narrator for an audiobook or movie',
|
||||
},
|
||||
{
|
||||
value: '829ccd10-f8b3-43cd-b8a0-4aeaa81f3b30',
|
||||
name: 'Customer Support Lady - This voice is polite and helpful, perfect for customer support agents',
|
||||
},
|
||||
{
|
||||
value: '79a125e8-cd45-4c13-8a67-188112f4dd22',
|
||||
name: 'British Lady - This voice is elegant with a slight British accent, perfect for storytelling and narrating',
|
||||
},
|
||||
{
|
||||
value: 'c8605446-247c-4d39-acd4-8f4c28aa363c',
|
||||
name: 'Wise Lady - This voice is wise and authoritative, perfect for a confident narrator',
|
||||
},
|
||||
{
|
||||
value: '8985388c-1332-4ce7-8d55-789628aa3df4',
|
||||
name: 'Australian Narrator Lady - This voice is even and neutral, with an Australian accent, designed for narrating content and stories',
|
||||
},
|
||||
{
|
||||
value: 'ff1bb1a9-c582-4570-9670-5f46169d0fc8',
|
||||
name: 'Indian Customer Support Lady - This voice is clear and polite, with an Indian accent, suitable for customer support agents',
|
||||
},
|
||||
{
|
||||
value: '820a3788-2b37-4d21-847a-b65d8a68c99a',
|
||||
name: 'Salesman - This voice is smooth and persuasive, perfect for sales pitches and phone conversations',
|
||||
},
|
||||
{
|
||||
value: 'f114a467-c40a-4db8-964d-aaba89cd08fa',
|
||||
name: 'Yogaman - This voice is calm, soothing, and stable, perfect for a yoga instructor',
|
||||
},
|
||||
{
|
||||
value: 'c45bc5ec-dc68-4feb-8829-6e6b2748095d',
|
||||
name: 'Movieman - This voice is deep, resonant, and assertive, perfect for a movie narrator',
|
||||
},
|
||||
{
|
||||
value: '87748186-23bb-4158-a1eb-332911b0b708',
|
||||
name: 'Wizardman - This voice is wise and mysterious, perfect for a Wizard character',
|
||||
},
|
||||
{
|
||||
value: '043cfc81-d69f-4bee-ae1e-7862cb358650',
|
||||
name: 'Australian Woman - This voice is deliberate and confident, with a slight Australian accent, perfect for inspiring characters in videos and stories',
|
||||
},
|
||||
{
|
||||
value: '5619d38c-cf51-4d8e-9575-48f61a280413',
|
||||
name: 'Announcer Man - This voice is deep and inviting, perfect for entertainment and broadcasting content',
|
||||
},
|
||||
{
|
||||
value: '42b39f37-515f-4eee-8546-73e841679c1d',
|
||||
name: 'Wise Guide Man - This voice is deep and deliberate, perfect for inspiring and guiding characters in games and videos',
|
||||
},
|
||||
{
|
||||
value: '565510e8-6b45-45de-8758-13588fbaec73',
|
||||
name: 'Midwestern Man - This voice is neutral and smooth, with a slight midwestern accent, perfect for narrations',
|
||||
},
|
||||
{
|
||||
value: '726d5ae5-055f-4c3d-8355-d9677de68937',
|
||||
name: 'Kentucky Man - This voice is laidback and smooth, with a Kentucky accent, perfect for a casual conversation',
|
||||
},
|
||||
{
|
||||
value: '63ff761f-c1e8-414b-b969-d1833d1c870c',
|
||||
name: 'Confident British Man - This voice is disciplined with a British accent, perfect for a commanding character or narrator',
|
||||
},
|
||||
{
|
||||
value: '98a34ef2-2140-4c28-9c71-663dc4dd7022',
|
||||
name: 'Southern Man - This voice is warm with a Southern accent, perfect for a narrator',
|
||||
},
|
||||
{
|
||||
value: '95856005-0332-41b0-935f-352e296aa0df',
|
||||
name: 'Classy British Man - This voice is light and smooth with a British accent, perfect for casual conversation',
|
||||
},
|
||||
{
|
||||
value: 'ee7ea9f8-c0c1-498c-9279-764d6b56d189',
|
||||
name: 'Polite Man - This voice is polite and conversational, with a slight accent, designed for customer support and casual conversations',
|
||||
},
|
||||
{
|
||||
value: '40104aff-a015-4da1-9912-af950fbec99e',
|
||||
name: 'Alabama Male - This voice has a strong Southern Accent, perfect for conversations and instructional videos',
|
||||
},
|
||||
{
|
||||
value: '13524ffb-a918-499a-ae97-c98c7c4408c4',
|
||||
name: 'Australian Male - This voice is smooth and disciplined, with an Australian Accent, suited for narrating educational content',
|
||||
},
|
||||
{
|
||||
value: '1001d611-b1a8-46bd-a5ca-551b23505334',
|
||||
name: 'Anime Girl - This voice is expressive and has a high pitch, suitable for anime or gaming characters',
|
||||
},
|
||||
{
|
||||
value: 'e3827ec5-697a-4b7c-9704-1a23041bbc51',
|
||||
name: 'Sweet Lady - This voice is sweet and passionate, perfect for a character in a game or book',
|
||||
},
|
||||
{
|
||||
value: 'c2ac25f9-ecc4-4f56-9095-651354df60c0',
|
||||
name: 'Commercial Lady - This voice is inviting, enthusiastic, and relatable, perfect for a commercial or advertisement',
|
||||
},
|
||||
{
|
||||
value: '573e3144-a684-4e72-ac2b-9b2063a50b53',
|
||||
name: 'Teacher Lady - This voice is neutral and clear, perfect for narrating educational content',
|
||||
},
|
||||
{
|
||||
value: '8f091740-3df1-4795-8bd9-dc62d88e5131',
|
||||
name: 'Princess - This voice is light, freindly and has a flourish, perfect for character work in videos and games',
|
||||
},
|
||||
{
|
||||
value: '7360f116-6306-4e9a-b487-1235f35a0f21',
|
||||
name: 'Commercial Man - This voice is upbeat and enthusiastic, perfect for commercials and advertisements',
|
||||
},
|
||||
{
|
||||
value: '03496517-369a-4db1-8236-3d3ae459ddf7',
|
||||
name: 'ASMR Lady - This voice is calming and soft, perfect for guided meditations and soothing content',
|
||||
},
|
||||
{
|
||||
value: '248be419-c632-4f23-adf1-5324ed7dbf1d',
|
||||
name: 'Professional Woman - This voice is neutral and calm, perfect for a call center',
|
||||
},
|
||||
{
|
||||
value: 'bd9120b6-7761-47a6-a446-77ca49132781',
|
||||
name: 'Tutorial Man - This voice is inviting and calming, perfect for tutorials',
|
||||
},
|
||||
{
|
||||
value: '34bde396-9fde-4ebf-ad03-e3a1d1155205',
|
||||
name: 'New York Woman - This voice commands authority, with a New York accent, perfect for a commanding narrator or character',
|
||||
},
|
||||
{
|
||||
value: '11af83e2-23eb-452f-956e-7fee218ccb5c',
|
||||
name: 'Midwestern Woman - This voice is neutral and deliberate, with a midwestern accent, suitable for news broadcasts and narration',
|
||||
},
|
||||
{
|
||||
value: 'ed81fd13-2016-4a49-8fe3-c0d2761695fc',
|
||||
name: 'Sportsman - This voice is energetic and enthusiastic, perfect for a sports broadcaster',
|
||||
},
|
||||
{
|
||||
value: '996a8b96-4804-46f0-8e05-3fd4ef1a87cd',
|
||||
name: 'Storyteller Lady - This voice is neutral and smooth, with a slight Canadian accent, perfect for narrations',
|
||||
},
|
||||
{
|
||||
value: 'fb26447f-308b-471e-8b00-8e9f04284eb5',
|
||||
name: 'Doctor Mischief - This is an expressive character voice, suited to whimsical characters for games and educational content',
|
||||
},
|
||||
{
|
||||
value: '50d6beb4-80ea-4802-8387-6c948fe84208',
|
||||
name: 'The Merchant - This voice is playful and quirky, designed for character work in games and videos',
|
||||
},
|
||||
{
|
||||
value: 'e13cae5c-ec59-4f71-b0a6-266df3c9bb8e',
|
||||
name: 'Madame Mischief - This voice is mischeivious and playful, suitable for voicing characters for kids content and games',
|
||||
},
|
||||
{
|
||||
value: '5c42302c-194b-4d0c-ba1a-8cb485c84ab9',
|
||||
name: 'Female Nurse - This voice is clear and firm, perfect for nurse characters and instructional videos',
|
||||
},
|
||||
{
|
||||
value: 'f9836c6e-a0bd-460e-9d3c-f7299fa60f94',
|
||||
name: 'Southern Woman - This voice is friendly and inviting, with a slight Southern Accent, perfect for conversations and phone calls',
|
||||
},
|
||||
{
|
||||
value: 'a01c369f-6d2d-4185-bc20-b32c225eab70',
|
||||
name: 'British Customer Support Lady - This voice is friendly and polite, with a British accent, perfect for phone conversations',
|
||||
},
|
||||
{
|
||||
value: 'b7d50908-b17c-442d-ad8d-810c63997ed9',
|
||||
name: 'California Girl - This voice is enthusiastic and friendly, perfect for a casual conversation between friends',
|
||||
},
|
||||
{
|
||||
value: 'f785af04-229c-4a7c-b71b-f3194c7f08bb',
|
||||
name: 'John - This voice is natural and empathetic with an American accent, perfect for use cases like demos and customer support calls.',
|
||||
},
|
||||
{
|
||||
value: '729651dc-c6c3-4ee5-97fa-350da1f88600',
|
||||
name: 'Pleasant Man - A pleasant male voice that\'s good for use cases like demos and customer support calls',
|
||||
},
|
||||
{
|
||||
value: '91b4cf29-5166-44eb-8054-30d40ecc8081',
|
||||
name: 'Anna - This voice is natural and expressive with an American accent, perfect for use cases like interviews and customer support calls.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
28
lib/utils/speech-data/tts-deepgram.js
Normal file
28
lib/utils/speech-data/tts-deepgram.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const TtsDeepgramLanguagesVoiceRaw = require('./tts-model-deepgram');
|
||||
|
||||
const languagesVoices = [];
|
||||
|
||||
TtsDeepgramLanguagesVoiceRaw.forEach((data) => {
|
||||
const lang = languagesVoices.find((l) => {
|
||||
return l.value === data.locale;
|
||||
});
|
||||
|
||||
if (!lang) {
|
||||
languagesVoices.push({
|
||||
value: data.locale,
|
||||
name: data.localeName,
|
||||
voices: TtsDeepgramLanguagesVoiceRaw
|
||||
.filter((d) => {
|
||||
return d.locale === data.locale;
|
||||
})
|
||||
.map((d) => {
|
||||
return {
|
||||
value: d.value,
|
||||
name: `${d.name}`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = languagesVoices;
|
||||
152
lib/utils/speech-data/tts-languages-playht.js
Normal file
152
lib/utils/speech-data/tts-languages-playht.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// languages.js
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
name: 'English',
|
||||
value: 'english'
|
||||
},
|
||||
{
|
||||
name: 'Mandarin',
|
||||
value: 'mandarin'
|
||||
},
|
||||
{
|
||||
name: 'Hindi',
|
||||
value: 'hindi'
|
||||
},
|
||||
{
|
||||
name: 'Japanese',
|
||||
value: 'japanese'
|
||||
},
|
||||
{
|
||||
name: 'Korean',
|
||||
value: 'korean'
|
||||
},
|
||||
{
|
||||
name: 'Arabic',
|
||||
value: 'arabic'
|
||||
},
|
||||
{
|
||||
name: 'Spanish',
|
||||
value: 'spanish'
|
||||
},
|
||||
{
|
||||
name: 'French',
|
||||
value: 'french'
|
||||
},
|
||||
{
|
||||
name: 'Italian',
|
||||
value: 'italian'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
value: 'portuguese'
|
||||
},
|
||||
{
|
||||
name: 'German',
|
||||
value: 'german'
|
||||
},
|
||||
{
|
||||
name: 'Dutch',
|
||||
value: 'dutch'
|
||||
},
|
||||
{
|
||||
name: 'Swedish',
|
||||
value: 'swedish'
|
||||
},
|
||||
{
|
||||
name: 'Czech',
|
||||
value: 'czech'
|
||||
},
|
||||
{
|
||||
name: 'Polish',
|
||||
value: 'polish'
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
value: 'russian'
|
||||
},
|
||||
{
|
||||
name: 'Bulgarian',
|
||||
value: 'bulgarian'
|
||||
},
|
||||
{
|
||||
name: 'Hebrew',
|
||||
value: 'hebrew'
|
||||
},
|
||||
{
|
||||
name: 'Greek',
|
||||
value: 'greek'
|
||||
},
|
||||
{
|
||||
name: 'Turkish',
|
||||
value: 'turkish'
|
||||
},
|
||||
{
|
||||
name: 'Afrikaans',
|
||||
value: 'afrikaans'
|
||||
},
|
||||
{
|
||||
name: 'Xhosa',
|
||||
value: 'xhosa'
|
||||
},
|
||||
{
|
||||
name: 'Tagalog',
|
||||
value: 'tagalog'
|
||||
},
|
||||
{
|
||||
name: 'Malay',
|
||||
value: 'malay'
|
||||
},
|
||||
{
|
||||
name: 'Indonesian',
|
||||
value: 'indonesian'
|
||||
},
|
||||
{
|
||||
name: 'Bengali',
|
||||
value: 'bengali'
|
||||
},
|
||||
{
|
||||
name: 'Serbian',
|
||||
value: 'serbian'
|
||||
},
|
||||
{
|
||||
name: 'Thai',
|
||||
value: 'thai'
|
||||
},
|
||||
{
|
||||
name: 'Urdu',
|
||||
value: 'urdu'
|
||||
},
|
||||
{
|
||||
name: 'Croatian',
|
||||
value: 'croatian'
|
||||
},
|
||||
{
|
||||
name: 'Hungarian',
|
||||
value: 'hungarian'
|
||||
},
|
||||
{
|
||||
name: 'Danish',
|
||||
value: 'danish'
|
||||
},
|
||||
{
|
||||
name: 'Amharic',
|
||||
value: 'amharic'
|
||||
},
|
||||
{
|
||||
name: 'Albanian',
|
||||
value: 'albanian'
|
||||
},
|
||||
{
|
||||
name: 'Catalan',
|
||||
value: 'catalan'
|
||||
},
|
||||
{
|
||||
name: 'Ukrainian',
|
||||
value: 'ukrainian'
|
||||
},
|
||||
{
|
||||
name: 'Galician',
|
||||
value: 'galician'
|
||||
}
|
||||
];
|
||||
35
lib/utils/speech-data/tts-model-cartesia.js
Normal file
35
lib/utils/speech-data/tts-model-cartesia.js
Normal file
@@ -0,0 +1,35 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Sonic',
|
||||
value: 'sonic',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{
|
||||
name: 'Sonic 2',
|
||||
value: 'sonic-2',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{
|
||||
name: 'Sonic Turbo',
|
||||
value: 'sonic-turbo',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{ name: 'Sonic Preview', value: 'sonic-preview', languages: ['en'] },
|
||||
{
|
||||
name: 'Sonic 2024-12-12',
|
||||
value: 'sonic-2024-12-12',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{
|
||||
name: 'Sonic 2024-10-19',
|
||||
value: 'sonic-2024-10-19',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{ name: 'Sonic English', value: 'sonic-english', languages: ['en'] },
|
||||
{
|
||||
name: 'Sonic Multilingual',
|
||||
value: 'sonic-multilingual',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,14 +1,320 @@
|
||||
module.exports = [
|
||||
{ name: 'Asteria English (US) Female', value: 'aura-asteria-en' },
|
||||
{ name: 'Luna English (US) Female', value: 'aura-luna-en' },
|
||||
{ name: 'Stella English (US) Female', value: 'aura-stella-en' },
|
||||
{ name: 'Stella English (UK) Female', value: 'aura-athena-en' },
|
||||
{ name: 'Hera English (US) Female', value: 'aura-hera-en' },
|
||||
{ name: 'Orion English (US) Male', value: 'aura-orion-en' },
|
||||
{ name: 'Arcas English (US) Male', value: 'aura-arcas-en' },
|
||||
{ name: 'Perseus English (US) Male', value: 'aura-perseus-en' },
|
||||
{ name: 'Angus English (Ireland) Male', value: 'aura-angus-en' },
|
||||
{ name: 'Orpheus English (US) Male', value: 'aura-orpheus-en' },
|
||||
{ name: 'Helios English (UK) Male', value: 'aura-helios-en' },
|
||||
{ name: 'Zeus English (US) Male', value: 'aura-zeus-en' },
|
||||
{
|
||||
locale: 'en-ph',
|
||||
localeName: 'English (PH)',
|
||||
name: 'Amalthea English (PH) Female Aura-2',
|
||||
value: 'aura-2-amalthea-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Andromeda English (US) Female Aura-2',
|
||||
value: 'aura-2-andromeda-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Apollo English (US) Male Aura-2',
|
||||
value: 'aura-2-apollo-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Arcas English (US) Male Aura-2',
|
||||
value: 'aura-2-arcas-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Aries English (US) Male Aura-2',
|
||||
value: 'aura-2-aries-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Asteria English (US) Female Aura-2',
|
||||
value: 'aura-2-asteria-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Athena English (US) Female Aura-2',
|
||||
value: 'aura-2-athena-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Atlas English (US) Male Aura-2',
|
||||
value: 'aura-2-atlas-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Aurora English (US) Female Aura-2',
|
||||
value: 'aura-2-aurora-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Callista English (US) Female Aura-2',
|
||||
value: 'aura-2-callista-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Cora English (US) Female Aura-2',
|
||||
value: 'aura-2-cora-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Cordelia English (US) Female Aura-2',
|
||||
value: 'aura-2-cordelia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Delia English (US) Female Aura-2',
|
||||
value: 'aura-2-delia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-gb',
|
||||
localeName: 'English (GB)',
|
||||
name: 'Draco English (GB) Male Aura-2',
|
||||
value: 'aura-2-draco-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Electra English (US) Female Aura-2',
|
||||
value: 'aura-2-electra-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Harmonia English (US) Female Aura-2',
|
||||
value: 'aura-2-harmonia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Helena English (US) Female Aura-2',
|
||||
value: 'aura-2-helena-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Hera English (US) Female Aura-2',
|
||||
value: 'aura-2-hera-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Hermes English (US) Male Aura-2',
|
||||
value: 'aura-2-hermes-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-au',
|
||||
localeName: 'English (AU)',
|
||||
name: 'Hyperion English (AU) Male Aura-2',
|
||||
value: 'aura-2-hyperion-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Iris English (US) Female Aura-2',
|
||||
value: 'aura-2-iris-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Janus English (US) Female Aura-2',
|
||||
value: 'aura-2-janus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Juno English (US) Female Aura-2',
|
||||
value: 'aura-2-juno-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Jupiter English (US) Male Aura-2',
|
||||
value: 'aura-2-jupiter-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Luna English (US) Female Aura-2',
|
||||
value: 'aura-2-luna-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Mars English (US) Male Aura-2',
|
||||
value: 'aura-2-mars-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Minerva English (US) Female Aura-2',
|
||||
value: 'aura-2-minerva-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Neptune English (US) Male Aura-2',
|
||||
value: 'aura-2-neptune-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Odysseus English (US) Male Aura-2',
|
||||
value: 'aura-2-odysseus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Ophelia English (US) Female Aura-2',
|
||||
value: 'aura-2-ophelia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orion English (US) Male Aura-2',
|
||||
value: 'aura-2-orion-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orpheus English (US) Male Aura-2',
|
||||
value: 'aura-2-orpheus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-gb',
|
||||
localeName: 'English (GB)',
|
||||
name: 'Pandora English (GB) Female Aura-2',
|
||||
value: 'aura-2-pandora-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Phoebe English (US) Female Aura-2',
|
||||
value: 'aura-2-phoebe-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Pluto English (US) Male Aura-2',
|
||||
value: 'aura-2-pluto-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Saturn English (US) Male Aura-2',
|
||||
value: 'aura-2-saturn-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Selene English (US) Female Aura-2',
|
||||
value: 'aura-2-selene-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Thalia English (US) Female Aura-2',
|
||||
value: 'aura-2-thalia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Theia English (US) Female Aura-2',
|
||||
value: 'aura-2-theia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Vesta English (US) Female Aura-2',
|
||||
value: 'aura-2-vesta-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Zeus English (US) Male Aura-2',
|
||||
value: 'aura-2-zeus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Asteria English (US) Female Aura-1',
|
||||
value: 'aura-asteria-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Luna English (US) Female Aura-1',
|
||||
value: 'aura-luna-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Stella English (US) Female Aura-1',
|
||||
value: 'aura-stella-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-GB',
|
||||
localeName: 'English (UK)',
|
||||
name: 'Stella English (UK) Female Aura-1',
|
||||
value: 'aura-athena-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Hera English (US) Female Aura-1',
|
||||
value: 'aura-hera-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orion English (US) Male Aura-1',
|
||||
value: 'aura-orion-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Arcas English (US) Male Aura-1',
|
||||
value: 'aura-arcas-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Perseus English (US) Male Aura-1',
|
||||
value: 'aura-perseus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-IE',
|
||||
localeName: 'English (Ireland)',
|
||||
name: 'Angus English (Ireland) Male Aura-1',
|
||||
value: 'aura-angus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orpheus English (US) Male Aura-1',
|
||||
value: 'aura-orpheus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-gb',
|
||||
localeName: 'English (US)',
|
||||
name: 'Helios English (GB) Male Aura-1',
|
||||
value: 'aura-helios-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Zeus English (US) Male Aura-1',
|
||||
value: 'aura-zeus-en'
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
module.exports = [
|
||||
{ name: 'Turbo v2', value: 'eleven_turbo_v2' },
|
||||
{ name: 'Multilingual v2', value: 'eleven_multilingual_v2' },
|
||||
{ name: 'Turbo v2.5', value: 'eleven_turbo_v2_5' },
|
||||
{ name: 'Flash v2', value: 'eleven_flash_v2' },
|
||||
{ name: 'Flash v2.5', value: 'eleven_flash_v2_5' },
|
||||
{ name: 'Multilingual v1', value: 'eleven_multilingual_v1' },
|
||||
{ name: 'Multilingual v2', value: 'eleven_multilingual_v2' },
|
||||
{ name: 'Multilingual STS v2', value: 'eleven_multilingual_sts_v2' },
|
||||
{ name: 'English v1', value: 'eleven_monolingual_v1' },
|
||||
{ name: 'English v2', value: 'eleven_english_sts_v2' },
|
||||
];
|
||||
|
||||
6
lib/utils/speech-data/tts-model-openai.js
Normal file
6
lib/utils/speech-data/tts-model-openai.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = [
|
||||
{ name: 'TTS-1', value: 'tts-1' },
|
||||
{ name: 'TTS-1-HD', value: 'tts-1-hd' },
|
||||
{ name: 'GPT-4o-Mini-TTS', value: 'gpt-4o-mini-tts' },
|
||||
];
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
module.exports = [
|
||||
{ name: 'Play3.0', value: 'Play3.0' },
|
||||
{ name: 'PlayHT2.0-turbo', value: 'PlayHT2.0-turbo' },
|
||||
{ name: 'PlayHT2.0', value: 'PlayHT2.0' },
|
||||
{ name: 'PlayHT1.0', value: 'PlayHT1.0' },
|
||||
{ name: 'Dialog 1.0', value: 'PlayDialog'}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
module.exports = [
|
||||
{ name: 'Arcana', value: 'arcana' },
|
||||
{ name: 'Mist', value: 'mist' },
|
||||
{ name: 'Mistv2', value: 'mistv2' },
|
||||
{ name: 'V1', value: 'v1' },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = [
|
||||
{ name: 'TTS-1', value: 'tts-1' },
|
||||
{ name: 'TTS-1-HD', value: 'tts-1-hd' },
|
||||
{ name: 'GPT-4o mini TTS', value: 'gpt-4o-mini-tts' },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,16 +1,66 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'English',
|
||||
voices: [
|
||||
{
|
||||
value: 'English-US.Female-1',
|
||||
name: 'Female',
|
||||
},
|
||||
{
|
||||
value: 'English-US.Male-1',
|
||||
name: 'Male',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
module.exports = (() => {
|
||||
const voices = [
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Female-1', name: 'Female Magpie' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Calm', name: 'Female Calm' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Fearful', name: 'Female Fearful' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Happy', name: 'Female Happy' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Angry', name: 'Female Angry' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Neutral', name: 'Female Neutral' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Calm', name: 'Male Calm' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Fearful', name: 'Male Fearful' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Happy', name: 'Male Happy' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Neutral', name: 'Male Neutral' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Angry', name: 'Male Angry' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Disgusted', name: 'Male Disgusted' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Male-1', name: 'Male Magpie' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Male-1', name: 'Male Magpie' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Female-1', name: 'Female Magpie' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Angry', name: 'Female Angry' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Calm', name: 'Female Calm' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Disgust', name: 'Female Disgust' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Sad', name: 'Female Sad' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Happy', name: 'Female Happy' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Fearful', name: 'Female Fearful' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Neutral', name: 'Female Neutral' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Neutral', name: 'Male Neutral' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Angry', name: 'Male Angry' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Calm', name: 'Male Calm' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Sad', name: 'Male Sad' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Male-1', name: 'Male Magpie' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Female-1', name: 'Female Magpie' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Neutral', name: 'Female Neutral' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Neutral', name: 'Male Neutral' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Angry', name: 'Male Angry' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Angry', name: 'Female Angry' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Happy', name: 'Female Happy' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Happy', name: 'Male Happy' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Calm', name: 'Female Calm' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Calm', name: 'Male Calm' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Pleasant_Surprise', name: 'Female Pleasant Surprise' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Pleasant_Surprise', name: 'Male Pleasant Surprise' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Sad', name: 'Female Sad' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Sad', name: 'Male Sad' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Disgust', name: 'Male Disgust' }
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'English',
|
||||
voices: [
|
||||
{ value: 'English-US.Female-1', name: 'Female' },
|
||||
{ value: 'English-US.Male-1', name: 'Male' },
|
||||
...voices.filter((voice) => voice.value.includes('EN-US'))]
|
||||
},
|
||||
{
|
||||
value: 'fr-FR',
|
||||
name: 'French',
|
||||
voices: voices.filter((voice) => voice.value.includes('FR-FR'))
|
||||
},
|
||||
{
|
||||
value: 'es-US',
|
||||
name: 'Spanish',
|
||||
voices: voices.filter((voice) => voice.value.includes('ES-US'))
|
||||
}
|
||||
];
|
||||
})();
|
||||
|
||||
46
lib/utils/speech-data/tts-verbio.js
Normal file
46
lib/utils/speech-data/tts-verbio.js
Normal file
@@ -0,0 +1,46 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'es-ES',
|
||||
name: 'Castilian Spanish',
|
||||
voices: [
|
||||
{
|
||||
value: 'david_es_es',
|
||||
name: 'David-Male',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es-PE',
|
||||
name: 'Peruvian Spanish',
|
||||
voices: [
|
||||
{
|
||||
value: 'miguel_es_pe',
|
||||
name: 'Miguel-Male',
|
||||
},
|
||||
{
|
||||
value: 'luz_es_pe',
|
||||
name: 'Luz-Female',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'pt-BR',
|
||||
name: 'Brazilian Portuguese',
|
||||
voices: [
|
||||
{
|
||||
value: 'bel_pt_br',
|
||||
name: 'Bel-Female',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ca-ES',
|
||||
name: 'Catalan',
|
||||
voices: [
|
||||
{
|
||||
value: 'anna_ca',
|
||||
name: 'Anna-Female',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -3,10 +3,10 @@ const { TranscribeClient, ListVocabulariesCommand } = require('@aws-sdk/client-t
|
||||
const { Deepgram } = require('@deepgram/sdk');
|
||||
const sdk = require('microsoft-cognitiveservices-speech-sdk');
|
||||
const { SpeechClient } = require('@soniox/soniox-node');
|
||||
const bent = require('bent');
|
||||
const fs = require('fs');
|
||||
const { AssemblyAI } = require('assemblyai');
|
||||
const {decrypt, obscureKey} = require('./encrypt-decrypt');
|
||||
const { RealtimeSession } = require('speechmatics');
|
||||
|
||||
const TtsGoogleLanguagesVoices = require('./speech-data/tts-google');
|
||||
const TtsAwsLanguagesVoices = require('./speech-data/tts-aws');
|
||||
@@ -18,12 +18,18 @@ const TtsNvidiaLanguagesVoices = require('./speech-data/tts-nvidia');
|
||||
const TtsElevenlabsLanguagesVoices = require('./speech-data/tts-elevenlabs');
|
||||
const TtsWhisperLanguagesVoices = require('./speech-data/tts-whisper');
|
||||
const TtsPlayHtLanguagesVoices = require('./speech-data/tts-playht');
|
||||
const TtsVerbioLanguagesVoices = require('./speech-data/tts-verbio');
|
||||
const ttsCartesia = require('./speech-data/tts-cartesia');
|
||||
|
||||
const TtsModelDeepgram = require('./speech-data/tts-model-deepgram');
|
||||
const TtsLanguagesDeepgram = require('./speech-data/tts-deepgram');
|
||||
const TtsModelElevenLabs = require('./speech-data/tts-model-elevenlabs');
|
||||
const TtsModelWhisper = require('./speech-data/tts-model-whisper');
|
||||
const TtsModelPlayHT = require('./speech-data/tts-model-playht');
|
||||
const ttsLanguagesPlayHt = require('./speech-data/tts-languages-playht');
|
||||
const TtsModelRimelabs = require('./speech-data/tts-model-rimelabs');
|
||||
const TtsModelCartesia = require('./speech-data/tts-model-cartesia');
|
||||
const TtsModelOpenai = require('./speech-data/tts-model-openai');
|
||||
|
||||
const SttGoogleLanguagesVoices = require('./speech-data/stt-google');
|
||||
const SttAwsLanguagesVoices = require('./speech-data/stt-aws');
|
||||
@@ -34,7 +40,21 @@ const SttIbmLanguagesVoices = require('./speech-data/stt-ibm');
|
||||
const SttNvidiaLanguagesVoices = require('./speech-data/stt-nvidia');
|
||||
const SttCobaltLanguagesVoices = require('./speech-data/stt-cobalt');
|
||||
const SttSonioxLanguagesVoices = require('./speech-data/stt-soniox');
|
||||
const SttSpeechmaticsLanguagesVoices = require('./speech-data/stt-speechmatics');
|
||||
const SttAssemblyaiLanguagesVoices = require('./speech-data/stt-assemblyai');
|
||||
const SttVoxistLanguagesVoices = require('./speech-data/stt-voxist');
|
||||
const SttVerbioLanguagesVoices = require('./speech-data/stt-verbio');
|
||||
const SttOpenaiLanguagesVoices = require('./speech-data/stt-openai');
|
||||
|
||||
|
||||
const SttModelOpenai = require('./speech-data/stt-model-openai');
|
||||
const sttModelDeepgram = require('./speech-data/stt-model-deepgram');
|
||||
|
||||
function capitalizeFirst(str) {
|
||||
if (!str) return str;
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
|
||||
const testSonioxStt = async(logger, credentials) => {
|
||||
const api_key = credentials;
|
||||
@@ -52,6 +72,61 @@ const testSonioxStt = async(logger, credentials) => {
|
||||
});
|
||||
};
|
||||
|
||||
const testSpeechmaticsStt = async(logger, credentials) => {
|
||||
const {api_key, speechmatics_stt_uri} = credentials;
|
||||
return new Promise(async(resolve, reject) => {
|
||||
try {
|
||||
const session = new RealtimeSession({ apiKey: api_key, realtimeUrl: speechmatics_stt_uri });
|
||||
let transcription = '';
|
||||
session.addListener('Error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
session.addListener('AddTranscript', (message) => {
|
||||
transcription += message.metadata.transcript;
|
||||
});
|
||||
|
||||
session.addListener('EndOfTranscript', () => {
|
||||
resolve(transcription);
|
||||
});
|
||||
|
||||
session
|
||||
.start({
|
||||
transcription_config: {
|
||||
language: 'en',
|
||||
operating_point: 'enhanced',
|
||||
enable_partials: true,
|
||||
max_delay: 2,
|
||||
},
|
||||
audio_format: { type: 'file' },
|
||||
})
|
||||
.then(() => {
|
||||
//prepare file stream
|
||||
const fileStream = fs.createReadStream(`${__dirname}/../../data/test_audio.wav`);
|
||||
|
||||
//send it
|
||||
fileStream.on('data', (sample) => {
|
||||
session.sendAudio(sample);
|
||||
});
|
||||
|
||||
//end the session
|
||||
fileStream.on('end', () => {
|
||||
session.stop();
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.info({error}, 'failed to get speechmatics transcript');
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testNuanceTts = async(logger, getTtsVoices, credentials) => {
|
||||
const voices = await getTtsVoices({vendor: 'nuance', credentials});
|
||||
return voices;
|
||||
@@ -90,8 +165,8 @@ const testGoogleStt = async(logger, credentials) => {
|
||||
};
|
||||
|
||||
const testDeepgramStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
const deepgram = new Deepgram(api_key);
|
||||
const {api_key, deepgram_stt_uri, deepgram_stt_use_tls} = credentials;
|
||||
const deepgram = new Deepgram(api_key, deepgram_stt_uri, deepgram_stt_uri && deepgram_stt_use_tls);
|
||||
|
||||
const mimetype = 'audio/wav';
|
||||
const source = {
|
||||
@@ -117,9 +192,10 @@ const testDeepgramStt = async(logger, credentials) => {
|
||||
};
|
||||
|
||||
const testMicrosoftStt = async(logger, credentials) => {
|
||||
const {api_key, region} = credentials;
|
||||
|
||||
const speechConfig = sdk.SpeechConfig.fromSubscription(api_key, region);
|
||||
const {api_key, region, use_custom_stt, custom_stt_endpoint_url} = credentials;
|
||||
const speechConfig = use_custom_stt ? sdk.SpeechConfig.fromEndpoint(
|
||||
new URL(custom_stt_endpoint_url), api_key) :
|
||||
sdk.SpeechConfig.fromSubscription(api_key, region);
|
||||
const audioConfig = sdk.AudioConfig.fromWavFileInput(fs.readFileSync(`${__dirname}/../../data/test_audio.wav`));
|
||||
speechConfig.speechRecognitionLanguage = 'en-US';
|
||||
|
||||
@@ -180,7 +256,10 @@ const testAwsStt = async(logger, getAwsAuthToken, credentials) => {
|
||||
} else if (roleArn) {
|
||||
client = new TranscribeClient({
|
||||
region,
|
||||
credentials: await getAwsAuthToken(null, null, region, roleArn),
|
||||
credentials: await getAwsAuthToken({
|
||||
region,
|
||||
roleArn
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
client = new TranscribeClient({region});
|
||||
@@ -189,7 +268,7 @@ const testAwsStt = async(logger, getAwsAuthToken, credentials) => {
|
||||
const response = await client.send(command);
|
||||
return response;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}');
|
||||
logger.info({err}, 'testAwsStt - failed to list voices for region ${region}');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -202,7 +281,8 @@ const testMicrosoftTts = async(logger, synthAudio, credentials) => {
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
voice: 'en-US-JennyMultilingualNeural',
|
||||
text: 'Hi there and welcome to jambones!'
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -213,44 +293,46 @@ const testMicrosoftTts = async(logger, synthAudio, credentials) => {
|
||||
|
||||
const testWellSaidTts = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
try {
|
||||
const post = bent('https://api.wellsaidlabs.com', 'POST', 'buffer', {
|
||||
const response = await fetch('https://api.wellsaidlabs.com/v1/tts/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Api-Key': api_key,
|
||||
'Accept': 'audio/mpeg',
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
const mp3 = await post('/v1/tts/stream', {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: 'Hello, world',
|
||||
speaker_id: '3'
|
||||
});
|
||||
return mp3;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'testWellSaidTts returned error');
|
||||
throw err;
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to synthesize speech');
|
||||
}
|
||||
return response.body;
|
||||
};
|
||||
|
||||
const testElevenlabs = async(logger, credentials) => {
|
||||
const {api_key, model_id} = credentials;
|
||||
try {
|
||||
const post = bent('https://api.elevenlabs.io', 'POST', 'buffer', {
|
||||
const response = await fetch('https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'xi-api-key': api_key,
|
||||
'Accept': 'audio/mpeg',
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
const mp3 = await post('/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM', {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: 'Hello',
|
||||
model_id,
|
||||
voice_settings: {
|
||||
stability: 0.5,
|
||||
similarity_boost: 0.5
|
||||
}
|
||||
});
|
||||
return mp3;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synthEvenlabs returned error');
|
||||
throw err;
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to synthesize speech');
|
||||
}
|
||||
return response.body;
|
||||
};
|
||||
|
||||
const testPlayHT = async(logger, synthAudio, credentials) => {
|
||||
@@ -263,9 +345,10 @@ const testPlayHT = async(logger, synthAudio, credentials) => {
|
||||
{
|
||||
vendor: 'playht',
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
language: 'english',
|
||||
voice: 's3://voice-cloning-zero-shot/d9ff78ba-d016-47f6-b0ef-dd630f59414e/female-cs/manifest.json',
|
||||
text: 'Hi there and welcome to jambones!'
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
// Test if playHT can fetch voices
|
||||
@@ -286,13 +369,14 @@ const testRimelabs = async(logger, synthAudio, credentials) => {
|
||||
{
|
||||
vendor: 'rimelabs',
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
language: 'eng',
|
||||
voice: 'amber',
|
||||
text: 'Hi there and welcome to jambones!'
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth Playht returned error');
|
||||
logger.info({err}, 'synth rimelabs returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -305,11 +389,12 @@ const testWhisper = async(logger, synthAudio, credentials) => {
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
voice: 'alloy',
|
||||
text: 'Hi there and welcome to jambones!'
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synthEvenlabs returned error');
|
||||
logger.info({err}, 'synth whisper returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -321,7 +406,8 @@ const testDeepgramTTS = async(logger, synthAudio, credentials) => {
|
||||
vendor: 'deepgram',
|
||||
credentials,
|
||||
model: 'aura-asteria-en',
|
||||
text: 'Hi there and welcome to jambones!'
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -364,6 +450,81 @@ const testWellSaidStt = async(logger, credentials) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const testVerbioTts = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio(
|
||||
{
|
||||
increment: () => {},
|
||||
histogram: () => {}
|
||||
},
|
||||
{
|
||||
vendor: 'verbio',
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
voice: 'tommy_en-us',
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth Verbio returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
const testVerbioStt = async(logger, getVerbioAccessToken, credentials) => {
|
||||
const token = await getVerbioAccessToken(credentials);
|
||||
const response = await fetch('https://us.rest.speechcenter.verbio.com/api/v1/recognize?language=en-US&version=V1', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.access_token}`,
|
||||
'User-Agent': 'jambonz',
|
||||
'Content-Type': 'audio/wav'
|
||||
},
|
||||
body: fs.readFileSync(`${__dirname}/../../data/test_audio.wav`)
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({Error: await response.text()}, 'Error transcribing speech');
|
||||
throw new Error('failed to transcribe speech');
|
||||
}
|
||||
};
|
||||
|
||||
const testOpenAiStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
try {
|
||||
// Create a FormData object to properly format the multipart request
|
||||
const formData = new FormData();
|
||||
|
||||
// Add the audio file as 'file' field
|
||||
const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`);
|
||||
const blob = new Blob([audioBuffer], { type: 'audio/wav' });
|
||||
formData.append('file', blob, 'audio.wav');
|
||||
|
||||
// Add the model parameter (required by OpenAI)
|
||||
formData.append('model', 'whisper-1');
|
||||
|
||||
// Make the request using fetch
|
||||
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${api_key}`,
|
||||
'User-Agent': 'jambonz'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenAI API error: ${response.status} ${(await response.json()).error?.message}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
logger.debug({json}, 'successfully speech to text from OpenAI');
|
||||
return json;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'OpenAI speech-to-text request failed');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const testAssemblyStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
|
||||
@@ -390,6 +551,21 @@ const testAssemblyStt = async(logger, credentials) => {
|
||||
});
|
||||
};
|
||||
|
||||
const testVoxistStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
const response = await fetch('https://api-asr.voxist.com/clients', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'x-lvl-key': api_key
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving clients');
|
||||
throw new Error('failed to get clients');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const getSpeechCredential = (credential, logger) => {
|
||||
const {vendor} = credential;
|
||||
logger.info(
|
||||
@@ -465,6 +641,8 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.deepgram_stt_uri = o.deepgram_stt_uri;
|
||||
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
|
||||
obj.deepgram_tts_uri = o.deepgram_tts_uri;
|
||||
obj.model_id = o.model_id;
|
||||
}
|
||||
else if ('ibm' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -482,6 +660,10 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
} else if ('soniox' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
} else if ('speechmatics' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.speechmatics_stt_uri = o.speechmatics_stt_uri;
|
||||
} else if ('elevenlabs' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
@@ -492,6 +674,12 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.user_id = o.user_id;
|
||||
obj.voice_engine = o.voice_engine;
|
||||
obj.playht_tts_uri = o.playht_tts_uri;
|
||||
obj.options = o.options;
|
||||
} else if ('cartesia' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
obj.options = o.options;
|
||||
} else if ('rimelabs' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -503,13 +691,26 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.auth_token = isObscureKey ? obscureKey(o.auth_token) : o.auth_token;
|
||||
obj.custom_stt_url = o.custom_stt_url;
|
||||
obj.custom_tts_url = o.custom_tts_url;
|
||||
obj.custom_tts_streaming_url = o.custom_tts_streaming_url;
|
||||
} else if ('assemblyai' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
} else if ('voxist' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
} else if ('whisper' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
} else if ('openai' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
} else if ('verbio' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.client_id = o.client_id;
|
||||
obj.client_secret = isObscureKey ? obscureKey(o.client_secret) : o.client_secret;
|
||||
obj.engine_version = o.engine_version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,8 +765,18 @@ async function getLanguagesAndVoicesForVendor(logger, vendor, credential, getTts
|
||||
return await getLanguagesVoicesForRimelabs(credential, getTtsVoices, logger);
|
||||
case 'assemblyai':
|
||||
return await getLanguagesVoicesForAssemblyAI(credential, getTtsVoices, logger);
|
||||
case 'voxist':
|
||||
return await getLanguagesVoicesForVoxist(credential, getTtsVoices, logger);
|
||||
case 'whisper':
|
||||
return await getLanguagesVoicesForWhisper(credential, getTtsVoices, logger);
|
||||
case 'openai':
|
||||
return await getLanguagesVoicesForOpenAi(credential, getTtsVoices, logger);
|
||||
case 'verbio':
|
||||
return await getLanguagesVoicesForVerbio(credential, getTtsVoices, logger);
|
||||
case 'speechmatics':
|
||||
return await getLanguagesVoicesForSpeechmatics(credential, getTtsVoices, logger);
|
||||
case 'cartesia':
|
||||
return await getLanguagesVoicesForCartesia(credential, getTtsVoices, logger);
|
||||
default:
|
||||
logger.info(`invalid vendor ${vendor}, return empty result`);
|
||||
throw new Error(`Invalid vendor ${vendor}`);
|
||||
@@ -611,17 +822,19 @@ async function getLanguagesVoicesForAws(credential, getTtsVoices, logger) {
|
||||
|
||||
async function getLanguagesVoicesForMicrosoft(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
try {
|
||||
const get = bent('https://westus.tts.speech.microsoft.com', 'GET', 'json', {
|
||||
'Ocp-Apim-Subscription-Key' : credential.api_key
|
||||
});
|
||||
|
||||
const voices = await get('/cognitiveservices/voices/list');
|
||||
const tts = parseMicrosoftLanguagesVoices(voices);
|
||||
return tranform(tts, SttMicrosoftLanguagesVoices);
|
||||
} catch (err) {
|
||||
logger.info('Error while fetching Microsoft languages, voices, return predefined values', err);
|
||||
const {region, api_key} = credential;
|
||||
const response = await fetch(`https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`, {
|
||||
headers: {
|
||||
'Ocp-Apim-Subscription-Key': api_key
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error fetching Microsoft voices');
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
const voices = await response.json();
|
||||
const tts = parseMicrosoftLanguagesVoices(voices);
|
||||
return tranform(tts, SttMicrosoftLanguagesVoices);
|
||||
}
|
||||
return tranform(TtsMicrosoftLanguagesVoices, SttMicrosoftLanguagesVoices);
|
||||
}
|
||||
@@ -646,8 +859,47 @@ async function getLanguagesVoicesForNuane(credential, getTtsVoices, logger) {
|
||||
return tranform(TtsNuanceLanguagesVoices, SttNuanceLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForDeepgram(credential) {
|
||||
return tranform(undefined, SttDeepgramLanguagesVoices, TtsModelDeepgram);
|
||||
async function getLanguagesVoicesForDeepgram(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
const {model_id, api_key, deepgram_stt_uri, deepgram_tts_uri} = credential;
|
||||
// currently just fetching STT and TTS models from Deepgram cloud
|
||||
if (!deepgram_stt_uri && !deepgram_tts_uri) {
|
||||
const response = await fetch('https://api.deepgram.com/v1/models', {
|
||||
headers: {
|
||||
'Authorization': `Token ${api_key}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error fetching Deepgram voices');
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
const {stt, tts} = await response.json();
|
||||
let sttLangs = SttDeepgramLanguagesVoices;
|
||||
const sttModels = Array.from(
|
||||
new Map(
|
||||
stt.map((m) => [m.canonical_name, { name: capitalizeFirst(m.canonical_name), value: m.canonical_name }])
|
||||
).values()
|
||||
).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const ttsModels = Array.from(
|
||||
new Map(
|
||||
tts.map((m) => [m.canonical_name, { name: capitalizeFirst(m.canonical_name), value: m.canonical_name }])
|
||||
).values()
|
||||
).sort((a, b) => a.name.localeCompare(b.name));
|
||||
// if model_id is not provided, return all models, all voices, all languages
|
||||
if (!model_id) {
|
||||
return tranform(TtsLanguagesDeepgram, sttLangs, ttsModels, sttModels);
|
||||
}
|
||||
|
||||
const selectedSttModel = stt.find((m) => m.canonical_name === model_id);
|
||||
const selectedSttLangs = selectedSttModel ? selectedSttModel.languages : [];
|
||||
sttLangs = SttDeepgramLanguagesVoices.filter((l) => {
|
||||
return selectedSttLangs.includes(l.value);
|
||||
});
|
||||
return tranform(TtsLanguagesDeepgram, sttLangs, ttsModels, sttModels);
|
||||
}
|
||||
}
|
||||
return tranform(TtsLanguagesDeepgram, SttDeepgramLanguagesVoices,
|
||||
TtsModelDeepgram, sttModelDeepgram.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForIbm(credential, getTtsVoices, logger) {
|
||||
@@ -678,15 +930,39 @@ async function getLanguagesVoicesForSoniox(credential) {
|
||||
return tranform(undefined, SttSonioxLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForSpeechmatics(credential) {
|
||||
return tranform(undefined, SttSpeechmaticsLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForElevenlabs(credential) {
|
||||
if (credential) {
|
||||
const get = bent('https://api.elevenlabs.io', 'GET', 'json', {
|
||||
'xi-api-key' : credential.api_key
|
||||
const headers = {
|
||||
'xi-api-key': credential.api_key
|
||||
};
|
||||
|
||||
const getModelPromise = fetch('https://api.elevenlabs.io/v1/models', {
|
||||
headers
|
||||
});
|
||||
const getVoicePromise = fetch('https://api.elevenlabs.io/v1/voices', {
|
||||
headers
|
||||
});
|
||||
const [langResp, voiceResp] = await Promise.all([getModelPromise, getVoicePromise]);
|
||||
|
||||
const [langResp, voiceResp] = await Promise.all([get('/v1/models'), get('/v1/voices')]);
|
||||
if (!langResp.ok || !voiceResp.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
|
||||
const langs = await langResp.json();
|
||||
const voicesR = await voiceResp.json();
|
||||
|
||||
const model = langs.find((m) => m.model_id === credential.model_id);
|
||||
const models = langs.map((m) => {
|
||||
return {
|
||||
value: m.model_id,
|
||||
name: m.name
|
||||
};
|
||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const model = langResp.find((m) => m.model_id === credential.model_id);
|
||||
const languages = model ? model.languages.map((l) => {
|
||||
return {
|
||||
value: l.language_id,
|
||||
@@ -695,27 +971,35 @@ async function getLanguagesVoicesForElevenlabs(credential) {
|
||||
}).sort((a, b) => a.name.localeCompare(b.name)) : [];
|
||||
|
||||
if (languages && languages.length > 0) {
|
||||
const voices = voiceResp ? voiceResp.voices.map((v) => {
|
||||
let name = `${v.name}${v.category !== 'premade' ? ` (${v.category})` : ''} -
|
||||
${v.labels.accent ? ` ${v.labels.accent},` : ''}
|
||||
${v.labels.description ? ` ${v.labels.description},` : ''}
|
||||
${v.labels.age ? ` ${v.labels.age},` : ''}
|
||||
${v.labels.gender ? ` ${v.labels.gender},` : ''}
|
||||
${v.labels['use case'] ? ` ${v.labels['use case']},` : ''}
|
||||
`;
|
||||
// using if condition to avoid \n character in name
|
||||
const voices = voicesR ? voicesR.voices.map((v) => {
|
||||
let name = `${v.name}${v.category !== 'premade' ? ` (${v.category.trim()})` : ''} - (`;
|
||||
if (v.labels.accent) name += `${v.labels.accent}, `;
|
||||
if (v.labels.description) name += `${v.labels.description}, `;
|
||||
if (v.labels.age) name += `${v.labels.age}, `;
|
||||
if (v.labels.gender) name += `${v.labels.gender}, `;
|
||||
if (v.labels['use case']) name += `${v.labels['use case']}, `;
|
||||
|
||||
const lastIndex = name.lastIndexOf(',');
|
||||
if (lastIndex !== -1) {
|
||||
name = name.substring(0, lastIndex);
|
||||
}
|
||||
name += ')';
|
||||
return {
|
||||
value: v.voice_id,
|
||||
name
|
||||
};
|
||||
}).sort((a, b) => a.name.localeCompare(b.name)) : [];
|
||||
languages[0].voices = voices;
|
||||
for (const language of languages) {
|
||||
language.voices = voices;
|
||||
}
|
||||
}
|
||||
return tranform(languages, undefined, TtsModelElevenLabs);
|
||||
return tranform(languages, undefined, models);
|
||||
} else {
|
||||
const voices = TtsElevenlabsLanguagesVoices[0].voices;
|
||||
for (const language of TtsElevenlabsLanguagesVoices) {
|
||||
language.voices = voices;
|
||||
}
|
||||
return tranform(TtsElevenlabsLanguagesVoices, undefined, TtsModelElevenLabs);
|
||||
}
|
||||
}
|
||||
@@ -726,18 +1010,28 @@ const concat = (a) => {
|
||||
|
||||
const fetchLayHTVoices = async(credential) => {
|
||||
if (credential) {
|
||||
const get = bent('https://api.play.ht', 'GET', 'json', {
|
||||
'AUTHORIZATION' : credential.api_key,
|
||||
const headers = {
|
||||
'AUTHORIZATION': credential.api_key,
|
||||
'X-USER-ID': credential.user_id,
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
const response = await fetch('https://api.play.ht/api/v2/voices', {
|
||||
headers
|
||||
});
|
||||
|
||||
const voices = await get('/api/v2/voices');
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
const voices = await response.json();
|
||||
let clone_voices = [];
|
||||
try {
|
||||
// try if the account has permission to cloned voice
|
||||
//otherwise ignore this.
|
||||
clone_voices = await get('/api/v2/cloned-voices');
|
||||
const clone_voices_Response = await fetch('https://api.play.ht/api/v2/cloned-voices', {
|
||||
headers
|
||||
});
|
||||
if (clone_voices_Response.ok) {
|
||||
clone_voices = await clone_voices_Response.json();
|
||||
}
|
||||
} catch {}
|
||||
return [clone_voices, voices];
|
||||
}
|
||||
@@ -745,64 +1039,92 @@ const fetchLayHTVoices = async(credential) => {
|
||||
|
||||
async function getLanguagesVoicesForPlayHT(credential) {
|
||||
if (credential) {
|
||||
const {voice_engine} = credential;
|
||||
const [cloned_voice, voices] = await fetchLayHTVoices(credential);
|
||||
const list_voices = [...cloned_voice, ...voices];
|
||||
|
||||
const buildVoice = (d) => {
|
||||
let name = `${d.name} -${concat(d.accent)}${concat(d.age)}${concat(d.gender)}${concat(d.loudness)}` +
|
||||
let name = `${d.name} - (${concat(d.accent)}${concat(d.age)}${concat(d.gender)}${concat(d.loudness)}` +
|
||||
`${concat(d.style)}${concat(d.tempo)}${concat(d.texture)}` ;
|
||||
name = name.endsWith(',') ? name.trim().slice(0, -1) : name;
|
||||
name += !d.language_code ? ' - Custom Voice' : '';
|
||||
name += ')';
|
||||
name = name.replaceAll('( ', '(');
|
||||
|
||||
return {
|
||||
value: `${d.id}`,
|
||||
name
|
||||
};
|
||||
};
|
||||
|
||||
const ttsVoices = list_voices.reduce((acc, voice) => {
|
||||
if (!credential.voice_engine.includes(voice.voice_engine)) {
|
||||
const buildPlay30Payload = () => {
|
||||
// PlayHT3.0 can play different languages with differrent voice.
|
||||
// all voices will be added to english language by default and orther langauges will get voices from english.
|
||||
const ttsVoices = ttsLanguagesPlayHt.map((l) => ({
|
||||
...l,
|
||||
voices: l.value === 'english' ? list_voices.map((v) => buildVoice(v)) : []
|
||||
}));
|
||||
return tranform(ttsVoices, undefined, TtsModelPlayHT);
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const ttsVoices = list_voices.reduce((acc, voice) => {
|
||||
if (!voice_engine.includes(voice.voice_engine)) {
|
||||
return acc;
|
||||
}
|
||||
const languageCode = voice.language_code;
|
||||
// custom voice does not have language code
|
||||
if (!languageCode) {
|
||||
voice.language_code = 'en';
|
||||
voice.language = 'Custom-English';
|
||||
}
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push(buildVoice(voice));
|
||||
} else {
|
||||
acc.push({
|
||||
value: voice.language_code,
|
||||
name: voice.language,
|
||||
voices: [buildVoice(voice)]
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
const languageCode = voice.language_code;
|
||||
// custom voice does not have language code
|
||||
if (!languageCode) {
|
||||
voice.language_code = 'en';
|
||||
voice.language = 'Custom-English';
|
||||
}
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push(buildVoice(voice));
|
||||
} else {
|
||||
acc.push({
|
||||
value: voice.language_code,
|
||||
name: voice.language,
|
||||
voices: [buildVoice(voice)]
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return tranform(ttsVoices, undefined, TtsModelPlayHT);
|
||||
}, []);
|
||||
return tranform(ttsVoices, undefined, TtsModelPlayHT);
|
||||
};
|
||||
|
||||
switch (voice_engine) {
|
||||
case 'Play3.0':
|
||||
return buildPlay30Payload();
|
||||
|
||||
default:
|
||||
return buildPayload();
|
||||
}
|
||||
}
|
||||
return tranform(TtsPlayHtLanguagesVoices, undefined, TtsModelPlayHT);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForRimelabs(credential) {
|
||||
const model_id = credential ? credential.model_id : null;
|
||||
const get = bent('https://users.rime.ai', 'GET', 'json', {
|
||||
'Accept': 'application/json'
|
||||
});
|
||||
const voices = await get('/data/voices/all.json');
|
||||
let selectedVoices = model_id ? voices[model_id] : Object.values(voices).reduce((acc, val) => [...acc, ...val], []);
|
||||
selectedVoices = selectedVoices.map((v) => ({
|
||||
name: v.charAt(0).toUpperCase() + v.slice(1),
|
||||
value: v
|
||||
}));
|
||||
const ttsVoices = [
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'English (US)',
|
||||
voices: selectedVoices
|
||||
const response = await fetch('https://users.rime.ai//data/voices/all-v2.json', {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
];
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list models');
|
||||
}
|
||||
const voices = await response.json();
|
||||
const modelVoices = model_id ? voices[model_id] :
|
||||
Object.keys(voices).length > 0 ? voices[Object.keys(voices)[0]] : [];
|
||||
const ttsVoices = Object.entries(modelVoices).map(([key, voices]) => ({
|
||||
value: key,
|
||||
name: capitalizeFirst(key),
|
||||
voices: voices.map((v) => ({
|
||||
name: capitalizeFirst(v),
|
||||
value: v
|
||||
}))
|
||||
}));
|
||||
return tranform(ttsVoices, undefined, TtsModelRimelabs);
|
||||
}
|
||||
|
||||
@@ -810,15 +1132,44 @@ async function getLanguagesVoicesForAssemblyAI(credential) {
|
||||
return tranform(undefined, SttAssemblyaiLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForVoxist(credential) {
|
||||
return tranform(undefined, SttVoxistLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForWhisper(credential) {
|
||||
return tranform(TtsWhisperLanguagesVoices, undefined, TtsModelWhisper);
|
||||
}
|
||||
|
||||
function tranform(tts, stt, models) {
|
||||
async function getLanguagesVoicesForOpenAi(credential) {
|
||||
return tranform(undefined, SttOpenaiLanguagesVoices, TtsModelOpenai, SttModelOpenai);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForVerbio(credentials, getTtsVoices, logger) {
|
||||
const stt = SttVerbioLanguagesVoices.reduce((acc, v) => {
|
||||
if (!v.version || (credentials && credentials.engine_version === v.version)) {
|
||||
acc.push(v);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
try {
|
||||
if (credentials) {
|
||||
const data = await getTtsVoices({vendor: 'verbio', credentials});
|
||||
const voices = parseVerbioLanguagesVoices(data);
|
||||
return tranform(voices, stt, undefined);
|
||||
}
|
||||
return tranform(TtsVerbioLanguagesVoices, stt, undefined);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'there is error while fetching verbio speech voices');
|
||||
return tranform(TtsVerbioLanguagesVoices, stt, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function tranform(tts, stt, models, sttModels) {
|
||||
return {
|
||||
...(tts && {tts}),
|
||||
...(stt && {stt}),
|
||||
...(models && {models})
|
||||
...(models && {models}),
|
||||
...(sttModels && {sttModels})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -877,7 +1228,7 @@ function parseAwsLanguagesVoices(data) {
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push({
|
||||
value: voice.Id,
|
||||
name: `(${voice.Gender}) ${voice.Name}`
|
||||
name: `${voice.Name} (${voice.Gender})`
|
||||
});
|
||||
} else {
|
||||
acc.push({
|
||||
@@ -885,7 +1236,7 @@ function parseAwsLanguagesVoices(data) {
|
||||
name: voice.LanguageName,
|
||||
voices: [{
|
||||
value: voice.Id,
|
||||
name: `(${voice.Gender}) ${voice.Name}`
|
||||
name: `${voice.Name} (${voice.Gender})`
|
||||
}]
|
||||
});
|
||||
}
|
||||
@@ -941,6 +1292,122 @@ function parseMicrosoftLanguagesVoices(data) {
|
||||
}, []);
|
||||
}
|
||||
|
||||
function parseVerbioLanguagesVoices(data) {
|
||||
return data.voices.reduce((acc, voice) => {
|
||||
const languageCode = voice.language;
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push({
|
||||
value: voice.voice_id,
|
||||
name: voice.name,
|
||||
});
|
||||
} else {
|
||||
acc.push({
|
||||
value: voice.language,
|
||||
name: voice.language,
|
||||
voices: [{
|
||||
value: voice.voice_id,
|
||||
name: voice.name,
|
||||
}]
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
const fetchCartesiaVoices = async(credential) => {
|
||||
if (credential) {
|
||||
const response = await fetch('https://api.cartesia.ai/voices', {
|
||||
headers: {
|
||||
'X-API-Key': credential.api_key,
|
||||
'Cartesia-Version': '2024-06-10',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
};
|
||||
|
||||
const testCartesia = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio(
|
||||
{
|
||||
increment: () => {},
|
||||
histogram: () => {}
|
||||
},
|
||||
{
|
||||
vendor: 'cartesia',
|
||||
credentials,
|
||||
language: 'en',
|
||||
voice: '694f9389-aac1-45b6-b726-9d9369183238',
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
// Test if Cartesia can fetch voices
|
||||
await fetchCartesiaVoices(credentials);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth cartesia returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
async function getLanguagesVoicesForCartesia(credential) {
|
||||
if (credential) {
|
||||
const {model_id} = credential;
|
||||
const {languages} = TtsModelCartesia.find((m) => m.value === model_id);
|
||||
const voices = await fetchCartesiaVoices(credential);
|
||||
|
||||
const buildVoice = (d) => (
|
||||
{
|
||||
value: `${d.id}`,
|
||||
name: `${d.name} - ${d.description}`
|
||||
});
|
||||
const languageMap = {
|
||||
en: 'English',
|
||||
fr: 'French',
|
||||
de: 'German',
|
||||
es: 'Spanish',
|
||||
pt: 'Portuguese',
|
||||
zh: 'Chinese',
|
||||
ja: 'Japanese',
|
||||
hi: 'Hindi',
|
||||
it: 'Italian',
|
||||
ko: 'Korean',
|
||||
nl: 'Dutch',
|
||||
pl: 'Polish',
|
||||
ru: 'Russian',
|
||||
sv: 'Swedish',
|
||||
tr: 'Turkish',
|
||||
};
|
||||
const ttsVoices = voices.reduce((acc, voice) => {
|
||||
if (!languages.includes(voice.language)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const languageCode = voice.language;
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push(buildVoice(voice));
|
||||
} else {
|
||||
acc.push({
|
||||
value: languageCode,
|
||||
name: languageMap[languageCode],
|
||||
voices: [buildVoice(voice)]
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return tranform(ttsVoices, undefined, TtsModelCartesia);
|
||||
}
|
||||
return tranform(ttsCartesia, undefined, TtsModelCartesia);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testGoogleTts,
|
||||
testGoogleStt,
|
||||
@@ -964,5 +1431,11 @@ module.exports = {
|
||||
getSpeechCredential,
|
||||
decryptCredential,
|
||||
testWhisper,
|
||||
getLanguagesAndVoicesForVendor
|
||||
testVerbioTts,
|
||||
testVerbioStt,
|
||||
getLanguagesAndVoicesForVendor,
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt,
|
||||
testOpenAiStt
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ assert.ok(process.env.STRIPE_API_KEY || process.env.NODE_ENV === 'test',
|
||||
assert.ok(process.env.STRIPE_BASE_URL || process.env.NODE_ENV === 'test',
|
||||
'missing env STRIPE_BASE_URL for billing operations');
|
||||
|
||||
const bent = require('bent');
|
||||
const formurlencoded = require('form-urlencoded');
|
||||
const qs = require('qs');
|
||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
@@ -14,10 +13,47 @@ const basicAuth = () => {
|
||||
const header = `Basic ${toBase64(process.env.STRIPE_API_KEY)}`;
|
||||
return {Authorization: header};
|
||||
};
|
||||
const postForm = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'POST', 'string',
|
||||
Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, basicAuth()), 200);
|
||||
const getJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'GET', 'json', basicAuth(), 200);
|
||||
const deleteJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'DELETE', 'json', basicAuth(), 200);
|
||||
const STRIPE_BASE_URL = process.env.STRIPE_BASE_URL || 'http://127.0.0.1';
|
||||
const getJSON = async(path) => {
|
||||
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...basicAuth()
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error retrieving ${path} from stripe: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
const postForm = async(path, body) => {
|
||||
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...basicAuth()
|
||||
},
|
||||
body
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error posting to ${path} from stripe: ${response.status}`);
|
||||
}
|
||||
return await response.text();
|
||||
};
|
||||
const deleteJSON = async(path) => {
|
||||
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...basicAuth()
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error deleting ${path} from stripe: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
};
|
||||
//const debug = require('debug')('jambonz:api-server');
|
||||
|
||||
const listProducts = async(logger) => await getJSON('/products?active=true');
|
||||
|
||||
6884
package-lock.json
generated
6884
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "jambonz-api-server",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.4",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test": "NODE_ENV=test APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall K8S=true K8S_FEATURE_SERVER_SERVICE_NAME=127.0.0.1 K8S_FEATURE_SERVER_SERVICE_PORT=3100 node test/ ",
|
||||
"test:encrypt-decrypt": "ENCRYPTION_SECRET=12345 node --test ./test/encrypt-decrypt.test.js",
|
||||
"integration-test": "NODE_ENV=test JAMBONES_AUTH_USE_JWT=1 JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
|
||||
"upgrade-db": "node ./db/upgrade-jambonz-db.js",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
@@ -19,23 +20,24 @@
|
||||
"url": "https://github.com/jambonz/jambonz-api-server.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.750.0",
|
||||
"@aws-sdk/client-s3": "^3.550.0",
|
||||
"@aws-sdk/client-transcribe": "^3.549.0",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@deepgram/sdk": "^1.21.0",
|
||||
"@google-cloud/speech": "^6.5.0",
|
||||
"@google-cloud/storage": "^7.9.0",
|
||||
"@jambonz/db-helpers": "^0.9.3",
|
||||
"@jambonz/db-helpers": "^0.9.12",
|
||||
"@jambonz/lamejs": "^1.2.2",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.8",
|
||||
"@jambonz/speech-utils": "^0.1.0",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
||||
"@jambonz/speech-utils": "^0.2.10",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.69",
|
||||
"@jambonz/verb-specifications": "^0.0.104",
|
||||
"@soniox/soniox-node": "^1.2.2",
|
||||
"ajv": "^8.17.1",
|
||||
"argon2": "^0.40.1",
|
||||
"assemblyai": "^4.3.4",
|
||||
"bent": "^7.3.12",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.4",
|
||||
"express": "^4.19.2",
|
||||
@@ -43,15 +45,18 @@
|
||||
"form-data": "^4.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"ibm-watson": "^9.0.1",
|
||||
"is-valid-hostname": "^1.0.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mailgun.js": "^10.2.1",
|
||||
"microsoft-cognitiveservices-speech-sdk": "1.36.0",
|
||||
"mysql2": "^3.9.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.11.0",
|
||||
"nocache": "4.0.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"pino": "^8.20.0",
|
||||
"short-uuid": "^4.2.2",
|
||||
"speechmatics": "^4.0.0",
|
||||
"stream-buffers": "^3.0.2",
|
||||
"stripe": "^14.24.0",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
@@ -65,8 +70,6 @@
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"husky": "9.0.11",
|
||||
"nyc": "^15.1.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.9",
|
||||
"tape": "^5.7.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const SP_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734ds';
|
||||
const authSP = {bearer: ADMIN_TOKEN};
|
||||
const ACC_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734da';
|
||||
const authAcc = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {
|
||||
@@ -14,7 +11,7 @@ const {
|
||||
createPhoneNumber,
|
||||
deleteObjectBySid} = require('./utils');
|
||||
const logger = require('../lib/logger');
|
||||
const { addToSortedSet } = require('@jambonz/realtimedb-helpers')({
|
||||
const { addToSortedSet, createHash } = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
@@ -152,7 +149,9 @@ test('account tests', async(t) => {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
console.log(result);
|
||||
t.ok(result.name === 'daveh' , 'successfully retrieved account by sid');
|
||||
t.ok(result.enable_debug_log === 0 , 'enable_debug_log default value ok');
|
||||
|
||||
/* update account with account level token */
|
||||
result = await request.put(`/Accounts/${sid}`, {
|
||||
@@ -177,8 +176,8 @@ test('account tests', async(t) => {
|
||||
name: 'recordings',
|
||||
access_key_id: 'access_key_id',
|
||||
secret_access_key: 'secret access key'
|
||||
}
|
||||
|
||||
},
|
||||
enable_debug_log: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated account using account level token');
|
||||
@@ -194,6 +193,7 @@ test('account tests', async(t) => {
|
||||
t.ok(result.bucket_credential.access_key_id === 'access_key_id', 'bucket_access_key_id was updated');
|
||||
t.ok(result.record_all_calls === 1, 'record_all_calls was updated');
|
||||
t.ok(result.record_format === 'wav', 'record_format was updated');
|
||||
t.ok(result.enable_debug_log, 'enable_debug_log was updated');
|
||||
|
||||
/* verify that account level api key last_used was updated*/
|
||||
result = await request.get(`/Accounts/${sid}/ApiKeys`, {
|
||||
@@ -314,6 +314,19 @@ test('account tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.length === 0, 'successfully queried account queue info with for an invalid account');
|
||||
|
||||
// query conferences
|
||||
await createHash(`conf:${sid}:conf1`, 'url1');
|
||||
await createHash(`conf:${sid}:conf2`, 'url2');
|
||||
await createHash(`conf:${sid}:conf3`, 'url3');
|
||||
await createHash(`conf:${sid}:conf4`, 'url4');
|
||||
|
||||
result = await request.get(`/Accounts/${sid}/Conferences`, {
|
||||
auth: authAdmin,
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.length === 4, 'successfully queried account conferences info for an account');
|
||||
|
||||
/* delete account */
|
||||
result = await request.delete(`/Accounts/${sid}`, {
|
||||
auth: authAdmin,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, createServiceProvider,
|
||||
@@ -121,6 +122,25 @@ test('application tests', async(t) => {
|
||||
let app_json = JSON.parse(result.app_json);
|
||||
t.ok(app_json[0].verb === 'play', 'successfully retrieved app_json from application')
|
||||
|
||||
/* query one application by name*/
|
||||
result = await request.get(`/Applications`, {
|
||||
qs : {
|
||||
name: 'daveh'
|
||||
},
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 1 && result[0].name === 'daveh', 'successfully queried application by name');
|
||||
|
||||
/* query application with invalid name*/
|
||||
result = await request.get(`/Applications`, {
|
||||
qs : {
|
||||
name: 'daveh-invalid'
|
||||
},
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 0, 'successfully queried application by invalid name, no results found');
|
||||
|
||||
/* update applications */
|
||||
result = await request.put(`/Applications/${sid}`, {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, createServiceProvider, createPhoneNumber, createAccount, deleteObjectBySid} = require('./utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const test = require('tape');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const consoleLogger = { debug: console.log, info: console.log, error: console.error }
|
||||
@@ -47,8 +48,7 @@ test('Create Call Success With Synthesizer in Payload', async (t) => {
|
||||
});
|
||||
// THEN
|
||||
t.ok(result.statusCode === 201, 'successfully created Call without Synthesizer && application_sid');
|
||||
const fs_request = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
const obj = JSON.parse(fs_request);
|
||||
const obj = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
t.ok(obj.body.speech_synthesis_vendor == 'google', 'speech synthesizer successfully added')
|
||||
t.ok(obj.body.speech_recognizer_vendor == 'google', 'speech recognizer successfully added')
|
||||
});
|
||||
@@ -82,7 +82,7 @@ test('Create Call Success Without Synthesizer in Payload', async (t) => {
|
||||
}
|
||||
}
|
||||
}).then(data => { t.ok(false, 'Create Call should not be success') })
|
||||
.catch(error => { t.ok(error.response.statusCode === 400, 'Call failed for no synthesizer') });
|
||||
.catch(error => { t.ok(error.statusCode === 400, 'Call failed for no synthesizer') });
|
||||
});
|
||||
|
||||
test("Create call with application sid and app_json", async(t) => {
|
||||
@@ -150,7 +150,6 @@ result = await request.post(`/Accounts/${account_sid}/Calls`, {
|
||||
});
|
||||
// THEN
|
||||
t.ok(result.statusCode === 201, 'successfully created Call without Synthesizer && application_sid');
|
||||
const fs_request = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
const obj = JSON.parse(fs_request);
|
||||
const obj = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
t.ok(obj.body.app_json == app_json, 'app_json successfully added')
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const { createClient } = require('./http-client');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -32,3 +32,11 @@ test('add predefined carriers', (t) => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('prepare permissions', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('permissions prepared');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const test = require('tape');
|
||||
const {emailSimpleText} = require('../lib/utils/email-utils');
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('json')
|
||||
const logger = {
|
||||
debug: () =>{},
|
||||
info: () => {}
|
||||
@@ -15,7 +13,7 @@ test('email-test', async(t) => {
|
||||
|
||||
await emailSimpleText(logger, 'test@gmail.com', 'subject', 'body text');
|
||||
|
||||
const obj = await getJSON(`http://127.0.0.1:3101/lastRequest/custom_email_vendor`);
|
||||
const obj = await (await fetch(`http://127.0.0.1:3101/lastRequest/custom_email_vendor`)).json();
|
||||
t.ok(obj.headers['Content-Type'] == 'application/json');
|
||||
t.ok(obj.headers.Authorization == 'Basic VVNFUk5BTUU6UEFTU1dPUkQ=');
|
||||
t.ok(obj.body.from == 'jambonz Support <support@jambonz.org>');
|
||||
|
||||
132
test/encrypt-decrypt.test.js
Normal file
132
test/encrypt-decrypt.test.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const assert = require('node:assert');
|
||||
const { describe, it, beforeEach, before } = require('node:test');
|
||||
const { obscureBucketCredentialsSensitiveData, isObscureKey, obscureKey } = require('../lib/utils/encrypt-decrypt');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
|
||||
let bucket_credential = {};
|
||||
let service_key = {};
|
||||
|
||||
describe('encrypt-decrypt test suite', { skip: false }, () => {
|
||||
before(() => {
|
||||
process.env.ENCRYPTION_SECRET = "12345";
|
||||
});
|
||||
beforeEach(() => {
|
||||
service_key = {
|
||||
"type": "service_account",
|
||||
"project_id": "voice-gateway",
|
||||
"private_key_id": randomUUID(),
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nJ6T2sQmBAD8TBm0frkGeyw0FA0aVAASVBJcwggSjBgEAAoabAQDUbiVwgufJm9R3\nv7YpECxRnQgHduX8jnbjtubi6FVqqVx78W3NM/gFNJFBC7NXtpsslqefKM6SmlkK\nNP6XqXy/ppXO8u00se7+cOFhsS6crncnsCeIYPxLFPV8P4BjExi7v88RBdektLeV\nX6sRWhxiYeWe+ORxkyC0KR4IKZjHt7ZHrg0kNQyuNx1KOhJnN3rRUkKSP2zozd1c\n3V+4EfpZjGmlQegXNHzkwUjvTOe6nuyxynWe3smjsSw/3RQda5m674Kh9tnVWEiX\n5KWnfRbWDdEBp5azzOAdeSR5W+qQfS0Jo6blREDQxWfMNmut87m81gmn+DKMhq6k\nV0JVRQStAgMBAAECggEAAf7AsAdI24zUdXkZv98gEuC4S10e2Luoz6Yvm41V3sP6\nWx0mdgY7RB9RW8U4WPnu3GhPGGJj03SyHUuI8Ix15MNM9WGDAaV3S+kRb1LqChLO\nCoNSO/qYPPg4t1iQ9+s7sWTnM93MAXjujmSveHJd7+MrUQOxOdPjB7I/ozMkBXBb\nWYsBIfeOG/7DsC4N4V/hKVXAq9NekGQv85yCUPR/DpuG9vqpztXwaSC1Wihntlu3\nNJYmMave4PbO3OxAekBl70WmukERZo7ksR+34WWse4HXlphaUVbpvnbQdGT1EzXW\nZukanqpfkIwrHme2Ko5NdP0C54pRhg6kpmWszVUMQQKBgQD6yWMuuwdPuFLg0wc2\nnDUeyHZSq/SEMSrb1wNXhL3qhaL2tCjBZwG9CHDuFMhqDLU5F36mEdAsM5MHCzyC\nTJ7VbqvCFz69SRt1CVp76Hu4HO/1Nhxm1GhF+NKSDIbnUg3o4DaC7EUtLpqYXcWj\nsXHEqVEhkrNVQ/JOIfJr42LDfQKBgQDM0U3ghGg9ufe9mb0dOonwtJA/ay/pd08Q\nyq3ue9C3yoQiNf28bP3AGKIjhA6wtd0KTSkQs6ktabGHIM8o/eTrMAMQllKh/3xe\nON7iND8Xz2GFMuIraQ7Mq9RvYWiqqIkVg1GQfJmiQ9wcmGj2PHy25LfjBXfHAYqK\nQ++P/i+s8QKBgD0pRi4MYNEZY+T+skCoQfA69VheJWjj0M8ClgcPEX4Tj1XZRCM+\nqtbeKyR1Hxd19/BvgWyg5YMSJOZP4Dbq1sW4ktzn7F4faTnWySF05k9Vh1PnGXAe\nlzuRXlFOCsx5X3kOzVyKoKhPOFa2b8/nI5bRsD6e12uRAZP6hXO4ZcrFAoGABVJ/\nCpGGP+xgMq4XCvZldTrL8MnxQcjW5iHOKT9QaiY6DsWGZWoTofVB6VhaJV9kcgsV\nQRjaEZMIiPFiULdgRnhF7B1r4kfITI5/xDMFXLIH37U1yVj+iHUCnS5T0PN2NHfo\nG7ARMfU/eALB33ws5XfGC4Et3p78oaEoTX6WcJECgYEAhysSt4qieGRSXn24V0+u\ny/ubU4dysn4SFe8BB4bjgYa8v+6VwucU+nnU4wOwykEgJmzN/ukzpvFN0CkN8eAN\n8xwtjBX9Zc1S90Wf7/7IrQGlUnsSFpDh5TW+oCqVo8JK7UGxJHR1mCvJmYVmSk3c\nD4AMvJ2x/Z5d9NKIAzdET4o=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "voicegatewayteam@voice-gateway.iam.gserviceaccount.com",
|
||||
"client_id": "333827616171177028875",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cognigyvoicegatewayteam%40cognigy-voice-gateway.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
};
|
||||
bucket_credential = {
|
||||
"vendor": "google",
|
||||
"service_key": "",
|
||||
"connection_string": "",
|
||||
"secret_access_key": ""
|
||||
}
|
||||
});
|
||||
|
||||
describe('obscureBucketCredentialsSensitiveData', { skip: false }, () => {
|
||||
it('should obscure "private_key" - vendor: google', { skip: false }, () => {
|
||||
bucket_credential = {
|
||||
...bucket_credential,
|
||||
"vendor": "google",
|
||||
"service_key": JSON.stringify(service_key),
|
||||
};
|
||||
const result = obscureBucketCredentialsSensitiveData(bucket_credential);
|
||||
const googleCredentials = JSON.parse(result.service_key);
|
||||
assert.strictEqual(googleCredentials.private_key, '-----BEGIN PRIVATE KEY-----\nJ6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
|
||||
assert.strictEqual(googleCredentials.private_key_id, googleCredentials.private_key_id);
|
||||
assert.strictEqual(googleCredentials.project_id, googleCredentials.project_id);
|
||||
assert.strictEqual(googleCredentials.client_email, googleCredentials.client_email);
|
||||
assert.strictEqual(googleCredentials.client_id, googleCredentials.client_id);
|
||||
assert.strictEqual(googleCredentials.auth_uri, googleCredentials.auth_uri);
|
||||
assert.strictEqual(googleCredentials.token_uri, googleCredentials.token_uri);
|
||||
});
|
||||
|
||||
it('should skip obscure since it is already obscured. vendor: google - "private key"', { skip: false }, () => {
|
||||
service_key.private_key = '-----BEGIN PRIVATE KEY-----\nJ6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
|
||||
bucket_credential = {
|
||||
...bucket_credential,
|
||||
"vendor": "google",
|
||||
"service_key": JSON.stringify(service_key),
|
||||
};
|
||||
const result = obscureBucketCredentialsSensitiveData(bucket_credential);
|
||||
const googleCredentials = JSON.parse(result.service_key);
|
||||
assert.strictEqual(googleCredentials.private_key, '-----BEGIN PRIVATE KEY-----\nJ6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');
|
||||
assert.strictEqual(googleCredentials.private_key_id, googleCredentials.private_key_id);
|
||||
assert.strictEqual(googleCredentials.project_id, googleCredentials.project_id);
|
||||
assert.strictEqual(googleCredentials.client_email, googleCredentials.client_email);
|
||||
assert.strictEqual(googleCredentials.client_id, googleCredentials.client_id);
|
||||
assert.strictEqual(googleCredentials.auth_uri, googleCredentials.auth_uri);
|
||||
assert.strictEqual(googleCredentials.token_uri, googleCredentials.token_uri);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObscureKey', { skip: false }, () => {
|
||||
it('vendor: google - should return true', { skip: false }, () => {
|
||||
service_key.private_key = '-----BEGIN PRIVATE KEY-----\nJ6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
|
||||
bucket_credential = {
|
||||
...bucket_credential,
|
||||
"vendor": "google",
|
||||
"service_key": JSON.stringify(service_key),
|
||||
};
|
||||
const result = isObscureKey(bucket_credential);
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
it('vendor: google - should return false', { skip: false }, () => {
|
||||
bucket_credential = {
|
||||
...bucket_credential,
|
||||
"vendor": "google",
|
||||
"service_key": JSON.stringify(service_key),
|
||||
};
|
||||
const result = isObscureKey(bucket_credential);
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
it('vendor: aws_s3 - should return true', { skip: false }, () => {
|
||||
const obscuredKey = "J6T2sQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
|
||||
bucket_credential = {
|
||||
...bucket_credential,
|
||||
"vendor": "aws_s3",
|
||||
"secret_access_key": obscuredKey,
|
||||
};
|
||||
const result = isObscureKey(bucket_credential);
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
it('vendor: aws_s3 - should return false', { skip: false }, () => {
|
||||
bucket_credential = {
|
||||
...bucket_credential,
|
||||
"vendor": "aws_s3",
|
||||
"service_key": "EFEU2fhcbqiw3211ffw3f1kezhcbqiw3211ffw3f",
|
||||
};
|
||||
const result = isObscureKey(bucket_credential);
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
it('vendor: azure - should return true', { skip: false }, () => {
|
||||
const obscuredKey = 'https:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
|
||||
bucket_credential = {
|
||||
...bucket_credential,
|
||||
"vendor": "azure",
|
||||
"connection_string": obscuredKey,
|
||||
};
|
||||
const result = isObscureKey(bucket_credential);
|
||||
assert.strictEqual(result, true);
|
||||
});
|
||||
it('vendor: azure - should return false', { skip: false }, () => {
|
||||
bucket_credential = {
|
||||
...bucket_credential,
|
||||
"vendor": "azure",
|
||||
connection_string: 'https://cognigydevstorage.blob.core.windows.net/voicegateway-test?sp=rw&st=2023-09-10T13:35:44Z&se=2023-09-11T21:35:44Z&spr=https&sv=2022-11-02&sr=c&sig=9WN8Bg5UMOvnV1h1cJpCnTUG%2FnonTbRZ1Q1rbKnDUl4%3D',
|
||||
};
|
||||
const result = isObscureKey(bucket_credential);
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const test = require('tape');
|
||||
const request = require("request-promise-native").defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: "http://127.0.0.1:3000/v1",
|
||||
});
|
||||
|
||||
|
||||
172
test/http-client.js
Normal file
172
test/http-client.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Fetch-based HTTP client that mimics the request-promise-native API
|
||||
*/
|
||||
|
||||
class HttpClient {
|
||||
constructor(defaults = {}) {
|
||||
this.defaults = defaults;
|
||||
this.baseUrl = defaults.baseUrl || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP GET request
|
||||
*/
|
||||
async get(url, options = {}) {
|
||||
return this._makeRequest(url, 'GET', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP POST request
|
||||
*/
|
||||
async post(url, options = {}) {
|
||||
return this._makeRequest(url, 'POST', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP PUT request
|
||||
*/
|
||||
async put(url, options = {}) {
|
||||
return this._makeRequest(url, 'PUT', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP DELETE request
|
||||
*/
|
||||
async delete(url, options = {}) {
|
||||
return this._makeRequest(url, 'DELETE', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to handle all HTTP requests
|
||||
*/
|
||||
async _makeRequest(url, method, options = {}) {
|
||||
const {
|
||||
auth,
|
||||
body,
|
||||
json = true, // Changed default to true since most API calls expect JSON
|
||||
qs = {},
|
||||
simple = true,
|
||||
resolveWithFullResponse = false
|
||||
} = options;
|
||||
|
||||
// Build full URL with query parameters
|
||||
const fullUrl = this._buildUrl(url, qs);
|
||||
|
||||
// Set up headers
|
||||
const headers = {};
|
||||
if (auth?.bearer) {
|
||||
headers['Authorization'] = `Bearer ${auth.bearer}`;
|
||||
}
|
||||
|
||||
// Set JSON headers for all requests when json is true
|
||||
if (json) {
|
||||
headers['Accept'] = 'application/json';
|
||||
|
||||
// Only set Content-Type when sending data
|
||||
if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
// Build request options
|
||||
const fetchOptions = {
|
||||
method,
|
||||
headers
|
||||
};
|
||||
|
||||
// Add request body if needed
|
||||
if (body && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
||||
fetchOptions.body = json ? JSON.stringify(body) : body;
|
||||
}
|
||||
|
||||
try {
|
||||
// Make the request
|
||||
const response = await fetch(fullUrl, fetchOptions);
|
||||
|
||||
// Clone the response before consuming it
|
||||
// This allows us to use the body in error handling if needed
|
||||
const clonedResponse = response.clone();
|
||||
|
||||
// Parse response body based on content type - only once
|
||||
let responseBody = null;
|
||||
if (response.status !== 204) { // No content
|
||||
if (json) {
|
||||
try {
|
||||
responseBody = await response.json();
|
||||
} catch (e) {
|
||||
// If can't parse JSON, get text
|
||||
responseBody = await clonedResponse.text();
|
||||
}
|
||||
} else {
|
||||
responseBody = await response.text();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors if simple mode is enabled
|
||||
if (simple && !response.ok) {
|
||||
const error = new Error(`Request failed with status code ${response.status}`);
|
||||
error.statusCode = response.status;
|
||||
error.body = responseBody; // Include the already parsed body
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Return full response object or just body based on options
|
||||
if (resolveWithFullResponse) {
|
||||
return {
|
||||
statusCode: response.status,
|
||||
body: responseBody,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
};
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
} catch (error) {
|
||||
if (!simple) {
|
||||
// If simple=false, return error response instead of throwing
|
||||
return {
|
||||
statusCode: error.statusCode || 500,
|
||||
body: error.body || { message: error.message }
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Build URL with query parameters
|
||||
*/
|
||||
_buildUrl(url, qs) {
|
||||
// Start with base URL
|
||||
let fullUrl = this.baseUrl + url;
|
||||
|
||||
// Add query parameters
|
||||
if (Object.keys(qs).length > 0) {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(qs).forEach(([key, value]) => {
|
||||
params.append(key, value);
|
||||
});
|
||||
fullUrl += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
return fullUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client with default options
|
||||
*/
|
||||
function createClient(defaults = {}) {
|
||||
const client = new HttpClient(defaults);
|
||||
|
||||
// Return the methods directly for API compatibility
|
||||
return {
|
||||
get: (url, options) => client.get(url, options),
|
||||
post: (url, options) => client.post(url, options),
|
||||
put: (url, options) => client.put(url, options),
|
||||
delete: (url, options) => client.delete(url, options),
|
||||
defaults: client.defaults
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createClient
|
||||
};
|
||||
@@ -13,6 +13,7 @@ require('./ms-teams');
|
||||
require('./speech-credentials');
|
||||
require('./recent-calls');
|
||||
require('./users');
|
||||
require('./users-view-only');
|
||||
require('./login');
|
||||
require('./webapp_tests');
|
||||
// require('./homer');
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const { createClient } = require('./http-client');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const exec = require('child_process').exec ;
|
||||
const {generateHashedPassword} = require('../lib/utils/password-utils');
|
||||
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
@@ -52,7 +54,7 @@ test('login tests', async(t) => {
|
||||
password: 'adm',
|
||||
}
|
||||
}).catch(error => {
|
||||
t.ok(error.response.statusCode === 403, `Maximum login attempts reached. Please try again in ${attempTime} seconds.`)
|
||||
t.ok(error.statusCode === 403, `Maximum login attempts reached. Please try again in ${attempTime} seconds.`)
|
||||
});
|
||||
} else if (index < maxAttempts) {
|
||||
attemptResult = await request.post('/login', {
|
||||
@@ -62,7 +64,10 @@ test('login tests', async(t) => {
|
||||
username: 'admin',
|
||||
password: 'adm',
|
||||
}
|
||||
}).catch(error => t.ok(error.response.statusCode === 403));
|
||||
}).catch(error => {
|
||||
console.log(JSON.stringify(error));
|
||||
t.ok(error.statusCode === 403);
|
||||
});
|
||||
} else {
|
||||
attemptResult = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -71,7 +76,7 @@ test('login tests', async(t) => {
|
||||
username: 'admin',
|
||||
password: 'adm',
|
||||
}
|
||||
}).catch(error => t.ok(error.response.statusCode === 403, 'Maximum login attempts reached. Please try again later or reset your password.'));
|
||||
}).catch(error => t.ok(error.statusCode === 403, 'Maximum login attempts reached. Please try again later or reset your password.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils');
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('GET', 200);
|
||||
const request = require('request');
|
||||
|
||||
const test = async() => {
|
||||
request.get('https://api.github.com/user', {
|
||||
fetch('https://api.github.com/user', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GH_CODE}`,
|
||||
Accept: 'application/json',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const { createClient } = require('./http-client');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, deleteObjectBySid} = require('./utils');
|
||||
|
||||
@@ -3,7 +3,8 @@ const fs = require('fs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const consoleLogger = {debug: console.log, info: console.log, error: console.error}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createServiceProvider, deleteObjectBySid} = require('./utils');
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {deleteObjectBySid} = require('./utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, deleteObjectBySid} = require('./utils');
|
||||
@@ -17,6 +18,53 @@ test('sip gateway tests', async(t) => {
|
||||
let result;
|
||||
const voip_carrier_sid = await createVoipCarrier(request);
|
||||
|
||||
/* add a invalid sip gateway */
|
||||
const STORED_JAMBONZ_MIN_GATEWAY_NETMASK = process.env.JAMBONZ_MIN_GATEWAY_NETMASK;
|
||||
process.env.JAMBONZ_MIN_GATEWAY_NETMASK = 24;
|
||||
|
||||
result = await request.post('/SipGateways', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
simple: false,
|
||||
body: {
|
||||
voip_carrier_sid,
|
||||
ipv4: '1.2.3.4',
|
||||
netmask: 1,
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
protocol: 'tcp'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 400, 'successfully created sip gateway ');
|
||||
|
||||
result = await request.post('/SipGateways', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
voip_carrier_sid,
|
||||
ipv4: '1.2.3.4',
|
||||
netmask: 24,
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
protocol: 'tcp'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created sip gateway ');
|
||||
|
||||
process.env.JAMBONZ_MIN_GATEWAY_NETMASK = STORED_JAMBONZ_MIN_GATEWAY_NETMASK;
|
||||
|
||||
/* delete sip gateways */
|
||||
result = await request.delete(`/SipGateways/${result.body.sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
json: true,
|
||||
auth: authAdmin
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
t.ok(result.statusCode === 204, 'successfully deleted sip gateway');
|
||||
|
||||
/* add a sip gateway */
|
||||
result = await request.post('/SipGateways', {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -75,6 +123,44 @@ test('sip gateway tests', async(t) => {
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
t.ok(result.statusCode === 204, 'successfully deleted sip gateway');
|
||||
|
||||
/* add a sip gateway */
|
||||
result = await request.post('/SipGateways', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
voip_carrier_sid,
|
||||
ipv4: '192.168.1.2',
|
||||
netmask: 32,
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
protocol: 'tls',
|
||||
use_sips_scheme: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created sip gateway ');
|
||||
const sipsSid = result.body.sid;
|
||||
|
||||
/* query one sip gateway */
|
||||
result = await request.get(`/SipGateways/${sipsSid}`, {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
t.ok(result.ipv4 === '192.168.1.2' , 'successfully retrieved voip carrier by sid');
|
||||
t.ok(result.protocol === 'tls' , 'successfully retrieved voip carrier by sid');
|
||||
t.ok(result.use_sips_scheme, 'successfully retrieved voip carrier by sid');
|
||||
|
||||
/* delete sip gateways */
|
||||
result = await request.delete(`/SipGateways/${sipsSid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
json: true,
|
||||
auth: authAdmin
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
t.ok(result.statusCode === 204, 'successfully deleted sip gateway');
|
||||
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, deleteObjectBySid} = require('./utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
@@ -3,12 +3,14 @@ const fs = require('fs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils');
|
||||
const { noopLogger } = require('@jambonz/realtimedb-helpers/lib/utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
@@ -371,7 +373,8 @@ test('speech credentials tests', async(t) => {
|
||||
vendor: 'deepgram',
|
||||
use_for_stt: true,
|
||||
deepgram_stt_uri: "127.0.0.1:50002",
|
||||
deepgram_stt_use_tls: true
|
||||
deepgram_stt_use_tls: true,
|
||||
deepgram_tts_uri: 'https://server.com'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for deepgram');
|
||||
@@ -386,6 +389,7 @@ test('speech credentials tests', async(t) => {
|
||||
t.ok(result.statusCode === 200, 'successfully get speech credential for deepgram');
|
||||
t.ok(result.body.deepgram_stt_uri === '127.0.0.1:50002', "deepgram_stt_uri is correct for deepgram");
|
||||
t.ok(result.body.deepgram_stt_use_tls === true, "deepgram_stt_use_tls is correct for deepgram");
|
||||
t.ok(result.body.deepgram_tts_uri === 'https://server.com', "deepgram_tts_uri is correct for deepgram")
|
||||
|
||||
result = await request.put(`/Accounts/${account_sid}/SpeechCredentials/${dg_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -395,7 +399,8 @@ test('speech credentials tests', async(t) => {
|
||||
vendor: 'deepgram',
|
||||
use_for_stt: true,
|
||||
deepgram_stt_uri: "127.0.0.2:50002",
|
||||
deepgram_stt_use_tls: false
|
||||
deepgram_stt_use_tls: false,
|
||||
deepgram_tts_uri: 'https://server2.com'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated speech credential for deepgram onprem');
|
||||
@@ -409,6 +414,7 @@ test('speech credentials tests', async(t) => {
|
||||
t.ok(result.statusCode === 200, 'successfully get speech credential for deepgram onprem');
|
||||
t.ok(result.body.deepgram_stt_uri === '127.0.0.2:50002', "deepgram_stt_uri is correct for deepgram onprem");
|
||||
t.ok(result.body.deepgram_stt_use_tls === false, "deepgram_stt_use_tls is correct for deepgram onprem");
|
||||
t.ok(result.body.deepgram_tts_uri === 'https://server2.com', "deepgram_tts_uri is correct for deepgram onprem");
|
||||
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${dg_sid}`, {
|
||||
auth: authUser,
|
||||
@@ -528,6 +534,39 @@ test('speech credentials tests', async(t) => {
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
}
|
||||
|
||||
/* add a credential for Speechmatics */
|
||||
if (process.env.SPEECHMATICS_API_KEY) {
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'speechmatics',
|
||||
use_for_stt: true,
|
||||
api_key: process.env.SPEECHMATICS_API_KEY,
|
||||
speechmatics_stt_uri: 'eu2.rt.speechmatics.com'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for speechmatics');
|
||||
const ms_sid = result.body.sid;
|
||||
|
||||
/* test the speech credential */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
});
|
||||
console.log(JSON.stringify(result));
|
||||
t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for speechmatics');
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
}
|
||||
|
||||
/* add a credential for nvidia */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -743,6 +782,27 @@ test('speech credentials tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for Voxist */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'voxist',
|
||||
use_for_stt: true,
|
||||
api_key: "APIKEY"
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Voxist');
|
||||
const voxistSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${voxistSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for aws polly by roleArn */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -750,7 +810,7 @@ test('speech credentials tests', async(t) => {
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'aws',
|
||||
labe: 'aws_polly_with_arn',
|
||||
label: 'aws_polly_with_arn',
|
||||
use_for_tts: true,
|
||||
use_for_stt: false,
|
||||
role_arn: 'Arn::aws::role',
|
||||
@@ -767,6 +827,58 @@ test('speech credentials tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for verbio */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'verbio',
|
||||
use_for_tts: true,
|
||||
use_for_stt: true,
|
||||
client_id: 'client:id',
|
||||
client_secret: 'client:secret',
|
||||
engine_version: 'V1'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Verbio');
|
||||
const verbioSid = result.body.sid;
|
||||
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${verbioSid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.body.engine_version === "V1", 'successfully get verbio speech credential');
|
||||
|
||||
result = await request.put(`/Accounts/${account_sid}/SpeechCredentials/${verbioSid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
use_for_tts: true,
|
||||
use_for_stt: true,
|
||||
engine_version: 'V2'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated speech credential for verbio');
|
||||
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${verbioSid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.body.engine_version === "V2", 'successfully Updated verbio speech credential');
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${verbioSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* Check google supportedLanguagesAndVoices */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=google`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -883,6 +995,15 @@ test('speech credentials tests', async(t) => {
|
||||
});
|
||||
t.ok(result.body.stt.length !== 0, 'successfully get assemblyai supported languages and voices');
|
||||
|
||||
/* Check voxist supportedLanguagesAndVoices */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=voxist`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.body.stt.length !== 0, 'successfully get voxist supported languages and voices');
|
||||
|
||||
/* Check whisper supportedLanguagesAndVoices */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=whisper`, {
|
||||
resolveWithFullResponse: true,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { createClient } = require('./http-client');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
@@ -16,7 +17,9 @@ test('system information test', async(t) => {
|
||||
body: {
|
||||
domain_name: 'test.com',
|
||||
sip_domain_name: 'sip.test.com',
|
||||
monitoring_domain_name: 'monitor.test.com'
|
||||
monitoring_domain_name: 'monitor.test.com',
|
||||
private_network_cidr: '192.168.1.0/24, 10.10.100.1',
|
||||
log_level: 'info'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created system information ');
|
||||
@@ -24,6 +27,8 @@ test('system information test', async(t) => {
|
||||
t.ok(body.domain_name === 'test.com', 'added domain_name ok');
|
||||
t.ok(body.sip_domain_name === 'sip.test.com', 'added sip_domain_name ok');
|
||||
t.ok(body.monitoring_domain_name === 'monitor.test.com', 'added monitoring_domain_name ok');
|
||||
t.ok(body.private_network_cidr === '192.168.1.0/24, 10.10.100.1', 'added private_network_cidr ok');
|
||||
t.ok(body.log_level === 'info', 'added log_level ok');
|
||||
|
||||
result = await request.get('/SystemInformation', {
|
||||
auth: authAdmin,
|
||||
@@ -32,6 +37,8 @@ test('system information test', async(t) => {
|
||||
t.ok(result.domain_name === 'test.com', 'get domain_name ok');
|
||||
t.ok(result.sip_domain_name === 'sip.test.com', 'get sip_domain_name ok');
|
||||
t.ok(result.monitoring_domain_name === 'monitor.test.com', 'get monitoring_domain_name ok');
|
||||
t.ok(result.private_network_cidr === '192.168.1.0/24, 10.10.100.1', 'get private_network_cidr ok');
|
||||
t.ok(result.log_level === 'info', 'added log_level ok');
|
||||
|
||||
result = await request.post('/SystemInformation', {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -40,7 +47,9 @@ test('system information test', async(t) => {
|
||||
body: {
|
||||
domain_name: 'test1.com',
|
||||
sip_domain_name: 'sip1.test.com',
|
||||
monitoring_domain_name: 'monitor1.test.com'
|
||||
monitoring_domain_name: 'monitor1.test.com',
|
||||
private_network_cidr: '',
|
||||
log_level: 'debug'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully updated system information ');
|
||||
@@ -48,6 +57,8 @@ test('system information test', async(t) => {
|
||||
t.ok(body.domain_name === 'test1.com', 'updated domain_name ok');
|
||||
t.ok(body.sip_domain_name === 'sip1.test.com', 'updated sip_domain_name ok');
|
||||
t.ok(body.monitoring_domain_name === 'monitor1.test.com', 'updated monitoring_domain_name ok');
|
||||
t.ok(body.private_network_cidr === '', 'updated private_network_cidr ok');
|
||||
t.ok(body.log_level === 'debug', 'updated log_level ok');
|
||||
|
||||
result = await request.get('/SystemInformation', {
|
||||
auth: authAdmin,
|
||||
@@ -56,6 +67,7 @@ test('system information test', async(t) => {
|
||||
t.ok(result.domain_name === 'test1.com', 'get domain_name ok');
|
||||
t.ok(result.sip_domain_name === 'sip1.test.com', 'get sip_domain_name ok');
|
||||
t.ok(result.monitoring_domain_name === 'monitor1.test.com', 'get monitoring_domain_name ok');
|
||||
t.ok(result.log_level === 'debug', 'updated log_level ok');
|
||||
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user