Organize directories

This commit is contained in:
Darren Clarke 2023-02-13 13:10:48 +00:00
parent 8a91c9b89b
commit 4898382f78
433 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,124 @@
import * as Hapi from "@hapi/hapi";
import * as Joi from "joi";
import * as Boom from "@hapi/boom";
import * as R from "remeda";
import * as Helpers from "../helpers";
import Twilio from "twilio";
import { crudRoutesFor, CrudControllerBase } from "common";
import { VoiceLineRecord, SavedVoiceLine } from "db";
const TwilioHandlers = {
freeNumbers: async (provider, request: Hapi.Request) => {
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
const client = Twilio(apiKeySid, apiKeySecret, {
accountSid,
});
const numbers = R.pipe(
await client.incomingPhoneNumbers.list({ limit: 100 }),
R.filter((n) => n.capabilities.voice),
R.map(R.pick(["sid", "phoneNumber"]))
);
const numberSids = R.map(numbers, R.prop("sid"));
const voiceLineRepo = request.db().voiceLines;
const voiceLines: SavedVoiceLine[] =
await voiceLineRepo.findAllByProviderLineSids(numberSids);
const voiceLineSids = new Set(R.map(voiceLines, R.prop("providerLineSid")));
return R.pipe(
numbers,
R.reject((n) => voiceLineSids.has(n.sid)),
R.map((n) => ({ id: n.sid, name: n.phoneNumber }))
);
},
};
export const VoiceProviderRoutes = Helpers.withDefaults([
{
method: "GET",
path: "/api/voice/providers/{providerId}/freeNumbers",
options: {
description:
"get a list of the incoming numbers for a provider account that aren't assigned to a voice line",
validate: {
params: {
providerId: Joi.string().uuid().required(),
},
},
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { providerId } = request.params;
const voiceProvidersRepo = request.db().voiceProviders;
const provider = await voiceProvidersRepo.findById(providerId);
if (!provider) return Boom.notFound();
switch (provider.kind) {
case "TWILIO":
return TwilioHandlers.freeNumbers(provider, request);
default:
return Boom.badImplementation();
}
},
},
},
]);
class VoiceLineRecordController extends CrudControllerBase(VoiceLineRecord) { }
const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
create: {
payload: Joi.object({
providerType: Joi.string().required(),
providerId: Joi.string().required(),
number: Joi.string().required(),
language: Joi.string().required(),
voice: Joi.string().required(),
promptText: Joi.string().optional(),
promptRecording: Joi.binary()
.encoding("base64")
.max(50 * 1000 * 1000)
.optional(),
}).label("VoiceLineCreate"),
},
updateById: {
params: {
id: Joi.string().uuid().required(),
},
payload: Joi.object({
providerType: Joi.string().optional(),
providerId: Joi.string().optional(),
number: Joi.string().optional(),
language: Joi.string().optional(),
voice: Joi.string().optional(),
promptText: Joi.string().optional(),
promptRecording: Joi.binary()
.encoding("base64")
.max(50 * 1000 * 1000)
.optional(),
}).label("VoiceLineUpdate"),
},
deleteById: {
params: {
id: Joi.string().uuid().required(),
},
},
getById: {
params: {
id: Joi.string().uuid().required(),
},
},
});
export const VoiceLineRoutes = async (
_server: Hapi.Server
): Promise<Hapi.ServerRoute[]> => {
const controller = new VoiceLineRecordController("voiceLines", "id");
return Helpers.withDefaults(
crudRoutesFor(
"voice-line",
"/api/voice/voice-line",
controller,
"id",
validator()
)
);
};
export * from "./twilio";

View file

@ -0,0 +1,230 @@
import * as Hapi from "@hapi/hapi";
import * as Joi from "joi";
import * as Boom from "@hapi/boom";
import Twilio from "twilio";
import { SavedVoiceProvider } from "db";
import pMemoize from "p-memoize";
import ms from "ms";
import * as Helpers from "../../helpers";
import workerUtils from "../../../../worker-utils";
import { SayLanguage, SayVoice } from "twilio/lib/twiml/VoiceResponse";
const queueRecording = async (meta) => {
return workerUtils.addJob("twilio-recording", meta, { jobKey: meta.callSid });
};
const twilioClientFor = (provider: SavedVoiceProvider): Twilio.Twilio => {
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
if (!accountSid || !apiKeySid || !apiKeySecret)
throw new Error(
`twilio provider ${provider.name} does not have credentials`
);
return Twilio(apiKeySid, apiKeySecret, {
accountSid,
});
};
const _getOrCreateTTSTestApplication = async (
url,
name,
client: Twilio.Twilio
) => {
const application = await client.applications.list({ friendlyName: name });
if (application[0] && application[0].voiceUrl === url) {
return application[0];
}
return client.applications.create({
voiceMethod: "POST",
voiceUrl: url,
friendlyName: name,
});
};
const getOrCreateTTSTestApplication = pMemoize(_getOrCreateTTSTestApplication, {
maxAge: ms("1h"),
});
export const TwilioRoutes = Helpers.noAuth([
{
method: "get",
path: "/api/voice/twilio/prompt/{voiceLineId}",
options: {
description: "download the mp3 file to play as a prompt for the user",
validate: {
params: {
voiceLineId: Joi.string().uuid().required(),
},
},
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const { voiceLineId } = request.params;
const voiceLine = await request
.db()
.voiceLines.findById({ id: voiceLineId });
if (!voiceLine) return Boom.notFound();
if (!voiceLine.audioPromptEnabled) return Boom.badRequest();
const mp3 = voiceLine.promptAudio["audio/mpeg"];
if (!mp3) {
return Boom.serverUnavailable();
}
return h
.response(Buffer.from(mp3, "base64"))
.header("Content-Type", "audio/mpeg")
.header("Content-Disposition", "attachment; filename=prompt.mp3");
},
},
},
{
method: "post",
path: "/api/voice/twilio/record/{voiceLineId}",
options: {
description: "webhook for twilio to handle an incoming call",
validate: {
params: {
voiceLineId: Joi.string().uuid().required(),
},
},
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { voiceLineId } = request.params;
const { To } = request.payload as { To: string };
const voiceLine = await request.db().voiceLines.findBy({ number: To });
if (!voiceLine) return Boom.notFound();
if (voiceLine.id !== voiceLineId) return Boom.badRequest();
const frontendUrl = request.server.config().frontend.url;
const useTextPrompt = !voiceLine.audioPromptEnabled;
const twiml = new Twilio.twiml.VoiceResponse();
if (useTextPrompt) {
let prompt = voiceLine.promptText;
if (!prompt || prompt.length === 0)
prompt =
"The grabadora text prompt is unconfigured. Please set a prompt in the administration screen.";
twiml.say(
{
language: voiceLine.language as SayLanguage,
voice: voiceLine.voice as SayVoice,
},
prompt
);
} else {
const promptUrl = `${frontendUrl}/api/v1/voice/twilio/prompt/${voiceLineId}`;
twiml.play({ loop: 1 }, promptUrl);
}
twiml.record({
playBeep: true,
finishOnKey: "1",
recordingStatusCallback: `${frontendUrl}/api/v1/voice/twilio/recording-ready/${voiceLineId}`,
});
return twiml.toString();
},
},
},
{
method: "post",
path: "/api/voice/twilio/recording-ready/{voiceLineId}",
options: {
description: "webhook for twilio to handle a recording",
validate: {
params: {
voiceLineId: Joi.string().uuid().required(),
},
},
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const { voiceLineId } = request.params;
const voiceLine = await request
.db()
.voiceLines.findById({ id: voiceLineId });
if (!voiceLine) return Boom.notFound();
const { AccountSid, RecordingSid, CallSid } = request.payload as {
AccountSid: string;
RecordingSid: string;
CallSid: string;
};
await queueRecording({
voiceLineId,
accountSid: AccountSid,
callSid: CallSid,
recordingSid: RecordingSid,
});
return h.response().code(203);
},
},
},
{
method: "post",
path: "/api/voice/twilio/text-to-speech/{providerId}",
options: {
description: "webook for twilio to test the twilio text-to-speech",
validate: {
params: {
providerId: Joi.string().uuid().required(),
},
},
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { language, voice, prompt } = request.payload as {
language: SayLanguage;
voice: SayVoice;
prompt: string;
};
const twiml = new Twilio.twiml.VoiceResponse();
twiml.say({ language, voice }, prompt);
return twiml.toString();
},
},
},
{
method: "get",
path: "/api/voice/twilio/text-to-speech-token/{providerId}",
options: {
description:
"generates a one time token to test the twilio text-to-speech",
validate: {
params: {
providerId: Joi.string().uuid().required(),
},
},
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const { providerId } = request.params as { providerId: string };
const provider: SavedVoiceProvider = await request
.db()
.voiceProviders.findById({ id: providerId });
if (!provider) return Boom.notFound();
const frontendUrl = request.server.config().frontend.url;
const url = `${frontendUrl}/api/v1/voice/twilio/text-to-speech/${providerId}`;
const name = `Grabadora text-to-speech tester: ${providerId}`;
const app = await getOrCreateTTSTestApplication(
url,
name,
twilioClientFor(provider)
);
const { accountSid, apiKeySecret, apiKeySid } = provider.credentials;
const token = new Twilio.jwt.AccessToken(
accountSid,
apiKeySid,
apiKeySecret,
{ identity: "tts-test" }
);
const grant = new Twilio.jwt.AccessToken.VoiceGrant({
outgoingApplicationSid: app.sid,
incomingAllow: true,
});
token.addGrant(grant);
return h.response({
token: token.toJwt(),
});
},
},
},
]);