mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10159a0ba6 | ||
|
|
7a558c7349 | ||
|
|
4dbe7af9db | ||
|
|
4ec34faa29 | ||
|
|
e2d6086f9f | ||
|
|
0e056ad296 | ||
|
|
1d69457ddc | ||
|
|
dcfe6cc05d | ||
|
|
a474c2d4cc | ||
|
|
0f244cf6d5 |
8
app.js
8
app.js
@@ -46,18 +46,14 @@ const {
|
||||
addKey,
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey,
|
||||
JAMBONES_REDIS_SENTINELS
|
||||
incrKey
|
||||
} = require('./lib/helpers/realtimedb-helpers');
|
||||
const {
|
||||
getTtsVoices,
|
||||
getTtsSize,
|
||||
purgeTtsCache,
|
||||
synthAudio
|
||||
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
} = require('@jambonz/speech-utils')({}, logger);
|
||||
const {
|
||||
lookupAppBySid,
|
||||
lookupAccountBySid,
|
||||
|
||||
@@ -1,29 +1,5 @@
|
||||
const logger = require('../logger');
|
||||
|
||||
const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
|
||||
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
|
||||
let host, port = 26379;
|
||||
if (sentinel.includes(':')) {
|
||||
const arr = sentinel.split(':');
|
||||
host = arr[0];
|
||||
port = parseInt(arr[1], 10);
|
||||
} else {
|
||||
host = sentinel;
|
||||
}
|
||||
return {host, port};
|
||||
}),
|
||||
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
|
||||
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
|
||||
}),
|
||||
...(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,
|
||||
@@ -37,10 +13,7 @@ const {
|
||||
deleteKey,
|
||||
incrKey,
|
||||
client: redisClient,
|
||||
} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
|
||||
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
module.exports = {
|
||||
client,
|
||||
@@ -54,6 +27,5 @@ module.exports = {
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
redisClient,
|
||||
incrKey,
|
||||
JAMBONES_REDIS_SENTINELS
|
||||
incrKey
|
||||
};
|
||||
|
||||
@@ -155,6 +155,9 @@ router.get('/:sid/RegisteredSipUsers', async(req, res) => {
|
||||
if (!result || result.length === 0) {
|
||||
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
|
||||
}
|
||||
if (!result[0].sip_realm) {
|
||||
throw new DbErrorBadRequest('account does not have sip_realm configuration');
|
||||
}
|
||||
const users = await registrar.getRegisteredUsersForRealm(result[0].sip_realm);
|
||||
res.status(200).json(users.map((u) => `${u}@${result[0].sip_realm}`));
|
||||
} catch (err) {
|
||||
@@ -162,6 +165,42 @@ router.get('/:sid/RegisteredSipUsers', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:sid/RegisteredSipUsers', async(req, res) => {
|
||||
const {logger, registrar} = req.app.locals;
|
||||
const users = req.body;
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const result = await Account.retrieve(account_sid);
|
||||
if (!result || result.length === 0) {
|
||||
throw new DbErrorBadRequest(`account not found for sid ${account_sid}`);
|
||||
}
|
||||
if (!result[0].sip_realm) {
|
||||
throw new DbErrorBadRequest('account does not have sip_realm configuration');
|
||||
}
|
||||
if (!users || !Array.isArray(users) || users.length === 0) {
|
||||
return res.status(200).json(await registrar.getRegisteredUsersDetailsForRealm(result[0].sip_realm));
|
||||
}
|
||||
const ret = [];
|
||||
for (const u of users) {
|
||||
const user = await registrar.query(`${u}@${result[0].sip_realm}`) || {
|
||||
name: u,
|
||||
contact: null,
|
||||
expiryTime: 0,
|
||||
protocol: null
|
||||
};
|
||||
ret.push({
|
||||
name: u,
|
||||
...user,
|
||||
registered_status: user.expiryTime > 0 ? 'active' : 'inactive',
|
||||
});
|
||||
}
|
||||
res.status(200).json(ret);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:sid/RegisteredSipUsers/:client', async(req, res) => {
|
||||
const {logger, registrar, lookupClientByAccountAndUsername} = req.app.locals;
|
||||
const client = req.params.client;
|
||||
@@ -224,7 +263,9 @@ function validateUpdateCall(opts) {
|
||||
'conf_mute_status',
|
||||
'mute_status',
|
||||
'sip_request',
|
||||
'record'
|
||||
'record',
|
||||
'tag',
|
||||
'dtmf'
|
||||
]
|
||||
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
|
||||
|
||||
@@ -266,9 +307,15 @@ function validateUpdateCall(opts) {
|
||||
if (opts.record && !opts.record.action) {
|
||||
throw new DbErrorBadRequest('record requires action property');
|
||||
}
|
||||
if (opts.dtmf && !opts.dtmf.digit) {
|
||||
throw new DbErrorBadRequest('invalid dtmf');
|
||||
}
|
||||
if ('startCallRecording' === opts.record?.action && !opts.record.siprecServerURL) {
|
||||
throw new DbErrorBadRequest('record requires siprecServerURL property when starting recording');
|
||||
}
|
||||
if (opts.tag && (typeof opts.tag !== 'object' || Array.isArray(opts.tag) || opts.tag === null)) {
|
||||
throw new DbErrorBadRequest('invalid tag data');
|
||||
}
|
||||
}
|
||||
|
||||
function validateTo(to) {
|
||||
|
||||
@@ -100,6 +100,93 @@ const preconditions = {
|
||||
|
||||
decorate(router, Lcr, ['add', 'update', 'delete'], preconditions);
|
||||
|
||||
const validateLcrBatchAdd = async(lcr_sid, body, lookupCarrierBySid) => {
|
||||
for (const lcr_route of body) {
|
||||
lcr_route.lcr_sid = lcr_sid;
|
||||
if (!lcr_route.lcr_carrier_set_entries || lcr_route.lcr_carrier_set_entries.length === 0) {
|
||||
throw new DbErrorBadRequest('Lcr Route batch process require lcr_carrier_set_entries');
|
||||
}
|
||||
for (const entry of lcr_route.lcr_carrier_set_entries) {
|
||||
// check voip_carrier_sid is exist
|
||||
if (!entry.voip_carrier_sid) {
|
||||
throw new DbErrorBadRequest('One of lcr_carrier_set_entries is missing voip_carrier_sid');
|
||||
}
|
||||
const carrier = await lookupCarrierBySid(entry.voip_carrier_sid);
|
||||
if (!carrier) {
|
||||
throw new DbErrorBadRequest('unknown voip_carrier_sid');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const addNewLcrRoute = async(lcr_route) => {
|
||||
const lcr_sid = lcr_route.lcr_sid;
|
||||
const lcr_carrier_set_entries = lcr_route.lcr_carrier_set_entries;
|
||||
delete lcr_route.lcr_carrier_set_entries;
|
||||
const lcr_route_sid = await LcrRoutes.make(lcr_route);
|
||||
for (const entry of lcr_carrier_set_entries) {
|
||||
entry.lcr_route_sid = lcr_route_sid;
|
||||
const lcr_carrier_set_entry_sid = await LcrCarrierSetEntry.make(entry);
|
||||
if (lcr_route.priority === 9999) {
|
||||
// this is default lcr set entry
|
||||
const [lcr] = await Lcr.retrieve(lcr_sid);
|
||||
if (lcr) {
|
||||
lcr.default_carrier_set_entry_sid = lcr_carrier_set_entry_sid;
|
||||
delete lcr.lcr_sid;
|
||||
await Lcr.update(lcr_sid, lcr);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
router.post('/:sid/Routes', async(req, res) => {
|
||||
const results = await Lcr.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const {logger, lookupCarrierBySid} = req.app.locals;
|
||||
try {
|
||||
const body = req.body;
|
||||
await validateLcrBatchAdd(req.params.sid, body, lookupCarrierBySid);
|
||||
for (const lcr_route of body) {
|
||||
await addNewLcrRoute(lcr_route, lookupCarrierBySid);
|
||||
}
|
||||
res.sendStatus(204);
|
||||
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:sid/Routes', async(req, res) => {
|
||||
const results = await Lcr.retrieve(req.params.sid);
|
||||
if (results.length === 0) return res.sendStatus(404);
|
||||
const {logger, lookupCarrierBySid} = req.app.locals;
|
||||
try {
|
||||
const body = req.body;
|
||||
await validateLcrBatchAdd(req.params.sid, body, lookupCarrierBySid);
|
||||
for (const lcr_route of body) {
|
||||
if (lcr_route.lcr_route_sid) {
|
||||
const lcr_route_sid = lcr_route.lcr_route_sid;
|
||||
delete lcr_route.lcr_route_sid;
|
||||
const lcr_carrier_set_entries = lcr_route.lcr_carrier_set_entries;
|
||||
delete lcr_route.lcr_carrier_set_entries;
|
||||
await LcrRoutes.update(lcr_route_sid, lcr_route);
|
||||
for (const entry of lcr_carrier_set_entries) {
|
||||
const lcr_carrier_set_entry_sid = entry.lcr_carrier_set_entry_sid;
|
||||
delete entry.lcr_carrier_set_entry_sid;
|
||||
await LcrCarrierSetEntry.update(lcr_carrier_set_entry_sid, entry);
|
||||
}
|
||||
} else {
|
||||
// Route is not available yet, let create it now
|
||||
await addNewLcrRoute(lcr_route, lookupCarrierBySid);
|
||||
}
|
||||
}
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
|
||||
@@ -132,7 +132,8 @@ const encryptCredential = (obj) => {
|
||||
custom_tts_url,
|
||||
auth_token = '',
|
||||
cobalt_server_uri,
|
||||
model_id
|
||||
model_id,
|
||||
options
|
||||
} = obj;
|
||||
|
||||
switch (vendor) {
|
||||
@@ -210,7 +211,7 @@ const encryptCredential = (obj) => {
|
||||
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});
|
||||
const elevenlabsData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(elevenlabsData);
|
||||
|
||||
case 'assemblyai':
|
||||
@@ -411,7 +412,8 @@ router.put('/:sid', async(req, res) => {
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
cobalt_server_uri,
|
||||
model_id
|
||||
model_id,
|
||||
options
|
||||
} = req.body;
|
||||
|
||||
const newCred = {
|
||||
@@ -433,7 +435,8 @@ router.put('/:sid', async(req, res) => {
|
||||
custom_stt_url,
|
||||
custom_tts_url,
|
||||
cobalt_server_uri,
|
||||
model_id
|
||||
model_id,
|
||||
options
|
||||
};
|
||||
logger.info({o, newCred}, 'updating speech credential with this new credential');
|
||||
obj.credential = encryptCredential(newCred);
|
||||
@@ -553,7 +556,7 @@ router.get('/:sid/test', async(req, res) => {
|
||||
} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testMicrosoftTts(logger, {
|
||||
await testMicrosoftTts(logger, synthAudio, {
|
||||
api_key,
|
||||
region,
|
||||
use_custom_tts,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: jambonz REST API
|
||||
description: jambonz REST API
|
||||
title: Jambonz REST API
|
||||
description: Jambonz REST API specification
|
||||
contact:
|
||||
email: daveh@drachtio.org
|
||||
license:
|
||||
@@ -3799,7 +3799,7 @@ paths:
|
||||
/Accounts/{AccountSid}/Calls:
|
||||
post:
|
||||
tags:
|
||||
- Accounts
|
||||
- Accounts
|
||||
summary: create a call
|
||||
operationId: createCall
|
||||
parameters:
|
||||
@@ -3858,6 +3858,10 @@ paths:
|
||||
type: object
|
||||
description: The customer SIP headers to associate with the call
|
||||
example: {"X-Custom-Header": "Hello"}
|
||||
sipRequestWithinDialogHook:
|
||||
type: string
|
||||
description: The sip indialog hook to receive session messages
|
||||
example: '/customHook'
|
||||
responses:
|
||||
201:
|
||||
description: call successfully created
|
||||
@@ -4188,6 +4192,27 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
post:
|
||||
tags:
|
||||
- Accounts
|
||||
summary: retrieve online sip users for an account by list of sip username
|
||||
operationId: listRegisteredSipUsers
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: retrieve online sip users for an account
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RegisteredClient'
|
||||
/Accounts/{AccountSid}/RegisteredSipUsers/{Client}:
|
||||
parameters:
|
||||
- name: Client
|
||||
@@ -4415,6 +4440,69 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
/Lcrs/{LcrSid}/Routes:
|
||||
parameters:
|
||||
- name: LcrSid
|
||||
in: path
|
||||
required: true
|
||||
style: simple
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
post:
|
||||
tags:
|
||||
- Lcrs
|
||||
summary: Create least cost routing routes and carrier set entries
|
||||
operationId: createLeastCostRoutingRoutesAndCarrierEntries
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LcrRoutes'
|
||||
responses:
|
||||
204:
|
||||
description: least cost routing routes and carrier set entries created
|
||||
400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
404:
|
||||
description: least cost routing not found
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
put:
|
||||
tags:
|
||||
- Lcrs
|
||||
summary: update least cost routing routes and carrier set entries
|
||||
operationId: updateLeastCostRoutingRoutesAndCarrierEntries
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LcrRoutes'
|
||||
responses:
|
||||
204:
|
||||
description: least cost routing ruoutes and carrier entries updated
|
||||
400:
|
||||
description: bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
404:
|
||||
description: least cost routing not found
|
||||
500:
|
||||
description: system error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GeneralError'
|
||||
/LcrRoutes:
|
||||
post:
|
||||
tags:
|
||||
@@ -5832,6 +5920,19 @@ components:
|
||||
- lcr_route_sid
|
||||
- voip_carrier_sid
|
||||
- priority
|
||||
LcrRouteAndCarrierEntries:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/LcrRoute'
|
||||
- type: object
|
||||
properties:
|
||||
lcr_carrier_set_entries:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LcrCarrierSetEntry'
|
||||
LcrRoutes:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LcrRouteAndCarrierEntries'
|
||||
GoogleCustomVoice:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -150,38 +150,19 @@ const testAwsStt = async(logger, credentials) => {
|
||||
}
|
||||
};
|
||||
|
||||
const testMicrosoftTts = async(logger, credentials) => {
|
||||
const {
|
||||
api_key,
|
||||
region,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
use_custom_tts,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
custom_tts_endpoint,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
use_custom_stt,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
custom_stt_endpoint
|
||||
} = credentials;
|
||||
|
||||
logger.info({
|
||||
api_key,
|
||||
region,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
use_custom_stt,
|
||||
custom_stt_endpoint
|
||||
}, 'testing microsoft tts');
|
||||
if (!api_key) throw new Error('testMicrosoftTts: credentials are missing api_key');
|
||||
if (!region) throw new Error('testMicrosoftTts: credentials are missing region');
|
||||
const testMicrosoftTts = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
const getJSON = bent('json', {
|
||||
'Ocp-Apim-Subscription-Key': api_key
|
||||
});
|
||||
const response = await getJSON(`https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`);
|
||||
return response;
|
||||
await synthAudio({increment: () => {}, histogram: () => {}},
|
||||
{
|
||||
vendor: 'microsoft',
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
voice: 'en-US-JennyMultilingualNeural',
|
||||
text: 'Hi there and welcome to jambones!'
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, `testMicrosoftTts - failed to list voices for region ${region}`);
|
||||
logger.info({err}, 'testMicrosoftTts returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -395,6 +376,7 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
obj.options = o.options;
|
||||
} else if (obj.vendor.startsWith('custom:')) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.auth_token = isObscureKey ? obscureKey(o.auth_token) : o.auth_token;
|
||||
|
||||
2361
package-lock.json
generated
2361
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,9 +27,9 @@
|
||||
"@google-cloud/storage": "^6.12.0",
|
||||
"@jambonz/db-helpers": "^0.9.1",
|
||||
"@jambonz/lamejs": "^1.2.2",
|
||||
"@jambonz/mw-registrar": "^0.2.5",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.7",
|
||||
"@jambonz/speech-utils": "^0.0.25",
|
||||
"@jambonz/speech-utils": "^0.0.31",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.45",
|
||||
"@soniox/soniox-node": "^1.1.1",
|
||||
|
||||
@@ -85,6 +85,24 @@ test('client test', async(t) => {
|
||||
t.ok(result.length === 1 && result[0] === 'dhorton@drachtio.org',
|
||||
'successfully queried all registered clients');
|
||||
|
||||
result = await request.post(`/Accounts/${account_sid}/RegisteredSipUsers`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: ['dhorton']
|
||||
});
|
||||
t.ok(result.body.length === 1 && result.body[0].name === 'dhorton',
|
||||
'successfully queried all registered clients');
|
||||
|
||||
result = await request.post(`/Accounts/${account_sid}/RegisteredSipUsers`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: []
|
||||
});
|
||||
t.ok(result.body.length === 1 && result.body[0].name === 'dhorton',
|
||||
'successfully queried all registered clients');
|
||||
|
||||
/* query all entity */
|
||||
result = await request.get('/Clients', {
|
||||
auth: authAdmin,
|
||||
|
||||
Reference in New Issue
Block a user