mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77dbe964aa | ||
|
|
3609b8e828 | ||
|
|
27addfa543 | ||
|
|
8181d56a48 | ||
|
|
6341132807 | ||
|
|
0bf68b6a9b | ||
|
|
69046ab5d2 | ||
|
|
3f1e756467 | ||
|
|
4201ebbe9c | ||
|
|
e02db2e025 | ||
|
|
dd79813229 | ||
|
|
1aa28e8ba0 | ||
|
|
15f2d92f71 | ||
|
|
6ef40a648c | ||
|
|
40754deb3e | ||
|
|
eb681f9ddf | ||
|
|
486428727a | ||
|
|
4c86adf1f7 | ||
|
|
4f0f8a0f46 | ||
|
|
38afe0da18 | ||
|
|
0d66dc9c27 | ||
|
|
e9d14e9e38 | ||
|
|
1d609135fc | ||
|
|
16dcd26216 | ||
|
|
42f4318a17 | ||
|
|
0f1f5e9b73 | ||
|
|
bcff9b35a6 | ||
|
|
8267ddaffd | ||
|
|
c3d12fafee | ||
|
|
9421bb8aa1 | ||
|
|
a15c5cd267 | ||
|
|
4de66789ef | ||
|
|
a297d2038f | ||
|
|
2e0ea56925 | ||
|
|
9c8bfebd53 | ||
|
|
035458ad3c | ||
|
|
fd9dc77a58 | ||
|
|
2b66a121a0 | ||
|
|
3a6d10e725 | ||
|
|
6f87204d88 | ||
|
|
9854666d4f | ||
|
|
0d4b7e88ad | ||
|
|
819319dbe5 | ||
|
|
0ba69e872b | ||
|
|
9b4f1b67bf | ||
|
|
542ccfca79 | ||
|
|
5421f1421f | ||
|
|
0842793aea | ||
|
|
781179bf0e | ||
|
|
1532a4ab9c | ||
|
|
5fd89b1d65 | ||
|
|
e2fc0216e1 | ||
|
|
fcff3d4b32 | ||
|
|
2dd06df641 | ||
|
|
579a586a03 | ||
|
|
3e1b383284 | ||
|
|
c51b7bab82 | ||
|
|
bb5dba7c20 | ||
|
|
c7e279d0ee | ||
|
|
6700ff35be | ||
|
|
3f2a304830 | ||
|
|
f23c4fbd48 | ||
|
|
0c2f5becdc | ||
|
|
cd6772c10f | ||
|
|
b708f7beb6 | ||
|
|
431cc9e4f4 | ||
|
|
35b10d55d5 | ||
|
|
533e202474 | ||
|
|
e506fc8b66 | ||
|
|
76a2054745 | ||
|
|
be300ebd51 | ||
|
|
27c3664391 | ||
|
|
48e39f37d3 | ||
|
|
2b4b3056e9 | ||
|
|
3cad5219b4 | ||
|
|
30a799030c | ||
|
|
e41caf8887 | ||
|
|
217c11a5e1 | ||
|
|
561de0532f | ||
|
|
ce2ea8bd62 | ||
|
|
c21f5b871f | ||
|
|
9a2e48b538 | ||
|
|
29adbfc6ae | ||
|
|
ffda2398f4 | ||
|
|
b05b32d73e | ||
|
|
b8bf18f8ca | ||
|
|
1e532212f9 | ||
|
|
92347c26bf | ||
|
|
bc51b60e9b | ||
|
|
f0ec0a916f | ||
|
|
c94f14f27d | ||
|
|
06873186ac | ||
|
|
956da4334f | ||
|
|
c144758d44 | ||
|
|
e24f3472ae | ||
|
|
4c935c7fda |
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -6,22 +6,17 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- run: npm install
|
||||
- run: npm run jslint
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- run: npm test
|
||||
- run: npm run test:encrypt-decrypt
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
@@ -37,14 +37,14 @@ jobs:
|
||||
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.db-create
|
||||
|
||||
8
.github/workflows/docker-publish.yml
vendored
8
.github/workflows/docker-publish.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
@@ -37,14 +37,14 @@ jobs:
|
||||
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
|
||||
FROM --platform=linux/amd64 node:24-alpine AS base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
WORKDIR /opt/app/
|
||||
|
||||
FROM base as build
|
||||
FROM base AS build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
@@ -18,6 +18,6 @@ COPY --from=build /opt/app /opt/app/
|
||||
|
||||
ARG NODE_ENV
|
||||
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
ENV NODE_ENV=$NODE_ENV
|
||||
|
||||
CMD [ "node", "app.js" ]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
|
||||
FROM --platform=linux/amd64 node:24-alpine AS base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
WORKDIR /opt/app/
|
||||
|
||||
FROM base as build
|
||||
FROM base AS build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
@@ -18,6 +18,6 @@ COPY --from=build /opt/app /opt/app/
|
||||
|
||||
ARG NODE_ENV
|
||||
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
ENV NODE_ENV=$NODE_ENV
|
||||
|
||||
CMD [ "npm", "run", "upgrade-db" ]
|
||||
@@ -35,6 +35,7 @@ Configuration is provided via environment variables:
|
||||
|K8S_FEATURE_SERVER_SERVICE_PORT| feature server port(required for K8S) |no|
|
||||
|JAMBONZ_RECORD_WS_USERNAME| recording websocket username|no|
|
||||
|JAMBONZ_RECORD_WS_PASSWORD| recording websocket password|no|
|
||||
|DISABLE_RATE_LIMITS| disable rate limiting|no
|
||||
|
||||
#### Database dependency
|
||||
A mysql database is used to store long-lived objects such as Accounts, Applications, etc. To create the database schema, use or review the scripts in the 'db' folder, particularly:
|
||||
|
||||
43
app.js
43
app.js
@@ -7,6 +7,7 @@ const nocache = require('nocache');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const cors = require('cors');
|
||||
const passport = require('passport');
|
||||
const {verifyViewOnlyUser} = require('./lib/middleware');
|
||||
const routes = require('./lib/routes');
|
||||
const Registrar = require('@jambonz/mw-registrar');
|
||||
|
||||
@@ -47,7 +48,8 @@ const {
|
||||
retrieveKey,
|
||||
deleteKey,
|
||||
incrKey,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
} = require('./lib/helpers/realtimedb-helpers');
|
||||
const {
|
||||
getTtsVoices,
|
||||
@@ -117,7 +119,8 @@ app.locals = {
|
||||
queryAlertsSP,
|
||||
writeCdrs,
|
||||
writeAlerts,
|
||||
AlertType
|
||||
AlertType,
|
||||
getCallCount
|
||||
};
|
||||
|
||||
const unless = (paths, middleware) => {
|
||||
@@ -127,11 +130,27 @@ const unless = (paths, middleware) => {
|
||||
};
|
||||
};
|
||||
|
||||
const RATE_LIMIT_BY = process.env.RATE_LIMIT_BY || 'system';
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: (process.env.RATE_LIMIT_WINDOWS_MINS || 5) * 60 * 1000, // 5 minutes
|
||||
max: process.env.RATE_LIMIT_MAX_PER_WINDOW || 600, // Limit each IP to 600 requests per `window`
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
keyGenerator: (req, res) => {
|
||||
switch (RATE_LIMIT_BY) {
|
||||
case 'system':
|
||||
return '127.0.0.1';
|
||||
case 'apikey':
|
||||
// uses shared limit for requests without an authorization header
|
||||
const token = req.headers.authorization?.split(' ')[1] || '127.0.0.1';
|
||||
return token;
|
||||
case 'ip':
|
||||
return req.headers['x-real-ip'];
|
||||
default:
|
||||
return '127.0.0.1';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Setup websocket for recording audio
|
||||
@@ -151,7 +170,12 @@ if (process.env.JAMBONES_TRUST_PROXY) {
|
||||
});
|
||||
}
|
||||
}
|
||||
app.use(limiter);
|
||||
|
||||
const disableRateLimit = process.env.DISABLE_RATE_LIMITS === 'true' || process.env.DISABLE_RATE_LIMITS === '1';
|
||||
|
||||
if (!disableRateLimit) {
|
||||
app.use(limiter);
|
||||
}
|
||||
app.use(helmet());
|
||||
app.use(helmet.hidePoweredBy());
|
||||
app.use(nocache());
|
||||
@@ -172,6 +196,19 @@ app.use('/v1', unless(
|
||||
'/InviteCodes',
|
||||
'/PredefinedCarriers'
|
||||
], passport.authenticate('bearer', {session: false})));
|
||||
app.use('/v1', unless(
|
||||
[
|
||||
'/register',
|
||||
'/forgot-password',
|
||||
'/signin',
|
||||
'/login',
|
||||
'/messaging',
|
||||
'/outboundSMS',
|
||||
'/AccountTest',
|
||||
'/InviteCodes',
|
||||
'/PredefinedCarriers',
|
||||
'/logout'
|
||||
], verifyViewOnlyUser));
|
||||
app.use('/', routes);
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error(err, 'burped error');
|
||||
|
||||
@@ -162,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
|
||||
(
|
||||
@@ -173,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
|
||||
(
|
||||
@@ -204,6 +204,7 @@ tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to thi
|
||||
inbound_auth_username VARCHAR(64),
|
||||
inbound_auth_password VARCHAR(64),
|
||||
diversion VARCHAR(32),
|
||||
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
|
||||
PRIMARY KEY (predefined_carrier_sid)
|
||||
);
|
||||
|
||||
@@ -418,6 +419,7 @@ register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
|
||||
register_status VARCHAR(4096),
|
||||
dtmf_type ENUM('rfc2833','tones','info') NOT NULL DEFAULT 'rfc2833',
|
||||
outbound_sip_proxy VARCHAR(255),
|
||||
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
|
||||
PRIMARY KEY (voip_carrier_sid)
|
||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
|
||||
@@ -503,7 +505,7 @@ messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
||||
app_json TEXT,
|
||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(256),
|
||||
speech_synthesis_voice VARCHAR(256) DEFAULT 'en-US-Standard-C',
|
||||
speech_synthesis_label VARCHAR(64),
|
||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||
@@ -516,6 +518,7 @@ fallback_speech_synthesis_label VARCHAR(64),
|
||||
fallback_speech_recognizer_vendor VARCHAR(64),
|
||||
fallback_speech_recognizer_language VARCHAR(64),
|
||||
fallback_speech_recognizer_label VARCHAR(64),
|
||||
env_vars TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (application_sid)
|
||||
@@ -702,6 +705,12 @@ ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_
|
||||
|
||||
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_carrier ON sip_gateways (inbound,voip_carrier_sid);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_lookup ON sip_gateways (inbound,netmask,ipv4);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_netmask ON sip_gateways (inbound,netmask);
|
||||
|
||||
CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid);
|
||||
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
|
||||
123
db/jambones.sqs
123
db/jambones.sqs
@@ -546,12 +546,12 @@
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[A Carrier or customer PBX that can send or receive calls]]></comment>
|
||||
<location>
|
||||
<x>16.00</x>
|
||||
<y>427.00</y>
|
||||
<x>20.00</x>
|
||||
<y>418.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>293.00</width>
|
||||
<height>580.00</height>
|
||||
<height>600.00</height>
|
||||
</size>
|
||||
<zorder>6</zorder>
|
||||
<SQLField>
|
||||
@@ -749,6 +749,13 @@
|
||||
<type><![CDATA[VARCHAR(255)]]></type>
|
||||
<uid><![CDATA[556ABA45-BC63-444D-8CB1-973EFCCF9FE7]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[trunk_type]]></name>
|
||||
<type><![CDATA[ENUM('static_ip','auth','reg')]]></type>
|
||||
<defaultValue><![CDATA[static_ip]]></defaultValue>
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[CCF1560C-349E-4DB9-91E5-120F1EDB7CDE]]></uid>
|
||||
</SQLField>
|
||||
<labelWindowIndex><![CDATA[28]]></labelWindowIndex>
|
||||
<objectComment><![CDATA[A Carrier or customer PBX that can send or receive calls]]></objectComment>
|
||||
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
|
||||
@@ -1293,7 +1300,7 @@
|
||||
<comment><![CDATA[a regex-based pattern match for call routing]]></comment>
|
||||
<location>
|
||||
<x>16.00</x>
|
||||
<y>1007.00</y>
|
||||
<y>1039.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>254.00</width>
|
||||
@@ -1880,7 +1887,7 @@
|
||||
</location>
|
||||
<size>
|
||||
<width>302.00</width>
|
||||
<height>260.00</height>
|
||||
<height>280.00</height>
|
||||
</size>
|
||||
<zorder>20</zorder>
|
||||
<SQLField>
|
||||
@@ -1959,6 +1966,13 @@
|
||||
<type><![CDATA[VARCHAR(32)]]></type>
|
||||
<uid><![CDATA[CE2015BC-8538-4FB0-B4D9-454436FAB1D9]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[trunk_type]]></name>
|
||||
<type><![CDATA[ENUM('static_ip','auth','reg')]]></type>
|
||||
<defaultValue><![CDATA[static_ip]]></defaultValue>
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[F50906E5-2CA5-47D0-BF7B-6CB75EFD83B8]]></uid>
|
||||
</SQLField>
|
||||
<labelWindowIndex><![CDATA[17]]></labelWindowIndex>
|
||||
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
|
||||
<uid><![CDATA[AF34726D-EDFD-414E-9B44-5243DA9D9497]]></uid>
|
||||
@@ -2165,8 +2179,8 @@
|
||||
<schema><![CDATA[]]></schema>
|
||||
<comment><![CDATA[A phone number that has been assigned to an account]]></comment>
|
||||
<location>
|
||||
<x>16.00</x>
|
||||
<y>1128.00</y>
|
||||
<x>11.00</x>
|
||||
<y>1162.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>522.00</width>
|
||||
@@ -2363,8 +2377,8 @@
|
||||
<y>17.00</y>
|
||||
</location>
|
||||
<size>
|
||||
<width>281.00</width>
|
||||
<height>280.00</height>
|
||||
<width>391.00</width>
|
||||
<height>340.00</height>
|
||||
</size>
|
||||
<zorder>7</zorder>
|
||||
<SQLField>
|
||||
@@ -2453,6 +2467,13 @@
|
||||
<notNull><![CDATA[1]]></notNull>
|
||||
<uid><![CDATA[C5C0043B-100A-4476-BF01-BE0777AE27C0]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[protocol]]></name>
|
||||
<type><![CDATA[ENUM('udp','tcp','tls', 'tls/srtp')]]></type>
|
||||
<defaultValue><![CDATA[udp]]></defaultValue>
|
||||
<objectComment><![CDATA[Outbound call protocol]]></objectComment>
|
||||
<uid><![CDATA[30661D66-96EC-4B02-995C-5E7EB8A3BD70]]></uid>
|
||||
</SQLField>
|
||||
<SQLIndex>
|
||||
<name><![CDATA[sip_gateway_idx_hostport]]></name>
|
||||
<fieldName><![CDATA[ipv4]]></fieldName>
|
||||
@@ -2470,13 +2491,63 @@
|
||||
<indexNamePrefix><![CDATA[sip_gateway]]></indexNamePrefix>
|
||||
<uid><![CDATA[1C744DE3-39BD-4EC6-B427-7EB2DD258771]]></uid>
|
||||
</SQLIndex>
|
||||
<SQLField>
|
||||
<name><![CDATA[protocol]]></name>
|
||||
<type><![CDATA[ENUM('udp','tcp','tls', 'tls/srtp')]]></type>
|
||||
<defaultValue><![CDATA[udp]]></defaultValue>
|
||||
<objectComment><![CDATA[Outbound call protocol]]></objectComment>
|
||||
<uid><![CDATA[30661D66-96EC-4B02-995C-5E7EB8A3BD70]]></uid>
|
||||
</SQLField>
|
||||
<SQLIndex>
|
||||
<name><![CDATA[idx_sip_gateways_inbound_carrier]]></name>
|
||||
<fieldName><![CDATA[inbound]]></fieldName>
|
||||
<fieldName><![CDATA[voip_carrier_sid]]></fieldName>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[inbound]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[CDE029DC-0C7C-400C-85E9-5005C53B7460]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[voip_carrier_sid]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[BC25D27E-54E4-4D14-B53D-D1C6254D1D72]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<indexNamePrefix><![CDATA[sip_gateways]]></indexNamePrefix>
|
||||
<uid><![CDATA[BCE047C6-F70E-42AD-9201-FECF1BAD6BEA]]></uid>
|
||||
</SQLIndex>
|
||||
<SQLIndex>
|
||||
<name><![CDATA[idx_sip_gateways_inbound_lookup]]></name>
|
||||
<fieldName><![CDATA[inbound]]></fieldName>
|
||||
<fieldName><![CDATA[netmask]]></fieldName>
|
||||
<fieldName><![CDATA[ipv4]]></fieldName>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[inbound]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[CDE029DC-0C7C-400C-85E9-5005C53B7460]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[netmask]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[717ACB37-EF84-48DC-94E4-2AAC066C0A33]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[ipv4]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[F18DB7D4-F902-4863-870C-CB07032AE17C]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<indexNamePrefix><![CDATA[sip_gateways]]></indexNamePrefix>
|
||||
<uid><![CDATA[83F405A9-2AE5-415C-9B5E-5E9B92A32F57]]></uid>
|
||||
</SQLIndex>
|
||||
<SQLIndex>
|
||||
<name><![CDATA[idx_sip_gateways_inbound_netmask]]></name>
|
||||
<fieldName><![CDATA[inbound]]></fieldName>
|
||||
<fieldName><![CDATA[netmask]]></fieldName>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[inbound]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[CDE029DC-0C7C-400C-85E9-5005C53B7460]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[netmask]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[717ACB37-EF84-48DC-94E4-2AAC066C0A33]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<indexNamePrefix><![CDATA[sip_gateways]]></indexNamePrefix>
|
||||
<uid><![CDATA[8322B9B7-DC3A-4B0D-85A8-2D15E4C51340]]></uid>
|
||||
</SQLIndex>
|
||||
<labelWindowIndex><![CDATA[31]]></labelWindowIndex>
|
||||
<objectComment><![CDATA[A whitelisted sip gateway used for origination/termination]]></objectComment>
|
||||
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
|
||||
@@ -2492,7 +2563,7 @@
|
||||
</location>
|
||||
<size>
|
||||
<width>345.00</width>
|
||||
<height>540.00</height>
|
||||
<height>560.00</height>
|
||||
</size>
|
||||
<zorder>0</zorder>
|
||||
<SQLField>
|
||||
@@ -2608,6 +2679,7 @@
|
||||
<SQLField>
|
||||
<name><![CDATA[speech_synthesis_voice]]></name>
|
||||
<type><![CDATA[VARCHAR(256)]]></type>
|
||||
<defaultValue><![CDATA[en-US-Standard-C]]></defaultValue>
|
||||
<notNull><![CDATA[0]]></notNull>
|
||||
<uid><![CDATA[929D66F0-64B9-4D7C-AB4B-24F131E1178F]]></uid>
|
||||
</SQLField>
|
||||
@@ -2685,6 +2757,11 @@
|
||||
<notNull><![CDATA[0]]></notNull>
|
||||
<uid><![CDATA[65AA5173-6523-49F7-9D95-78C4B3A7C7E6]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[env_vars]]></name>
|
||||
<type><![CDATA[TEXT]]></type>
|
||||
<uid><![CDATA[C22DCA56-385D-45EE-A36F-2B9C6167AFAA]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[created_at]]></name>
|
||||
<type><![CDATA[DATETIME]]></type>
|
||||
@@ -3156,17 +3233,17 @@
|
||||
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
|
||||
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
|
||||
<PageGridVisible><![CDATA[0]]></PageGridVisible>
|
||||
<RightSidebarWidth><![CDATA[1393.000000]]></RightSidebarWidth>
|
||||
<RightSidebarWidth><![CDATA[2944.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[58.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[24.000000]]></windowLocationY>
|
||||
<windowScrollOrigin><![CDATA[{0, 278}]]></windowScrollOrigin>
|
||||
<windowWidth><![CDATA[1670.000000]]></windowWidth>
|
||||
<windowHeight><![CDATA[965.000000]]></windowHeight>
|
||||
<windowLocationX><![CDATA[-1886.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[1072.000000]]></windowLocationY>
|
||||
<windowScrollOrigin><![CDATA[{0, 0}]]></windowScrollOrigin>
|
||||
<windowWidth><![CDATA[3221.000000]]></windowWidth>
|
||||
</SQLDocumentInfo>
|
||||
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
|
||||
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>
|
||||
|
||||
11
db/prepare-permissions-test.sql
Normal file
11
db/prepare-permissions-test.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
/* remove VIEW_ONLY permission for admin user as it will prevent write operations*/
|
||||
delete from user_permissions;
|
||||
|
||||
delete from permissions;
|
||||
|
||||
insert into permissions (permission_sid, name, description)
|
||||
values
|
||||
('ffbc342a-546a-11ed-bdc3-0242ac120002', 'VIEW_ONLY', 'Can view data but not make changes'),
|
||||
('ffbc3a10-546a-11ed-bdc3-0242ac120002', 'PROVISION_SERVICES', 'Can provision services'),
|
||||
('ffbc3c5e-546a-11ed-bdc3-0242ac120002', 'PROVISION_USERS', 'Can provision users');
|
||||
|
||||
@@ -6,6 +6,7 @@ const {readFile} = require('fs/promises');
|
||||
const {execSync} = require('child_process');
|
||||
const {version:desiredVersion} = require('../package.json');
|
||||
const logger = require('pino')();
|
||||
const fs = require('fs');
|
||||
|
||||
logger.info(`upgrade-jambonz-db: desired version ${desiredVersion}`);
|
||||
|
||||
@@ -22,6 +23,20 @@ const opts = {
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
multipleStatements: true
|
||||
};
|
||||
const rejectUnauthorized = process.env.JAMBONES_MYSQL_REJECT_UNAUTHORIZED;
|
||||
const ssl_ca_file = process.env.JAMBONES_MYSQL_SSL_CA_FILE;
|
||||
const ssl_cert_file = process.env.JAMBONES_MYSQL_SSL_CERT_FILE;
|
||||
const ssl_key_file = process.env.JAMBONES_MYSQL_SSL_KEY_FILE;
|
||||
const sslFilesProvided = Boolean(ssl_ca_file && ssl_cert_file && ssl_key_file);
|
||||
if (rejectUnauthorized !== undefined || sslFilesProvided) {
|
||||
opts.ssl = {
|
||||
...(rejectUnauthorized !== undefined && { rejectUnauthorized: rejectUnauthorized === '0' ? false : true }),
|
||||
...(ssl_ca_file && { ca: fs.readFileSync(ssl_ca_file) }),
|
||||
...(ssl_cert_file && { cert: fs.readFileSync(ssl_cert_file) }),
|
||||
...(ssl_key_file && { key: fs.readFileSync(ssl_key_file) })
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const sql = {
|
||||
'7006': [
|
||||
@@ -210,9 +225,20 @@ const sql = {
|
||||
'ALTER TABLE google_custom_voices ADD COLUMN use_voice_cloning_key BOOLEAN DEFAULT false',
|
||||
'ALTER TABLE voip_carriers ADD COLUMN dtmf_type ENUM(\'rfc2833\',\'tones\',\'info\') NOT NULL DEFAULT \'rfc2833\'',
|
||||
'ALTER TABLE voip_carriers ADD COLUMN outbound_sip_proxy VARCHAR(255)',
|
||||
]
|
||||
],
|
||||
9004: [
|
||||
'ALTER TABLE applications ADD COLUMN env_vars TEXT',
|
||||
],
|
||||
9005: [
|
||||
'UPDATE applications SET speech_synthesis_voice = \'en-US-Standard-C\' WHERE speech_synthesis_voice IS NULL AND speech_synthesis_vendor = \'google\' AND speech_synthesis_language = \'en-US\'',
|
||||
'ALTER TABLE applications MODIFY COLUMN speech_synthesis_voice VARCHAR(255) DEFAULT \'en-US-Standard-C\'',
|
||||
'ALTER TABLE voip_carriers ADD COLUMN trunk_type ENUM(\'static_ip\',\'auth\',\'reg\') NOT NULL DEFAULT \'static_ip\'',
|
||||
'ALTER TABLE predefined_carriers ADD COLUMN trunk_type ENUM(\'static_ip\',\'auth\',\'reg\') NOT NULL DEFAULT \'static_ip\'',
|
||||
'CREATE INDEX idx_sip_gateways_inbound_carrier ON sip_gateways (inbound,voip_carrier_sid)',
|
||||
'CREATE INDEX idx_sip_gateways_inbound_lookup ON sip_gateways (inbound,netmask,ipv4)',
|
||||
'CREATE INDEX idx_sip_gateways_inbound_netmask ON sip_gateways (inbound,netmask)'
|
||||
],
|
||||
};
|
||||
|
||||
const doIt = async() => {
|
||||
let connection;
|
||||
try {
|
||||
@@ -245,6 +271,8 @@ const doIt = async() => {
|
||||
if (val < 9000) upgrades.push(...sql['9000']);
|
||||
if (val < 9002) upgrades.push(...sql['9002']);
|
||||
if (val < 9003) upgrades.push(...sql['9003']);
|
||||
if (val < 9004) upgrades.push(...sql['9004']);
|
||||
if (val < 9005) upgrades.push(...sql['9005']);
|
||||
|
||||
// perform all upgrades
|
||||
logger.info({upgrades}, 'applying schema upgrades..');
|
||||
|
||||
@@ -35,8 +35,8 @@ function makeStrategy(logger) {
|
||||
debug(err);
|
||||
logger.info({err}, 'Error checking redis for jwt');
|
||||
}
|
||||
const { user_sid, service_provider_sid, account_sid, email, name, scope, permissions } = decoded;
|
||||
|
||||
const { user_sid, service_provider_sid, account_sid, email,
|
||||
name, scope, permissions, is_view_only } = decoded;
|
||||
const user = {
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
@@ -45,6 +45,7 @@ function makeStrategy(logger) {
|
||||
email,
|
||||
name,
|
||||
permissions,
|
||||
is_view_only,
|
||||
hasScope: (s) => s === scope,
|
||||
hasAdminAuth: scope === 'admin',
|
||||
hasServiceProviderAuth: scope === 'service_provider',
|
||||
|
||||
@@ -13,7 +13,8 @@ const {
|
||||
deleteKey,
|
||||
incrKey,
|
||||
client: redisClient,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
module.exports = {
|
||||
@@ -29,5 +30,6 @@ module.exports = {
|
||||
deleteKey,
|
||||
redisClient,
|
||||
incrKey,
|
||||
listConferences
|
||||
listConferences,
|
||||
getCallCount
|
||||
};
|
||||
|
||||
@@ -2,6 +2,6 @@ const opts = {
|
||||
level: process.env.JAMBONES_LOGLEVEL || 'info'
|
||||
};
|
||||
const pino = require('pino');
|
||||
const logger = pino(opts, pino.destination(1, {sync: false}));
|
||||
const logger = pino(opts);
|
||||
|
||||
module.exports = logger;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const logger = require('./logger');
|
||||
const {UserPermissionError} = require('./utils/errors');
|
||||
|
||||
function delayLoginMiddleware(req, res, next) {
|
||||
if (req.path.includes('/login') || req.path.includes('/signin')) {
|
||||
@@ -27,6 +28,26 @@ function delayLoginMiddleware(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function verifyViewOnlyUser(req, res, next) {
|
||||
// Skip check for GET requests
|
||||
if (req.method === 'GET') {
|
||||
return next();
|
||||
}
|
||||
// current user is changing their password which shuould be allowed
|
||||
if (req.body?.old_password && req.body?.new_password) {
|
||||
return next();
|
||||
}
|
||||
// Check if user is read-only
|
||||
if (req.user && !!req.user.is_view_only) {
|
||||
const upError = new UserPermissionError('User has view-only access');
|
||||
upError.status = 403;
|
||||
throw upError;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
delayLoginMiddleware
|
||||
delayLoginMiddleware,
|
||||
verifyViewOnlyUser
|
||||
};
|
||||
|
||||
@@ -47,10 +47,14 @@ class ApiKey extends Model {
|
||||
}
|
||||
|
||||
/**
|
||||
* update last_used api key for an account
|
||||
*/
|
||||
* update last_used api key for an account
|
||||
* (only if last_used is null or more than a minute ago)
|
||||
*/
|
||||
static updateLastUsed(account_sid) {
|
||||
const sql = 'UPDATE api_keys SET last_used = NOW() WHERE account_sid = ?';
|
||||
const sql = `UPDATE api_keys
|
||||
SET last_used = NOW()
|
||||
WHERE account_sid = ?
|
||||
AND (last_used IS NULL OR last_used < NOW() - INTERVAL 1 MINUTE)`;
|
||||
const args = [account_sid];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -36,25 +36,118 @@ class Application extends Model {
|
||||
super();
|
||||
}
|
||||
|
||||
static _criteriaBuilder(obj, args) {
|
||||
let sql = '';
|
||||
if (obj.account_sid) {
|
||||
sql += ' AND app.account_sid = ?';
|
||||
args.push(obj.account_sid);
|
||||
}
|
||||
if (obj.service_provider_sid) {
|
||||
sql += ' AND app.account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)';
|
||||
args.push(obj.service_provider_sid);
|
||||
}
|
||||
if (obj.name) {
|
||||
sql += ' AND app.name LIKE ?';
|
||||
args.push(`%${obj.name}%`);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
static countAll(obj) {
|
||||
const args = [];
|
||||
const criteriaClause = Application._criteriaBuilder(obj, args);
|
||||
|
||||
// Only use "WHERE 1 = 1" if there are no filters
|
||||
// Otherwise start with the actual filter for better index usage
|
||||
let sql;
|
||||
if (criteriaClause) {
|
||||
// Remove leading ' AND ' from criteriaBuilder output and use as WHERE clause
|
||||
sql = 'SELECT COUNT(*) AS count FROM applications app WHERE ' + criteriaClause.substring(5);
|
||||
} else {
|
||||
// No filters provided - count all applications
|
||||
sql = 'SELECT COUNT(*) AS count FROM applications app WHERE 1 = 1';
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
conn.query({sql}, args, (err, results) => {
|
||||
conn.release();
|
||||
if (err) return reject(err);
|
||||
resolve(results[0].count);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* list all applications - for all service providers, for one service provider, or for one account,
|
||||
* or by an optional name
|
||||
*/
|
||||
static retrieveAll(service_provider_sid, account_sid, name) {
|
||||
let sql = retrieveSql + ' WHERE 1 = 1';
|
||||
static retrieveAll(obj) {
|
||||
const { page, page_size = 50 } = obj || {};
|
||||
|
||||
// If pagination is requested, first get the application IDs
|
||||
if (page !== null && page !== undefined) {
|
||||
let idSql = 'SELECT application_sid, name FROM applications app WHERE 1 = 1';
|
||||
const idArgs = [];
|
||||
idSql += Application._criteriaBuilder(obj, idArgs);
|
||||
idSql += ' ORDER BY app.name';
|
||||
|
||||
const limit = Number(page_size);
|
||||
const offset = Number(page > 0 ? (page - 1) : page) * limit;
|
||||
idSql += ' LIMIT ? OFFSET ?';
|
||||
idArgs.push(limit);
|
||||
idArgs.push(offset);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
// Get paginated application IDs
|
||||
conn.query(idSql, idArgs, (err, idResults) => {
|
||||
if (err) {
|
||||
conn.release();
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (idResults.length === 0) {
|
||||
conn.release();
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
// Get full data for these applications
|
||||
const appIds = idResults.map((row) => row.application_sid);
|
||||
const placeholders = appIds.map(() => '?').join(',');
|
||||
const fullSql = `${retrieveSql}
|
||||
WHERE app.application_sid IN (${placeholders}) ORDER BY app.name`;
|
||||
|
||||
conn.query({sql: fullSql, nestTables: true}, appIds, (err, results) => {
|
||||
conn.release();
|
||||
if (err) return reject(err);
|
||||
const r = transmogrifyResults(results);
|
||||
resolve(r);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// No pagination - use original query
|
||||
const args = [];
|
||||
if (account_sid) {
|
||||
sql = `${sql} AND app.account_sid = ?`;
|
||||
args.push(account_sid);
|
||||
}
|
||||
else if (service_provider_sid) {
|
||||
sql = `${sql} AND account_sid in (SELECT account_sid from accounts WHERE service_provider_sid = ?)`;
|
||||
args.push(service_provider_sid);
|
||||
}
|
||||
if (name) {
|
||||
sql = `${sql} AND app.name = ?`;
|
||||
args.push(name);
|
||||
const criteriaClause = Application._criteriaBuilder(obj, args);
|
||||
|
||||
// Only use "WHERE 1 = 1" if there are no filters
|
||||
// Otherwise start with the actual filter for better index usage
|
||||
let sql;
|
||||
if (criteriaClause) {
|
||||
// Remove leading ' AND ' from criteriaBuilder output and use as WHERE clause
|
||||
sql = retrieveSql + ' WHERE ' + criteriaClause.substring(5);
|
||||
} else {
|
||||
// No filters provided - must list all applications
|
||||
sql = retrieveSql + ' WHERE 1 = 1';
|
||||
}
|
||||
sql += ' ORDER BY app.application_sid';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
45
lib/models/permissions.js
Normal file
45
lib/models/permissions.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const sqlAll = `
|
||||
SELECT * from permissions
|
||||
`;
|
||||
const sqlByName = `
|
||||
SELECT * from permissions where name = ?
|
||||
`;
|
||||
|
||||
class Permissions extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAll() {
|
||||
const [rows] = await promisePool.query(sqlAll);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveByName(name) {
|
||||
const [rows] = await promisePool.query(sqlByName, [name]);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
Permissions.table = 'permissions';
|
||||
Permissions.fields = [
|
||||
{
|
||||
name: 'permission_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = Permissions;
|
||||
@@ -26,6 +26,49 @@ class PhoneNumber extends Model {
|
||||
return rows;
|
||||
}
|
||||
|
||||
static _criteriaBuilder(obj, params) {
|
||||
let sql = '';
|
||||
if (obj.service_provider_sid) {
|
||||
sql += ' AND account_sid IN (SELECT account_sid FROM accounts WHERE service_provider_sid = ?)';
|
||||
params.push(obj.service_provider_sid);
|
||||
}
|
||||
if (obj.account_sid) {
|
||||
sql += ' AND account_sid = ?';
|
||||
params.push(obj.account_sid);
|
||||
}
|
||||
if (obj.filter) {
|
||||
sql += ' AND number LIKE ?';
|
||||
params.push(`%${obj.filter}%`);
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
static async countAll(obj) {
|
||||
let sql = 'SELECT COUNT(*) AS count FROM phone_numbers WHERE 1 = 1';
|
||||
const args = [];
|
||||
sql += PhoneNumber._criteriaBuilder(obj, args);
|
||||
const [rows] = await promisePool.query(sql, args);
|
||||
return rows[0].count;
|
||||
}
|
||||
|
||||
static async retrieveAllByCriteria(obj) {
|
||||
let sql = 'SELECT * FROM phone_numbers WHERE 1=1';
|
||||
const params = [];
|
||||
const { page, page_size = 50 } = obj || {};
|
||||
sql += PhoneNumber._criteriaBuilder(obj, params);
|
||||
sql += ' ORDER BY number';
|
||||
if (page !== null && page !== undefined) {
|
||||
const limit = Number(page_size);
|
||||
const offset = Number(page > 0 ? (page - 1) : page) * limit;
|
||||
sql += ' LIMIT ? OFFSET ?';
|
||||
params.push(limit);
|
||||
params.push(offset);
|
||||
}
|
||||
const [rows] = await promisePool.query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve a phone number
|
||||
*/
|
||||
|
||||
53
lib/models/user-permissions.js
Normal file
53
lib/models/user-permissions.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const Model = require('./model');
|
||||
const {promisePool} = require('../db');
|
||||
const sqlAll = `
|
||||
SELECT * from user_permissions
|
||||
`;
|
||||
const sqlByUserIdPermissionSid = `
|
||||
SELECT * from user_permissions where user_sid = ? and permission_sid = ?
|
||||
`;
|
||||
const sqlByUserId = `
|
||||
SELECT * from user_permissions where user_sid = ?
|
||||
`;
|
||||
|
||||
class UserPermissions extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static async retrieveAll() {
|
||||
const [rows] = await promisePool.query(sqlAll);
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveByUserIdPermissionSid(user_sid, permission_sid) {
|
||||
const [rows] = await promisePool.query(sqlByUserIdPermissionSid, [user_sid, permission_sid]);
|
||||
return rows;
|
||||
}
|
||||
static async retrieveByUserId(user_sid) {
|
||||
const [rows] = await promisePool.query(sqlByUserId, [user_sid]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UserPermissions.table = 'user_permissions';
|
||||
UserPermissions.fields = [
|
||||
{
|
||||
name: 'user_permissions_sid',
|
||||
type: 'string',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'user_sid',
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'permission_sid',
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = UserPermissions;
|
||||
@@ -8,6 +8,57 @@ class VoipCarrier extends Model {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
static _criteriaBuilder(obj, args) {
|
||||
let sql = '';
|
||||
if (obj.account_sid) {
|
||||
// carrier belong to an account when
|
||||
// 1. account_sid is set
|
||||
// 2. account_sid is null and service_provider_sid matches the account's service_provider_sid
|
||||
sql += ` AND (vc.account_sid = ? OR
|
||||
(vc.account_sid IS NULL AND vc.service_provider_sid IN
|
||||
(SELECT service_provider_sid FROM accounts WHERE account_sid = ?))
|
||||
)`;
|
||||
args.push(obj.account_sid);
|
||||
args.push(obj.account_sid);
|
||||
}
|
||||
if (obj.service_provider_sid) {
|
||||
sql += ' AND vc.service_provider_sid = ?';
|
||||
args.push(obj.service_provider_sid);
|
||||
}
|
||||
if (obj.name) {
|
||||
sql += ' AND vc.name LIKE ?';
|
||||
args.push(`%${obj.name}%`);
|
||||
}
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
static async countAll(obj) {
|
||||
let sql = 'SELECT COUNT(*) AS count FROM voip_carriers vc WHERE 1 = 1';
|
||||
const args = [];
|
||||
sql += VoipCarrier._criteriaBuilder(obj, args);
|
||||
const [rows] = await promisePool.query(sql, args);
|
||||
return rows[0].count;
|
||||
}
|
||||
|
||||
static async retrieveByCriteria(obj) {
|
||||
let sql = 'SELECT * from voip_carriers vc WHERE 1 =1';
|
||||
const args = [];
|
||||
sql += VoipCarrier._criteriaBuilder(obj, args);
|
||||
if (obj.page !== null && obj.page !== undefined) {
|
||||
const limit = Number(obj.page_size || 50);
|
||||
const offset = (Number(obj.page) - 1) * limit;
|
||||
sql += ' LIMIT ? OFFSET ?';
|
||||
args.push(limit, offset);
|
||||
}
|
||||
const [rows] = await promisePool.query(sql, args);
|
||||
if (rows) {
|
||||
rows.map((r) => r.register_status = JSON.parse(r.register_status || '{}'));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
static async retrieveAll(account_sid) {
|
||||
if (!account_sid) return super.retrieveAll();
|
||||
const [rows] = await promisePool.query(retrieveSql, account_sid);
|
||||
|
||||
@@ -15,7 +15,9 @@ class S3MultipartUploadStream extends Writable {
|
||||
this.uploadId = null;
|
||||
this.partNumber = 1;
|
||||
this.multipartETags = [];
|
||||
this.buffer = Buffer.alloc(0);
|
||||
// accumulate incoming chunks to avoid O(n^2) Buffer.concat on every write
|
||||
this.chunks = [];
|
||||
this.bufferedBytes = 0;
|
||||
this.minPartSize = 5 * 1024 * 1024; // 5 MB
|
||||
this.s3 = new S3Client(opts.bucketCredential);
|
||||
this.metadata = opts.metadata;
|
||||
@@ -31,13 +33,13 @@ class S3MultipartUploadStream extends Writable {
|
||||
return response.UploadId;
|
||||
}
|
||||
|
||||
async _uploadBuffer() {
|
||||
async _uploadPart(bodyBuffer) {
|
||||
const uploadPartCommand = new UploadPartCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: this.objectKey,
|
||||
PartNumber: this.partNumber,
|
||||
UploadId: this.uploadId,
|
||||
Body: this.buffer,
|
||||
Body: bodyBuffer,
|
||||
});
|
||||
|
||||
const uploadPartResponse = await this.s3.send(uploadPartCommand);
|
||||
@@ -54,11 +56,16 @@ class S3MultipartUploadStream extends Writable {
|
||||
this.uploadId = await this._initMultipartUpload();
|
||||
}
|
||||
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
// accumulate without concatenating on every write
|
||||
this.chunks.push(chunk);
|
||||
this.bufferedBytes += chunk.length;
|
||||
|
||||
if (this.buffer.length >= this.minPartSize) {
|
||||
await this._uploadBuffer();
|
||||
this.buffer = Buffer.alloc(0);
|
||||
if (this.bufferedBytes >= this.minPartSize) {
|
||||
const partBuffer = Buffer.concat(this.chunks, this.bufferedBytes);
|
||||
// reset accumulators before awaiting upload to allow GC
|
||||
this.chunks = [];
|
||||
this.bufferedBytes = 0;
|
||||
await this._uploadPart(partBuffer);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
@@ -69,8 +76,11 @@ class S3MultipartUploadStream extends Writable {
|
||||
|
||||
async _finalize(err) {
|
||||
try {
|
||||
if (this.buffer.length > 0) {
|
||||
await this._uploadBuffer();
|
||||
if (this.bufferedBytes > 0) {
|
||||
const finalBuffer = Buffer.concat(this.chunks, this.bufferedBytes);
|
||||
this.chunks = [];
|
||||
this.bufferedBytes = 0;
|
||||
await this._uploadPart(finalBuffer);
|
||||
}
|
||||
|
||||
const completeMultipartUploadCommand = new CompleteMultipartUploadCommand({
|
||||
|
||||
@@ -51,8 +51,10 @@ async function upload(logger, socket) {
|
||||
|
||||
/**encoder */
|
||||
let encoder;
|
||||
let recordFormat;
|
||||
if (account[0].record_format === 'wav') {
|
||||
encoder = new wav.Writer({ channels: 2, sampleRate, bitDepth: 16 });
|
||||
recordFormat = 'wav';
|
||||
} else {
|
||||
// default is mp3
|
||||
encoder = new PCMToMP3Encoder({
|
||||
@@ -60,7 +62,9 @@ async function upload(logger, socket) {
|
||||
sampleRate: sampleRate,
|
||||
bitrate: 128
|
||||
}, logger);
|
||||
recordFormat = 'mp3';
|
||||
}
|
||||
logger.info({ record_format: recordFormat, channels: 2, sampleRate }, 'record upload: selected encoder');
|
||||
|
||||
/* start streaming data */
|
||||
pipeline(
|
||||
|
||||
@@ -26,7 +26,7 @@ const getUploader = (key, metadata, bucket_credential, logger) => {
|
||||
accessKeyId: bucket_credential.access_key_id,
|
||||
secretAccessKey: bucket_credential.secret_access_key,
|
||||
},
|
||||
region: 'us-east-1',
|
||||
region: bucket_credential.region || 'us-east-1',
|
||||
forcePathStyle: true
|
||||
};
|
||||
return new S3MultipartUploadStream(logger, uploaderOpts);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const router = require('express').Router();
|
||||
const assert = require('assert');
|
||||
const request = require('request');
|
||||
const {DbErrorBadRequest, DbErrorForbidden, DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const Account = require('../../models/account');
|
||||
const Application = require('../../models/application');
|
||||
@@ -24,7 +23,8 @@ const {
|
||||
} = require('./utils');
|
||||
const short = require('short-uuid');
|
||||
const VoipCarrier = require('../../models/voip-carrier');
|
||||
const { encrypt, obscureBucketCredentialsSensitiveData, isObscureKey } = require('../../utils/encrypt-decrypt');
|
||||
const { encrypt, obscureBucketCredentialsSensitiveData,
|
||||
isObscureKey, decrypt } = require('../../utils/encrypt-decrypt');
|
||||
const { testS3Storage, testGoogleStorage, testAzureStorage } = require('../../utils/storage-utils');
|
||||
const translator = short();
|
||||
|
||||
@@ -93,8 +93,34 @@ router.get('/:sid/Applications', async(req, res) => {
|
||||
try {
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
const results = await Application.retrieveAll(null, account_sid);
|
||||
res.status(200).json(results);
|
||||
const {page, page_size, name} = req.query || {};
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
let results = [];
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await Application.countAll({account_sid, name});
|
||||
results = await Application.retrieveAll({
|
||||
account_sid, name, page, page_size
|
||||
});
|
||||
} else {
|
||||
results = await Application.retrieveAll({account_sid});
|
||||
}
|
||||
const ret = results.map((a) => {
|
||||
if (a.env_vars) {
|
||||
a.env_vars = JSON.parse(decrypt(a.env_vars));
|
||||
return a;
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
});
|
||||
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: ret
|
||||
} : ret;
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -114,6 +140,11 @@ router.put('/:sid/VoipCarriers/:voip_carrier_sid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
try {
|
||||
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
|
||||
&& !req.user.hasScope('admin'))) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
|
||||
const sid = parseVoipCarrierSid(req);
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
@@ -133,8 +164,15 @@ router.post('/:sid/VoipCarriers', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const payload = req.body;
|
||||
try {
|
||||
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
|
||||
|| !!req.user.hasScope('admin'))) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
const account_sid = parseAccountSid(req);
|
||||
await validateRequest(req, account_sid);
|
||||
// Set the service_provder_sid to the relevent value for the account
|
||||
const account = await Account.retrieve(req.user.account_sid);
|
||||
payload.service_provider_sid = account[0].service_provider_sid;
|
||||
|
||||
logger.debug({payload}, 'POST /:sid/VoipCarriers');
|
||||
const uuid = await VoipCarrier.make({
|
||||
@@ -222,7 +260,8 @@ router.get('/:sid/RegisteredSipUsers/:client', async(req, res) => {
|
||||
allow_direct_app_calling: clientDb ? clientDb.allow_direct_app_calling : 0,
|
||||
allow_direct_queue_calling: clientDb ? clientDb.allow_direct_queue_calling : 0,
|
||||
allow_direct_user_calling: clientDb ? clientDb.allow_direct_user_calling : 0,
|
||||
registered_status: user ? 'active' : 'inactive'
|
||||
registered_status: user ? 'active' : 'inactive',
|
||||
proxy: user ? user.proxy : null
|
||||
});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -268,7 +307,9 @@ function validateUpdateCall(opts) {
|
||||
'tag',
|
||||
'dtmf',
|
||||
'conferenceParticipantAction',
|
||||
'dub'
|
||||
'dub',
|
||||
'boostAudioSignal',
|
||||
'media_path'
|
||||
]
|
||||
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
|
||||
|
||||
@@ -332,6 +373,9 @@ function validateUpdateCall(opts) {
|
||||
throw new DbErrorBadRequest('conferenceParticipantAction requires tag property when action is \'coach\'');
|
||||
}
|
||||
}
|
||||
if (opts.media_path && !['no-media', 'partial-media', 'full-media'].includes(opts.media_path)) {
|
||||
throw new DbErrorBadRequest('invalid media_path');
|
||||
}
|
||||
}
|
||||
|
||||
function validateTo(to) {
|
||||
@@ -356,7 +400,10 @@ async function validateCreateCall(logger, sid, req) {
|
||||
const {lookupAppBySid} = req.app.locals;
|
||||
const obj = req.body;
|
||||
|
||||
if (req.user.account_sid !== sid) throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
|
||||
if (req.user.hasServiceProviderAuth ||
|
||||
req.user.hasAccountAuth && req.user.account_sid !== sid) {
|
||||
throw new DbErrorBadRequest(`unauthorized createCall request for account ${sid}`);
|
||||
}
|
||||
|
||||
obj.account_sid = sid;
|
||||
if (!obj.from) throw new DbErrorBadRequest('missing from parameter');
|
||||
@@ -527,6 +574,8 @@ router.post('/', async(req, res) => {
|
||||
}
|
||||
delete obj[prop];
|
||||
}
|
||||
//force sip realm to lowercase
|
||||
if (obj.sip_realm) { obj.sip_realm = obj.sip_realm.toLowerCase(); }
|
||||
|
||||
logger.debug(`Attempting to add account ${JSON.stringify(obj)}`);
|
||||
const uuid = await Account.make(obj);
|
||||
@@ -651,12 +700,12 @@ function encryptBucketCredential(obj, storedCredentials = {}) {
|
||||
name,
|
||||
access_key_id,
|
||||
tags,
|
||||
endpoint
|
||||
endpoint,
|
||||
} = obj.bucket_credential;
|
||||
let {
|
||||
secret_access_key,
|
||||
service_key,
|
||||
connection_string
|
||||
connection_string,
|
||||
} = obj.bucket_credential;
|
||||
|
||||
switch (vendor) {
|
||||
@@ -681,7 +730,9 @@ function encryptBucketCredential(obj, storedCredentials = {}) {
|
||||
secret_access_key = storedCredentials.secret_access_key;
|
||||
}
|
||||
const s3Data = JSON.stringify({vendor, endpoint, name, access_key_id,
|
||||
secret_access_key, tags});
|
||||
secret_access_key, tags,
|
||||
...(region && {region})
|
||||
});
|
||||
obj.bucket_credential = encrypt(s3Data);
|
||||
break;
|
||||
case 'google':
|
||||
@@ -767,6 +818,9 @@ router.put('/:sid', async(req, res) => {
|
||||
|
||||
encryptBucketCredential(obj, storedBucketCredentials);
|
||||
|
||||
//force sip realm to lowercase
|
||||
if (obj.sip_realm) { obj.sip_realm = obj.sip_realm.toLowerCase();}
|
||||
|
||||
const rowsAffected = await Account.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
@@ -940,24 +994,25 @@ router.post('/:sid/Calls', async(req, res) => {
|
||||
|
||||
await validateCreateCall(logger, sid, req);
|
||||
updateLastUsed(logger, sid, req).catch((err) => {});
|
||||
request({
|
||||
url: serviceUrl,
|
||||
const response = await fetch(serviceUrl, {
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: Object.assign(req.body, {account_sid: sid})
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
if (response.statusCode !== 201) {
|
||||
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
return res.status(201).json(body);
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(Object.assign(req.body, {account_sid: sid}))
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error(`Error sending createCall POST to ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
|
||||
if (response.status !== 201) {
|
||||
logger.error(`Non-success response returned by createCall ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
const body = await response.json();
|
||||
return res.status(201).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -1048,22 +1103,54 @@ const updateCall = async(req, res) => {
|
||||
await validateRequest(req, accountSid);
|
||||
const callSid = parseCallSid(req);
|
||||
validateUpdateCall(req.body);
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
const call = await retrieveCall(accountSid, callSid);
|
||||
if (call) {
|
||||
const url = `${call.serviceUrl}/${process.env.JAMBONES_API_VERSION || 'v1'}/updateCall/${callSid}`;
|
||||
logger.debug({call, url, payload: req.body}, `updateCall: retrieved call info for call sid ${callSid}`);
|
||||
request({
|
||||
url: url,
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: req.body
|
||||
}).pipe(res);
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(req.body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
try {
|
||||
const text = await response.text();
|
||||
logger.error(`Error sending updateCall POST to ${url}, status: ${response.status} body: ${text}`);
|
||||
|
||||
// Try to parse as JSON if there's content
|
||||
if (text) {
|
||||
try {
|
||||
const body = JSON.parse(text);
|
||||
return res.status(response.status).json(body);
|
||||
} catch {
|
||||
// Not valid JSON
|
||||
return res.status(response.status).send(text);
|
||||
}
|
||||
}
|
||||
return res.sendStatus(response.status);
|
||||
} catch (err) {
|
||||
logger.error({err}, `updateCall: error reading response from ${url}`);
|
||||
return res.sendStatus(response.status);
|
||||
}
|
||||
}
|
||||
if (response.status === 200) {
|
||||
// feature server return json for sip_request command
|
||||
// with 200 OK
|
||||
const body = await response.json();
|
||||
return res.status(200).json(body);
|
||||
} else {
|
||||
// rest commander returns 202 Accepted for all other commands
|
||||
return res.sendStatus(response.status);
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.debug(`updateCall: call not found for call sid ${callSid}`);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -1090,7 +1177,9 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
|
||||
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
|
||||
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
|
||||
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
|
||||
if (!serviceUrl) {
|
||||
return res.status(480).json({msg: 'no available feature servers at this time'});
|
||||
}
|
||||
await validateCreateMessage(logger, account_sid, req);
|
||||
|
||||
const payload = {
|
||||
@@ -1100,22 +1189,19 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
};
|
||||
logger.debug({payload}, `sending createMessage API request to to ${serviceUrl}`);
|
||||
updateLastUsed(logger, account_sid, req).catch(() => {});
|
||||
request({
|
||||
url: serviceUrl,
|
||||
const response = await fetch(serviceUrl, {
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: payload
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
logger.error(err, `Error sending createMessage POST to ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
logger.error({statusCode: response.statusCode}, `Non-success response returned by createMessage ${serviceUrl}`);
|
||||
return body ? res.status(response.statusCode).json(body) : res.sendStatus(response.statusCode);
|
||||
}
|
||||
res.status(201).json(body);
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error(`Error sending createMessage POST to ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
const body = await response.json();
|
||||
return res.status(response.status).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -1156,4 +1242,23 @@ router.get('/:sid/Conferences', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* retrieve counts of calls under an account
|
||||
*/
|
||||
router.get('/:sid/CallCount', async(req, res) => {
|
||||
const {logger, getCallCount} = req.app.locals;
|
||||
try {
|
||||
const accountSid = parseAccountSid(req);
|
||||
await validateRequest(req, accountSid);
|
||||
const count = await getCallCount(accountSid);
|
||||
count.outbound = Number(count.outbound);
|
||||
count.inbound = Number(count.inbound);
|
||||
logger.debug(`retrieved, outbound: ${count.outbound}, inbound: ${count.inbound}, for account sid ${accountSid}`);
|
||||
res.status(200).json(count);
|
||||
updateLastUsed(logger, accountSid, req).catch((err) => {});
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
47
lib/routes/api/appenv.js
Normal file
47
lib/routes/api/appenv.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const { fetchAppEnvSchema, validateAppEnvSchema } = require('../../utils/appenv_utils');
|
||||
|
||||
const URL = require('url').URL;
|
||||
|
||||
const isValidUrl = (s) => {
|
||||
const protocols = ['https:', 'http:', 'ws:', 'wss:'];
|
||||
try {
|
||||
const url = new URL(s);
|
||||
if (protocols.includes(url.protocol)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/* get appenv schema for endpoint */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const url = req.query.url;
|
||||
if (!isValidUrl(url)) {
|
||||
sysError(logger, res, 'Invalid URL');
|
||||
} else {
|
||||
try {
|
||||
const appenv = await fetchAppEnvSchema(logger, url);
|
||||
if (appenv && validateAppEnvSchema(appenv)) {
|
||||
return res.status(200).json(appenv);
|
||||
} else if (appenv) {
|
||||
return res.status(400).json({
|
||||
msg: 'Invalid appenv schema',
|
||||
});
|
||||
} else {
|
||||
return res.status(204).end(); //No appenv returned from url, normal scenario
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -7,11 +7,13 @@ const {promisePool} = require('../../db');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
const { validate } = require('@jambonz/verb-specifications');
|
||||
const { parseApplicationSid } = require('./utils');
|
||||
const { parseApplicationSid, isInvalidUrl } = require('./utils');
|
||||
const preconditions = {
|
||||
'add': validateAdd,
|
||||
'update': validateUpdate
|
||||
};
|
||||
const { fetchAppEnvSchema, validateAppEnvData } = require('../../utils/appenv_utils');
|
||||
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
|
||||
|
||||
const validateRequest = async(req, account_sid) => {
|
||||
try {
|
||||
@@ -62,6 +64,14 @@ async function validateAdd(req) {
|
||||
if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') {
|
||||
throw new DbErrorBadRequest('\'call_status_hook\' must be an object when adding an application');
|
||||
}
|
||||
let urlError = await isInvalidUrl(req.body.call_hook.url);
|
||||
if (urlError) {
|
||||
throw new DbErrorBadRequest(`call_hook ${urlError}`);
|
||||
}
|
||||
urlError = await isInvalidUrl(req.body.call_status_hook.url);
|
||||
if (urlError) {
|
||||
throw new DbErrorBadRequest(`call_status_hook ${urlError}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateUpdate(req, sid) {
|
||||
@@ -91,6 +101,20 @@ async function validateUpdate(req, sid) {
|
||||
if (req.body.call_status_hook && typeof req.body.call_hook !== 'object') {
|
||||
throw new DbErrorBadRequest('\'call_status_hook\' must be an object when updating an application');
|
||||
}
|
||||
|
||||
let urlError;
|
||||
if (req.body.call_hook) {
|
||||
urlError = await isInvalidUrl(req.body.call_hook.url);
|
||||
if (urlError) {
|
||||
throw new DbErrorBadRequest(`call_hook ${urlError}`);
|
||||
}
|
||||
}
|
||||
if (req.body.call_status_hook) {
|
||||
urlError = await isInvalidUrl(req.body.call_status_hook.url);
|
||||
if (urlError) {
|
||||
throw new DbErrorBadRequest(`call_status_hook ${urlError}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function validateDelete(req, sid) {
|
||||
@@ -142,6 +166,16 @@ router.post('/', async(req, res) => {
|
||||
throw new DbErrorBadRequest(err);
|
||||
}
|
||||
}
|
||||
// validate env_vars data if required
|
||||
if (obj['env_vars']) {
|
||||
const appenvschema = await fetchAppEnvSchema(logger, req.body.call_hook.url);
|
||||
const errors = await validateAppEnvData(appenvschema, obj['env_vars']);
|
||||
if (errors) {
|
||||
throw new DbErrorBadRequest(errors);
|
||||
} else {
|
||||
obj['env_vars'] = encrypt(JSON.stringify(obj['env_vars']));
|
||||
}
|
||||
}
|
||||
|
||||
const uuid = await Application.make(obj);
|
||||
res.status(201).json({sid: uuid});
|
||||
@@ -153,12 +187,34 @@ router.post('/', async(req, res) => {
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {page, page_size, name} = req.query || {};
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
try {
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const name = req.query.name;
|
||||
const results = await Application.retrieveAll(service_provider_sid, account_sid, name);
|
||||
res.status(200).json(results);
|
||||
let results = [];
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await Application.countAll({service_provider_sid, account_sid, name});
|
||||
}
|
||||
results = await Application.retrieveAll({
|
||||
service_provider_sid, account_sid, name, page, page_size
|
||||
});
|
||||
const ret = results.map((a) => {
|
||||
if (a.env_vars) {
|
||||
a.env_vars = JSON.parse(decrypt(a.env_vars));
|
||||
return a;
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
});
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: ret
|
||||
} : ret;
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -174,6 +230,9 @@ router.get('/:sid', async(req, res) => {
|
||||
const results = await Application.retrieve(application_sid, service_provider_sid, account_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
await validateRequest(req, results[0].account_sid);
|
||||
if (results[0].env_vars) {
|
||||
results[0].env_vars = JSON.parse(decrypt(results[0].env_vars));
|
||||
}
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
@@ -228,6 +287,8 @@ router.put('/:sid', async(req, res) => {
|
||||
try {
|
||||
const sid = parseApplicationSid(req);
|
||||
await validateUpdate(req, sid);
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
|
||||
// create webhooks if provided
|
||||
const obj = Object.assign({}, req.body);
|
||||
@@ -243,9 +304,6 @@ router.put('/:sid', async(req, res) => {
|
||||
obj[`${prop}_sid`] = sid;
|
||||
}
|
||||
}
|
||||
else {
|
||||
obj[`${prop}_sid`] = null;
|
||||
}
|
||||
delete obj[prop];
|
||||
}
|
||||
|
||||
@@ -259,6 +317,21 @@ router.put('/:sid', async(req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// validate env_vars data if required
|
||||
if (obj['env_vars']) {
|
||||
const applications = await Application.retrieve(sid, service_provider_sid, account_sid);
|
||||
const call_hook_url = req.body.call_hook ?
|
||||
(req.body.call_hook.url || req.body.call_hook) :
|
||||
applications[0].call_hook.url;
|
||||
const appenvschema = await fetchAppEnvSchema(logger, call_hook_url);
|
||||
const errors = await validateAppEnvData(appenvschema, obj['env_vars']);
|
||||
if (errors) {
|
||||
throw new DbErrorBadRequest(errors);
|
||||
} else {
|
||||
obj['env_vars'] = encrypt(JSON.stringify(obj['env_vars']));
|
||||
}
|
||||
}
|
||||
|
||||
const rowsAffected = await Application.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
|
||||
@@ -52,20 +52,21 @@ router.post('/', async(req, res) => {
|
||||
let obj;
|
||||
try {
|
||||
if (!email || !validateEmail(email)) {
|
||||
logger.info({email}, 'Bad POST to /forgot-password is missing email or invalid email');
|
||||
return res.status(400).json({error: 'invalid or missing email'});
|
||||
}
|
||||
|
||||
const [r] = await promisePool.query({sql, nestTables: true}, email);
|
||||
if (0 === r.length) {
|
||||
logger.info('user not found');
|
||||
logger.info(`user not found: ${email}`);
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
}
|
||||
obj = r[0];
|
||||
if (!obj.user.is_active) {
|
||||
logger.info(obj.user.name, 'user is inactive');
|
||||
logger.info({user: obj.user.name, obj}, 'user is inactive');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
} else if (obj.acc.account_sid !== null && !obj.acc.is_active) {
|
||||
logger.info(obj.acc.account_sid, 'account is inactive');
|
||||
logger.info({account_sid: obj.acc.account_sid, obj}, 'account is inactive');
|
||||
return res.status(400).json({error: 'failed to reset your password'});
|
||||
}
|
||||
res.sendStatus(204);
|
||||
|
||||
@@ -39,6 +39,7 @@ api.use('/change-password', require('./change-password'));
|
||||
api.use('/ActivationCode', require('./activation-code'));
|
||||
api.use('/Availability', require('./availability'));
|
||||
api.use('/AccountTest', require('./account-test'));
|
||||
api.use('/AppEnv', require('./appenv'));
|
||||
//api.use('/Products', require('./products'));
|
||||
api.use('/Prices', require('./prices'));
|
||||
api.use('/StripeCustomerId', require('./stripe-customer-id'));
|
||||
|
||||
@@ -29,6 +29,9 @@ router.post('/', async(req, res) => {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
logger.info({r}, 'successfully retrieved user account');
|
||||
if (r[0].provider !== 'local') {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
const maxLoginAttempts = process.env.LOGIN_ATTEMPTS_MAX_RETRIES || 6;
|
||||
const loginAttempsBlocked = await retrieveKey(`login:${r[0].user_sid}`) >= maxLoginAttempts;
|
||||
@@ -71,9 +74,13 @@ router.post('/', async(req, res) => {
|
||||
obj.service_provider_sid = r[0].service_provider_sid;
|
||||
obj.service_provider_name = service_provider[0].name;
|
||||
}
|
||||
// if there is only one permission and it is VIEW_ONLY, then the user is view only
|
||||
// this is to prevent write operations on the API
|
||||
const is_view_only = permissions.length === 1 && permissions.includes('VIEW_ONLY');
|
||||
const payload = {
|
||||
scope: obj.scope,
|
||||
permissions,
|
||||
is_view_only,
|
||||
...(obj.service_provider_sid && {
|
||||
service_provider_sid: obj.service_provider_sid,
|
||||
service_provider_name: obj.service_provider_name
|
||||
@@ -83,7 +90,8 @@ router.post('/', async(req, res) => {
|
||||
account_name: obj.account_name,
|
||||
service_provider_name: obj.service_provider_name
|
||||
}),
|
||||
user_sid: obj.user_sid
|
||||
user_sid: obj.user_sid,
|
||||
name: username
|
||||
};
|
||||
|
||||
const expiresIn = parseInt(process.env.JWT_EXPIRES_IN || 60) * 60;
|
||||
|
||||
@@ -13,11 +13,17 @@ const preconditions = {
|
||||
};
|
||||
const sysError = require('../error');
|
||||
const { parsePhoneNumberSid } = require('./utils');
|
||||
const hasWhitespace = (str) => /\s/.test(str);
|
||||
|
||||
|
||||
/* check for required fields when adding */
|
||||
async function validateAdd(req) {
|
||||
try {
|
||||
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
|
||||
&& !req.user.hasScope('admin'))) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
|
||||
/* account level user can only act on carriers associated to his/her account */
|
||||
if (req.user.hasAccountAuth) {
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
@@ -28,6 +34,7 @@ async function validateAdd(req) {
|
||||
}
|
||||
|
||||
if (!req.body.number) throw new DbErrorBadRequest('number is required');
|
||||
if (hasWhitespace(req.body.number)) throw new DbErrorBadRequest('number cannot contain whitespace');
|
||||
const formattedNumber = e164(req.body.number);
|
||||
req.body.number = formattedNumber;
|
||||
} catch (err) {
|
||||
@@ -97,11 +104,35 @@ decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions);
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {service_provider_sid: query_service_provider_sid,
|
||||
account_sid: query_account_sid, filter, page, page_size} = req.query;
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
let service_provider_sid = null, account_sid = query_account_sid;
|
||||
if (req.user.hasAccountAuth) {
|
||||
account_sid = req.user.account_sid;
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
service_provider_sid = req.user.service_provider_sid;
|
||||
} else {
|
||||
// admin user can query all phone numbers
|
||||
service_provider_sid = query_service_provider_sid;
|
||||
account_sid = query_account_sid;
|
||||
}
|
||||
try {
|
||||
const results = req.user.hasServiceProviderAuth ?
|
||||
await PhoneNumber.retrieveAllForSP(req.user.service_provider_sid) :
|
||||
await PhoneNumber.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null);
|
||||
res.status(200).json(results);
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await PhoneNumber.countAll({service_provider_sid, account_sid, filter});
|
||||
}
|
||||
const results = await PhoneNumber.retrieveAllByCriteria({
|
||||
service_provider_sid, account_sid, filter, page, page_size
|
||||
});
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: results,
|
||||
} : results;
|
||||
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -392,6 +392,8 @@ router.post('/', async(req, res) => {
|
||||
account_sid: userProfile.account_sid
|
||||
}, 'generated jwt');
|
||||
|
||||
// Remove activation code from the response data!
|
||||
delete userProfile.email_activation_code;
|
||||
res.json({jwt: token, ...userProfile});
|
||||
|
||||
/* Store jwt based on user_id after successful login */
|
||||
|
||||
@@ -46,10 +46,16 @@ async function validateRetrieve(req) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('service_provider') || req.user.hasScope('account')) {
|
||||
if (req.user.hasScope('service_provider')) {
|
||||
if (service_provider_sid === req.user.service_provider_sid) return;
|
||||
}
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
const results = await Account.retrieve(req.user.account_sid);
|
||||
if (service_provider_sid === results[0].service_provider_sid) return;
|
||||
}
|
||||
|
||||
|
||||
throw new DbErrorForbidden('insufficient permissions');
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -149,13 +155,30 @@ router.get('/:sid/VoipCarriers', async(req, res) => {
|
||||
try {
|
||||
await validateRetrieve(req);
|
||||
const service_provider_sid = parseServiceProviderSid(req);
|
||||
const carriers = await VoipCarrier.retrieveAllForSP(service_provider_sid);
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
return res.status(200).json(carriers.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
|
||||
const {account_sid: query_account_sid, name, page, page_size} = req.query || {};
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : query_account_sid || null;
|
||||
let carriers = [];
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await VoipCarrier.countAll({service_provider_sid, account_sid, name});
|
||||
}
|
||||
carriers = await VoipCarrier.retrieveByCriteria({
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
name,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
|
||||
res.status(200).json(carriers);
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: carriers,
|
||||
} : carriers;
|
||||
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ const {DbErrorBadRequest, DbErrorForbidden} = require('../../utils/errors');
|
||||
//const {parseSipGatewaySid} = require('./utils');
|
||||
const decorate = require('./decorate');
|
||||
const sysError = require('../error');
|
||||
const net = require('net');
|
||||
|
||||
const hasWhitespace = (str) => /\s/.test(str);
|
||||
const checkUserScope = async(req, voip_carrier_sid) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
if (!voip_carrier_sid) {
|
||||
@@ -16,8 +18,7 @@ const checkUserScope = async(req, voip_carrier_sid) => {
|
||||
const carrier = await lookupCarrierBySid(voip_carrier_sid);
|
||||
if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid');
|
||||
|
||||
if ((!carrier.service_provider_sid || carrier.service_provider_sid === req.user.service_provider_sid) &&
|
||||
(!carrier.account_sid || carrier.account_sid === req.user.account_sid)) {
|
||||
if (!carrier.account_sid || carrier.account_sid === req.user.account_sid) {
|
||||
|
||||
if (req.method !== 'GET' && !carrier.account_sid) {
|
||||
throw new DbErrorForbidden('insufficient privileges');
|
||||
@@ -41,9 +42,15 @@ const checkUserScope = async(req, voip_carrier_sid) => {
|
||||
|
||||
const validate = async(req, sid) => {
|
||||
const {lookupSipGatewayBySid} = req.app.locals;
|
||||
const {netmask} = req.body;
|
||||
const {netmask, ipv4, inbound, outbound} = req.body;
|
||||
let voip_carrier_sid;
|
||||
|
||||
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
|
||||
&& !req.user.hasScope('admin'))) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
|
||||
|
||||
if (sid) {
|
||||
const gateway = await lookupSipGatewayBySid(sid);
|
||||
if (!gateway) throw new DbErrorBadRequest('invalid sip_gateway_sid');
|
||||
@@ -59,6 +66,15 @@ const validate = async(req, sid) => {
|
||||
throw new DbErrorBadRequest(
|
||||
`netmask required to have value equal or greater than ${process.env.JAMBONZ_MIN_GATEWAY_NETMASK}`);
|
||||
}
|
||||
if (hasWhitespace(ipv4)) {
|
||||
throw new DbErrorBadRequest('Gateway must not contain whitespace');
|
||||
}
|
||||
if (inbound && !net.isIPv4(ipv4)) {
|
||||
throw new DbErrorBadRequest('Inbound gateway must be IPv4 address');
|
||||
}
|
||||
if (!inbound && outbound && (netmask && netmask != 32)) {
|
||||
throw new DbErrorBadRequest('For outbound only gateway netmask can only be 32');
|
||||
}
|
||||
await checkUserScope(req, voip_carrier_sid);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const router = require('express').Router();
|
||||
const request = require('request');
|
||||
const getProvider = require('../../utils/sms-provider');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const sysError = require('../error');
|
||||
@@ -122,25 +121,19 @@ router.post('/:provider', async(req, res) => {
|
||||
|
||||
logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${serviceUrl}`);
|
||||
|
||||
request({
|
||||
url: serviceUrl,
|
||||
const response = await fetch(serviceUrl, {
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: payload,
|
||||
},
|
||||
async(err, response, body) => {
|
||||
if (err) {
|
||||
logger.error(err, `Error sending incomingSms POST to ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
if (200 === response.statusCode) {
|
||||
// success
|
||||
logger.info({body}, 'sending response to provider for incomingSMS');
|
||||
return doSendResponse(res, respondFn, body);
|
||||
}
|
||||
logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
body: JSON.stringify(payload),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error({response}, `Error sending incomingSms POST to ${serviceUrl}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
const body = await response.json();
|
||||
logger.info({body}, 'sending response to provider for incomingSMS');
|
||||
return doSendResponse(res, respondFn, body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ const {decryptCredential, testWhisper, testDeepgramTTS,
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt,
|
||||
testOpenAiStt} = require('../../utils/speech-utils');
|
||||
testOpenAiStt,
|
||||
testInworld,
|
||||
testResembleTTS,
|
||||
testHoundifyStt,
|
||||
testGladiaStt} = require('../../utils/speech-utils');
|
||||
const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors');
|
||||
const {
|
||||
testGoogleTts,
|
||||
@@ -122,6 +126,7 @@ const encryptCredential = (obj) => {
|
||||
role_arn,
|
||||
region,
|
||||
client_id,
|
||||
client_key,
|
||||
client_secret,
|
||||
secret,
|
||||
nuance_tts_uri,
|
||||
@@ -130,10 +135,15 @@ const encryptCredential = (obj) => {
|
||||
deepgram_stt_uri,
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
playht_tts_uri,
|
||||
resemble_tts_uri,
|
||||
resemble_tts_use_tls,
|
||||
use_custom_tts,
|
||||
custom_tts_endpoint,
|
||||
custom_tts_endpoint_url,
|
||||
use_custom_stt,
|
||||
use_for_stt,
|
||||
use_for_tts,
|
||||
custom_stt_endpoint,
|
||||
custom_stt_endpoint_url,
|
||||
tts_api_key,
|
||||
@@ -147,24 +157,39 @@ const encryptCredential = (obj) => {
|
||||
custom_tts_streaming_url,
|
||||
auth_token = '',
|
||||
cobalt_server_uri,
|
||||
// For most vendors, model_id is being used for both TTS and STT, or one of them.
|
||||
// for Cartesia, model_id is used for TTS only. introduce stt_model_id for STT
|
||||
model_id,
|
||||
stt_model_id,
|
||||
user_id,
|
||||
voice_engine,
|
||||
engine_version,
|
||||
service_version,
|
||||
api_uri,
|
||||
houndify_server_uri,
|
||||
options
|
||||
} = obj;
|
||||
|
||||
switch (vendor) {
|
||||
case 'google':
|
||||
assert(service_key, 'invalid json key: service_key is required');
|
||||
let modified_service_key = service_key;
|
||||
try {
|
||||
const o = JSON.parse(service_key);
|
||||
// support google gemini tts
|
||||
if (model_id) {
|
||||
o.model_id = model_id;
|
||||
} else {
|
||||
delete o.model_id;
|
||||
}
|
||||
|
||||
assert(o.client_email && o.private_key, 'invalid google service account key');
|
||||
modified_service_key = JSON.stringify(o);
|
||||
}
|
||||
catch (err) {
|
||||
assert(false, 'invalid google service account key - not JSON');
|
||||
}
|
||||
return encrypt(service_key);
|
||||
return encrypt(modified_service_key);
|
||||
|
||||
case 'aws':
|
||||
// AWS polly can work for 3 types of credentials:
|
||||
@@ -214,9 +239,28 @@ const encryptCredential = (obj) => {
|
||||
if (!deepgram_stt_uri || !deepgram_tts_uri) {
|
||||
assert(api_key, 'invalid deepgram speech credential: api_key is required');
|
||||
}
|
||||
const deepgramData = JSON.stringify({api_key, deepgram_stt_uri, deepgram_stt_use_tls, deepgram_tts_uri});
|
||||
const deepgramData = JSON.stringify({api_key, deepgram_stt_uri,
|
||||
deepgram_stt_use_tls, deepgram_tts_uri, model_id});
|
||||
return encrypt(deepgramData);
|
||||
|
||||
case 'gladia':
|
||||
const gladiaData = JSON.stringify({api_key, region});
|
||||
return encrypt(gladiaData);
|
||||
|
||||
case 'resemble':
|
||||
assert(api_key, 'invalid resemble speech credential: api_key is required');
|
||||
const resembleData = JSON.stringify({
|
||||
api_key,
|
||||
...(resemble_tts_uri && {resemble_tts_uri}),
|
||||
...(resemble_tts_use_tls && {resemble_tts_use_tls})
|
||||
});
|
||||
return encrypt(resembleData);
|
||||
|
||||
case 'deepgramflux':
|
||||
assert(api_key, 'invalid deepgram flux speech credential: api_key is required');
|
||||
const deepgramfluxData = JSON.stringify({api_key});
|
||||
return encrypt(deepgramfluxData);
|
||||
|
||||
case 'ibm':
|
||||
const ibmData = JSON.stringify({tts_api_key, tts_region, stt_api_key, stt_region, instance_id});
|
||||
return encrypt(ibmData);
|
||||
@@ -239,7 +283,11 @@ 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, options});
|
||||
const elevenlabsData = JSON.stringify({
|
||||
api_key,
|
||||
model_id,
|
||||
...(api_uri && {api_uri}),
|
||||
options});
|
||||
return encrypt(elevenlabsData);
|
||||
|
||||
case 'speechmatics':
|
||||
@@ -252,13 +300,22 @@ const encryptCredential = (obj) => {
|
||||
assert(api_key, 'invalid playht speech credential: api_key is required');
|
||||
assert(user_id, 'invalid playht speech credential: user_id is required');
|
||||
assert(voice_engine, 'invalid voice_engine speech credential: voice_engine is required');
|
||||
const playhtData = JSON.stringify({api_key, user_id, voice_engine, options});
|
||||
const playhtData = JSON.stringify({api_key, user_id, voice_engine, playht_tts_uri, options});
|
||||
return encrypt(playhtData);
|
||||
|
||||
case 'cartesia':
|
||||
assert(api_key, 'invalid cartesia speech credential: api_key is required');
|
||||
assert(model_id, 'invalid cartesia speech credential: model_id is required');
|
||||
const cartesiaData = JSON.stringify({api_key, model_id, options});
|
||||
if (use_for_tts) {
|
||||
assert(model_id, 'invalid cartesia speech credential: model_id is required');
|
||||
}
|
||||
if (use_for_stt) {
|
||||
assert(stt_model_id, 'invalid cartesia speech credential: stt_model_id is required');
|
||||
}
|
||||
const cartesiaData = JSON.stringify({
|
||||
api_key,
|
||||
...(model_id && {model_id}),
|
||||
...(stt_model_id && {stt_model_id}),
|
||||
options});
|
||||
return encrypt(cartesiaData);
|
||||
|
||||
case 'rimelabs':
|
||||
@@ -267,11 +324,24 @@ const encryptCredential = (obj) => {
|
||||
const rimelabsData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(rimelabsData);
|
||||
|
||||
case 'inworld':
|
||||
assert(api_key, 'invalid inworld speech credential: api_key is required');
|
||||
assert(model_id, 'invalid inworld speech credential: model_id is required');
|
||||
const inworldData = JSON.stringify({api_key, model_id, options});
|
||||
return encrypt(inworldData);
|
||||
|
||||
case 'assemblyai':
|
||||
assert(api_key, 'invalid assemblyai speech credential: api_key is required');
|
||||
const assemblyaiData = JSON.stringify({api_key});
|
||||
const assemblyaiData = JSON.stringify({api_key, service_version});
|
||||
return encrypt(assemblyaiData);
|
||||
|
||||
case 'houndify':
|
||||
assert(client_id, 'invalid houndify speech credential: client_id is required');
|
||||
assert(client_key, 'invalid houndify speech credential: client_key is required');
|
||||
assert(user_id, 'invalid houndify speech credential: user_id is required');
|
||||
const houndifyData = JSON.stringify({client_id, client_key, user_id, houndify_server_uri});
|
||||
return encrypt(houndifyData);
|
||||
|
||||
case 'voxist':
|
||||
assert(api_key, 'invalid voxist speech credential: api_key is required');
|
||||
const voxistData = JSON.stringify({api_key});
|
||||
@@ -485,12 +555,20 @@ router.put('/:sid', async(req, res) => {
|
||||
custom_tts_streaming_url,
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
stt_model_id,
|
||||
voice_engine,
|
||||
options,
|
||||
deepgram_stt_uri,
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
engine_version
|
||||
playht_tts_uri,
|
||||
engine_version,
|
||||
service_version,
|
||||
speechmatics_stt_uri,
|
||||
resemble_tts_use_tls,
|
||||
resemble_tts_uri,
|
||||
api_uri,
|
||||
houndify_server_uri
|
||||
} = req.body;
|
||||
|
||||
const newCred = {
|
||||
@@ -513,13 +591,22 @@ router.put('/:sid', async(req, res) => {
|
||||
custom_tts_url,
|
||||
custom_tts_streaming_url,
|
||||
cobalt_server_uri,
|
||||
model_id,
|
||||
model_id: model_id !== undefined ? model_id : o.model_id,
|
||||
stt_model_id,
|
||||
voice_engine,
|
||||
options,
|
||||
deepgram_stt_uri,
|
||||
deepgram_stt_use_tls,
|
||||
deepgram_tts_uri,
|
||||
engine_version
|
||||
playht_tts_uri,
|
||||
engine_version,
|
||||
service_version,
|
||||
speechmatics_stt_uri,
|
||||
resemble_tts_uri,
|
||||
resemble_tts_use_tls,
|
||||
api_uri,
|
||||
houndify_server_uri,
|
||||
...(vendor === 'google' && {service_key: JSON.stringify(o)})
|
||||
};
|
||||
logger.info({o, newCred}, 'updating speech credential with this new credential');
|
||||
obj.credential = encryptCredential(newCred);
|
||||
@@ -719,6 +806,17 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'resemble') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testResembleTTS(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'deepgram') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
@@ -742,6 +840,31 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'deepgramflux') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt && api_key) {
|
||||
try {
|
||||
await testDeepgramStt(logger, {api_key});
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'gladia') {
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testGladiaStt(logger, credential);
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cred.vendor === 'ibm') {
|
||||
const {getTtsVoices} = req.app.locals;
|
||||
|
||||
@@ -788,10 +911,10 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'elevenlabs') {
|
||||
const {api_key, model_id} = credential;
|
||||
const {api_key, model_id, api_uri} = credential;
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testElevenlabs(logger, {api_key, model_id});
|
||||
await testElevenlabs(logger, {api_key, model_id, api_uri});
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
@@ -819,7 +942,6 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
// if error is from bent, let get the body
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
@@ -828,18 +950,39 @@ router.get('/:sid/test', async(req, res) => {
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'cartesia') {
|
||||
if (cred.use_for_tts) {
|
||||
if (cred.use_for_tts || cred.use_for_stt) {
|
||||
try {
|
||||
// Cartesia does not have API for testing STT, same key is used for both TTS and STT
|
||||
await testCartesia(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
if (cred.use_for_tts) {
|
||||
results.tts.status = 'ok';
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
results.stt.status = 'ok';
|
||||
}
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
// if error is from bent, let get the body
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
results.tts = {status: 'fail', reason};
|
||||
if (cred.use_for_tts) {
|
||||
results.tts = {status: 'fail', reason};
|
||||
}
|
||||
if (cred.use_for_stt) {
|
||||
results.stt = {status: 'fail', reason};
|
||||
}
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'inworld') {
|
||||
if (cred.use_for_tts) {
|
||||
try {
|
||||
await testInworld(logger, synthAudio, credential);
|
||||
results.tts.status = 'ok';
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.tts = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.ttsTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
@@ -866,6 +1009,17 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'houndify') {
|
||||
if (cred.use_for_stt) {
|
||||
try {
|
||||
await testHoundifyStt(logger, credential);
|
||||
results.stt.status = 'ok';
|
||||
SpeechCredential.sttTestResult(sid, true);
|
||||
} catch (err) {
|
||||
results.stt = {status: 'fail', reason: err.message};
|
||||
SpeechCredential.sttTestResult(sid, false);
|
||||
}
|
||||
}
|
||||
} else if (cred.vendor === 'voxist') {
|
||||
const {api_key} = credential;
|
||||
if (cred.use_for_stt) {
|
||||
|
||||
@@ -17,6 +17,7 @@ const {
|
||||
} = require('../../utils/stripe-utils');
|
||||
const {setupFreeTrial} = require('./utils');
|
||||
const sysError = require('../error');
|
||||
const Product = require('../../models/product');
|
||||
const actions = [
|
||||
'upgrade-to-paid',
|
||||
'downgrade-to-free',
|
||||
@@ -24,6 +25,8 @@ const actions = [
|
||||
'update-quantities'
|
||||
];
|
||||
|
||||
const MIN_VOICE_CALL_SESSION_QUANTITY = 5;
|
||||
|
||||
const handleError = async(logger, method, res, err) => {
|
||||
if ('StatusError' === err.name) {
|
||||
const text = await err.text();
|
||||
@@ -146,6 +149,22 @@ const upgradeToPaidPlan = async(req, res) => {
|
||||
|
||||
await handleSubscriptionOutcome(req, res, subscription);
|
||||
};
|
||||
|
||||
const validateProductQuantities = async(products) => {
|
||||
const availableProducts = await Product.retrieveAll();
|
||||
const voiceCallSessionsProductSid =
|
||||
availableProducts.find((p) => p.category === 'voice_call_session')?.product_sid;
|
||||
if (voiceCallSessionsProductSid) {
|
||||
const invalid = products.find((p) => {
|
||||
return (p.product_sid === voiceCallSessionsProductSid &&
|
||||
(typeof p.quantity !== 'number' || p.quantity < MIN_VOICE_CALL_SESSION_QUANTITY));
|
||||
});
|
||||
if (invalid) {
|
||||
throw new DbErrorBadRequest('invalid voice call session value, minimum is ' +
|
||||
MIN_VOICE_CALL_SESSION_QUANTITY);
|
||||
}
|
||||
}
|
||||
};
|
||||
const downgradeToFreePlan = async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid} = req.user;
|
||||
@@ -229,7 +248,7 @@ const updateQuantities = async(req, res) => {
|
||||
const obj = {
|
||||
quantity: product.quantity,
|
||||
};
|
||||
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price_id: product.price_id});
|
||||
return Object.assign(obj, existingItem ? {id: existingItem.id} : {price: product.price_id});
|
||||
});
|
||||
|
||||
if (dry_run) {
|
||||
@@ -291,11 +310,11 @@ router.post('/', async(req, res) => {
|
||||
if ('update-payment-method' === action && typeof payment_method_id !== 'string') {
|
||||
throw new DbErrorBadRequest('missing payment_method_id');
|
||||
}
|
||||
if ('upgrade-to-paid' === action && (!Array.isArray(products) || 0 === products.length)) {
|
||||
throw new DbErrorBadRequest('missing products');
|
||||
}
|
||||
if ('update-quantities' === action && (!Array.isArray(products) || 0 === products.length)) {
|
||||
throw new DbErrorBadRequest('missing products');
|
||||
if (['update-quantities', 'upgrade-to-paid'].includes(action)) {
|
||||
if ((!Array.isArray(products) || 0 === products.length)) {
|
||||
throw new DbErrorBadRequest('missing products');
|
||||
}
|
||||
await validateProductQuantities(products);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const User = require('../../models/user');
|
||||
const UserPermissions = require('../../models/user-permissions');
|
||||
const Permissions = require('../../models/permissions');
|
||||
const {DbErrorBadRequest, BadRequestError, DbErrorForbidden} = require('../../utils/errors');
|
||||
const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils');
|
||||
const {promisePool} = require('../../db');
|
||||
@@ -38,7 +40,8 @@ const validateRequest = async(user_sid, req) => {
|
||||
email,
|
||||
email_activation_code,
|
||||
force_change,
|
||||
is_active
|
||||
is_active,
|
||||
is_view_only
|
||||
} = payload;
|
||||
|
||||
const [r] = await promisePool.query(retrieveSql, user_sid);
|
||||
@@ -93,7 +96,8 @@ const validateRequest = async(user_sid, req) => {
|
||||
if (email_activation_code && !email) {
|
||||
throw new DbErrorBadRequest('email and email_activation_code both required');
|
||||
}
|
||||
if (!name && !new_password && !email && !initial_password && !force_change && !is_active)
|
||||
if (!name && !new_password && !email && !initial_password && !force_change && !is_active &&
|
||||
is_view_only === undefined)
|
||||
throw new DbErrorBadRequest('no updates requested');
|
||||
|
||||
return user;
|
||||
@@ -140,7 +144,35 @@ const ensureUserRetrievalIsAllowed = (req, user) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
async function updateViewOnlyUserPermission(is_view_only, user_sid) {
|
||||
try {
|
||||
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||
if (!viewOnlyPermission) {
|
||||
throw new Error('VIEW_ONLY permission not found');
|
||||
}
|
||||
|
||||
const existingPermissions = await UserPermissions.retrieveByUserIdPermissionSid(
|
||||
user_sid,
|
||||
viewOnlyPermission.permission_sid
|
||||
);
|
||||
if (is_view_only && existingPermissions.length === 0) {
|
||||
await UserPermissions.make({
|
||||
user_sid,
|
||||
permission_sid: viewOnlyPermission.permission_sid,
|
||||
});
|
||||
} else if (!is_view_only && existingPermissions.length > 0) {
|
||||
await UserPermissions.remove(existingPermissions[0].user_permissions_sid);
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to update user permissions: ${err.message}`);
|
||||
}
|
||||
}
|
||||
async function removeViewOnlyUserPermission(user_id) {
|
||||
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||
if (viewOnlyPermission) {
|
||||
await UserPermissions.remove(user_id, viewOnlyPermission.permission_sid);
|
||||
}
|
||||
}
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
|
||||
@@ -293,6 +325,7 @@ router.get('/me', async(req, res) => {
|
||||
res.json(payload);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
logger.info({err, payload}, 'payload');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -308,7 +341,14 @@ router.get('/:user_sid', async(req, res) => {
|
||||
}
|
||||
|
||||
ensureUserRetrievalIsAllowed(req, user);
|
||||
|
||||
const [viewOnlyPermission] = await Permissions.retrieveByName('VIEW_ONLY');
|
||||
const existingPermissions = await UserPermissions.retrieveByUserId(
|
||||
user_sid
|
||||
);
|
||||
logger.debug(`existingPermissions of ${user_sid}: ${JSON.stringify(existingPermissions)}`);
|
||||
user.is_view_only = existingPermissions.length === 1 &&
|
||||
existingPermissions[0].permission_sid === viewOnlyPermission.permission_sid;
|
||||
logger.debug(`User ${user_sid} is view-only user: ${user.is_view_only}`);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { hashed_password, ...rest } = user;
|
||||
return res.status(200).json(rest);
|
||||
@@ -332,9 +372,9 @@ router.put('/:user_sid', async(req, res) => {
|
||||
is_active,
|
||||
force_change,
|
||||
account_sid,
|
||||
service_provider_sid
|
||||
service_provider_sid,
|
||||
is_view_only
|
||||
} = req.body;
|
||||
|
||||
//if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403);
|
||||
|
||||
if (!hasAdminAuth &&
|
||||
@@ -427,6 +467,8 @@ router.put('/:user_sid', async(req, res) => {
|
||||
//TODO: send email with activation code
|
||||
}
|
||||
}
|
||||
// update user permissions
|
||||
await updateViewOnlyUserPermission(is_view_only, user_sid);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
@@ -443,6 +485,8 @@ router.post('/', async(req, res) => {
|
||||
};
|
||||
const allUsers = await User.retrieveAll();
|
||||
delete payload.initial_password;
|
||||
const is_view_only = payload.is_view_only;
|
||||
delete payload.is_view_only;
|
||||
|
||||
try {
|
||||
if (req.body.initial_password) {
|
||||
@@ -464,6 +508,7 @@ router.post('/', async(req, res) => {
|
||||
if (req.user.hasAdminAuth) {
|
||||
logger.debug({payload}, 'POST /users');
|
||||
const uuid = await User.make(payload);
|
||||
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
else if (req.user.hasAccountAuth) {
|
||||
@@ -472,6 +517,7 @@ router.post('/', async(req, res) => {
|
||||
...payload,
|
||||
account_sid: req.user.account_sid,
|
||||
});
|
||||
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
else if (req.user.hasServiceProviderAuth) {
|
||||
@@ -480,6 +526,7 @@ router.post('/', async(req, res) => {
|
||||
...payload,
|
||||
service_provider_sid: req.user.service_provider_sid,
|
||||
});
|
||||
await updateViewOnlyUserPermission(is_view_only, uuid);
|
||||
res.status(201).json({user_sid: uuid});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -497,6 +544,8 @@ router.delete('/:user_sid', async(req, res) => {
|
||||
const user = allUsers.filter((user) => user.user_sid === user_sid);
|
||||
|
||||
ensureUserDeletionIsAllowed(req, activeAdminUsers, user);
|
||||
logger.debug(`Removing view-only permission for user ${user_sid}`);
|
||||
await removeViewOnlyUserPermission(user_sid);
|
||||
await User.remove(user_sid);
|
||||
|
||||
/* invalidate the jwt of the deleted user */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { v4: uuid, validate } = require('uuid');
|
||||
const bent = require('bent');
|
||||
const URL = require('url').URL;
|
||||
const isValidHostname = require('is-valid-hostname');
|
||||
const Account = require('../../models/account');
|
||||
const {promisePool} = require('../../db');
|
||||
const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils');
|
||||
@@ -11,8 +12,6 @@ values (?, ?)`;
|
||||
const replaceOldSubscriptionSql = `UPDATE account_subscriptions
|
||||
SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ?
|
||||
WHERE account_subscription_sid = ?`;
|
||||
//const request = require('request');
|
||||
//require('request-debug')(request);
|
||||
|
||||
const setupFreeTrial = async(logger, account_sid, isReturningUser) => {
|
||||
const sid = uuid();
|
||||
@@ -287,7 +286,11 @@ const hasAccountPermissions = async(req, res, next) => {
|
||||
message: 'insufficient privileges'
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
// return 400 on errors
|
||||
res.status(400).json({
|
||||
status: 'fail',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -370,35 +373,44 @@ const checkLimits = async(req, res, next) => {
|
||||
};
|
||||
|
||||
const getSubspaceJWT = async(id, secret) => {
|
||||
const postJwt = bent('https://id.subspace.com', 'POST', 'json', 200);
|
||||
const jwt = await postJwt('/oauth/token',
|
||||
{
|
||||
const response = await fetch('https://id.subspace.com/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: id,
|
||||
client_secret: secret,
|
||||
audience: 'https://api.subspace.com/',
|
||||
grant_type: 'client_credentials',
|
||||
}
|
||||
);
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get JWT: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const jwt = await response.json();
|
||||
return jwt.access_token;
|
||||
};
|
||||
|
||||
const enableSubspace = async(opts) => {
|
||||
const {subspace_client_id, subspace_client_secret, destination} = opts;
|
||||
const accessToken = await getSubspaceJWT(subspace_client_id, subspace_client_secret);
|
||||
const postTeleport = bent('https://api.subspace.com', 'POST', 'json', 200);
|
||||
|
||||
const teleport = await postTeleport('/v1/sipteleport',
|
||||
{
|
||||
const response = await fetch('https://api.subspace.com/v1/sipteleport', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'Jambonz',
|
||||
destination,
|
||||
status: 'ENABLED'
|
||||
},
|
||||
{
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
);
|
||||
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to enable teleport: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const teleport = await response.json();
|
||||
return teleport;
|
||||
};
|
||||
|
||||
@@ -406,13 +418,15 @@ const disableSubspace = async(opts) => {
|
||||
const {subspace_client_id, subspace_client_secret, subspace_sip_teleport_id} = opts;
|
||||
const accessToken = await getSubspaceJWT(subspace_client_id, subspace_client_secret);
|
||||
const relativeUrl = `/v1/sipteleport/${subspace_sip_teleport_id}`;
|
||||
const deleteTeleport = bent('https://api.subspace.com', 'DELETE', 'json', 200);
|
||||
await deleteTeleport(relativeUrl, {},
|
||||
{
|
||||
const response = await fetch(`https://api.subspace.com${relativeUrl}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
);
|
||||
return;
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete teleport: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
const validatePasswordSettings = async(password) => {
|
||||
@@ -456,6 +470,28 @@ function hasValue(data) {
|
||||
}
|
||||
}
|
||||
|
||||
const isInvalidUrl = async(s) => {
|
||||
const protocols = ['https:', 'http:', 'ws:', 'wss:'];
|
||||
try {
|
||||
const url = new URL(s);
|
||||
if (s.length != s.trim().length) {
|
||||
return 'URL contains leading/trailing whitespace';
|
||||
}
|
||||
else if (!isValidHostname(url.hostname)) {
|
||||
return `URL has invalid hostname ${url.hostname}`;
|
||||
}
|
||||
else if (!protocols.includes(url.protocol)) {
|
||||
return `URL has missing or invalid protocol ${url.protocol}`;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
return 'URL is invalid';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
setupFreeTrial,
|
||||
createTestCdrs,
|
||||
@@ -478,4 +514,5 @@ module.exports = {
|
||||
disableSubspace,
|
||||
validatePasswordSettings,
|
||||
hasValue,
|
||||
isInvalidUrl
|
||||
};
|
||||
|
||||
@@ -9,6 +9,11 @@ const { parseVoipCarrierSid } = require('./utils');
|
||||
const validate = async(req) => {
|
||||
const {lookupAppBySid, lookupAccountBySid} = req.app.locals;
|
||||
|
||||
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
|
||||
&& !req.user.hasScope('admin'))) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
|
||||
/* account level user can only act on carriers associated to his/her account */
|
||||
if (req.user.hasAccountAuth) {
|
||||
req.body.account_sid = req.user.account_sid;
|
||||
@@ -45,6 +50,12 @@ const validateUpdate = async(req, sid) => {
|
||||
|
||||
const validateDelete = async(req, sid) => {
|
||||
const {lookupCarrierBySid} = req.app.locals;
|
||||
if (process.env.JAMBONES_ADMIN_CARRIER == 1 && (!req.user.hasScope('service_provider')
|
||||
&& !req.user.hasScope('admin'))) {
|
||||
throw new DbErrorBadRequest('insufficient privileges');
|
||||
}
|
||||
|
||||
|
||||
if (req.user.hasAccountAuth) {
|
||||
/* can only update carriers for the user's account */
|
||||
const carrier = await lookupCarrierBySid(sid);
|
||||
@@ -73,16 +84,36 @@ decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions);
|
||||
/* list */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {account_sid: query_account_sid, name, page, page_size} = req.query || {};
|
||||
const isPaginationRequest = page !== null && page !== undefined;
|
||||
let service_provider_sid = null, account_sid = query_account_sid;
|
||||
if (req.user.hasAccountAuth) {
|
||||
account_sid = req.user.account_sid;
|
||||
} else if (req.user.hasServiceProviderAuth) {
|
||||
service_provider_sid = req.user.service_provider_sid;
|
||||
}
|
||||
try {
|
||||
const results = req.user.hasAdminAuth ?
|
||||
await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null) :
|
||||
await VoipCarrier.retrieveAllForSP(req.user.service_provider_sid);
|
||||
|
||||
if (req.user.hasScope('account')) {
|
||||
return res.status(200).json(results.filter((c) => c.account_sid === req.user.account_sid || !c.account_sid));
|
||||
let total = 0;
|
||||
if (isPaginationRequest) {
|
||||
total = await VoipCarrier.countAll({service_provider_sid, account_sid, name});
|
||||
}
|
||||
|
||||
res.status(200).json(results);
|
||||
const carriers = await VoipCarrier.retrieveByCriteria({
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
name,
|
||||
page,
|
||||
page_size,
|
||||
});
|
||||
|
||||
const body = isPaginationRequest ? {
|
||||
total,
|
||||
page: Number(page),
|
||||
page_size: Number(page_size),
|
||||
data: carriers,
|
||||
} : carriers;
|
||||
|
||||
res.status(200).json(body);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ const handleInvoicePaymentSucceeded = async(logger, obj) => {
|
||||
const sub = await retrieveSubscription(logger, subscription);
|
||||
if ('active' === sub.status) {
|
||||
const {account_sid} = sub.metadata;
|
||||
if (!account_sid) {
|
||||
logger.info({subscription}, `handleInvoicePaymentSucceeded: received subscription ${sub.id} without account_sid`);
|
||||
return;
|
||||
}
|
||||
if (await Account.activateSubscription(logger, account_sid, sub.id,
|
||||
'subscription_create' === obj.billing_reason ? 'upgrade to paid plan' : 'change plan details')) {
|
||||
logger.info(`handleInvoicePaymentSucceeded: activated subscription for account ${account_sid}`);
|
||||
@@ -35,6 +39,10 @@ const handleInvoicePaymentFailed = async(logger, obj) => {
|
||||
const sub = await retrieveSubscription(logger, subscription);
|
||||
logger.debug({obj}, `payment for ${obj.billing_reason} failed, subscription status is ${sub.status}`);
|
||||
const {account_sid} = sub.metadata;
|
||||
if (!account_sid) {
|
||||
logger.info({subscription}, `handleInvoicePaymentFailed: received subscription ${sub.id} without account_sid`);
|
||||
return;
|
||||
}
|
||||
if (await Account.deactivateSubscription(logger, account_sid, 'payment failed')) {
|
||||
logger.info(`handleInvoicePaymentFailed: deactivated subscription for account ${account_sid}`);
|
||||
}
|
||||
|
||||
50
lib/utils/appenv_schemaSchema.json
Normal file
50
lib/utils/appenv_schemaSchema.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9_]+$": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
]
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"default": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"obscure": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"description"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
78
lib/utils/appenv_utils.js
Normal file
78
lib/utils/appenv_utils.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const Ajv = require('ajv');
|
||||
const assert = require('assert');
|
||||
|
||||
const ajv = new Ajv();
|
||||
const schemaSchema = require('./appenv_schemaSchema.json');
|
||||
|
||||
|
||||
const validateAppEnvSchema = (schema) => {
|
||||
const validate = ajv.compile(schemaSchema);
|
||||
return validate(schema);
|
||||
};
|
||||
|
||||
//Currently this request is not signed with the webhook secret as it is outside an account
|
||||
const fetchAppEnvSchema = async(logger, url) => {
|
||||
// Translate WebSocket URLs to HTTP equivalents (case-insensitive)
|
||||
let fetchUrl = url;
|
||||
if (url.toLowerCase().startsWith('ws://')) {
|
||||
fetchUrl = 'http://' + url.substring(5);
|
||||
} else if (url.toLowerCase().startsWith('wss://')) {
|
||||
fetchUrl = 'https://' + url.substring(6);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(fetchUrl, {
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.info(`Failure to fetch app env schema ${response.status} ${response.statusText}`);
|
||||
return false;
|
||||
}
|
||||
const schema = await response.json();
|
||||
return schema;
|
||||
}
|
||||
catch (e) {
|
||||
logger.info(`Failure to fetch app env schema ${e}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const validateAppEnvData = async(schema, data) => {
|
||||
const schemaKeys = Object.keys(schema);
|
||||
const dataKeys = Object.keys(data);
|
||||
let errorMsg = false;
|
||||
// Check for required keys
|
||||
schemaKeys.forEach((k) => {
|
||||
if (schema[k].required) {
|
||||
if (!dataKeys.includes(k)) {
|
||||
errorMsg = `Missing required value env_vars.${k}`;
|
||||
console.log(errorMsg);
|
||||
}
|
||||
}
|
||||
});
|
||||
//Validate the values
|
||||
dataKeys.forEach((k) => {
|
||||
if (schemaKeys.includes(k)) {
|
||||
try {
|
||||
// Check value is correct type
|
||||
assert(typeof data[k] == schema[k].type);
|
||||
// if enum check value is valid
|
||||
if (schema[k].enum) {
|
||||
assert(schema[k].enum.includes(data[k]));
|
||||
}
|
||||
} catch (error) {
|
||||
errorMsg = `Invalid value/type for env_vars.${k}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
return errorMsg;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateAppEnvSchema,
|
||||
fetchAppEnvSchema,
|
||||
validateAppEnvData
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
if (!process.env.JAMBONES_HOSTING) return;
|
||||
|
||||
const bent = require('bent');
|
||||
const crypto = require('crypto');
|
||||
const assert = require('assert');
|
||||
const domains = new Map();
|
||||
@@ -26,17 +25,20 @@ const createAuthHeaders = () => {
|
||||
const getDnsDomainId = async(logger, name) => {
|
||||
checkAsserts();
|
||||
const headers = createAuthHeaders();
|
||||
const get = bent(process.env.DME_BASE_URL, 'GET', 'json', headers);
|
||||
try {
|
||||
const result = await get('/dns/managed');
|
||||
debug(result, 'getDnsDomainId: all domains');
|
||||
if (Array.isArray(result.data)) {
|
||||
const domain = result.data.find((o) => o.name === name);
|
||||
if (domain) return domain.id;
|
||||
debug(`getDnsDomainId: failed to find domain ${name}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error retrieving domains');
|
||||
const response = await fetch(`${process.env.DME_BASE_URL}/dns/managed`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving domains');
|
||||
return;
|
||||
}
|
||||
const result = await response.json();
|
||||
debug(result, 'getDnsDomainId: all domains');
|
||||
if (Array.isArray(result.data)) {
|
||||
const domain = result.data.find((o) => o.name === name);
|
||||
if (domain) return domain.id;
|
||||
debug(`getDnsDomainId: failed to find domain ${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,21 +82,20 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
|
||||
];
|
||||
const headers = createAuthHeaders();
|
||||
const records = [...a_records, ...srv_records];
|
||||
const post = bent(process.env.DME_BASE_URL, 'POST', 201, 400, headers);
|
||||
logger.debug({records}, 'Attemting to create dns records');
|
||||
const res = await post(`/dns/managed/${domainId}/records/createMulti`,
|
||||
[...a_records, ...srv_records]);
|
||||
|
||||
if (201 === res.statusCode) {
|
||||
const str = await res.text();
|
||||
return JSON.parse(str);
|
||||
const response = await fetch(`${process.env.DME_BASE_URL}/dns/managed/${domainId}/records/createMulti`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(records)
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error creating records');
|
||||
return;
|
||||
}
|
||||
let body;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch (err) {
|
||||
const result = await response.json();
|
||||
logger.debug({result}, 'createDnsRecords: created records');
|
||||
if (201 === response.status) {
|
||||
return result;
|
||||
}
|
||||
logger.error({headers: res.headers, body}, `Error creating records, status ${res.statusCode}`);
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error retrieving domains');
|
||||
}
|
||||
@@ -103,7 +104,6 @@ const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
|
||||
const deleteDnsRecords = async(logger, domain, recIds) => {
|
||||
checkAsserts();
|
||||
const headers = createAuthHeaders();
|
||||
const del = bent(process.env.DME_BASE_URL, 'DELETE', 200, headers);
|
||||
try {
|
||||
if (!domains.has(domain)) {
|
||||
const domainId = await getDnsDomainId(logger, domain);
|
||||
@@ -112,7 +112,10 @@ const deleteDnsRecords = async(logger, domain, recIds) => {
|
||||
}
|
||||
const domainId = domains.get(domain);
|
||||
const url = `/dns/managed/${domainId}/records?${recIds.map((r) => `ids=${r}`).join('&')}`;
|
||||
await del(url);
|
||||
await fetch(`${process.env.DME_BASE_URL}${url}`, {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const formData = require('form-data');
|
||||
const Mailgun = require('mailgun.js');
|
||||
const mailgun = new Mailgun(formData);
|
||||
const bent = require('bent');
|
||||
const validateEmail = (email) => {
|
||||
// eslint-disable-next-line max-len
|
||||
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
@@ -19,8 +18,9 @@ const emailSimpleText = async(logger, to, subject, text) => {
|
||||
};
|
||||
|
||||
const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
|
||||
try {
|
||||
const post = bent('POST', {
|
||||
const response = await fetch(process.env.CUSTOM_EMAIL_VENDOR_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...((process.env.CUSTOM_EMAIL_VENDOR_USERNAME && process.env.CUSTOM_EMAIL_VENDOR_PASSWORD) &&
|
||||
({
|
||||
@@ -28,22 +28,22 @@ const sendEmailByCustomVendor = async(logger, from, to, subject, text) => {
|
||||
`${process.env.CUSTOM_EMAIL_VENDOR_USERNAME}:${process.env.CUSTOM_EMAIL_VENDOR_PASSWORD}`
|
||||
).toString('base64')}`
|
||||
}))
|
||||
});
|
||||
|
||||
const res = await post(process.env.CUSTOM_EMAIL_VENDOR_URL, {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text
|
||||
});
|
||||
logger.debug({
|
||||
res
|
||||
}, 'sent email to custom vendor.');
|
||||
} catch (err) {
|
||||
logger.info({
|
||||
err
|
||||
}, 'Error sending email From Custom email vendor');
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error sending email to custom vendor');
|
||||
return;
|
||||
}
|
||||
const res = await response.json();
|
||||
logger.debug({
|
||||
res
|
||||
}, 'sent email to custom vendor.');
|
||||
};
|
||||
|
||||
const sendEmailByMailgun = async(logger, from, to, subject, text) => {
|
||||
|
||||
@@ -50,18 +50,15 @@ function isObscureKey(bucketCredentials) {
|
||||
service_key = '',
|
||||
connection_string = ''
|
||||
} = bucketCredentials || {};
|
||||
let pattern;
|
||||
// Pattern matches: 4-6 any characters followed by one or more X's
|
||||
const pattern = /^.{4,6}X+$/;
|
||||
switch (vendor) {
|
||||
case 'aws_s3':
|
||||
case 's3_compatible':
|
||||
pattern = /^([A-Za-z0-9]{4,6}X+$)/;
|
||||
return pattern.test(secret_access_key);
|
||||
case 'azure':
|
||||
pattern = /^https:[A-Za-z0-9\/.:?=&_-]+$/;
|
||||
return pattern.test(connection_string);
|
||||
|
||||
case 'google': {
|
||||
pattern = /^([A-Za-z0-9]{4,6}X+$)/;
|
||||
let {private_key} = JSON.parse(service_key);
|
||||
const key_header = '-----BEGIN PRIVATE KEY-----\n';
|
||||
private_key = private_key.slice(key_header.length, private_key.length);
|
||||
|
||||
@@ -27,11 +27,17 @@ class DbErrorForbidden extends DbError {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
class UserPermissionError extends Error {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BadRequestError,
|
||||
DbError,
|
||||
DbErrorBadRequest,
|
||||
DbErrorUnprocessableRequest,
|
||||
DbErrorForbidden
|
||||
DbErrorForbidden,
|
||||
UserPermissionError
|
||||
};
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
const bent = require('bent');
|
||||
const basicAuth = (apiKey) => {
|
||||
const header = `Bearer ${apiKey}`;
|
||||
return {Authorization: header};
|
||||
};
|
||||
const postJSON = bent(process.env.HOMER_BASE_URL || 'http://127.0.0.1', 'POST', 'json', 200, 201);
|
||||
const postPcap = bent(process.env.HOMER_BASE_URL || 'http://127.0.0.1', 'POST', 200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
});
|
||||
const { Readable } = require('stream');
|
||||
const SEVEN_DAYS_IN_MS = (1000 * 3600 * 24 * 7);
|
||||
const HOMER_BASE_URL = process.env.HOMER_BASE_URL || 'http://127.0.0.1';
|
||||
|
||||
const getHomerApiKey = async(logger) => {
|
||||
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
|
||||
@@ -17,11 +13,21 @@ const getHomerApiKey = async(logger) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const obj = await postJSON('/api/v3/auth', {
|
||||
username: process.env.HOMER_USERNAME,
|
||||
password: process.env.HOMER_PASSWORD
|
||||
const response = await fetch(`${HOMER_BASE_URL}/api/v3/auth`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: process.env.HOMER_USERNAME,
|
||||
password: process.env.HOMER_PASSWORD
|
||||
})
|
||||
});
|
||||
debug(obj);
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving apikey');
|
||||
return;
|
||||
}
|
||||
const obj = await response.json();
|
||||
logger.debug({obj}, `getHomerApiKey for user ${process.env.HOMER_USERNAME}`);
|
||||
return obj.token;
|
||||
} catch (err) {
|
||||
@@ -36,28 +42,40 @@ const getHomerSipTrace = async(logger, apiKey, callId) => {
|
||||
}
|
||||
try {
|
||||
const now = Date.now();
|
||||
const obj = await postJSON('/api/v3/call/transaction', {
|
||||
param: {
|
||||
transaction: {
|
||||
call: true,
|
||||
registration: true,
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: [callId]
|
||||
},
|
||||
'1_registration': {
|
||||
callid: [callId]
|
||||
}
|
||||
},
|
||||
const response = await fetch(`${HOMER_BASE_URL}/api/v3/call/transaction`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...basicAuth(apiKey)
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
}, basicAuth(apiKey));
|
||||
body: JSON.stringify({
|
||||
param: {
|
||||
transaction: {
|
||||
call: true,
|
||||
registration: true,
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: [callId]
|
||||
},
|
||||
'1_registration': {
|
||||
callid: [callId]
|
||||
}
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving messages');
|
||||
return;
|
||||
}
|
||||
const obj = await response.json();
|
||||
return obj;
|
||||
} catch (err) {
|
||||
logger.info({err}, `getHomerSipTrace: Error retrieving messages for callid ${callId}`);
|
||||
@@ -70,34 +88,45 @@ const getHomerPcap = async(logger, apiKey, callIds, method) => {
|
||||
}
|
||||
try {
|
||||
const now = Date.now();
|
||||
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
|
||||
param: {
|
||||
transaction: {
|
||||
call: method === 'invite',
|
||||
registration: method === 'register',
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
...(method === 'invite' && {
|
||||
'1_call': {
|
||||
callid: callIds
|
||||
}
|
||||
})
|
||||
,
|
||||
...(method === 'register' && {
|
||||
'1_registration': {
|
||||
callid: callIds
|
||||
}
|
||||
})
|
||||
},
|
||||
const response = await fetch(`${HOMER_BASE_URL}/api/v3/export/call/messages/pcap`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...basicAuth(apiKey)
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
}, basicAuth(apiKey));
|
||||
return stream;
|
||||
body: JSON.stringify({
|
||||
param: {
|
||||
transaction: {
|
||||
call: method === 'invite',
|
||||
registration: method === 'register',
|
||||
rest: false
|
||||
},
|
||||
orlogic: true,
|
||||
search: {
|
||||
...(method === 'invite' && {
|
||||
'1_call': {
|
||||
callid: callIds
|
||||
}
|
||||
})
|
||||
,
|
||||
...(method === 'register' && {
|
||||
'1_registration': {
|
||||
callid: callIds
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving messages');
|
||||
return;
|
||||
}
|
||||
return Readable.fromWeb(response.body);
|
||||
} catch (err) {
|
||||
logger.info({err}, `getHomerPcap: Error retrieving messages for callid ${callIds}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const bent = require('bent');
|
||||
const getJSON = bent(process.env.JAEGER_BASE_URL || 'http://127.0.0.1', 'GET', 'json', 200);
|
||||
const JAEGER_BASE_URL = process.env.JAEGER_BASE_URL || 'http://127.0.0.1';
|
||||
|
||||
const getJaegerTrace = async(logger, traceId) => {
|
||||
if (!process.env.JAEGER_BASE_URL) {
|
||||
@@ -7,7 +6,12 @@ const getJaegerTrace = async(logger, traceId) => {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await getJSON(`/api/v3/traces/${traceId}`);
|
||||
const response = await fetch(`${JAEGER_BASE_URL}/api/v3/traces/${traceId}`);
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving spans');
|
||||
return;
|
||||
}
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
const url = `${process.env.JAEGER_BASE_URL}/api/traces/${traceId}`;
|
||||
logger.error({err, traceId}, `getJaegerTrace: Error retrieving spans from ${url}`);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
const assert = require('assert');
|
||||
const bent = require('bent');
|
||||
const postJSON = bent('POST', 'json', 200);
|
||||
const getJSON = bent('GET', 'json', 200);
|
||||
const {emailSimpleText} = require('./email-utils');
|
||||
const {DbErrorForbidden} = require('../utils/errors');
|
||||
|
||||
@@ -10,13 +7,26 @@ const doGithubAuth = async(logger, payload) => {
|
||||
|
||||
try {
|
||||
/* exchange the code for an access token */
|
||||
const obj = await postJSON('https://github.com/login/oauth/access_token', {
|
||||
client_id: payload.oauth2_client_id,
|
||||
client_secret: process.env.GITHUB_CLIENT_SECRET,
|
||||
code: payload.oauth2_code,
|
||||
state: payload.oauth2_state,
|
||||
redirect_uri: payload.oauth2_redirect_uri
|
||||
const response = await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: payload.oauth2_client_id,
|
||||
client_secret: process.env.GITHUB_CLIENT_SECRET,
|
||||
code: payload.oauth2_code,
|
||||
state: payload.oauth2_state,
|
||||
redirect_uri: payload.oauth2_redirect_uri
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving access_token from github');
|
||||
throw new DbErrorForbidden(await response.text());
|
||||
}
|
||||
|
||||
const obj = await response.json();
|
||||
if (!obj.access_token) {
|
||||
logger.error({obj}, 'Error retrieving access_token from github');
|
||||
if (obj.error === 'bad_verification_code') throw new Error('bad verification code');
|
||||
@@ -25,17 +35,31 @@ const doGithubAuth = async(logger, payload) => {
|
||||
logger.debug({obj}, 'got response from github for access_token');
|
||||
|
||||
/* use the access token to get basic public info as well as primary email */
|
||||
const userDetails = await getJSON('https://api.github.com/user', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
const userResponse = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
}
|
||||
});
|
||||
if (!userResponse.ok) {
|
||||
logger.error({userResponse}, 'Error retrieving user details from github');
|
||||
throw new DbErrorForbidden(await userResponse.text());
|
||||
}
|
||||
const userDetails = await userResponse.json();
|
||||
|
||||
const emails = await getJSON('https://api.github.com/user/emails', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
const emailsResponse = await fetch('https://api.github.com/user/emails', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
}
|
||||
});
|
||||
if (!emailsResponse.ok) {
|
||||
logger.error({emailsResponse}, 'Error retrieving emails from github');
|
||||
throw new DbErrorForbidden(await emailsResponse.text());
|
||||
}
|
||||
const emails = await emailsResponse.json();
|
||||
const primary = emails.find((e) => e.primary);
|
||||
if (primary) Object.assign(userDetails, {
|
||||
email: primary.email,
|
||||
@@ -55,14 +79,26 @@ const doGoogleAuth = async(logger, payload) => {
|
||||
|
||||
try {
|
||||
/* exchange the code for an access token */
|
||||
const obj = await postJSON('https://oauth2.googleapis.com/token', {
|
||||
client_id: payload.oauth2_client_id,
|
||||
client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET,
|
||||
code: payload.oauth2_code,
|
||||
state: payload.oauth2_state,
|
||||
redirect_uri: payload.oauth2_redirect_uri,
|
||||
grant_type: 'authorization_code'
|
||||
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: payload.oauth2_client_id,
|
||||
client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET,
|
||||
code: payload.oauth2_code,
|
||||
state: payload.oauth2_state,
|
||||
redirect_uri: payload.oauth2_redirect_uri,
|
||||
grant_type: 'authorization_code'
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving access_token from google');
|
||||
throw new DbErrorForbidden(await response.text());
|
||||
}
|
||||
const obj = await response.json();
|
||||
if (!obj.access_token) {
|
||||
logger.error({obj}, 'Error retrieving access_token from github');
|
||||
if (obj.error === 'bad_verification_code') throw new Error('bad verification code');
|
||||
@@ -71,12 +107,18 @@ const doGoogleAuth = async(logger, payload) => {
|
||||
logger.debug({obj}, 'got response from google for access_token');
|
||||
|
||||
/* use the access token to get basic public info as well as primary email */
|
||||
const userDetails = await getJSON('https://www.googleapis.com/oauth2/v2/userinfo', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
const userDetailsResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
}
|
||||
});
|
||||
|
||||
if (!userDetailsResponse.ok) {
|
||||
logger.error({userDetailsResponse}, 'Error retrieving user details from google');
|
||||
throw new DbErrorForbidden(await userDetailsResponse.text());
|
||||
}
|
||||
const userDetails = await userDetailsResponse.json();
|
||||
logger.info({userDetails}, 'retrieved user details from google');
|
||||
return userDetails;
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,10 +1,57 @@
|
||||
module.exports = [
|
||||
{ name: 'Arabic, Gulf', value: 'ar-AE' },
|
||||
{ name: 'Arabic, Modern Standard', value: 'ar-SA' },
|
||||
{ name: 'Afrikaans', value: 'af-ZA' },
|
||||
{ name: 'Basque', value: 'eu-ES' },
|
||||
{ name: 'Catalan', value: 'ca-ES' },
|
||||
{ name: 'Chinese, Simplified', value: 'zh-CN' },
|
||||
{ name: 'Chinese, Traditional', value: 'zh-TW' },
|
||||
{ name: 'Chinese (Cantonese), Hong-Kong', value: 'zh-HK' },
|
||||
{ name: 'Croatian', value: 'hr-HR' },
|
||||
{ name: 'Czech', value: 'cs-CZ' },
|
||||
{ name: 'Danish', value: 'da-DK' },
|
||||
{ name: 'Dutch', value: 'nl-NL' },
|
||||
{ name: 'Australian English', value: 'en-AU' },
|
||||
{ name: 'British English', value: 'en-GB' },
|
||||
{ name: 'US English', value: 'en-US' },
|
||||
{ name: 'Indian English', value: 'en-IN' },
|
||||
{ name: 'Irish English', value: 'en-IE' },
|
||||
{ name: 'New Zealand English', value: 'en-NZ' },
|
||||
{ name: 'Scottish English', value: 'en-AB' },
|
||||
{ name: 'South African English', value: 'en-ZA' },
|
||||
{ name: 'Welsh English', value: 'en-WL' },
|
||||
{ name: 'Farsi', value: 'fa-IR' },
|
||||
{ name: 'Finnish', value: 'fi-FI' },
|
||||
{ name: 'French', value: 'fr-FR' },
|
||||
{ name: 'Canadian French', value: 'fr-CA' },
|
||||
{ name: 'Galician', value: 'gl-ES' },
|
||||
{ name: 'German', value: 'de-DE' },
|
||||
{ name: 'Swiss German', value: 'de-CH' },
|
||||
{ name: 'Greek', value: 'el-GR' },
|
||||
{ name: 'Hindi', value: 'hi-IN' },
|
||||
{ name: 'Hebrew', value: 'he-IL' },
|
||||
{ name: 'Italian', value: 'it-IT' },
|
||||
{ name: 'Indian Hindi', value: 'hi-IN' },
|
||||
{ name: 'Indonesian', value: 'id-ID' },
|
||||
{ name: 'Japanese', value: 'ja-JP' },
|
||||
{ name: 'Korean', value: 'ko-KR' },
|
||||
{ name: 'Latvian', value: 'lv-LV' },
|
||||
{ name: 'Malay', value: 'ms-MY' },
|
||||
{ name: 'Norwegian Bokmål', value: 'no-NO' },
|
||||
{ name: 'Polish', value: 'pl-PL' },
|
||||
{ name: 'Portuguese', value: 'pt-PT' },
|
||||
{ name: 'Brazilian Portuguese', value: 'pt-BR' },
|
||||
{ name: 'Romanian', value: 'ro-RO' },
|
||||
{ name: 'Russian', value: 'ru-RU' },
|
||||
{ name: 'Serbian', value: 'sr-RS' },
|
||||
{ name: 'Slovak', value: 'sk-SK' },
|
||||
{ name: 'Somali', value: 'so-SO' },
|
||||
{ name: 'Spanish', value: 'es-ES' },
|
||||
{ name: 'US Spanish', value: 'es-US' },
|
||||
{ name: 'Swedish', value: 'sv-SE' },
|
||||
{ name: 'Tagalog/Filipino', value: 'tl-PH' },
|
||||
{ name: 'Thai', value: 'th-TH' },
|
||||
{ name: 'Ukrainian', value: 'uk-UA' },
|
||||
{ name: 'Vietnamese', value: 'vi-VN' },
|
||||
{ name: 'Zulu', value: 'zu-ZA' }
|
||||
];
|
||||
|
||||
@@ -1,56 +1,153 @@
|
||||
module.exports = [
|
||||
{ name: 'Multilingual', value: 'multi' },
|
||||
{ name: 'Afrikaans', value: 'af' },
|
||||
{ name: 'Amharic', value: 'am' },
|
||||
{ name: 'Arabic', value: 'ar' },
|
||||
{ name: 'Assamese', value: 'as' },
|
||||
{ name: 'Azerbaijani', value: 'az' },
|
||||
{ name: 'Bashkir', value: 'ba' },
|
||||
{ name: 'Belarusian', value: 'be' },
|
||||
{ name: 'Bulgarian', value: 'bg' },
|
||||
{ name: 'Bengali', value: 'bn' },
|
||||
{ name: 'Tibetan', value: 'bo' },
|
||||
{ name: 'Breton', value: 'br' },
|
||||
{ name: 'Bosnian', value: 'bs' },
|
||||
{ name: 'Catalan', value: 'ca' },
|
||||
{ name: 'Chinese (Mandarin, Simplified)', value: 'zh' },
|
||||
{ name: 'Chinese (Mandarin, Simplified - China)', value: 'zh-CN' },
|
||||
{ name: 'Chinese (Mandarin, Simplified - Hans)', value: 'zh-Hans' },
|
||||
{ name: 'Chinese (Mandarin, Traditional)', value: 'zh-TW' },
|
||||
{ name: 'Chinese (Mandarin, Traditional - Hant)', value: 'zh-Hant' },
|
||||
{ name: 'Chinese (Cantonese, Traditional - Hong Kong)', value: 'zh-HK' },
|
||||
{ name: 'Czech', value: 'cs' },
|
||||
{ name: 'Welsh', value: 'cy' },
|
||||
{ name: 'Danish', value: 'da' },
|
||||
{ name: 'Danish (Denmark)', value: 'da-DK' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'English (United States)', value: 'en-US' },
|
||||
{ name: 'English (Australia)', value: 'en-AU' },
|
||||
{ name: 'English (United Kingdom)', value: 'en-GB' },
|
||||
{ name: 'English (New Zealand)', value: 'en-NZ' },
|
||||
{ name: 'English (India)', value: 'en-IN' },
|
||||
{ name: 'Estonian', value: 'et' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'Flemish', value: 'nl-BE' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'French (Canada)', value: 'fr-CA' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'German (Austria)', value: 'de-AT' },
|
||||
{ name: 'German (Switzerland)', value: 'de-CH' },
|
||||
{ name: 'Greek', value: 'el' },
|
||||
{ name: 'German (Germany)', value: 'de-DE' },
|
||||
{ name: 'Modern Greek', value: 'el' },
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'English (Australia)', value: 'en-AU' },
|
||||
{ name: 'English (Canada)', value: 'en-CA' },
|
||||
{ name: 'English (United Kingdom)', value: 'en-GB' },
|
||||
{ name: 'English (Ireland)', value: 'en-IE' },
|
||||
{ name: 'English (India)', value: 'en-IN' },
|
||||
{ name: 'English (Malaysia)', value: 'en-MY' },
|
||||
{ name: 'English (New Zealand)', value: 'en-NZ' },
|
||||
{ name: 'English (Philippines)', value: 'en-PH' },
|
||||
{ name: 'English (United States)', value: 'en-US' },
|
||||
{ name: 'English (South Africa)', value: 'en-ZA' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Spanish (Latin America and the Caribbean)', value: 'es-419' },
|
||||
{ name: 'Spanish (Argentina)', value: 'es-AR' },
|
||||
{ name: 'Spanish (Colombia)', value: 'es-CO' },
|
||||
{ name: 'Spanish (Spain)', value: 'es-ES' },
|
||||
{ name: 'Spanish (Latin America)', value: 'es-LATAM' },
|
||||
{ name: 'Spanish (Mexico)', value: 'es-MX' },
|
||||
{ name: 'Spanish (United States)', value: 'es-US' },
|
||||
{ name: 'Estonian', value: 'et' },
|
||||
{ name: 'Basque', value: 'eu' },
|
||||
{ name: 'Persian', value: 'fa' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'Faroese', value: 'fo' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'French (Belgium)', value: 'fr-BE' },
|
||||
{ name: 'French (Canada)', value: 'fr-CA' },
|
||||
{ name: 'French (Switzerland)', value: 'fr-CH' },
|
||||
{ name: 'French (France)', value: 'fr-FR' },
|
||||
{ name: 'French (Canada)', value: 'fr-ca' },
|
||||
{ name: 'Galician', value: 'gl' },
|
||||
{ name: 'Gujarati', value: 'gu' },
|
||||
{ name: 'Hausa', value: 'ha' },
|
||||
{ name: 'Hawaiian', value: 'haw' },
|
||||
{ name: 'Hebrew', value: 'he' },
|
||||
{ name: 'Hindi', value: 'hi' },
|
||||
{ name: 'Hindi (Latin)', value: 'hi-Latn' },
|
||||
{ name: 'Croatian', value: 'hr' },
|
||||
{ name: 'Haitian', value: 'ht' },
|
||||
{ name: 'Hungarian', value: 'hu' },
|
||||
{ name: 'Armenian', value: 'hy' },
|
||||
{ name: 'Indonesian', value: 'id' },
|
||||
{ name: 'Indonesian (Indonesia)', value: 'id-ID' },
|
||||
{ name: 'Icelandic', value: 'is' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Italian (Italy)', value: 'it-IT' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Japanese (Japan)', value: 'ja-JP' },
|
||||
{ name: 'Javanese', value: 'jw' },
|
||||
{ name: 'Georgian', value: 'ka' },
|
||||
{ name: 'Kazakh', value: 'kk' },
|
||||
{ name: 'Khmer', value: 'km' },
|
||||
{ name: 'Kannada', value: 'kn' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Korean (South Korea)', value: 'ko-KR' },
|
||||
{ name: 'Latvian', value: 'lv' },
|
||||
{ name: 'Korean (Republic of Korea)', value: 'ko-KR' },
|
||||
{ name: 'Latin', value: 'la' },
|
||||
{ name: 'Luxembourgish', value: 'lb' },
|
||||
{ name: 'Lingala', value: 'ln' },
|
||||
{ name: 'Lao', value: 'lo' },
|
||||
{ name: 'Lithuanian', value: 'lt' },
|
||||
{ name: 'Latvian', value: 'lv' },
|
||||
{ name: 'Malagasy', value: 'mg' },
|
||||
{ name: 'Maori', value: 'mi' },
|
||||
{ name: 'Macedonian', value: 'mk' },
|
||||
{ name: 'Malayalam', value: 'ml' },
|
||||
{ name: 'Mongolian', value: 'mn' },
|
||||
{ name: 'Marathi', value: 'mr' },
|
||||
{ name: 'Malay', value: 'ms' },
|
||||
{ name: 'Malay (Malaysia)', value: 'ms-MY' },
|
||||
{ name: 'Malay (Singapore)', value: 'ms-SG' },
|
||||
{ name: 'Maltese', value: 'mt' },
|
||||
{ name: 'Burmese', value: 'my' },
|
||||
{ name: 'Nepali', value: 'ne' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'Dutch (Belgium)', value: 'nl-BE' },
|
||||
{ name: 'Dutch (Netherlands)', value: 'nl-NL' },
|
||||
{ name: 'Norwegian Nynorsk', value: 'nn' },
|
||||
{ name: 'Norwegian', value: 'no' },
|
||||
{ name: 'Norwegian (Norway)', value: 'no-NO' },
|
||||
{ name: 'Occitan', value: 'oc' },
|
||||
{ name: 'Panjabi', value: 'pa' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Polish (Poland)', value: 'pl-PL' },
|
||||
{ name: 'Pushto', value: 'ps' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'Portuguese (Brazil)', value: 'pt-BR' },
|
||||
{ name: 'Portuguese (Portugal)', value: 'pt-PT' },
|
||||
{ name: 'Romanian', value: 'ro' },
|
||||
{ name: 'Romanian (Moldova)', value: 'ro-MD' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Russian (Latin)', value: 'ru-Latn' },
|
||||
{ name: 'Russian (Russian Federation)', value: 'ru-RU' },
|
||||
{ name: 'Sanskrit', value: 'sa' },
|
||||
{ name: 'Sindhi', value: 'sd' },
|
||||
{ name: 'Sinhala', value: 'si' },
|
||||
{ name: 'Slovak', value: 'sk' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Spanish (Latin America)', value: 'es-419' },
|
||||
{ name: 'Slovenian', value: 'sl' },
|
||||
{ name: 'Shona', value: 'sn' },
|
||||
{ name: 'Somali', value: 'so' },
|
||||
{ name: 'Albanian', value: 'sq' },
|
||||
{ name: 'Serbian', value: 'sr' },
|
||||
{ name: 'Sundanese', value: 'su' },
|
||||
{ name: 'Swedish', value: 'sv' },
|
||||
{ name: 'Swedish (Sweden)', value: 'sv-SE' },
|
||||
{ name: 'Swahili', value: 'sw' },
|
||||
{ name: 'Tamil', value: 'ta' },
|
||||
{ name: 'Tamasheq', value: 'taq' },
|
||||
{ name: 'Telugu', value: 'te' },
|
||||
{ name: 'Tajik', value: 'tg' },
|
||||
{ name: 'Thai', value: 'th' },
|
||||
{ name: 'Thai (Thailand)', value: 'th-TH' },
|
||||
{ name: 'Turkmen', value: 'tk' },
|
||||
{ name: 'Tagalog', value: 'tl' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Turkish (Türkiye)', value: 'tr-TR' },
|
||||
{ name: 'Tatar', value: 'tt' },
|
||||
{ name: 'Ukrainian', value: 'uk' },
|
||||
{ name: 'Vietnamese', value: 'vi' }
|
||||
{ name: 'Urdu', value: 'ur' },
|
||||
{ name: 'Uzbek', value: 'uz' },
|
||||
{ name: 'Vietnamese', value: 'vi' },
|
||||
{ name: 'Yiddish', value: 'yi' },
|
||||
{ name: 'Yoruba', value: 'yo' },
|
||||
{ name: 'Chinese (Mandarin, Mainland)', value: 'zh' },
|
||||
{ name: 'Chinese (China)', value: 'zh-CN' },
|
||||
{ name: 'Chinese (Cantonese, Hong Kong)', value: 'zh-HK' },
|
||||
{ name: 'Chinese (Han (Simplified variant))', value: 'zh-Hans' },
|
||||
{ name: 'Chinese (Han (Traditional variant))', value: 'zh-Hant' },
|
||||
{ name: 'Chinese (Traditional, Taiwan)', value: 'zh-TW' }
|
||||
];
|
||||
|
||||
103
lib/utils/speech-data/stt-gladia.js
Normal file
103
lib/utils/speech-data/stt-gladia.js
Normal file
@@ -0,0 +1,103 @@
|
||||
module.exports = [
|
||||
{ name: 'Afrikaans', value: 'af' },
|
||||
{ name: 'Albanian', value: 'sq' },
|
||||
{ name: 'Amharic', value: 'am' },
|
||||
{ name: 'Arabic', value: 'ar' },
|
||||
{ name: 'Armenian', value: 'hy' },
|
||||
{ name: 'Assamese', value: 'as' },
|
||||
{ name: 'Azerbaijani', value: 'az' },
|
||||
{ name: 'Bashkir', value: 'ba' },
|
||||
{ name: 'Basque', value: 'eu' },
|
||||
{ name: 'Belarusian', value: 'be' },
|
||||
{ name: 'Bengali', value: 'bn' },
|
||||
{ name: 'Bosnian', value: 'bs' },
|
||||
{ name: 'Breton', value: 'br' },
|
||||
{ name: 'Bulgarian', value: 'bg' },
|
||||
{ name: 'Cantonese', value: 'yue' },
|
||||
{ name: 'Catalan', value: 'ca' },
|
||||
{ name: 'Chinese', value: 'zh' },
|
||||
{ name: 'Croatian', value: 'hr' },
|
||||
{ name: 'Czech', value: 'cs' },
|
||||
{ name: 'Danish', value: 'da' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'Estonian', value: 'et' },
|
||||
{ name: 'Faroese', value: 'fo' },
|
||||
{ name: 'Finnish', value: 'fi' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'Galician', value: 'gl' },
|
||||
{ name: 'Georgian', value: 'ka' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Greek', value: 'el' },
|
||||
{ name: 'Gujarati', value: 'gu' },
|
||||
{ name: 'Haitian Creole', value: 'ht' },
|
||||
{ name: 'Hausa', value: 'ha' },
|
||||
{ name: 'Hawaiian', value: 'haw' },
|
||||
{ name: 'Hebrew', value: 'he' },
|
||||
{ name: 'Hindi', value: 'hi' },
|
||||
{ name: 'Hungarian', value: 'hu' },
|
||||
{ name: 'Icelandic', value: 'is' },
|
||||
{ name: 'Indonesian', value: 'id' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Javanese', value: 'jw' },
|
||||
{ name: 'Kannada', value: 'kn' },
|
||||
{ name: 'Kazakh', value: 'kk' },
|
||||
{ name: 'Khmer', value: 'km' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Lao', value: 'lo' },
|
||||
{ name: 'Latin', value: 'la' },
|
||||
{ name: 'Latvian', value: 'lv' },
|
||||
{ name: 'Lingala', value: 'ln' },
|
||||
{ name: 'Lithuanian', value: 'lt' },
|
||||
{ name: 'Luxembourgish', value: 'lb' },
|
||||
{ name: 'Macedonian', value: 'mk' },
|
||||
{ name: 'Malagasy', value: 'mg' },
|
||||
{ name: 'Malay', value: 'ms' },
|
||||
{ name: 'Malayalam', value: 'ml' },
|
||||
{ name: 'Maltese', value: 'mt' },
|
||||
{ name: 'Maori', value: 'mi' },
|
||||
{ name: 'Marathi', value: 'mr' },
|
||||
{ name: 'Mongolian', value: 'mn' },
|
||||
{ name: 'Myanmar', value: 'my' },
|
||||
{ name: 'Nepali', value: 'ne' },
|
||||
{ name: 'Norwegian', value: 'no' },
|
||||
{ name: 'Nynorsk', value: 'nn' },
|
||||
{ name: 'Occitan', value: 'oc' },
|
||||
{ name: 'Pashto', value: 'ps' },
|
||||
{ name: 'Persian', value: 'fa' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'Punjabi', value: 'pa' },
|
||||
{ name: 'Romanian', value: 'ro' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Sanskrit', value: 'sa' },
|
||||
{ name: 'Serbian', value: 'sr' },
|
||||
{ name: 'Shona', value: 'sn' },
|
||||
{ name: 'Sindhi', value: 'sd' },
|
||||
{ name: 'Sinhala', value: 'si' },
|
||||
{ name: 'Slovak', value: 'sk' },
|
||||
{ name: 'Slovenian', value: 'sl' },
|
||||
{ name: 'Somali', value: 'so' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Sundanese', value: 'su' },
|
||||
{ name: 'Swahili', value: 'sw' },
|
||||
{ name: 'Swedish', value: 'sv' },
|
||||
{ name: 'Tagalog', value: 'tl' },
|
||||
{ name: 'Tajik', value: 'tg' },
|
||||
{ name: 'Tamil', value: 'ta' },
|
||||
{ name: 'Tatar', value: 'tt' },
|
||||
{ name: 'Telugu', value: 'te' },
|
||||
{ name: 'Thai', value: 'th' },
|
||||
{ name: 'Tibetan', value: 'bo' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Turkmen', value: 'tk' },
|
||||
{ name: 'Ukrainian', value: 'uk' },
|
||||
{ name: 'Urdu', value: 'ur' },
|
||||
{ name: 'Uzbek', value: 'uz' },
|
||||
{ name: 'Vietnamese', value: 'vi' },
|
||||
{ name: 'Welsh', value: 'cy' },
|
||||
{ name: 'Wolof', value: 'wo' },
|
||||
{ name: 'Yiddish', value: 'yi' },
|
||||
{ name: 'Yoruba', value: 'yo' }
|
||||
];
|
||||
19
lib/utils/speech-data/stt-houndify.js
Normal file
19
lib/utils/speech-data/stt-houndify.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = [
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'Indian-accented English', value: 'en-IN' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Dutch', value: 'nl' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Mandarin', value: 'zh-CN' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Polish', value: 'pl' },
|
||||
{ name: 'Swedish', value: 'sv' },
|
||||
{ name: 'Arabic', value: 'ar' },
|
||||
{ name: 'Turkish', value: 'tr' },
|
||||
{ name: 'Hebrew', value: 'he' },
|
||||
];
|
||||
4
lib/utils/speech-data/stt-model-cartesia.js
Normal file
4
lib/utils/speech-data/stt-model-cartesia.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = [
|
||||
{ name: 'Ink-whisper', value: 'ink-whisper' },
|
||||
];
|
||||
|
||||
52
lib/utils/speech-data/stt-model-deepgram.js
Normal file
52
lib/utils/speech-data/stt-model-deepgram.js
Normal file
@@ -0,0 +1,52 @@
|
||||
module.exports = [
|
||||
// Nova-3
|
||||
{ name: 'Nova-3', value: 'nova-3' },
|
||||
{ name: 'Nova-3 General', value: 'nova-3-general' },
|
||||
{ name: 'Nova-3 Medical', value: 'nova-3-medical' },
|
||||
|
||||
// Nova-2
|
||||
{ name: 'Nova-2', value: 'nova-2' },
|
||||
{ name: 'Nova-2 General', value: 'nova-2-general' },
|
||||
{ name: 'Nova-2 Meeting', value: 'nova-2-meeting' },
|
||||
{ name: 'Nova-2 Phonecall', value: 'nova-2-phonecall' },
|
||||
{ name: 'Nova-2 Finance', value: 'nova-2-finance' },
|
||||
{ name: 'Nova-2 Conversational AI', value: 'nova-2-conversationalai' },
|
||||
{ name: 'Nova-2 Voicemail', value: 'nova-2-voicemail' },
|
||||
{ name: 'Nova-2 Video', value: 'nova-2-video' },
|
||||
{ name: 'Nova-2 Medical', value: 'nova-2-medical' },
|
||||
{ name: 'Nova-2 Drivethru', value: 'nova-2-drivethru' },
|
||||
{ name: 'Nova-2 Automotive', value: 'nova-2-automotive' },
|
||||
{ name: 'Nova-2 ATC', value: 'nova-2-atc' },
|
||||
|
||||
// Nova (legacy)
|
||||
{ name: 'Nova', value: 'nova' },
|
||||
{ name: 'Nova General', value: 'nova-general' },
|
||||
{ name: 'Nova Phonecall', value: 'nova-phonecall' },
|
||||
{ name: 'Nova Medical', value: 'nova-medical' },
|
||||
|
||||
// Enhanced (legacy)
|
||||
{ name: 'Enhanced', value: 'enhanced' },
|
||||
{ name: 'Enhanced General', value: 'enhanced-general' },
|
||||
{ name: 'Enhanced Meeting', value: 'enhanced-meeting' },
|
||||
{ name: 'Enhanced Phonecall', value: 'enhanced-phonecall' },
|
||||
{ name: 'Enhanced Finance', value: 'enhanced-finance' },
|
||||
|
||||
// Base (legacy)
|
||||
{ name: 'Base', value: 'base' },
|
||||
{ name: 'Base General', value: 'base-general' },
|
||||
{ name: 'Base Meeting', value: 'base-meeting' },
|
||||
{ name: 'Base Phonecall', value: 'base-phonecall' },
|
||||
{ name: 'Base Finance', value: 'base-finance' },
|
||||
{ name: 'Base Conversational AI', value: 'base-conversationalai' },
|
||||
{ name: 'Base Voicemail', value: 'base-voicemail' },
|
||||
{ name: 'Base Video', value: 'base-video' },
|
||||
|
||||
// Whisper
|
||||
{ name: 'Whisper Tiny', value: 'whisper-tiny' },
|
||||
{ name: 'Whisper Base', value: 'whisper-base' },
|
||||
{ name: 'Whisper Small', value: 'whisper-small' },
|
||||
{ name: 'Whisper Medium', value: 'whisper-medium' },
|
||||
{ name: 'Whisper Large', value: 'whisper-large' },
|
||||
{ name: 'Whisper', value: 'whisper' },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = [
|
||||
{ name: 'Auto Language', value: 'auto'},
|
||||
{ name: 'Afrikaans', value: 'af' },
|
||||
{ name: 'Arabic', value: 'ar' },
|
||||
{ name: 'Azerbaijani', value: 'az' },
|
||||
|
||||
118
lib/utils/speech-data/tts-inworld.js
Normal file
118
lib/utils/speech-data/tts-inworld.js
Normal file
@@ -0,0 +1,118 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en',
|
||||
name: 'English',
|
||||
voices: [
|
||||
{ name: 'Alex', value: 'Alex' },
|
||||
{ name: 'Ashley', value: 'Ashley' },
|
||||
{ name: 'Craig', value: 'Craig' },
|
||||
{ name: 'Deborah', value: 'Deborah' },
|
||||
{ name: 'Dennis', value: 'Dennis' },
|
||||
{ name: 'Edward', value: 'Edward' },
|
||||
{ name: 'Elizabeth', value: 'Elizabeth' },
|
||||
{ name: 'Hades', value: 'Hades' },
|
||||
{ name: 'Julia', value: 'Julia' },
|
||||
{ name: 'Pixie', value: 'Pixie' },
|
||||
{ name: 'Mark', value: 'Mark' },
|
||||
{ name: 'Olivia', value: 'Olivia' },
|
||||
{ name: 'Priya', value: 'Priya' },
|
||||
{ name: 'Ronald', value: 'Ronald' },
|
||||
{ name: 'Sarah', value: 'Sarah' },
|
||||
{ name: 'Shaun', value: 'Shaun' },
|
||||
{ name: 'Theodore', value: 'Theodore' },
|
||||
{ name: 'Timothy', value: 'Timothy' },
|
||||
{ name: 'Wendy', value: 'Wendy' },
|
||||
{ name: 'Dominus', value: 'Dominus' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'zh',
|
||||
name: 'Chinese',
|
||||
voices: [
|
||||
{ name: 'Yichen', value: 'Yichen' },
|
||||
{ name: 'Xiaoyin', value: 'Xiaoyin' },
|
||||
{ name: 'Xinyi', value: 'Xinyi' },
|
||||
{ name: 'Jing', value: 'Jing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'nl',
|
||||
name: 'Dutch',
|
||||
voices: [
|
||||
{ name: 'Erik', value: 'Erik' },
|
||||
{ name: 'Katrien', value: 'Katrien' },
|
||||
{ name: 'Lennart', value: 'Lennart' },
|
||||
{ name: 'Lore', value: 'Lore' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'fr',
|
||||
name: 'French',
|
||||
voices: [
|
||||
{ name: 'Alain', value: 'Alain' },
|
||||
{ name: 'Hélène', value: 'Hélène' },
|
||||
{ name: 'Mathieu', value: 'Mathieu' },
|
||||
{ name: 'Étienne', value: 'Étienne' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'de',
|
||||
name: 'German',
|
||||
voices: [
|
||||
{ name: 'Johanna', value: 'Johanna' },
|
||||
{ name: 'Josef', value: 'Josef' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'it',
|
||||
name: 'Italian',
|
||||
voices: [
|
||||
{ name: 'Gianni', value: 'Gianni' },
|
||||
{ name: 'Orietta', value: 'Orietta' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ja',
|
||||
name: 'Japanese',
|
||||
voices: [
|
||||
{ name: 'Asuka', value: 'Asuka' },
|
||||
{ name: 'Satoshi', value: 'Satoshi' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'ko',
|
||||
name: 'Korean',
|
||||
voices: [
|
||||
{ name: 'Hyunwoo', value: 'Hyunwoo' },
|
||||
{ name: 'Minji', value: 'Minji' },
|
||||
{ name: 'Seojun', value: 'Seojun' },
|
||||
{ name: 'Yoona', value: 'Yoona' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'pl',
|
||||
name: 'Polish',
|
||||
voices: [
|
||||
{ name: 'Szymon', value: 'Szymon' },
|
||||
{ name: 'Wojciech', value: 'Wojciech' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'pt',
|
||||
name: 'Portuguese',
|
||||
voices: [
|
||||
{ name: 'Heitor', value: 'Heitor' },
|
||||
{ name: 'Maitê', value: 'Maitê' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'es',
|
||||
name: 'Spanish',
|
||||
voices: [
|
||||
{ name: 'Diego', value: 'Diego' },
|
||||
{ name: 'Lupita', value: 'Lupita' },
|
||||
{ name: 'Miguel', value: 'Miguel' },
|
||||
{ name: 'Rafael', value: 'Rafael' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -9,6 +9,15 @@ module.exports = [
|
||||
value: 'sonic-2',
|
||||
languages: ['en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr']
|
||||
},
|
||||
{
|
||||
name: 'Sonic 3',
|
||||
value: 'sonic-3',
|
||||
languages: [
|
||||
'en', 'fr', 'de', 'es', 'pt', 'zh', 'ja', 'hi', 'it', 'ko', 'nl', 'pl', 'ru', 'sv', 'tr',
|
||||
'tl', 'bg', 'ro', 'ar', 'cs', 'el', 'fi', 'hr', 'ms', 'sk', 'da', 'ta', 'uk', 'hu', 'no',
|
||||
'vi', 'bn', 'th', 'he', 'ka', 'id', 'te', 'gu', 'kn', 'ml', 'mr', 'pa'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Sonic Turbo',
|
||||
value: 'sonic-turbo',
|
||||
|
||||
@@ -1,68 +1,320 @@
|
||||
module.exports = [
|
||||
{
|
||||
locale: 'en-ph',
|
||||
localeName: 'English (PH)',
|
||||
name: 'Amalthea English (PH) Female Aura-2',
|
||||
value: 'aura-2-amalthea-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Andromeda English (US) Female Aura-2',
|
||||
value: 'aura-2-andromeda-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Apollo English (US) Male Aura-2',
|
||||
value: 'aura-2-apollo-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Arcas English (US) Male Aura-2',
|
||||
value: 'aura-2-arcas-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Aries English (US) Male Aura-2',
|
||||
value: 'aura-2-aries-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Asteria English (US) Female Aura-2',
|
||||
value: 'aura-2-asteria-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Athena English (US) Female Aura-2',
|
||||
value: 'aura-2-athena-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Atlas English (US) Male Aura-2',
|
||||
value: 'aura-2-atlas-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Aurora English (US) Female Aura-2',
|
||||
value: 'aura-2-aurora-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Callista English (US) Female Aura-2',
|
||||
value: 'aura-2-callista-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Cora English (US) Female Aura-2',
|
||||
value: 'aura-2-cora-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Cordelia English (US) Female Aura-2',
|
||||
value: 'aura-2-cordelia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Delia English (US) Female Aura-2',
|
||||
value: 'aura-2-delia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-gb',
|
||||
localeName: 'English (GB)',
|
||||
name: 'Draco English (GB) Male Aura-2',
|
||||
value: 'aura-2-draco-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Electra English (US) Female Aura-2',
|
||||
value: 'aura-2-electra-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Harmonia English (US) Female Aura-2',
|
||||
value: 'aura-2-harmonia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Helena English (US) Female Aura-2',
|
||||
value: 'aura-2-helena-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Hera English (US) Female Aura-2',
|
||||
value: 'aura-2-hera-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Hermes English (US) Male Aura-2',
|
||||
value: 'aura-2-hermes-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-au',
|
||||
localeName: 'English (AU)',
|
||||
name: 'Hyperion English (AU) Male Aura-2',
|
||||
value: 'aura-2-hyperion-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Iris English (US) Female Aura-2',
|
||||
value: 'aura-2-iris-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Janus English (US) Female Aura-2',
|
||||
value: 'aura-2-janus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Juno English (US) Female Aura-2',
|
||||
value: 'aura-2-juno-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Jupiter English (US) Male Aura-2',
|
||||
value: 'aura-2-jupiter-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Luna English (US) Female Aura-2',
|
||||
value: 'aura-2-luna-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Mars English (US) Male Aura-2',
|
||||
value: 'aura-2-mars-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Minerva English (US) Female Aura-2',
|
||||
value: 'aura-2-minerva-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Neptune English (US) Male Aura-2',
|
||||
value: 'aura-2-neptune-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Odysseus English (US) Male Aura-2',
|
||||
value: 'aura-2-odysseus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Ophelia English (US) Female Aura-2',
|
||||
value: 'aura-2-ophelia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orion English (US) Male Aura-2',
|
||||
value: 'aura-2-orion-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orpheus English (US) Male Aura-2',
|
||||
value: 'aura-2-orpheus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-gb',
|
||||
localeName: 'English (GB)',
|
||||
name: 'Pandora English (GB) Female Aura-2',
|
||||
value: 'aura-2-pandora-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Phoebe English (US) Female Aura-2',
|
||||
value: 'aura-2-phoebe-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Pluto English (US) Male Aura-2',
|
||||
value: 'aura-2-pluto-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Saturn English (US) Male Aura-2',
|
||||
value: 'aura-2-saturn-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Selene English (US) Female Aura-2',
|
||||
value: 'aura-2-selene-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Thalia English (US) Female Aura-2',
|
||||
value: 'aura-2-thalia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Theia English (US) Female Aura-2',
|
||||
value: 'aura-2-theia-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Vesta English (US) Female Aura-2',
|
||||
value: 'aura-2-vesta-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-us',
|
||||
localeName: 'English (US)',
|
||||
name: 'Zeus English (US) Male Aura-2',
|
||||
value: 'aura-2-zeus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Asteria English (US) Female',
|
||||
name: 'Asteria English (US) Female Aura-1',
|
||||
value: 'aura-asteria-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Luna English (US) Female',
|
||||
name: 'Luna English (US) Female Aura-1',
|
||||
value: 'aura-luna-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Stella English (US) Female',
|
||||
name: 'Stella English (US) Female Aura-1',
|
||||
value: 'aura-stella-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-GB',
|
||||
localeName: 'English (UK)',
|
||||
name: 'Stella English (UK) Female',
|
||||
name: 'Stella English (UK) Female Aura-1',
|
||||
value: 'aura-athena-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Hera English (US) Female',
|
||||
name: 'Hera English (US) Female Aura-1',
|
||||
value: 'aura-hera-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orion English (US) Male',
|
||||
name: 'Orion English (US) Male Aura-1',
|
||||
value: 'aura-orion-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Arcas English (US) Male',
|
||||
name: 'Arcas English (US) Male Aura-1',
|
||||
value: 'aura-arcas-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Perseus English (US) Male',
|
||||
name: 'Perseus English (US) Male Aura-1',
|
||||
value: 'aura-perseus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-IE',
|
||||
localeName: 'English (Ireland)',
|
||||
name: 'Angus English (Ireland) Male',
|
||||
name: 'Angus English (Ireland) Male Aura-1',
|
||||
value: 'aura-angus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Orpheus English (US) Male',
|
||||
name: 'Orpheus English (US) Male Aura-1',
|
||||
value: 'aura-orpheus-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-gb',
|
||||
localeName: 'English (US)',
|
||||
name: 'Helios English (GB) Male Aura-1',
|
||||
value: 'aura-helios-en'
|
||||
},
|
||||
{
|
||||
locale: 'en-US',
|
||||
localeName: 'English (US)',
|
||||
name: 'Zeus English (US) Male',
|
||||
name: 'Zeus English (US) Male Aura-1',
|
||||
value: 'aura-zeus-en'
|
||||
},
|
||||
];
|
||||
|
||||
5
lib/utils/speech-data/tts-model-inworld.js
Normal file
5
lib/utils/speech-data/tts-model-inworld.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = [
|
||||
{ name: 'Llama Inworld TTS', value: 'inworld-tts-1' },
|
||||
{ name: 'Llama Inworld TTS Max', value: 'inworld-tts-1-max' },
|
||||
];
|
||||
|
||||
@@ -3,5 +3,6 @@ module.exports = [
|
||||
{ name: 'PlayHT2.0-turbo', value: 'PlayHT2.0-turbo' },
|
||||
{ name: 'PlayHT2.0', value: 'PlayHT2.0' },
|
||||
{ name: 'PlayHT1.0', value: 'PlayHT1.0' },
|
||||
{ name: 'Dialog 1.0', value: 'PlayDialog'}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = [
|
||||
{ name: 'Arcana', value: 'arcana' },
|
||||
{ name: 'Mist', value: 'mist' },
|
||||
{ name: 'Mistv2', value: 'mistv2' },
|
||||
{ name: 'V1', value: 'v1' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = [
|
||||
{ name: 'TTS-1', value: 'tts-1' },
|
||||
{ name: 'TTS-1-HD', value: 'tts-1-hd' },
|
||||
{ name: 'GPT-4o mini TTS', value: 'gpt-4o-mini-tts' },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,16 +1,66 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'English',
|
||||
voices: [
|
||||
{
|
||||
value: 'English-US.Female-1',
|
||||
name: 'Female',
|
||||
},
|
||||
{
|
||||
value: 'English-US.Male-1',
|
||||
name: 'Male',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
module.exports = (() => {
|
||||
const voices = [
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Female-1', name: 'Female Magpie' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Calm', name: 'Female Calm' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Fearful', name: 'Female Fearful' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Happy', name: 'Female Happy' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Angry', name: 'Female Angry' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Female.Neutral', name: 'Female Neutral' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Calm', name: 'Male Calm' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Fearful', name: 'Male Fearful' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Happy', name: 'Male Happy' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Neutral', name: 'Male Neutral' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Angry', name: 'Male Angry' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Disgusted', name: 'Male Disgusted' },
|
||||
{ value: 'Magpie-Multilingual.EN-US.Male.Male-1', name: 'Male Magpie' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Male-1', name: 'Male Magpie' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Female-1', name: 'Female Magpie' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Angry', name: 'Female Angry' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Calm', name: 'Female Calm' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Disgust', name: 'Female Disgust' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Sad', name: 'Female Sad' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Happy', name: 'Female Happy' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Fearful', name: 'Female Fearful' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Female.Neutral', name: 'Female Neutral' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Neutral', name: 'Male Neutral' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Angry', name: 'Male Angry' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Calm', name: 'Male Calm' },
|
||||
{ value: 'Magpie-Multilingual.FR-FR.Male.Sad', name: 'Male Sad' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Male-1', name: 'Male Magpie' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Female-1', name: 'Female Magpie' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Neutral', name: 'Female Neutral' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Neutral', name: 'Male Neutral' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Angry', name: 'Male Angry' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Angry', name: 'Female Angry' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Happy', name: 'Female Happy' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Happy', name: 'Male Happy' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Calm', name: 'Female Calm' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Calm', name: 'Male Calm' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Pleasant_Surprise', name: 'Female Pleasant Surprise' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Pleasant_Surprise', name: 'Male Pleasant Surprise' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Female.Sad', name: 'Female Sad' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Sad', name: 'Male Sad' },
|
||||
{ value: 'Magpie-Multilingual.ES-US.Male.Disgust', name: 'Male Disgust' }
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'English',
|
||||
voices: [
|
||||
{ value: 'English-US.Female-1', name: 'Female' },
|
||||
{ value: 'English-US.Male-1', name: 'Male' },
|
||||
...voices.filter((voice) => voice.value.includes('EN-US'))]
|
||||
},
|
||||
{
|
||||
value: 'fr-FR',
|
||||
name: 'French',
|
||||
voices: voices.filter((voice) => voice.value.includes('FR-FR'))
|
||||
},
|
||||
{
|
||||
value: 'es-US',
|
||||
name: 'Spanish',
|
||||
voices: voices.filter((voice) => voice.value.includes('ES-US'))
|
||||
}
|
||||
];
|
||||
})();
|
||||
|
||||
438
lib/utils/speech-data/tts-resemble.js
Normal file
438
lib/utils/speech-data/tts-resemble.js
Normal file
@@ -0,0 +1,438 @@
|
||||
module.exports = [
|
||||
{
|
||||
value: 'en-gb',
|
||||
name: 'En-gb',
|
||||
voices: [
|
||||
{
|
||||
name: 'Seth (Legacy) (professional) - Resemble Voice',
|
||||
value: 'a52c4efc',
|
||||
},
|
||||
{
|
||||
name: 'Seth (professional) - Resemble Voice',
|
||||
value: 'd3e61caf',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-GB',
|
||||
name: 'En-GB',
|
||||
voices: [
|
||||
{
|
||||
name: 'Beatrice Pendergast (professional) - Resemble Voice',
|
||||
value: '00b1fd4e',
|
||||
},
|
||||
{
|
||||
name: 'Ed Smart (professional) - Resemble Voice',
|
||||
value: '0c755526',
|
||||
},
|
||||
{
|
||||
name: 'Paula J (professional) - Resemble Voice',
|
||||
value: '33e64cd2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-us',
|
||||
name: 'En-us',
|
||||
voices: [
|
||||
{
|
||||
name: 'David (professional) - Resemble Voice',
|
||||
value: '5bb13f03',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'en-US',
|
||||
name: 'En-US',
|
||||
voices: [
|
||||
{
|
||||
name: 'Adam Lofbomm (professional) - Resemble Voice',
|
||||
value: '4e228dba',
|
||||
},
|
||||
{
|
||||
name: 'Alex (professional) - Resemble Voice',
|
||||
value: '41b99669',
|
||||
},
|
||||
{
|
||||
name: 'Amelia (professional) - Resemble Voice',
|
||||
value: 'ecbe5d97',
|
||||
},
|
||||
{
|
||||
name: 'Andrew (rapid) - Resemble Marketplace',
|
||||
value: 'd2f26a3e',
|
||||
},
|
||||
{
|
||||
name: 'Annika (professional) - Resemble Voice',
|
||||
value: 'b27f3cc0',
|
||||
},
|
||||
{
|
||||
name: 'Arthur (professional) - Resemble Voice',
|
||||
value: '9de11312',
|
||||
},
|
||||
{
|
||||
name: 'Ash (professional) - Resemble Voice',
|
||||
value: 'ee322483',
|
||||
},
|
||||
{
|
||||
name: 'Aurora (professional) - Resemble Voice',
|
||||
value: 'a72d9fca',
|
||||
},
|
||||
{
|
||||
name: 'Austin (professional) - Resemble Voice',
|
||||
value: '82a67e58',
|
||||
},
|
||||
{
|
||||
name: 'Beth (Legacy) (professional) - Resemble Voice',
|
||||
value: '25c7823f',
|
||||
},
|
||||
{
|
||||
name: 'Beth (professional) - Resemble Voice',
|
||||
value: 'fa66d263',
|
||||
},
|
||||
{
|
||||
name: 'Blade (professional) - Resemble Voice',
|
||||
value: '8bedd793',
|
||||
},
|
||||
{
|
||||
name: 'Brandy Sky (professional) - Resemble Voice',
|
||||
value: '79e2f1dc',
|
||||
},
|
||||
{
|
||||
name: 'Brenley (professional) - Resemble Voice',
|
||||
value: 'e6ec3ca4',
|
||||
},
|
||||
{
|
||||
name: 'Britney (professional) - Resemble Voice',
|
||||
value: 'e57e23ff',
|
||||
},
|
||||
{
|
||||
name: 'Broadcast Joe (professional) - Resemble Voice',
|
||||
value: '21e49584',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Angry) (professional) - Resemble Voice',
|
||||
value: 'f06cd770',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Conversational) (professional) - Resemble Voice',
|
||||
value: '7f40ff35',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Happy) (professional) - Resemble Voice',
|
||||
value: '99751e42',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (professional) - Resemble Voice',
|
||||
value: '01bcc102',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Scared) (Legacy) (professional) - Resemble Voice',
|
||||
value: '1dcf0222',
|
||||
},
|
||||
{
|
||||
name: 'Carl Bishop (Scared) (professional) - Resemble Voice',
|
||||
value: 'eacbc44f',
|
||||
},
|
||||
{
|
||||
name: 'Charles (Legacy) (professional) - Resemble Voice',
|
||||
value: '4c6d3da5',
|
||||
},
|
||||
{
|
||||
name: 'Charles (professional) - Resemble Voice',
|
||||
value: 'd79a5198',
|
||||
},
|
||||
{
|
||||
name: 'Charlotte (professional) - Resemble Voice',
|
||||
value: '96b91cf9',
|
||||
},
|
||||
{
|
||||
name: 'Chris Whiting (professional) - Resemble Voice',
|
||||
value: '95b7560a',
|
||||
},
|
||||
{
|
||||
name: 'Cliff (professional) - Resemble Voice',
|
||||
value: 'fcf8490c',
|
||||
},
|
||||
{
|
||||
name: 'Connor (professional) - Resemble Voice',
|
||||
value: 'a6131acf',
|
||||
},
|
||||
{
|
||||
name: 'Deanna (professional) - Resemble Voice',
|
||||
value: '0842fdf9',
|
||||
},
|
||||
{
|
||||
name: 'Ember (professional) - Resemble Voice',
|
||||
value: '55592656',
|
||||
},
|
||||
{
|
||||
name: 'Gene Amore (professional) - Resemble Voice',
|
||||
value: 'f2ea7aa0',
|
||||
},
|
||||
{
|
||||
name: 'Harry Robinson (professional) - Resemble Voice',
|
||||
value: '3c36d67d',
|
||||
},
|
||||
{
|
||||
name: 'Helena (professional) - Resemble Voice',
|
||||
value: 'ac948df2',
|
||||
},
|
||||
{
|
||||
name: 'Hem (professional) - Resemble Voice',
|
||||
value: 'b6edbe5f',
|
||||
},
|
||||
{
|
||||
name: 'John (professional) - Resemble Voice',
|
||||
value: 'ac48daeb',
|
||||
},
|
||||
{
|
||||
name: 'Josh (professional) - Resemble Voice',
|
||||
value: '987c99e9',
|
||||
},
|
||||
{
|
||||
name: 'Julie Hoverson (professional) - Resemble Voice',
|
||||
value: 'b119524c',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Legacy) (professional) - Resemble Voice',
|
||||
value: 'b2d1bb75',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Meditative) (Legacy) (professional) - Resemble Voice',
|
||||
value: '93ce0920',
|
||||
},
|
||||
{
|
||||
name: 'Justin (Meditative) (professional) - Resemble Voice',
|
||||
value: '2570000e',
|
||||
},
|
||||
{
|
||||
name: 'Justin (professional) - Resemble Voice',
|
||||
value: '9d513c17',
|
||||
},
|
||||
{
|
||||
name: 'Karl Nordman (professional) - Resemble Voice',
|
||||
value: 'da67f17e',
|
||||
},
|
||||
{
|
||||
name: 'Kate (professional) - Resemble Voice',
|
||||
value: '28b4cc5a',
|
||||
},
|
||||
{
|
||||
name: 'Katya (professional) - Resemble Voice',
|
||||
value: 'c9ee13b4',
|
||||
},
|
||||
{
|
||||
name: 'Ken (professional) - Resemble Voice',
|
||||
value: '3dbfbf3d',
|
||||
},
|
||||
{
|
||||
name: 'Kessi (professional) - Resemble Voice',
|
||||
value: '2211cb8c',
|
||||
},
|
||||
{
|
||||
name: 'Little Ari (professional) - Resemble Voice',
|
||||
value: '805adead',
|
||||
},
|
||||
{
|
||||
name: 'Little Brittle (professional) - Resemble Voice',
|
||||
value: '8a73f115',
|
||||
},
|
||||
{
|
||||
name: 'Liz (professional) - Resemble Voice',
|
||||
value: '4884d94a',
|
||||
},
|
||||
{
|
||||
name: 'Lothar (professional) - Resemble Voice',
|
||||
value: '78671217',
|
||||
},
|
||||
{
|
||||
name: 'Luna (professional) - Resemble Voice',
|
||||
value: 'ae8223ca',
|
||||
},
|
||||
{
|
||||
name: 'Matt Weller (professional) - Resemble Voice',
|
||||
value: 'f4da4639',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Angry) (professional) - Resemble Voice',
|
||||
value: '482babfc',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Caring) (professional) - Resemble Voice',
|
||||
value: 'b15e550f',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Happy) (professional) - Resemble Voice',
|
||||
value: '91947e5c',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (professional) - Resemble Voice',
|
||||
value: '7d94218f',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Sad) (professional) - Resemble Voice',
|
||||
value: 'bca7481c',
|
||||
},
|
||||
{
|
||||
name: 'Maureen (Scared) (professional) - Resemble Voice',
|
||||
value: '251c9439',
|
||||
},
|
||||
{
|
||||
name: 'Mauren (Announcer) (professional) - Resemble Voice',
|
||||
value: 'e984fb89',
|
||||
},
|
||||
{
|
||||
name: 'Melody (Legacy) (professional) - Resemble Voice',
|
||||
value: '15be93bd',
|
||||
},
|
||||
{
|
||||
name: 'Melody (professional) - Resemble Voice',
|
||||
value: '1c49e774',
|
||||
},
|
||||
{
|
||||
name: 'Mike (professional) - Resemble Voice',
|
||||
value: '3a02dc40',
|
||||
},
|
||||
{
|
||||
name: 'Niki (professional) - Resemble Voice',
|
||||
value: 'db37643c',
|
||||
},
|
||||
{
|
||||
name: 'Olga (professional) - Resemble Voice',
|
||||
value: '07c1d6b5',
|
||||
},
|
||||
{
|
||||
name: 'Olivia (Legacy) (professional) - Resemble Voice',
|
||||
value: '405b58e3',
|
||||
},
|
||||
{
|
||||
name: 'Olivia (professional) - Resemble Voice',
|
||||
value: 'ef49f972',
|
||||
},
|
||||
{
|
||||
name: 'Orion (professional) - Resemble Voice',
|
||||
value: 'aa8053cc',
|
||||
},
|
||||
{
|
||||
name: 'Pete (professional) - Resemble Voice',
|
||||
value: '1864fd63',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Legacy) (professional) - Resemble Voice',
|
||||
value: '7c8e47ca',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (professional) - Resemble Voice',
|
||||
value: '33eecc17',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Whispering) (Legacy) (professional) - Resemble Voice',
|
||||
value: 'a56c5c6f',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Whispering) (professional) - Resemble Voice',
|
||||
value: '28fcdf76',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Winded) (Legacy) (professional) - Resemble Voice',
|
||||
value: '6f9a77a4',
|
||||
},
|
||||
{
|
||||
name: 'Primrose (Winded) (professional) - Resemble Voice',
|
||||
value: '0097f246',
|
||||
},
|
||||
{
|
||||
name: 'Professor Shaposhnikov (professional) - Resemble Voice',
|
||||
value: '3f5fb9f1',
|
||||
},
|
||||
{
|
||||
name: 'Radio Nikole (professional) - Resemble Voice',
|
||||
value: '19eae884',
|
||||
},
|
||||
{
|
||||
name: 'Richard Garifo (professional) - Resemble Voice',
|
||||
value: '85ba84f2',
|
||||
},
|
||||
{
|
||||
name: 'Rico (professional) - Resemble Voice',
|
||||
value: '14ca34b3',
|
||||
},
|
||||
{
|
||||
name: 'Robert (professional) - Resemble Voice',
|
||||
value: '3e907bcc',
|
||||
},
|
||||
{
|
||||
name: 'Rupert (rapid) - Resemble Voice',
|
||||
value: '28f1626c',
|
||||
},
|
||||
{
|
||||
name: 'Sam (professional) - Resemble Voice',
|
||||
value: '0f2f9a7e',
|
||||
},
|
||||
{
|
||||
name: 'Samantha (Legacy) (professional) - Resemble Voice',
|
||||
value: '266bfae9',
|
||||
},
|
||||
{
|
||||
name: 'Samantha (professional) - Resemble Voice',
|
||||
value: 'e28236ee',
|
||||
},
|
||||
{
|
||||
name: 'Siobhan (professional) - Resemble Voice',
|
||||
value: 'af72c1ac',
|
||||
},
|
||||
{
|
||||
name: 'Steve (Scared) (professional) - Resemble Voice',
|
||||
value: 'aaa56e79',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (professional) - Resemble Voice',
|
||||
value: 'adb84c77',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (Telephonic) (professional) - Resemble Voice',
|
||||
value: '4f5a470b',
|
||||
},
|
||||
{
|
||||
name: 'Tanja (Warm Word Weaver) (professional) - Resemble Voice',
|
||||
value: 'abbbc383',
|
||||
},
|
||||
{
|
||||
name: 'Tarkos (professional) - Resemble Voice',
|
||||
value: '779842bf',
|
||||
},
|
||||
{
|
||||
name: 'Tyler (professional) - Resemble Voice',
|
||||
value: 'ff225977',
|
||||
},
|
||||
{
|
||||
name: 'Vicky (professional) - Resemble Voice',
|
||||
value: 'f453b918',
|
||||
},
|
||||
{
|
||||
name: 'Vivian (Legacy) (professional) - Resemble Voice',
|
||||
value: 'bed1044d',
|
||||
},
|
||||
{
|
||||
name: 'Vivian (professional) - Resemble Voice',
|
||||
value: '1ff0045f',
|
||||
},
|
||||
{
|
||||
name: 'William (Whispering) (Legacy) (professional) - Resemble Voice',
|
||||
value: '79eb7953',
|
||||
},
|
||||
{
|
||||
name: 'William (Whispering) (professional) - Resemble Voice',
|
||||
value: 'e2180df0',
|
||||
},
|
||||
{
|
||||
name: 'Willow (Whispering) (professional) - Resemble Voice',
|
||||
value: 'f2906c4a',
|
||||
},
|
||||
{
|
||||
name: 'Willow II (Whispering) (professional) - Resemble Voice',
|
||||
value: 'c815cd7a',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -3,9 +3,10 @@ const { TranscribeClient, ListVocabulariesCommand } = require('@aws-sdk/client-t
|
||||
const { Deepgram } = require('@deepgram/sdk');
|
||||
const sdk = require('microsoft-cognitiveservices-speech-sdk');
|
||||
const { SpeechClient } = require('@soniox/soniox-node');
|
||||
const bent = require('bent');
|
||||
const fs = require('fs');
|
||||
const { AssemblyAI } = require('assemblyai');
|
||||
const Houndify = require('houndify');
|
||||
const { GladiaClient } = require('@gladiaio/sdk');
|
||||
const {decrypt, obscureKey} = require('./encrypt-decrypt');
|
||||
const { RealtimeSession } = require('speechmatics');
|
||||
|
||||
@@ -20,7 +21,9 @@ const TtsElevenlabsLanguagesVoices = require('./speech-data/tts-elevenlabs');
|
||||
const TtsWhisperLanguagesVoices = require('./speech-data/tts-whisper');
|
||||
const TtsPlayHtLanguagesVoices = require('./speech-data/tts-playht');
|
||||
const TtsVerbioLanguagesVoices = require('./speech-data/tts-verbio');
|
||||
const TtsInworldLanguagesVoices = require('./speech-data/tts-inworld');
|
||||
const ttsCartesia = require('./speech-data/tts-cartesia');
|
||||
const TtsResembleLanguagesVoices = require('./speech-data/tts-resemble');
|
||||
|
||||
const TtsModelDeepgram = require('./speech-data/tts-model-deepgram');
|
||||
const TtsLanguagesDeepgram = require('./speech-data/tts-deepgram');
|
||||
@@ -29,6 +32,7 @@ const TtsModelWhisper = require('./speech-data/tts-model-whisper');
|
||||
const TtsModelPlayHT = require('./speech-data/tts-model-playht');
|
||||
const ttsLanguagesPlayHt = require('./speech-data/tts-languages-playht');
|
||||
const TtsModelRimelabs = require('./speech-data/tts-model-rimelabs');
|
||||
const TtsModelInworld = require('./speech-data/tts-model-inworld');
|
||||
const TtsModelCartesia = require('./speech-data/tts-model-cartesia');
|
||||
const TtsModelOpenai = require('./speech-data/tts-model-openai');
|
||||
|
||||
@@ -43,12 +47,21 @@ const SttCobaltLanguagesVoices = require('./speech-data/stt-cobalt');
|
||||
const SttSonioxLanguagesVoices = require('./speech-data/stt-soniox');
|
||||
const SttSpeechmaticsLanguagesVoices = require('./speech-data/stt-speechmatics');
|
||||
const SttAssemblyaiLanguagesVoices = require('./speech-data/stt-assemblyai');
|
||||
const SttHoundifyLanguagesVoices = require('./speech-data/stt-houndify');
|
||||
const SttVoxistLanguagesVoices = require('./speech-data/stt-voxist');
|
||||
const SttVerbioLanguagesVoices = require('./speech-data/stt-verbio');
|
||||
const SttOpenaiLanguagesVoices = require('./speech-data/stt-openai');
|
||||
const SttGladiaLanguagesVoices = require('./speech-data/stt-gladia');
|
||||
|
||||
|
||||
const SttModelOpenai = require('./speech-data/stt-model-openai');
|
||||
const sttModelDeepgram = require('./speech-data/stt-model-deepgram');
|
||||
const sttModelCartesia = require('./speech-data/stt-model-cartesia');
|
||||
|
||||
function capitalizeFirst(str) {
|
||||
if (!str) return str;
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
|
||||
const testSonioxStt = async(logger, credentials) => {
|
||||
@@ -57,7 +70,9 @@ const testSonioxStt = async(logger, credentials) => {
|
||||
|
||||
return new Promise(async(resolve, reject) => {
|
||||
try {
|
||||
const result = await soniox.transcribeFileShort('data/test_audio.wav');
|
||||
const result = await soniox.transcribeFileShort('data/test_audio.wav', {
|
||||
model: 'en_v2'
|
||||
});
|
||||
if (result.words.length > 0) resolve(result);
|
||||
else reject(new Error('no transcript returned'));
|
||||
} catch (error) {
|
||||
@@ -159,6 +174,65 @@ const testGoogleStt = async(logger, credentials) => {
|
||||
}
|
||||
};
|
||||
|
||||
const testGladiaStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
|
||||
try {
|
||||
const gladiaClient = new GladiaClient({
|
||||
apiKey: api_key,
|
||||
});
|
||||
const gladiaConfig = {
|
||||
model: 'solaria-1',
|
||||
encoding: 'wav/pcm',
|
||||
sample_rate: 16000,
|
||||
bit_depth: 16,
|
||||
channels: 1,
|
||||
language_config: {
|
||||
languages: ['en'],
|
||||
code_switching: false,
|
||||
},
|
||||
};
|
||||
// Start the live session
|
||||
const liveSession = gladiaClient.liveV2().startSession(gladiaConfig);
|
||||
// Read the test audio file
|
||||
const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`);
|
||||
|
||||
// Wait for final transcript
|
||||
return new Promise((resolve, reject) => {
|
||||
liveSession.on('message', (message) => {
|
||||
if (message.type === 'transcript' && message.data.is_final) {
|
||||
logger.debug(`${message.data.id}: ${message.data.utterance.text}`);
|
||||
liveSession.stopRecording();
|
||||
resolve(message.data.utterance.text);
|
||||
}
|
||||
});
|
||||
|
||||
liveSession.on('error', (error) => {
|
||||
logger.error({error}, 'Gladia Live STT error');
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Send audio in chunks
|
||||
const chunkSize = 1024;
|
||||
for (let i = 0; i < audioBuffer.length; i += chunkSize) {
|
||||
const chunk = audioBuffer.slice(i, i + chunkSize);
|
||||
liveSession.sendAudio(chunk);
|
||||
}
|
||||
// Stop recording after sending all audio
|
||||
liveSession.stopRecording();
|
||||
|
||||
// Set a timeout to prevent hanging
|
||||
setTimeout(() => {
|
||||
reject(new Error('Gladia STT test timeout'));
|
||||
}, 30000); // 30 second timeout
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Failed to create Gladia Live STT session');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const testDeepgramStt = async(logger, credentials) => {
|
||||
const {api_key, deepgram_stt_uri, deepgram_stt_use_tls} = credentials;
|
||||
const deepgram = new Deepgram(api_key, deepgram_stt_uri, deepgram_stt_uri && deepgram_stt_use_tls);
|
||||
@@ -288,44 +362,46 @@ const testMicrosoftTts = async(logger, synthAudio, credentials) => {
|
||||
|
||||
const testWellSaidTts = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
try {
|
||||
const post = bent('https://api.wellsaidlabs.com', 'POST', 'buffer', {
|
||||
const response = await fetch('https://api.wellsaidlabs.com/v1/tts/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Api-Key': api_key,
|
||||
'Accept': 'audio/mpeg',
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
const mp3 = await post('/v1/tts/stream', {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: 'Hello, world',
|
||||
speaker_id: '3'
|
||||
});
|
||||
return mp3;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'testWellSaidTts returned error');
|
||||
throw err;
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to synthesize speech');
|
||||
}
|
||||
return response.body;
|
||||
};
|
||||
|
||||
const testElevenlabs = async(logger, credentials) => {
|
||||
const {api_key, model_id} = credentials;
|
||||
try {
|
||||
const post = bent('https://api.elevenlabs.io', 'POST', 'buffer', {
|
||||
const {api_key, model_id, api_uri} = credentials;
|
||||
const response = await fetch(`https://${api_uri || 'api.elevenlabs.io'}/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'xi-api-key': api_key,
|
||||
'Accept': 'audio/mpeg',
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
const mp3 = await post('/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM', {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: 'Hello',
|
||||
model_id,
|
||||
voice_settings: {
|
||||
stability: 0.5,
|
||||
similarity_boost: 0.5
|
||||
}
|
||||
});
|
||||
return mp3;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synthEvenlabs returned error');
|
||||
throw err;
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to synthesize speech');
|
||||
}
|
||||
return response.body;
|
||||
};
|
||||
|
||||
const testPlayHT = async(logger, synthAudio, credentials) => {
|
||||
@@ -362,14 +438,36 @@ const testRimelabs = async(logger, synthAudio, credentials) => {
|
||||
{
|
||||
vendor: 'rimelabs',
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
language: 'eng',
|
||||
voice: 'amber',
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth Playht returned error');
|
||||
logger.info({err}, 'synth rimelabs returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const testInworld = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio(
|
||||
{
|
||||
increment: () => {},
|
||||
histogram: () => {}
|
||||
},
|
||||
{
|
||||
vendor: 'inworld',
|
||||
credentials,
|
||||
language: 'en',
|
||||
voice: 'Ashley',
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth inworld returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -387,7 +485,25 @@ const testWhisper = async(logger, synthAudio, credentials) => {
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synthEvenlabs returned error');
|
||||
logger.info({err}, 'synth whisper returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const testResembleTTS = async(logger, synthAudio, credentials) => {
|
||||
try {
|
||||
await synthAudio({increment: () => {}, histogram: () => {}},
|
||||
{
|
||||
vendor: 'resemble',
|
||||
credentials,
|
||||
language: 'en-US',
|
||||
voice: '3f5fb9f1',
|
||||
text: 'Hi there and welcome to jambones!',
|
||||
renderForCaching: true
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
logger.info({err}, 'synth resemble returned error');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -466,18 +582,18 @@ const testVerbioTts = async(logger, synthAudio, credentials) => {
|
||||
};
|
||||
const testVerbioStt = async(logger, getVerbioAccessToken, credentials) => {
|
||||
const token = await getVerbioAccessToken(credentials);
|
||||
try {
|
||||
const post = bent('https://us.rest.speechcenter.verbio.com', 'POST', 'json', {
|
||||
const response = await fetch('https://us.rest.speechcenter.verbio.com/api/v1/recognize?language=en-US&version=V1', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token.access_token}`,
|
||||
'User-Agent': 'jambonz',
|
||||
'Content-Type': 'audio/wav'
|
||||
});
|
||||
const json = await post('/api/v1/recognize?language=en-US&version=V1',
|
||||
fs.readFileSync(`${__dirname}/../../data/test_audio.wav`));
|
||||
logger.debug({json}, 'successfully speech to text from verbio');
|
||||
} catch (err) {
|
||||
logger.info({err}, 'testWellSaidTts returned error');
|
||||
throw err;
|
||||
},
|
||||
body: fs.readFileSync(`${__dirname}/../../data/test_audio.wav`)
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({Error: await response.text()}, 'Error transcribing speech');
|
||||
throw new Error('failed to transcribe speech');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -544,18 +660,87 @@ const testAssemblyStt = async(logger, credentials) => {
|
||||
});
|
||||
};
|
||||
|
||||
const testHoundifyStt = async(logger, credentials) => {
|
||||
const {client_id, client_key, user_id} = credentials;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Read the test audio file
|
||||
const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`);
|
||||
|
||||
// Create VoiceRequest for speech-to-text testing
|
||||
const voiceRequest = new Houndify.VoiceRequest({
|
||||
// Your Houndify Client ID and Key
|
||||
clientId: client_id,
|
||||
clientKey: client_key,
|
||||
|
||||
// Request info
|
||||
requestInfo: {
|
||||
UserID: user_id || 'test_user',
|
||||
Latitude: 37.388309,
|
||||
Longitude: -121.973968,
|
||||
},
|
||||
// custom endpint is used only for feature server.
|
||||
// ...(houndify_server_uri && {endpoint: houndify_server_uri}),
|
||||
|
||||
// Audio format configuration
|
||||
sampleRate: 16000,
|
||||
enableVAD: true,
|
||||
|
||||
// Response and error handlers
|
||||
onResponse: function(response, info) {
|
||||
logger.debug({response, info}, 'Houndify STT response received');
|
||||
if (response && response.AllResults && response.AllResults.length > 0) {
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error('No transcription results received'));
|
||||
}
|
||||
},
|
||||
|
||||
onError: function(err, info) {
|
||||
logger.error({err, info}, 'Houndify STT error');
|
||||
reject(err);
|
||||
},
|
||||
|
||||
onRecordingStarted: function() {
|
||||
logger.debug('Houndify recording started');
|
||||
},
|
||||
|
||||
onRecordingStopped: function() {
|
||||
logger.debug('Houndify recording stopped');
|
||||
}
|
||||
});
|
||||
|
||||
// Send audio in chunks (VoiceRequest automatically starts when you write data)
|
||||
const chunkSize = 1024;
|
||||
for (let i = 0; i < audioBuffer.length; i += chunkSize) {
|
||||
const chunk = audioBuffer.slice(i, i + chunkSize);
|
||||
voiceRequest.write(chunk);
|
||||
}
|
||||
|
||||
// End the request
|
||||
voiceRequest.end();
|
||||
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Failed to create Houndify VoiceRequest');
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testVoxistStt = async(logger, credentials) => {
|
||||
const {api_key} = credentials;
|
||||
try {
|
||||
const get = bent('https://api-asr.voxist.com', 'GET', 'json', {
|
||||
const response = await fetch('https://api-asr.voxist.com/clients', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'x-lvl-key': api_key
|
||||
});
|
||||
await get('/clients');
|
||||
} catch (err) {
|
||||
logger.info({err}, 'failed to get clients from Voxist');
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error retrieving clients');
|
||||
throw new Error('failed to get clients');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const getSpeechCredential = (credential, logger) => {
|
||||
@@ -596,6 +781,7 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
o.private_key.slice(key_header.length, o.private_key.length)}`
|
||||
};
|
||||
obj.service_key = JSON.stringify(obscured);
|
||||
obj.model_id = o.model_id || null;
|
||||
}
|
||||
else if ('aws' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -603,7 +789,6 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.role_arn = o.role_arn;
|
||||
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));
|
||||
@@ -615,7 +800,6 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
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));
|
||||
@@ -634,6 +818,15 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
obj.deepgram_stt_uri = o.deepgram_stt_uri;
|
||||
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
|
||||
obj.deepgram_tts_uri = o.deepgram_tts_uri;
|
||||
obj.model_id = o.model_id;
|
||||
}
|
||||
else if ('deepgramflux' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
}
|
||||
else if ('gladia' === 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));
|
||||
@@ -659,14 +852,22 @@ 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.api_uri = o.api_uri;
|
||||
obj.options = o.options;
|
||||
} else if ('playht' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.user_id = o.user_id;
|
||||
obj.voice_engine = o.voice_engine;
|
||||
obj.playht_tts_uri = o.playht_tts_uri;
|
||||
obj.options = o.options;
|
||||
} else if ('cartesia' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
obj.stt_model_id = o.stt_model_id;
|
||||
obj.options = o.options;
|
||||
} else if ('inworld' === 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;
|
||||
@@ -685,6 +886,18 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) {
|
||||
} else if ('assemblyai' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.service_version = o.service_version;
|
||||
} else if ('houndify' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.client_key = isObscureKey ? obscureKey(o.client_key) : o.client_key;
|
||||
obj.client_id = o.client_id;
|
||||
obj.user_id = o.user_id;
|
||||
obj.houndify_server_uri = o.houndify_server_uri;
|
||||
} else if ('resemble' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
obj.resemble_tts_uri = o.resemble_tts_uri;
|
||||
obj.resemble_tts_use_tls = o.resemble_tts_use_tls;
|
||||
} else if ('voxist' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key;
|
||||
@@ -739,6 +952,8 @@ async function getLanguagesAndVoicesForVendor(logger, vendor, credential, getTts
|
||||
return await getLanguagesVoicesForNuane(credential, getTtsVoices, logger);
|
||||
case 'deepgram':
|
||||
return await getLanguagesVoicesForDeepgram(credential, getTtsVoices, logger);
|
||||
case 'gladia':
|
||||
return await getLanguagesVoicesForGladia(credential, getTtsVoices, logger);
|
||||
case 'ibm':
|
||||
return await getLanguagesVoicesForIbm(credential, getTtsVoices, logger);
|
||||
case 'nvidia':
|
||||
@@ -753,8 +968,14 @@ async function getLanguagesAndVoicesForVendor(logger, vendor, credential, getTts
|
||||
return await getLanguagesVoicesForPlayHT(credential, getTtsVoices, logger);
|
||||
case 'rimelabs':
|
||||
return await getLanguagesVoicesForRimelabs(credential, getTtsVoices, logger);
|
||||
case 'inworld':
|
||||
return await getLanguagesVoicesForInworld(credential, getTtsVoices, logger);
|
||||
case 'resemble':
|
||||
return await getLanguagesAndVoicesForResemble(credential, getTtsVoices, logger);
|
||||
case 'assemblyai':
|
||||
return await getLanguagesVoicesForAssemblyAI(credential, getTtsVoices, logger);
|
||||
case 'houndify':
|
||||
return await getLanguagesVoicesForHoundify(credential, getTtsVoices, logger);
|
||||
case 'voxist':
|
||||
return await getLanguagesVoicesForVoxist(credential, getTtsVoices, logger);
|
||||
case 'whisper':
|
||||
@@ -812,17 +1033,19 @@ async function getLanguagesVoicesForAws(credential, getTtsVoices, logger) {
|
||||
|
||||
async function getLanguagesVoicesForMicrosoft(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
try {
|
||||
const get = bent('https://westus.tts.speech.microsoft.com', 'GET', 'json', {
|
||||
'Ocp-Apim-Subscription-Key' : credential.api_key
|
||||
});
|
||||
|
||||
const voices = await get('/cognitiveservices/voices/list');
|
||||
const tts = parseMicrosoftLanguagesVoices(voices);
|
||||
return tranform(tts, SttMicrosoftLanguagesVoices);
|
||||
} catch (err) {
|
||||
logger.info('Error while fetching Microsoft languages, voices, return predefined values', err);
|
||||
const {region, api_key} = credential;
|
||||
const response = await fetch(`https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`, {
|
||||
headers: {
|
||||
'Ocp-Apim-Subscription-Key': api_key
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error fetching Microsoft voices');
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
const voices = await response.json();
|
||||
const tts = parseMicrosoftLanguagesVoices(voices);
|
||||
return tranform(tts, SttMicrosoftLanguagesVoices);
|
||||
}
|
||||
return tranform(TtsMicrosoftLanguagesVoices, SttMicrosoftLanguagesVoices);
|
||||
}
|
||||
@@ -847,8 +1070,64 @@ async function getLanguagesVoicesForNuane(credential, getTtsVoices, logger) {
|
||||
return tranform(TtsNuanceLanguagesVoices, SttNuanceLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForDeepgram(credential) {
|
||||
return tranform(TtsLanguagesDeepgram, SttDeepgramLanguagesVoices, TtsModelDeepgram);
|
||||
async function getLanguagesVoicesForDeepgram(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
const {model_id, api_key, deepgram_stt_uri, deepgram_tts_uri} = credential;
|
||||
// currently just fetching STT and TTS models from Deepgram cloud
|
||||
if (!deepgram_stt_uri && !deepgram_tts_uri) {
|
||||
const response = await fetch('https://api.deepgram.com/v1/models', {
|
||||
headers: {
|
||||
'Authorization': `Token ${api_key}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.error({response}, 'Error fetching Deepgram voices');
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
const {stt, tts, languages} = await response.json();
|
||||
// Helper function to get language name
|
||||
const getLanguageName = (langCode) => {
|
||||
if (languages && languages[langCode]) {
|
||||
return languages[langCode];
|
||||
}
|
||||
const existingLang = SttDeepgramLanguagesVoices.find((l) => l.value === langCode);
|
||||
return existingLang ? existingLang.name : capitalizeFirst(langCode);
|
||||
};
|
||||
// Collect unique languages from selected models
|
||||
const allSttLanguages = new Set();
|
||||
const modelsToProcess = model_id ?
|
||||
stt.filter((m) => m.canonical_name === model_id) :
|
||||
stt;
|
||||
modelsToProcess.forEach((model) => {
|
||||
if (model.languages && Array.isArray(model.languages)) {
|
||||
model.languages.forEach((lang) => allSttLanguages.add(lang));
|
||||
}
|
||||
});
|
||||
// Convert to expected format
|
||||
const sttLangs = Array.from(allSttLanguages).map((langCode) => ({
|
||||
name: getLanguageName(langCode),
|
||||
value: langCode
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const sttModels = Array.from(
|
||||
new Map(
|
||||
stt.map((m) => [m.canonical_name, { name: capitalizeFirst(m.canonical_name), value: m.canonical_name }])
|
||||
).values()
|
||||
).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const ttsModels = Array.from(
|
||||
new Map(
|
||||
tts.map((m) => [m.canonical_name, { name: capitalizeFirst(m.canonical_name), value: m.canonical_name }])
|
||||
).values()
|
||||
).sort((a, b) => a.name.localeCompare(b.name));
|
||||
return tranform(TtsLanguagesDeepgram, sttLangs, ttsModels, sttModels);
|
||||
}
|
||||
}
|
||||
return tranform(TtsLanguagesDeepgram, SttDeepgramLanguagesVoices,
|
||||
TtsModelDeepgram, sttModelDeepgram.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForGladia(credential, getTtsVoices, logger) {
|
||||
return tranform(undefined, SttGladiaLanguagesVoices.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
undefined, undefined);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForIbm(credential, getTtsVoices, logger) {
|
||||
@@ -885,14 +1164,29 @@ async function getLanguagesVoicesForSpeechmatics(credential) {
|
||||
|
||||
async function getLanguagesVoicesForElevenlabs(credential) {
|
||||
if (credential) {
|
||||
const get = bent('https://api.elevenlabs.io', 'GET', 'json', {
|
||||
'xi-api-key' : credential.api_key
|
||||
const headers = {
|
||||
'xi-api-key': credential.api_key
|
||||
};
|
||||
|
||||
const api_uri = credential.api_uri || 'api.elevenlabs.io';
|
||||
|
||||
const getModelPromise = fetch(`https://${api_uri}/v1/models`, {
|
||||
headers
|
||||
});
|
||||
const getVoicePromise = fetch(`https://${api_uri}/v1/voices`, {
|
||||
headers
|
||||
});
|
||||
const [langResp, voiceResp] = await Promise.all([getModelPromise, getVoicePromise]);
|
||||
|
||||
const [langResp, voiceResp] = await Promise.all([get('/v1/models'), get('/v1/voices')]);
|
||||
if (!langResp.ok || !voiceResp.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
|
||||
const model = langResp.find((m) => m.model_id === credential.model_id);
|
||||
const models = langResp.map((m) => {
|
||||
const langs = await langResp.json();
|
||||
const voicesR = await voiceResp.json();
|
||||
|
||||
const model = langs.find((m) => m.model_id === credential.model_id);
|
||||
const models = langs.map((m) => {
|
||||
return {
|
||||
value: m.model_id,
|
||||
name: m.name
|
||||
@@ -908,7 +1202,7 @@ async function getLanguagesVoicesForElevenlabs(credential) {
|
||||
|
||||
if (languages && languages.length > 0) {
|
||||
// using if condition to avoid \n character in name
|
||||
const voices = voiceResp ? voiceResp.voices.map((v) => {
|
||||
const voices = voicesR ? voicesR.voices.map((v) => {
|
||||
let name = `${v.name}${v.category !== 'premade' ? ` (${v.category.trim()})` : ''} - (`;
|
||||
if (v.labels.accent) name += `${v.labels.accent}, `;
|
||||
if (v.labels.description) name += `${v.labels.description}, `;
|
||||
@@ -946,18 +1240,28 @@ const concat = (a) => {
|
||||
|
||||
const fetchLayHTVoices = async(credential) => {
|
||||
if (credential) {
|
||||
const get = bent('https://api.play.ht', 'GET', 'json', {
|
||||
'AUTHORIZATION' : credential.api_key,
|
||||
const headers = {
|
||||
'AUTHORIZATION': credential.api_key,
|
||||
'X-USER-ID': credential.user_id,
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
const response = await fetch('https://api.play.ht/api/v2/voices', {
|
||||
headers
|
||||
});
|
||||
|
||||
const voices = await get('/api/v2/voices');
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
const voices = await response.json();
|
||||
let clone_voices = [];
|
||||
try {
|
||||
// try if the account has permission to cloned voice
|
||||
//otherwise ignore this.
|
||||
clone_voices = await get('/api/v2/cloned-voices');
|
||||
const clone_voices_Response = await fetch('https://api.play.ht/api/v2/cloned-voices', {
|
||||
headers
|
||||
});
|
||||
if (clone_voices_Response.ok) {
|
||||
clone_voices = await clone_voices_Response.json();
|
||||
}
|
||||
} catch {}
|
||||
return [clone_voices, voices];
|
||||
}
|
||||
@@ -1032,27 +1336,76 @@ async function getLanguagesVoicesForPlayHT(credential) {
|
||||
|
||||
async function getLanguagesVoicesForRimelabs(credential) {
|
||||
const model_id = credential ? credential.model_id : null;
|
||||
const get = bent('https://users.rime.ai', 'GET', 'json', {
|
||||
'Accept': 'application/json'
|
||||
const response = await fetch('https://users.rime.ai//data/voices/all-v2.json', {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
const voices = await get('/data/voices/all-v2.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list models');
|
||||
}
|
||||
const voices = await response.json();
|
||||
const modelVoices = model_id ? voices[model_id] :
|
||||
Object.keys(voices).length > 0 ? voices[Object.keys(voices)[0]] : [];
|
||||
const ttsVoices = Object.entries(modelVoices).map(([key, voices]) => ({
|
||||
value: key,
|
||||
name: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
name: capitalizeFirst(key),
|
||||
voices: voices.map((v) => ({
|
||||
name: v.charAt(0).toUpperCase() + v.slice(1),
|
||||
name: capitalizeFirst(v),
|
||||
value: v
|
||||
}))
|
||||
}));
|
||||
return tranform(ttsVoices, undefined, TtsModelRimelabs);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForInworld(credential) {
|
||||
const api_key = credential ? credential.api_key : null;
|
||||
if (!api_key) {
|
||||
return tranform(TtsInworldLanguagesVoices, undefined, TtsModelInworld);
|
||||
}
|
||||
const response = await fetch('https://api.inworld.ai/tts/v1/voices', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Basic ${api_key}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list models');
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const ttsVoices = data.voices.reduce((acc, voice) => {
|
||||
// Process each language for this voice
|
||||
voice.languages.forEach((languageCode) => {
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
const voiceEntry = {
|
||||
name: voice.displayName || capitalizeFirst(voice.voiceId),
|
||||
value: voice.voiceId
|
||||
};
|
||||
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push(voiceEntry);
|
||||
} else {
|
||||
acc.push({
|
||||
value: languageCode,
|
||||
name: capitalizeFirst(languageCode),
|
||||
voices: [voiceEntry]
|
||||
});
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
return tranform(ttsVoices, undefined, TtsModelInworld);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForAssemblyAI(credential) {
|
||||
return tranform(undefined, SttAssemblyaiLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForHoundify(credential) {
|
||||
return tranform(undefined, SttHoundifyLanguagesVoices);
|
||||
}
|
||||
|
||||
async function getLanguagesVoicesForVoxist(credential) {
|
||||
return tranform(undefined, SttVoxistLanguagesVoices);
|
||||
}
|
||||
@@ -1085,6 +1438,82 @@ async function getLanguagesVoicesForVerbio(credentials, getTtsVoices, logger) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getLanguagesAndVoicesForResemble(credential, getTtsVoices, logger) {
|
||||
if (credential) {
|
||||
try {
|
||||
const {api_key} = credential;
|
||||
let allVoices = [];
|
||||
let page = 1;
|
||||
let hasMorePages = true;
|
||||
// Fetch all pages of voices
|
||||
while (hasMorePages) {
|
||||
const response = await fetch(`https://app.resemble.ai/api/v2/voices?page=${page}&page_size=100`, {
|
||||
headers: {
|
||||
'Authorization': `Token token=${api_key}`,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error('API returned unsuccessful response');
|
||||
}
|
||||
|
||||
allVoices = allVoices.concat(data.items);
|
||||
|
||||
// Check if there are more pages
|
||||
hasMorePages = page < data.num_pages;
|
||||
page++;
|
||||
}
|
||||
|
||||
// Filter only finished voices that support text_to_speech
|
||||
const availableVoices = allVoices.filter((voice) =>
|
||||
voice.status === 'finished' &&
|
||||
voice.component_status?.text_to_speech?.status === 'ready'
|
||||
);
|
||||
|
||||
// Group voices by language
|
||||
const ttsVoices = availableVoices.reduce((acc, voice) => {
|
||||
const languageCode = voice.default_language || 'en-US';
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
|
||||
const voiceEntry = {
|
||||
name: `${voice.name} (${voice.voice_type}) - ${voice.source}`,
|
||||
value: voice.uuid
|
||||
};
|
||||
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push(voiceEntry);
|
||||
} else {
|
||||
|
||||
acc.push({
|
||||
value: languageCode,
|
||||
name: capitalizeFirst(languageCode),
|
||||
voices: [voiceEntry]
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
// Sort languages and voices
|
||||
ttsVoices.sort((a, b) => a.name.localeCompare(b.name));
|
||||
ttsVoices.forEach((lang) => {
|
||||
lang.voices.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
return tranform(ttsVoices);
|
||||
} catch (err) {
|
||||
logger.info('Error while fetching Resemble languages, voices, return predefined values', err);
|
||||
}
|
||||
}
|
||||
|
||||
return tranform(TtsResembleLanguagesVoices);
|
||||
}
|
||||
|
||||
function tranform(tts, stt, models, sttModels) {
|
||||
return {
|
||||
...(tts && {tts}),
|
||||
@@ -1098,11 +1527,11 @@ function parseGooglelanguagesVoices(data) {
|
||||
return data.reduce((acc, voice) => {
|
||||
const languageCode = voice.languageCodes[0];
|
||||
const existingLanguage = acc.find((lang) => lang.value === languageCode);
|
||||
|
||||
if (existingLanguage) {
|
||||
existingLanguage.voices.push({
|
||||
value: voice.name,
|
||||
name: `${voice.name.substring(languageCode.length + 1, voice.name.length)} (${voice.ssmlGender})`
|
||||
name: `${voice.name.startsWith(languageCode) ?
|
||||
voice.name.substring(languageCode.length + 1, voice.name.length) : voice.name} (${voice.ssmlGender})`
|
||||
});
|
||||
} else {
|
||||
acc.push({
|
||||
@@ -1110,7 +1539,8 @@ function parseGooglelanguagesVoices(data) {
|
||||
name: SttGoogleLanguagesVoices.find((lang) => lang.value === languageCode)?.name || languageCode,
|
||||
voices: [{
|
||||
value: voice.name,
|
||||
name: `${voice.name.substring(languageCode.length + 1, voice.name.length)} (${voice.ssmlGender})`
|
||||
name: `${voice.name.startsWith(languageCode) ?
|
||||
voice.name.substring(languageCode.length + 1, voice.name.length) : voice.name} (${voice.ssmlGender})`
|
||||
}]
|
||||
});
|
||||
}
|
||||
@@ -1238,14 +1668,18 @@ function parseVerbioLanguagesVoices(data) {
|
||||
|
||||
const fetchCartesiaVoices = async(credential) => {
|
||||
if (credential) {
|
||||
const get = bent('https://api.cartesia.ai', 'GET', 'json', {
|
||||
'X-API-Key' : credential.api_key,
|
||||
'Cartesia-Version': '2024-06-10',
|
||||
'Accept': 'application/json'
|
||||
const response = await fetch('https://api.cartesia.ai/voices', {
|
||||
headers: {
|
||||
'X-API-Key': credential.api_key,
|
||||
'Cartesia-Version': '2024-06-10',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('failed to list voices');
|
||||
}
|
||||
|
||||
const voices = await get('/voices');
|
||||
return voices;
|
||||
return await response.json();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1320,9 +1754,23 @@ async function getLanguagesVoicesForCartesia(credential) {
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return tranform(ttsVoices, undefined, TtsModelCartesia);
|
||||
return tranform(
|
||||
ttsVoices,
|
||||
ttsVoices.map((voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
})),
|
||||
TtsModelCartesia,
|
||||
sttModelCartesia);
|
||||
}
|
||||
return tranform(ttsCartesia, undefined, TtsModelCartesia);
|
||||
return tranform(
|
||||
ttsCartesia,
|
||||
ttsCartesia.map((voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
})),
|
||||
TtsModelCartesia,
|
||||
sttModelCartesia);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -1337,12 +1785,14 @@ module.exports = {
|
||||
testNuanceTts,
|
||||
testNuanceStt,
|
||||
testDeepgramStt,
|
||||
testGladiaStt,
|
||||
testIbmTts,
|
||||
testIbmStt,
|
||||
testSonioxStt,
|
||||
testElevenlabs,
|
||||
testPlayHT,
|
||||
testRimelabs,
|
||||
testInworld,
|
||||
testAssemblyStt,
|
||||
testDeepgramTTS,
|
||||
getSpeechCredential,
|
||||
@@ -1354,5 +1804,7 @@ module.exports = {
|
||||
testSpeechmaticsStt,
|
||||
testCartesia,
|
||||
testVoxistStt,
|
||||
testOpenAiStt
|
||||
testOpenAiStt,
|
||||
testResembleTTS,
|
||||
testHoundifyStt
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ assert.ok(process.env.STRIPE_API_KEY || process.env.NODE_ENV === 'test',
|
||||
assert.ok(process.env.STRIPE_BASE_URL || process.env.NODE_ENV === 'test',
|
||||
'missing env STRIPE_BASE_URL for billing operations');
|
||||
|
||||
const bent = require('bent');
|
||||
const formurlencoded = require('form-urlencoded');
|
||||
const qs = require('qs');
|
||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
@@ -14,10 +13,47 @@ const basicAuth = () => {
|
||||
const header = `Basic ${toBase64(process.env.STRIPE_API_KEY)}`;
|
||||
return {Authorization: header};
|
||||
};
|
||||
const postForm = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'POST', 'string',
|
||||
Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, basicAuth()), 200);
|
||||
const getJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'GET', 'json', basicAuth(), 200);
|
||||
const deleteJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'DELETE', 'json', basicAuth(), 200);
|
||||
const STRIPE_BASE_URL = process.env.STRIPE_BASE_URL || 'http://127.0.0.1';
|
||||
const getJSON = async(path) => {
|
||||
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...basicAuth()
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error retrieving ${path} from stripe: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
const postForm = async(path, body) => {
|
||||
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...basicAuth()
|
||||
},
|
||||
body
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error posting to ${path} from stripe: ${response.status}`);
|
||||
}
|
||||
return await response.text();
|
||||
};
|
||||
const deleteJSON = async(path) => {
|
||||
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...basicAuth()
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error deleting ${path} from stripe: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
};
|
||||
//const debug = require('debug')('jambonz:api-server');
|
||||
|
||||
const listProducts = async(logger) => await getJSON('/products?active=true');
|
||||
|
||||
5883
package-lock.json
generated
5883
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-api-server",
|
||||
"version": "0.9.3",
|
||||
"version": "0.9.5",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
@@ -25,26 +25,29 @@
|
||||
"@aws-sdk/client-transcribe": "^3.549.0",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@deepgram/sdk": "^1.21.0",
|
||||
"@gladiaio/sdk": "^0.5.2",
|
||||
"@google-cloud/speech": "^6.5.0",
|
||||
"@google-cloud/storage": "^7.9.0",
|
||||
"@jambonz/db-helpers": "^0.9.3",
|
||||
"@jambonz/db-helpers": "^0.9.18",
|
||||
"@jambonz/lamejs": "^1.2.2",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
||||
"@jambonz/speech-utils": "^0.2.3",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.15",
|
||||
"@jambonz/speech-utils": "^0.2.30",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.72",
|
||||
"@jambonz/verb-specifications": "^0.0.122",
|
||||
"@soniox/soniox-node": "^1.2.2",
|
||||
"ajv": "^8.17.1",
|
||||
"argon2": "^0.40.1",
|
||||
"assemblyai": "^4.3.4",
|
||||
"bent": "^7.3.12",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.4",
|
||||
"express": "^4.19.2",
|
||||
"debug": "^4.4.3",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^7.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"houndify": "^3.1.14",
|
||||
"ibm-watson": "^9.0.1",
|
||||
"is-valid-hostname": "^1.0.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mailgun.js": "^10.2.1",
|
||||
"microsoft-cognitiveservices-speech-sdk": "1.36.0",
|
||||
@@ -69,8 +72,6 @@
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"husky": "9.0.11",
|
||||
"nyc": "^15.1.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.9",
|
||||
"tape": "^5.7.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const SP_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734ds';
|
||||
const authSP = {bearer: ADMIN_TOKEN};
|
||||
const ACC_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734da';
|
||||
const authAcc = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {
|
||||
@@ -269,6 +266,19 @@ test('account tests', async(t) => {
|
||||
t.ok(err.statusCode === 400, 'returns 400 bad request if account sid param is not a valid uuid');
|
||||
}
|
||||
|
||||
/* try to fetch Alerts with an invalid account SID */
|
||||
try {
|
||||
result = await request.get(`/Accounts/INVALID/Alerts?page=1&count=1`, {
|
||||
auth: {bearer: accountLevelToken},
|
||||
resolveWithFullResponse: true,
|
||||
json: true
|
||||
});
|
||||
t.fail('Expected request to fail with invalid account SID');
|
||||
console.log(result)
|
||||
} catch (err) {
|
||||
t.ok(err.statusCode === 400, 'returns 400 bad request if account sid param is not a valid uuid');
|
||||
}
|
||||
|
||||
/* query all limits for an account */
|
||||
result = await request.get(`/Accounts/${sid}/Limits`, {
|
||||
auth: authAdmin,
|
||||
@@ -340,6 +350,7 @@ test('account tests', async(t) => {
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
//t.end();
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, createServiceProvider,
|
||||
@@ -284,3 +285,177 @@ test('application tests', async(t) => {
|
||||
}
|
||||
});
|
||||
|
||||
test('application query optimization tests', async(t) => {
|
||||
const app = require('../app');
|
||||
try {
|
||||
let result;
|
||||
|
||||
/* Create multiple service providers and accounts to test filtering */
|
||||
const voip_carrier_sid = await createVoipCarrier(request, 'test-carrier-apps');
|
||||
|
||||
const service_provider_sid_1 = await createServiceProvider(request, 'test-sp-1');
|
||||
const service_provider_sid_2 = await createServiceProvider(request, 'test-sp-2');
|
||||
|
||||
const account_sid_1 = await createAccount(request, service_provider_sid_1, 'test-account-1');
|
||||
const account_sid_2 = await createAccount(request, service_provider_sid_2, 'test-account-2');
|
||||
|
||||
/* Create applications for different accounts */
|
||||
const app1_result = await request.post('/Applications', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'test-app-account-1',
|
||||
account_sid: account_sid_1,
|
||||
call_hook: { url: 'http://example.com/app1' },
|
||||
call_status_hook: { url: 'http://example.com/app1/status' }
|
||||
}
|
||||
});
|
||||
const app1_sid = app1_result.sid;
|
||||
|
||||
const app2_result = await request.post('/Applications', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'test-app-account-2',
|
||||
account_sid: account_sid_2,
|
||||
call_hook: { url: 'http://example.com/app2' },
|
||||
call_status_hook: { url: 'http://example.com/app2/status' }
|
||||
}
|
||||
});
|
||||
const app2_sid = app2_result.sid;
|
||||
|
||||
const app3_result = await request.post('/Applications', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'another-app-account-1',
|
||||
account_sid: account_sid_1,
|
||||
call_hook: { url: 'http://example.com/app3' },
|
||||
call_status_hook: { url: 'http://example.com/app3/status' }
|
||||
}
|
||||
});
|
||||
const app3_sid = app3_result.sid;
|
||||
|
||||
/* Test 1: Query all applications as admin (no filter) - tests WHERE 1=1 fallback */
|
||||
result = await request.get('/Applications', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length >= 3, 'admin can see all applications using WHERE 1=1');
|
||||
const ourApps = result.filter(app =>
|
||||
[app1_sid, app2_sid, app3_sid].includes(app.application_sid)
|
||||
);
|
||||
t.ok(ourApps.length === 3, 'all three test applications are included in results');
|
||||
|
||||
/* Test 2: Query applications with name filter (LIKE query) - tests WHERE name LIKE optimization */
|
||||
result = await request.get('/Applications', {
|
||||
qs: { name: 'test-app' },
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 2, 'successfully filtered applications by name prefix');
|
||||
t.ok(result.every(app => app.name.includes('test-app')), 'all results match name filter');
|
||||
|
||||
/* Test 3: Query applications with exact name match - tests WHERE optimization */
|
||||
result = await request.get('/Applications', {
|
||||
qs: { name: 'test-app-account-1' },
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 1, 'successfully filtered applications by exact name');
|
||||
t.ok(result[0].name === 'test-app-account-1', 'exact name match works correctly');
|
||||
|
||||
/* Test 4: Query with name filter that matches nothing */
|
||||
result = await request.get('/Applications', {
|
||||
qs: { name: 'nonexistent-app-12345' },
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 0, 'non-matching name filter returns empty array');
|
||||
|
||||
/* Test 5: Query with pagination and name filter - tests countAll optimization */
|
||||
result = await request.get('/Applications', {
|
||||
qs: {
|
||||
name: 'test-app',
|
||||
page: 1,
|
||||
page_size: 10
|
||||
},
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 2, 'pagination with name filter returns correct count');
|
||||
t.ok(result.total === 2, 'countAll with name filter returns correct total');
|
||||
t.ok(result.page === 1, 'pagination returns correct page number');
|
||||
t.ok(result.page_size === 10, 'pagination returns correct page size');
|
||||
|
||||
/* Test 6: Query with pagination and no filter - tests WHERE 1=1 fallback */
|
||||
result = await request.get('/Applications', {
|
||||
qs: {
|
||||
page: 1,
|
||||
page_size: 2
|
||||
},
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 2, 'pagination without filter returns page_size results');
|
||||
t.ok(result.total >= 3, 'pagination without filter uses WHERE 1=1 and returns all');
|
||||
|
||||
/* Test 7: Create SP-scoped token and verify WHERE service_provider_sid optimization */
|
||||
const sp1_token_result = await request.post('/ApiKeys', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
service_provider_sid: service_provider_sid_1
|
||||
}
|
||||
});
|
||||
const sp1_token = sp1_token_result.token;
|
||||
const sp1_token_sid = sp1_token_result.sid;
|
||||
|
||||
result = await request.get('/Applications', {
|
||||
auth: {bearer: sp1_token},
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 2, 'SP-scoped token sees only their applications via WHERE service_provider_sid');
|
||||
t.ok(result.every(app => app.account_sid === account_sid_1), 'all apps belong to SP1 accounts');
|
||||
|
||||
/* Test 8: SP-scoped token with name filter - tests combined WHERE clause */
|
||||
result = await request.get('/Applications', {
|
||||
qs: { name: 'test-app' },
|
||||
auth: {bearer: sp1_token},
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 1, 'SP-scoped token with name filter combines filters correctly');
|
||||
t.ok(result[0].name === 'test-app-account-1', 'combined filter returns correct app');
|
||||
|
||||
/* Test 9: SP-scoped token with pagination - tests countAll with service_provider_sid */
|
||||
result = await request.get('/Applications', {
|
||||
qs: {
|
||||
page: 1,
|
||||
page_size: 10
|
||||
},
|
||||
auth: {bearer: sp1_token},
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 2, 'SP-scoped pagination returns correct count');
|
||||
t.ok(result.total === 2, 'countAll with service_provider_sid returns correct total');
|
||||
|
||||
/* Cleanup tokens */
|
||||
await deleteObjectBySid(request, '/ApiKeys', sp1_token_sid);
|
||||
|
||||
/* Cleanup */
|
||||
await deleteObjectBySid(request, '/Applications', app1_sid);
|
||||
await deleteObjectBySid(request, '/Applications', app2_sid);
|
||||
await deleteObjectBySid(request, '/Applications', app3_sid);
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid_1);
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid_2);
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid_1);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid_2);
|
||||
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
t.end(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, createServiceProvider, createPhoneNumber, createAccount, deleteObjectBySid} = require('./utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const test = require('tape');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const consoleLogger = { debug: console.log, info: console.log, error: console.error }
|
||||
@@ -47,8 +48,7 @@ test('Create Call Success With Synthesizer in Payload', async (t) => {
|
||||
});
|
||||
// THEN
|
||||
t.ok(result.statusCode === 201, 'successfully created Call without Synthesizer && application_sid');
|
||||
const fs_request = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
const obj = JSON.parse(fs_request);
|
||||
const obj = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
t.ok(obj.body.speech_synthesis_vendor == 'google', 'speech synthesizer successfully added')
|
||||
t.ok(obj.body.speech_recognizer_vendor == 'google', 'speech recognizer successfully added')
|
||||
});
|
||||
@@ -82,7 +82,7 @@ test('Create Call Success Without Synthesizer in Payload', async (t) => {
|
||||
}
|
||||
}
|
||||
}).then(data => { t.ok(false, 'Create Call should not be success') })
|
||||
.catch(error => { t.ok(error.response.statusCode === 400, 'Call failed for no synthesizer') });
|
||||
.catch(error => { t.ok(error.statusCode === 400, 'Call failed for no synthesizer') });
|
||||
});
|
||||
|
||||
test("Create call with application sid and app_json", async(t) => {
|
||||
@@ -150,7 +150,6 @@ result = await request.post(`/Accounts/${account_sid}/Calls`, {
|
||||
});
|
||||
// THEN
|
||||
t.ok(result.statusCode === 201, 'successfully created Call without Synthesizer && application_sid');
|
||||
const fs_request = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
const obj = JSON.parse(fs_request);
|
||||
const obj = await getLastRequestFromFeatureServer('15083778299_createCall');
|
||||
t.ok(obj.body.app_json == app_json, 'app_json successfully added')
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const { createClient } = require('./http-client');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -32,3 +32,11 @@ test('add predefined carriers', (t) => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('prepare permissions', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('permissions prepared');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const test = require('tape');
|
||||
const {emailSimpleText} = require('../lib/utils/email-utils');
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('json')
|
||||
const logger = {
|
||||
debug: () =>{},
|
||||
info: () => {}
|
||||
@@ -15,7 +13,7 @@ test('email-test', async(t) => {
|
||||
|
||||
await emailSimpleText(logger, 'test@gmail.com', 'subject', 'body text');
|
||||
|
||||
const obj = await getJSON(`http://127.0.0.1:3101/lastRequest/custom_email_vendor`);
|
||||
const obj = await (await fetch(`http://127.0.0.1:3101/lastRequest/custom_email_vendor`)).json();
|
||||
t.ok(obj.headers['Content-Type'] == 'application/json');
|
||||
t.ok(obj.headers.Authorization == 'Basic VVNFUk5BTUU6UEFTU1dPUkQ=');
|
||||
t.ok(obj.body.from == 'jambonz Support <support@jambonz.org>');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const test = require('tape');
|
||||
const request = require("request-promise-native").defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: "http://127.0.0.1:3000/v1",
|
||||
});
|
||||
|
||||
|
||||
172
test/http-client.js
Normal file
172
test/http-client.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Fetch-based HTTP client that mimics the request-promise-native API
|
||||
*/
|
||||
|
||||
class HttpClient {
|
||||
constructor(defaults = {}) {
|
||||
this.defaults = defaults;
|
||||
this.baseUrl = defaults.baseUrl || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP GET request
|
||||
*/
|
||||
async get(url, options = {}) {
|
||||
return this._makeRequest(url, 'GET', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP POST request
|
||||
*/
|
||||
async post(url, options = {}) {
|
||||
return this._makeRequest(url, 'POST', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP PUT request
|
||||
*/
|
||||
async put(url, options = {}) {
|
||||
return this._makeRequest(url, 'PUT', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP DELETE request
|
||||
*/
|
||||
async delete(url, options = {}) {
|
||||
return this._makeRequest(url, 'DELETE', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to handle all HTTP requests
|
||||
*/
|
||||
async _makeRequest(url, method, options = {}) {
|
||||
const {
|
||||
auth,
|
||||
body,
|
||||
json = true, // Changed default to true since most API calls expect JSON
|
||||
qs = {},
|
||||
simple = true,
|
||||
resolveWithFullResponse = false
|
||||
} = options;
|
||||
|
||||
// Build full URL with query parameters
|
||||
const fullUrl = this._buildUrl(url, qs);
|
||||
|
||||
// Set up headers
|
||||
const headers = {};
|
||||
if (auth?.bearer) {
|
||||
headers['Authorization'] = `Bearer ${auth.bearer}`;
|
||||
}
|
||||
|
||||
// Set JSON headers for all requests when json is true
|
||||
if (json) {
|
||||
headers['Accept'] = 'application/json';
|
||||
|
||||
// Only set Content-Type when sending data
|
||||
if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
// Build request options
|
||||
const fetchOptions = {
|
||||
method,
|
||||
headers
|
||||
};
|
||||
|
||||
// Add request body if needed
|
||||
if (body && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
||||
fetchOptions.body = json ? JSON.stringify(body) : body;
|
||||
}
|
||||
|
||||
try {
|
||||
// Make the request
|
||||
const response = await fetch(fullUrl, fetchOptions);
|
||||
|
||||
// Clone the response before consuming it
|
||||
// This allows us to use the body in error handling if needed
|
||||
const clonedResponse = response.clone();
|
||||
|
||||
// Parse response body based on content type - only once
|
||||
let responseBody = null;
|
||||
if (response.status !== 204) { // No content
|
||||
if (json) {
|
||||
try {
|
||||
responseBody = await response.json();
|
||||
} catch (e) {
|
||||
// If can't parse JSON, get text
|
||||
responseBody = await clonedResponse.text();
|
||||
}
|
||||
} else {
|
||||
responseBody = await response.text();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors if simple mode is enabled
|
||||
if (simple && !response.ok) {
|
||||
const error = new Error(`Request failed with status code ${response.status}`);
|
||||
error.statusCode = response.status;
|
||||
error.body = responseBody; // Include the already parsed body
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Return full response object or just body based on options
|
||||
if (resolveWithFullResponse) {
|
||||
return {
|
||||
statusCode: response.status,
|
||||
body: responseBody,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
};
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
} catch (error) {
|
||||
if (!simple) {
|
||||
// If simple=false, return error response instead of throwing
|
||||
return {
|
||||
statusCode: error.statusCode || 500,
|
||||
body: error.body || { message: error.message }
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Build URL with query parameters
|
||||
*/
|
||||
_buildUrl(url, qs) {
|
||||
// Start with base URL
|
||||
let fullUrl = this.baseUrl + url;
|
||||
|
||||
// Add query parameters
|
||||
if (Object.keys(qs).length > 0) {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(qs).forEach(([key, value]) => {
|
||||
params.append(key, value);
|
||||
});
|
||||
fullUrl += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
return fullUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client with default options
|
||||
*/
|
||||
function createClient(defaults = {}) {
|
||||
const client = new HttpClient(defaults);
|
||||
|
||||
// Return the methods directly for API compatibility
|
||||
return {
|
||||
get: (url, options) => client.get(url, options),
|
||||
post: (url, options) => client.post(url, options),
|
||||
put: (url, options) => client.put(url, options),
|
||||
delete: (url, options) => client.delete(url, options),
|
||||
defaults: client.defaults
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createClient
|
||||
};
|
||||
@@ -13,6 +13,7 @@ require('./ms-teams');
|
||||
require('./speech-credentials');
|
||||
require('./recent-calls');
|
||||
require('./users');
|
||||
require('./users-view-only');
|
||||
require('./login');
|
||||
require('./webapp_tests');
|
||||
// require('./homer');
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const { createClient } = require('./http-client');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const exec = require('child_process').exec ;
|
||||
const {generateHashedPassword} = require('../lib/utils/password-utils');
|
||||
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
@@ -52,7 +54,7 @@ test('login tests', async(t) => {
|
||||
password: 'adm',
|
||||
}
|
||||
}).catch(error => {
|
||||
t.ok(error.response.statusCode === 403, `Maximum login attempts reached. Please try again in ${attempTime} seconds.`)
|
||||
t.ok(error.statusCode === 403, `Maximum login attempts reached. Please try again in ${attempTime} seconds.`)
|
||||
});
|
||||
} else if (index < maxAttempts) {
|
||||
attemptResult = await request.post('/login', {
|
||||
@@ -62,7 +64,10 @@ test('login tests', async(t) => {
|
||||
username: 'admin',
|
||||
password: 'adm',
|
||||
}
|
||||
}).catch(error => t.ok(error.response.statusCode === 403));
|
||||
}).catch(error => {
|
||||
console.log(JSON.stringify(error));
|
||||
t.ok(error.statusCode === 403);
|
||||
});
|
||||
} else {
|
||||
attemptResult = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -71,7 +76,7 @@ test('login tests', async(t) => {
|
||||
username: 'admin',
|
||||
password: 'adm',
|
||||
}
|
||||
}).catch(error => t.ok(error.response.statusCode === 403, 'Maximum login attempts reached. Please try again later or reset your password.'));
|
||||
}).catch(error => t.ok(error.statusCode === 403, 'Maximum login attempts reached. Please try again later or reset your password.'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils');
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('GET', 200);
|
||||
const request = require('request');
|
||||
|
||||
const test = async() => {
|
||||
request.get('https://api.github.com/user', {
|
||||
fetch('https://api.github.com/user', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GH_CODE}`,
|
||||
Accept: 'application/json',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const { createClient } = require('./http-client');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, deleteObjectBySid} = require('./utils');
|
||||
|
||||
@@ -3,7 +3,8 @@ const fs = require('fs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const consoleLogger = {debug: console.log, info: console.log, error: console.error}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createServiceProvider, deleteObjectBySid} = require('./utils');
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {deleteObjectBySid} = require('./utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, deleteObjectBySid} = require('./utils');
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createVoipCarrier, deleteObjectBySid} = require('./utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
@@ -3,12 +3,14 @@ const fs = require('fs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils');
|
||||
const { noopLogger } = require('@jambonz/realtimedb-helpers/lib/utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
@@ -654,7 +656,8 @@ test('speech credentials tests', async(t) => {
|
||||
use_for_stt: true,
|
||||
use_for_tts: false,
|
||||
api_key: 'asdasdasdasddsadasda',
|
||||
model_id: 'eleven_multilingual_v2'
|
||||
model_id: 'eleven_multilingual_v2',
|
||||
api_uri: 'api.elevenlabs.io'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for elevenlabs');
|
||||
@@ -715,6 +718,28 @@ test('speech credentials tests', async(t) => {
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for rimelabs');
|
||||
|
||||
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'inworld',
|
||||
use_for_stt: false,
|
||||
use_for_tts: true,
|
||||
api_key: 'asdasdasdasddsadasda',
|
||||
model_id: 'inworld-tts-1',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for inworld');
|
||||
const inworld_sid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${inworld_sid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for inworld');
|
||||
|
||||
/* add a credential for custom voices google */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -767,7 +792,8 @@ test('speech credentials tests', async(t) => {
|
||||
body: {
|
||||
vendor: 'assemblyai',
|
||||
use_for_stt: true,
|
||||
api_key: "APIKEY"
|
||||
api_key: "APIKEY",
|
||||
service_version: 'v2'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for assemblyai');
|
||||
@@ -780,6 +806,29 @@ test('speech credentials tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for houndify */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'houndify',
|
||||
use_for_stt: true,
|
||||
client_key: "ClientKey",
|
||||
client_id: "ClientID",
|
||||
user_id: "test_user"
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for houndify');
|
||||
const houndifySid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${houndifySid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for Voxist */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -877,6 +926,72 @@ test('speech credentials tests', async(t) => {
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
|
||||
|
||||
/* add a credential for resemble */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'resemble',
|
||||
use_for_tts: true,
|
||||
use_for_stt: false,
|
||||
api_key: 'api_key',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Resemble');
|
||||
const resembleSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${resembleSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for Resemble');
|
||||
|
||||
/* add a credential for deepgram river */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'deepgramflux',
|
||||
use_for_tts: false,
|
||||
use_for_stt: true,
|
||||
api_key: 'api_key',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Verbio');
|
||||
const deepgramfluxSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${deepgramfluxSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential deepgramflux');
|
||||
|
||||
/* add a credential for gladia */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'gladia',
|
||||
use_for_tts: false,
|
||||
use_for_stt: true,
|
||||
api_key: 'api_key',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for Gladia');
|
||||
const gladiaSid = result.body.sid;
|
||||
|
||||
/* delete the credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${gladiaSid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted speech credential for Gladia');
|
||||
|
||||
/* Check google supportedLanguagesAndVoices */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=google`, {
|
||||
resolveWithFullResponse: true,
|
||||
@@ -1012,6 +1127,124 @@ test('speech credentials tests', async(t) => {
|
||||
t.ok(result.body.tts.length !== 0, 'successfully get whisper supported languages and voices');
|
||||
t.ok(result.body.models.length !== 0, 'successfully get whisper supported languages and voices');
|
||||
|
||||
/* Check gladia supportedLanguagesAndVoices */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=gladia`, {
|
||||
resolveWithFullResponse: true,
|
||||
simple: false,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.body.stt.length !== 0, 'successfully get gladia supported languages and voices');
|
||||
|
||||
/* add a credential for google with model_id */
|
||||
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'google',
|
||||
label: 'google_gemini_tts',
|
||||
service_key: jsonKey,
|
||||
use_for_tts: true,
|
||||
use_for_stt: true,
|
||||
model_id: 'gemini-2.0-flash-exp'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added speech credential for google with model_id');
|
||||
const google_gemini_sid = result.body.sid;
|
||||
|
||||
/* query the credential and verify model_id are stored */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${google_gemini_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200, 'successfully retrieved google gemini speech credential');
|
||||
t.ok(result.body.vendor === 'google', 'vendor is google');
|
||||
t.ok(result.body.label === 'google_gemini_tts', 'label is correct');
|
||||
t.ok(result.body.model_id === 'gemini-2.0-flash-exp', 'model_id is correct');
|
||||
|
||||
/* update the credential to change model_id */
|
||||
result = await request.put(`/Accounts/${account_sid}/SpeechCredentials/${google_gemini_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
use_for_tts: true,
|
||||
use_for_stt: true,
|
||||
model_id: 'gemini-2.5-flash-preview-native-audio'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated google gemini speech credential');
|
||||
|
||||
/* verify the update */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${google_gemini_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200, 'successfully retrieved updated google gemini speech credential');
|
||||
t.ok(result.body.model_id === 'gemini-2.5-flash-preview-native-audio', 'model_id is updated correctly');
|
||||
|
||||
/* update the credential to disable gemini tts */
|
||||
result = await request.put(`/Accounts/${account_sid}/SpeechCredentials/${google_gemini_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authUser,
|
||||
json: true,
|
||||
body: {
|
||||
use_for_tts: true,
|
||||
use_for_stt: true,
|
||||
model_id: null
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated google speech credential to disable gemini tts');
|
||||
|
||||
/* verify the update to disable gemini tts */
|
||||
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${google_gemini_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200, 'successfully retrieved google speech credential after disabling gemini');
|
||||
t.ok(!result.body.model_id, 'model_id is now null');
|
||||
|
||||
/* delete the google gemini credential */
|
||||
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${google_gemini_sid}`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully deleted google gemini speech credential');
|
||||
|
||||
/* add a credential for google at service provider level with gemini tts */
|
||||
result = await request.post(`/ServiceProviders/${service_provider_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
vendor: 'google',
|
||||
service_key: jsonKey,
|
||||
use_for_tts: true,
|
||||
use_for_stt: true,
|
||||
model_id: 'gemini-2.0-flash-exp'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added google gemini speech credential to service provider');
|
||||
const sp_google_gemini_sid = result.body.sid;
|
||||
|
||||
/* query the service provider credential */
|
||||
result = await request.get(`/ServiceProviders/${service_provider_sid}/SpeechCredentials`, {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.statusCode === 200, 'successfully queried service provider speech credentials');
|
||||
const spCred = result.body.find(c => c.speech_credential_sid === sp_google_gemini_sid);
|
||||
t.ok(spCred, 'found google gemini credential in service provider credentials');
|
||||
t.ok(spCred.model_id === 'gemini-2.0-flash-exp', 'model_id is correct for SP credential');
|
||||
|
||||
/* delete the service provider google gemini credential */
|
||||
await deleteObjectBySid(request, `/ServiceProviders/${service_provider_sid}/SpeechCredentials`, sp_google_gemini_sid);
|
||||
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
t.end();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { createClient } = require('./http-client');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const crypto = require('crypto');
|
||||
|
||||
298
test/users-view-only.js
Normal file
298
test/users-view-only.js
Normal file
@@ -0,0 +1,298 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const exec = require('child_process').exec ;
|
||||
const {generateHashedPassword} = require('../lib/utils/password-utils');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
test('add an admin user', (t) => {
|
||||
exec(`${__dirname}/../db/reset_admin_password.js`, (err, stdout, stderr) => {
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
if (err) return t.end(err);
|
||||
t.pass('successfully added admin user');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('prepare permissions', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('permissions prepared');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('view-only user tests', async(t) => {
|
||||
const app = require('../app');
|
||||
const password = 'abcde12345-';
|
||||
try {
|
||||
let result;
|
||||
/* login as admin to get a jwt */
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as admin');
|
||||
const authAdmin = {bearer: result.body.token};
|
||||
const decodedJwt = jwt.verify(result.body.token, process.env.JWT_SECRET);
|
||||
/* add admin user */
|
||||
result = await request.post(`/Users`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
name: 'admin2',
|
||||
email: 'admin2@jambonz.com',
|
||||
is_active: true,
|
||||
force_change: true,
|
||||
initial_password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201 && result.body.user_sid, 'admin user created');
|
||||
const admin_user_sid = result.body.user_sid;
|
||||
/* add a service provider */
|
||||
result = await request.post('/ServiceProviders', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'sp1',
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created service provider');
|
||||
const sp_sid = result.body.sid;
|
||||
/* add service_provider read only user */
|
||||
result = await request.post(`/Users`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
name: 'service_provider',
|
||||
email: 'sp@jambonz.com',
|
||||
is_active: true,
|
||||
force_change: true,
|
||||
initial_password: password,
|
||||
service_provider_sid: sp_sid,
|
||||
is_view_only: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201 && result.body.user_sid, 'service_provider scope view-only user created');
|
||||
|
||||
// login as service_provider read only user
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'service_provider',
|
||||
password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as service provider view-only user');
|
||||
const spToken = {bearer: result.body.token};
|
||||
const spDecodedJwt = jwt.verify(result.body.token, process.env.JWT_SECRET);
|
||||
try {
|
||||
result = await request.post('/Accounts', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: spToken,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'sample_account',
|
||||
service_provider_sid: sp_sid,
|
||||
registration_hook: {
|
||||
url: 'http://example.com/reg',
|
||||
method: 'get'
|
||||
},
|
||||
webhook_secret: 'foobar'
|
||||
}
|
||||
})
|
||||
} catch(err) {
|
||||
t.ok(err.statusCode === 403, 'As a view-only user, you cannot create an account');
|
||||
}
|
||||
result = await request.post('/Accounts', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'sample_account',
|
||||
service_provider_sid: sp_sid,
|
||||
registration_hook: {
|
||||
url: 'http://example.com/reg',
|
||||
method: 'get'
|
||||
},
|
||||
webhook_secret: 'foobar'
|
||||
}
|
||||
})
|
||||
t.ok(result.statusCode === 201, 'successfully created account using admin token');
|
||||
const account_sid = result.body.sid;
|
||||
/* add account scope view-only user */
|
||||
result = await request.post(`/Users`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
name: 'account',
|
||||
email: 'account@jambonz.com',
|
||||
is_active: true,
|
||||
force_change: true,
|
||||
initial_password: password,
|
||||
service_provider_sid: sp_sid,
|
||||
account_sid: account_sid,
|
||||
is_view_only: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201 && result.body.user_sid, 'account scope user created');
|
||||
const account_user_sid = result.body.user_sid;
|
||||
// login as account read only user
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'account',
|
||||
password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account view-only user');
|
||||
let userToken = {bearer: result.body.token};
|
||||
/* add an application which should fail as the logged in user is a view-only user */
|
||||
try {
|
||||
result = await request.post('/Applications', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: userToken,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'daveh',
|
||||
account_sid,
|
||||
call_hook: {
|
||||
url: 'http://example.com'
|
||||
},
|
||||
call_status_hook: {
|
||||
url: 'http://example.com/status',
|
||||
method: 'POST'
|
||||
},
|
||||
messaging_hook: {
|
||||
url: 'http://example.com/sms'
|
||||
},
|
||||
app_json : '[\
|
||||
{\
|
||||
"verb": "play",\
|
||||
"url": "https://example.com/example.mp3",\
|
||||
"timeoutSecs": 10,\
|
||||
"seekOffset": 8000,\
|
||||
"actionHook": "/play/action"\
|
||||
}\
|
||||
]',
|
||||
use_for_fallback_speech: 1,
|
||||
fallback_speech_synthesis_vendor: 'google',
|
||||
fallback_speech_synthesis_language: 'en-US',
|
||||
fallback_speech_synthesis_voice: 'man',
|
||||
fallback_speech_synthesis_label: 'label1',
|
||||
fallback_speech_recognizer_vendor: 'google',
|
||||
fallback_speech_recognizer_language: 'en-US',
|
||||
fallback_speech_recognizer_label: 'label1'
|
||||
}
|
||||
});
|
||||
} catch(err) {
|
||||
t.ok(err.statusCode === 403, 'As a view-only user, you cannot create an application');
|
||||
}
|
||||
// change user as read/write user and create an application - it should succeed
|
||||
result = await request.put(`/Users/${account_user_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
is_view_only: false
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated user to read/write permissions');
|
||||
// login as account read only user
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'account',
|
||||
password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account read-write user');
|
||||
userToken = {bearer: result.body.token};
|
||||
/* add an application which should succeed as the logged in user is a read-write user */
|
||||
result = await request.post('/Applications', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: userToken,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'daveh',
|
||||
account_sid,
|
||||
call_hook: {
|
||||
url: 'http://example.com'
|
||||
},
|
||||
call_status_hook: {
|
||||
url: 'http://example.com/status',
|
||||
method: 'POST'
|
||||
},
|
||||
messaging_hook: {
|
||||
url: 'http://example.com/sms'
|
||||
},
|
||||
app_json : '[\
|
||||
{\
|
||||
"verb": "play",\
|
||||
"url": "https://example.com/example.mp3",\
|
||||
"timeoutSecs": 10,\
|
||||
"seekOffset": 8000,\
|
||||
"actionHook": "/play/action"\
|
||||
}\
|
||||
]',
|
||||
use_for_fallback_speech: 1,
|
||||
fallback_speech_synthesis_vendor: 'google',
|
||||
fallback_speech_synthesis_language: 'en-US',
|
||||
fallback_speech_synthesis_voice: 'man',
|
||||
fallback_speech_synthesis_label: 'label1',
|
||||
fallback_speech_recognizer_vendor: 'google',
|
||||
fallback_speech_recognizer_language: 'en-US',
|
||||
fallback_speech_recognizer_label: 'label1'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully created an application');
|
||||
// change user back to view-only and query the application - it should succeed
|
||||
result = await request.put(`/Users/${account_user_sid}`, {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
auth: authAdmin,
|
||||
body: {
|
||||
is_view_only: true
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 204, 'successfully updated user permission to view-only');
|
||||
// login as account read only user
|
||||
result = await request.post('/login', {
|
||||
resolveWithFullResponse: true,
|
||||
json: true,
|
||||
body: {
|
||||
username: 'account',
|
||||
password: password,
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 200 && result.body.token, 'successfully logged in as account view-only user');
|
||||
userToken = {bearer: result.body.token};
|
||||
result = await request.get('/Applications', {
|
||||
auth: userToken,
|
||||
json: true,
|
||||
});
|
||||
//console.log(`result: ${JSON.stringify(result)}`);
|
||||
t.ok(result.length === 1 , 'successfully queried all applications with view-only user');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
t.end(err);
|
||||
}
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
const test = require('tape') ;
|
||||
const jwt = require('jsonwebtoken');
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const exec = require('child_process').exec ;
|
||||
const {generateHashedPassword} = require('../lib/utils/password-utils');
|
||||
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
@@ -20,7 +21,13 @@ test('add an admin user', (t) => {
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('prepare permissions', (t) => {
|
||||
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/prepare-permissions-test.sql`, (err, stdout, stderr) => {
|
||||
if (err) return t.end(err);
|
||||
t.pass('permissions prepared');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
test('user tests', async(t) => {
|
||||
const app = require('../app');
|
||||
const password = 'abcde12345-';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { v4: uuid } = require('uuid');
|
||||
const fs = require('fs');
|
||||
const request_fs_mock = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request_fs_mock = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3100'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const test = require('tape') ;
|
||||
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
|
||||
const authAdmin = {bearer: ADMIN_TOKEN};
|
||||
const request = require('request-promise-native').defaults({
|
||||
const { createClient } = require('./http-client');
|
||||
const request = createClient({
|
||||
baseUrl: 'http://127.0.0.1:3000/v1'
|
||||
});
|
||||
const {createServiceProvider, createAccount, createApplication, deleteObjectBySid} = require('./utils');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user