Compare commits

..

3 Commits

Author SHA1 Message Date
Quan HL
43b784edd4 obfuscate client password 2023-06-16 07:08:33 +07:00
Quan HL
1439c1b6a4 encrypt client password and fix upgrade db script 2023-06-16 06:19:48 +07:00
Quan HL
9ce5bb398f encrypt client password and fix upgrade db script 2023-06-16 06:17:51 +07:00
55 changed files with 2779 additions and 5077 deletions

View File

@@ -33,8 +33,6 @@ Configuration is provided via environment variables:
|K8S| service running as kubernetes service |no|
|K8S_FEATURE_SERVER_SERVICE_NAME| feature server name(required for K8S) |no|
|K8S_FEATURE_SERVER_SERVICE_PORT| feature server port(required for K8S) |no|
|JAMBONZ_RECORD_WS_USERNAME| recording websocket username|no|
|JAMBONZ_RECORD_WS_PASSWORD| recording websocket password|no|
#### Database dependency
A mysql database is used to store long-lived objects such as Accounts, Applications, etc. To create the database schema, use or review the scripts in the 'db' folder, particularly:

34
app.js
View File

@@ -8,7 +8,6 @@ const rateLimit = require('express-rate-limit');
const cors = require('cors');
const passport = require('passport');
const routes = require('./lib/routes');
const Registrar = require('@jambonz/mw-registrar');
assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_USER &&
@@ -36,7 +35,6 @@ const {
logger, process.env.JAMBONES_TIME_SERIES_HOST
);
const {
client,
retrieveCall,
deleteCall,
listCalls,
@@ -52,8 +50,7 @@ const {
const {
getTtsVoices,
getTtsSize,
purgeTtsCache,
synthAudio
purgeTtsCache
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
@@ -65,8 +62,7 @@ const {
lookupAppByPhoneNumber,
lookupCarrierBySid,
lookupSipGatewayBySid,
lookupSmppGatewayBySid,
lookupClientByAccountAndUsername
lookupSmppGatewayBySid
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
@@ -85,7 +81,6 @@ passport.use(authStrategy);
app.locals = app.locals || {};
app.locals = {
...app.locals,
registrar: new Registrar(logger, client),
logger,
retrieveCall,
deleteCall,
@@ -100,7 +95,6 @@ app.locals = {
getTtsVoices,
getTtsSize,
purgeTtsCache,
synthAudio,
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,
@@ -108,7 +102,6 @@ app.locals = {
lookupCarrierBySid,
lookupSipGatewayBySid,
lookupSmppGatewayBySid,
lookupClientByAccountAndUsername,
queryCdrs,
queryCdrsSP,
queryAlerts,
@@ -182,22 +175,11 @@ const server = app.listen(PORT);
const isValidWsKey = (hdr) => {
const username = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME;
const password = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD;
if (username && password) {
if (!hdr) {
// auth header is missing
return false;
}
const token = Buffer.from(`${username}:${password}`).toString('base64');
const arr = /^Basic (.*)$/.exec(hdr);
if (!Array.isArray(arr)) {
// malformed auth header
return false;
}
return arr[1] === token;
}
return true;
const username = process.env.JAMBONZ_RECORD_WS_USERNAME;
const password = process.env.JAMBONZ_RECORD_WS_PASSWORD;
const token = Buffer.from(`${username}:${password}`).toString('base64');
const arr = /^Basic (.*)$/.exec(hdr);
return arr[1] === token;
};
server.on('upgrade', (request, socket, head) => {
@@ -214,7 +196,7 @@ server.on('upgrade', (request, socket, head) => {
/* verify the api key */
if (!isValidWsKey(request.headers['authorization'])) {
logger.info(`invalid auth header: ${request.headers['authorization'] || 'authorization header missing'}`);
logger.info(`invalid auth header: ${request.headers['authorization']}`);
return socket.write('HTTP/1.1 403 Forbidden \r\n\r\n', () => socket.destroy());
}

View File

@@ -1,5 +1,4 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips;
@@ -54,8 +53,6 @@ DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS google_custom_voices;
DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS system_information;
@@ -139,9 +136,6 @@ account_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
username VARCHAR(64),
password VARCHAR(1024),
allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (client_sid)
);
@@ -162,7 +156,7 @@ regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed num
description VARCHAR(1024),
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
) COMMENT='An ordered list of digit patterns in an LCR table. The pat';
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches';
CREATE TABLE lcr
(
@@ -173,7 +167,7 @@ default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use whe
service_provider_sid CHAR(36),
account_sid CHAR(36),
PRIMARY KEY (lcr_sid)
) COMMENT='An LCR (least cost routing) table that is used by a service ';
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.';
CREATE TABLE password_settings
(
@@ -340,20 +334,9 @@ last_tested DATETIME,
tts_tested_ok BOOLEAN,
stt_tested_ok BOOLEAN,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
label VARCHAR(64),
PRIMARY KEY (speech_credential_sid)
);
CREATE TABLE google_custom_voices
(
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL,
PRIMARY KEY (google_custom_voice_sid)
);
CREATE TABLE system_information
(
domain_name VARCHAR(255),
@@ -453,12 +436,11 @@ CREATE TABLE sip_gateways
sip_gateway_sid CHAR(36),
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
netmask INTEGER NOT NULL DEFAULT 32,
port INTEGER COMMENT 'sip signaling port',
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
pad_crypto BOOLEAN NOT NULL DEFAULT 0,
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
@@ -496,18 +478,8 @@ app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
speech_synthesis_label VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
speech_recognizer_label VARCHAR(64),
use_for_fallback_speech BOOLEAN DEFAULT false,
fallback_speech_synthesis_vendor VARCHAR(64),
fallback_speech_synthesis_language VARCHAR(12),
fallback_speech_synthesis_voice VARCHAR(64),
fallback_speech_synthesis_label VARCHAR(64),
fallback_speech_recognizer_vendor VARCHAR(64),
fallback_speech_recognizer_language VARCHAR(64),
fallback_speech_recognizer_label VARCHAR(64),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
record_all_calls BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (application_sid)
@@ -637,6 +609,8 @@ CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -644,10 +618,6 @@ ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (ser
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid);
CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid);
ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE;
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone);
@@ -733,5 +703,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -87,7 +87,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[49E56AF4-4E40-49B6-BA88-4E378F1E6C18]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[13]]></labelWindowIndex>
<labelWindowIndex><![CDATA[12]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[0507BD46-ACAC-48A3-841E-4DEC2FEDCB72]]></uid>
</SQLTable>
@@ -148,7 +148,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[6E651E52-F91E-4086-9A1E-FB3425476B2F]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[23]]></labelWindowIndex>
<labelWindowIndex><![CDATA[22]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[0A95311F-47FA-429F-BAF9-1442C6EE0C0E]]></uid>
</SQLTable>
@@ -225,7 +225,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[D1019218-F1FC-4BC5-A890-F8DBB7153375]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[16]]></labelWindowIndex>
<labelWindowIndex><![CDATA[15]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[0AC2FD20-B22F-48DB-8611-801CEF6BFA12]]></uid>
</SQLTable>
@@ -279,7 +279,7 @@
<uid><![CDATA[755D10B0-F60D-4250-8971-C8E4FDB0E0CD]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<labelWindowIndex><![CDATA[20]]></labelWindowIndex>
<labelWindowIndex><![CDATA[19]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[16B9E383-E044-4D71-AB46-FEB86A46A298]]></uid>
</SQLTable>
@@ -316,7 +316,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[C4EEBFF0-C3CB-4897-8720-12D14DBA93A5]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[7]]></labelWindowIndex>
<labelWindowIndex><![CDATA[6]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[1A80FB9B-419E-483A-86FF-B44A00A44D7F]]></uid>
</SQLTable>
@@ -471,7 +471,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[601FA05B-78A5-4E7E-9983-39BB0E6D18EB]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[27]]></labelWindowIndex>
<labelWindowIndex><![CDATA[26]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[2A735FAB-592C-42E5-9C8B-06B109314799]]></uid>
</SQLTable>
@@ -537,7 +537,7 @@
<indexed><![CDATA[1]]></indexed>
<uid><![CDATA[365FB018-429D-4DA4-AC33-D9D106EA97E5]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[12]]></labelWindowIndex>
<labelWindowIndex><![CDATA[11]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[311D99B2-DC8B-4C4A-A1FC-4AFAA1F450F3]]></uid>
</SQLTable>
@@ -737,7 +737,7 @@
<type><![CDATA[VARCHAR(4096)]]></type>
<uid><![CDATA[7C7DFE92-D7AC-4447-A1C2-E0F10C1EA26A]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[28]]></labelWindowIndex>
<labelWindowIndex><![CDATA[27]]></labelWindowIndex>
<objectComment><![CDATA[A Carrier or customer PBX that can send or receive calls]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[3D3136A7-AFC0-4A70-AEC3-68577955CA2E]]></uid>
@@ -819,7 +819,7 @@
<defaultValue><![CDATA[CURRENT_TIMESTAMP]]></defaultValue>
<uid><![CDATA[C84C9B6A-80B5-4B0B-8C14-EB02F7421BBE]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[37]]></labelWindowIndex>
<labelWindowIndex><![CDATA[36]]></labelWindowIndex>
<objectComment><![CDATA[An authorization token that is used to access the REST api]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[3EDF89A0-FD38-4DF9-BB65-E0FCD0A678BE]]></uid>
@@ -872,7 +872,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[EA4C1A7E-68ED-41D5-9EE9-345DD61F00C7]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[9]]></labelWindowIndex>
<labelWindowIndex><![CDATA[8]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[4893A0F0-BE1B-4322-9034-644528E802DE]]></uid>
</SQLTable>
@@ -884,7 +884,7 @@
<y>958.00</y>
</location>
<size>
<width>302.00</width>
<width>368.00</width>
<height>280.00</height>
</size>
<zorder>14</zorder>
@@ -892,7 +892,6 @@
<name><![CDATA[speech_credential_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<primaryKey>1</primaryKey>
<forcedUnique><![CDATA[1]]></forcedUnique>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[A5B51E8C-D4F3-4D7A-953D-B3082D87A226]]></uid>
@@ -982,12 +981,25 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[8860648C-4790-4A01-9E2E-60DC52A287FA]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[label]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<uid><![CDATA[0D42A22C-DF14-42A1-BDE2-A53AC8B0D8D6]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[22]]></labelWindowIndex>
<SQLIndex>
<name><![CDATA[speech_credentials_idx_1]]></name>
<fieldName><![CDATA[vendor]]></fieldName>
<fieldName><![CDATA[account_sid]]></fieldName>
<SQLIndexEntry>
<name><![CDATA[vendor]]></name>
<prefixSize><![CDATA[]]></prefixSize>
<fieldUid><![CDATA[9D8FCF55-D68E-44D3-90DF-27B5ABD1D0BE]]></fieldUid>
</SQLIndexEntry>
<SQLIndexEntry>
<name><![CDATA[account_sid]]></name>
<prefixSize><![CDATA[]]></prefixSize>
<fieldUid><![CDATA[7E964ED2-EC2E-4BCB-8DEC-C455B87FAC07]]></fieldUid>
</SQLIndexEntry>
<indexNamePrefix><![CDATA[speech_credentials]]></indexNamePrefix>
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[554ABEC2-3E1B-41B1-BF07-25F403D5E3B4]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[21]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[49A68E1C-DEE2-446C-A4EB-9850E16155CC]]></uid>
</SQLTable>
@@ -1008,7 +1020,7 @@
<type><![CDATA[VARCHAR(16)]]></type>
<uid><![CDATA[1EA572BD-FF6B-43CC-9EBB-33A735781429]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[10]]></labelWindowIndex>
<labelWindowIndex><![CDATA[9]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[518AC592-D3E6-4032-8A33-15A3DB72B060]]></uid>
</SQLTable>
@@ -1069,7 +1081,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[6B2F726C-48A6-49D9-B7B1-8850DD6FB3EC]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[15]]></labelWindowIndex>
<labelWindowIndex><![CDATA[14]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[51A02EFE-AA51-46EF-8671-E8B2F1FC5F8D]]></uid>
</SQLTable>
@@ -1121,7 +1133,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[2EA3A57F-7EF7-4958-B06B-62B0279BB87E]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[8]]></labelWindowIndex>
<labelWindowIndex><![CDATA[7]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[5784AC2F-BEBC-466F-9818-F9A7D227A5B5]]></uid>
</SQLTable>
@@ -1171,7 +1183,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[04BB457A-D532-4780-8A58-5900094171EC]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[29]]></labelWindowIndex>
<labelWindowIndex><![CDATA[28]]></labelWindowIndex>
<objectComment><![CDATA[An HTTP callback]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></uid>
@@ -1271,7 +1283,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[9A75A20B-1EFD-4E16-994A-5376C650EAB5]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[11]]></labelWindowIndex>
<labelWindowIndex><![CDATA[10]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[6511AF7D-91FD-40C7-9F73-B8E9E66DC249]]></uid>
</SQLTable>
@@ -1340,7 +1352,7 @@
<uid><![CDATA[9B4208B5-9E3B-4B76-B7F7-4E5D36B99BF2]]></uid>
<unsigned><![CDATA[0]]></unsigned>
</SQLField>
<labelWindowIndex><![CDATA[36]]></labelWindowIndex>
<labelWindowIndex><![CDATA[35]]></labelWindowIndex>
<objectComment><![CDATA[a regex-based pattern match for call routing]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[78584D93-2CD7-4495-9C5E-893C7B869133]]></uid>
@@ -1377,7 +1389,7 @@
<noQuoteDefault><![CDATA[1]]></noQuoteDefault>
<uid><![CDATA[4D2F7B02-F183-4239-8CE8-3E98206708AE]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[14]]></labelWindowIndex>
<labelWindowIndex><![CDATA[13]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[86FAB0AB-DC68-4ADF-8A08-BBAF61BA1840]]></uid>
</SQLTable>
@@ -1415,7 +1427,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[673137EA-B74C-4BA7-AD25-1B71360A2E26]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[6]]></labelWindowIndex>
<labelWindowIndex><![CDATA[5]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[87F254ED-D381-48E3-8E8F-C0F3D99CC01C]]></uid>
</SQLTable>
@@ -1447,7 +1459,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[8998AAD6-A21C-4697-9660-8DC5005AED07]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[5]]></labelWindowIndex>
<labelWindowIndex><![CDATA[4]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[8E75DB2D-9078-40E6-88BF-7DDED5033362]]></uid>
</SQLTable>
@@ -1524,7 +1536,7 @@
<uid><![CDATA[1DDAD1A1-942D-4487-89C8-D496B7F82274]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<labelWindowIndex><![CDATA[26]]></labelWindowIndex>
<labelWindowIndex><![CDATA[25]]></labelWindowIndex>
<objectComment><![CDATA[A Microsoft Teams customer tenant]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[92FD042A-5AEC-4D8F-AB94-C73C0F566F75]]></uid>
@@ -1595,7 +1607,7 @@
<objectComment><![CDATA[lower priority carriers are attempted first]]></objectComment>
<uid><![CDATA[01F61C68-799B-49B0-9E6A-0E2162EE5A54]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[32]]></labelWindowIndex>
<labelWindowIndex><![CDATA[31]]></labelWindowIndex>
<objectComment><![CDATA[An entry in the LCR routing list]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[956025F5-0798-47F7-B76C-457814C7B52E]]></uid>
@@ -1809,7 +1821,7 @@
<objectComment><![CDATA[credential used to authenticate with storage service]]></objectComment>
<uid><![CDATA[E81859C0-DCBD-4FF3-BEEF-FA575394326B]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[34]]></labelWindowIndex>
<labelWindowIndex><![CDATA[33]]></labelWindowIndex>
<objectComment><![CDATA[An enterprise that uses the platform for comm services]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[985D6997-B1A7-4AB3-80F4-4D59B45480C8]]></uid>
@@ -1848,7 +1860,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[D0BF7D36-E40C-4385-9BA5-2099B49A1042]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[21]]></labelWindowIndex>
<labelWindowIndex><![CDATA[20]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[A8ED2178-3CC4-4174-A8FC-C2B58FD28214]]></uid>
</SQLTable>
@@ -1940,7 +1952,7 @@
<type><![CDATA[VARCHAR(32)]]></type>
<uid><![CDATA[CE2015BC-8538-4FB0-B4D9-454436FAB1D9]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[17]]></labelWindowIndex>
<labelWindowIndex><![CDATA[16]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[AF34726D-EDFD-414E-9B44-5243DA9D9497]]></uid>
</SQLTable>
@@ -2010,7 +2022,7 @@
<indexed><![CDATA[1]]></indexed>
<uid><![CDATA[EA936343-89D3-4E3F-BD92-9F8967DC27C4]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[4]]></labelWindowIndex>
<labelWindowIndex><![CDATA[3]]></labelWindowIndex>
<objectComment><![CDATA[An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[AFD51FD4-44C8-4442-94C2-0DFA93CD48AE]]></uid>
@@ -2019,8 +2031,8 @@
<name><![CDATA[dns_records]]></name>
<schema><![CDATA[]]></schema>
<location>
<x>1270.00</x>
<y>1425.00</y>
<x>947.00</x>
<y>1303.00</y>
</location>
<size>
<width>262.00</width>
@@ -2062,72 +2074,10 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[73092A7A-9F3F-4C49-8478-39CE5DAF5ADD]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[19]]></labelWindowIndex>
<labelWindowIndex><![CDATA[18]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[B10C0DE3-03CD-4C5A-B1FB-F9F81ED14A40]]></uid>
</SQLTable>
<SQLTable>
<name><![CDATA[google_custom_voices]]></name>
<schema><![CDATA[]]></schema>
<location>
<x>1803.00</x>
<y>1036.00</y>
</location>
<size>
<width>521.00</width>
<height>120.00</height>
</size>
<zorder>37</zorder>
<SQLField>
<name><![CDATA[google_custom_voice_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<primaryKey>1</primaryKey>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[D246DAFE-98AB-4DC6-A384-1DFE8E40573D]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[speech_credential_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>speech_credential_sid</referencesField>
<referencesTable>speech_credentials</referencesTable>
<deleteAction>1</deleteAction>
<referencesField><![CDATA[speech_credential_sid]]></referencesField>
<referencesTable><![CDATA[speech_credentials]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>1</destinationCardinality>
<referencesFieldUID><![CDATA[A5B51E8C-D4F3-4D7A-953D-B3082D87A226]]></referencesFieldUID>
<referencesTableUID><![CDATA[49A68E1C-DEE2-446C-A4EB-9850E16155CC]]></referencesTableUID>
<forcedUnique><![CDATA[0]]></forcedUnique>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[AB763F3E-8E49-46EE-BE99-5ED02A688E93]]></uid>
<unique><![CDATA[0]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[model]]></name>
<type><![CDATA[VARCHAR(512)]]></type>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[CB4E2A6B-7C59-4C8F-B5F5-5F27200B971F]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[reported_usage]]></name>
<type><![CDATA[ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE')]]></type>
<defaultValue><![CDATA[REALTIME]]></defaultValue>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[A17005D7-81E3-4E12-91EB-6DB23DEAA618]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[name]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[D22EED9A-3502-489E-BE0A-5609B76697A8]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[1]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[B83321B9-0B89-4E5F-95D4-864ADE2EC405]]></uid>
</SQLTable>
<SQLTable>
<name><![CDATA[phone_numbers]]></name>
<schema><![CDATA[]]></schema>
@@ -2238,7 +2188,7 @@
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[4E84523A-7F30-4A5D-A0A8-578652102BD0]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[35]]></labelWindowIndex>
<labelWindowIndex><![CDATA[34]]></labelWindowIndex>
<objectComment><![CDATA[A phone number that has been assigned to an account]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[BA650DDC-AC7B-4DFE-A5E5-828C75607807]]></uid>
@@ -2247,12 +2197,12 @@
<name><![CDATA[clients]]></name>
<schema><![CDATA[]]></schema>
<location>
<x>974.00</x>
<y>1496.00</y>
<x>916.00</x>
<y>1447.00</y>
</location>
<size>
<width>282.00</width>
<height>180.00</height>
<width>228.00</width>
<height>120.00</height>
</size>
<zorder>36</zorder>
<SQLField>
@@ -2297,28 +2247,7 @@
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[2963DB28-7248-4D58-92E6-F21F2EB9E680]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[allow_direct_app_calling]]></name>
<type><![CDATA[BOOLEAN]]></type>
<defaultValue><![CDATA[1]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[EA252940-708B-4BD3-B211-5EC59AA540D3]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[allow_direct_queue_calling]]></name>
<type><![CDATA[BOOLEAN]]></type>
<defaultValue><![CDATA[1]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[76CE0DFC-7CC6-46EE-82C6-2CF16C7EAD4D]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[allow_direct_user_calling]]></name>
<type><![CDATA[BOOLEAN]]></type>
<defaultValue><![CDATA[1]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[206E169C-E5F6-4774-8D4A-5BECF25E49A3]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[2]]></labelWindowIndex>
<labelWindowIndex><![CDATA[1]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[C2CDB45C-3F50-40B3-B8CE-2F093EB0D517]]></uid>
</SQLTable>
@@ -2332,7 +2261,7 @@
</location>
<size>
<width>281.00</width>
<height>240.00</height>
<height>220.00</height>
</size>
<zorder>7</zorder>
<SQLField>
@@ -2358,7 +2287,8 @@
<SQLField>
<name><![CDATA[port]]></name>
<type><![CDATA[INTEGER]]></type>
<notNull><![CDATA[0]]></notNull>
<defaultValue><![CDATA[5060]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<objectComment><![CDATA[sip signaling port]]></objectComment>
<uid><![CDATA[26B20F1E-4DB0-48C0-90F7-CA90A06A1070]]></uid>
</SQLField>
@@ -2399,13 +2329,6 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[27D4A5BD-8093-4ADD-B5B5-D546844206F9]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[pad_crypto]]></name>
<type><![CDATA[BOOLEAN]]></type>
<defaultValue><![CDATA[0]]></defaultValue>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[C5C0043B-100A-4476-BF01-BE0777AE27C0]]></uid>
</SQLField>
<SQLIndex>
<name><![CDATA[sip_gateway_idx_hostport]]></name>
<fieldName><![CDATA[ipv4]]></fieldName>
@@ -2430,7 +2353,7 @@
<objectComment><![CDATA[Outbound call protocol]]></objectComment>
<uid><![CDATA[30661D66-96EC-4B02-995C-5E7EB8A3BD70]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[31]]></labelWindowIndex>
<labelWindowIndex><![CDATA[30]]></labelWindowIndex>
<objectComment><![CDATA[A whitelisted sip gateway used for origination/termination]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[D8A564E2-DA41-4217-8ACE-06CF77E9BEC1]]></uid>
@@ -2445,7 +2368,7 @@
</location>
<size>
<width>345.00</width>
<height>540.00</height>
<height>340.00</height>
</size>
<zorder>0</zorder>
<SQLField>
@@ -2564,12 +2487,6 @@
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[929D66F0-64B9-4D7C-AB4B-24F131E1178F]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[speech_synthesis_label]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[BFA24DF2-9CF5-47B0-848D-8B685B7C6750]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[speech_recognizer_vendor]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
@@ -2584,65 +2501,10 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[A03AFB7B-492F-48E3-AE3C-B1416D5B6B12]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[speech_recognizer_label]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[A247A784-CCD6-40B4-9D0A-2F0EF8F8AFD2]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[use_for_fallback_speech]]></name>
<type><![CDATA[BOOLEAN]]></type>
<defaultValue><![CDATA[false]]></defaultValue>
<uid><![CDATA[DDA48DD6-4B0F-4AD5-9B32-D508BBA1A8EE]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[fallback_speech_synthesis_vendor]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[26BBDEEF-E179-4280-9917-6F2BD6367459]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[fallback_speech_synthesis_language]]></name>
<type><![CDATA[VARCHAR(12)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[E008D6D7-9BB7-4372-8B46-F92C0EB15082]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[fallback_speech_synthesis_voice]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[6A0E92C9-32B9-4179-A893-3DADF5DD7728]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[fallback_speech_synthesis_label]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[8576DEF6-D81A-4D4D-8980-00580779D164]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[fallback_speech_recognizer_vendor]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[14ECF5EA-81C5-4EAE-9575-9785CEB672E6]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[fallback_speech_recognizer_language]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[EC792500-6B2B-4E54-AA89-43E7A0FD8642]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[fallback_speech_recognizer_label]]></name>
<type><![CDATA[VARCHAR(64)]]></type>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[65AA5173-6523-49F7-9D95-78C4B3A7C7E6]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[created_at]]></name>
<type><![CDATA[DATETIME]]></type>
<defaultValue><![CDATA[CURRENT_TIMESTAMP]]></defaultValue>
<forcedUnique><![CDATA[0]]></forcedUnique>
<noQuoteDefault><![CDATA[1]]></noQuoteDefault>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[C09B1BDB-8390-4B8A-B70A-642EC5E12899]]></uid>
@@ -2672,7 +2534,7 @@
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[3FDDDF3B-375D-4DE4-B759-514438845F7D]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[33]]></labelWindowIndex>
<labelWindowIndex><![CDATA[32]]></labelWindowIndex>
<objectComment><![CDATA[A defined set of behaviors to be applied to phone calls ]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[E97EE4F0-7ED7-4E8C-862E-D98192D6EAE0]]></uid>
@@ -2780,7 +2642,7 @@
<type><![CDATA[VARBINARY(52)]]></type>
<uid><![CDATA[B4793720-635C-4E25-A306-62E7416541C4]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[18]]></labelWindowIndex>
<labelWindowIndex><![CDATA[17]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[EB4BC5F9-CC10-4C8C-AB31-6D942256AEFB]]></uid>
</SQLTable>
@@ -2811,7 +2673,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[0A8DB34E-76C9-4D40-9E31-786E0228DCEE]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[3]]></labelWindowIndex>
<labelWindowIndex><![CDATA[2]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F0C2DC80-CBA7-4BE7-8856-D10B062A7B17]]></uid>
</SQLTable>
@@ -2896,7 +2758,7 @@
<type><![CDATA[DATETIME]]></type>
<uid><![CDATA[CD43B91B-F34E-4422-9C0F-A4B92E2E7B95]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[25]]></labelWindowIndex>
<labelWindowIndex><![CDATA[24]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F0EE651E-DBF6-4CAC-A517-AC85BCC2D3AF]]></uid>
</SQLTable>
@@ -2956,7 +2818,7 @@
<uid><![CDATA[B73773BA-AB1B-47AA-B995-2D2FE006198F]]></uid>
<unique><![CDATA[0]]></unique>
</SQLField>
<labelWindowIndex><![CDATA[30]]></labelWindowIndex>
<labelWindowIndex><![CDATA[29]]></labelWindowIndex>
<objectComment><![CDATA[An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F283D572-F670-4571-91FD-A665A9D3E15D]]></uid>
@@ -3023,7 +2885,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[FA39B463-61C7-4654-BE9C-D1AC39AB1B97]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[24]]></labelWindowIndex>
<labelWindowIndex><![CDATA[23]]></labelWindowIndex>
<objectComment><![CDATA[A partition of the platform used by one service provider]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F294B51E-F867-47CA-BC1F-F70BDF8170FF]]></uid>
@@ -3097,17 +2959,17 @@
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
<PageGridVisible><![CDATA[0]]></PageGridVisible>
<RightSidebarWidth><![CDATA[1924.000000]]></RightSidebarWidth>
<RightSidebarWidth><![CDATA[1681.000000]]></RightSidebarWidth>
<sidebarIndex><![CDATA[2]]></sidebarIndex>
<snapToGrid><![CDATA[0]]></snapToGrid>
<SourceSidebarWidth><![CDATA[0.000000]]></SourceSidebarWidth>
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
<windowHeight><![CDATA[985.000000]]></windowHeight>
<windowLocationX><![CDATA[-1307.000000]]></windowLocationX>
<windowLocationY><![CDATA[1008.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{1.5, 786}]]></windowScrollOrigin>
<windowWidth><![CDATA[2201.000000]]></windowWidth>
<windowHeight><![CDATA[1055.000000]]></windowHeight>
<windowLocationX><![CDATA[0.000000]]></windowLocationX>
<windowLocationY><![CDATA[24.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{157, 832}]]></windowScrollOrigin>
<windowWidth><![CDATA[1682.000000]]></windowWidth>
</SQLDocumentInfo>
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>

View File

@@ -22,7 +22,7 @@ values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9
-- create one service provider and one account
insert into service_providers (service_provider_sid, name, root_domain)
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.cloud');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.us');
insert into accounts (account_sid, service_provider_sid, name, webhook_secret)
values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'wh_secret_cJqgtMDPzDhhnjmaJH6Mtk');
@@ -38,9 +38,9 @@ values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36fs', '38700987-c7a4-4685-a5bb-af378f9
-- create two applications
insert into webhooks(webhook_sid, url, method)
values
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.cloud/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.cloud/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.cloud/dial-time', 'POST');
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
VALUES

View File

@@ -24,9 +24,10 @@ values ('09e92f3c-9d73-4303-b63f-3668574862ce', '1cf2f4f4-64c4-4249-9a3e-5bb4cb5
-- create two applications
insert into webhooks(webhook_sid, url, method)
values
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.cloud/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.cloud/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.cloud/dial-time', 'POST');
('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'),
('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'),
('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST');
insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
VALUES
('7087fe50-8acb-4f3b-b820-97b573723aab', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'hello world', 'd31568d0-b193-4a05-8ff6-778369bc6efe', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'),

View File

@@ -143,7 +143,7 @@ const sql = {
8004: [
'alter table accounts add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
'alter table accounts add column bucket_credential VARCHAR(8192)',
'alter table accounts add column record_format VARCHAR(16) NOT NULL DEFAULT \'mp3\'',
'alter table accounts add column record_format VARCHAR(16) NOT NULL DEFAULT `mp3`',
'alter table applications add column record_all_calls BOOLEAN NOT NULL DEFAULT false',
'alter table phone_numbers DROP INDEX number',
'create unique index phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid)',
@@ -157,40 +157,7 @@ const sql = {
PRIMARY KEY (client_sid)
)`,
'CREATE INDEX client_sid_idx ON clients (client_sid)',
'ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid)',
'ALTER TABLE sip_gateways ADD COLUMN protocol ENUM(\'udp\',\'tcp\',\'tls\', \'tls/srtp\') DEFAULT \'udp\''
],
8005: [
'DROP INDEX speech_credentials_idx_1 ON speech_credentials',
'ALTER TABLE speech_credentials ADD COLUMN label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN speech_synthesis_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN speech_recognizer_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN use_for_fallback_speech BOOLEAN DEFAULT false',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_vendor VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_language VARCHAR(12)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_voice VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_synthesis_label VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_vendor VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_language VARCHAR(64)',
'ALTER TABLE applications ADD COLUMN fallback_speech_recognizer_label VARCHAR(64)',
'ALTER TABLE sip_gateways ADD COLUMN pad_crypto BOOLEAN NOT NULL DEFAULT 0',
'ALTER TABLE sip_gateways MODIFY port INTEGER',
`CREATE TABLE google_custom_voices
(
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL,
PRIMARY KEY (google_custom_voice_sid)
)
`,
'CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid)',
'CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid)',
'ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE',
'ALTER TABLE clients ADD COLUMN allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1',
'ALTER TABLE clients ADD COLUMN allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1',
'ALTER TABLE clients ADD COLUMN allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1'
'ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid)'
]
};
@@ -221,8 +188,6 @@ const doIt = async() => {
if (val < 7007) upgrades.push(...sql['7007']);
if (val < 8000) upgrades.push(...sql['8000']);
if (val < 8003) upgrades.push(...sql['8003']);
if (val < 8004) upgrades.push(...sql['8004']);
if (val < 8005) upgrades.push(...sql['8005']);
// perform all upgrades
logger.info({upgrades}, 'applying schema upgrades..');

View File

@@ -2,7 +2,7 @@ SET FOREIGN_KEY_CHECKS=0;
-- create one service provider
insert into service_providers (service_provider_sid, name, description, root_domain)
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.cloud', 'jambonz.cloud service provider', 'sip.yakeeda.com');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.us', 'jambonz.us service provider', 'sip.yakeeda.com');
insert into api_keys (api_key_sid, token)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
@@ -19,8 +19,8 @@ insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound
values ('46b727eb-c7dc-44fa-b063-96e48d408e4a', '5145b436-2f38-4029-8d4c-fd8c67831c7a', '3.3.3.3', 5060, 1, 1, 1);
-- create the test application and test phone number
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.cloud/testCall', 'POST');
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.cloud/callStatus', 'POST');
insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.us/testCall', 'POST');
insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.us/callStatus', 'POST');
insert into applications (application_sid, name, service_provider_sid, call_hook_sid, call_status_hook_sid,
speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language)
values ('7a489343-02ed-471e-8df0-fc5e1b98ce8f', 'Test application', '2708b1b3-2736-40ea-b502-c53d8396247f',

View File

@@ -18,14 +18,10 @@ const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
}),
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
}),
...(process.env.JAMBONES_REDIS_SENTINEL_SENTINAL_PASSWORD && {
sentinelPassword: process.env.JAMBONES_REDIS_SENTINEL_SENTINAL_PASSWORD
}),
})
} : null;
const {
client,
retrieveCall,
deleteCall,
listCalls,
@@ -43,7 +39,6 @@ const {
}, logger);
module.exports = {
client,
retrieveCall,
deleteCall,
listCalls,

View File

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

View File

@@ -34,7 +34,7 @@ AND effective_end_date IS NULL
AND pending=0`;
const updatePaymentInfoSql = `UPDATE account_subscriptions
SET last4 = ?, stripe_payment_method_id=?, exp_month = ?, exp_year = ?, card_type = ?
SET last4 = ?, exp_month = ?, exp_year = ?, card_type = ?
WHERE account_sid = ?
AND effective_end_date IS NULL`;
@@ -206,10 +206,10 @@ class Account extends Model {
}
static async updatePaymentInfo(logger, account_sid, pm) {
const {id, card} = pm;
const {card} = pm;
const last4_encrypted = encrypt(card.last4);
await promisePool.execute(updatePaymentInfoSql,
[last4_encrypted, id, card.exp_month, card.exp_year, card.brand, account_sid]);
[last4_encrypted, card.exp_month, card.exp_year, card.brand, account_sid]);
}
static async provisionPendingSubscription(logger, account_sid, products, payment_method, subscription_id) {

View File

@@ -52,18 +52,6 @@ Client.fields = [
{
name: 'password',
type: 'string'
},
{
name: 'allow_direct_app_calling',
type: 'number'
},
{
name: 'allow_direct_queue_calling',
type: 'number'
},
{
name: 'allow_direct_user_calling',
type: 'number'
}
];

View File

@@ -1,61 +0,0 @@
const Model = require('./model');
const {promisePool} = require('../db');
class GoogleCustomVoice extends Model {
constructor() {
super();
}
static async retrieveAllBySpeechCredentialSid(speech_credential_sid) {
const sql = `SELECT * FROM ${this.table} WHERE speech_credential_sid = ?`;
const [rows] = await promisePool.query(sql, speech_credential_sid);
return rows;
}
static async deleteAllBySpeechCredentialSid(speech_credential_sid) {
const sql = `DELETE FROM ${this.table} WHERE speech_credential_sid = ?`;
const [rows] = await promisePool.query(sql, speech_credential_sid);
return rows;
}
static async retrieveAllByLabel(service_provider_sid, account_sid, label) {
let sql;
if (account_sid) {
sql = `SELECT gcv.* FROM ${this.table} gcv
LEFT JOIN speech_credentials sc ON gcv.speech_credential_sid = sc.speech_credential_sid
WHERE sc.account_sid = ? OR (sc.account_sid is NULL && sc.service_provider_sid = ?)
${label ? 'AND label = ?' : 'AND label is NULL'}`;
} else {
sql = `SELECT gcv.* FROM ${this.table} gcv
LEFT JOIN speech_credentials sc ON gcv.speech_credential_sid = sc.speech_credential_sid
WHERE sc.service_provider_sid = ? ${label ? 'AND label = ?' : 'AND label is NULL'}`;
}
const [rows] = await promisePool.query(sql, [...(account_sid ?
[account_sid, service_provider_sid] : [service_provider_sid]), label]);
return rows;
}
}
GoogleCustomVoice.table = 'google_custom_voices';
GoogleCustomVoice.fields = [
{
name: 'google_custom_voice_sid',
type: 'string',
primaryKey: true
},
{
name: 'model',
type: 'string',
required: true
},
{
name: 'reported_usage',
type: 'number'
},
{
name: 'name',
type: 'string',
required: true
}
];
module.exports = GoogleCustomVoice;

View File

@@ -1,7 +1,6 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sqlRetrieveAll = 'SELECT * from phone_numbers WHERE account_sid = ? ORDER BY number';
const sqlRetrieveOne = 'SELECT * from phone_numbers WHERE phone_number_sid = ? AND account_sid = ? ORDER BY number';
const sql = 'SELECT * from phone_numbers WHERE account_sid = ? ORDER BY number';
const sqlSP = `SELECT *
FROM phone_numbers
WHERE account_sid IN
@@ -17,8 +16,8 @@ class PhoneNumber extends Model {
}
static async retrieveAll(account_sid) {
if (!account_sid) return await super.retrieveAll();
const [rows] = await promisePool.query(sqlRetrieveAll, account_sid);
if (!account_sid) return super.retrieveAll();
const [rows] = await promisePool.query(sql, account_sid);
return rows;
}
static async retrieveAllForSP(service_provider_sid) {
@@ -31,7 +30,7 @@ class PhoneNumber extends Model {
*/
static async retrieve(sid, account_sid) {
if (!account_sid) return super.retrieve(sid);
const [rows] = await promisePool.query(sqlRetrieveOne, [sid, account_sid]);
const [rows] = await promisePool.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid]);
return rows;
}
}

View File

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

View File

@@ -20,18 +20,6 @@ class SpeechCredential extends Model {
return rows;
}
static async getSpeechCredentialsByVendorAndLabel(service_provider_sid, account_sid, vendor, label) {
let sql;
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 = ?' : ''}`;
}
const [rows] = await promisePool.query(sql, [account_sid ? account_sid : service_provider_sid, vendor, label]);
return rows;
}
static async disableStt(account_sid) {
await promisePool.execute('UPDATE speech_credentials SET use_for_stt = 0 WHERE account_sid = ?', [account_sid]);
}
@@ -98,10 +86,6 @@ SpeechCredential.fields = [
{
name: 'last_tested',
type: 'date'
},
{
name: 'label',
type: 'string'
}
];

View File

@@ -1,41 +0,0 @@
const { Writable } = require('stream');
const { BlobServiceClient } = require('@azure/storage-blob');
const { v4: uuidv4 } = require('uuid');
class AzureStorageUploadStream extends Writable {
constructor(logger, opts) {
super(opts);
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
this.blockBlobClient = blobServiceClient.getContainerClient(opts.bucketName).getBlockBlobClient(opts.Key);
this.metadata = opts.metadata;
this.blocks = [];
}
async _write(chunk, encoding, callback) {
const blockID = uuidv4().replace(/-/g, '');
this.blocks.push(blockID);
try {
await this.blockBlobClient.stageBlock(blockID, chunk, chunk.length);
callback();
} catch (error) {
callback(error);
}
}
async _final(callback) {
try {
await this.blockBlobClient.commitBlockList(this.blocks);
// remove all null/undefined props
const filteredObj = Object.entries(this.metadata).reduce((acc, [key, val]) => {
if (val !== undefined && val !== null) acc[key] = val;
return acc;
}, {});
await this.blockBlobClient.setMetadata(filteredObj);
callback();
} catch (error) {
callback(error);
}
}
}
module.exports = AzureStorageUploadStream;

View File

@@ -2,7 +2,7 @@ const { Transform } = require('stream');
const lamejs = require('@jambonz/lamejs');
class PCMToMP3Encoder extends Transform {
constructor(options, logger) {
constructor(options) {
super(options);
const channels = options.channels || 1;
@@ -11,40 +11,33 @@ class PCMToMP3Encoder extends Transform {
this.encoder = new lamejs.Mp3Encoder(channels, sampleRate, bitRate);
this.channels = channels;
this.logger = logger;
}
_transform(chunk, encoding, callback) {
try {
// Convert chunk buffer into Int16Array for lamejs
const samples = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
// Convert chunk buffer into Int16Array for lamejs
const samples = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
// Split input samples into left and right channel arrays if stereo
let leftChannel, rightChannel;
if (this.channels === 2) {
leftChannel = new Int16Array(samples.length / 2);
rightChannel = new Int16Array(samples.length / 2);
// Split input samples into left and right channel arrays if stereo
let leftChannel, rightChannel;
if (this.channels === 2) {
leftChannel = new Int16Array(samples.length / 2);
rightChannel = new Int16Array(samples.length / 2);
for (let i = 0; i < samples.length; i += 2) {
leftChannel[i / 2] = samples[i];
rightChannel[i / 2] = samples[i + 1];
}
} else {
leftChannel = samples;
for (let i = 0; i < samples.length; i += 2) {
leftChannel[i / 2] = samples[i];
rightChannel[i / 2] = samples[i + 1];
}
// Encode the input data
const mp3Data = this.encoder.encodeBuffer(leftChannel, rightChannel);
if (mp3Data.length > 0) {
this.push(Buffer.from(mp3Data));
}
callback();
} catch (err) {
this.logger.error(
{ err },
'Error while mp3 transform');
} else {
leftChannel = samples;
}
// Encode the input data
const mp3Data = this.encoder.encodeBuffer(leftChannel, rightChannel);
if (mp3Data.length > 0) {
this.push(Buffer.from(mp3Data));
}
callback();
}
_flush(callback) {

View File

@@ -1,41 +0,0 @@
const { Storage } = require('@google-cloud/storage');
const { Writable } = require('stream');
class GoogleStorageUploadStream extends Writable {
constructor(logger, opts) {
super(opts);
this.logger = logger;
this.metadata = opts.metadata;
const storage = new Storage(opts.bucketCredential);
this.gcsFile = storage.bucket(opts.bucketName).file(opts.Key);
this.writeStream = this.gcsFile.createWriteStream();
this.writeStream.on('error', (err) => this.logger.error(err));
this.writeStream.on('finish', () => {
this.logger.info('google storage Upload completed.');
this._addMetadata();
});
}
_write(chunk, encoding, callback) {
this.writeStream.write(chunk, encoding, callback);
}
_final(callback) {
this.writeStream.end();
this.writeStream.once('finish', callback);
}
async _addMetadata() {
try {
await this.gcsFile.setMetadata({metadata: this.metadata});
this.logger.info('Google storage Upload and metadata setting completed.');
} catch (err) {
this.logger.error(err, 'Google storage An error occurred while setting metadata');
}
}
}
module.exports = GoogleStorageUploadStream;

View File

@@ -1,6 +1,17 @@
async function record(logger, socket) {
return require('./upload')(logger, socket);
const path = require('node:path');
async function record(logger, socket, url) {
const p = path.basename(url);
const idx = p.lastIndexOf('/');
const vendor = p.substring(idx + 1);
switch (vendor) {
case 'aws_s3':
return require('./s3')(logger, socket);
default:
logger.info(`unknown bucket vendor: ${vendor}`);
socket.send(`unknown bucket vendor: ${vendor}`);
socket.close();
}
}
module.exports = record;

View File

@@ -11,7 +11,7 @@ class S3MultipartUploadStream extends Writable {
super(opts);
this.logger = logger;
this.bucketName = opts.bucketName;
this.objectKey = opts.objectKey;
this.objectKey = opts.Key;
this.uploadId = null;
this.partNumber = 1;
this.multipartETags = [];

View File

@@ -1,20 +1,20 @@
const Account = require('../models/account');
const Websocket = require('ws');
const PCMToMP3Encoder = require('./encoder');
const S3MultipartUploadStream = require('./s3-multipart-upload-stream');
const wav = require('wav');
const { getUploader } = require('./utils');
async function upload(logger, socket) {
socket._recvInitialMetadata = false;
socket.on('message', async function(data, isBinary) {
try {
if (!isBinary && !socket._recvInitialMetadata) {
socket._recvInitialMetadata = true;
logger.debug(`initial metadata: ${data}`);
const obj = JSON.parse(data.toString());
logger.info({ obj }, 'received JSON message from jambonz');
const { sampleRate, accountSid, callSid, direction, from, to,
callId, applicationSid, originatingSipIp, originatingSipTrunkName } = obj;
logger.info({obj}, 'received JSON message from jambonz');
const {sampleRate, accountSid, callSid, direction, from, to,
callId, applicationSid, originatingSipIp, originatingSipTrunkName} = obj;
const account = await Account.retrieve(accountSid);
if (account && account.length && account[0].bucket_credential) {
const obj = account[0].bucket_credential;
@@ -38,19 +38,27 @@ async function upload(logger, socket) {
}
// create S3 path
const day = new Date();
let key = `${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
key += `/${day.getDate().toString().padStart(2, '0')}/${callSid}.${account[0].record_format}`;
let Key = `${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
Key += `/${day.getDate().toString().padStart(2, '0')}/${callSid}.${account[0].record_format}`;
// Uploader
const uploadStream = getUploader(key, metadata, obj, logger);
if (!uploadStream) {
logger.info('There is no available record uploader, close the socket.');
socket.close();
}
const uploaderOpts = {
bucketName: obj.name,
Key,
metadata,
bucketCredential: {
credentials: {
accessKeyId: obj.access_key_id,
secretAccessKey: obj.secret_access_key,
},
region: obj.region || 'us-east-1'
}
};
const uploadStream = new S3MultipartUploadStream(logger, uploaderOpts);
/**encoder */
let encoder;
if (account[0].record_format === 'wav') {
if (obj.output_format === 'wav') {
encoder = new wav.Writer({ channels: 2, sampleRate, bitDepth: 16 });
} else {
// default is mp3
@@ -58,41 +66,28 @@ async function upload(logger, socket) {
channels: 2,
sampleRate: sampleRate,
bitrate: 128
}, logger);
});
}
const handleError = (err, streamType) => {
logger.error(
{ err },
`Error while streaming for vendor: ${obj.vendor}, pipe: ${streamType}: ${err.message}`
);
};
/* start streaming data */
const duplex = Websocket.createWebSocketStream(socket);
duplex
.on('error', (err) => handleError(err, 'duplex'))
.pipe(encoder)
.on('error', (err) => handleError(err, 'encoder'))
.pipe(uploadStream)
.on('error', (err) => handleError(err, 'uploadStream'));
duplex.pipe(encoder).pipe(uploadStream);
} else {
logger.info(`account ${accountSid} does not have any bucket credential, close the socket`);
socket.close();
}
}
} catch (err) {
logger.error({ err, data }, 'error parsing message during connection');
logger.error({err}, 'error parsing message during connection');
}
});
socket.on('error', function(err) {
logger.error({ err }, 'record upload: error');
logger.error({err}, 'aws upload: error');
});
socket.on('close', (data) => {
logger.info({ data }, 'record upload: close');
logger.info({data}, 'aws_s3: close');
});
socket.on('end', function(err) {
logger.error({ err }, 'record upload: socket closed from jambonz');
logger.error({err}, 'aws upload: socket closed from jambonz');
});
}

View File

@@ -1,58 +0,0 @@
const AzureStorageUploadStream = require('./azure-storage');
const GoogleStorageUploadStream = require('./google-storage');
const S3MultipartUploadStream = require('./s3-multipart-upload-stream');
const getUploader = (key, metadata, bucket_credential, logger) => {
const uploaderOpts = {
bucketName: bucket_credential.name,
objectKey: key,
metadata
};
try {
switch (bucket_credential.vendor) {
case 'aws_s3':
uploaderOpts.bucketCredential = {
credentials: {
accessKeyId: bucket_credential.access_key_id,
secretAccessKey: bucket_credential.secret_access_key,
},
region: bucket_credential.region || 'us-east-1'
};
return new S3MultipartUploadStream(logger, uploaderOpts);
case 's3_compatible':
uploaderOpts.bucketCredential = {
endpoint: bucket_credential.endpoint,
credentials: {
accessKeyId: bucket_credential.access_key_id,
secretAccessKey: bucket_credential.secret_access_key,
},
region: 'us-east-1',
forcePathStyle: true
};
return new S3MultipartUploadStream(logger, uploaderOpts);
case 'google':
const serviceKey = JSON.parse(bucket_credential.service_key);
uploaderOpts.bucketCredential = {
projectId: serviceKey.project_id,
credentials: {
client_email: serviceKey.client_email,
private_key: serviceKey.private_key
}
};
return new GoogleStorageUploadStream(logger, uploaderOpts);
case 'azure':
uploaderOpts.connection_string = bucket_credential.connection_string;
return new AzureStorageUploadStream(logger, uploaderOpts);
default:
logger.error(`unknown bucket vendor: ${bucket_credential.vendor}`);
break;
}
} catch (err) {
logger.error(`Error creating uploader, vendor: ${bucket_credential.vendor}, reason: ${err.message}`);
}
return null;
};
module.exports = {
getUploader
};

View File

@@ -1,7 +1,7 @@
const router = require('express').Router();
const assert = require('assert');
const request = require('request');
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest, DbError} = require('../../utils/errors');
const Account = require('../../models/account');
const Application = require('../../models/application');
const Webhook = require('../../models/webhook');
@@ -23,8 +23,8 @@ const {
} = require('./utils');
const short = require('short-uuid');
const VoipCarrier = require('../../models/voip-carrier');
const { encrypt } = require('../../utils/encrypt-decrypt');
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
const { encrypt, decrypt } = require('../../utils/encrypt-decrypt');
const { testAwsS3 } = require('../../utils/storage-utils');
const translator = short();
let idx = 0;
@@ -41,14 +41,20 @@ const getFsUrl = async(logger, retrieveSet, setName) => {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const f = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/createCall`;
const ip = stripPort(fs[idx++ % fs.length]);
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
return `http://${ip}:3000/v1/createCall`;
} catch (err) {
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
}
};
const stripPort = (hostport) => {
const arr = /^(.*):(.*)$/.exec(hostport);
if (arr) return arr[1];
return hostport;
};
const validateRequest = async(req, account_sid) => {
try {
if (req.user.hasScope('admin')) {
@@ -146,49 +152,6 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
}
});
router.get('/:sid/RegisteredSipUsers', async(req, res) => {
const {logger, registrar} = req.app.locals;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const result = await Account.retrieve(account_sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
}
const users = await registrar.getRegisteredUsersForRealm(result[0].sip_realm);
res.status(200).json(users.map((u) => `${u}@${result[0].sip_realm}`));
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid/RegisteredSipUsers/:client', async(req, res) => {
const {logger, registrar, lookupClientByAccountAndUsername} = req.app.locals;
const client = req.params.client;
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const result = await Account.retrieve(account_sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
}
const user = await registrar.query(`${client}@${result[0].sip_realm}`);
const [clientDb] = await lookupClientByAccountAndUsername(account_sid, client);
res.status(200).json({
name: client,
contact: user ? user.contact : null,
expiryTime: user ? user.expiryTime : 0,
protocol: user ? user.protocol : null,
allow_direct_app_calling: clientDb ? clientDb.allow_direct_app_calling : 0,
allow_direct_queue_calling: clientDb ? clientDb.allow_direct_queue_calling : 0,
allow_direct_user_calling: clientDb ? clientDb.allow_direct_user_calling : 0,
registered_status: user ? 'active' : 'inactive'
});
} catch (err) {
sysError(logger, res, err);
}
});
function coerceNumbers(callInfo) {
if (Array.isArray(callInfo)) {
return callInfo.map((ci) => {
@@ -219,7 +182,6 @@ function validateUpdateCall(opts) {
'child_call_hook',
'call_status',
'listen_status',
'transcribe_status',
'conf_hold_status',
'conf_mute_status',
'mute_status',
@@ -260,8 +222,8 @@ function validateUpdateCall(opts) {
throw new DbErrorBadRequest('invalid conf_mute_status');
}
if (opts.sip_request &&
(!opts.sip_request.method || !opts.sip_request.content_type || !opts.sip_request.content)) {
throw new DbErrorBadRequest('sip_request requires method, content_type and content properties');
(!opts.sip_request.method && !opts.sip_request.content_type || !opts.sip_request.content_type)) {
throw new DbErrorBadRequest('sip_request requires content_type and content properties');
}
if (opts.record && !opts.record.action) {
throw new DbErrorBadRequest('record requires action property');
@@ -585,10 +547,7 @@ function encryptBucketCredential(obj) {
name,
access_key_id,
secret_access_key,
tags,
service_key,
connection_string,
endpoint
tags
} = obj.bucket_credential;
switch (vendor) {
@@ -601,31 +560,11 @@ function encryptBucketCredential(obj) {
secret_access_key, tags});
obj.bucket_credential = encrypt(awsData);
break;
case 's3_compatible':
assert(access_key_id, 'invalid aws S3 bucket credential: access_key_id is required');
assert(secret_access_key, 'invalid aws S3 bucket credential: secret_access_key is required');
assert(name, 'invalid aws bucket name: name is required');
assert(endpoint, 'invalid endpoint uri: endpoint is required');
const s3Data = JSON.stringify({vendor, endpoint, name, access_key_id,
secret_access_key, tags});
obj.bucket_credential = encrypt(s3Data);
break;
case 'google':
assert(service_key, 'invalid google cloud storage credential: service_key is required');
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');
const azureData = JSON.stringify({vendor, name, connection_string, tags});
obj.bucket_credential = encrypt(azureData);
break;
case 'none':
obj.bucket_credential = null;
break;
default:
throw new DbErrorBadRequest(`unknown storage vendor: ${vendor}`);
throw DbErrorBadRequest(`unknow storage vendor: ${vendor}`);
}
}
@@ -775,26 +714,33 @@ router.post('/:sid/BucketCredentialTest', async(req, res) => {
try {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const {vendor, name, region, access_key_id, secret_access_key, service_key, connection_string, endpoint} = req.body;
let {vendor, name, region, access_key_id, secret_access_key} = req.body;
const ret = {
status: 'not tested'
};
if (secret_access_key.endsWith('XXXXXX')) {
// this is when the password already saved in account
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) throw new DbError('Invalid Account Sid');
const {bucket_credential} = results[0];
if (bucket_credential) {
const o = JSON.parse(decrypt(bucket_credential));
vendor = o.vendor;
switch (vendor) {
case 'aws_s3':
name = o.name;
region = o.region;
access_key_id = o.access_key_id;
secret_access_key = o.secret_access_key;
break;
}
}
}
switch (vendor) {
case 'aws_s3':
await testS3Storage(logger, {vendor, name, region, access_key_id, secret_access_key});
ret.status = 'ok';
break;
case 's3_compatible':
await testS3Storage(logger, {vendor, name, endpoint, access_key_id, secret_access_key});
ret.status = 'ok';
break;
case 'google':
await testGoogleStorage(logger, {vendor, name, service_key});
ret.status = 'ok';
break;
case 'azure':
await testAzureStorage(logger, {vendor, name, connection_string});
await testAwsS3(logger, {vendor, name, region, access_key_id, secret_access_key});
ret.status = 'ok';
break;
default:
@@ -830,7 +776,7 @@ router.get('/:sid/ApiKeys', async(req, res) => {
*/
router.post('/:sid/Calls', async(req, res) => {
const {retrieveSet, logger} = req.app.locals;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
@@ -991,7 +937,7 @@ router.post('/:sid/Messages', async(req, res) => {
const account_sid = parseAccountSid(req);
await validateRequest(req, account_sid);
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
await validateCreateMessage(logger, account_sid, req);
@@ -1042,5 +988,4 @@ router.get('/:sid/Queues', async(req, res) => {
}
});
module.exports = router;

View File

@@ -6,7 +6,6 @@ const {validateEmail, emailSimpleText} = require('../../utils/email-utils');
const {promisePool} = require('../../db');
const {cacheClient} = require('../../helpers');
const sysError = require('../error');
const assert = require('assert');
const sql = `SELECT * from users user
LEFT JOIN accounts AS acc
ON acc.account_sid = user.account_sid
@@ -27,8 +26,7 @@ function createOauthEmailText(provider) {
}
function createResetEmailText(link) {
assert(process.env.JAMBONZ_BASE_URL, 'process.env.JAMBONZ_BASE_URL is missing');
const baseUrl = process.env.JAMBONZ_BASE_URL;
const baseUrl = 'http://localhost:3001';
return `Hi there!

View File

@@ -1,77 +0,0 @@
const router = require('express').Router();
const GoogleCustomVoice = require('../../models/google-custom-voice');
const SpeechCredential = require('../../models/speech-credential');
const decorate = require('./decorate');
const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
const sysError = require('../error');
const validateCredentialPermission = async(req) => {
const credential = await SpeechCredential.retrieve(req.body.speech_credential_sid);
if (!credential || credential.length === 0) {
throw new DbErrorBadRequest('Invalid speech_credential_sid');
}
const cred = credential[0];
if (req.user.hasServiceProviderAuth && cred.service_provider_sid !== req.user.service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (req.user.hasAccountAuth && cred.account_sid !== req.user.account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
};
const validateAdd = async(req) => {
if (!req.body.speech_credential_sid) {
throw new DbErrorBadRequest('missing speech_credential_sid');
}
await validateCredentialPermission(req);
};
const validateUpdate = async(req) => {
if (req.body.speech_credential_sid) {
await validateCredentialPermission(req);
}
};
const preconditions = {
add: validateAdd,
update: validateUpdate,
};
decorate(router, GoogleCustomVoice, ['add', 'retrieve', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
const account_sid = req.user.account_sid || req.query.account_sid;
const service_provider_sid = req.user.service_provider_sid || req.query.service_provider_sid;
const speech_credential_sid = req.query.speech_credential_sid;
const label = req.query.label;
try {
let results = [];
if (speech_credential_sid) {
const [cred] = await SpeechCredential.retrieve(speech_credential_sid);
if (!cred) {
return res.sendStatus(404);
}
if (account_sid && cred.account_sid && cred.account_sid !== account_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
if (service_provider_sid && cred.service_provider_sid && cred.service_provider_sid !== service_provider_sid) {
throw new DbErrorForbidden('Insufficient privileges');
}
results = await GoogleCustomVoice.retrieveAllBySpeechCredentialSid(speech_credential_sid);
} else {
if (!account_sid && !service_provider_sid) {
throw new DbErrorBadRequest('missing account_sid or service_provider_sid in query parameters');
}
results = await GoogleCustomVoice.retrieveAllByLabel(service_provider_sid, account_sid, label);
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -52,8 +52,6 @@ api.use('/Lcrs', require('./lcrs'));
api.use('/LcrRoutes', require('./lcr-routes'));
api.use('/LcrCarrierSetEntries', require('./lcr-carrier-set-entries'));
api.use('/Clients', require('./clients'));
// Google Custom Voices
api.use('/GoogleCustomVoices', require('./google-custom-voices'));
// messaging
api.use('/Smpps', require('./smpps')); // our smpp server info

View File

@@ -13,6 +13,8 @@ WHERE up.permission_sid = p.permission_sid
AND up.user_sid = ?
`;
const retrieveSql = 'SELECT * from users where name = ?';
const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL';
router.post('/', async(req, res) => {
const {logger, incrKey, retrieveKey} = req.app.locals;
@@ -52,6 +54,11 @@ router.post('/', async(req, res) => {
return res.sendStatus(403);
}
const force_change = !!r[0].force_change;
const [t] = await promisePool.query(tokenSql);
if (t.length === 0) {
logger.error('Database has no admin token provisioned...run reset_admin_password');
return res.sendStatus(500);
}
const [p] = await promisePool.query(retrievePemissionsSql, r[0].user_sid);
const permissions = p.map((x) => x.name);

View File

@@ -94,9 +94,9 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
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);
const results = req.user.hasAdminAuth ?
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
@@ -120,9 +120,6 @@ router.get('/:sid', async(req, res) => {
throw new DbErrorBadRequest('insufficient privileges');
}
}
if (req.user.hasAccountAuth && results.length > 1) {
return res.status(200).json(results.filter((r) => r.phone_number_sid === sid)[0]);
}
return res.status(200).json(results[0]);
}
catch (err) {

View File

@@ -4,14 +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 {
getS3Object,
getGoogleStorageObject,
getAzureStorageObject,
deleteS3Object,
deleteGoogleStorageObject,
deleteAzureStorageObject
} = require('../../utils/storage-utils');
const { getS3Object } = require('../../utils/storage-utils');
const parseAccountSid = (url) => {
const arr = /Accounts\/([^\/]*)/.exec(url);
@@ -29,7 +22,7 @@ router.get('/', async(req, res) => {
logger.debug({opts: req.query}, 'GET /RecentCalls');
const account_sid = parseAccountSid(req.originalUrl);
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
const {page, count, trunk, direction, days, answered, start, end, filter} = req.query || {};
const {page, count, trunk, direction, days, answered, start, end, from, to} = 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');
@@ -44,7 +37,8 @@ router.get('/', async(req, res) => {
answered,
start: days ? undefined : start,
end: days ? undefined : end,
filter
from,
to
});
res.status(200).json(data);
}
@@ -59,7 +53,8 @@ router.get('/', async(req, res) => {
answered,
start: days ? undefined : start,
end: days ? undefined : end,
filter
from,
to
});
res.status(200).json(data);
}
@@ -131,75 +126,25 @@ router.get('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
const r = await Account.retrieve(account_sid);
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
const {bucket_credential} = r[0];
const getOptions = {
...bucket_credential,
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
};
let stream;
switch (bucket_credential.vendor) {
case 'aws_s3':
case 's3_compatible':
stream = await getS3Object(logger, getOptions);
break;
case 'google':
stream = await getGoogleStorageObject(logger, getOptions);
break;
case 'azure':
stream = await getAzureStorageObject(logger, getOptions);
const getS3Options = {
...bucket_credential,
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
};
const stream = await getS3Object(logger, getS3Options);
res.set({
'Content-Type': `audio/${format || 'mp3'}`
});
stream.pipe(res);
break;
default:
logger.error(`There is no handler for fetching record from ${bucket_credential.vendor}`);
return res.sendStatus(500);
}
res.set({
'Content-Type': `audio/${format || 'mp3'}`
});
if (stream) {
stream.pipe(res);
} else {
return res.sendStatus(404);
}
} catch (err) {
logger.error({err}, ` error retrieving recording ${call_sid}`);
res.sendStatus(404);
}
});
router.delete('/:call_sid/record/:year/:month/:day/:format', async(req, res) => {
const {logger} = req.app.locals;
const {call_sid, year, month, day, format} = req.params;
try {
const account_sid = parseAccountSid(req.originalUrl);
const r = await Account.retrieve(account_sid);
if (r.length === 0 || !r[0].bucket_credential) return res.sendStatus(404);
const {bucket_credential} = r[0];
const deleteOptions = {
...bucket_credential,
key: `${year}/${month}/${day}/${call_sid}.${format || 'mp3'}`
};
switch (bucket_credential.vendor) {
case 'aws_s3':
case 's3_compatible':
await deleteS3Object(logger, deleteOptions);
break;
case 'google':
await deleteGoogleStorageObject(logger, deleteOptions);
break;
case 'azure':
await deleteAzureStorageObject(logger, deleteOptions);
break;
default:
logger.error(`There is no handler for deleting record from ${bucket_credential.vendor}`);
return res.sendStatus(500);
}
res.sendStatus(204);
} catch (err) {
logger.error({err}, ` error deleting recording ${call_sid}`);
res.sendStatus(404);
}
});
module.exports = router;

View File

@@ -16,9 +16,8 @@ const insertUserSql = `INSERT into users
(user_sid, account_sid, name, email, provider, provider_userid, email_validated)
values (?, ?, ?, ?, ?, ?, 1)`;
const insertUserLocalSql = `INSERT into users
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider,
hashed_password, service_provider_sid)
values (?, ?, ?, ?, ?, 0, 'local', ?, ?)`;
(user_sid, account_sid, name, email, email_activation_code, email_validated, provider, hashed_password)
values (?, ?, ?, ?, ?, 0, 'local', ?)`;
const insertAccountSql = `INSERT into accounts
(account_sid, service_provider_sid, name, is_active, webhook_secret, trial_end_date)
values (?, ?, ?, ?, ?, CURDATE() + INTERVAL 21 DAY)`;
@@ -37,7 +36,7 @@ const insertSignupHistorySql = `INSERT into signup_history
values (?, ?)`;
const addLocalUser = async(logger, user_sid, account_sid,
name, email, email_activation_code, passwordHash, service_provider_sid) => {
name, email, email_activation_code, passwordHash) => {
const [r] = await promisePool.execute(insertUserLocalSql,
[
user_sid,
@@ -45,8 +44,7 @@ const addLocalUser = async(logger, user_sid, account_sid,
name,
email,
email_activation_code,
passwordHash,
service_provider_sid
passwordHash
]);
debug({r}, 'Result from adding user');
};
@@ -147,7 +145,7 @@ router.post('/', async(req, res) => {
const user = await doGithubAuth(logger, req.body);
logger.info({user}, 'retrieved user details from github');
Object.assign(userProfile, {
name: user.email,
name: user.name,
email: user.email,
email_validated: user.email_validated,
avatar_url: user.avatar_url,
@@ -159,7 +157,7 @@ router.post('/', async(req, res) => {
const user = await doGoogleAuth(logger, req.body);
logger.info({user}, 'retrieved user details from google');
Object.assign(userProfile, {
name: user.email || user.email,
name: user.name || user.email,
email: user.email,
email_validated: user.verified_email,
picture: user.picture,
@@ -172,7 +170,7 @@ router.post('/', async(req, res) => {
logger.info({user}, 'retrieved user details for local provider');
debug({user}, 'retrieved user details for local provider');
Object.assign(userProfile, {
name: user.email,
name: user.name,
email: user.email,
provider: 'local',
email_activation_code: user.email_activation_code
@@ -282,8 +280,7 @@ router.post('/', async(req, res) => {
const passwordHash = await generateHashedPassword(req.body.password);
debug(`hashed password: ${passwordHash}`);
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
userProfile.name, userProfile.email, userProfile.email_activation_code,
passwordHash, req.body.service_provider_sid);
userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash);
debug('added local user');
}
else {
@@ -296,25 +293,17 @@ router.post('/', async(req, res) => {
const callStatusSid = uuid();
const helloWordSid = uuid();
const dialTimeSid = uuid();
const echoSid = uuid();
/* 4 webhooks */
await promisePool.execute(insertWebookSql,
[callStatusSid, 'https://public-apps.jambonz.cloud/call-status', 'POST']);
await promisePool.execute(insertWebookSql,
[helloWordSid, 'https://public-apps.jambonz.cloud/hello-world', 'POST']);
await promisePool.execute(insertWebookSql,
[dialTimeSid, 'https://public-apps.jambonz.cloud/dial-time', 'POST']);
await promisePool.execute(insertWebookSql,
[echoSid, 'https://public-apps.jambonz.cloud/echo', 'POST']);
/* 3 webhooks */
await promisePool.execute(insertWebookSql, [callStatusSid, 'https://public-apps.jambonz.us/call-status', 'POST']);
await promisePool.execute(insertWebookSql, [helloWordSid, 'https://public-apps.jambonz.us/hello-world', 'POST']);
await promisePool.execute(insertWebookSql, [dialTimeSid, 'https://public-apps.jambonz.us/dial-time', 'POST']);
/* 2 applications */
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'hello world',
helloWordSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'dial time clock',
dialTimeSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'simple echo test',
echoSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
Object.assign(userProfile, {
pristine: true,
@@ -338,7 +327,7 @@ router.post('/', async(req, res) => {
await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid,
userProfile.name, userProfile.email, userProfile.email_activation_code,
passwordHash, req.body.service_provider_sid);
passwordHash);
/* note: we deactivate the old user once the new email is validated */
}
@@ -360,8 +349,6 @@ router.post('/', async(req, res) => {
const token = jwt.sign({
user_sid: userProfile.user_sid,
account_sid: userProfile.account_sid,
service_provider_sid: req.body.service_provider_sid,
scope: 'account',
email: userProfile.email,
name: userProfile.name
}, process.env.JWT_SECRET, { expiresIn });

View File

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

View File

@@ -14,14 +14,20 @@ const getFsUrl = async(logger, retrieveSet, setName, provider) => {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const f = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${f}`);
return `${f}/v1/messaging/${provider}`;
const ip = stripPort(fs[idx++ % fs.length]);
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
return `http://${ip}:3000/v1/messaging/${provider}`;
} catch (err) {
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
}
};
const stripPort = (hostport) => {
const arr = /^(.*):(.*)$/.exec(hostport);
if (arr) return arr[1];
return hostport;
};
const doSendResponse = async(res, respondFn, body) => {
if (typeof respondFn === 'number') res.sendStatus(respondFn);
else if (typeof respondFn !== 'function') res.sendStatus(200);
@@ -38,7 +44,7 @@ router.post('/:provider', async(req, res) => {
lookupAppByPhoneNumber,
logger
} = req.app.locals;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:fs-service-url`;
const setName = `${process.env.JAMBONES_CLUSTER_ID || 'default'}:active-fs`;
logger.debug({path: req.path, body: req.body}, 'incomingSMS from carrier');
// search for provider module

View File

@@ -3,9 +3,8 @@ const assert = require('assert');
const Account = require('../../models/account');
const SpeechCredential = require('../../models/speech-credential');
const sysError = require('../error');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
const {decrypt, encrypt, obscureKey} = require('../../utils/encrypt-decrypt');
const {parseAccountSid, parseServiceProviderSid, parseSpeechCredentialSid} = require('./utils');
const {decryptCredential, testWhisper} = require('../../utils/speech-utils');
const {DbErrorUnprocessableRequest, DbErrorForbidden} = require('../../utils/errors');
const {
testGoogleTts,
@@ -20,11 +19,8 @@ const {
testDeepgramStt,
testSonioxStt,
testIbmTts,
testIbmStt,
testElevenlabs,
testAssemblyStt
testIbmStt
} = require('../../utils/speech-utils');
const bent = require('bent');
const {promisePool} = require('../../db');
const validateAdd = async(req) => {
@@ -118,10 +114,8 @@ const encryptCredential = (obj) => {
nuance_stt_uri,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url,
tts_api_key,
tts_region,
stt_api_key,
@@ -130,9 +124,7 @@ const encryptCredential = (obj) => {
instance_id,
custom_stt_url,
custom_tts_url,
auth_token = '',
cobalt_server_uri,
model_id
auth_token = ''
} = obj;
switch (vendor) {
@@ -155,19 +147,15 @@ const encryptCredential = (obj) => {
return encrypt(awsData);
case 'microsoft':
if (!custom_tts_endpoint_url && !custom_stt_endpoint_url) {
assert(region, 'invalid azure speech credential: region is required');
assert(api_key, 'invalid azure speech credential: api_key is required');
}
assert(region, 'invalid azure speech credential: region is required');
assert(api_key, 'invalid azure speech credential: api_key is required');
const azureData = JSON.stringify({
...(region && {region}),
...(api_key && {api_key}),
region,
api_key,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url
custom_stt_endpoint
});
return encrypt(azureData);
@@ -202,28 +190,6 @@ const encryptCredential = (obj) => {
const sonioxData = JSON.stringify({api_key});
return encrypt(sonioxData);
case 'cobalt':
assert(cobalt_server_uri, 'invalid cobalt speech credential: cobalt_server_uri is required');
const cobaltData = JSON.stringify({cobalt_server_uri});
return encrypt(cobaltData);
case 'elevenlabs':
assert(api_key, 'invalid elevenLabs speech credential: api_key is required');
assert(model_id, 'invalid elevenLabs speech credential: model_id is required');
const elevenlabsData = JSON.stringify({api_key, model_id});
return encrypt(elevenlabsData);
case 'assemblyai':
assert(api_key, 'invalid assemblyai speech credential: api_key is required');
const assemblyaiData = JSON.stringify({api_key});
return encrypt(assemblyaiData);
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);
default:
if (vendor.startsWith('custom:')) {
const customData = JSON.stringify({auth_token, custom_stt_url, custom_tts_url});
@@ -241,7 +207,6 @@ router.post('/', async(req, res) => {
use_for_stt,
use_for_tts,
vendor,
label
} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
const service_provider_sid = req.user.service_provider_sid ||
@@ -256,21 +221,11 @@ router.post('/', async(req, res) => {
}
}
// Check if vendor and label is already used for account or SP
if (label) {
const existingSpeech = await SpeechCredential.getSpeechCredentialsByVendorAndLabel(
service_provider_sid, account_sid, vendor, label);
if (existingSpeech.length > 0) {
throw new DbErrorUnprocessableRequest(`Label ${label} is already in use for another speech credential`);
}
}
const encrypted_credential = encryptCredential(req.body);
const uuid = await SpeechCredential.make({
account_sid,
service_provider_sid,
vendor,
label,
use_for_tts,
use_for_stt,
credential: encrypted_credential
@@ -307,7 +262,66 @@ router.get('/', async(req, res) => {
res.status(200).json(creds.map((c) => {
const {credential, ...obj} = c;
decryptCredential(obj, credential, logger);
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = obscured;
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
logger.info({obj, o}, 'retrieving aws speech credential');
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint;
logger.info({obj, o}, 'retrieving azure speech credential');
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret ? obscureKey(o.secret) : null;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.tts_api_key = obscureKey(o.tts_api_key);
obj.tts_region = o.tts_region;
obj.stt_api_key = obscureKey(o.stt_api_key);
obj.stt_region = o.stt_region;
obj.instance_id = o.instance_id;
} else if ('nvidia' == obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
@@ -337,7 +351,66 @@ router.get('/:sid', async(req, res) => {
await validateRetrieveUpdateDelete(req, cred);
const {credential, ...obj} = cred[0];
decryptCredential(obj, credential, logger);
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = JSON.stringify(obscured);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint;
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret ? obscureKey(o.secret) : null;
obj.nuance_tts_uri = o.nuance_tts_uri;
obj.nuance_stt_uri = o.nuance_stt_uri;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.tts_api_key = obscureKey(o.tts_api_key);
obj.tts_region = o.tts_region;
obj.stt_api_key = obscureKey(o.stt_api_key);
obj.stt_region = o.stt_region;
obj.instance_id = o.instance_id;
} else if ('nvidia' == obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
@@ -404,14 +477,8 @@ router.put('/:sid', async(req, res) => {
const {
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url,
custom_stt_url,
custom_tts_url,
cobalt_server_uri,
model_id
custom_stt_endpoint
} = req.body;
const newCred = {
@@ -421,19 +488,13 @@ router.put('/:sid', async(req, res) => {
aws_region,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url,
stt_region,
tts_region,
riva_server_uri,
nuance_stt_uri,
nuance_tts_uri,
custom_stt_url,
custom_tts_url,
cobalt_server_uri,
model_id
nuance_tts_uri
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -462,7 +523,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 = req.app.locals.logger;
try {
const sid = parseSpeechCredentialSid(req);
const creds = await SpeechCredential.retrieve(sid);
@@ -546,10 +607,8 @@ router.get('/:sid/test', async(req, res) => {
region,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url
custom_stt_endpoint
} = credential;
if (cred.use_for_tts) {
try {
@@ -558,10 +617,8 @@ router.get('/:sid/test', async(req, res) => {
region,
use_custom_tts,
custom_tts_endpoint,
custom_tts_endpoint_url,
use_custom_stt,
custom_stt_endpoint,
custom_stt_endpoint_url
custom_stt_endpoint
});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
@@ -690,41 +747,6 @@ router.get('/:sid/test', async(req, res) => {
SpeechCredential.sttTestResult(sid, false);
}
}
} else if (cred.vendor === 'elevenlabs') {
const {api_key, model_id} = credential;
if (cred.use_for_tts) {
try {
await testElevenlabs(logger, {api_key, model_id});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
results.tts = {status: 'fail', reason: err.message};
SpeechCredential.ttsTestResult(sid, false);
}
}
} else if (cred.vendor === 'assemblyai') {
const {api_key} = credential;
if (cred.use_for_stt) {
try {
await testAssemblyStt(logger, {api_key});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
} else if (cred.vendor === 'whisper') {
if (cred.use_for_tts) {
try {
await testWhisper(logger, synthAudio, credential);
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
results.tts = {status: 'fail', reason: err.message};
SpeechCredential.ttsTestResult(sid, false);
}
}
}
res.status(200).json(results);
@@ -734,93 +756,4 @@ router.get('/:sid/test', async(req, res) => {
}
});
/**
* Fetch speech voices and languages
*/
router.post('/voices', async(req, res) => {
const logger = req.app.locals.logger;
const {vendor, label} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
const service_provider_sid = req.user.service_provider_sid ||
req.body.service_provider_sid || parseServiceProviderSid(req);
try {
res.status(200).json(await getTtsVoices(vendor, label, service_provider_sid, account_sid));
} catch (err) {
sysError(logger, res, err);
}
});
router.post('/languages', async(req, res) => {
const logger = req.app.locals.logger;
const {vendor, label} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
const service_provider_sid = req.user.service_provider_sid ||
req.body.service_provider_sid || parseServiceProviderSid(req);
try {
res.status(200).json(await getTtsLanguages(vendor, label, service_provider_sid, account_sid));
} catch (err) {
sysError(logger, res, err);
}
});
const getTtsVoices = async(vendor, label, service_provider_sid, account_sid) => {
const credentials = 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;
if (vendor === 'elevenlabs') {
const get = bent('https://api.elevenlabs.io', 'GET', 'json', {
...(cred && {
'xi-api-key' : cred.api_key
})
});
const resp = await get('/v1/voices');
return resp ? resp.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']},` : ''}
`;
const lastIndex = name.lastIndexOf(',');
if (lastIndex !== -1) {
name = name.substring(0, lastIndex);
}
return {
value: v.voice_id,
name
};
}).sort((a, b) => a.name.localeCompare(b.name)) : [];
}
return [];
};
const getTtsLanguages = async(vendor, label, service_provider_sid, account_sid) => {
const credentials = 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;
if (vendor === 'elevenlabs') {
if (!cred) {
return [];
}
const get = bent('https://api.elevenlabs.io', 'GET', 'json', {
'xi-api-key' : cred.api_key
});
const resp = await get('/v1/models');
if (!resp || resp.length === 0) {
return [];
}
const model = resp.find((m) => m.model_id === cred.model_id);
return model ? model.languages.map((l) => {
return {
value: l.language_id,
name: l.name
};
}).sort((a, b) => a.name.localeCompare(b.name)) : [];
}
};
module.exports = router;

View File

@@ -2,13 +2,6 @@ const router = require('express').Router();
const {
parseAccountSid
} = require('./utils');
const SpeechCredential = require('../../models/speech-credential');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const {DbErrorBadRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const sysError = require('../error');
const { getSpeechCredential, decryptCredential } = require('../../utils/speech-utils');
router.delete('/', async(req, res) => {
const {purgeTtsCache} = req.app.locals;
@@ -33,79 +26,4 @@ router.get('/', async(req, res) => {
res.status(200).json({size});
});
router.post('/Synthesize', async(req, res) => {
const {logger, synthAudio} = req.app.locals;
try {
const accountSid = parseAccountSid(req);
const body = req.body;
if (!body.speech_credential_sid || !body.text || !body.language || !body.voice) {
throw new DbErrorBadRequest('speech_credential_sid, text, language, voice are all required');
}
const result = await Account.retrieve(accountSid);
if (!result || result.length === 0 || !result[0].is_active) {
throw new DbErrorBadRequest(`Account not found for sid ${accountSid}`);
}
const credentials = await SpeechCredential.retrieve(body.speech_credential_sid);
if (!credentials || credentials.length === 0) {
throw new
DbErrorBadRequest(`There is no available speech credential for ${body.speech_credential_sid}`);
}
const {credential, ...obj} = credentials[0];
decryptCredential(obj, credential, logger, false);
const cred = getSpeechCredential(obj, logger);
const { text, language, engine = 'standard' } = body;
const salt = uuidv4();
/* parse Nuance voices into name and model */
let voice = body.voice;
let model;
if (cred.vendor === 'nuance' && voice) {
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
if (arr) {
voice = arr[1];
model = arr[2];
}
}
const stats = {
histogram: () => {},
increment: () => {},
};
const { filePath } = await synthAudio(stats, {
account_sid: accountSid,
text,
vendor: cred.vendor,
language,
voice,
engine,
model,
salt,
credentials: cred,
disableTtsCache: false
});
const stat = fs.statSync(filePath);
res.writeHead(200, {
'Content-Type': 'audio/mpeg',
'Content-Length': stat.size,
});
const readStream = fs.createReadStream(filePath);
// We replaced all the event handlers with a simple call to readStream.pipe()
readStream.pipe(res);
readStream.on('end', () => {
// Delete the file after it's been read
fs.unlink(filePath, (err) => {
if (err) throw err;
logger.info(`${filePath} was deleted`);
});
});
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -338,8 +338,8 @@ router.put('/:user_sid', async(req, res) => {
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
if (!hasAdminAuth &&
!(hasAccountAuth && user[0] && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && user[0] && req.user.service_provider_sid === user[0].service_provider_sid) &&
!(hasAccountAuth && req.user.account_sid === user[0].account_sid) &&
!(hasServiceProviderAuth && req.user.service_provider_sid === user[0].service_provider_sid) &&
(req.user.user_sid && req.user.user_sid !== user_sid)) {
return res.sendStatus(403);
}

View File

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

View File

@@ -44,8 +44,6 @@ tags:
description: Least Cost Routing Routes operations
- name: LcrCarrierSetEntries
description: Least Cost Routing Carrier Set Entries operation
- name: GoogleCustomVoices
description: Google Custom voices operation
paths:
/BetaInviteCodes:
post:
@@ -788,7 +786,7 @@ paths:
required: true
schema:
type: string
example: mycorp.sip.jambonz.cloud
example: mycorp.sip.jambonz.us
responses:
200:
description: indicates whether value is already in use
@@ -3049,11 +3047,17 @@ paths:
- inbound
- outbound
- in: query
name: filter
name: from
required: false
schema:
type: string
description: Filter value can be caller ID, callee ID or call Sid
description: calling number to retrieve
- in: query
name: to
required: false
schema:
type: string
description: called number to retrieve
get:
tags:
- Accounts
@@ -4166,108 +4170,6 @@ paths:
type: string
length:
type: string
/Accounts/{AccountSid}/RegisteredSipUsers:
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
get:
tags:
- Accounts
summary: retrieve online sip users for an account
operationId: listQueues
responses:
200:
description: retrieve online sip users for an account
content:
application/json:
schema:
type: array
items:
type: string
/Accounts/{AccountSid}/RegisteredSipUsers/{Client}:
parameters:
- name: Client
in: path
required: true
style: simple
explode: false
schema:
type: string
get:
tags:
- Accounts
summary: retrieve registered client registration
operationId: getRegisteredClient
responses:
200:
description: registered client found
content:
application/json:
schema:
$ref: '#/components/schemas/RegisteredClient'
/Accounts/{AccountSid}/TtsCache/Synthesize:
post:
tags:
- Accounts
summary: get TTS from provider
operationId: Synthesize
requestBody:
content:
application/json:
schema:
type: object
properties:
speech_credential_sid:
type: string
description: Speech credential Sid
example: 553b4b6b-8918-4394-a46d-1e3c5a3c717b
text:
type: string
description: the text to convert to audio
example: Hello How are you
language:
type: string
description: language is used in text
example: en-US
voice:
type: string
description: voice ID
example: en-US-Standard-C
required:
- speech_credential_sid
- text
- language
- voice
responses:
200:
description: Audio is created
content:
audio/mpeg:
schema:
type: string
format: binary
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Lcrs:
post:
tags:
@@ -4707,173 +4609,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/GoogleCustomVoices:
post:
tags:
- GoogleCustomVoices
summary: create a Google custom voice
operationId: createGoogleCustomVoice
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/GoogleCustomVoice'
required:
- speech_credential_sid
- name
- reported_usage
- model
responses:
201:
description: Least Cost Routing Carrier Set Entry successfully created
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- GoogleCustomVoices
parameters:
- in: query
name: service_provider_sid
required: false
schema:
type: string
description: return only the google voice custom operated belong to this service provider
- in: query
name: account_sid
required: false
schema:
type: string
description: return only the google voice custom operated belong to this account_sid
- in: query
name: speech_credential_sid
required: false
schema:
type: string
description: return only the google voice custom operated belong to this speech credential
summary: list google custom voices
operationId: listGoogleCustomVoices
responses:
200:
description: list oflist google custom voices
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/GoogleCustomVoice'
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/GoogleCustomVoices/{GoogleCustomVoiceSid}:
parameters:
- name: GoogleCustomVoiceSid
in: path
required: true
style: simple
explode: false
schema:
type: string
delete:
tags:
- GoogleCustomVoices
summary: delete a google custom voice
operationId: deleteGoogleCustomVoice
responses:
204:
description: google custom voice successfully deleted
404:
description: google custom voice not found
422:
description: unprocessable entity
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
example:
msg: a service provider with active accounts can not be deleted
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
tags:
- GoogleCustomVoices
summary: retrieve google custom voice
operationId: getGoogleCustomVoice
responses:
200:
description: google custom voice found
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleCustomVoice'
404:
description: google custom voice not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
put:
tags:
- GoogleCustomVoices
summary: update google custom voice
operationId: updateGoogleCustomVoice
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleCustomVoice'
responses:
204:
description: google custom voice updated
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleCustomVoice'
400:
description: bad request
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
404:
description: least cost routing carrier set entry not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
components:
securitySchemes:
bearerAuth:
@@ -5832,60 +5567,6 @@ components:
- lcr_route_sid
- voip_carrier_sid
- priority
GoogleCustomVoice:
type: object
properties:
speech_credential_sid:
type: string
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
name:
type: string
example: Sally
reported_usage:
type: string
example: REALTIME
model:
type: string
example: projects/12412312/locations/global/models/2134124123-2dbf-43be-9593-12314123
required:
- speech_credential_sid
- name
- reported_usage
- model
RegisteredClient:
type: object
properties:
name:
type: string
example: xhoaluu
contact:
type: string
example: sip:0dluqjt6@od41sl9jfc9m.invalid;transport=ws
expiryTime:
type: number
example: 1698981449173
protocol:
type: string
example: wss
allow_direct_app_calling:
type: number
example: 1
allow_direct_queue_calling:
type: number
example: 1
allow_direct_user_calling:
type: number
example: 1
registered_status:
type: string
enum:
- active
- inactive
required:
- speech_credential_sid
- name
- reported_usage
- model
security:
- bearerAuth: []

View File

@@ -89,12 +89,7 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
const str = await res.text();
return JSON.parse(str);
}
let body;
try {
body = await res.json();
} catch (err) {
}
logger.error({headers: res.headers, body}, `Error creating records, status ${res.statusCode}`);
logger.error({res}, 'Error creating records');
} catch (err) {
logger.error({err}, 'Error retrieving domains');
}

View File

@@ -47,14 +47,12 @@ const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
};
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
if (!process.env.MAILGUN_API_KEY) throw new Error('MAILGUN_API_KEY env variable is not defined!');
if (!process.env.MAILGUN_DOMAIN) throw new Error('MAILGUN_DOMAIN env variable is not defined!');
const mg = mailgun.client({
username: 'api',
key: process.env.MAILGUN_API_KEY,
...(process.env.MAILGUN_URL && {url: process.env.MAILGUN_URL})
key: process.env.MAILGUN_API_KEY
});
if (!process.env.MAILGUN_API_KEY) throw new Error('MAILGUN_API_KEY env variable is not defined!');
if (!process.env.MAILGUN_DOMAIN) throw new Error('MAILGUN_DOMAIN env variable is not defined!');
try {
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {

View File

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

View File

@@ -9,8 +9,7 @@ const getJaegerTrace = async(logger, traceId) => {
try {
return await getJSON(`/api/v3/traces/${traceId}`);
} catch (err) {
const url = `${process.env.JAEGER_BASE_URL}/api/traces/${traceId}`;
logger.error({err, traceId}, `getJaegerTrace: Error retrieving spans from ${url}`);
logger.error({err}, `getJaegerTrace: Error retrieving spans for traceId ${traceId}`);
}
};

View File

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

View File

@@ -5,8 +5,6 @@ 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 testSonioxStt = async(logger, credentials) => {
@@ -205,46 +203,6 @@ const testWellSaidTts = async(logger, credentials) => {
}
};
const testElevenlabs = async(logger, credentials) => {
const {api_key, model_id} = credentials;
try {
const post = bent('https://api.elevenlabs.io', 'POST', 'buffer', {
'xi-api-key': api_key,
'Accept': 'audio/mpeg',
'Content-Type': 'application/json'
});
const mp3 = await post('/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM', {
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;
}
};
const testWhisper = async(logger, synthAudio, credentials) => {
try {
await synthAudio({increment: () => {}, histogram: () => {}},
{
vendor: 'whisper',
credentials,
language: 'en-US',
voice: 'alloy',
text: 'Hi there and welcome to jambones!'
}
);
} catch (err) {
logger.info({err}, 'synthEvenlabs returned error');
throw err;
}
};
const testIbmTts = async(logger, getTtsVoices, credentials) => {
const {tts_api_key, tts_region} = credentials;
const voices = await getTtsVoices({vendor: 'ibm', credentials: {tts_api_key, tts_region}});
@@ -279,137 +237,6 @@ const testWellSaidStt = async(logger, credentials) => {
return true;
};
const testAssemblyStt = async(logger, credentials) => {
const {api_key} = credentials;
const assemblyai = new AssemblyAI({
apiKey: api_key
});
const audioUrl = `${__dirname}/../../data/test_audio.wav`;
return new Promise((resolve, reject) => {
assemblyai.transcripts
.create({ audio_url: audioUrl })
.then((transcript) => {
logger.debug({transcript}, 'got transcription from AssemblyAi');
if (transcript.status === 'error') {
return reject({message: transcript.error});
}
return resolve(transcript.text);
})
.catch((err) => {
logger.info({err}, 'failed to get assemblyAI transcription');
reject(err);
});
});
};
const getSpeechCredential = (credential, logger) => {
const {vendor} = credential;
logger.info(
`Speech vendor: ${credential.vendor} ${credential.label ? `, label: ${credential.label}` : ''} selected`);
if ('google' === vendor) {
try {
const cred = JSON.parse(credential.service_key.replace(/\n/g, '\\n'));
return {
...credential,
credentials: cred
};
} catch (err) {
logger.info({err}, `malformed google service_key provisioned for account ${credential.speech_credential_sid}`);
}
}
else if (['aws', 'polly'].includes(vendor)) {
return {
...credential,
accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key,
region: credential.aws_region || 'us-east-1'
};
}
return credential;
};
function decryptCredential(obj, credential, logger, isObscureKey = true) {
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${isObscureKey ?
obscureKey(o.private_key.slice(key_header.length, o.private_key.length)) :
o.private_key.slice(key_header.length, o.private_key.length)}`
};
obj.service_key = JSON.stringify(obscured);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = isObscureKey ? obscureKey(o.secret_access_key) : o.secret_access_key;
obj.aws_region = o.aws_region;
logger.info({obj, o}, 'retrieving aws speech credential');
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
obj.region = o.region;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.custom_tts_endpoint_url = o.custom_tts_endpoint_url;
obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint;
obj.custom_stt_endpoint_url = o.custom_stt_endpoint_url;
logger.info({obj, o}, 'retrieving azure speech credential');
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret ? (isObscureKey ? obscureKey(o.secret) : o.secret) : null;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.tts_api_key = isObscureKey ? obscureKey(o.tts_api_key) : o.tts_api_key;
obj.tts_region = o.tts_region;
obj.stt_api_key = isObscureKey ? obscureKey(o.stt_api_key) : o.stt_api_key;
obj.stt_region = o.stt_region;
obj.instance_id = o.instance_id;
} else if ('nvidia' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
} else if ('cobalt' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.cobalt_server_uri = o.cobalt_server_uri;
} else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
} else if ('elevenlabs' === 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 (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
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;
} else if ('assemblyai' === 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;
}
}
module.exports = {
testGoogleTts,
testGoogleStt,
@@ -424,10 +251,5 @@ module.exports = {
testDeepgramStt,
testIbmTts,
testIbmStt,
testSonioxStt,
testElevenlabs,
testAssemblyStt,
getSpeechCredential,
decryptCredential,
testWhisper
testSonioxStt
};

View File

@@ -1,134 +1,42 @@
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const {Storage} = require('@google-cloud/storage');
const fs = require('fs');
const { BlobServiceClient } = require('@azure/storage-blob');
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
// Azure
async function testAzureStorage(logger, opts) {
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
const containerClient = blobServiceClient.getContainerClient(opts.name);
const blockBlobClient = containerClient.getBlockBlobClient('jambonz-sample.text');
await blockBlobClient.uploadFile(`${__dirname}/jambonz-sample.text`);
}
async function getAzureStorageObject(logger, opts) {
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
const containerClient = blobServiceClient.getContainerClient(opts.name);
const blockBlobClient = containerClient.getBlockBlobClient(opts.key);
const response = await blockBlobClient.download(0);
return response.readableStreamBody;
}
async function deleteAzureStorageObject(logger, opts) {
const blobServiceClient = BlobServiceClient.fromConnectionString(opts.connection_string);
const containerClient = blobServiceClient.getContainerClient(opts.name);
const blockBlobClient = containerClient.getBlockBlobClient(opts.key);
await blockBlobClient.delete();
}
// Google
function _initGoogleClient(opts) {
const serviceKey = JSON.parse(opts.service_key);
return new Storage({
projectId: serviceKey.project_id,
credentials: {
client_email: serviceKey.client_email,
private_key: serviceKey.private_key
},
});
}
async function testGoogleStorage(logger, opts) {
return new Promise((resolve, reject) => {
const storage = _initGoogleClient(opts);
const blob = storage.bucket(opts.name).file('jambonz-sample.text');
fs.createReadStream(`${__dirname}/jambonz-sample.text`)
.pipe(blob.createWriteStream())
.on('error', (err) => reject(err))
.on('finish', () => resolve());
});
}
async function getGoogleStorageObject(logger, opts) {
const storage = _initGoogleClient(opts);
const bucket = storage.bucket(opts.name);
const file = bucket.file(opts.key);
const [exists] = await file.exists();
if (exists) {
return file.createReadStream();
}
}
async function deleteGoogleStorageObject(logger, opts) {
const storage = _initGoogleClient(opts);
const bucket = storage.bucket(opts.name);
const file = bucket.file(opts.key);
await file.delete();
}
// S3
function _initS3Client(opts) {
return new S3Client({
async function testAwsS3(logger, opts) {
const s3 = new S3Client({
credentials: {
accessKeyId: opts.access_key_id,
secretAccessKey: opts.secret_access_key,
},
region: opts.region || 'us-east-1',
...(opts.vendor === 's3_compatible' && { endpoint: opts.endpoint, forcePathStyle: true })
region: opts.region || 'us-east-1'
});
}
async function testS3Storage(logger, opts) {
const s3 = _initS3Client(opts);
const input = {
'Body': 'Hello From Jambonz',
'Bucket': opts.name,
'Key': 'jambonz-sample.text'
};
const command = new PutObjectCommand(input);
await s3.send(command);
}
async function getS3Object(logger, opts) {
const s3 = _initS3Client(opts);
const command = new GetObjectCommand(
{
Bucket: opts.name,
Key: opts.key
}
);
const s3 = new S3Client({
credentials: {
accessKeyId: opts.access_key_id,
secretAccessKey: opts.secret_access_key,
},
region: opts.region || 'us-east-1'
});
const command = new GetObjectCommand({
Bucket: opts.name,
Key: opts.key
});
const res = await s3.send(command);
return res.Body;
}
async function deleteS3Object(logger, opts) {
const s3 = _initS3Client(opts);
const command = new DeleteObjectCommand(
{
Bucket: opts.name,
Key: opts.key
}
);
await s3.send(command);
}
module.exports = {
testS3Storage,
getS3Object,
deleteS3Object,
testGoogleStorage,
getGoogleStorageObject,
deleteGoogleStorageObject,
testAzureStorage,
getAzureStorageObject,
deleteAzureStorageObject
testAwsS3,
getS3Object
};

5274
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-api-server",
"version": "0.8.5",
"version": "0.8.3",
"description": "",
"main": "app.js",
"scripts": {
@@ -19,22 +19,18 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.363.0",
"@aws-sdk/client-transcribe": "^3.363.0",
"@azure/storage-blob": "^12.15.0",
"@aws-sdk/client-transcribe": "^3.348.0",
"@aws-sdk/client-s3": "^3.348.0",
"@deepgram/sdk": "^1.21.0",
"@google-cloud/speech": "^5.2.0",
"@google-cloud/storage": "^6.12.0",
"@jambonz/db-helpers": "^0.9.1",
"@jambonz/db-helpers": "^0.9.0",
"@jambonz/realtimedb-helpers": "^0.8.6",
"@jambonz/speech-utils": "^0.0.15",
"@jambonz/time-series": "^0.2.7",
"@jambonz/verb-specifications": "^0.0.24",
"@jambonz/lamejs": "^1.2.2",
"@jambonz/mw-registrar": "^0.2.5",
"@jambonz/realtimedb-helpers": "^0.8.7",
"@jambonz/speech-utils": "^0.0.25",
"@jambonz/time-series": "^0.2.8",
"@jambonz/verb-specifications": "^0.0.45",
"@soniox/soniox-node": "^1.1.1",
"argon2": "^0.30.3",
"assemblyai": "^3.0.1",
"bent": "^7.3.12",
"cors": "^2.8.5",
"debug": "^4.3.4",
@@ -44,8 +40,8 @@
"helmet": "^5.1.0",
"ibm-watson": "^7.1.2",
"jsonwebtoken": "^9.0.0",
"mailgun.js": "^9.1.2",
"microsoft-cognitiveservices-speech-sdk": "1.31.0",
"mailgun.js": "^3.7.3",
"microsoft-cognitiveservices-speech-sdk": "^1.24.1",
"mysql2": "^2.3.3",
"nocache": "3.0.4",
"passport": "^0.6.0",
@@ -55,9 +51,9 @@
"stripe": "^8.222.0",
"swagger-ui-express": "^4.4.0",
"uuid": "^8.3.2",
"wav": "^1.0.2",
"yamljs": "^0.3.0",
"ws": "^8.12.1",
"yamljs": "^0.3.0"
"wav": "^1.0.2"
},
"devDependencies": {
"eslint": "^8.39.0",

View File

@@ -53,7 +53,7 @@ test('application tests', async(t) => {
]'
}
});
t.ok(result.statusCode === 400, 'Cant create application with invalid app_json');
t.ok(result.statusCode === 400, 'Cant create application with invalid app_josn');
/* add an application */
result = await request.post('/Applications', {
@@ -81,15 +81,7 @@ test('application tests', async(t) => {
"seekOffset": 8000,\
"actionHook": "/play/action"\
}\
]',
use_for_fallback_speech: 1,
fallback_speech_synthesis_vendor: 'google',
fallback_speech_synthesis_language: 'en-US',
fallback_speech_synthesis_voice: 'man',
fallback_speech_synthesis_label: 'label1',
fallback_speech_recognizer_vendor: 'google',
fallback_speech_recognizer_language: 'en-US',
fallback_speech_recognizer_label: 'label1'
]'
}
});
t.ok(result.statusCode === 201, 'successfully created application');
@@ -110,14 +102,6 @@ test('application tests', async(t) => {
});
t.ok(result.name === 'daveh' , 'successfully retrieved application by sid');
t.ok(result.messaging_hook.url === 'http://example.com/sms' , 'successfully retrieved messaging_hook from application');
t.ok(result.use_for_fallback_speech === 1, 'successfully create use_for_fallback_speech');
t.ok(result.fallback_speech_synthesis_vendor === 'google', 'successfully create fallback_speech_synthesis_vendor');
t.ok(result.fallback_speech_synthesis_language === 'en-US', 'successfully create fallback_speech_synthesis_language');
t.ok(result.fallback_speech_synthesis_voice === 'man', 'successfully create fallback_speech_synthesis_voice');
t.ok(result.fallback_speech_synthesis_label === 'label1', 'successfully create fallback_speech_synthesis_label');
t.ok(result.fallback_speech_recognizer_vendor === 'google', 'successfully create fallback_speech_recognizer_vendor');
t.ok(result.fallback_speech_recognizer_language === 'en-US', 'successfully create fallback_speech_recognizer_language');
t.ok(result.fallback_speech_recognizer_label === 'label1', 'successfully create fallback_speech_recognizer_label');
let app_json = JSON.parse(result.app_json);
t.ok(app_json[0].verb === 'play', 'successfully retrieved app_json from application')
@@ -142,15 +126,7 @@ test('application tests', async(t) => {
}\
}\
]',
record_all_calls: true,
use_for_fallback_speech: 0,
fallback_speech_synthesis_vendor: 'microsoft',
fallback_speech_synthesis_language: 'en-US',
fallback_speech_synthesis_voice: 'woman',
fallback_speech_synthesis_label: 'label2',
fallback_speech_recognizer_vendor: 'microsoft',
fallback_speech_recognizer_language: 'en-US',
fallback_speech_recognizer_label: 'label2'
record_all_calls: true
}
});
t.ok(result.statusCode === 204, 'successfully updated application');
@@ -164,14 +140,6 @@ test('application tests', async(t) => {
app_json = JSON.parse(result.app_json);
t.ok(app_json[0].verb === 'hangup', 'successfully updated app_json from application')
t.ok(result.record_all_calls === 1, 'successfully updated record_all_calls from application')
t.ok(result.use_for_fallback_speech === 0, 'successfully update use_for_fallback_speech');
t.ok(result.fallback_speech_synthesis_vendor === 'microsoft', 'successfully update fallback_speech_synthesis_vendor');
t.ok(result.fallback_speech_synthesis_language === 'en-US', 'successfully update fallback_speech_synthesis_language');
t.ok(result.fallback_speech_synthesis_voice === 'woman', 'successfully update fallback_speech_synthesis_voice');
t.ok(result.fallback_speech_synthesis_label === 'label2', 'successfully update fallback_speech_synthesis_label');
t.ok(result.fallback_speech_recognizer_vendor === 'microsoft', 'successfully update fallback_speech_recognizer_vendor');
t.ok(result.fallback_speech_recognizer_language === 'en-US', 'successfully update fallback_speech_recognizer_language');
t.ok(result.fallback_speech_recognizer_label === 'label2', 'successfully update fallback_speech_recognizer_label');
/* remove applications app_json*/
result = await request.put(`/Applications/${sid}`, {

View File

@@ -31,8 +31,8 @@ test('Create Call Success With Synthesizer in Payload', async (t) => {
auth: authUser,
json: true,
body: {
call_hook: "https://public-apps.jambonz.cloud/hello-world",
call_status_hook: "https://public-apps.jambonz.cloud/call-status",
call_hook: "https://public-apps.jambonz.us/hello-world",
call_status_hook: "https://public-apps.jambonz.us/call-status",
from: "15083778299",
to: {
type: "phone",
@@ -73,8 +73,8 @@ test('Create Call Success Without Synthesizer in Payload', async (t) => {
auth: authUser,
json: true,
body: {
call_hook: "https://public-apps.jambonz.cloud/hello-world",
call_status_hook: "https://public-apps.jambonz.cloud/call-status",
call_hook: "https://public-apps.jambonz.us/hello-world",
call_status_hook: "https://public-apps.jambonz.us/call-status",
from: "15083778299",
to: {
type: "phone",

View File

@@ -12,7 +12,6 @@ process.on('unhandledRejection', (reason, p) => {
test('client test', async(t) => {
const app = require('../app');
const {registrar} = app.locals;
try {
let result;
@@ -36,7 +35,6 @@ test('client test', async(t) => {
body: {
name: 'sample_account',
service_provider_sid: sp_sid,
sip_realm: 'drachtio.org',
registration_hook: {
url: 'http://example.com/reg',
method: 'get'
@@ -56,35 +54,12 @@ test('client test', async(t) => {
account_sid,
username: 'client1',
password: 'sdf12412',
is_active: 1,
allow_direct_app_calling: 0,
allow_direct_queue_calling: 1,
allow_direct_user_calling: 1,
is_active: 1
}
});
t.ok(result.statusCode === 201, 'successfully created Client');
const sid = result.body.sid;
/* register the client */
const r = await registrar.add(
"dhorton@drachtio.org",
{
contact: "10.10.1.1",
sbcAddress: "192.168.1.1",
protocol: "udp",
},
5
);
t.ok(r, 'successfully registered Client');
/* query all registered clients */
result = await request.get(`/Accounts/${account_sid}/RegisteredSipUsers`, {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 && result[0] === 'dhorton@drachtio.org',
'successfully queried all registered clients');
/* query all entity */
result = await request.get('/Clients', {
auth: authAdmin,
@@ -102,7 +77,6 @@ test('client test', async(t) => {
t.ok(result.username === 'client1', 'successfully retrieved Client by sid');
t.ok(result.is_active === 1 , 'successfully retrieved Client by sid');
t.ok(result.password === 'sXXXXXXX' , 'successfully retrieved Client by sid');
t.ok(result.allow_direct_app_calling === 0 , 'successfully retrieved Client by sid');
/* update the entity */
result = await request.put(`/Clients/${sid}`, {

View File

@@ -7,7 +7,7 @@ const test = async() => {
headers: {
Authorization: `Bearer ${process.env.GH_CODE}`,
Accept: 'application/json',
'User-Agent': 'jambonz.cloud'
'User-Agent': 'jambonz.us'
}
}, (err, response, body) => {
if (err) console.log(error);

View File

@@ -75,29 +75,32 @@ test('recent calls tests', async(t) => {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account');
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&filter=1508`, {
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&from=16`, {
auth: authUser,
json: true,
});
t.ok(result.data.length === 0, 'retrieved 5 recent calls by account and from');
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&from=15`, {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account and from');
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&filter=15080`, {
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&to=19`, {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.data.length === 0, 'retrieved 0 recent calls by account and non-matching from');
t.ok(result.data.length === 0, 'retrieved 5 recent calls by account and to');
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&filter=1888`, {
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25&to=18`, {
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account and to');
//console.log({data: result.data}, 'Account recent calls');
/* query last 7 days by service provider */
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25`, {
@@ -106,18 +109,29 @@ test('recent calls tests', async(t) => {
});
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider');
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&filter=1508`, {
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&from=16`, {
auth: authAdmin,
json: true,
});
t.ok(result.data.length === 0, 'retrieved 5 recent calls by service provider and from');
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&from=15`, {
auth: authAdmin,
json: true,
});
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider and from');
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&filter=1888`, {
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&to=19`, {
auth: authAdmin,
json: true,
});
t.ok(result.data.length === 0, 'retrieved 5 recent calls by service provider and to');
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25&to=18`, {
auth: authAdmin,
json: true,
});
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider and to');
//console.log({data: result.data}, 'SP recent calls');
/* pull sip traces and pcap from homer */

View File

@@ -17,37 +17,6 @@ test('sbc_addresses tests', async(t) => {
let result;
const service_provider_sid = await createServiceProvider(request);
/* add service_provider user */
const sp_name = 'sbc_service_provider';
const sp_password = 'password';
result = await request.post(`/Users`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
body: {
name: sp_name,
email: 'sbc_sp@jambonz.com',
is_active: true,
force_change: false,
initial_password: sp_password,
service_provider_sid,
}
});
t.ok(result.statusCode === 201 && result.body.user_sid, 'SBC service_provider scope user created');
const sbc_sp_user_sid = result.body.user_sid;
result = await request.post('/login', {
resolveWithFullResponse: true,
json: true,
body: {
username: sp_name,
password: sp_password,
}
});
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as sbc user');
const authSbcSp = {bearer: result.body.token};
/* add a service provider sbc */
result = await request.post('/Sbcs', {
resolveWithFullResponse: true,
@@ -69,20 +38,6 @@ test('sbc_addresses tests', async(t) => {
//console.log(result.body)
t.ok(result.body.length === 1 && result.body[0].ipv4 === '192.168.1.4', 'successfully retrieved service provider sbc');
result = await request.get('/Sbcs', {
resolveWithFullResponse: true,
auth: authSbcSp,
json: true
});
//console.log(result.body)
t.ok(result.body.length === 1 && result.body[0].ipv4 === '192.168.1.4', 'successfully retrieved service provider sbc');
await request.delete(`/Users/${sbc_sp_user_sid}`, {
resolveWithFullResponse: true,
json: true,
auth: authAdmin,
});
await deleteObjectBySid(request, '/Sbcs', sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);

View File

@@ -84,7 +84,6 @@ test('speech credentials tests', async(t) => {
json: true,
body: {
vendor: 'google',
label: 'label1',
service_key: jsonKey,
use_for_tts: true,
use_for_stt: true
@@ -112,7 +111,6 @@ test('speech credentials tests', async(t) => {
json: true,
});
t.ok(result.vendor === 'google' , 'successfully retrieved speech credential by sid');
t.ok(result.label === 'label1' , 'label is successfully created');
/* query all credentials */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials`, {
@@ -447,117 +445,6 @@ test('speech credentials tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for cobalt */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'cobalt',
use_for_stt: true,
use_for_tts: false,
cobalt_server_uri: '32.32.32.32:2727',
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for Cobalt');
const cobalt_sid = result.body.sid;
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${cobalt_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential for Cobalt');
/* add a credential for elevenlabs */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'elevenlabs',
use_for_stt: true,
use_for_tts: false,
api_key: 'asdasdasdasddsadasda',
model_id: 'eleven_multilingual_v2'
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for Cobalt');
const elevenlabs_sid = result.body.sid;
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${elevenlabs_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential for Cobalt');
/* add a credential for custom voices google */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'google',
use_for_stt: true,
use_for_tts: false,
service_key: jsonKey
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for custom voice google');
const customvoice_google_speech_credential_sid = result.body.sid;
result = await request.post(`/GoogleCustomVoices`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
speech_credential_sid: customvoice_google_speech_credential_sid,
name: "Sally",
reported_usage: 'REALTIME',
model: "path/to/modelId"
}
});
t.ok(result.statusCode === 201, 'successfully added custom voice google');
const customvoice_google_sid = result.body.sid;
/* delete the credential */
result = await request.delete(`/GoogleCustomVoices/${customvoice_google_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted custom voice google');
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${customvoice_google_speech_credential_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential for custom voice google');
/* add a credential for assemblyAI */
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'assemblyai',
use_for_stt: true,
api_key: "APIKEY"
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential for assemblyai');
const assemblyAiSid = result.body.sid;
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${assemblyAiSid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
await deleteObjectBySid(request, '/Accounts', account_sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
t.end();

View File

@@ -192,7 +192,7 @@ test('webapp tests', async(t) => {
t.ok(result.statusCode === 200 && result.body.available === true, 'indicates when email is available');
/* check if a subdomain is available */
result = await request.get('/Availability?type=subdomain&value=mycompany.sip.jambonz.cloud', {
result = await request.get('/Availability?type=subdomain&value=mycompany.sip.jambonz.us', {
resolveWithFullResponse: true,
auth: authUser,
json: true,