Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add twilio integration to send sms messages πŸ“² #663

Merged
merged 16 commits into from
Dec 14, 2024
3 changes: 3 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ STUDENT_PROFILE_URL=http://localhost:3000
# SMTP_HOST=
# SMTP_PASSWORD=
# SMTP_USERNAME=
# TWILIO_ACCOUNT_SID=
# TWILIO_AUTH_TOKEN=
# TWILIO_PHONE_NUMBER=
6 changes: 6 additions & 0 deletions apps/api/src/shared/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const BaseEnvironmentConfig = z.object({
SLACK_INTRODUCTIONS_CHANNEL_ID: EnvironmentVariable,
SLACK_SIGNING_SECRET: EnvironmentVariable,
STUDENT_PROFILE_URL: EnvironmentVariable,
TWILIO_ACCOUNT_SID: EnvironmentVariable,
TWILIO_AUTH_TOKEN: EnvironmentVariable,
TWILIO_PHONE_NUMBER: EnvironmentVariable,
});

const EnvironmentConfig = z.discriminatedUnion('ENVIRONMENT', [
Expand Down Expand Up @@ -100,6 +103,9 @@ const EnvironmentConfig = z.discriminatedUnion('ENVIRONMENT', [
SLACK_FEED_CHANNEL_ID: true,
SLACK_INTRODUCTIONS_CHANNEL_ID: true,
SLACK_SIGNING_SECRET: true,
TWILIO_ACCOUNT_SID: true,
TWILIO_AUTH_TOKEN: true,
TWILIO_PHONE_NUMBER: true,
}).extend({
ENVIRONMENT: z.literal(Environment.DEVELOPMENT),
SMTP_HOST: EnvironmentVariable.optional(),
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/infrastructure/bull.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,13 @@ export const NotificationBullJob = z.discriminatedUnion('name', [
}),
]),
}),
z.object({
name: z.literal('notification.sms.send'),
data: z.object({
message: z.string().trim().min(1),
phoneNumber: z.string().trim().min(1),
}),
}),
]);

export const OfferBullJob = z.discriminatedUnion('name', [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { match } from 'ts-pattern';

import { registerWorker } from '@/infrastructure/bull';
import { NotificationBullJob } from '@/infrastructure/bull.types';
import { sendSMS } from '@/modules/notifications/twilio';
import { sendEphemeralSlackNotification } from '@/modules/notifications/use-cases/send-ephemeral-slack-notification';
import { sendEmail } from './use-cases/send-email';
import { sendSlackNotification } from './use-cases/send-slack-notification';
Expand All @@ -20,6 +21,9 @@ export const notificationWorker = registerWorker(
.with({ name: 'notification.slack.send' }, async ({ data }) => {
return sendSlackNotification(data);
})
.with({ name: 'notification.sms.send' }, ({ data }) => {
return sendSMS(data);
})
.exhaustive();
}
);
83 changes: 83 additions & 0 deletions packages/core/src/modules/notifications/twilio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { reportException } from '@/infrastructure/sentry';
import { encodeBasicAuthenticationToken } from '@/shared/utils/auth';
import { fail, type Result, success } from '@/shared/utils/core';

// Environment Variables

const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID as string;
const TWILIO_AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN as string;
const TWILIO_PHONE_NUMBER = process.env.TWILIO_PHONE_NUMBER as string;

// Constants

const TWILIO_API_URL = 'https://api.twilio.com/2010-04-01';

const TWILIO_TOKEN = encodeBasicAuthenticationToken(
TWILIO_ACCOUNT_SID,
TWILIO_AUTH_TOKEN
);

// Core

type SendSMSInput = {
message: string;

/**
* The phone number to send the SMS message to. Must be in E.164 format.
*
* @example '+18777804236'
* @example '+18444351670'
*/
phoneNumber: string;
};

/**
* Sends an SMS message to a phone number using Twilio.
*
* @param input - The message and recipient phone number for the SMS message.
* @returns A result indicating the success or failure of the SMS sending.
*
* @see https://www.twilio.com/docs/messaging/api/message-resource#send-an-sms-message
*/
export async function sendSMS({
message,
phoneNumber,
}: SendSMSInput): Promise<Result> {
const form = new FormData();

form.set('Body', message);
form.set('From', TWILIO_PHONE_NUMBER);
form.set('To', phoneNumber);

const response = await fetch(
TWILIO_API_URL + `/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json`,
{
body: form,
headers: { Authorization: `Basic ${TWILIO_TOKEN}` },
method: 'POST',
}
);

const data = await response.json();

if (!response.ok) {
const error = new Error('Failed to send SMS message with Twilio.');

reportException(error, {
data,
status: response.status,
});

return fail({
code: response.status,
error: error.message,
});
}

console.log('SMS message sent!', {
phoneNumber,
message,
});

return success({});
}
Loading