Compare commits
No commits in common. "13e7cde7235e16e037166c54de307eacc84277b3" and "33d7764ca270b2edcf31cf72a21edc6242a69498" have entirely different histories.
13e7cde723
...
33d7764ca2
@ -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);
|
||||
});
|
||||
@ -50,7 +50,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, StatusRequestTw
|
||||
|
||||
setResponseJson(response, 200, body);
|
||||
|
||||
await db.entities.lockStatus.deleteById(getLockStatusID(door));
|
||||
// this hardcoded door, keep open for timeout
|
||||
if (door !== ONBOARDING_DOOR_NAME) {
|
||||
await db.entities.lockStatus.deleteById(getLockStatusID(door));
|
||||
}
|
||||
} else {
|
||||
const body = StatusResponseSchema.parse({
|
||||
status: DoorStatus.CLOSED,
|
||||
|
||||
@ -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"],
|
||||
};
|
||||
@ -4,7 +4,6 @@ import { DoorConfigEntity, EditDoorConfigEntity, OnboardDoorConfigEntity } from
|
||||
import { DoorAliasEntity } from "../schema/DoorAlias";
|
||||
import { LockStatusEntity } from "../schema/LockStatus";
|
||||
import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
|
||||
import { LogCallEntity } from "../schema/LogCall";
|
||||
|
||||
export const createDynaBridgeClient = (context: TwilioContext) => {
|
||||
let config: DynamoDBClientConfig = {
|
||||
@ -29,7 +28,6 @@ export const createDynaBridgeClient = (context: TwilioContext) => {
|
||||
doorAlias: DoorAliasEntity,
|
||||
lockStatus: LockStatusEntity,
|
||||
onboardDoorConfig: OnboardDoorConfigEntity,
|
||||
logCall: LogCallEntity
|
||||
}, {
|
||||
serialize: (entity) => entity,
|
||||
deserialize: (entity) => {
|
||||
|
||||
@ -3,8 +3,7 @@ import { waitForService, baseUrl, doorName, buzzerNumber, key, buzzerUrl } from
|
||||
import { DoorStatus } from "../src/types/DoorStatus";
|
||||
import { StatusResponse } from "../src/functions/api/door/status";
|
||||
import { sleep } from "bun";
|
||||
import { ONBOARDING_DOOR_NAME, ONBOARDING_DOOR_PIN } from "../src/schema/DoorConfig";
|
||||
import { LogCallResponse } from "../src/functions/api/door/logCall";
|
||||
import { InfoResponseClient } from "../src/functions/api/door/info";
|
||||
|
||||
// these tests should only run locally
|
||||
if (process.env.STAGE === 'staging') {
|
||||
@ -54,7 +53,7 @@ describe("unlock path works", () => {
|
||||
// unlock door with wrong code should be 401
|
||||
const badAuthResp = await fetch(baseUrl + `/api/door/auth?door=${doorName}&key=thisisthewrongkey`);
|
||||
expect(badAuthResp.status).toBe(401);
|
||||
|
||||
|
||||
// unlock door with correct code should be 200
|
||||
const authResp = await fetch(baseUrl + `/api/door/auth?door=${doorName}&key=${key}`);
|
||||
expect(authResp.status).toBe(200);
|
||||
@ -74,56 +73,15 @@ describe("unlock path works", () => {
|
||||
test("auth works for timeout", async () => {
|
||||
// run status first, to make sure we are closed
|
||||
const statusReset = await fetch(baseUrl + `/api/door/status?door=${doorName}`).then(res => res.json()) as StatusResponse;
|
||||
|
||||
|
||||
// run auth with timeout specified
|
||||
const authResp = await fetch(baseUrl + `/api/door/auth?door=${doorName}&key=${key}&timeout=1`);
|
||||
|
||||
|
||||
// sleep 1s
|
||||
await sleep(1_000);
|
||||
|
||||
|
||||
// we should be closed, because we passed the timeout
|
||||
const infoClosed = await fetch(baseUrl + `/api/door/info?door=${doorName}`).then(res => res.json()) as any;
|
||||
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);
|
||||
expect(infoClosed.status).toBe(DoorStatus.CLOSED);
|
||||
});
|
||||
});
|
||||
|
||||
@ -50,15 +50,15 @@ describe("onboardingflag door should exist", () => {
|
||||
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}`);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// close it
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -8,8 +8,8 @@ import {
|
||||
ServerlessFunctionSignature,
|
||||
} from '@twilio-labs/serverless-runtime-types/types';
|
||||
import { BuzzerDialEvent } from '../types/BuzzerDialEvent';
|
||||
import { getConfig, notifyAllDiscord, notifyDiscord, tryLogCallerForOnboarding } from '../utils/DoormanUtils';
|
||||
import { dialFallbackTwiml, doorOnboardTwiml, doorOpenTwiml } from '../utils/TwimlUtils';
|
||||
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';
|
||||
@ -39,31 +39,15 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
|
||||
}
|
||||
}
|
||||
|
||||
// if this is not configured
|
||||
// reject the call if this is not configured
|
||||
if (!config || !config.door) {
|
||||
// check if onboarding is enabled
|
||||
const callLog = await tryLogCallerForOnboarding(context, event.From);
|
||||
let twiml = new Twilio.twiml.VoiceResponse();
|
||||
twiml.reject();
|
||||
|
||||
// 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();
|
||||
twiml.reject();
|
||||
|
||||
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.CALL_REJECTED)
|
||||
.inc({ From: event.From }, 1);
|
||||
callback(null, twiml);
|
||||
return;
|
||||
}
|
||||
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.CALL_REJECTED)
|
||||
.inc({ From: event.From }, 1);
|
||||
callback(null, twiml);
|
||||
return;
|
||||
}
|
||||
|
||||
// let users know someone is currently buzzing, and allow unlock by discord user
|
||||
|
||||
@ -8,7 +8,6 @@ export enum BuzzerActivatedMetrics {
|
||||
POLL_ATTEMPTS = "PollAttempts",
|
||||
POLL_LATENCY = "PollLatency",
|
||||
NOTIFY_LATENCY = "NotifyLatency",
|
||||
ONBOARDING_ATTEMPT = "OnboardingAttempt",
|
||||
}
|
||||
|
||||
export const registerMetrics = (metricsRegistry: Registry) => {
|
||||
@ -18,12 +17,6 @@ export const registerMetrics = (metricsRegistry: Registry) => {
|
||||
labelNames: ["From"],
|
||||
}));
|
||||
|
||||
metricsRegistry.registerMetric(new Counter({
|
||||
name: BuzzerActivatedMetrics.ONBOARDING_ATTEMPT,
|
||||
help: "A non registered number attempted onboarding",
|
||||
labelNames: ["From"],
|
||||
}));
|
||||
|
||||
metricsRegistry.registerMetric(new Counter({
|
||||
name: BuzzerActivatedMetrics.API_UNLOCK,
|
||||
help: "Door was unlocked with the API",
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
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 { lambdaFastHttp } from "./LambdaUtils";
|
||||
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> {
|
||||
return await fetch(context.DOORMAN_URL + `/api/door/info?buzzer=${buzzer}`)
|
||||
@ -14,17 +12,9 @@ export async function getConfig(context: TwilioContext, buzzer: string): Promise
|
||||
}) 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 res = await lambdaFastHttp(context.DOORMAN_URL +
|
||||
const res = await lambdaFastHttp(context.DOORMAN_URL +
|
||||
`/api/door/notify?discordUser=${encodeURIComponent(JSON.stringify(u))}&msg=${encodeURIComponent(JSON.stringify(msg))}&json=${encodeURIComponent(JSON.stringify(optionalJsonStr))}&key=${context.NOTIFY_SECRET_KEY}`,
|
||||
).catch(err => console.log(err));
|
||||
endTimer();
|
||||
|
||||
@ -3,10 +3,10 @@ import { InfoResponseClient } from '../../../doorman-api/src/functions/api/door/
|
||||
|
||||
export function doorOpenTwiml(config: InfoResponseClient): VoiceResponse {
|
||||
const twiml = new Twilio.twiml.VoiceResponse();
|
||||
|
||||
|
||||
// play audio
|
||||
twiml.play('https://buzzer-2439-prod.twil.io/buzzing_up_boosted.mp3');
|
||||
|
||||
|
||||
// press digit
|
||||
twiml.play({ digits: config.pressKey }); // configured in doorman what button to click and passed into this function
|
||||
|
||||
@ -24,7 +24,7 @@ export function dialFallbackTwiml(config: InfoResponseClient): VoiceResponse {
|
||||
const twiml = new Twilio.twiml.VoiceResponse();
|
||||
|
||||
let dial = twiml.dial({
|
||||
timeLimit: 20,
|
||||
timeLimit: 20,
|
||||
timeout: 20
|
||||
});
|
||||
|
||||
@ -35,36 +35,3 @@ export function dialFallbackTwiml(config: InfoResponseClient): VoiceResponse {
|
||||
// @ts-ignore
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user