/** * 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 = 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((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((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((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); });