From bc3b0a388d5fb6b43e7fd3a9ad18ff2ccace1edf Mon Sep 17 00:00:00 2001 From: Marcus Noble Date: Wed, 11 Oct 2017 20:42:47 +0100 Subject: [PATCH] Added call center post --- src/images/twilioPhoneNumberConfig.png | Bin 0 -> 30480 bytes .../2017-10-11-create-a-mini-call-center.md | 363 ++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 src/images/twilioPhoneNumberConfig.png create mode 100644 src/posts/2017-10-11-create-a-mini-call-center.md diff --git a/src/images/twilioPhoneNumberConfig.png b/src/images/twilioPhoneNumberConfig.png new file mode 100644 index 0000000000000000000000000000000000000000..0f17e96d24c54f08fcaf77d596c202826394bc9c GIT binary patch literal 30480 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 literal 0 HcmV?d00001 diff --git a/src/posts/2017-10-11-create-a-mini-call-center.md b/src/posts/2017-10-11-create-a-mini-call-center.md new file mode 100644 index 0000000..3157052 --- /dev/null +++ b/src/posts/2017-10-11-create-a-mini-call-center.md @@ -0,0 +1,363 @@ +--- +layout: post.html +title: Create your own mini call center with Twilio +date: 2017-10-11 +tags: +summary: "Have you ever wanted to run your own call center, but, like, a really small one? No? OK, do you know of any small groups of people that could benefit from all being contactable with a single number? Imagine a conference or meetup with a phone number you can call to report code of conduct violations that will put you through to the first available organiser. + + +I'm going to show you how you can do this with the help of [Twilio](https://www.twilio.com/) and a small Node.js application." +--- + +Have you ever wanted to run your own call center, but, like, a really small one? No? OK, do you know of any small groups of people that could benefit from all being contactable with a single number? Imagine a conference or meetup with a phone number you can call to report code of conduct violations that will put you through to the first available organiser. + +I'm going to show you how you can do this with the help of [Twilio](https://www.twilio.com/) and a small Node.js application. + +First steps would be [signing up for Twilio](https://www.twilio.com/try-twilio) and acquiring a [new phone number](https://www.twilio.com/console/phone-numbers). Once setup you will need to configure your number to work with the Node.js application we will be building. Setup your number like shown below: + +{{> picture alt="Twilio phone number configuration" caption="Twilio phone number configuration" url="/images/twilioPhoneNumberConfig.png" }} + +* **Accept Incoming**: Voice calls +* **Configure with**: Webhooks, or TwiML Bins or Functions +* **A call comes in**: Webhook **/** {URL of your application} **/** HTTP GET +* **Call status changes**: {URL of your application}/updates **/** HTTP GET +* _All other settings can be ignored._ + +> While developing locally I recommend using [ngrok](https://ngrok.com/) to create a secure tunnel to localhost. The rest of this post will assume you have that setup, running and that is the URL you have used above. + +With our new number configured we can make a start on building the application to handle it. Before we move on it is a good idea to find you [Account SID / Auth Token](https://www.twilio.com/console) as we'll need these shortly and make sure you know where the [Twilio Debugger](https://www.twilio.com/console/runtime/debugger?quickDate=24) is as this is super useful if anything goes wrong. + +We are aiming to create an application that will listen for an incoming call and then call out to all of our defined organisers. Once one of them picks up the incoming call will be connected to that organiser and the remaining outgoing calls will be cancelled. + +Lets begin! Create a new Node.js project and install `express`, `twilio` and `config`. + +```shell +npm init -f +npm install --save express twilio config +``` + +> _**Note:** All code examples are in [TypeScript](https://www.typescriptlang.org/)_, you can use standard JavaScript without too much being changed but I won't be covering that in this post. + +Our initial project setup will look something like this: + +```text +├─ config +│ └─ default.json +├─ callHandler.ts +├─ index.ts +├─ package.json +└─ tsconfig.json +``` + +As [Twilio](https://www.twilio.com/) uses webhooks for all incoming actions we will be using [Express](https://expressjs.com/) to listen for the HTTP requests. + + +```typescript +/* 📄 index.ts */ +import * as CallHandler from './callHandler'; // We'll be creating this shortly +import * as Express from 'express'; +const app = Express(); + +app.get('/', async (req: Express.Request, res: Express.Response) => { + res.set({ 'Content-Type': 'text/xml' }); + res.status(200).send(await CallHandler.initiateCall(req.query.CallSid)); // CallSid is a unique ID for the incoming call +}); + +app.get('/join', async (req: Express.Request, res: Express.Response) => { + res.set({ 'Content-Type': 'text/xml' }); + res.status(200).send(await CallHandler.joinCall(req.query.id)); +}); + +app.get('/updates', async (req: Express.Request, res: Express.Response) => { + // TODO: We will handle all event updates here later + res.status(200).end(); +}); + +app.listen(7000, () => { + console.log(`Server running at http://127.0.0.1:7000/`); +}); +``` + +When someone calls our new number Twilio will send a request to `'/'`, any changes made to that call (for example the user hanging up) and a request will be made to `'/updates'` with all the details. Finally we will use the `'/join'` endpoint to direct our organisers to in order to connect their call. For now we will focus on receiving the inbound call, Twilio expects us to respond with some text telling it what to do with the call. We can use the [Twilio NPM module](https://www.npmjs.com/package/twilio) to help us here. When we get an inbound call we will create a new conference call (using the ID of the call as the unique name) and have the caller join. + +```typescript +/* 📄 callHander.ts */ +import * as Twilio from 'twilio'; +const VoiceResponse = Twilio.twiml.VoiceResponse; + +interface Call { + status: 'pending' | 'active' | 'ended'; + outgoingNumbers: { [key: string]: string }; +} + +interface CallList { + [key: string]: Call; +} + +// We will use this to keep track of the status of each call +const calls: CallList = {}; + +export async function initiateCall(callId: string) { + calls[callId] = { + status: 'pending', + outgoingNumbers: {} + }; + + const voiceResponse = new VoiceResponse(); + const dial = voiceResponse.dial(); + dial.conference({ + beep: false, + startConferenceOnEnter: true, + endConferenceOnExit: true, + maxParticipants: 2 + }, callId); + return voiceResponse.toString(); +} +``` + +Great! Now we have our caller sat in a conference call all alone. Wouldn't it be good if they had someone to talk to? + +Lets create a function that will call out to each of our organisers. We are using the [config](https://www.npmjs.com/package/config) NPM module to pull in some configuration variables. + +```typescript +/* 📄 callHander.ts */ +import * as Config from 'config'; +const client = Twilio(Config.get('twilio.accountSid'), Config.get('twilio.authToken')); + +const hostname = Config.get('hostname'); // The full hostname we set up earlier as the 'A call comes in' webhook +const fromNumber = Config.get('twilio.phoneNumber'); // Our Twilio phone number +const organisersNumbers = Config.get('organisersNumbers'); // An array of all numbers we want to try and call out to + +function callOut(callId: string): void { + organisersNumbers.forEach(number => { + console.log(`Calling +${number}`); + client.calls.create({ + url: `${hostname}/join?id=${callId}&number=${number}`, + method: 'GET', + to: `+${number}`, + from: `+${fromNumber}`, + machineDetection: 'Enable' + }).then((call) => { + calls[callId].outgoingNumbers[number] = call.sid; + }); + }); +} +``` + +Lets go back to our `initiateCall` and trigger our new function. + +```typescript +/* 📄callHander.ts */ +export async function initiateCall(callId: string) { + calls[callId] = { + status: 'pending', + outgoingNumbers: {} + }; + + callOut(callId); + + const voiceResponse = new VoiceResponse(); + const dial = voiceResponse.dial(); + dial.conference({ + beep: false, + startConferenceOnEnter: true, + endConferenceOnExit: true, + maxParticipants: 2 + }, callId); + return voiceResponse.toString(); +} +``` + +We now have all our organisers being called when someone calls our number but so far nothing will happen when they pick up. Lets add the `joinCall` function that will have the organiser join the conference call we have previously set up with our inbound caller. + +```typescript +/* 📄 callHander.ts */ +export async function joinCall(callId: string) { + const voiceResponse = new VoiceResponse(); + const dial = voiceResponse.dial(); + dial.conference({ + beep: false, + startConferenceOnEnter: true, + endConferenceOnExit: true, + maxParticipants: 2 + }, callId); + return voiceResponse.toString(); +} +``` + +Hurray! We now have two people talking to each other! + +But... we still have outbound calls waiting to be picked up. We need to handle those. Lets create a function to hangup all other calls. + +```typescript +/* 📄 callHandler.ts */ +export async function hangup(callId: string, ignore?: string): void { + organisersNumbers.filter(number => number !== ignore).forEach(number => { + client.calls(calls[callId].outgoingNumbers[number]) + .update({ + status: 'canceled' + }); + delete calls[callId].outgoingNumbers[number]; + }); + + if (Object.keys(calls[callId].outgoingNumbers).length === 0) { + // All calls hung up + calls[callId].status = 'ended'; + } +} +``` + +We need to make sure we don't hang up our connected call so lets pass through the number so we can ignore it. + +```typescript +/* 📄 index.ts */ +app.get('/join', async (req: Express.Request, res: Express.Response) => { + res.set({ 'Content-Type': 'text/xml' }); + res.status(200).send(await CallHandler.joinCall(req.query.id, req.query.number)); +}); + +/* 📄 callHandler.ts */ +export async function joinCall(callId: string, number: string) { + + hangup(callId, number); + + const voiceResponse = new VoiceResponse(); + const dial = voiceResponse.dial(); + dial.conference({ + beep: false, + startConferenceOnEnter: true, + endConferenceOnExit: true, + maxParticipants: 2 + }, callId); + return voiceResponse.toString(); +} +``` + +It's also a good idea to make sure that if someone manages to pick up before we have a chance to hang up the call we handle it correctly by cancelling. + +```typescript +/* 📄 callHandler.ts */ +export async function joinCall(callId: string, number: string) { + const voiceResponse = new VoiceResponse(); + if (!calls[callId]) { + voiceResponse.hangup(); + } else if(calls[callId].status !== 'pending') { + delete calls[callId].outgoingNumbers[number]; + voiceResponse.hangup(); + } else { + // Mark our call as active to prevent any other organisers joining + calls[callId].status = 'active'; + + hangup(callId, number); + + const dial = voiceResponse.dial(); + dial.conference({ + beep: false, + startConferenceOnEnter: true, + endConferenceOnExit: true, + maxParticipants: 2 + }, callId); + } + return voiceResponse.toString(); +} +``` + +Finally, we need to make sure we cancel all outbound calls if the initial caller decides to hang up before anyone has answered. For this we'll need to use our `'/updates'` endpoint. + +```typescript +/* 📄 index.ts */ +app.get('/updates', async (req: Express.Request, res: Express.Response) => { + if (req.query.CallStatus === 'completed' && req.query.Direction === 'inbound') { + // We're not providing a number to ignore here so all outbound calls will be cancelled + CallHandler.hangup(req.query.CallSid); + } + res.status(200).end(); +}); +``` + +That's it! We now have a working, albeit a little crude, call center. + +## Bonus + +### Answering machine detection + +This approach isn't great if one of our organisers has their phone turned off and our user gets put through to someone's voicemail rather than waiting for someone who is available to pick up. You may have noticed in the `callOut` function that we are setting the `machineDetection: 'Enable'` property. This allows us to check if the call was picked up by a real person or not. We can use this by making a small change to our `joinCall` function. + +```typescript +/* 📄 index.ts */ +app.get('/join', async (req: Express.Request, res: Express.Response) => { + res.set({ 'Content-Type': 'text/xml' }); + res.status(200).send(await CallHandler.joinCall(req.query.id, req.query.number, req.query.AnsweredBy === 'human')); +}); + +/* 📄 callHandler.ts */ +export async function joinCall(callId: string, number: string, isHuman: boolean) { + const voiceResponse = new VoiceResponse(); + + if (!calls[callId]) { + voiceResponse.hangup(); + } else if(calls[callId].status !== 'pending') { + delete calls[callId].outgoingNumbers[number]; + voiceResponse.hangup(); + } else { + // Mark our call as active to prevent any other organisers joining + calls[callId].status = 'active'; + + hangup(callId, number); + + const voiceResponse = new VoiceResponse(); + const dial = voiceResponse.dial(); + dial.conference({ + beep: false, + startConferenceOnEnter: true, + endConferenceOnExit: true, + maxParticipants: 2 + }, callId); + } + + return voiceResponse.toString(); +} +``` + +### Wait for answer before connecting to conference call + +Having a mini call center is great and all but one thing that isn't great is... _hold music_. No one wants to sit and listen to some small clip of music on loop while waiting for someone to answer. It's much more natural and comfortable to have a ringing tone while waiting for someone to answer. + +We can achieve this by simply delaying our initial response until one of our organisers have answered. There will be a little awkward silence for our organiser while our caller connects but this shouldn't cause any issues. + +```typescript +/* 📄 callHander.ts */ +export async function initiateCall(callId: string) { + calls[callId] = { + status: 'pending', + outgoingNumbers: {} + }; + + callOut(callId); + + const voiceResponse = new VoiceResponse(); + + return new Promise((resolve) => { + let intervalId = setInterval(() => { + if (calls[callId] && calls[callId].status === 'active') { + clearInterval(intervalId); + + const dial = voiceResponse.dial(); + dial.conference({ + beep: false, + startConferenceOnEnter: true, + endConferenceOnExit: true, + maxParticipants: 2 + }, callId); + return resolve(voiceResponse.toString()); + } else if (!call[callId] || calls[callId].status === 'ended') { + clearInterval(intervalId); + voiceResponse.hangup(); + return resolve(voiceResponse.toString()); + } + }, 500); + }); +} +``` + +The final product can be found on GitHub - [AverageMarcus/SharedPhone](https://github.com/AverageMarcus/SharedPhone). I would be happy for any issues reported, enhancement suggestions or pull requests.