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 => ({ 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 => { const controller = new VoiceLineRecordController("voiceLines", "id"); return Helpers.withDefaults( crudRoutesFor( "voice-line", "/api/voice/voice-line", controller, "id", validator() ) ); }; export * from "./twilio";