Skip to content
This repository was archived by the owner on Sep 17, 2024. It is now read-only.

Commit 5408b00

Browse files
committed
feat: Create the sms module
1 parent a84215f commit 5408b00

File tree

11 files changed

+476
-0
lines changed

11 files changed

+476
-0
lines changed

modules/sms/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { TestConfig } from "./utils/providers/test.ts";
2+
import { TwilioConfig } from "./utils/providers/twilio.ts";
3+
4+
export interface Config {
5+
provider: Provider;
6+
convertCurrencyTo?: string;
7+
}
8+
9+
export type Provider = { test: TestConfig } | { twilio: TwilioConfig };

modules/sms/module.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "SMS",
3+
"description": "Send SMS messages using multiple providers.",
4+
"icon": "envelope",
5+
"tags": [
6+
"sms"
7+
],
8+
"authors": [
9+
"rivet-gg",
10+
"Blckbrry-Pi"
11+
],
12+
"status": "stable",
13+
"scripts": {
14+
"send_sms": {
15+
"name": "Send SMS"
16+
},
17+
"get_message": {
18+
"name": "Get metadata of message, such as cost, from, to, and when it was sent."
19+
}
20+
},
21+
"errors": {
22+
"twilio_error": {
23+
"name": "Twilio SMS Error"
24+
},
25+
"currency_error": {
26+
"name": "Currency Conversion or Recognition Error"
27+
}
28+
}
29+
}

modules/sms/scripts/get_message.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { RuntimeError, ScriptContext } from "../module.gen.ts";
2+
import { Message } from "../utils/types.ts";
3+
import { TestProvider } from "../utils/providers/test.ts";
4+
import { TwilioProvider } from "../utils/providers/twilio.ts";
5+
import { SmsProvider } from "../utils/providers/index.ts";
6+
7+
export interface Request {
8+
id: string;
9+
}
10+
11+
export type Response = Message;
12+
13+
export async function run(
14+
ctx: ScriptContext,
15+
req: Request,
16+
): Promise<Response> {
17+
let provider: SmsProvider;
18+
if ("test" in ctx.config.provider) {
19+
provider = new TestProvider(ctx.config.provider.test);
20+
} else if ("twilio" in ctx.config.provider) {
21+
provider = new TwilioProvider(ctx.config.provider.twilio);
22+
} else {
23+
throw new RuntimeError("unreachable");
24+
}
25+
26+
return await provider.fetch(req.id);
27+
}

modules/sms/scripts/send_sms.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { RuntimeError, ScriptContext } from "../module.gen.ts";
2+
import { Message } from "../utils/types.ts";
3+
import { TestProvider } from "../utils/providers/test.ts";
4+
import { TwilioProvider } from "../utils/providers/twilio.ts";
5+
import { SmsProvider } from "../utils/providers/index.ts";
6+
7+
export interface Request {
8+
to: string;
9+
content: string;
10+
useGSM7?: boolean;
11+
}
12+
13+
export type Response = Message;
14+
15+
export async function run(
16+
ctx: ScriptContext,
17+
req: Request,
18+
): Promise<Response> {
19+
let provider: SmsProvider;
20+
if ("test" in ctx.config.provider) {
21+
provider = new TestProvider(ctx.config.provider.test);
22+
} else if ("twilio" in ctx.config.provider) {
23+
provider = new TwilioProvider(ctx.config.provider.twilio);
24+
} else {
25+
throw new RuntimeError("unreachable");
26+
}
27+
28+
return await provider.send(req.to, req.content, req.useGSM7 ?? false);
29+
}

modules/sms/tests/e2e.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { test, TestContext } from "../module.gen.ts";
2+
import { assertExists, assertEquals, assertNotEquals, assertGreaterOrEqual } from "https://deno.land/[email protected]/assert/mod.ts";
3+
import { faker } from "https://deno.land/x/[email protected]/mod.ts";
4+
import { MessageStatus } from "../utils/types.ts";
5+
6+
test("Test sms send E2E", async (ctx: TestContext) => {
7+
const username = faker.internet.userName();
8+
const fakeCode = faker.random.alphaNumeric(6);
9+
10+
const line0 = `Hello, ${username}! This is a test message for OpenGB.`;
11+
const line1 = `Have a cool code: ${fakeCode}!`;
12+
const messageContent = `${line0}\n${line1}`;
13+
14+
const phoneNumber = Deno.env.get("TEST_SMS_NUMBER") ?? "+1234567890";
15+
16+
const initialMessage = await ctx.modules.sms.sendSms({
17+
content: messageContent,
18+
to: phoneNumber,
19+
});
20+
assertExists(initialMessage);
21+
assertEquals(initialMessage.body, messageContent);
22+
assertEquals(initialMessage.to, phoneNumber);
23+
24+
while (true) {
25+
const message = await ctx.modules.sms.getMessage({ id: initialMessage.id });
26+
assertExists(message);
27+
assertEquals(message.body, initialMessage.body);
28+
assertEquals(message.id, initialMessage.id);
29+
assertEquals(message.to, initialMessage.to);
30+
31+
assertNotEquals(message.status, MessageStatus.FAILED);
32+
if (message.status === MessageStatus.SENT) break;
33+
}
34+
35+
const message = await ctx.modules.sms.getMessage({ id: initialMessage.id });
36+
37+
if (message.price) {
38+
assertGreaterOrEqual(message.price[0], 0);
39+
}
40+
});

modules/sms/utils/currency.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { RuntimeError } from "../module.gen.ts";
2+
3+
export async function convertCurrencyTo(from: string, to: string, amount: number): Promise<number> {
4+
if (from === to) return amount;
5+
6+
const url = `https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/${to}.json`;
7+
try {
8+
const res = await fetch(url);
9+
const data = await res.json();
10+
const fromPerTo = data[to][from];
11+
if (!fromPerTo || typeof fromPerTo !== "number") throw new Error();
12+
return amount / fromPerTo;
13+
} catch {
14+
throw new RuntimeError("currency_conversion_error", { meta: { from, to } });
15+
}
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export enum MessageStatus {
2+
QUEUED = "queued",
3+
SENT = "sent",
4+
FAILED = "failed",
5+
}
6+
7+
export interface Message {
8+
id: string;
9+
10+
to: string;
11+
from: string;
12+
body: string;
13+
segments: number;
14+
price?: [number, string];
15+
priceUSD?: number;
16+
17+
status: MessageStatus;
18+
}
19+
20+
export abstract class SmsProvider {
21+
public abstract send(to: string, content: string, useGSM7: boolean): Promise<Message>;
22+
public abstract fetch(id: string): Promise<Message>;
23+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Empty } from "../../module.gen.ts";
2+
import { Message, MessageStatus, SmsProvider } from "./index.ts";
3+
4+
export type TestConfig = Empty;
5+
6+
export class TestProvider extends SmsProvider {
7+
protected config: TestConfig;
8+
9+
public constructor(config: TestConfig) {
10+
super();
11+
12+
this.config = config;
13+
}
14+
15+
public async send(to: string, body: string): Promise<Message> {
16+
const isGSM7 = body.match(/^[A-Za-z0-9\+\/\=]*$/);
17+
const gsm7Segments = Math.ceil(body.length / 160);
18+
const unicodeSegments = Math.ceil(body.length / 70);
19+
const segments = isGSM7 ? gsm7Segments : unicodeSegments;
20+
const from = "+1 (000) 000-0000";
21+
22+
// Encode the metadata into the ID so we can pull it out later
23+
const id = btoa(JSON.stringify({
24+
to,
25+
from,
26+
segments,
27+
body,
28+
date: new Date().toISOString(),
29+
}));
30+
31+
return await Promise.resolve<Message>({
32+
id,
33+
from,
34+
to,
35+
status: MessageStatus.SENT,
36+
segments,
37+
body,
38+
price: [0, "usd"],
39+
priceUSD: 0,
40+
});
41+
}
42+
43+
public async fetch(id: string): Promise<Message> {
44+
const metadata = JSON.parse(atob(id));
45+
46+
return await Promise.resolve<Message>({
47+
id,
48+
from: metadata.from,
49+
to: metadata.to,
50+
segments: metadata.segments,
51+
status: MessageStatus.SENT,
52+
body: metadata.body,
53+
price: [0, "usd"],
54+
priceUSD: 0,
55+
});
56+
}
57+
}

0 commit comments

Comments
 (0)