Create a Code of Conduct Incident Line with Node.js
Published on May 5, 2021

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 running npm 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.

DT API Account

To complete this tutorial, you will need a DT 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 DT 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:

Web page showing one recording timestamp with a blue underlineWeb page showing one recording timestamp with a blue underline

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'))
  })
})

A page showing a phone number, timestamp, and download linkA page showing a phone number, timestamp, and download link

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 %}

Web page showing both recordings and two example messagesWeb page showing both recordings and two example messages

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:

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.

Kevin LewisVonage Alumni

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.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.