change to typescript for client
All checks were successful
Build and push image for doorman-homeassistant / docker (push) Successful in 30s
Build and push Doorman UI / API / docker (push) Successful in 1m24s
Build and push image for doorman-homeassistant / deploy-gitainer (push) Successful in 24s

This commit is contained in:
Martin Dimitrov 2024-12-10 19:04:15 -08:00
parent 01847281ad
commit 804d5b678d
14 changed files with 152 additions and 83 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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"

View File

@ -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/*

View File

@ -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 */,

View File

@ -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"

View File

@ -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"
); );

View 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',
});

View 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
}

View File

@ -0,0 +1,6 @@
export interface DoorConfig {
door: string;
pressKey: string;
fallbackNumbers: string[];
discordUsers: string[];
}

View File

@ -0,0 +1,9 @@
export enum DoorStatus {
OPEN = "OPEN",
CLOSED = "CLOSED",
}
export interface DoorStatusResponse {
status: DoorStatus,
fingerprint: any;
}

View File

@ -0,0 +1,5 @@
import { EnvironmentVariables } from "@twilio-labs/serverless-runtime-types/types";
export interface TwilioContext extends EnvironmentVariables {
DOORMAN_URL: string;
}

View 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));
}

View 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();
});
});
}

View 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;
}