Compare commits

..

No commits in common. "13e7cde7235e16e037166c54de307eacc84277b3" and "33d7764ca270b2edcf31cf72a21edc6242a69498" have entirely different histories.

24 changed files with 28 additions and 228 deletions

View File

@ -1,70 +0,0 @@
/**
* Try to get door info
*/
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { ONBOARDING_DOOR_NAME } from "../../../schema/DoorConfig";
import { getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
import { withMetrics } from "../../../common/DoormanHandler";
import { z } from "zod";
import { UserAgentHeader } from "../../../utils/blockUserAgent";
import { setResponseJson } from "../../../utils/responseUtils";
import { LOG_CALL_SK, LogCallSchema } from "../../../schema/LogCall";
export const LogCallRequestSchema = z.object({
caller: z.string(),
});
export type LogCallRequest = z.infer<typeof LogCallRequestSchema>;
export interface LogCallRequestTwilio extends ServerlessEventObject<LogCallRequest, UserAgentHeader> { };
export const LogCallResponseSchema = z.object({
otp: z.string(),
});
export type LogCallResponse = z.infer<typeof LogCallResponseSchema>;
function getCode() {
return `${Math.floor(Math.random() * 10000)}`.padStart(4, '0');
};
export const handler: ServerlessFunctionSignature<TwilioContext, LogCallRequestTwilio> = withMetrics("logCall", async (context, event, callback, metricsRegistry) => {
const response = new Twilio.Response();
const req = LogCallRequestSchema.parse(event);
let caller = req.caller;
const db = createDynaBridgeClient(context);
// check if onboarding is actually open
const lock = await db.entities.lockStatus.findById(getLockStatusID(ONBOARDING_DOOR_NAME));
if (!isLockOpen(lock)) {
setResponseJson(response, 400, {
msg: "Onboarding is not open",
});
} else {
// log this caller
const otp = getCode();
const logCall = LogCallSchema.parse({
PK: otp,
SK: LOG_CALL_SK,
caller,
TTL: Date.now() + 60 * 60 * 1000, // 60 minutes from now
});
await db.entities.logCall.save(logCall);
setResponseJson(response, 200, {
otp,
});
}
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
return callback(null, response);
});

View File

@ -50,7 +50,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, StatusRequestTw
setResponseJson(response, 200, body); setResponseJson(response, 200, body);
// this hardcoded door, keep open for timeout
if (door !== ONBOARDING_DOOR_NAME) {
await db.entities.lockStatus.deleteById(getLockStatusID(door)); await db.entities.lockStatus.deleteById(getLockStatusID(door));
}
} else { } else {
const body = StatusResponseSchema.parse({ const body = StatusResponseSchema.parse({
status: DoorStatus.CLOSED, status: DoorStatus.CLOSED,

View File

@ -1,23 +0,0 @@
import { z } from "zod";
import { DynaBridgeEntity } from 'dynabridge';
export const LOG_CALL_SK = "log-call";
export const LogCallSchema = z.object({
// keys
PK: z.string(), // OTP
SK: z.literal(LOG_CALL_SK).default(LOG_CALL_SK),
caller: z.string(),
TTL: z.number(),
});
export const getLogCallID = (otp: string): string[] => {
return [otp, LOG_CALL_SK];
}
export type LogCall = z.infer<typeof LogCallSchema>;
export const LogCallEntity: DynaBridgeEntity<LogCall> = {
tableName: "doorman",
id: ["PK", "SK"],
};

View File

@ -4,7 +4,6 @@ import { DoorConfigEntity, EditDoorConfigEntity, OnboardDoorConfigEntity } from
import { DoorAliasEntity } from "../schema/DoorAlias"; import { DoorAliasEntity } from "../schema/DoorAlias";
import { LockStatusEntity } from "../schema/LockStatus"; import { LockStatusEntity } from "../schema/LockStatus";
import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb"; import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
import { LogCallEntity } from "../schema/LogCall";
export const createDynaBridgeClient = (context: TwilioContext) => { export const createDynaBridgeClient = (context: TwilioContext) => {
let config: DynamoDBClientConfig = { let config: DynamoDBClientConfig = {
@ -29,7 +28,6 @@ export const createDynaBridgeClient = (context: TwilioContext) => {
doorAlias: DoorAliasEntity, doorAlias: DoorAliasEntity,
lockStatus: LockStatusEntity, lockStatus: LockStatusEntity,
onboardDoorConfig: OnboardDoorConfigEntity, onboardDoorConfig: OnboardDoorConfigEntity,
logCall: LogCallEntity
}, { }, {
serialize: (entity) => entity, serialize: (entity) => entity,
deserialize: (entity) => { deserialize: (entity) => {

View File

@ -3,8 +3,7 @@ import { waitForService, baseUrl, doorName, buzzerNumber, key, buzzerUrl } from
import { DoorStatus } from "../src/types/DoorStatus"; import { DoorStatus } from "../src/types/DoorStatus";
import { StatusResponse } from "../src/functions/api/door/status"; import { StatusResponse } from "../src/functions/api/door/status";
import { sleep } from "bun"; import { sleep } from "bun";
import { ONBOARDING_DOOR_NAME, ONBOARDING_DOOR_PIN } from "../src/schema/DoorConfig"; import { InfoResponseClient } from "../src/functions/api/door/info";
import { LogCallResponse } from "../src/functions/api/door/logCall";
// these tests should only run locally // these tests should only run locally
if (process.env.STAGE === 'staging') { if (process.env.STAGE === 'staging') {
@ -86,44 +85,3 @@ describe("unlock path works", () => {
expect(infoClosed.status).toBe(DoorStatus.CLOSED); expect(infoClosed.status).toBe(DoorStatus.CLOSED);
}); });
}); });
describe("call log path works", () => {
test("call log returns nothing when onboarding is not enabled", async () => {
// run status first, to make sure we are closed
const statusReset = await fetch(baseUrl + `/api/door/status?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as StatusResponse;
// try to log call
const logCallRes = await fetch(baseUrl + `/api/door/callLog?caller=${buzzerNumber}`);
expect(logCallRes.status).toBe(400);
});
test("call log returns 4 digit OTP when onboarding enabled", async () => {
// run status first, to make sure we are closed
const statusReset = await fetch(baseUrl + `/api/door/status?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as StatusResponse;
// run auth with timeout specified
const authResp = await fetch(baseUrl + `/api/door/auth?door=${ONBOARDING_DOOR_NAME}&key=${ONBOARDING_DOOR_PIN}`);
// try to log call
const logCallRes = await fetch(baseUrl + `/api/door/callLog?caller=${buzzerNumber}`);
expect(logCallRes.status).toBe(200);
const otp = (await logCallRes.json() as LogCallResponse).otp
expect(otp.length).toBe(4)
});
test("call log after door closed, should not return OTP", async () => {
// run status first, to make sure we are closed
const statusReset = await fetch(baseUrl + `/api/door/status?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as StatusResponse;
// run auth with timeout specified
const authResp = await fetch(baseUrl + `/api/door/auth?door=${ONBOARDING_DOOR_NAME}&key=${ONBOARDING_DOOR_PIN}&timeout=1`);
// sleep 1s
await sleep(1_000);
// try to log call
const logCallRes = await fetch(baseUrl + `/api/door/callLog?caller=${buzzerNumber}`);
expect(logCallRes.status).toBe(400);
});
});

View File

@ -50,15 +50,15 @@ describe("onboardingflag door should exist", () => {
expect(door.status).toBe(DoorStatus.CLOSED); expect(door.status).toBe(DoorStatus.CLOSED);
}); });
test("onboarding door does not close after info check", async () => { test("onboarding door does not close after status check", async () => {
let authResp = await fetch(baseUrl + `/api/door/auth?door=${ONBOARDING_DOOR_NAME}&key=${ONBOARDING_DOOR_PIN}`); let authResp = await fetch(baseUrl + `/api/door/auth?door=${ONBOARDING_DOOR_NAME}&key=${ONBOARDING_DOOR_PIN}`);
expect(authResp.status).toBe(200); expect(authResp.status).toBe(200);
let statusResp = await fetch(baseUrl + `/api/door/info?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as InfoResponseUI; let statusResp = await fetch(baseUrl + `/api/door/status?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as StatusResponse;
expect(statusResp.status).toBe(DoorStatus.OPEN); expect(statusResp.status).toBe(DoorStatus.OPEN);
// open again // open again
statusResp = await fetch(baseUrl + `/api/door/info?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as InfoResponseUI; statusResp = await fetch(baseUrl + `/api/door/status?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as StatusResponse;
expect(statusResp.status).toBe(DoorStatus.OPEN); expect(statusResp.status).toBe(DoorStatus.OPEN);
// close it // close it

View File

@ -8,8 +8,8 @@ import {
ServerlessFunctionSignature, ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types'; } from '@twilio-labs/serverless-runtime-types/types';
import { BuzzerDialEvent } from '../types/BuzzerDialEvent'; import { BuzzerDialEvent } from '../types/BuzzerDialEvent';
import { getConfig, notifyAllDiscord, notifyDiscord, tryLogCallerForOnboarding } from '../utils/DoormanUtils'; import { getConfig, notifyAllDiscord, notifyDiscord } from '../utils/DoormanUtils';
import { dialFallbackTwiml, doorOnboardTwiml, doorOpenTwiml } from '../utils/TwimlUtils'; import { dialFallbackTwiml, doorOpenTwiml } from '../utils/TwimlUtils';
import { TwilioContext } from '../types/TwilioContext'; import { TwilioContext } from '../types/TwilioContext';
import VoiceResponse from 'twilio/lib/twiml/VoiceResponse'; import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
import { DoorStatus } from '../../../doorman-api/src/types/DoorStatus'; import { DoorStatus } from '../../../doorman-api/src/types/DoorStatus';
@ -39,23 +39,8 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
} }
} }
// if this is not configured // reject the call if this is not configured
if (!config || !config.door) { if (!config || !config.door) {
// check if onboarding is enabled
const callLog = await tryLogCallerForOnboarding(context, event.From);
// onboarding is enabled, we got OTP to speak to user
if (callLog.otp) {
console.log(invokeId + " going to speak OTP to caller");
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.ONBOARDING_ATTEMPT)
.inc({ From: event.From }, 1);
const response = doorOnboardTwiml(callLog.otp);
callback(null, response);
return;
} else {
// no onboarding, and this is an unknown caller. Just reject
let twiml = new Twilio.twiml.VoiceResponse(); let twiml = new Twilio.twiml.VoiceResponse();
twiml.reject(); twiml.reject();
@ -64,7 +49,6 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
callback(null, twiml); callback(null, twiml);
return; return;
} }
}
// let users know someone is currently buzzing, and allow unlock by discord user // 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 msg = `🔔 Someone is dialing right now @ Door "${config.door}" [Click to unlock](${context.DOORMAN_URL}/api/door/auth?door=${config.door}&key=`;

View File

@ -8,7 +8,6 @@ export enum BuzzerActivatedMetrics {
POLL_ATTEMPTS = "PollAttempts", POLL_ATTEMPTS = "PollAttempts",
POLL_LATENCY = "PollLatency", POLL_LATENCY = "PollLatency",
NOTIFY_LATENCY = "NotifyLatency", NOTIFY_LATENCY = "NotifyLatency",
ONBOARDING_ATTEMPT = "OnboardingAttempt",
} }
export const registerMetrics = (metricsRegistry: Registry) => { export const registerMetrics = (metricsRegistry: Registry) => {
@ -18,12 +17,6 @@ export const registerMetrics = (metricsRegistry: Registry) => {
labelNames: ["From"], labelNames: ["From"],
})); }));
metricsRegistry.registerMetric(new Counter({
name: BuzzerActivatedMetrics.ONBOARDING_ATTEMPT,
help: "A non registered number attempted onboarding",
labelNames: ["From"],
}));
metricsRegistry.registerMetric(new Counter({ metricsRegistry.registerMetric(new Counter({
name: BuzzerActivatedMetrics.API_UNLOCK, name: BuzzerActivatedMetrics.API_UNLOCK,
help: "Door was unlocked with the API", help: "Door was unlocked with the API",

View File

@ -1,10 +1,8 @@
import { register, Registry, Summary } from "prom-client"; import { register, Registry, Summary } from "prom-client";
import { InfoResponseClient, InfoResponseUI } from "../../../doorman-api/src/functions/api/door/info"; import { InfoResponseClient } from "../../../doorman-api/src/functions/api/door/info";
import { TwilioContext } from "../types/TwilioContext"; import { TwilioContext } from "../types/TwilioContext";
import { lambdaFastHttp } from "./LambdaUtils"; import { lambdaFastHttp } from "./LambdaUtils";
import { BuzzerActivatedMetrics } from "../metrics/BuzzerActivatedMetrics"; import { BuzzerActivatedMetrics } from "../metrics/BuzzerActivatedMetrics";
import { ONBOARDING_DOOR_NAME } from "../../../doorman-api/src/schema/DoorConfig";
import { LogCallResponse } from "../../../doorman-api/src/functions/api/door/logCall";
export async function getConfig(context: TwilioContext, buzzer: string): Promise<InfoResponseClient | undefined> { export async function getConfig(context: TwilioContext, buzzer: string): Promise<InfoResponseClient | undefined> {
return await fetch(context.DOORMAN_URL + `/api/door/info?buzzer=${buzzer}`) return await fetch(context.DOORMAN_URL + `/api/door/info?buzzer=${buzzer}`)
@ -14,14 +12,6 @@ export async function getConfig(context: TwilioContext, buzzer: string): Promise
}) as InfoResponseClient; }) as InfoResponseClient;
} }
export async function tryLogCallerForOnboarding(context: TwilioContext, caller: string): Promise<LogCallResponse> {
return await fetch(context.DOORMAN_URL + `/api/door/logCall?caller=${caller}`)
.then(res => res.json())
.catch(err => {
return undefined;
}) as LogCallResponse;
}
export async function notifyDiscord(context: TwilioContext, msg: string[], u: string[], optionalJsonStr: string[], metricsRegistry: Registry){ export async function notifyDiscord(context: TwilioContext, msg: string[], u: string[], optionalJsonStr: string[], metricsRegistry: Registry){
const endTimer = (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.NOTIFY_LATENCY) as Summary).startTimer(); const endTimer = (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.NOTIFY_LATENCY) as Summary).startTimer();
const res = await lambdaFastHttp(context.DOORMAN_URL + const res = await lambdaFastHttp(context.DOORMAN_URL +

View File

@ -35,36 +35,3 @@ export function dialFallbackTwiml(config: InfoResponseClient): VoiceResponse {
// @ts-ignore // @ts-ignore
return twiml; return twiml;
} }
export function doorOnboardTwiml(otp: string): VoiceResponse {
const twiml = new Twilio.twiml.VoiceResponse();
const prefix = 'https://buzzer-2439-prod.twil.io/';
// play intro
twiml.play(prefix + 'onboarding-intro.mp3');
twiml.pause({ length: 3 });
// play digits audio
otp.split('').forEach(digit => {
twiml.play(prefix + digit + ".mp3");
});
twiml.pause({ length: 3 });
// play repeat
twiml.play(prefix + 'onboarding-repeat.mp3');
twiml.pause({ length: 3 });
// play digits audio
otp.split('').forEach(digit => {
twiml.play(prefix + digit + ".mp3");
});
twiml.pause({ length: 3 });
// @ts-ignore
return twiml;
}