change to typescript for client
This commit is contained in:
parent
01847281ad
commit
804d5b678d
@ -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"
|
||||
|
||||
3
packages/doorman-client/.gitignore
vendored
3
packages/doorman-client/.gitignore
vendored
@ -131,3 +131,6 @@ dist
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
deprecated-functions
|
||||
functions/*
|
||||
@ -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 */,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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<TwilioContext, BuzzerDialEvent> = 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<VoiceResponse>((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<VoiceResponse>((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<VoiceResponse>((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"
|
||||
);
|
||||
9
packages/doorman-client/src/index.ts
Normal file
9
packages/doorman-client/src/index.ts
Normal file
@ -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',
|
||||
});
|
||||
6
packages/doorman-client/src/types/BuzzerDialEvent.ts
Normal file
6
packages/doorman-client/src/types/BuzzerDialEvent.ts
Normal file
@ -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
|
||||
}
|
||||
6
packages/doorman-client/src/types/DoorConfig.ts
Normal file
6
packages/doorman-client/src/types/DoorConfig.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface DoorConfig {
|
||||
door: string;
|
||||
pressKey: string;
|
||||
fallbackNumbers: string[];
|
||||
discordUsers: string[];
|
||||
}
|
||||
9
packages/doorman-client/src/types/DoorStatusResponse.ts
Normal file
9
packages/doorman-client/src/types/DoorStatusResponse.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum DoorStatus {
|
||||
OPEN = "OPEN",
|
||||
CLOSED = "CLOSED",
|
||||
}
|
||||
|
||||
export interface DoorStatusResponse {
|
||||
status: DoorStatus,
|
||||
fingerprint: any;
|
||||
}
|
||||
5
packages/doorman-client/src/types/TwilioContext.ts
Normal file
5
packages/doorman-client/src/types/TwilioContext.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { EnvironmentVariables } from "@twilio-labs/serverless-runtime-types/types";
|
||||
|
||||
export interface TwilioContext extends EnvironmentVariables {
|
||||
DOORMAN_URL: string;
|
||||
}
|
||||
21
packages/doorman-client/src/utils/DoormanUtils.ts
Normal file
21
packages/doorman-client/src/utils/DoormanUtils.ts
Normal file
@ -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<DoorConfig | undefined> {
|
||||
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));
|
||||
}
|
||||
23
packages/doorman-client/src/utils/LambdaUtils.ts
Normal file
23
packages/doorman-client/src/utils/LambdaUtils.ts
Normal file
@ -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<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let req;
|
||||
|
||||
if (url.startsWith("https://")) {
|
||||
req = https.request(url);
|
||||
} else {
|
||||
req = http.request(url);
|
||||
}
|
||||
req.end(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
29
packages/doorman-client/src/utils/TwimlUtils.ts
Normal file
29
packages/doorman-client/src/utils/TwimlUtils.ts
Normal file
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user