Add all repos

This commit is contained in:
Darren Clarke 2023-02-13 12:41:30 +00:00
parent faa12c60bc
commit 8a91c9b89b
369 changed files with 29047 additions and 28 deletions

View file

@ -0,0 +1,21 @@
import * as Metamigo from "common";
import Toys from "@hapipal/toys";
export const withDefaults = Toys.withRouteDefaults({
options: {
cors: true,
auth: "nextauth-jwt",
validate: {
failAction: Metamigo.validatingFailAction,
},
},
});
export const noAuth = Toys.withRouteDefaults({
options: {
cors: true,
validate: {
failAction: Metamigo.validatingFailAction,
},
},
});

View file

@ -0,0 +1,33 @@
import isFunction from "lodash/isFunction";
import type * as Hapi from "@hapi/hapi";
import * as RandomRoutes from "./random";
import * as UserRoutes from "./users";
import * as VoiceRoutes from "./voice";
import * as WhatsappRoutes from "./whatsapp";
import * as SignalRoutes from "./signal";
const loadRouteIndex = async (server, index) => {
const routes = [];
for (const exported in index) {
if (Object.prototype.hasOwnProperty.call(index, exported)) {
const route = index[exported];
routes.push(route);
}
}
routes.forEach(async (route) => {
if (isFunction(route)) server.route(await route(server));
else server.route(route);
});
};
export const register = async (server: Hapi.Server): Promise<void> => {
// Load your routes here.
// routes are loaded from the list of exported vars
// a route file should export routes directly or an async function that returns the routes.
loadRouteIndex(server, RandomRoutes);
loadRouteIndex(server, UserRoutes);
loadRouteIndex(server, VoiceRoutes);
loadRouteIndex(server, WhatsappRoutes);
loadRouteIndex(server, SignalRoutes);
};

View file

@ -0,0 +1,249 @@
import * as Hapi from "@hapi/hapi";
import * as Joi from "joi";
import * as Helpers from "../helpers";
import Boom from "boom";
const getSignalService = (request) => {
return request.services().signaldService;
};
export const GetAllSignalBotsRoute = Helpers.withDefaults({
method: "get",
path: "/api/signal/bots",
options: {
description: "Get all bots",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const signalService = getSignalService(request);
const bots = await signalService.findAll();
if (bots) {
// with the pino logger the first arg is an object of data to log
// the second arg is a message
// all other args are formated args for the msg
request.logger.info({ bots }, "Retrieved bot(s) at %s", new Date());
return { bots };
}
return _h.response().code(204);
},
},
});
export const GetBotsRoute = Helpers.noAuth({
method: "get",
path: "/api/signal/bots/{token}",
options: {
description: "Get one bot",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { token } = request.params;
const signalService = getSignalService(request);
const bot = await signalService.findByToken(token);
if (bot) {
// with the pino logger the first arg is an object of data to log
// the second arg is a message
// all other args are formated args for the msg
request.logger.info({ bot }, "Retrieved bot(s) at %s", new Date());
return bot;
}
throw Boom.notFound("Bot not found");
},
},
});
interface MessageRequest {
phoneNumber: string;
message: string;
}
export const SendBotRoute = Helpers.noAuth({
method: "post",
path: "/api/signal/bots/{token}/send",
options: {
description: "Send a message",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { token } = request.params;
const { phoneNumber, message } = request.payload as MessageRequest;
const signalService = getSignalService(request);
const bot = await signalService.findByToken(token);
if (bot) {
request.logger.info({ bot }, "Sent a message at %s", new Date());
await signalService.send(bot, phoneNumber, message as string);
return _h
.response({
result: {
recipient: phoneNumber,
timestamp: new Date().toISOString(),
source: bot.phoneNumber,
},
})
.code(200); // temp
}
throw Boom.notFound("Bot not found");
},
},
});
interface ResetSessionRequest {
phoneNumber: string;
}
export const ResetSessionBotRoute = Helpers.noAuth({
method: "post",
path: "/api/signal/bots/{token}/resetSession",
options: {
description: "Reset a session with another user",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { token } = request.params;
const { phoneNumber } = request.payload as ResetSessionRequest;
const signalService = getSignalService(request);
const bot = await signalService.findByToken(token);
if (bot) {
await signalService.resetSession(bot, phoneNumber);
return _h
.response({
result: {
recipient: phoneNumber,
timestamp: new Date().toISOString(),
source: bot.phoneNumber,
},
})
.code(200); // temp
}
throw Boom.notFound("Bot not found");
},
},
});
export const ReceiveBotRoute = Helpers.withDefaults({
method: "get",
path: "/api/signal/bots/{token}/receive",
options: {
description: "Receive messages",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { token } = request.params;
const signalService = getSignalService(request);
const bot = await signalService.findByToken(token);
if (bot) {
request.logger.info({ bot }, "Received messages at %s", new Date());
return signalService.receive(bot);
}
throw Boom.notFound("Bot not found");
},
},
});
export const RegisterBotRoute = Helpers.withDefaults({
method: "get",
path: "/api/signal/bots/{id}/register",
options: {
description: "Register a bot",
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const { id } = request.params;
const signalService = getSignalService(request);
const { code } = request.query;
const bot = await signalService.findById(id);
if (!bot) throw Boom.notFound("Bot not found");
try {
request.logger.info({ bot }, "Create bot at %s", new Date());
await signalService.register(bot, code);
return h.response(bot).code(200);
} catch (error) {
return h.response().code(error.code);
}
},
},
});
interface BotRequest {
phoneNumber: string;
description: string;
}
export const CreateBotRoute = Helpers.withDefaults({
method: "post",
path: "/api/signal/bots",
options: {
description: "Register a bot",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { phoneNumber, description } = request.payload as BotRequest;
const signalService = getSignalService(request);
console.log("request.auth.credentials:", request.auth.credentials);
const bot = await signalService.create(
phoneNumber,
description,
request.auth.credentials.email as string
);
if (bot) {
request.logger.info({ bot }, "Create bot at %s", new Date());
return bot;
}
throw Boom.notFound("Bot not found");
},
},
});
export const RequestCodeRoute = Helpers.withDefaults({
method: "get",
path: "/api/signal/bots/{id}/requestCode",
options: {
description: "Register a bot",
validate: {
params: Joi.object({
id: Joi.string().uuid().required(),
}),
query: Joi.object({
mode: Joi.string().valid("sms", "voice").required(),
captcha: Joi.string(),
}),
},
handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
const { id } = request.params;
const { mode, captcha } = request.query;
const signalService = getSignalService(request);
const bot = await signalService.findById(id);
if (!bot) {
throw Boom.notFound("Bot not found");
}
try {
if (mode === "sms") {
await signalService.requestSMSVerification(bot, captcha);
} else if (mode === "voice") {
await signalService.requestVoiceVerification(bot, captcha);
}
return h.response().code(200);
} catch (error) {
console.log(error);
if (error.name === "CaptchaRequiredException") {
return h.response().code(402);
} else if (error.code) {
return h.response().code(error.code);
} else {
return h.response().code(500);
}
}
},
},
});

View file

@ -0,0 +1,59 @@
import * as Joi from "joi";
import * as Hapi from "@hapi/hapi";
import { UserRecord, crudRoutesFor, CrudControllerBase } from "common";
import * as RouteHelpers from "../helpers";
class UserRecordController extends CrudControllerBase(UserRecord) { }
const validator = (): Record<string, Hapi.RouteOptionsValidate> => ({
create: {
payload: Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
emailVerified: Joi.string().isoDate().required(),
createdBy: Joi.string().required(),
avatar: Joi.string()
.uri({ scheme: ["http", "https"] })
.optional(),
userRole: Joi.string().optional(),
isActive: Joi.boolean().optional(),
}).label("UserCreate"),
},
updateById: {
params: {
userId: Joi.string().uuid().required(),
},
payload: Joi.object({
name: Joi.string().optional(),
email: Joi.string().email().optional(),
emailVerified: Joi.string().isoDate().optional(),
createdBy: Joi.boolean().optional(),
avatar: Joi.string()
.uri({ scheme: ["http", "https"] })
.optional(),
userRole: Joi.string().optional(),
isActive: Joi.boolean().optional(),
createdAt: Joi.string().isoDate().optional(),
updatedAt: Joi.string().isoDate().optional(),
}).label("UserUpdate"),
},
deleteById: {
params: {
userId: Joi.string().uuid().required(),
},
},
getById: {
params: {
userId: Joi.string().uuid().required(),
},
},
});
export const UserRoutes = async (
_server: Hapi.Server
): Promise<Hapi.ServerRoute[]> => {
const controller = new UserRecordController("users", "userId");
return RouteHelpers.withDefaults(
crudRoutesFor("user", "/api/users", controller, "userId", validator())
);
};

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(),
});
},
},
},
]);

View file

@ -0,0 +1,195 @@
import * as Hapi from "@hapi/hapi";
import * as Helpers from "../helpers";
import Boom from "boom";
export const GetAllWhatsappBotsRoute = Helpers.withDefaults({
method: "get",
path: "/api/whatsapp/bots",
options: {
description: "Get all bots",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { whatsappService } = request.services();
const bots = await whatsappService.findAll();
if (bots) {
// with the pino logger the first arg is an object of data to log
// the second arg is a message
// all other args are formated args for the msg
request.logger.info({ bots }, "Retrieved bot(s) at %s", new Date());
return { bots };
}
return _h.response().code(204);
},
},
});
export const GetBotsRoute = Helpers.noAuth({
method: "get",
path: "/api/whatsapp/bots/{token}",
options: {
description: "Get one bot",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { token } = request.params;
const { whatsappService } = request.services();
const bot = await whatsappService.findByToken(token);
if (bot) {
// with the pino logger the first arg is an object of data to log
// the second arg is a message
// all other args are formated args for the msg
request.logger.info({ bot }, "Retrieved bot(s) at %s", new Date());
return bot;
}
throw Boom.notFound("Bot not found");
},
},
});
interface MessageRequest {
phoneNumber: string;
message: string;
}
export const SendBotRoute = Helpers.noAuth({
method: "post",
path: "/api/whatsapp/bots/{token}/send",
options: {
description: "Send a message",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { token } = request.params;
const { phoneNumber, message } = request.payload as MessageRequest;
const { whatsappService } = request.services();
const bot = await whatsappService.findByToken(token);
if (bot) {
request.logger.info({ bot }, "Sent a message at %s", new Date());
await whatsappService.send(bot, phoneNumber, message as string);
return _h
.response({
result: {
recipient: phoneNumber,
timestamp: new Date().toISOString(),
source: bot.phoneNumber,
},
})
.code(200); // temp
}
throw Boom.notFound("Bot not found");
},
},
});
export const ReceiveBotRoute = Helpers.withDefaults({
method: "get",
path: "/api/whatsapp/bots/{token}/receive",
options: {
description: "Receive messages",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { token } = request.params;
const { whatsappService } = request.services();
const bot = await whatsappService.findByToken(token);
if (bot) {
request.logger.info({ bot }, "Received messages at %s", new Date());
// temp
const date = new Date();
const twoDaysAgo = new Date(date.getTime());
twoDaysAgo.setDate(date.getDate() - 2);
return whatsappService.receive(bot, twoDaysAgo);
}
throw Boom.notFound("Bot not found");
},
},
});
export const RegisterBotRoute = Helpers.withDefaults({
method: "get",
path: "/api/whatsapp/bots/{id}/register",
options: {
description: "Register a bot",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { id } = request.params;
const { whatsappService } = request.services();
const bot = await whatsappService.findById(id);
if (bot) {
await whatsappService.register(bot, (error: string) => {
if (error) {
return _h.response(error).code(500);
}
request.logger.info({ bot }, "Register bot at %s", new Date());
return _h.response().code(200);
});
}
throw Boom.notFound("Bot not found");
},
},
});
export const RefreshBotRoute = Helpers.withDefaults({
method: "get",
path: "/api/whatsapp/bots/{id}/refresh",
options: {
description: "Refresh messages",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { id } = request.params;
const { whatsappService } = request.services();
const bot = await whatsappService.findById(id);
if (bot) {
request.logger.info({ bot }, "Refreshed messages at %s", new Date());
// await whatsappService.refresh(bot);
return;
}
throw Boom.notFound("Bot not found");
},
},
});
interface BotRequest {
phoneNumber: string;
description: string;
}
export const CreateBotRoute = Helpers.withDefaults({
method: "post",
path: "/api/whatsapp/bots",
options: {
description: "Register a bot",
handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => {
const { phoneNumber, description } = request.payload as BotRequest;
const { whatsappService } = request.services();
console.log("request.auth.credentials:", request.auth.credentials);
const bot = await whatsappService.create(
phoneNumber,
description,
request.auth.credentials.email as string
);
if (bot) {
request.logger.info({ bot }, "Register bot at %s", new Date());
return bot;
}
throw Boom.notFound("Bot not found");
},
},
});