Compare commits

..

18 Commits

Author SHA1 Message Date
Dave Horton
92d9468570 0.8.5 2023-11-09 12:37:01 -05:00
Hoan Luu Huu
1b143f6aae support whisper tts (#255)
* support whisper tts

* wip

* update speech util version
2023-11-09 08:38:06 -05:00
Hoan Luu Huu
43344ae14b user retriction (#254)
* user retriction

* wip

* wip

* wip
2023-11-08 12:40:28 -05:00
Dave Horton
e6168d0a3c changes to enable/disable direct calling from chrome extension (#253)
* changes to enable/disable direct calling from chrome extension

* changes to db upgrade script

* fix upgrade db script

---------

Co-authored-by: Quan HL <quan.luuhoang8@gmail.com>
2023-11-06 09:37:35 -05:00
Hoan Luu Huu
f725d5f0a1 feat TTS synthAduio from API (#252)
* feat TTS synthAduio from API

* fix failing testcase

* wip

* wip

* wip

* wip

* uip

* wip

* fix

* fix

* swagger
2023-11-06 07:48:52 -05:00
Hoan Luu Huu
1e9f388f51 feat get registered client status (#251) 2023-11-03 07:33:03 -04:00
Hoan Luu Huu
72d2877ddf support assembly ai (#248)
* support assembly ai

* wip

* wip
2023-11-01 10:43:42 -04:00
Hoan Luu Huu
f15c339a2a support assembly ai (#248)
* support assembly ai

* wip

* wip
2023-11-01 08:02:32 -04:00
Hoan Luu Huu
30ba84d57b google custom voice (#245)
* google custom voice

* wip

* wip

* fix failing testcase
2023-10-30 20:29:12 -04:00
Dave Horton
a4e767e1e4 more useful logging of dns errors 2023-10-25 14:28:48 -04:00
Dave Horton
7b805130bb Feature/retrieve registered users (#243)
* add GET /Accounts/:sid/RegisteredSipUsers

* fix vulnerabilities
2023-10-22 11:32:59 -04:00
Hoan Luu Huu
a1c302f85c Fix/getvoicelanguage (#242)
* fix fetch eleven voice and language

* fix elevenlabs voice and language

* fix

* fix

* fix

* fix

* fix

* fix
2023-10-20 08:41:26 +02:00
Hoan Luu Huu
bf9ae3b5ce fix fetch eleven voice and language (#241) 2023-10-13 08:06:35 -04:00
Hoan Luu Huu
4c9af253a3 add eleven labs (#240)
* add eleven labs

* wip

* add voices and languages endpoints
2023-10-12 21:27:23 -04:00
Hoan Luu Huu
936a9244ba add sentinal Password option (#239) 2023-10-04 19:21:36 -04:00
Markus Frindt
0522ae408c Fix the query to retrieve a phone number as account user (#237)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2023-09-27 10:09:54 -04:00
Hoan Luu Huu
9c788cdedc add cobalt stt (#233)
* add cobalt stt

* fix review comments
2023-09-26 09:00:16 -04:00
Hoan Luu Huu
cbc5e2d6f7 fix jambones-sql.sql missing FOREIGN_KEY_CHECKS (#236) 2023-09-25 19:57:43 -04:00
22 changed files with 1941 additions and 544 deletions

11
app.js
View File

@@ -8,6 +8,7 @@ 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 &&
@@ -35,6 +36,7 @@ const {
logger, process.env.JAMBONES_TIME_SERIES_HOST
);
const {
client,
retrieveCall,
deleteCall,
listCalls,
@@ -50,7 +52,8 @@ const {
const {
getTtsVoices,
getTtsSize,
purgeTtsCache
purgeTtsCache,
synthAudio
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
@@ -62,7 +65,8 @@ const {
lookupAppByPhoneNumber,
lookupCarrierBySid,
lookupSipGatewayBySid,
lookupSmppGatewayBySid
lookupSmppGatewayBySid,
lookupClientByAccountAndUsername
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
@@ -81,6 +85,7 @@ passport.use(authStrategy);
app.locals = app.locals || {};
app.locals = {
...app.locals,
registrar: new Registrar(logger, client),
logger,
retrieveCall,
deleteCall,
@@ -95,6 +100,7 @@ app.locals = {
getTtsVoices,
getTtsSize,
purgeTtsCache,
synthAudio,
lookupAppBySid,
lookupAccountBySid,
lookupAccountByPhoneNumber,
@@ -102,6 +108,7 @@ app.locals = {
lookupCarrierBySid,
lookupSipGatewayBySid,
lookupSmppGatewayBySid,
lookupClientByAccountAndUsername,
queryCdrs,
queryCdrsSP,
queryAlerts,

View File

@@ -1,4 +1,5 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips;
@@ -53,6 +54,8 @@ 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;
@@ -136,6 +139,9 @@ 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)
);
@@ -156,7 +162,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 patterns are tested in sequence until one matches';
) COMMENT='An ordered list of digit patterns in an LCR table. The pat';
CREATE TABLE lcr
(
@@ -167,7 +173,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 provider or account to make decisions about routing outbound calls when multiple carriers are available.';
) COMMENT='An LCR (least cost routing) table that is used by a service ';
CREATE TABLE password_settings
(
@@ -338,6 +344,16 @@ 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),
@@ -628,6 +644,10 @@ ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (ser
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid);
CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid);
ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE;
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone);
@@ -713,4 +733,5 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
ALTER TABLE accounts ADD FOREIGN KEY 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[12]]></labelWindowIndex>
<labelWindowIndex><![CDATA[13]]></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[22]]></labelWindowIndex>
<labelWindowIndex><![CDATA[23]]></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[15]]></labelWindowIndex>
<labelWindowIndex><![CDATA[16]]></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[19]]></labelWindowIndex>
<labelWindowIndex><![CDATA[20]]></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[6]]></labelWindowIndex>
<labelWindowIndex><![CDATA[7]]></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[26]]></labelWindowIndex>
<labelWindowIndex><![CDATA[27]]></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[11]]></labelWindowIndex>
<labelWindowIndex><![CDATA[12]]></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[27]]></labelWindowIndex>
<labelWindowIndex><![CDATA[28]]></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[36]]></labelWindowIndex>
<labelWindowIndex><![CDATA[37]]></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[8]]></labelWindowIndex>
<labelWindowIndex><![CDATA[9]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[4893A0F0-BE1B-4322-9034-644528E802DE]]></uid>
</SQLTable>
@@ -892,6 +892,7 @@
<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>
@@ -986,7 +987,7 @@
<type><![CDATA[VARCHAR(64)]]></type>
<uid><![CDATA[0D42A22C-DF14-42A1-BDE2-A53AC8B0D8D6]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[21]]></labelWindowIndex>
<labelWindowIndex><![CDATA[22]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[49A68E1C-DEE2-446C-A4EB-9850E16155CC]]></uid>
</SQLTable>
@@ -1007,7 +1008,7 @@
<type><![CDATA[VARCHAR(16)]]></type>
<uid><![CDATA[1EA572BD-FF6B-43CC-9EBB-33A735781429]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[9]]></labelWindowIndex>
<labelWindowIndex><![CDATA[10]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[518AC592-D3E6-4032-8A33-15A3DB72B060]]></uid>
</SQLTable>
@@ -1068,7 +1069,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[6B2F726C-48A6-49D9-B7B1-8850DD6FB3EC]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[14]]></labelWindowIndex>
<labelWindowIndex><![CDATA[15]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[51A02EFE-AA51-46EF-8671-E8B2F1FC5F8D]]></uid>
</SQLTable>
@@ -1120,7 +1121,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[2EA3A57F-7EF7-4958-B06B-62B0279BB87E]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[7]]></labelWindowIndex>
<labelWindowIndex><![CDATA[8]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[5784AC2F-BEBC-466F-9818-F9A7D227A5B5]]></uid>
</SQLTable>
@@ -1170,7 +1171,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[04BB457A-D532-4780-8A58-5900094171EC]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[28]]></labelWindowIndex>
<labelWindowIndex><![CDATA[29]]></labelWindowIndex>
<objectComment><![CDATA[An HTTP callback]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[64D64CB9-0990-4C68-BE71-F9FD43C2BE19]]></uid>
@@ -1270,7 +1271,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[9A75A20B-1EFD-4E16-994A-5376C650EAB5]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[10]]></labelWindowIndex>
<labelWindowIndex><![CDATA[11]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[6511AF7D-91FD-40C7-9F73-B8E9E66DC249]]></uid>
</SQLTable>
@@ -1339,7 +1340,7 @@
<uid><![CDATA[9B4208B5-9E3B-4B76-B7F7-4E5D36B99BF2]]></uid>
<unsigned><![CDATA[0]]></unsigned>
</SQLField>
<labelWindowIndex><![CDATA[35]]></labelWindowIndex>
<labelWindowIndex><![CDATA[36]]></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>
@@ -1376,7 +1377,7 @@
<noQuoteDefault><![CDATA[1]]></noQuoteDefault>
<uid><![CDATA[4D2F7B02-F183-4239-8CE8-3E98206708AE]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[13]]></labelWindowIndex>
<labelWindowIndex><![CDATA[14]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[86FAB0AB-DC68-4ADF-8A08-BBAF61BA1840]]></uid>
</SQLTable>
@@ -1414,7 +1415,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[673137EA-B74C-4BA7-AD25-1B71360A2E26]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[5]]></labelWindowIndex>
<labelWindowIndex><![CDATA[6]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[87F254ED-D381-48E3-8E8F-C0F3D99CC01C]]></uid>
</SQLTable>
@@ -1446,7 +1447,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[8998AAD6-A21C-4697-9660-8DC5005AED07]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[4]]></labelWindowIndex>
<labelWindowIndex><![CDATA[5]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[8E75DB2D-9078-40E6-88BF-7DDED5033362]]></uid>
</SQLTable>
@@ -1523,7 +1524,7 @@
<uid><![CDATA[1DDAD1A1-942D-4487-89C8-D496B7F82274]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<labelWindowIndex><![CDATA[25]]></labelWindowIndex>
<labelWindowIndex><![CDATA[26]]></labelWindowIndex>
<objectComment><![CDATA[A Microsoft Teams customer tenant]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[92FD042A-5AEC-4D8F-AB94-C73C0F566F75]]></uid>
@@ -1594,7 +1595,7 @@
<objectComment><![CDATA[lower priority carriers are attempted first]]></objectComment>
<uid><![CDATA[01F61C68-799B-49B0-9E6A-0E2162EE5A54]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[31]]></labelWindowIndex>
<labelWindowIndex><![CDATA[32]]></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>
@@ -1808,7 +1809,7 @@
<objectComment><![CDATA[credential used to authenticate with storage service]]></objectComment>
<uid><![CDATA[E81859C0-DCBD-4FF3-BEEF-FA575394326B]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[33]]></labelWindowIndex>
<labelWindowIndex><![CDATA[34]]></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>
@@ -1847,7 +1848,7 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[D0BF7D36-E40C-4385-9BA5-2099B49A1042]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[20]]></labelWindowIndex>
<labelWindowIndex><![CDATA[21]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[A8ED2178-3CC4-4174-A8FC-C2B58FD28214]]></uid>
</SQLTable>
@@ -1939,7 +1940,7 @@
<type><![CDATA[VARCHAR(32)]]></type>
<uid><![CDATA[CE2015BC-8538-4FB0-B4D9-454436FAB1D9]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[16]]></labelWindowIndex>
<labelWindowIndex><![CDATA[17]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[AF34726D-EDFD-414E-9B44-5243DA9D9497]]></uid>
</SQLTable>
@@ -2009,7 +2010,7 @@
<indexed><![CDATA[1]]></indexed>
<uid><![CDATA[EA936343-89D3-4E3F-BD92-9F8967DC27C4]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[3]]></labelWindowIndex>
<labelWindowIndex><![CDATA[4]]></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>
@@ -2061,10 +2062,72 @@
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[73092A7A-9F3F-4C49-8478-39CE5DAF5ADD]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[18]]></labelWindowIndex>
<labelWindowIndex><![CDATA[19]]></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>
@@ -2175,7 +2238,7 @@
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[4E84523A-7F30-4A5D-A0A8-578652102BD0]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[34]]></labelWindowIndex>
<labelWindowIndex><![CDATA[35]]></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>
@@ -2188,8 +2251,8 @@
<y>1496.00</y>
</location>
<size>
<width>228.00</width>
<height>120.00</height>
<width>282.00</width>
<height>180.00</height>
</size>
<zorder>36</zorder>
<SQLField>
@@ -2234,7 +2297,28 @@
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[2963DB28-7248-4D58-92E6-F21F2EB9E680]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[1]]></labelWindowIndex>
<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>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[C2CDB45C-3F50-40B3-B8CE-2F093EB0D517]]></uid>
</SQLTable>
@@ -2346,7 +2430,7 @@
<objectComment><![CDATA[Outbound call protocol]]></objectComment>
<uid><![CDATA[30661D66-96EC-4B02-995C-5E7EB8A3BD70]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[30]]></labelWindowIndex>
<labelWindowIndex><![CDATA[31]]></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>
@@ -2588,7 +2672,7 @@
<indexType><![CDATA[UNIQUE]]></indexType>
<uid><![CDATA[3FDDDF3B-375D-4DE4-B759-514438845F7D]]></uid>
</SQLIndex>
<labelWindowIndex><![CDATA[32]]></labelWindowIndex>
<labelWindowIndex><![CDATA[33]]></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>
@@ -2696,7 +2780,7 @@
<type><![CDATA[VARBINARY(52)]]></type>
<uid><![CDATA[B4793720-635C-4E25-A306-62E7416541C4]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[17]]></labelWindowIndex>
<labelWindowIndex><![CDATA[18]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[EB4BC5F9-CC10-4C8C-AB31-6D942256AEFB]]></uid>
</SQLTable>
@@ -2727,7 +2811,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[0A8DB34E-76C9-4D40-9E31-786E0228DCEE]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[2]]></labelWindowIndex>
<labelWindowIndex><![CDATA[3]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F0C2DC80-CBA7-4BE7-8856-D10B062A7B17]]></uid>
</SQLTable>
@@ -2812,7 +2896,7 @@
<type><![CDATA[DATETIME]]></type>
<uid><![CDATA[CD43B91B-F34E-4422-9C0F-A4B92E2E7B95]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[24]]></labelWindowIndex>
<labelWindowIndex><![CDATA[25]]></labelWindowIndex>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[F0EE651E-DBF6-4CAC-A517-AC85BCC2D3AF]]></uid>
</SQLTable>
@@ -2872,7 +2956,7 @@
<uid><![CDATA[B73773BA-AB1B-47AA-B995-2D2FE006198F]]></uid>
<unique><![CDATA[0]]></unique>
</SQLField>
<labelWindowIndex><![CDATA[29]]></labelWindowIndex>
<labelWindowIndex><![CDATA[30]]></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>
@@ -2939,7 +3023,7 @@
<type><![CDATA[VARCHAR(255)]]></type>
<uid><![CDATA[FA39B463-61C7-4654-BE9C-D1AC39AB1B97]]></uid>
</SQLField>
<labelWindowIndex><![CDATA[23]]></labelWindowIndex>
<labelWindowIndex><![CDATA[24]]></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>
@@ -3013,17 +3097,17 @@
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
<PageGridVisible><![CDATA[0]]></PageGridVisible>
<RightSidebarWidth><![CDATA[1235.000000]]></RightSidebarWidth>
<RightSidebarWidth><![CDATA[1924.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[1055.000000]]></windowHeight>
<windowLocationX><![CDATA[0.000000]]></windowLocationX>
<windowLocationY><![CDATA[24.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{90, 0}]]></windowScrollOrigin>
<windowWidth><![CDATA[1682.000000]]></windowWidth>
<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>
</SQLDocumentInfo>
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>

View File

@@ -174,7 +174,23 @@ const sql = {
'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'
'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'
]
};

View File

@@ -18,10 +18,14 @@ 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,
@@ -39,6 +43,7 @@ const {
}, logger);
module.exports = {
client,
retrieveCall,
deleteCall,
listCalls,

View File

@@ -52,6 +52,18 @@ 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

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

View File

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

View File

@@ -20,12 +20,13 @@ class SpeechCredential extends Model {
return rows;
}
static async isAvailableVendorAndLabel(service_provider_sid, account_sid, vendor, label) {
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 = ? AND label = ?';
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 = ? AND label = ?';
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;

View File

@@ -146,6 +146,49 @@ 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) => {
@@ -999,4 +1042,5 @@ router.get('/:sid/Queues', async(req, res) => {
}
});
module.exports = router;

View File

@@ -0,0 +1,77 @@
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,6 +52,8 @@ 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,8 +13,6 @@ 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;
@@ -54,11 +52,6 @@ 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

@@ -3,8 +3,9 @@ const assert = require('assert');
const Account = require('../../models/account');
const SpeechCredential = require('../../models/speech-credential');
const sysError = require('../error');
const {decrypt, encrypt, obscureKey} = require('../../utils/encrypt-decrypt');
const {decrypt, encrypt} = 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,
@@ -19,8 +20,11 @@ const {
testDeepgramStt,
testSonioxStt,
testIbmTts,
testIbmStt
testIbmStt,
testElevenlabs,
testAssemblyStt
} = require('../../utils/speech-utils');
const bent = require('bent');
const {promisePool} = require('../../db');
const validateAdd = async(req) => {
@@ -126,7 +130,9 @@ const encryptCredential = (obj) => {
instance_id,
custom_stt_url,
custom_tts_url,
auth_token = ''
auth_token = '',
cobalt_server_uri,
model_id
} = obj;
switch (vendor) {
@@ -196,6 +202,28 @@ 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});
@@ -230,7 +258,7 @@ router.post('/', async(req, res) => {
// Check if vendor and label is already used for account or SP
if (label) {
const existingSpeech = await SpeechCredential.isAvailableVendorAndLabel(
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`);
@@ -279,68 +307,7 @@ router.get('/', async(req, res) => {
res.status(200).json(creds.map((c) => {
const {credential, ...obj} = c;
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = obscured;
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
logger.info({obj, o}, 'retrieving aws speech credential');
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.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 = obscureKey(o.api_key);
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret ? obscureKey(o.secret) : null;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.tts_api_key = obscureKey(o.tts_api_key);
obj.tts_region = o.tts_region;
obj.stt_api_key = obscureKey(o.stt_api_key);
obj.stt_region = o.stt_region;
obj.instance_id = o.instance_id;
} else if ('nvidia' == obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
decryptCredential(obj, credential, logger);
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
@@ -370,68 +337,7 @@ router.get('/:sid', async(req, res) => {
await validateRetrieveUpdateDelete(req, cred);
const {credential, ...obj} = cred[0];
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = JSON.stringify(obscured);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.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;
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('nuance' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.secret = o.secret ? obscureKey(o.secret) : null;
obj.nuance_tts_uri = o.nuance_tts_uri;
obj.nuance_stt_uri = o.nuance_stt_uri;
}
else if ('deepgram' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.tts_api_key = obscureKey(o.tts_api_key);
obj.tts_region = o.tts_region;
obj.stt_api_key = obscureKey(o.stt_api_key);
obj.stt_region = o.stt_region;
obj.instance_id = o.instance_id;
} else if ('nvidia' == obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if ('soniox' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = obscureKey(o.auth_token);
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
decryptCredential(obj, credential, logger);
if (req.user.hasAccountAuth && obj.account_sid === null) {
delete obj.api_key;
@@ -503,7 +409,9 @@ router.put('/:sid', async(req, res) => {
custom_stt_endpoint,
custom_stt_endpoint_url,
custom_stt_url,
custom_tts_url
custom_tts_url,
cobalt_server_uri,
model_id
} = req.body;
const newCred = {
@@ -523,7 +431,9 @@ router.put('/:sid', async(req, res) => {
nuance_stt_uri,
nuance_tts_uri,
custom_stt_url,
custom_tts_url
custom_tts_url,
cobalt_server_uri,
model_id
};
logger.info({o, newCred}, 'updating speech credential with this new credential');
obj.credential = encryptCredential(newCred);
@@ -552,7 +462,7 @@ router.put('/:sid', async(req, res) => {
* Test a credential
*/
router.get('/:sid/test', async(req, res) => {
const logger = req.app.locals.logger;
const {logger, synthAudio} = req.app.locals;
try {
const sid = parseSpeechCredentialSid(req);
const creds = await SpeechCredential.retrieve(sid);
@@ -780,6 +690,41 @@ 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);
@@ -789,4 +734,93 @@ 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,6 +2,13 @@ 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;
@@ -26,4 +33,79 @@ 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

@@ -44,6 +44,8 @@ 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:
@@ -4164,6 +4166,108 @@ 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:
@@ -4603,6 +4707,173 @@ 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:
@@ -5561,6 +5832,60 @@ 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,7 +89,12 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
const str = await res.text();
return JSON.parse(str);
}
logger.error({res}, 'Error creating records');
let body;
try {
body = await res.json();
} catch (err) {
}
logger.error({headers: res.headers, body}, `Error creating records, status ${res.statusCode}`);
} catch (err) {
logger.error({err}, 'Error retrieving domains');
}

View File

@@ -5,6 +5,8 @@ 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) => {
@@ -203,6 +205,46 @@ 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}});
@@ -237,6 +279,137 @@ 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,
@@ -251,5 +424,10 @@ module.exports = {
testDeepgramStt,
testIbmTts,
testIbmStt,
testSonioxStt
testSonioxStt,
testElevenlabs,
testAssemblyStt,
getSpeechCredential,
decryptCredential,
testWhisper
};

978
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.4",
"version": "0.8.5",
"description": "",
"main": "app.js",
"scripts": {
@@ -19,18 +19,22 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@aws-sdk/client-transcribe": "^3.363.0",
"@aws-sdk/client-s3": "^3.363.0",
"@aws-sdk/client-transcribe": "^3.363.0",
"@azure/storage-blob": "^12.15.0",
"@deepgram/sdk": "^1.21.0",
"@google-cloud/speech": "^5.2.0",
"@jambonz/db-helpers": "^0.9.0",
"@jambonz/realtimedb-helpers": "^0.8.6",
"@jambonz/speech-utils": "^0.0.15",
"@jambonz/time-series": "^0.2.8",
"@jambonz/verb-specifications": "^0.0.29",
"@google-cloud/storage": "^6.12.0",
"@jambonz/db-helpers": "^0.9.1",
"@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",
@@ -41,7 +45,7 @@
"ibm-watson": "^7.1.2",
"jsonwebtoken": "^9.0.0",
"mailgun.js": "^9.1.2",
"microsoft-cognitiveservices-speech-sdk": "^1.24.1",
"microsoft-cognitiveservices-speech-sdk": "1.31.0",
"mysql2": "^2.3.3",
"nocache": "3.0.4",
"passport": "^0.6.0",
@@ -51,11 +55,9 @@
"stripe": "^8.222.0",
"swagger-ui-express": "^4.4.0",
"uuid": "^8.3.2",
"yamljs": "^0.3.0",
"ws": "^8.12.1",
"wav": "^1.0.2",
"@google-cloud/storage": "^6.12.0",
"@azure/storage-blob": "^12.15.0"
"ws": "^8.12.1",
"yamljs": "^0.3.0"
},
"devDependencies": {
"eslint": "^8.39.0",

View File

@@ -12,6 +12,7 @@ process.on('unhandledRejection', (reason, p) => {
test('client test', async(t) => {
const app = require('../app');
const {registrar} = app.locals;
try {
let result;
@@ -35,6 +36,7 @@ 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'
@@ -54,12 +56,35 @@ test('client test', async(t) => {
account_sid,
username: 'client1',
password: 'sdf12412',
is_active: 1
is_active: 1,
allow_direct_app_calling: 0,
allow_direct_queue_calling: 1,
allow_direct_user_calling: 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,
@@ -77,6 +102,7 @@ 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

@@ -447,6 +447,117 @@ 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();