Martin Dimitrov a12c7bfb44
All checks were successful
Build and push image for doorman-homeassistant / docker (push) Successful in 53s
Build and push Doorman UI / API / docker (push) Successful in 1m35s
Build and push image for doorman-homeassistant / deploy-gitainer (push) Successful in 4s
add common handler decorator and metrics for buzzer client
2024-12-15 13:00:48 -08:00

141 lines
5.9 KiB
TypeScript

/**
* Doorman entrypoint
*/
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 VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
import { DoorStatus } from '../../../doorman-api/src/types/DoorStatus';
import { StatusResponse } from '../../../doorman-api/src/functions/api/door/status';
import { InfoResponseClient } from '../../../doorman-api/src/functions/api/door/info';
import { withMetrics } from '../../../doorman-api/src/common/DoormanHandler';
import { Counter, Summary } from 'prom-client';
import { BuzzerActivatedMetrics, registerMetrics } from '../metrics/BuzzerActivatedMetrics';
export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent> = withMetrics('buzzer-activated', async function(context, event, callback, metricsRegistry) {
// metrics
registerMetrics(metricsRegistry);
let invokeId = `[${randomUUID()}]`;
let configString = event.config;
let config: InfoResponseClient | undefined;
console.log(invokeId, "starting execution");
// get by api or parse it out from query
if (!configString) {
config = await getConfig(context, event.From);
} else {
try {
config = JSON.parse(configString);
} catch(e) {
config = await getConfig(context, event.From);
}
}
// reject the call if this is not configured
if (!config || !config.door) {
let twiml = new Twilio.twiml.VoiceResponse();
twiml.reject();
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.CALL_REJECTED) as Counter).inc({ From: event.From }, 1);
callback(null, twiml);
return;
}
// let users know someone is currently buzzing, and allow unlock by discord user
let msg = `🔔 Someone is dialing right now @ Door "${config.door}" [Click to unlock](${context.DOORMAN_URL}/api/door/auth?door=${config.door}&key=`;
let msgs = config.discordUsers.map((u) =>
msg + u + ')'
);
await notifyDiscord(context, msgs, config.discordUsers, config.discordUsers.map(() => ""), metricsRegistry);
let discordLock = false;
let intervals: Timer[] = [];
let timeouts: Timer[] = [];
const unlockPromise = new Promise<VoiceResponse>((resolve, reject) => {
intervals.push(setInterval(() => {
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.POLL_ATTEMPTS) as Counter).inc({ door: config.door }, 1);
const recordPollLatency = (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.POLL_LATENCY) as Summary).startTimer({ door: config.door });
fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`)
.then(res => res.json())
.then(async (rawBody) => {
let body = rawBody as StatusResponse;
recordPollLatency();
if (body?.status === DoorStatus.OPEN) {
clearInterval(intervals[0]);
const twiml = doorOpenTwiml(config);
if (!discordLock) {
discordLock = true;
console.log(
invokeId, "UnlockPromise: I was the fastest, so I will attempt to notify discord users before resolving with unlock"
);
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.API_UNLOCK) as Counter).inc({ door: config.door }, 1);
await notifyAllDiscord(context, config, `🔓 Doorman buzzed someone up @ Door "${config.door}"`, metricsRegistry, JSON.stringify(body.fingerprint));
resolve(twiml);
} else {
console.log(
invokeId, "UnlockPromise: dropping out of the race, graceful fallback is already notifiying discord users"
);
}
}
}).catch(err => console.log(invokeId, err));
}, 750));
});
const gracefulFallbackPromise = new Promise<VoiceResponse>((resolve, reject) => {
timeouts.push(setTimeout(async () => {
const twiml = dialFallbackTwiml(config);
if (!discordLock) {
discordLock = true;
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.DIAL_THROUGH) as Counter).inc({ door: config.door }, 1);
console.log(
invokeId, "GracefulFallbackPromise: I was the fastest, so I will attempt to notify discord users before resolving with a call"
);
await notifyAllDiscord(
context,
config,
`📞 Somebody buzzed the door and it dialed through to fallback phone numbers @ Door "${config.door}"`,
metricsRegistry,
);
resolve(twiml);
} else {
console.log(
invokeId, "GracefulFallbackPromise: dropping out of the race, unlock is already notifying discord users"
);
}
}, 8000));
});
const ungracefulFallbackPromise = new Promise<VoiceResponse>((resolve, reject) => {
timeouts.push(setTimeout(async () => {
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.RESULT_NOTIFICATION_FATE_UNKNOWN) as Counter).inc({ door: config.door }, 1);
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.DIAL_THROUGH) as Counter).inc({ door: config.door }, 1);
const twiml = dialFallbackTwiml(config);
console.error(
invokeId, "UngracefulFallbackPromise: Cutting it too close to timeout! Skipping notifying users and calling fallback"
);
resolve(twiml);
}, 9500));
});
const twiml = await Promise.race([unlockPromise, gracefulFallbackPromise, ungracefulFallbackPromise]);
console.log(invokeId, "Race ended, clearing residual timers");
timeouts.forEach(timeout => clearTimeout(timeout));
intervals.forEach(interval => clearInterval(interval));
callback(null, twiml);
});