migrate buzzer client
All checks were successful
Build and push Doorman UI / API / docker (push) Successful in 49s
All checks were successful
Build and push Doorman UI / API / docker (push) Successful in 49s
This commit is contained in:
parent
92e34d89c0
commit
e37df33c08
@ -11,9 +11,6 @@
|
||||
},
|
||||
{
|
||||
"path": "packages/serverless"
|
||||
},
|
||||
{
|
||||
"path": "../../sideprojects/2023/smart-door-buzzer-twilio-functions"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@ -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"
|
||||
|
||||
6
packages/buzzer-client/.env.example
Normal file
6
packages/buzzer-client/.env.example
Normal 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
133
packages/buzzer-client/.gitignore
vendored
Normal 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.*
|
||||
1
packages/buzzer-client/.nvmrc
Normal file
1
packages/buzzer-client/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
18
|
||||
42
packages/buzzer-client/.twilioserverlessrc
Normal file
42
packages/buzzer-client/.twilioserverlessrc
Normal 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 */,
|
||||
}
|
||||
3
packages/buzzer-client/.vscode/settings.json
vendored
Normal file
3
packages/buzzer-client/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"dotenv.enableAutocloaking": false
|
||||
}
|
||||
29
packages/buzzer-client/LICENSE
Normal file
29
packages/buzzer-client/LICENSE
Normal 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.
|
||||
51
packages/buzzer-client/README.md
Normal file
51
packages/buzzer-client/README.md
Normal 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
|
||||
```
|
||||
Binary file not shown.
Binary file not shown.
BIN
packages/buzzer-client/assets/buzzing_up_boosted.protected.mp3
Normal file
BIN
packages/buzzer-client/assets/buzzing_up_boosted.protected.mp3
Normal file
Binary file not shown.
BIN
packages/buzzer-client/bun.lockb
Executable file
BIN
packages/buzzer-client/bun.lockb
Executable file
Binary file not shown.
63
packages/buzzer-client/functions/buzzer-activated.js
Normal file
63
packages/buzzer-client/functions/buzzer-activated.js
Normal 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);
|
||||
};
|
||||
18
packages/buzzer-client/functions/call-residents.js
Normal file
18
packages/buzzer-client/functions/call-residents.js
Normal 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);
|
||||
}
|
||||
15
packages/buzzer-client/functions/door-open.js
Normal file
15
packages/buzzer-client/functions/door-open.js
Normal 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);
|
||||
};
|
||||
30
packages/buzzer-client/functions/text-me.js
Normal file
30
packages/buzzer-client/functions/text-me.js
Normal 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
9678
packages/buzzer-client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
packages/buzzer-client/package.json
Normal file
27
packages/buzzer-client/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user