141 lines
5.9 KiB
TypeScript
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);
|
|
});
|