migrate api to typescript
All checks were successful
Build and push image for doorman-homeassistant / docker (push) Successful in 26s
Build and push Doorman UI / API / docker (push) Successful in 1m24s
Build and push image for doorman-homeassistant / deploy-gitainer (push) Successful in 23s

This commit is contained in:
Martin Dimitrov 2024-12-10 21:01:55 -08:00
parent 804d5b678d
commit e2da767c86
23 changed files with 501 additions and 377 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -133,3 +133,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
functions/*
deprecated-functions

View File

@ -2,6 +2,7 @@
"commands": {},
"environments": {},
"projects": {},
"functionsFolder": "./build/functions",
// "assets": true /* Upload assets. Can be turned off with --no-assets */,
// "assetsFolder": null /* Specific folder name to be used for static assets */,
// "buildSid": null /* An existing Build SID to deploy to the new environment */,

View File

@ -1,99 +0,0 @@
/**
* Try to unlock the door with auth mode
*/
exports.handler = async function(context, event, callback) {
const response = new Twilio.Response();
const blockPath = Runtime.getFunctions()['common/blockUserAgent'].path;
const block = require(blockPath);
if (block.shouldBlockRequest(event)) {
response.setStatusCode(200);
return callback(null, response);
}
let door = event.door;
let pin = event.key;
if (!door || !pin) {
response.setStatusCode(400);
return callback(null, response);
}
const ddbPath = Runtime.getFunctions()['common/ddb'].path;
const ddb = require(ddbPath);
const client = ddb.createDDBClient(context);
const config = await client.send(ddb.getDoorConfigCommand(door));
if (!config.Item) {
response.setStatusCode(404);
return callback(null, response);
}
let correctPin = config.Item.pin.S;
let discordUsers = config.Item.discordUsers.SS;
let method;
if (correctPin === pin) {
method = "PIN";
}
if (discordUsers.includes(pin)) {
method = "DISCORD";
}
if (!method) {
response.setStatusCode(401);
return callback(null, response);
}
const fingerprint = {
method,
userAgent: event.request.headers['user-agent'],
ip: event.ip,
};
// take timeout from the query string
const timeout = event.timeout ? parseInt(event.timeout) : config.Item.timeout.N;
// check lock status if locked, then unlock. If unlocked then lock
await client.send(ddb.getLockStatusCommand(door))
.then(async (lock) => {
const isOpen = ddb.isLockOpen(lock);
if (isOpen) {
const fingerprint = JSON.parse(lock.Item.fingerprint.S);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: "CLOSED",
fingerprint,
});
await client.send(ddb.clearLockStatusCommand(lock));
return;
}
await client.send(ddb.setLockStatusCommand(door, timeout, fingerprint))
.then(async (item) => {
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: `Opened the door "${door}" for ${timeout}s` });
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
});
});
await client.destroy();
return callback(null, response);
};

View File

@ -1,105 +0,0 @@
/**
* Edit API for doors
*/
exports.handler = async function(context, event, callback) {
const response = new Twilio.Response();
const blockPath = Runtime.getFunctions()['common/blockUserAgent'].path;
const block = require(blockPath);
if (block.shouldBlockRequest(event)) {
response.setStatusCode(200);
return callback(null, response);
}
let door = event.door;
let approvalId = event.approvalId;
let newConfig = event.newConfig;
const ddbPath = Runtime.getFunctions()['common/ddb'].path;
const discordPath = Runtime.getFunctions()['common/discord'].path;
const ddb = require(ddbPath);
const discord = require(discordPath);
const client = ddb.createDDBClient(context);
// approve path
if (door && approvalId) {
const newConfig = await client.send(ddb.getDoorConfigUpdateCommand(door));
if (!newConfig || newConfig.Item.approvalId.S !== approvalId) {
response.setStatusCode(400);
return callback(null, response);
}
await client.send(ddb.replaceDoorConfigWithUpdateItem(newConfig));
// send update to discord users
const updateMessage = `Configuration change \`${approvalId}\` was approved @ Door "${door}"`;
const discordPromises = newConfig.Item.discordUsers.SS.map((user) => {
return discord.sendMessageToUser(
context,
user,
updateMessage,
).catch(e => console.error(e))
});
await Promise.all(discordPromises);
response.setStatusCode(200);
return callback(null, response);
}
if (!door || !newConfig) {
response.setStatusCode(400);
return callback(null, response);
}
newConfig = JSON.parse(event.newConfig);
const config = await client.send(ddb.getDoorConfigCommand(door));
if (!config.Item) {
response.setStatusCode(404);
return callback(null, response);
}
// set to old PIN if it is missing
if (newConfig.pin === "") {
newConfig.pin = config.Item.pin.S;
}
const input = ddb.putDoorUpdateConfigCommand(door, newConfig);
const update = await client.send(input);
newConfig.discordUser = undefined;
newConfig.fallbackNumber = undefined;
newConfig.status = undefined;
const approvalUrl = `https://doorman.chromart.cc/api/door/edit?door=${door}&approvalId=${input.input.Item.approvalId.S}`;
console.log(approvalUrl);
// send update to discord users
const approvalMessage = `Configuration change requested @ Door "${door}" [click to approve it](${approvalUrl})\`\`\`${JSON.stringify(newConfig, null, 4)}\`\`\``;
const discordPromises = config.Item.discordUsers.SS.map((user) => {
return discord.sendMessageToUser(
context,
user,
approvalMessage,
).catch(e => console.error(e))
});
await Promise.all(discordPromises);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: update });
await client.destroy();
return callback(null, response);
};

View File

@ -1,54 +0,0 @@
function jsonMsgSuffix(jsonString) {
if (!jsonString) {
return "";
}
try {
const fingerprint = JSON.parse(jsonString);
return `\`\`\`# Unlocked by\n${JSON.stringify(fingerprint, null, 4)}\`\`\``;
} catch (e) {
return `\`\`\`# Unlocked by\n# WARN: Unknown or corrupt raw fingerprint:\n ${jsonString}\`\`\``;
}
}
exports.handler = async function(context, event, callback) {
const response = new Twilio.Response();
const discordPath = Runtime.getFunctions()['common/discord'].path;
const discord = require(discordPath);
let users = event.discordUser;
let msgs = event.msg;
let jsons = event.json;
let promises = [];
try {
users = JSON.parse(users);
msgs = JSON.parse(msgs);
console.log("before parsing", jsons);
jsons = JSON.parse(jsons);
console.log("after parsing", jsons);
promises = msgs.map((msg, i) =>
discord.sendMessageToUser(
context,
users[i],
msg + jsonMsgSuffix(jsons[i])
).catch(e => console.error(e))
);
} catch (e) {
console.error(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e, event });
return callback(null, response);
}
let timer = setTimeout(() => {
console.log("Ungraceful finish: running out of time");
callback(null, response);
}, 9500);
await Promise.all(promises);
clearTimeout(timer);
return callback(null, response);
};

View File

@ -1,64 +0,0 @@
/**
* Try to update door status
*/
exports.handler = async function(context, event, callback) {
const response = new Twilio.Response();
const blockPath = Runtime.getFunctions()['common/blockUserAgent'].path;
const block = require(blockPath);
if (block.shouldBlockRequest(event)) {
response.setStatusCode(200);
return callback(null, response);
}
const door = event.door;
if (!door) {
response.setStatusCode(400);
return callback(null, response);
}
const ddbPath = Runtime.getFunctions()['common/ddb'].path;
const ddb = require(ddbPath);
const client = ddb.createDDBClient(context);
await client.send(ddb.getLockStatusCommand(door))
.then(async (lock) => {
const isOpen = ddb.isLockOpen(lock);
if (isOpen) {
const fingerprint = JSON.parse(lock.Item.fingerprint.S);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: "OPEN",
fingerprint,
});
await client.send(ddb.clearLockStatusCommand(lock));
return;
}
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: "CLOSED",
});
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
});
await client.destroy();
return callback(null, response);
};

View File

@ -4,17 +4,22 @@
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "twilio-run --live --port 8080",
"start-twilio": "twilio-run --live --port 8080",
"watch-build": "bun run --watch src/index.ts",
"start": "concurrently \"bun run watch-build\" \"bun run start-twilio\"",
"build": "bun run src/index.ts",
"deploy": "twilio-run deploy --load-system-env --env .env.example --service-name doorman --environment=prod --override-existing-project"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.609.0",
"@twilio-labs/serverless-runtime-types": "^4.0.1",
"@twilio/runtime-handler": "1.3.0",
"discord.js": "^14.16.3",
"twilio": "^3.56"
},
"devDependencies": {
"twilio-run": "^3.5.4"
"twilio-run": "^3.5.4",
"concurrently": "^9.1.0"
},
"engines": {
"node": "18"

View File

@ -0,0 +1,112 @@
/**
* Try to unlock the door with auth mode
*/
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { shouldBlockRequest, UserAgentHeader } from "../../../utils/blockUserAgent";
import { clearLockStatusCommand, createDDBClient, ddbItemToJSON, getDoorConfigCommand, getLockStatusCommand, isLockOpen, setLockStatusCommand } from "../../../utils/ddb";
import { DoorConfig } from "../../../types/DoorConfig";
import { AuthMethod } from "../../../types/AuthMethod";
import { Lock } from "../../../types/Lock";
import { DoorStatus } from "../../../types/DoorStatus";
export interface AuthRequest extends ServerlessEventObject<{}, UserAgentHeader> {
door?: string;
key?: string;
ip: string;
timeout?: string;
}
export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> = async function(context, event, callback) {
const response = new Twilio.Response();
if (shouldBlockRequest(event)) {
response.setStatusCode(200);
return callback(null, response);
}
let door = event.door;
let pin = event.key;
if (!door || !pin) {
response.setStatusCode(400);
return callback(null, response);
}
const client = createDDBClient(context);
const ddbConfig = await client.send(getDoorConfigCommand(door));
const config: DoorConfig = ddbItemToJSON<DoorConfig>(ddbConfig);
if (!config) {
response.setStatusCode(404);
return callback(null, response);
}
let correctPin = config.pin;
let discordUsers = config.discordUsers;
let method: AuthMethod | undefined;
if (correctPin === pin) {
method = AuthMethod.PIN;
}
if (discordUsers.includes(pin)) {
method = AuthMethod.DISCORD;
}
if (!method) {
response.setStatusCode(401);
return callback(null, response);
}
const fingerprint = {
method,
userAgent: event.request.headers['user-agent'],
ip: event.ip,
};
// take timeout from the query string
const timeout = event.timeout ? parseInt(event.timeout) : config.timeout;
// check lock status if locked, then unlock. If unlocked then lock
await client.send(getLockStatusCommand(door))
.then(async (lockDdb) => {
const isOpen = isLockOpen(lockDdb);
if (isOpen) {
const lock: Lock = ddbItemToJSON<Lock>(lockDdb);
const fingerprint = JSON.parse(lock.fingerprint);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
fingerprint,
});
await client.send(clearLockStatusCommand(lockDdb));
return;
}
await client.send(setLockStatusCommand(door, timeout, fingerprint))
.then(async (item) => {
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: `Opened the door "${door}" for ${timeout}s` });
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
});
});
await client.destroy();
return callback(null, response);
};

View File

@ -0,0 +1,113 @@
/**
* Edit API for doors
*/
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { shouldBlockRequest, UserAgentHeader } from "../../../utils/blockUserAgent";
import { createDDBClient, ddbItemToJSON, getDoorConfigCommand, getDoorConfigUpdateCommand, putDoorUpdateConfigCommand, replaceDoorConfigWithUpdateItem } from "../../../utils/ddb";
import { DoorConfig, EditDoorConfig } from "../../../types/DoorConfig";
import { sendMessageToUser } from "../../../utils/discord";
export interface EditRequest extends ServerlessEventObject<{}, UserAgentHeader> {
door?: string;
approvalId?: string;
newConfig?: string;
}
export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> = async function(context, event, callback) {
const response = new Twilio.Response();
if (shouldBlockRequest(event)) {
response.setStatusCode(200);
return callback(null, response);
}
let door = event.door;
let approvalId = event.approvalId;
let newConfigString = event.newConfig;
const client = createDDBClient(context);
// approve path
if (door && approvalId) {
const newConfigDdb = await client.send(getDoorConfigUpdateCommand(door));
const newConfig = ddbItemToJSON<EditDoorConfig>(newConfigDdb);
if (!newConfig || newConfig.approvalId !== approvalId) {
response.setStatusCode(400);
return callback(null, response);
}
await client.send(replaceDoorConfigWithUpdateItem(newConfigDdb as any));
// send update to discord users
const updateMessage = `Configuration change \`${approvalId}\` was approved @ Door "${door}"`;
const discordPromises = newConfig.discordUsers.map((user) => {
return sendMessageToUser(
context,
user,
updateMessage,
).catch(e => console.error(e))
});
await Promise.all(discordPromises);
response.setStatusCode(200);
return callback(null, response);
}
if (!door || !newConfigString) {
response.setStatusCode(400);
return callback(null, response);
}
const newConfig: EditDoorConfig = JSON.parse(newConfigString);
const configDdb = await client.send(getDoorConfigCommand(door));
const config = ddbItemToJSON<DoorConfig>(configDdb);
if (!config) {
response.setStatusCode(404);
return callback(null, response);
}
// set to old PIN if it is missing
if (newConfig.pin === "") {
newConfig.pin = config.pin;
}
const input = putDoorUpdateConfigCommand(door, newConfig);
const update = await client.send(input);
// newConfig.discordUsers = undefined;
// newConfig.fallbackNumbers = undefined;
// newConfig.status = undefined;
const approvalUrl = `https://doorman.chromart.cc/api/door/edit?door=${door}&approvalId=${input?.input?.Item?.approvalId.S as string}`;
console.log(approvalUrl);
// send update to discord users
const approvalMessage = `Configuration change requested @ Door "${door}" [click to approve it](${approvalUrl})\`\`\`${JSON.stringify(newConfig, null, 4)}\`\`\``;
const discordPromises = config.discordUsers.map((user) => {
return sendMessageToUser(
context,
user,
approvalMessage,
).catch(e => console.error(e))
});
await Promise.all(discordPromises);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: update });
await client.destroy();
return callback(null, response);
};

View File

@ -2,7 +2,23 @@
* Try to get door info
*/
exports.handler = async function(context, event, callback) {
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { createDDBClient, ddbItemToJSON, getDoorAliasCommand, getDoorConfigCommand, getLockStatusCommand, isLockOpen } from "../../../utils/ddb";
import { DoorStatus } from "../../../types/DoorStatus";
import { DoorConfig } from "../../../types/DoorConfig";
export interface InfoRequest extends ServerlessEventObject {
door?: string;
buzzer?: string;
// TODO: change these to be multiple
discordUser: string;
msg: string;
json: string;
}
export const handler: ServerlessFunctionSignature<TwilioContext, InfoRequest> = async function(context, event, callback) {
const response = new Twilio.Response();
let door = event.door;
@ -13,12 +29,10 @@ exports.handler = async function(context, event, callback) {
return callback(null, response);
}
const ddbPath = Runtime.getFunctions()['common/ddb'].path;
const ddb = require(ddbPath);
const client = ddb.createDDBClient(context);
const client = createDDBClient(context);
if (buzzer) {
door = await client.send(ddb.getDoorAliasCommand(buzzer))
door = await client.send(getDoorAliasCommand(buzzer))
.then(async (alias) => {
if (!alias.Item) {
response
@ -32,7 +46,7 @@ exports.handler = async function(context, event, callback) {
}
if (door) {
const config = await client.send(ddb.getDoorConfigCommand(door));
const config = await client.send(getDoorConfigCommand(door));
if (!config.Item) {
response
@ -53,9 +67,10 @@ exports.handler = async function(context, event, callback) {
discordUsers: config.Item?.discordUsers?.SS || [],
});
} else {
await client.send(ddb.getLockStatusCommand(door))
await client.send(getLockStatusCommand(door))
.then(async (lock) => {
const status = ddb.isLockOpen(lock) ? "OPEN": "CLOSED";
const status = isLockOpen(lock) ? DoorStatus.OPEN: DoorStatus.CLOSED;
const doorConfig: DoorConfig = ddbItemToJSON<DoorConfig>(lock);
// respond to UI
response
@ -63,14 +78,14 @@ exports.handler = async function(context, event, callback) {
.appendHeader('Content-Type', 'application/json')
.setBody({
id: door,
timeout: config.Item.timeout.N,
buzzer: config.Item.buzzer.S,
timeout: doorConfig.timeout,
buzzer: doorConfig.buzzer,
status,
buzzerCode: config.Item.buzzerCode.S,
fallbackNumbers: config.Item.fallbackNumbers.SS,
pressKey: config.Item.pressKey.S,
discordUsers: config.Item?.discordUsers?.SS || [],
greeting: config.Item?.greeting?.S || "",
buzzerCode: doorConfig.buzzerCode,
fallbackNumbers: doorConfig.fallbackNumbers,
pressKey: doorConfig.pressKey,
discordUsers: doorConfig.discordUsers || [],
greeting: doorConfig.greeting || "",
});
}).catch((e) => {

View File

@ -0,0 +1,54 @@
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { jsonMsgSuffix, sendMessageToUser } from "../../../utils/discord";
export interface NotifyRequest extends ServerlessEventObject {
door: string;
// TODO: change these to be multiple
discordUser: string;
msg: string;
json: string;
}
export const handler: ServerlessFunctionSignature<TwilioContext, NotifyRequest> = async function(context, event, callback) {
const response = new Twilio.Response();
let users: string[];
let msgs: string[];
let jsons: string[];
let promises = [];
try {
users = JSON.parse(event.discordUser);
msgs = JSON.parse(event.msg);
console.log("before parsing", event.json);
jsons = JSON.parse(event.json);
console.log("after parsing", event.json);
promises = msgs.map((msg, i) =>
sendMessageToUser(
context,
users[i],
msg + jsonMsgSuffix(jsons[i])
).catch((e: Error) => console.error(e))
);
} catch (e) {
console.error(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e, event });
return callback(null, response);
}
let timer = setTimeout(() => {
console.log("Ungraceful finish: running out of time");
callback(null, response);
}, 9500);
await Promise.all(promises);
clearTimeout(timer);
return callback(null, response);
};

View File

@ -0,0 +1,68 @@
/**
* Try to update door status
*/
import { ServerlessFunctionSignature, ServerlessEventObject } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { shouldBlockRequest } from "../../../utils/blockUserAgent";
import { clearLockStatusCommand, createDDBClient, getLockStatusCommand, isLockOpen } from "../../../utils/ddb";
import { DoorStatus } from "../../../types/DoorStatus";
export interface StatusRequest extends ServerlessEventObject {
door: string;
}
export const handler: ServerlessFunctionSignature<TwilioContext, StatusRequest> = async function(context, event, callback) {
const response = new Twilio.Response();
if (shouldBlockRequest(event)) {
response.setStatusCode(200);
return callback(null, response);
}
const door = event.door;
if (!door) {
response.setStatusCode(400);
return callback(null, response);
}
const client = createDDBClient(context);
await client.send(getLockStatusCommand(door))
.then(async (lock) => {
const isOpen = isLockOpen(lock);
if (isOpen) {
const fingerprint = JSON.parse(lock?.Item?.fingerprint.S as string);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.OPEN,
fingerprint,
});
await client.send(clearLockStatusCommand(lock));
return;
}
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
});
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
});
await client.destroy();
return callback(null, response);
};

View File

@ -0,0 +1,23 @@
import { readdirSync } from "node:fs";
import path from "path";
const paths = [
'./src/functions/api/door'
];
const functionFiles = paths.map(path => readdirSync(path).map(file => path + "/" + file)).flat();
// for hot reload to work, we import all the files we want to build
const imports = functionFiles.forEach(file => require('./' + path.relative('src', file)));
console.log("functions to build:", functionFiles);
console.log("Building functions...");
await Bun.build({
entrypoints: functionFiles,
outdir: './build/functions',
packages: 'external',
target: 'node',
root: './src/functions',
format: 'cjs',
});

View File

@ -0,0 +1,4 @@
export enum AuthMethod {
PIN = "PIN",
DISCORD = "DISCORD",
}

View File

@ -0,0 +1,14 @@
export interface DoorConfig {
buzzer: string;
buzzerCode: string;
discordUsers: string[];
fallbackNumbers: string[];
pin: string;
pressKey: string;
timeout: number;
greeting: string;
}
export interface EditDoorConfig extends DoorConfig {
approvalId: string;
}

View File

@ -0,0 +1,4 @@
export enum DoorStatus {
OPEN = "OPEN",
CLOSED = "CLOSED",
}

View File

@ -0,0 +1,3 @@
export interface Lock {
fingerprint: any;
}

View File

@ -0,0 +1,7 @@
import { EnvironmentVariables } from "@twilio-labs/serverless-runtime-types/types";
export interface TwilioContext extends EnvironmentVariables {
AWS_ACCESS_KEY: string;
AWS_SECRET_ACCESS_KEY: string;
DISCORD_BOT_TOKEN: string;
}

View File

@ -1,8 +1,12 @@
import { ServerlessEventObject } from "@twilio-labs/serverless-runtime-types/types";
export type UserAgentHeader = { "user-agent"?: string };
/**
* Helper method to BLOCK discordbot from scraping API links
* This is a bit of a hack until we process event links from UI instead of raw API
*/
exports.shouldBlockRequest = (event) => {
export function shouldBlockRequest(event: ServerlessEventObject<{}, UserAgentHeader>): boolean {
let headers = event?.request?.headers;
let userAgentString = "";

View File

@ -1,7 +1,9 @@
const { randomUUID } = require("crypto");
const { DynamoDBClient, GetItemCommand, DeleteItemCommand, PutItemCommand, UpdateItemCommand } = require("@aws-sdk/client-dynamodb");
import { randomUUID } from "crypto";
import { DynamoDBClient, GetItemCommand, DeleteItemCommand, PutItemCommand, UpdateItemCommand, GetItemOutput } from "@aws-sdk/client-dynamodb";
import { TwilioContext } from "../types/TwilioContext";
import { DoorConfig } from "../types/DoorConfig";
exports.createDDBClient = (context) => {
export const createDDBClient = (context: TwilioContext) => {
return new DynamoDBClient({
region: "us-east-1" ,
credentials: {
@ -11,7 +13,7 @@ exports.createDDBClient = (context) => {
});
};
exports.getLockStatusCommand = (door) => {
export const getLockStatusCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
@ -25,7 +27,7 @@ exports.getLockStatusCommand = (door) => {
});
};
exports.getDoorAliasCommand = (buzzerNumber) => {
export const getDoorAliasCommand = (buzzerNumber: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
@ -39,7 +41,7 @@ exports.getDoorAliasCommand = (buzzerNumber) => {
});
};
exports.getDoorConfigCommand = (door) => {
export const getDoorConfigCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
@ -53,18 +55,19 @@ exports.getDoorConfigCommand = (door) => {
});
};
exports.isLockOpen = (lock) => {
export const isLockOpen = (lock: GetItemOutput) => {
// ttl is a UTC ms time for how long it is unlocked
const ttl = lock.Item?.TTL?.N || 0;
return ttl > Date.now();
return parseInt("" + ttl) > Date.now();
};
exports.clearLockStatusCommand = (lock) => {
export const clearLockStatusCommand = (lock: GetItemOutput) => {
return new DeleteItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: lock.Item.PK.S,
S: lock?.Item?.PK.S as string,
},
"SK": {
S: "lock",
@ -73,7 +76,7 @@ exports.clearLockStatusCommand = (lock) => {
});
};
exports.setLockStatusCommand = (door, timeoutSeconds, fingerprintObj) => {
export const setLockStatusCommand = (door: string, timeoutSeconds: number, fingerprintObj: any) => {
return new PutItemCommand({
TableName: "doorman",
Item: {
@ -93,7 +96,7 @@ exports.setLockStatusCommand = (door, timeoutSeconds, fingerprintObj) => {
});
};
exports.putDoorUpdateConfigCommand = (door, config) => {
export const putDoorUpdateConfigCommand = (door: string, config: DoorConfig) => {
return new PutItemCommand({
TableName: "doorman",
Item: {
@ -134,21 +137,22 @@ exports.putDoorUpdateConfigCommand = (door, config) => {
});
};
exports.getDoorConfigCommand = (door) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: `door-${door}`,
},
"SK": {
S: "config",
},
},
export function ddbItemToJSON<T extends Record<string, any>>(ddbItem: GetItemOutput): T {
let obj: any = {};
if (!ddbItem.Item) {
return obj;
}
const Item = ddbItem.Item;
Object.keys(Item).forEach(key => {
obj[key] = Object.values(Item[key])[0];
});
return obj;
};
exports.getDoorConfigUpdateCommand = (door) => {
export const getDoorConfigUpdateCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
@ -162,7 +166,7 @@ exports.getDoorConfigUpdateCommand = (door) => {
});
};
exports.replaceDoorConfigWithUpdateItem = (newConfigItem) => {
export const replaceDoorConfigWithUpdateItem = (newConfigItem: GetItemOutput & { Item: { approvalId?: { S: string } }}) => {
const newItem = {
...newConfigItem.Item,
SK: { S: "config" },

View File

@ -2,13 +2,13 @@
// https://gitea.chromart.cc/martin/refbounty/src/commit/1f40d7870a530fe7e7acbc3b901024092c9fb90a/packages/server/src/connectors/DiscordConnector.ts
// https://gitea.chromart.cc/martin/refbounty/src/commit/1f40d7870a530fe7e7acbc3b901024092c9fb90a/packages/server/src/utils/DiscordUtils.ts
const { Client, GatewayIntentBits } = require("discord.js");
import { Client, GatewayIntentBits, User } from "discord.js";
import { TwilioContext } from "../types/TwilioContext";
// TODO: cache these at top level in handler code
let conn;
let userCache = {};
let conn: Client;
let userCache: Record<string, User> = {};
exports.getDiscordClient = async (context) => {
export const getDiscordClient = async (context: TwilioContext) => {
if (!conn) {
console.log("[DiscordClientCache] cache miss for discord");
const client = new Client({
@ -28,13 +28,13 @@ exports.getDiscordClient = async (context) => {
};
exports.sendMessageToUser = async (
context,
userId,
msg,
export const sendMessageToUser = async (
context: TwilioContext,
userId: string,
msg: string,
) => {
try {
const client = await exports.getDiscordClient(context);
const client = await getDiscordClient(context);
if (userCache[userId] === undefined) {
console.log("[UserCache] cache miss for", userId);
userCache[userId] = await client.users.fetch(userId);
@ -47,4 +47,16 @@ exports.sendMessageToUser = async (
console.log(e);
console.log(`Failed to send msg to ${userId}`);
}
};
export const jsonMsgSuffix = (jsonString: string = "") => {
if (!jsonString) {
return "";
}
try {
const fingerprint = JSON.parse(jsonString);
return `\`\`\`# Unlocked by\n${JSON.stringify(fingerprint, null, 4)}\`\`\``;
} catch (e) {
return `\`\`\`# Unlocked by\n# WARN: Unknown or corrupt raw fingerprint:\n ${jsonString}\`\`\``;
}
};

View File

@ -2,7 +2,7 @@ import { serve } from "bun";
import { Hono } from "hono";
import { prettyJSON } from "hono/pretty-json";
import * as ddb from "../../doorman-api/functions/common/ddb.private";
import * as ddb from "../../doorman-api/deprecated-functions/common/ddb.private";
const app = new Hono();