change to typescript for client
This commit is contained in:
parent
01847281ad
commit
804d5b678d
@ -9,7 +9,8 @@
|
|||||||
"scripts": {
|
"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",
|
"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-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": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"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/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
deprecated-functions
|
||||||
|
functions/*
|
||||||
@ -2,6 +2,7 @@
|
|||||||
"commands": {},
|
"commands": {},
|
||||||
"environments": {},
|
"environments": {},
|
||||||
"projects": {},
|
"projects": {},
|
||||||
|
"functionsFolder": "./build/functions",
|
||||||
// "assets": true /* Upload assets. Can be turned off with --no-assets */,
|
// "assets": true /* Upload assets. Can be turned off with --no-assets */,
|
||||||
// "assetsFolder": null /* Specific folder name to be used for static assets */,
|
// "assetsFolder": null /* Specific folder name to be used for static assets */,
|
||||||
// "buildSid": null /* An existing Build SID to deploy to the new environment */,
|
// "buildSid": null /* An existing Build SID to deploy to the new environment */,
|
||||||
|
|||||||
@ -4,7 +4,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"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"
|
"deploy": "twilio-run deploy --load-system-env --env .env.example --service-name buzzer --environment=prod --override-existing-project"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -14,8 +17,9 @@
|
|||||||
"twilio": "^3.56"
|
"twilio": "^3.56"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"twilio-run": "^3.5.4",
|
"@types/bun": "latest",
|
||||||
"@types/bun": "latest"
|
"concurrently": "^9.1.0",
|
||||||
|
"twilio-run": "^3.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18"
|
"node": "18"
|
||||||
|
|||||||
@ -1,79 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* Doorman entrypoint
|
* Doorman entrypoint
|
||||||
*/
|
*/
|
||||||
const { randomUUID } = require('crypto');
|
import { randomUUID } from 'crypto';
|
||||||
const fetch = require('node-fetch');
|
import fetch from "node-fetch";
|
||||||
const https = require('https');
|
import '@twilio-labs/serverless-runtime-types';
|
||||||
const http = require('http');
|
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';
|
||||||
|
|
||||||
/**
|
export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent> = async function(context, event, callback) {
|
||||||
* 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) {
|
|
||||||
let invokeId = `[${randomUUID()}]`;
|
let invokeId = `[${randomUUID()}]`;
|
||||||
let config = event.config;
|
let configString = event.config;
|
||||||
|
let config: DoorConfig | undefined;
|
||||||
console.log(invokeId, "starting execution");
|
console.log(invokeId, "starting execution");
|
||||||
|
|
||||||
// get by api or parse it out from query
|
// get by api or parse it out from query
|
||||||
if (!config) {
|
if (!configString) {
|
||||||
config = await getConfig(context, event.From);
|
config = await getConfig(context, event.From);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
config = JSON.parse(config);
|
config = JSON.parse(configString);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
config = await getConfig(context, event.From);
|
config = await getConfig(context, event.From);
|
||||||
}
|
}
|
||||||
@ -93,21 +46,22 @@ exports.handler = async function(context, event, callback) {
|
|||||||
msg + u + ')'
|
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 discordLock = false;
|
||||||
let intervals = [];
|
let intervals: Timer[] = [];
|
||||||
let timeouts = [];
|
let timeouts: Timer[] = [];
|
||||||
|
|
||||||
const unlockPromise = new Promise((resolve, reject) => {
|
const unlockPromise = new Promise<VoiceResponse>((resolve, reject) => {
|
||||||
intervals.push(setInterval(() => {
|
intervals.push(setInterval(() => {
|
||||||
fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`)
|
fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(async body => {
|
.then(async (rawBody) => {
|
||||||
if (body?.status === "OPEN") {
|
let body = rawBody as DoorStatusResponse;
|
||||||
|
if (body?.status === DoorStatus.OPEN) {
|
||||||
clearInterval(intervals[0]);
|
clearInterval(intervals[0]);
|
||||||
const twiml = new Twilio.twiml.VoiceResponse();
|
const twiml = doorOpenTwiml(config);
|
||||||
doorOpenTwiml(twiml, config);
|
|
||||||
if (!discordLock) {
|
if (!discordLock) {
|
||||||
discordLock = true;
|
discordLock = true;
|
||||||
console.log(
|
console.log(
|
||||||
@ -125,10 +79,9 @@ exports.handler = async function(context, event, callback) {
|
|||||||
}, 750));
|
}, 750));
|
||||||
});
|
});
|
||||||
|
|
||||||
const gracefulFallbackPromise = new Promise((resolve, reject) => {
|
const gracefulFallbackPromise = new Promise<VoiceResponse>((resolve, reject) => {
|
||||||
timeouts.push(setTimeout(async () => {
|
timeouts.push(setTimeout(async () => {
|
||||||
const twiml = new Twilio.twiml.VoiceResponse();
|
const twiml = dialFallbackTwiml(config);
|
||||||
dialFallbackTwiml(twiml, config);
|
|
||||||
|
|
||||||
if (!discordLock) {
|
if (!discordLock) {
|
||||||
discordLock = true;
|
discordLock = true;
|
||||||
@ -150,10 +103,9 @@ exports.handler = async function(context, event, callback) {
|
|||||||
}, 8000));
|
}, 8000));
|
||||||
});
|
});
|
||||||
|
|
||||||
const ungracefulFallbackPromise = new Promise((resolve, reject) => {
|
const ungracefulFallbackPromise = new Promise<VoiceResponse>((resolve, reject) => {
|
||||||
timeouts.push(setTimeout(async () => {
|
timeouts.push(setTimeout(async () => {
|
||||||
const twiml = new Twilio.twiml.VoiceResponse();
|
const twiml = dialFallbackTwiml(config);
|
||||||
dialFallbackTwiml(twiml, config);
|
|
||||||
console.error(
|
console.error(
|
||||||
invokeId, "UngracefulFallbackPromise: Cutting it too close to timeout! Skipping notifying users and calling fallback"
|
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