Move packages/apps back
This commit is contained in:
parent
6eaaf8e9be
commit
5535d6b575
348 changed files with 0 additions and 0 deletions
21
apps/metamigo-api/app/routes/helpers/index.ts
Normal file
21
apps/metamigo-api/app/routes/helpers/index.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
33
apps/metamigo-api/app/routes/index.ts
Normal file
33
apps/metamigo-api/app/routes/index.ts
Normal 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);
|
||||
};
|
||||
249
apps/metamigo-api/app/routes/signal/index.ts
Normal file
249
apps/metamigo-api/app/routes/signal/index.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
59
apps/metamigo-api/app/routes/users/index.ts
Normal file
59
apps/metamigo-api/app/routes/users/index.ts
Normal 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())
|
||||
);
|
||||
};
|
||||
124
apps/metamigo-api/app/routes/voice/index.ts
Normal file
124
apps/metamigo-api/app/routes/voice/index.ts
Normal 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";
|
||||
230
apps/metamigo-api/app/routes/voice/twilio/index.ts
Normal file
230
apps/metamigo-api/app/routes/voice/twilio/index.ts
Normal 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(),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
195
apps/metamigo-api/app/routes/whatsapp/index.ts
Normal file
195
apps/metamigo-api/app/routes/whatsapp/index.ts
Normal 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");
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue