diff --git a/block-spam-calls/.env b/block-spam-calls/.env new file mode 100644 index 00000000..2c294ce3 --- /dev/null +++ b/block-spam-calls/.env @@ -0,0 +1,3 @@ +# description: The path to the webhook +# configurable: false +TWILIO_VOICE_WEBHOOK_URL=/block-spam-calls diff --git a/block-spam-calls/CHANGELOG.md b/block-spam-calls/CHANGELOG.md new file mode 100644 index 00000000..144828c2 --- /dev/null +++ b/block-spam-calls/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## [Unreleased] + +## [1.0.0] + +### Added + +- Initial release. diff --git a/block-spam-calls/README.md b/block-spam-calls/README.md new file mode 100644 index 00000000..77ad395e --- /dev/null +++ b/block-spam-calls/README.md @@ -0,0 +1,54 @@ +# block-spam-calls + +Uses Twilio Add-ons to block unwanted calls by checking the spam ratings of incoming phone numbers. + +## Pre-requisites + +- A Twilio account - [sign up here](https://www.twilio.com/try-twilio) +- A Twilio phone number +- Spam filtering Add-ons (see below) + +### Install Add-Ons + +The following guide will help you to [install Add-ons](https://www.twilio.com/docs/add-ons/install). You can access the Add-ons in the Twilio console [here](https://www.twilio.com/console/add-ons). The Spam Filtering Add-ons that are used on this application are: + +- [Marchex Clean Call](https://www.twilio.com/console/add-ons/XBac2c99d9c684a765ced0b18cf0e5e1c7) +- [Nomorobo Spam Score](https://www.twilio.com/console/add-ons/XB06d5274893cc9af4198667d2f7d74d09) + +Once you've selected the Add-on, just click on `Install` button. Then, you will see a pop-up window where you should read and agree the terms, then, click the button `Agree & Install`. For this application, you just need to handle the incoming voice calls, so make sure the `Incoming Voice Call` box for `Use In` is checked and click `Save`. + +### Function Parameters + +This template by default accepts no additional parameters. + +## Create a new project with the template + +1. Install the [serverless toolkit](https://www.twilio.com/docs/labs/serverless-toolkit/getting-started) +2. Install the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart#install-twilio-cli) +3. Initiate a new project + +``` +twilio serverless:init sample --template=block-spam-calls && cd sample +``` + +4. Start the server with the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): + +``` +twilio serverless:start --ngrok +``` + +5. Set your incoming call webhook URL for the phone number you want to configure to `https://.ngrok.io/block-spam-calls` + +ℹ️ Check the developer console and terminal for any errors + +## Deploying + +Deploy your functions and assets with either of the following commands. Note: you must run these commands from inside your project folder. [More details in the docs.](https://www.twilio.com/docs/labs/serverless-toolkit) + +With the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): + +``` +twilio serverless:deploy +``` + +Make sure to update your incoming voice URL to your newly deployed Function URL. diff --git a/block-spam-calls/assets/index.html b/block-spam-calls/assets/index.html new file mode 100644 index 00000000..0de5e91d --- /dev/null +++ b/block-spam-calls/assets/index.html @@ -0,0 +1,141 @@ + + + + + + + Get started with your Twilio Functions! + + + + + + + +
+
+ + +
+
+
+
+

+ +
+

Welcome!

+

Your live application with Twilio is ready to use!

+
+

+
+

Get started with your application

+

+ Follow the set up steps located in the + Block Spam Calls functions template repo + to get started. +

+ +

+ This app will return the + TwiML + required to reject or accept incoming Voice calls based on the phone + numbers spam rating. +

+
+
+ +
+
+

Troubleshooting

+
    +
  • + Check the + + phone number configuration + + and make sure the Twilio phone number you want for your app has a + Voice webhook configured to point at the following URL +
    + + +
    +
  • +
+
+
+
+ + + diff --git a/block-spam-calls/functions/block-spam-calls.protected.js b/block-spam-calls/functions/block-spam-calls.protected.js new file mode 100644 index 00000000..ece7f720 --- /dev/null +++ b/block-spam-calls/functions/block-spam-calls.protected.js @@ -0,0 +1,83 @@ +/* + * Block Spam Calls + * + * Description: + * This application uses Twilio Add-ons from the Twilio Marketplace to block + * unwanted voice calls. The 2 Add-ons used in this application are Marchex + * Clean Call and Nomorobo Spam Score. The application provides the spam rating + * of every inbound call to a Twilio number via the three Add-ons. + * If the phone number is classified as spam by any of the two integrations, the + * call is rejected. If the number isn't categorized as spam, the call will go through. + * + * Contents: + * 1. Input Helpers + * 2. Main Handler + */ + +/* + * 1. Input Helpers + * These helper functions help read the results from the spam Add-ons + * in the incoming voice TwiML callback. + * + * Function will return true if the call is classified as spam. + * Otherwise, it will return false. + */ + +function blockedByMarchex(response) { + if (!response || response.status !== 'successful') { + return false; + } + + return response.result.result.recommendation === 'BLOCK'; +} + +function blockedByNomorobo(response) { + if (!response || response.status !== 'successful') { + return false; + } + + return response.result.score === 1; +} +/** + * 2. Main Handler + * + * This is the entry point to your Twilio Function, + * which will create a new Voice Response using Twiml based on + * the spam filters. If the call is flagged as spam by any of the + * spam filtering add-ons, the call will blocked by the Twiml + * verb. Else, the call will proceed and the Voice Response + * will respond to the caller with a greeting. + * + * The callback will be used to return from your function + * with the Twiml Voice Response you defined earlier. + * In the callback in non-error situations, the first + * parameter is null and the second parameter + * is the value you want to return. + */ +exports.handler = function (context, event, callback) { + const twiml = new Twilio.twiml.VoiceResponse(); + + let blockCalls = false; + + /* + * If the request body contains add-ons, check to see if any of + * the spam filtering add-ons have flagged the number. + */ + const addOns = 'AddOns' in event && JSON.parse(event.AddOns); + if (addOns && addOns.status === 'successful') { + const { results } = addOns; + blockCalls = + blockedByMarchex(results.marchex_cleancall) || + blockedByNomorobo(results.nomorobo_spamscore); + } + + if (blockCalls) { + twiml.reject(); + } else { + // Add instructions here on what to do if call goes through + twiml.say('Welcome to the jungle.'); + twiml.hangup(); + } + + callback(null, twiml); +}; diff --git a/block-spam-calls/package.json b/block-spam-calls/package.json new file mode 100644 index 00000000..333fc1ca --- /dev/null +++ b/block-spam-calls/package.json @@ -0,0 +1,6 @@ +{ + "name": "block-spam-calls", + "version": "1.0.0", + "private": true, + "dependencies": {} +} diff --git a/block-spam-calls/tests/block-spam-calls.test.js b/block-spam-calls/tests/block-spam-calls.test.js new file mode 100644 index 00000000..6e0898ab --- /dev/null +++ b/block-spam-calls/tests/block-spam-calls.test.js @@ -0,0 +1,119 @@ +const helpers = require('../../test/test-helper'); +const blockSpamCalls = + require('../functions/block-spam-calls.protected').handler; +const Twilio = require('twilio'); + +let context = {}; +let event = {}; + +beforeAll(() => { + helpers.setup(context); +}); + +afterEach(() => { + context = {}; + event = {}; +}); + +afterAll(() => { + helpers.teardown(); +}); + +test('returns a VoiceResponse', (done) => { + const callback = (_err, result) => { + expect(result).toBeInstanceOf(Twilio.twiml.VoiceResponse); + done(); + }; + + blockSpamCalls(context, event, callback); +}); + +describe('call flagged as spam and rejected', () => { + const nomoroboSpamEvent = { + AddOns: JSON.stringify(require('./spam-filter-results/spam-nomorobo.json')), + }; + const marchexSpamEvent = { + AddOns: JSON.stringify(require('./spam-filter-results/spam-marchex.json')), + }; + + beforeAll(() => { + helpers.setup(context); + }); + + afterAll(() => { + helpers.teardown(); + }); + + test('flagged spam by nomorobo', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch(/Reject/); + done(); + }; + blockSpamCalls(context, nomoroboSpamEvent, callback); + }); + + test('flagged spam by marchex', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch(/Reject/); + done(); + }; + blockSpamCalls(context, marchexSpamEvent, callback); + }); +}); + +describe('call not flagged as spam', () => { + const failedNomoroboEvent = { + AddOns: JSON.stringify( + require('./spam-filter-results/failed-nomorobo.json') + ), + }; + const cleanNomoroboEvent = { + AddOns: JSON.stringify( + require('./spam-filter-results/clean-nomorobo.json') + ), + }; + const cleanMarchexEvent = { + AddOns: JSON.stringify(require('./spam-filter-results/clean-marchex.json')), + }; + const noAddonsEvent = {}; + + beforeAll(() => { + helpers.setup(context); + }); + + afterAll(() => { + helpers.teardown(); + }); + + test('flagged clean by nomorobo', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch(/Welcome to the jungle./); + done(); + }; + blockSpamCalls(context, cleanNomoroboEvent, callback); + }); + + test('flagged clean by marchex', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch(/Welcome to the jungle./); + done(); + }; + blockSpamCalls(context, cleanMarchexEvent, callback); + }); + + test('failed nomorobo response (call goes through)', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch(/Welcome to the jungle./); + done(); + }; + blockSpamCalls(context, failedNomoroboEvent, callback); + }); + + test('No addons present (call goes through)', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch(/Welcome to the jungle./); + done(); + }; + blockSpamCalls(context, noAddonsEvent, callback); + }); +}); diff --git a/block-spam-calls/tests/spam-filter-results/clean-marchex.json b/block-spam-calls/tests/spam-filter-results/clean-marchex.json new file mode 100644 index 00000000..b7067841 --- /dev/null +++ b/block-spam-calls/tests/spam-filter-results/clean-marchex.json @@ -0,0 +1,19 @@ +{ + "status": "successful", + "message": null, + "code": null, + "results": { + "marchex_cleancall": { + "request_sid": "XRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "status": "successful", + "message": null, + "code": null, + "result": { + "result": { + "recommendation": "PASS", + "reason": "CleanCall" + } + } + } + } +} diff --git a/block-spam-calls/tests/spam-filter-results/clean-nomorobo.json b/block-spam-calls/tests/spam-filter-results/clean-nomorobo.json new file mode 100644 index 00000000..dc55ddc2 --- /dev/null +++ b/block-spam-calls/tests/spam-filter-results/clean-nomorobo.json @@ -0,0 +1,18 @@ +{ + "status": "successful", + "message": null, + "code": null, + "results": { + "nomorobo_spamscore": { + "request_sid": "XRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "status": "successful", + "message": null, + "code": null, + "result": { + "status": "success", + "message": "success", + "score": 0 + } + } + } +} diff --git a/block-spam-calls/tests/spam-filter-results/failed-nomorobo.json b/block-spam-calls/tests/spam-filter-results/failed-nomorobo.json new file mode 100644 index 00000000..7b888be7 --- /dev/null +++ b/block-spam-calls/tests/spam-filter-results/failed-nomorobo.json @@ -0,0 +1,14 @@ +{ + "status": "successful", + "message": null, + "code": null, + "results": { + "nomorobo_spamscore": { + "request_sid": "XRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "status": "failed", + "message": "Vendor could not complete request", + "code": 61002, + "result": {} + } + } +} diff --git a/block-spam-calls/tests/spam-filter-results/spam-marchex.json b/block-spam-calls/tests/spam-filter-results/spam-marchex.json new file mode 100644 index 00000000..64a3176f --- /dev/null +++ b/block-spam-calls/tests/spam-filter-results/spam-marchex.json @@ -0,0 +1,19 @@ +{ + "status": "successful", + "message": null, + "code": null, + "results": { + "marchex_cleancall": { + "request_sid": "XRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "status": "successful", + "message": null, + "code": null, + "result": { + "result": { + "recommendation": "BLOCK", + "reason": "Testing" + } + } + } + } +} diff --git a/block-spam-calls/tests/spam-filter-results/spam-nomorobo.json b/block-spam-calls/tests/spam-filter-results/spam-nomorobo.json new file mode 100644 index 00000000..7f7ab168 --- /dev/null +++ b/block-spam-calls/tests/spam-filter-results/spam-nomorobo.json @@ -0,0 +1,18 @@ +{ + "status": "successful", + "message": null, + "code": null, + "results": { + "nomorobo_spamscore": { + "request_sid": "XRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "status": "successful", + "message": null, + "code": null, + "result": { + "status": "success", + "message": "success", + "score": 1 + } + } + } +} diff --git a/package.json b/package.json index aff7b881..0e07a451 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "verify-prefill", "reminder-message", "passkeys-backend", + "block-spam-calls", "ai-assistants-samples" ] } diff --git a/templates.json b/templates.json index 5f6a47b1..8d7d8a40 100644 --- a/templates.json +++ b/templates.json @@ -355,6 +355,11 @@ "name": "Backend for passkeys app", "description": "Connect applications with the passkeys service" }, + { + "id": "block-spam-calls", + "name": "Block Spam Calls", + "description": "Uses Twilio Add-ons to block unwanted calls by checking the spam ratings of incoming phone numbers." + }, { "id": "ai-assistants-samples", "name": "AI Assistants Samples",