add onboarding flow to buzzer client
This commit is contained in:
parent
4975945306
commit
01243ce96c
70
packages/doorman-api/src/functions/api/door/logCall.ts
Normal file
70
packages/doorman-api/src/functions/api/door/logCall.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 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,10 +50,7 @@ export const handler: ServerlessFunctionSignature<TwilioContext, StatusRequestTw
|
|||||||
|
|
||||||
setResponseJson(response, 200, body);
|
setResponseJson(response, 200, body);
|
||||||
|
|
||||||
// this hardcoded door, keep open for timeout
|
await db.entities.lockStatus.deleteById(getLockStatusID(door));
|
||||||
if (door !== ONBOARDING_DOOR_NAME) {
|
|
||||||
await db.entities.lockStatus.deleteById(getLockStatusID(door));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const body = StatusResponseSchema.parse({
|
const body = StatusResponseSchema.parse({
|
||||||
status: DoorStatus.CLOSED,
|
status: DoorStatus.CLOSED,
|
||||||
|
|||||||
23
packages/doorman-api/src/schema/LogCall.ts
Normal file
23
packages/doorman-api/src/schema/LogCall.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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,6 +4,7 @@ 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 = {
|
||||||
@ -28,6 +29,7 @@ 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) => {
|
||||||
|
|||||||
@ -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 status check", async () => {
|
test("onboarding door does not close after info 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/status?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as StatusResponse;
|
let statusResp = await fetch(baseUrl + `/api/door/info?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as InfoResponseUI;
|
||||||
expect(statusResp.status).toBe(DoorStatus.OPEN);
|
expect(statusResp.status).toBe(DoorStatus.OPEN);
|
||||||
|
|
||||||
// open again
|
// open again
|
||||||
statusResp = await fetch(baseUrl + `/api/door/status?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as StatusResponse;
|
statusResp = await fetch(baseUrl + `/api/door/info?door=${ONBOARDING_DOOR_NAME}`).then(res => res.json()) as InfoResponseUI;
|
||||||
expect(statusResp.status).toBe(DoorStatus.OPEN);
|
expect(statusResp.status).toBe(DoorStatus.OPEN);
|
||||||
|
|
||||||
// close it
|
// close it
|
||||||
|
|||||||
@ -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 } from '../utils/DoormanUtils';
|
import { getConfig, notifyAllDiscord, notifyDiscord, tryLogCallerForOnboarding } from '../utils/DoormanUtils';
|
||||||
import { dialFallbackTwiml, doorOpenTwiml } from '../utils/TwimlUtils';
|
import { dialFallbackTwiml, doorOnboardTwiml, 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,15 +39,31 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reject the call if this is not configured
|
// if this is not configured
|
||||||
if (!config || !config.door) {
|
if (!config || !config.door) {
|
||||||
let twiml = new Twilio.twiml.VoiceResponse();
|
// check if onboarding is enabled
|
||||||
twiml.reject();
|
const callLog = await tryLogCallerForOnboarding(context, event.From);
|
||||||
|
|
||||||
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.CALL_REJECTED)
|
// onboarding is enabled, we got OTP to speak to user
|
||||||
.inc({ From: event.From }, 1);
|
if (callLog.otp) {
|
||||||
callback(null, twiml);
|
console.log(invokeId + " going to speak OTP to caller");
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@ -8,6 +8,7 @@ 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) => {
|
||||||
@ -17,6 +18,12 @@ 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",
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { register, Registry, Summary } from "prom-client";
|
import { register, Registry, Summary } from "prom-client";
|
||||||
import { InfoResponseClient } from "../../../doorman-api/src/functions/api/door/info";
|
import { InfoResponseClient, InfoResponseUI } 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}`)
|
||||||
@ -12,7 +14,15 @@ export async function getConfig(context: TwilioContext, buzzer: string): Promise
|
|||||||
}) as InfoResponseClient;
|
}) as InfoResponseClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function notifyDiscord(context: TwilioContext, msg: string[], u: string[], optionalJsonStr: string[], metricsRegistry: Registry){
|
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 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}`,
|
`/api/door/notify?discordUser=${encodeURIComponent(JSON.stringify(u))}&msg=${encodeURIComponent(JSON.stringify(msg))}&json=${encodeURIComponent(JSON.stringify(optionalJsonStr))}&key=${context.NOTIFY_SECRET_KEY}`,
|
||||||
|
|||||||
@ -35,3 +35,36 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user