diff --git a/bun.lockb b/bun.lockb index d08672b..e3ed26c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/doorman-api/.gitignore b/packages/doorman-api/.gitignore index 655b8ab..5bb2f0a 100644 --- a/packages/doorman-api/.gitignore +++ b/packages/doorman-api/.gitignore @@ -133,3 +133,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +functions/* +deprecated-functions \ No newline at end of file diff --git a/packages/doorman-api/.twilioserverlessrc b/packages/doorman-api/.twilioserverlessrc index a7f5a1c..31959de 100644 --- a/packages/doorman-api/.twilioserverlessrc +++ b/packages/doorman-api/.twilioserverlessrc @@ -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 */, diff --git a/packages/doorman-api/functions/api/door/auth.js b/packages/doorman-api/functions/api/door/auth.js deleted file mode 100644 index 0b3b228..0000000 --- a/packages/doorman-api/functions/api/door/auth.js +++ /dev/null @@ -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); -}; diff --git a/packages/doorman-api/functions/api/door/edit.js b/packages/doorman-api/functions/api/door/edit.js deleted file mode 100644 index 72268de..0000000 --- a/packages/doorman-api/functions/api/door/edit.js +++ /dev/null @@ -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); -}; diff --git a/packages/doorman-api/functions/api/door/notify.js b/packages/doorman-api/functions/api/door/notify.js deleted file mode 100644 index 85b8711..0000000 --- a/packages/doorman-api/functions/api/door/notify.js +++ /dev/null @@ -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); -}; diff --git a/packages/doorman-api/functions/api/door/status.js b/packages/doorman-api/functions/api/door/status.js deleted file mode 100644 index cf5927d..0000000 --- a/packages/doorman-api/functions/api/door/status.js +++ /dev/null @@ -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); -}; diff --git a/packages/doorman-api/package.json b/packages/doorman-api/package.json index d65b5c1..329afb2 100644 --- a/packages/doorman-api/package.json +++ b/packages/doorman-api/package.json @@ -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" diff --git a/packages/doorman-api/src/functions/api/door/auth.ts b/packages/doorman-api/src/functions/api/door/auth.ts new file mode 100644 index 0000000..2ac80c8 --- /dev/null +++ b/packages/doorman-api/src/functions/api/door/auth.ts @@ -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 = 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(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(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); +}; diff --git a/packages/doorman-api/src/functions/api/door/edit.ts b/packages/doorman-api/src/functions/api/door/edit.ts new file mode 100644 index 0000000..c261dce --- /dev/null +++ b/packages/doorman-api/src/functions/api/door/edit.ts @@ -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 = 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(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(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); +}; diff --git a/packages/doorman-api/functions/api/door/info.js b/packages/doorman-api/src/functions/api/door/info.ts similarity index 55% rename from packages/doorman-api/functions/api/door/info.js rename to packages/doorman-api/src/functions/api/door/info.ts index d81a5b0..92952c2 100644 --- a/packages/doorman-api/functions/api/door/info.js +++ b/packages/doorman-api/src/functions/api/door/info.ts @@ -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 = 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(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) => { diff --git a/packages/doorman-api/src/functions/api/door/notify.ts b/packages/doorman-api/src/functions/api/door/notify.ts new file mode 100644 index 0000000..67eb7fb --- /dev/null +++ b/packages/doorman-api/src/functions/api/door/notify.ts @@ -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 = 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); +}; diff --git a/packages/doorman-api/src/functions/api/door/status.ts b/packages/doorman-api/src/functions/api/door/status.ts new file mode 100644 index 0000000..4a4c401 --- /dev/null +++ b/packages/doorman-api/src/functions/api/door/status.ts @@ -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 = 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); +}; diff --git a/packages/doorman-api/src/index.ts b/packages/doorman-api/src/index.ts new file mode 100644 index 0000000..2b35544 --- /dev/null +++ b/packages/doorman-api/src/index.ts @@ -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', +}); diff --git a/packages/doorman-api/src/types/AuthMethod.ts b/packages/doorman-api/src/types/AuthMethod.ts new file mode 100644 index 0000000..f897065 --- /dev/null +++ b/packages/doorman-api/src/types/AuthMethod.ts @@ -0,0 +1,4 @@ +export enum AuthMethod { + PIN = "PIN", + DISCORD = "DISCORD", +} diff --git a/packages/doorman-api/src/types/DoorConfig.ts b/packages/doorman-api/src/types/DoorConfig.ts new file mode 100644 index 0000000..297a99e --- /dev/null +++ b/packages/doorman-api/src/types/DoorConfig.ts @@ -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; +} diff --git a/packages/doorman-api/src/types/DoorStatus.ts b/packages/doorman-api/src/types/DoorStatus.ts new file mode 100644 index 0000000..1c5b4eb --- /dev/null +++ b/packages/doorman-api/src/types/DoorStatus.ts @@ -0,0 +1,4 @@ +export enum DoorStatus { + OPEN = "OPEN", + CLOSED = "CLOSED", +} \ No newline at end of file diff --git a/packages/doorman-api/src/types/Lock.ts b/packages/doorman-api/src/types/Lock.ts new file mode 100644 index 0000000..d0608f2 --- /dev/null +++ b/packages/doorman-api/src/types/Lock.ts @@ -0,0 +1,3 @@ +export interface Lock { + fingerprint: any; +} diff --git a/packages/doorman-api/src/types/TwilioContext.ts b/packages/doorman-api/src/types/TwilioContext.ts new file mode 100644 index 0000000..fa427af --- /dev/null +++ b/packages/doorman-api/src/types/TwilioContext.ts @@ -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; +} diff --git a/packages/doorman-api/functions/common/blockUserAgent.private.js b/packages/doorman-api/src/utils/blockUserAgent.ts similarity index 73% rename from packages/doorman-api/functions/common/blockUserAgent.private.js rename to packages/doorman-api/src/utils/blockUserAgent.ts index c073d4a..c681201 100644 --- a/packages/doorman-api/functions/common/blockUserAgent.private.js +++ b/packages/doorman-api/src/utils/blockUserAgent.ts @@ -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 = ""; diff --git a/packages/doorman-api/functions/common/ddb.private.js b/packages/doorman-api/src/utils/ddb.ts similarity index 63% rename from packages/doorman-api/functions/common/ddb.private.js rename to packages/doorman-api/src/utils/ddb.ts index 03ebb44..3637c72 100644 --- a/packages/doorman-api/functions/common/ddb.private.js +++ b/packages/doorman-api/src/utils/ddb.ts @@ -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>(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" }, diff --git a/packages/doorman-api/functions/common/discord.private.js b/packages/doorman-api/src/utils/discord.ts similarity index 59% rename from packages/doorman-api/functions/common/discord.private.js rename to packages/doorman-api/src/utils/discord.ts index d785b83..6829ab8 100644 --- a/packages/doorman-api/functions/common/discord.private.js +++ b/packages/doorman-api/src/utils/discord.ts @@ -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 = {}; -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}\`\`\``; + } +}; diff --git a/packages/doorman-homeassistant/src/index.ts b/packages/doorman-homeassistant/src/index.ts index 980cda9..be987a0 100644 --- a/packages/doorman-homeassistant/src/index.ts +++ b/packages/doorman-homeassistant/src/index.ts @@ -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();