migrate buzzer client
All checks were successful
Build and push Doorman UI / API / docker (push) Successful in 49s

This commit is contained in:
Martin Dimitrov 2024-10-26 11:39:48 -07:00
parent 92e34d89c0
commit e37df33c08
20 changed files with 10098 additions and 4 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -11,9 +11,6 @@
},
{
"path": "packages/serverless"
},
{
"path": "../../sideprojects/2023/smart-door-buzzer-twilio-functions"
}
],
"settings": {

View File

@ -8,7 +8,8 @@
},
"scripts": {
"prepare-client-serverless": "bun --filter 'doorman-client' build && rm -rf packages/serverless/assets/* && mkdir -p packages/serverless/assets/assets && cp -fr packages/client/dist/* packages/serverless/assets/ && cp -f packages/serverless/assets/index.html packages/serverless/assets/assets/index.html",
"deploy-serverless": "bun run prepare-client-serverless && bun --filter 'serverless' deploy"
"deploy-serverless": "bun run prepare-client-serverless && bun --filter 'serverless' deploy",
"deploy-buzzer-client": "bun --filter 'buzzer-client' deploy"
},
"peerDependencies": {
"typescript": "^5.0.0"

View File

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

133
packages/buzzer-client/.gitignore vendored Normal file
View File

@ -0,0 +1,133 @@
# Twilio Serverless
.twiliodeployinfo
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -0,0 +1 @@
18

View File

@ -0,0 +1,42 @@
{
"commands": {},
"environments": {},
"projects": {},
// "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 */,
// "createEnvironment": false /* Creates environment if it couldn't find it. */,
// "cwd": null /* Sets the directory of your existing Serverless project. Defaults to current directory */,
// "detailedLogs": false /* Toggles detailed request logging by showing request body and query params */,
// "edge": null /* Twilio API Region */,
// "env": null /* Path to .env file for environment variables that should be installed */,
// "environment": "dev" /* The environment name (domain suffix) you want to use for your deployment. Alternatively you can specify an environment SID starting with ZE. */,
// "extendedOutput": false /* Show an extended set of properties on the output */,
// "force": false /* Will run deployment in force mode. Can be dangerous. */,
// "forkProcess": true /* Disable forking function processes to emulate production environment */,
// "functionSid": null /* Specific Function SID to retrieve logs for */,
// "functions": true /* Upload functions. Can be turned off with --no-functions */,
// "functionsFolder": null /* Specific folder name to be used for static functions */,
// "inspect": null /* Enables Node.js debugging protocol */,
// "inspectBrk": null /* Enables Node.js debugging protocol, stops execution until debugger is attached */,
// "legacyMode": false /* Enables legacy mode, it will prefix your asset paths with /assets */,
// "live": true /* Always serve from the current functions (no caching) */,
// "loadLocalEnv": false /* Includes the local environment variables */,
// "loadSystemEnv": false /* Uses system environment variables as fallback for variables specified in your .env file. Needs to be used with --env explicitly specified. */,
// "logCacheSize": null /* Tailing the log endpoint will cache previously seen entries to avoid duplicates. The cache is topped at a maximum of 1000 by default. This option can change that. */,
// "logLevel": "info" /* Level of logging messages. */,
// "logs": true /* Toggles request logging */,
// "ngrok": null /* Uses ngrok to create a public url. Pass a string to set the subdomain (requires a paid-for ngrok account). */,
// "outputFormat": "" /* Output the results in a different format */,
// "overrideExistingProject": false /* Deploys Serverless project to existing service if a naming conflict has been found. */,
// "port": "3000" /* Override default port of 3000 */,
// "production": false /* Promote build to the production environment (no domain suffix). Overrides environment flag */,
// "properties": null /* Specify the output properties you want to see. Works best on single types */,
// "region": null /* Twilio API Region */,
"runtime": "node18" /* The version of Node.js to deploy the build to. (node18) */,
// "serviceName": null /* Overrides the name of the Serverless project. Default: the name field in your package.json */,
// "serviceSid": null /* SID of the Twilio Serverless Service to deploy to */,
// "sourceEnvironment": null /* SID or suffix of an existing environment you want to deploy from. */,
// "tail": false /* Continuously stream the logs */,
// "template": null /* undefined */,
}

View File

@ -0,0 +1,3 @@
{
"dotenv.enableAutocloaking": false
}

View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2018, John Lian
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,51 @@
# Make your apartment buzzer smart with Twilio Functions
I wanted to make my apartment buzzer acessible without a physical key, so I created these Twilio Functions to make my buzzer smarter. Features include:
* Voice password (make your friends shout stupid things in public to get into your building!)
* PIN password, a classic
* No obvious weirdness or extra waiting for first-time guests or delivery people
* Notify multiple people until someone picks up the phone - as long as one of the roommates pick up, you won't miss your package
I kind of went overboard I think, given my original goal. But this was actually *really* easy to develop and set up. And also really cheap.
## How to set this up
1. You could `git clone https://github.com/jlian/smart-door-buzzer-twilio-functions.git`, but it's not critical.
2. Get a Twilio account and valid Twilio number.
3. Go to https://www.twilio.com/console/runtime/functions/manage and hit **+**.
4. Add each of the 4 `.js` files into its own function with names that you'd remember.
5. Go to https://www.twilio.com/console/runtime/functions/configure and configure the environment variables:
* `TWILIO_PHONE` with the Twilio number you bought
* `PASSPHRASE` for voice password
* `PASSCODE` for PIN
* `JOHN_PHONE` and others for your cellphone number
5. Go to https://www.twilio.com/console/phone-numbers/incoming and select the phone number you bought earlier.
6. Scroll to where it says **A call comes in**, select **Function**, and then the function that corresponds to `buzzer-activated.js`.
7. Contact your HOA to make the Twilio number your buzzer number - this might be the hardest step.
## How this works
[Twilio Functions](https://www.twilio.com/functions) is pretty sweet. It's completely serverless so you don't need a VM or computer to keep running an app. It's perfect for something small scale like your apartment buzzer. The flow of this program goes like this:
1. A call comes to the Twilio phone number, `buzzer-activated.js` runs.
1. The [Gather](https://www.twilio.com/docs/api/twiml/gather) verb is used to get either a voice password or a 4-digit PIN.
1. If correct, `door-open.js` dials a `9` to the buzzer (my building's buzzer code, yours may be different), which opens the door.
1. If incorrect, `call-residents.js` calls all the residents until someone picks up and manually dial `9` to open the door.
1. When everything is finished, `text-me.js` texts a specified number with info on what happened.
## How much this costs
According to Twilio docs, collecting speech is charged at $.02 per 15 seconds. A Twilio number costs $1/month. Looking at my own billing dashboard, it never exceeds $2/month - pretty reasonable.
## Twilio tooling
Using the twilio serverless toolbox you can locally run the functions and test.
```
bun run start
```
Deploy to your account with
```
bun run deploy
```

BIN
packages/buzzer-client/bun.lockb Executable file

Binary file not shown.

View File

@ -0,0 +1,63 @@
/**
* Simple call box routine
*
* This function is meant for the apartment building callbox
* It gives the user a couple of seconds to produce the password
* Then dials all the residents to grant manual entry
*/
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}`)
.then(res => res.json())
.catch(err => {
return undefined;
});
// reject the call if this is not configured
if (!config || !config.door) {
twiml.reject();
callback(null, twiml);
return;
}
const promise = new Promise(() => null, () => null);
// poll Doorman, to see if we should unlock
const interval = setInterval(() => {
fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`)
.then(async res => {
// handle the case where doorman is explictly rejecting the buzzer
if (res.status === 410) {
clearInterval(interval);
twiml.redirect('/text-me?method=doorman-time-lock');
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}`);
callback(null, twiml);
promise.resolve();
}
})
.catch(err => console.log(err));
}, 500);
// redirect to call after 6s
setTimeout(() => {
twiml.redirect(`/call-residents?numbers=${encodeURIComponent(config.fallbackNumbers)}`);
callback(null, twiml);
promise.resolve();
}, 6000);
await promise;
return callback(null, twiml);
};

View File

@ -0,0 +1,18 @@
/**
* Fallback behavior, if the code is wrong or unspecified, then we should dial the fallback numbers
*/
exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse();
// numbers are passed in
let numbers = event.numbers.split(',');
// 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});
numbers.forEach(number => {
dial.number(number);
});
return callback(null, twiml);
}

View File

@ -0,0 +1,15 @@
/**
* Automatically open the door
*/
exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse();
let passAlong = `fingerprint=${encodeURIComponent(event.fingerprint)}`;
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
twiml.pause({ length: 1 });
twiml.redirect(`/text-me?Method=doorman&${passAlong}`);
callback(null, twiml);
};

View File

@ -0,0 +1,30 @@
/**
* Send a NTFY message with an update of what happened. If the password was used or not...
*/
const fetch = require('node-fetch');
exports.handler = function(context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse();
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)}`;
} 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));
};

9678
packages/buzzer-client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
{
"name": "buzzer-client",
"version": "0.0.0",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "twilio-run --live",
"deploy": "twilio-run deploy --load-system-env --env .env.example --service-name buzzer --environment=prod --override-existing-project"
},
"dependencies": {
"@twilio-labs/serverless-runtime-types": "^3.0.0",
"@twilio/runtime-handler": "1.3.0",
"node-fetch": "2",
"twilio": "^3.56"
},
"devDependencies": {
"twilio-run": "^3.5.4",
"@types/bun": "latest"
},
"engines": {
"node": "18"
},
"type": "commonjs",
"peerDependencies": {
"typescript": "^5.0.0"
}
}