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);
|
||||
|
||||
// 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') {
|
||||
@ -86,44 +85,3 @@ describe("unlock path works", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,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) {
|
||||
// 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();
|
||||
twiml.reject();
|
||||
|
||||
@ -64,7 +49,6 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
|
||||
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=`;
|
||||
|
||||
@ -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,14 +12,6 @@ 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){
|
||||
const endTimer = (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.NOTIFY_LATENCY) as Summary).startTimer();
|
||||
const res = await lambdaFastHttp(context.DOORMAN_URL +
|
||||
|
||||
@ -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