diff --git a/app/phone-calls/api/webhook/call.ts b/app/phone-calls/api/webhook/call.ts index d7249e2..654c4ff 100644 --- a/app/phone-calls/api/webhook/call.ts +++ b/app/phone-calls/api/webhook/call.ts @@ -1,20 +1,68 @@ import type { BlitzApiRequest, BlitzApiResponse } from "blitz"; -import Twilio from "twilio"; +import { getConfig } from "blitz"; +import twilio from "twilio"; -import db from "../../../../db"; +import type { ApiError } from "../../../_types"; +import db, { Direction } from "../../../../db"; +import appLogger from "../../../../integrations/logger"; + +const { serverRuntimeConfig } = getConfig(); +const logger = appLogger.child({ route: "/api/webhook/call" }); export default async function incomingCallHandler(req: BlitzApiRequest, res: BlitzApiResponse) { console.log("req.body", req.body); - const isOutgoingCall = true; + const url = `https://${serverRuntimeConfig.app.baseUrl}/api/webhook/incoming-message`; + const twilioSignature = req.headers["X-Twilio-Signature"] || req.headers["x-twilio-signature"]; + if (!twilioSignature || Array.isArray(twilioSignature)) { + const statusCode = 400; + const apiError: ApiError = { + statusCode, + errorMessage: "Invalid header X-Twilio-Signature", + }; + logger.error(apiError); + + res.status(statusCode).send(apiError); + return; + } + + const isOutgoingCall = req.body.From.startsWith("client:"); if (isOutgoingCall) { const recipient = req.body.To; const organizationId = req.body.From.slice("client:".length).split("__")[0]; const phoneNumber = await db.phoneNumber.findFirst({ where: { organizationId }, - select: { number: true }, + include: { organization: true }, }); - const twiml = new Twilio.twiml.VoiceResponse(); + if ( + !phoneNumber || + !phoneNumber.organization.twilioAuthToken || + !twilio.validateRequest(phoneNumber.organization.twilioAuthToken, twilioSignature, url, req.body) + ) { + const statusCode = 400; + const apiError: ApiError = { + statusCode, + errorMessage: "Invalid webhook", + }; + logger.error(apiError); + + res.status(statusCode).send(apiError); + return; + } + + await db.phoneCall.create({ + data: { + id: req.body.CallSid, + from: phoneNumber.number, + to: req.body.To, + status: req.body.CallStatus, + direction: Direction.Outbound, + duration: "", // TODO + organizationId: phoneNumber.organization.id, + phoneNumberId: phoneNumber.id, + }, + }); + const twiml = new twilio.twiml.VoiceResponse(); const dial = twiml.dial({ answerOnBridge: true, callerId: phoneNumber!.number, @@ -24,8 +72,42 @@ export default async function incomingCallHandler(req: BlitzApiRequest, res: Bli res.setHeader("content-type", "text/xml"); return res.status(200).send(twiml.toString()); + } else { + const phoneNumbers = await db.phoneNumber.findMany({ + where: { number: req.body.To }, + include: { organization: true }, + }); + if (phoneNumbers.length === 0) { + // phone number is not registered by any organization + res.status(500).end(); + return; + } + + const phoneNumber = phoneNumbers.find((phoneNumber) => { + // if multiple organizations have the same number + // find the organization currently using that phone number + // maybe we shouldn't let multiple organizations use the same phone number + const authToken = phoneNumber.organization.twilioAuthToken ?? ""; + return twilio.validateRequest(authToken, twilioSignature, url, req.body); + }); + if (!phoneNumber) { + const statusCode = 400; + const apiError: ApiError = { + statusCode, + errorMessage: "Invalid webhook", + }; + logger.error(apiError); + + res.status(statusCode).send(apiError); + return; + } + + // TODO send notification + // TODO db.phoneCall.create(...); } + // TODO queue job to update duration when call ends + res.status(500).end(); }