shellphone.app/app/messages/api/webhook/incoming-message.ts

135 lines
3.5 KiB
TypeScript
Raw Normal View History

2021-07-31 17:22:48 +00:00
import type { BlitzApiRequest, BlitzApiResponse } from "blitz";
import twilio from "twilio";
2021-07-31 14:33:18 +00:00
import appLogger from "../../../../integrations/logger";
import db, { SubscriptionStatus } from "../../../../db";
import insertIncomingMessageQueue from "../queue/insert-incoming-message";
import { smsUrl } from "../../../../integrations/twilio";
2021-09-26 22:08:02 +00:00
import type { ApiError } from "../../../core/types";
2021-08-27 21:09:45 +00:00
const logger = appLogger.child({ route: "/api/webhook/incoming-message" });
2021-07-31 14:33:18 +00:00
2021-07-31 17:22:48 +00:00
export default async function incomingMessageHandler(req: BlitzApiRequest, res: BlitzApiResponse) {
2021-07-31 14:33:18 +00:00
if (req.method !== "POST") {
const statusCode = 405;
2021-07-31 14:33:18 +00:00
const apiError: ApiError = {
statusCode,
errorMessage: `Method ${req.method} Not Allowed`,
};
logger.error(apiError);
2021-07-31 14:33:18 +00:00
res.setHeader("Allow", ["POST"]);
res.status(statusCode).send(apiError);
return;
2021-07-31 14:33:18 +00:00
}
const twilioSignature = req.headers["X-Twilio-Signature"] || req.headers["x-twilio-signature"];
2021-07-31 14:33:18 +00:00
if (!twilioSignature || Array.isArray(twilioSignature)) {
const statusCode = 400;
2021-07-31 14:33:18 +00:00
const apiError: ApiError = {
statusCode,
errorMessage: "Invalid header X-Twilio-Signature",
};
logger.error(apiError);
2021-07-31 14:33:18 +00:00
res.status(statusCode).send(apiError);
return;
2021-07-31 14:33:18 +00:00
}
const body: Body = req.body;
2021-07-31 14:33:18 +00:00
try {
2021-08-05 17:07:15 +00:00
const phoneNumbers = await db.phoneNumber.findMany({
where: { number: body.To },
include: {
organization: {
include: {
subscriptions: {
where: { status: SubscriptionStatus.active },
},
},
},
},
});
2021-08-05 17:07:15 +00:00
if (phoneNumbers.length === 0) {
// phone number is not registered by any organization
res.status(500).end();
2021-08-01 10:36:32 +00:00
return;
}
const phoneNumbersWithActiveSub = phoneNumbers.filter(
(phoneNumber) => phoneNumber.organization.subscriptions.length > 0,
);
if (phoneNumbersWithActiveSub.length === 0) {
// accept the webhook but don't store incoming message
// because the organization is on the free plan
res.status(200).end();
return;
}
const phoneNumber = phoneNumbersWithActiveSub.find((phoneNumber) => {
2021-08-05 17:07:15 +00:00
// if multiple organizations have the same number
// find the organization currently using that phone number
// maybe we shouldn't let that happen by restricting a phone number to one org?
2021-08-05 17:07:15 +00:00
const authToken = phoneNumber.organization.twilioAuthToken ?? "";
return twilio.validateRequest(authToken, twilioSignature, smsUrl, req.body);
2021-08-05 17:07:15 +00:00
});
if (!phoneNumber) {
const statusCode = 400;
2021-07-31 14:33:18 +00:00
const apiError: ApiError = {
statusCode,
errorMessage: "Invalid webhook",
};
logger.error(apiError);
2021-07-31 14:33:18 +00:00
res.status(statusCode).send(apiError);
return;
2021-07-31 14:33:18 +00:00
}
2021-08-01 10:36:32 +00:00
const messageSid = body.MessageSid;
2021-08-05 17:07:15 +00:00
const organizationId = phoneNumber.organization.id;
const phoneNumberId = phoneNumber.id;
await insertIncomingMessageQueue.enqueue(
{
messageSid,
organizationId,
phoneNumberId,
},
{ id: `insert-${messageSid}-${organizationId}-${phoneNumberId}` },
);
res.setHeader("content-type", "text/html");
res.status(200).send("<Response></Response>");
2021-08-27 18:05:44 +00:00
} catch (error: any) {
const statusCode = error.statusCode ?? 500;
2021-07-31 14:33:18 +00:00
const apiError: ApiError = {
statusCode,
errorMessage: error.message,
};
logger.error(error);
2021-07-31 14:33:18 +00:00
res.status(statusCode).send(apiError);
2021-07-31 14:33:18 +00:00
}
}
2021-08-01 10:36:32 +00:00
type Body = {
ToCountry: string;
ToState: string;
SmsMessageSid: string;
NumMedia: string;
ToCity: string;
FromZip: string;
SmsSid: string;
FromState: string;
SmsStatus: string;
FromCity: string;
Body: string;
FromCountry: string;
To: string;
ToZip: string;
NumSegments: string;
MessageSid: string;
AccountSid: string;
From: string;
ApiVersion: string;
};