diff --git a/bun.lockb b/bun.lockb index 82cfba7..d08672b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 973cf63..2a9d1fe 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "scripts": { "prepare-ui": "bun --filter 'doorman-ui' build && rm -rf packages/doorman-api/assets/* && mkdir -p packages/doorman-api/assets/assets && cp -fr packages/doorman-ui/dist/* packages/doorman-api/assets/ && cp -f packages/doorman-api/assets/index.html packages/doorman-api/assets/assets/index.html", "deploy-serverless": "bun run prepare-ui && bun --filter 'doorman-api' deploy", - "deploy-buzzer-client": "bun --filter 'doorman-client' deploy" + "build-twilio-functions": "bun --filter 'doorman-client' build", + "deploy-buzzer-client": "bun run build-twilio-functions && bun --filter 'doorman-client' deploy" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/packages/doorman-client/.gitignore b/packages/doorman-client/.gitignore index ca7e037..c06e76e 100644 --- a/packages/doorman-client/.gitignore +++ b/packages/doorman-client/.gitignore @@ -131,3 +131,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +deprecated-functions +functions/* \ No newline at end of file diff --git a/packages/doorman-client/.twilioserverlessrc b/packages/doorman-client/.twilioserverlessrc index a7f5a1c..31959de 100644 --- a/packages/doorman-client/.twilioserverlessrc +++ b/packages/doorman-client/.twilioserverlessrc @@ -2,6 +2,7 @@ "commands": {}, "environments": {}, "projects": {}, + "functionsFolder": "./build/functions", // "assets": true /* Upload assets. Can be turned off with --no-assets */, // "assetsFolder": null /* Specific folder name to be used for static assets */, // "buildSid": null /* An existing Build SID to deploy to the new environment */, diff --git a/packages/doorman-client/package.json b/packages/doorman-client/package.json index 86a9770..3e2e755 100644 --- a/packages/doorman-client/package.json +++ b/packages/doorman-client/package.json @@ -4,7 +4,10 @@ "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "twilio-run --live --port 4500", + "watch-build": "bun run --watch src/index.ts", + "start-twilio": "twilio-run --live --port 4500", + "start": "concurrently \"bun run watch-build\" \"bun run start-twilio\"", + "build": "bun run src/index.ts", "deploy": "twilio-run deploy --load-system-env --env .env.example --service-name buzzer --environment=prod --override-existing-project" }, "dependencies": { @@ -14,8 +17,9 @@ "twilio": "^3.56" }, "devDependencies": { - "twilio-run": "^3.5.4", - "@types/bun": "latest" + "@types/bun": "latest", + "concurrently": "^9.1.0", + "twilio-run": "^3.5.4" }, "engines": { "node": "18" diff --git a/packages/doorman-client/functions/buzzer-activated.js b/packages/doorman-client/src/functions/buzzer-activated.ts similarity index 52% rename from packages/doorman-client/functions/buzzer-activated.js rename to packages/doorman-client/src/functions/buzzer-activated.ts index 96ee908..363c495 100644 --- a/packages/doorman-client/functions/buzzer-activated.js +++ b/packages/doorman-client/src/functions/buzzer-activated.ts @@ -1,79 +1,32 @@ /** * Doorman entrypoint */ -const { randomUUID } = require('crypto'); -const fetch = require('node-fetch'); -const https = require('https'); -const http = require('http'); +import { randomUUID } from 'crypto'; +import fetch from "node-fetch"; +import '@twilio-labs/serverless-runtime-types'; +import { + ServerlessFunctionSignature, +} from '@twilio-labs/serverless-runtime-types/types'; +import { BuzzerDialEvent } from '../types/BuzzerDialEvent'; +import { getConfig, notifyAllDiscord, notifyDiscord } from '../utils/DoormanUtils'; +import { dialFallbackTwiml, doorOpenTwiml } from '../utils/TwimlUtils'; +import { TwilioContext } from '../types/TwilioContext'; +import { DoorConfig } from '../types/DoorConfig'; +import { DoorStatus, DoorStatusResponse } from '../types/DoorStatusResponse'; +import VoiceResponse from 'twilio/lib/twiml/VoiceResponse'; -/** - * Helper function to do an HTTP request and just await transmission, but not await a response. - * ref: https://www.sensedeep.com/blog/posts/stories/lambda-fast-http.html - * @param url - the URL to do HTTP request to - * @returns promise signalling HTTP request has been transmitted - */ -async function lambdaFastHttp(url) { - return new Promise((resolve, reject) => { - let req; - - if (url.startsWith("https://")) { - req = https.request(url); - } else { - req = http.request(url); - } - req.end(null, null, () => { - resolve(req); - }); - }); -} - -async function getConfig(context, buzzer) { - return await fetch(context.DOORMAN_URL + `/api/door/info?buzzer=${buzzer}`) - .then(res => res.json()) - .catch(err => { - return undefined; - }); -} - -async function notifyDiscord(context, msg, u, optionalJsonStr) { - return lambdaFastHttp(context.DOORMAN_URL + - `/api/door/notify?discordUser=${encodeURIComponent(JSON.stringify(u))}&msg=${encodeURIComponent(JSON.stringify(msg))}&json=${encodeURIComponent(JSON.stringify(optionalJsonStr))}`, - ).catch(err => console.log(err)) -} - -async function notifyAllDiscord(context, config, msg, optionalJsonStr) { - return notifyDiscord(context, config.discordUsers.map(() => msg), config.discordUsers, config.discordUsers.map(() => optionalJsonStr || "")); -} - -function doorOpenTwiml(twiml, config) { - twiml.play('https://buzzer-2439-prod.twil.io/buzzing_up_boosted.mp3'); - twiml.play({ digits: config.pressKey }); // configured in doorman what button to click and passed into this function - twiml.pause({ length: 1 }); - twiml.hangup(); -} - -function dialFallbackTwiml(twiml, config) { - let dial = twiml.dial({ - timeLimit: 20, - timeout: 20 - }); - - config.fallbackNumbers.forEach(number => { - dial.number(number); - }); -} - -exports.handler = async function(context, event, callback) { +export const handler: ServerlessFunctionSignature = async function(context, event, callback) { let invokeId = `[${randomUUID()}]`; - let config = event.config; + let configString = event.config; + let config: DoorConfig | undefined; console.log(invokeId, "starting execution"); // get by api or parse it out from query - if (!config) { + if (!configString) { config = await getConfig(context, event.From); } else { try { - config = JSON.parse(config); + config = JSON.parse(configString); } catch(e) { config = await getConfig(context, event.From); } @@ -93,21 +46,22 @@ exports.handler = async function(context, event, callback) { msg + u + ')' ); - await notifyDiscord(context, msgs, config.discordUsers, config.discordUsers.map(() => undefined)); + await notifyDiscord(context, msgs, config.discordUsers, config.discordUsers.map(() => "")); let discordLock = false; - let intervals = []; - let timeouts = []; + let intervals: Timer[] = []; + let timeouts: Timer[] = []; - const unlockPromise = new Promise((resolve, reject) => { + const unlockPromise = new Promise((resolve, reject) => { intervals.push(setInterval(() => { fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`) .then(res => res.json()) - .then(async body => { - if (body?.status === "OPEN") { + .then(async (rawBody) => { + let body = rawBody as DoorStatusResponse; + if (body?.status === DoorStatus.OPEN) { clearInterval(intervals[0]); - const twiml = new Twilio.twiml.VoiceResponse(); - doorOpenTwiml(twiml, config); + const twiml = doorOpenTwiml(config); + if (!discordLock) { discordLock = true; console.log( @@ -125,10 +79,9 @@ exports.handler = async function(context, event, callback) { }, 750)); }); - const gracefulFallbackPromise = new Promise((resolve, reject) => { + const gracefulFallbackPromise = new Promise((resolve, reject) => { timeouts.push(setTimeout(async () => { - const twiml = new Twilio.twiml.VoiceResponse(); - dialFallbackTwiml(twiml, config); + const twiml = dialFallbackTwiml(config); if (!discordLock) { discordLock = true; @@ -150,10 +103,9 @@ exports.handler = async function(context, event, callback) { }, 8000)); }); - const ungracefulFallbackPromise = new Promise((resolve, reject) => { + const ungracefulFallbackPromise = new Promise((resolve, reject) => { timeouts.push(setTimeout(async () => { - const twiml = new Twilio.twiml.VoiceResponse(); - dialFallbackTwiml(twiml, config); + const twiml = dialFallbackTwiml(config); console.error( invokeId, "UngracefulFallbackPromise: Cutting it too close to timeout! Skipping notifying users and calling fallback" ); diff --git a/packages/doorman-client/src/index.ts b/packages/doorman-client/src/index.ts new file mode 100644 index 0000000..92549b2 --- /dev/null +++ b/packages/doorman-client/src/index.ts @@ -0,0 +1,9 @@ +console.log("Building functions..."); +await Bun.build({ + entrypoints: ['./src/functions/buzzer-activated.ts'], + outdir: './build/functions', + packages: 'external', + naming: '[dir]/[name].[ext]' , + target: 'node', + format: 'cjs', +}); diff --git a/packages/doorman-client/src/types/BuzzerDialEvent.ts b/packages/doorman-client/src/types/BuzzerDialEvent.ts new file mode 100644 index 0000000..ef33289 --- /dev/null +++ b/packages/doorman-client/src/types/BuzzerDialEvent.ts @@ -0,0 +1,6 @@ +import { ServerlessEventObject } from "@twilio-labs/serverless-runtime-types/types"; + +export interface BuzzerDialEvent extends ServerlessEventObject { + From: string; + config?: string; // DoorConfig serialized as string +} \ No newline at end of file diff --git a/packages/doorman-client/src/types/DoorConfig.ts b/packages/doorman-client/src/types/DoorConfig.ts new file mode 100644 index 0000000..5a51b7a --- /dev/null +++ b/packages/doorman-client/src/types/DoorConfig.ts @@ -0,0 +1,6 @@ +export interface DoorConfig { + door: string; + pressKey: string; + fallbackNumbers: string[]; + discordUsers: string[]; +} diff --git a/packages/doorman-client/src/types/DoorStatusResponse.ts b/packages/doorman-client/src/types/DoorStatusResponse.ts new file mode 100644 index 0000000..5ab8c5c --- /dev/null +++ b/packages/doorman-client/src/types/DoorStatusResponse.ts @@ -0,0 +1,9 @@ +export enum DoorStatus { + OPEN = "OPEN", + CLOSED = "CLOSED", +} + +export interface DoorStatusResponse { + status: DoorStatus, + fingerprint: any; +} diff --git a/packages/doorman-client/src/types/TwilioContext.ts b/packages/doorman-client/src/types/TwilioContext.ts new file mode 100644 index 0000000..674ded7 --- /dev/null +++ b/packages/doorman-client/src/types/TwilioContext.ts @@ -0,0 +1,5 @@ +import { EnvironmentVariables } from "@twilio-labs/serverless-runtime-types/types"; + +export interface TwilioContext extends EnvironmentVariables { + DOORMAN_URL: string; +} diff --git a/packages/doorman-client/src/utils/DoormanUtils.ts b/packages/doorman-client/src/utils/DoormanUtils.ts new file mode 100644 index 0000000..9b6aa42 --- /dev/null +++ b/packages/doorman-client/src/utils/DoormanUtils.ts @@ -0,0 +1,21 @@ +import { TwilioContext } from "../types/TwilioContext"; +import { DoorConfig } from "../types/DoorConfig"; +import { lambdaFastHttp } from "./LambdaUtils"; + +export async function getConfig(context: TwilioContext, buzzer: string): Promise { + return await fetch(context.DOORMAN_URL + `/api/door/info?buzzer=${buzzer}`) + .then(res => res.json()) + .catch(err => { + return undefined; + }); +} + +export function notifyDiscord(context: TwilioContext, msg: string[], u: string[], optionalJsonStr: string[]){ + return lambdaFastHttp(context.DOORMAN_URL + + `/api/door/notify?discordUser=${encodeURIComponent(JSON.stringify(u))}&msg=${encodeURIComponent(JSON.stringify(msg))}&json=${encodeURIComponent(JSON.stringify(optionalJsonStr))}`, + ).catch(err => console.log(err)) +} + +export async function notifyAllDiscord(context: TwilioContext, config: DoorConfig, msg: string, optionalJsonStr: string = "") { + return notifyDiscord(context, config.discordUsers.map(() => msg), config.discordUsers, config.discordUsers.map(() => optionalJsonStr)); +} \ No newline at end of file diff --git a/packages/doorman-client/src/utils/LambdaUtils.ts b/packages/doorman-client/src/utils/LambdaUtils.ts new file mode 100644 index 0000000..9712336 --- /dev/null +++ b/packages/doorman-client/src/utils/LambdaUtils.ts @@ -0,0 +1,23 @@ +import https from "https"; +import http from "http"; + +/** + * Helper function to do an HTTP request and just await transmission, but not await a response. + * ref: https://www.sensedeep.com/blog/posts/stories/lambda-fast-http.html + * @param url - the URL to do HTTP request to + * @returns promise signalling HTTP request has been transmitted + */ +export async function lambdaFastHttp(url: string): Promise { + return new Promise((resolve, reject) => { + let req; + + if (url.startsWith("https://")) { + req = https.request(url); + } else { + req = http.request(url); + } + req.end(() => { + resolve(); + }); + }); +} \ No newline at end of file diff --git a/packages/doorman-client/src/utils/TwimlUtils.ts b/packages/doorman-client/src/utils/TwimlUtils.ts new file mode 100644 index 0000000..9bfdc95 --- /dev/null +++ b/packages/doorman-client/src/utils/TwimlUtils.ts @@ -0,0 +1,29 @@ +import VoiceResponse from 'twilio/lib/twiml/VoiceResponse'; +import { DoorConfig } from '../types/DoorConfig'; + +export function doorOpenTwiml(config: DoorConfig): VoiceResponse { + const twiml = new Twilio.twiml.VoiceResponse(); + + twiml.play('https://buzzer-2439-prod.twil.io/buzzing_up_boosted.mp3'); + twiml.play({ digits: config.pressKey }); // configured in doorman what button to click and passed into this function + twiml.pause({ length: 1 }); + twiml.hangup(); + // @ts-ignore + return twiml; +} + +export function dialFallbackTwiml(config: DoorConfig): VoiceResponse { + const twiml = new Twilio.twiml.VoiceResponse(); + + let dial = twiml.dial({ + timeLimit: 20, + timeout: 20 + }); + + config.fallbackNumbers.forEach(number => { + dial.number(number); + }); + + // @ts-ignore + return twiml; +}