Skip to content
This repository was archived by the owner on Sep 17, 2024. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions modules/sms/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { TestConfig } from "./utils/providers/test.ts";
import { TwilioConfig } from "./utils/providers/twilio.ts";

export interface Config {
provider: Provider;
convertCurrencyTo?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove me (see get_message)

}

export type Provider = { test: TestConfig } | { twilio: TwilioConfig };
29 changes: 29 additions & 0 deletions modules/sms/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "SMS",
"description": "Send SMS messages using multiple providers.",
"icon": "envelope",
"tags": [
"sms"
],
"authors": [
"rivet-gg",
"Blckbrry-Pi"
],
"status": "stable",
"scripts": {
"send_sms": {
"name": "Send SMS"
},
"get_message": {
"name": "Get metadata of message, such as cost, from, to, and when it was sent."
}
},
"errors": {
"twilio_error": {
"name": "Twilio SMS Error"
},
"currency_error": {
"name": "Currency Conversion or Recognition Error"
}
}
}
27 changes: 27 additions & 0 deletions modules/sms/scripts/get_message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { RuntimeError, ScriptContext } from "../module.gen.ts";
import { Message } from "../utils/types.ts";
import { TestProvider } from "../utils/providers/test.ts";
import { TwilioProvider } from "../utils/providers/twilio.ts";
import { SmsProvider } from "../utils/providers/index.ts";

export interface Request {
id: string;
}

export type Response = Message;

export async function run(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is too verbose. we need to provide a generic superset over other peoples' apis. it's better to expose less than more so we can maintain consistent behavior across providers. i'd recommend completely deleting this script unless you see a good reason otherwise.

ctx: ScriptContext,
req: Request,
): Promise<Response> {
let provider: SmsProvider;
if ("test" in ctx.config.provider) {
provider = new TestProvider(ctx.config.provider.test);
} else if ("twilio" in ctx.config.provider) {
provider = new TwilioProvider(ctx.config.provider.twilio);
} else {
throw new RuntimeError("unreachable");
}

return await provider.fetch(req.id);
}
29 changes: 29 additions & 0 deletions modules/sms/scripts/send_sms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { RuntimeError, ScriptContext } from "../module.gen.ts";
import { Message } from "../utils/types.ts";
import { TestProvider } from "../utils/providers/test.ts";
import { TwilioProvider } from "../utils/providers/twilio.ts";
import { SmsProvider } from "../utils/providers/index.ts";

export interface Request {
to: string;
content: string;
useGSM7?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaik we don't need to provide this, it's done by default: https://www.twilio.com/docs/glossary/what-is-gsm-7-character-encoding#how-twilio-encodes-your-messages

keep the api as minimal as possible

}

export type Response = Message;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not need to return anything. see comment in get_message


export async function run(
ctx: ScriptContext,
req: Request,
): Promise<Response> {
let provider: SmsProvider;
if ("test" in ctx.config.provider) {
provider = new TestProvider(ctx.config.provider.test);
} else if ("twilio" in ctx.config.provider) {
provider = new TwilioProvider(ctx.config.provider.twilio);
} else {
throw new RuntimeError("unreachable");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use unreachableerror

}

return await provider.send(req.to, req.content, req.useGSM7 ?? false);
}
40 changes: 40 additions & 0 deletions modules/sms/tests/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { test, TestContext } from "../module.gen.ts";
import { assertExists, assertEquals, assertNotEquals, assertGreaterOrEqual } from "https://deno.land/[email protected]/assert/mod.ts";
import { faker } from "https://deno.land/x/[email protected]/mod.ts";
import { MessageStatus } from "../utils/types.ts";

test("Test sms send E2E", async (ctx: TestContext) => {
const username = faker.internet.userName();
const fakeCode = faker.random.alphaNumeric(6);

const line0 = `Hello, ${username}! This is a test message for OpenGB.`;
const line1 = `Have a cool code: ${fakeCode}!`;
const messageContent = `${line0}\n${line1}`;

const phoneNumber = Deno.env.get("TEST_SMS_NUMBER") ?? "+1234567890";

const initialMessage = await ctx.modules.sms.sendSms({
content: messageContent,
to: phoneNumber,
});
assertExists(initialMessage);
assertEquals(initialMessage.body, messageContent);
assertEquals(initialMessage.to, phoneNumber);

while (true) {
const message = await ctx.modules.sms.getMessage({ id: initialMessage.id });
assertExists(message);
assertEquals(message.body, initialMessage.body);
assertEquals(message.id, initialMessage.id);
assertEquals(message.to, initialMessage.to);

assertNotEquals(message.status, MessageStatus.FAILED);
if (message.status === MessageStatus.SENT) break;
}

const message = await ctx.modules.sms.getMessage({ id: initialMessage.id });

if (message.price) {
assertGreaterOrEqual(message.price[0], 0);
}
});
16 changes: 16 additions & 0 deletions modules/sms/utils/currency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RuntimeError } from "../module.gen.ts";

export async function convertCurrencyTo(from: string, to: string, amount: number): Promise<number> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

if (from === to) return amount;

const url = `https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/${to}.json`;
try {
const res = await fetch(url);
const data = await res.json();
const fromPerTo = data[to][from];
if (!fromPerTo || typeof fromPerTo !== "number") throw new Error();
return amount / fromPerTo;
} catch {
throw new RuntimeError("currency_conversion_error", { meta: { from, to } });
}
}
23 changes: 23 additions & 0 deletions modules/sms/utils/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export enum MessageStatus {
QUEUED = "queued",
SENT = "sent",
FAILED = "failed",
}

export interface Message {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remoev

id: string;

to: string;
from: string;
body: string;
segments: number;
price?: [number, string];
priceUSD?: number;

status: MessageStatus;
}

export abstract class SmsProvider {
public abstract send(to: string, content: string, useGSM7: boolean): Promise<Message>;
public abstract fetch(id: string): Promise<Message>;
}
57 changes: 57 additions & 0 deletions modules/sms/utils/providers/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Empty } from "../../module.gen.ts";
import { Message, MessageStatus, SmsProvider } from "./index.ts";

export type TestConfig = Empty;

export class TestProvider extends SmsProvider {
protected config: TestConfig;

public constructor(config: TestConfig) {
super();

this.config = config;
}

public async send(to: string, body: string): Promise<Message> {
const isGSM7 = body.match(/^[A-Za-z0-9\+\/\=]*$/);
const gsm7Segments = Math.ceil(body.length / 160);
const unicodeSegments = Math.ceil(body.length / 70);
const segments = isGSM7 ? gsm7Segments : unicodeSegments;
const from = "+1 (000) 000-0000";

// Encode the metadata into the ID so we can pull it out later
const id = btoa(JSON.stringify({
to,
from,
segments,
body,
date: new Date().toISOString(),
}));

return await Promise.resolve<Message>({
id,
from,
to,
status: MessageStatus.SENT,
segments,
body,
price: [0, "usd"],
priceUSD: 0,
});
}

public async fetch(id: string): Promise<Message> {
const metadata = JSON.parse(atob(id));

return await Promise.resolve<Message>({
id,
from: metadata.from,
to: metadata.to,
segments: metadata.segments,
status: MessageStatus.SENT,
body: metadata.body,
price: [0, "usd"],
priceUSD: 0,
});
}
}
Loading