DEV Community

Cover image for Create a Code of Conduct Incident Line with Node.js
Kevin Lewis for Vonage

Posted on • Edited on

Create a Code of Conduct Incident Line with Node.js

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
  • A Vonage API account - take note of your API Key & Secret on the dashboard
  • 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.

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Take note of the Application ID shown in your terminal, then seach for a number (you can replace GB with your country code):

nexmo number:search GB --sms --voice
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In index.js, initialize the Nexmo client:

const nexmo = new Nexmo({ 
  apiKey: 'API_KEY', 
  apiSecret: 'API_SECRET',
  applicationId: 'APPLICATION_ID',
  privateKey: './private.key'
})
Enter fullscreen mode Exit fullscreen mode

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()
})
Enter fullscreen mode Exit fullscreen mode

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 }
])
Enter fullscreen mode Exit fullscreen mode

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 }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

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()
})
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 })
})
Enter fullscreen mode Exit fullscreen mode

The page now looks like this, with latest recordings first:

Web page showing one recording timestamp with a blue underline

he 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 })
  })
})
Enter fullscreen mode Exit fullscreen mode

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'))
  })
})
Enter fullscreen mode Exit fullscreen mode

A 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 a 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 })
Enter fullscreen mode Exit fullscreen mode

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()
})
Enter fullscreen mode Exit fullscreen mode

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 })
})
Enter fullscreen mode Exit fullscreen mode

Add this section to the bottom of index.html:


{% for message in messages %}
  <p>{{message.msisdn}} ({{message['message-timestamp']}}): {{message.text}}</p>
{% endfor %}
Enter fullscreen mode Exit fullscreen mode

Web page showing both recordings and two example messages

Checkpoint: Restart your server and send a 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()
})
Enter fullscreen mode Exit fullscreen mode

Checkpoint: Restart your server and send a 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.

Top comments (0)