Having a Code of Conduct as a community organizer is only one part of the story—having well-thought-out ways to report and respond to bad behavior is also vital. At events I've run in the past, a phone number has been one way provided to attendees—they can either call or text the number and it forwards on to several organizers who have the responsibility to be available to deal with any issues.
Today I'll show you how to build your own with the Vonage Voice and Messages APIs, complete with a simple dashboard to download call recordings and log incoming messages.
You can find the final project code at https://github.com/nexmo-community/node-code-of-conduct-conference-call
Prerequisites
Node.js installed on your machine
node-cli
, which you can install by runningnpm install nexmo-cli@beta -g
Create a new directory and open it in a terminal. Run npm init -y
to create a package.json
file and install dependencies with npm install express body-parser nunjucks uuid nedb-promises nexmo@beta
.
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.
Set up Dependencies
Create an index.js
file and set up the dependencies:
index.js
const uuid = require('uuid')
const app = require('express')()
const bodyParser = require('body-parser')
const nedb = require('nedb-promises')
const Nexmo = require('nexmo')
const nunjucks = require('nunjucks')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
// Future code goes here
app.listen(3000)
Once you've done this, run npx ngrok http 3000
in a new terminal, and take note of the temporary ngrok URL. This is used to make localhost:3000
available to the public web.
Buy a Virtual Number & Set up the Nexmo Client
Open another terminal in your project directory and create a new application with the command line interface (CLI):
nexmo app:create
-> Select Capabilities: voice, messages
-> Use the default HTTP methods? Y
-> Voice Answer URL: https://NGROK_URL/answer
-> Voice Event URL: https://NGROK_URL/event
-> Messages Inbound URL: https://NGROK_URL/inbound
-> Messages Status URL: https://NGROK_URL/event
-> Private Key path: private.key
Take note of the Application ID shown in your terminal, then search for a number (you can replace GB with your country code):
nexmo number:search GB --sms --voice
Copy one of the numbers to your clipboard, buy it and link it to your application:
nexmo number:buy NUMBER
nexmo link:app NUMBER APP_ID
nexmo numbers:update NUMBER --mo_http_url https://NGROK_URL/sms
In index.js
, initialize the Nexmo client:
const nexmo = new Nexmo({
apiKey: 'API_KEY',
apiSecret: 'API_SECRET',
applicationId: 'APPLICATION_ID',
privateKey: './private.key'
})
Respond to an Incoming Call With Speech
Create the GET /answer
endpoint and return a Nexmo Call Control Object (NCCO) with a single talk
action:
app.get('/answer', async (req, res) => {
res.json([
{ action: 'talk', voiceName: 'Amy', text: 'This is the Code of Conduct Incident Response Line' }
])
})
app.post('/event', (req, res) => {
res.status(200).end()
})
The POST /event
endpoint will later have call data sent to it, and for now, should just respond with a HTTP 200 OK
status.
Checkpoint: Start your server by running node index.js
and then call the number you bought with the CLI - you should have the message read aloud, and then the call should hang up. If there are issues, you can always check the number and application settings in the dashboard.
Respond to an Incoming Call by Dialling In Organizers
Instead of just reading out the message, add the caller to a brand new conversation. We can control conversations with code, including adding multiple participants into the call - you only need to know the conversation name to do this. Replace the content of the /answer
endpoint with:
const conferenceId = uuid.v4()
res.json([
{ action: 'talk', voiceName: 'Amy', text: 'This is the Code of Conduct Incident Response Line' },
{ action: 'conversation', name: conferenceId, record: true }
])
This code generates a new unique ID and then adds the caller to a conversation which uses a name as an identifer (conversations are calls with one more more participants in this context). However, one-person conference calls are sad. Before res.json()
, call each organizer and add them to the conference call:
for(let organizerNumber of ['NUMBER ONE', 'NUMBER TWO']) {
nexmo.calls.create({
to: [{ type: 'phone', number: organizerNumber }],
from: { type: 'phone', number: 'NEXMO NUMBER' },
ncco: [
{ action: 'conversation', name: conferenceId }
]
})
}
Each number must be in E.164 format, and you should replace NEXMO NUMBER
with the number linked to your application. While testing, make sure the numbers in the array are not the same as the one you'll use to call.
Checkpoint: Restart your server and call your Nexmo number. The application should ring in any numbers provided in the for() loop array.
Record the Call
When adding the caller to the conference call, record: true
was passed as an option, and, as a result, the entire call was recorded. Once the call is completed, the POST /event
endpoint is sent a payload containing the conversation ID and a recording URL.
Before the existing endpoints create a new nedb database:
const recordingsDb = nedb.create({ filename: 'data/recordings.db', autoload: true })
Once you restart your server, a file will be created inside of a data
directory. Update the event endpoint to look like this:
app.post('/event', async (req, res) => {
if(req.body.recording_url) {
await recordingsDb.insert(req.body)
}
res.status(200).end()
})
Checkpoint: Restart your server and call your Nexmo number. Once all participants hang up, you should see a new entry in the data/recordings.db file.
Create a Recordings Dashboard
Now the recording data is saved in a database; it's time to create a dashboard. Configure nunjucks before the first endpoint:
nunjucks.configure('views', { express: app })
This sets up nunjucks to render any file in the views
directory and links to the express application stored in the app
variable. Create a views
directory and an index.html
file inside of it:
<h1>Recordings</h1>
{% for recording in recordings %}
<p>
<a href="/details/{{recording.conversation_uuid}}">{{recording.start_time}}</a>
</p>
{% endfor %}
Also create a details.html
file in the views
directory:
<ul>
<li>{{caller}}</li>
<li>{{recording.timestamp}}</li>
<li><a href="/details/{{recording.conversation_uuid}}/download">Download</a></li>
</ul>
Three endpoints are required in index.js
to get these views working. The first one loads all of the recordings from the database and renders the index page:
app.get('/', async (req, res) => {
const recordings = await recordingsDb.find().sort({ timestamp: -1 })
res.render('index.html', { recordings })
})
The page now looks like this, with latest recordings first:
The next endpoint loads the details page after getting details from the Conversations API, including the phone number of the caller:
app.get('/details/:conversation', (req, res) => {
nexmo.conversations.get(req.params.conversation, async (error, result) => {
const caller = result.members.find(member => member.channel.from != process.env.NEXMO_NUMBER)
const number = caller.channel.from.number
const recording = await recordingsDb.findOne({ conversation_uuid: req.params.conversation })
res.render('detail.html', { caller: number, recording })
})
})
Finally, an endpoint which gets the raw audio file from the API and sends it as a downloadable MP3:
app.get('/details/:conversation/download', async (req, res) => {
const recording = await recordingsDb.findOne({ conversation_uuid: req.params.conversation })
nexmo.files.get(recording.recording_url, (error, result) => {
res.writeHead(200, {
'Content-Disposition': 'attachment; filename="recording.mp3"',
'Content-Type': 'audio/mpeg',
})
res.end(Buffer.from(result, 'base64'))
})
})
Checkpoint: Restart your server and call your Nexmo number. Once a call has completed, you should see the new entry on the dashboard. Go to the details page and download it.
Accept & Save SMS
Being a phone number, some people using this service may also send an SMS message to it. Using a similar pattern these messages will be stored and shown on the dashboard. Underneath the existing database creation, add a new one for messages:
const messagesDb = nedb.create({ filename: 'data/messages.db', autoload: true })
Save new messages as they are received by creating an endpoint which we previously pointed to when setting up our virtual number:
app.post('/sms', async (req, res) => {
await messagesDb.insert(req.body)
res.status(200).end()
})
Update the dashboard endpoint to also retrieve and display messages:
app.get('/', async (req, res) => {
const recordings = await recordingsDb.find().sort({ timestamp: -1 })
const messages = await messagesDb.find().sort({ 'message-timestamp': -1 })
res.render('index.html', { recordings, messages })
})
Add this section to the bottom of index.html
:
{% for message in messages %}
<p>{{message.msisdn}} ({{message['message-timestamp']}}): {{message.text}}</p>
{% endfor %}
Checkpoint: Restart your server and send an SMS to your Nexmo number. You should see it appear on your dashboard once you refresh.
Forward SMS and Send a Response
Finally, update the SMS endpoint to both forward the message to organizers and respond to the sender:
app.post('/sms', async (req, res) => {
await messagesDb.insert(req.body)
for(let organizerNumber of ['NUMBER ONE', 'NUMBER TWO']) {
nexmo.channel.send(
{ type: 'sms', number: organizerNumber },
{ type: 'sms', number: 'NEXMO NUMBER' },
{ content: { type: 'text', text: `From ${req.body.msisdn}\n\n${req.body.text}` } }
)
}
nexmo.channel.send(
{ type: 'sms', number: req.body.msisdn },
{ type: 'sms', number: 'NEXMO NUMBER' },
{ content: { type: 'text', text: 'Thank you for sending us a message. Organizers have been made aware and may be in touch for more information.' } }
)
res.status(200).end()
})
Checkpoint: Restart your server and send an SMS to your Nexmo number. You should receive a response, and all listed organizers should also receive the message.
Next Steps
Congratulations! You now have a functional Code of Conduct Incident Response Line that works for both phone calls and SMS messages. If you have more time, you may want to explore:
Implementing error handling
Using our new Speech Recognition to transcribe calls
You can find the final project code at https://github.com/nexmo-community/node-code-of-conduct-conference-call
As ever, if you need any support feel free to reach out in the Vonage Developer Community Slack. We hope to see you there.
Former Developer Advocate for Vonage, where his role was to support the local tech community in London. He’s an experienced events organiser, boardgamer and dad to a cute little dog called Moo. He’s also the lead organizer for You Got This - a network of events on the core skills needed for a happy, healthy work life.