diff --git a/.gitea/workflows/deploy-twilio.yaml b/.gitea/workflows/deploy-twilio.yaml index afc975c..5a25c6a 100644 --- a/.gitea/workflows/deploy-twilio.yaml +++ b/.gitea/workflows/deploy-twilio.yaml @@ -25,6 +25,7 @@ jobs: AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }} AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} - name: Deploy Doorman Buzzer Client run: bun run deploy-buzzer-client diff --git a/bun.lockb b/bun.lockb index 55ec619..c9b4164 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/doorman-api/.env.example b/packages/doorman-api/.env.example index 7d6834c..f4659f8 100644 --- a/packages/doorman-api/.env.example +++ b/packages/doorman-api/.env.example @@ -4,4 +4,7 @@ AUTH_TOKEN= # aws stuff AWS_ACCESS_KEY= -AWS_SECRET_ACCESS_KEY= \ No newline at end of file +AWS_SECRET_ACCESS_KEY= + +# discord notifs +DISCORD_BOT_TOKEN= \ No newline at end of file diff --git a/packages/doorman-api/functions/api/door/info.js b/packages/doorman-api/functions/api/door/info.js index 784a710..694d92c 100644 --- a/packages/doorman-api/functions/api/door/info.js +++ b/packages/doorman-api/functions/api/door/info.js @@ -49,6 +49,7 @@ exports.handler = async function(context, event, callback) { door, fallbackNumbers: config.Item.fallbackNumbers.SS, pressKey: config.Item.pressKey.S, + discordUsers: config.Item?.discordUsers?.SS || [], }); } else { await client.send(ddb.getLockStatusCommand(door)) diff --git a/packages/doorman-api/functions/api/door/notify.js b/packages/doorman-api/functions/api/door/notify.js new file mode 100644 index 0000000..f878306 --- /dev/null +++ b/packages/doorman-api/functions/api/door/notify.js @@ -0,0 +1,15 @@ +exports.handler = async function(context, event, callback) { + const response = new Twilio.Response(); + + const discordPath = Runtime.getFunctions()['common/discord'].path; + const discord = require(discordPath); + + // user must be in "Doorman" server + await discord.sendMessageToUser(context, + event.discordUser, + event.msg, + ) + .catch((e) => console.error(e)); + + return callback(null, response); +}; diff --git a/packages/doorman-api/functions/common/discord.private.js b/packages/doorman-api/functions/common/discord.private.js new file mode 100644 index 0000000..448b9ee --- /dev/null +++ b/packages/doorman-api/functions/common/discord.private.js @@ -0,0 +1,39 @@ +// reusing from refbounty: +// 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"); + +let conn; + +exports.getDiscordClient = async (context) => { + if (!conn) { + const client = new Client({ + intents: [GatewayIntentBits.DirectMessages], + rest: { + timeout: 60_000, // 1 minute + } + }); + + await client.login(context.DISCORD_BOT_TOKEN); + conn = client; + } + + return conn; +}; + + +exports.sendMessageToUser = async ( + context, + userId, + msg, +) => { + try { + const client = await exports.getDiscordClient(context); + const user = await client.users.fetch(userId); + return user.send(msg); + } catch (e) { + console.log(e); + console.log(`Failed to send msg to ${userId}`); + } +} diff --git a/packages/doorman-api/package.json b/packages/doorman-api/package.json index 15b4d70..d65b5c1 100644 --- a/packages/doorman-api/package.json +++ b/packages/doorman-api/package.json @@ -4,12 +4,13 @@ "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "twilio-run", + "start": "twilio-run --live --port 8080", "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/runtime-handler": "1.3.0", + "discord.js": "^14.16.3", "twilio": "^3.56" }, "devDependencies": { diff --git a/packages/doorman-client/.env.example b/packages/doorman-client/.env.example index 5af0929..03505de 100644 --- a/packages/doorman-client/.env.example +++ b/packages/doorman-client/.env.example @@ -1,5 +1,4 @@ DOORMAN_URL=https://doorman.chromart.cc -NTFY_DOMAIN=ntfy.chromart.cc # twilio auth ACCOUNT_SID= diff --git a/packages/doorman-client/functions/buzzer-activated.js b/packages/doorman-client/functions/buzzer-activated.js index c8b6788..b079764 100644 --- a/packages/doorman-client/functions/buzzer-activated.js +++ b/packages/doorman-client/functions/buzzer-activated.js @@ -8,7 +8,6 @@ const fetch = require('node-fetch'); exports.handler = async function(context, event, callback) { - let twiml = new Twilio.twiml.VoiceResponse(); let config = await fetch(context.DOORMAN_URL + `/api/door/info?buzzer=${event.From}`) @@ -24,7 +23,7 @@ exports.handler = async function(context, event, callback) { return; } - const promise = new Promise(() => null, () => null); + let configQuery = `config=${encodeURIComponent(JSON.stringify(config))}`; // poll Doorman, to see if we should unlock const interval = setInterval(() => { @@ -33,19 +32,16 @@ exports.handler = async function(context, event, callback) { // handle the case where doorman is explictly rejecting the buzzer if (res.status === 410) { clearInterval(interval); - twiml.redirect('/text-me?method=doorman-time-lock'); + twiml.redirect(`/text-me?method=doorman-time-lock&${configQuery}`); callback(null, twiml); - promise.resolve(); } // we got the successful unlock else if (res.status === 200) { clearInterval(interval); - const body = await res.json(); - twiml.redirect(`/door-open?fingerprint=${encodeURIComponent(JSON.stringify(body))}&pressKey=${config.pressKey}`); + twiml.redirect(`/door-open?fingerprint=${encodeURIComponent(JSON.stringify(body))}&${configQuery}`); callback(null, twiml); - promise.resolve(); } }) .catch(err => console.log(err)); @@ -53,11 +49,9 @@ exports.handler = async function(context, event, callback) { // redirect to call after 6s setTimeout(() => { - twiml.redirect(`/call-residents?numbers=${encodeURIComponent(config.fallbackNumbers)}`); + twiml.redirect(`/call-residents?${configQuery}`); callback(null, twiml); - promise.resolve(); }, 6000); - await promise; - return callback(null, twiml); + // return callback(null, twiml); }; \ No newline at end of file diff --git a/packages/doorman-client/functions/call-residents.js b/packages/doorman-client/functions/call-residents.js index 891c15f..f1f4752 100644 --- a/packages/doorman-client/functions/call-residents.js +++ b/packages/doorman-client/functions/call-residents.js @@ -4,13 +4,18 @@ exports.handler = function(context, event, callback) { let twiml = new Twilio.twiml.VoiceResponse(); - // numbers are passed in - let numbers = event.numbers.split(','); + let config = JSON.parse(event.config); + console.log(config); + let configQuery = `config=${encodeURIComponent(JSON.stringify(config))}`; // If no valid answer after timeout, dial all residents until someone picks up - let dial = twiml.dial({action: '/text-me?Method=call', timeLimit: 20, timeout: 20}); + let dial = twiml.dial({ + action: `/text-me?Method=call&${configQuery}`, + timeLimit: 20, + timeout: 20 + }); - numbers.forEach(number => { + config.fallbackNumbers.forEach(number => { dial.number(number); }); diff --git a/packages/doorman-client/functions/door-open.js b/packages/doorman-client/functions/door-open.js index 3399d38..7075f74 100644 --- a/packages/doorman-client/functions/door-open.js +++ b/packages/doorman-client/functions/door-open.js @@ -4,10 +4,13 @@ exports.handler = function(context, event, callback) { let twiml = new Twilio.twiml.VoiceResponse(); - let passAlong = `fingerprint=${encodeURIComponent(event.fingerprint)}`; + let config = JSON.parse(event.config); + let configQuery = `config=${encodeURIComponent(JSON.stringify(config))}`; - twiml.play('https://smart-door-buzzer-3172.twil.io/buzzing_up_boosted.mp3'); - twiml.play({ digits: event.pressKey }); // configured in doorman what button to click and passed into this function + let passAlong = `fingerprint=${encodeURIComponent(event.fingerprint)}&${configQuery}`; + + twiml.play('https://buzzer-2439-prod.twil.io/buzzing_up_boosted.mp3'); + twiml.play({ digits: config.pressKey }); // configured in doorman what button to click and passed into this function twiml.pause({ length: 1 }); twiml.redirect(`/text-me?Method=doorman&${passAlong}`); diff --git a/packages/doorman-client/functions/text-me.js b/packages/doorman-client/functions/text-me.js index 06de072..f06886b 100644 --- a/packages/doorman-client/functions/text-me.js +++ b/packages/doorman-client/functions/text-me.js @@ -4,27 +4,31 @@ const fetch = require('node-fetch'); -exports.handler = function(context, event, callback) { +exports.handler = async function(context, event, callback) { let twiml = new Twilio.twiml.VoiceResponse(); + // should be a list of strings representing user ids in discord + let discordUsers = JSON.parse(event.config).discordUsers || []; + let bodyText; if (event.Method == 'doorman') { bodyText = 'Doorman buzzed someone up!'; const fingerprint = JSON.parse(event.fingerprint); - bodyText += `\n\n${JSON.stringify(fingerprint, null, 4)}`; + bodyText += `\n\n\`\`\`${JSON.stringify(fingerprint, null, 4)}\`\`\``; } else if (event.Method == 'doorman-time-lock') { bodyText = 'Doorman rejected a buzzer call due to time restriction'; } else if (event.Method == 'call') { bodyText = 'Somebody buzzed the door and it dialed through to a phone.'; } - // send webhook to ntfy - fetch(`https://${context.NTFY_DOMAIN}/buzzer`, { - method: "POST", - body: bodyText, - }) - .then(res => callback(null, twiml)) - // even if we error then we should just end the call normally - .catch(err => callback(null, twiml)); + let promises = discordUsers.map((u) => + fetch(context.DOORMAN_URL + `/api/door/notify?discordUser=${u}&msg=${bodyText}`) + .catch(err => console.log(err)) + ); + + await Promise.all(promises) + .catch (e => console.log(e)); + + return callback(null, twiml); }; \ No newline at end of file diff --git a/packages/doorman-client/package.json b/packages/doorman-client/package.json index 75dd944..86a9770 100644 --- a/packages/doorman-client/package.json +++ b/packages/doorman-client/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "twilio-run --live", + "start": "twilio-run --live --port 4500", "deploy": "twilio-run deploy --load-system-env --env .env.example --service-name buzzer --environment=prod --override-existing-project" }, "dependencies": {