discord notifications
All checks were successful
Build and push Doorman UI / API / docker (push) Successful in 1m27s

This commit is contained in:
Martin Dimitrov 2024-10-26 13:31:15 -07:00
parent f4ac30260c
commit 4cad1fbe2b
13 changed files with 97 additions and 32 deletions

View File

@ -25,6 +25,7 @@ jobs:
AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }} AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
- name: Deploy Doorman Buzzer Client - name: Deploy Doorman Buzzer Client
run: bun run deploy-buzzer-client run: bun run deploy-buzzer-client

BIN
bun.lockb

Binary file not shown.

View File

@ -4,4 +4,7 @@ AUTH_TOKEN=
# aws stuff # aws stuff
AWS_ACCESS_KEY= AWS_ACCESS_KEY=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
# discord notifs
DISCORD_BOT_TOKEN=

View File

@ -49,6 +49,7 @@ exports.handler = async function(context, event, callback) {
door, door,
fallbackNumbers: config.Item.fallbackNumbers.SS, fallbackNumbers: config.Item.fallbackNumbers.SS,
pressKey: config.Item.pressKey.S, pressKey: config.Item.pressKey.S,
discordUsers: config.Item?.discordUsers?.SS || [],
}); });
} else { } else {
await client.send(ddb.getLockStatusCommand(door)) await client.send(ddb.getLockStatusCommand(door))

View File

@ -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);
};

View File

@ -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}`);
}
}

View File

@ -4,12 +4,13 @@
"private": true, "private": true,
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "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" "deploy": "twilio-run deploy --load-system-env --env .env.example --service-name doorman --environment=prod --override-existing-project"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-dynamodb": "^3.609.0", "@aws-sdk/client-dynamodb": "^3.609.0",
"@twilio/runtime-handler": "1.3.0", "@twilio/runtime-handler": "1.3.0",
"discord.js": "^14.16.3",
"twilio": "^3.56" "twilio": "^3.56"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,5 +1,4 @@
DOORMAN_URL=https://doorman.chromart.cc DOORMAN_URL=https://doorman.chromart.cc
NTFY_DOMAIN=ntfy.chromart.cc
# twilio auth # twilio auth
ACCOUNT_SID= ACCOUNT_SID=

View File

@ -8,7 +8,6 @@
const fetch = require('node-fetch'); const fetch = require('node-fetch');
exports.handler = async function(context, event, callback) { exports.handler = async function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse(); let twiml = new Twilio.twiml.VoiceResponse();
let config = await fetch(context.DOORMAN_URL + `/api/door/info?buzzer=${event.From}`) 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; return;
} }
const promise = new Promise(() => null, () => null); let configQuery = `config=${encodeURIComponent(JSON.stringify(config))}`;
// poll Doorman, to see if we should unlock // poll Doorman, to see if we should unlock
const interval = setInterval(() => { const interval = setInterval(() => {
@ -33,19 +32,16 @@ exports.handler = async function(context, event, callback) {
// handle the case where doorman is explictly rejecting the buzzer // handle the case where doorman is explictly rejecting the buzzer
if (res.status === 410) { if (res.status === 410) {
clearInterval(interval); clearInterval(interval);
twiml.redirect('/text-me?method=doorman-time-lock'); twiml.redirect(`/text-me?method=doorman-time-lock&${configQuery}`);
callback(null, twiml); callback(null, twiml);
promise.resolve();
} }
// we got the successful unlock // we got the successful unlock
else if (res.status === 200) { else if (res.status === 200) {
clearInterval(interval); clearInterval(interval);
const body = await res.json(); 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); callback(null, twiml);
promise.resolve();
} }
}) })
.catch(err => console.log(err)); .catch(err => console.log(err));
@ -53,11 +49,9 @@ exports.handler = async function(context, event, callback) {
// redirect to call after 6s // redirect to call after 6s
setTimeout(() => { setTimeout(() => {
twiml.redirect(`/call-residents?numbers=${encodeURIComponent(config.fallbackNumbers)}`); twiml.redirect(`/call-residents?${configQuery}`);
callback(null, twiml); callback(null, twiml);
promise.resolve();
}, 6000); }, 6000);
await promise; // return callback(null, twiml);
return callback(null, twiml);
}; };

View File

@ -4,13 +4,18 @@
exports.handler = function(context, event, callback) { exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse(); let twiml = new Twilio.twiml.VoiceResponse();
// numbers are passed in let config = JSON.parse(event.config);
let numbers = event.numbers.split(','); console.log(config);
let configQuery = `config=${encodeURIComponent(JSON.stringify(config))}`;
// If no valid answer after timeout, dial all residents until someone picks up // 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); dial.number(number);
}); });

View File

@ -4,10 +4,13 @@
exports.handler = function(context, event, callback) { exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse(); 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'); let passAlong = `fingerprint=${encodeURIComponent(event.fingerprint)}&${configQuery}`;
twiml.play({ digits: event.pressKey }); // configured in doorman what button to click and passed into this function
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.pause({ length: 1 });
twiml.redirect(`/text-me?Method=doorman&${passAlong}`); twiml.redirect(`/text-me?Method=doorman&${passAlong}`);

View File

@ -4,27 +4,31 @@
const fetch = require('node-fetch'); const fetch = require('node-fetch');
exports.handler = function(context, event, callback) { exports.handler = async function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse(); 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; let bodyText;
if (event.Method == 'doorman') { if (event.Method == 'doorman') {
bodyText = 'Doorman buzzed someone up!'; bodyText = 'Doorman buzzed someone up!';
const fingerprint = JSON.parse(event.fingerprint); 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') { } else if (event.Method == 'doorman-time-lock') {
bodyText = 'Doorman rejected a buzzer call due to time restriction'; bodyText = 'Doorman rejected a buzzer call due to time restriction';
} else if (event.Method == 'call') { } else if (event.Method == 'call') {
bodyText = 'Somebody buzzed the door and it dialed through to a phone.'; bodyText = 'Somebody buzzed the door and it dialed through to a phone.';
} }
// send webhook to ntfy let promises = discordUsers.map((u) =>
fetch(`https://${context.NTFY_DOMAIN}/buzzer`, { fetch(context.DOORMAN_URL + `/api/door/notify?discordUser=${u}&msg=${bodyText}`)
method: "POST", .catch(err => console.log(err))
body: bodyText, );
})
.then(res => callback(null, twiml)) await Promise.all(promises)
// even if we error then we should just end the call normally .catch (e => console.log(e));
.catch(err => callback(null, twiml));
return callback(null, twiml);
}; };

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "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" "deploy": "twilio-run deploy --load-system-env --env .env.example --service-name buzzer --environment=prod --override-existing-project"
}, },
"dependencies": { "dependencies": {