Build a Chatbot


Build a Zoom Chatbot from scratch using Node.js, PostgreSQL, the Unsplash API, and Heroku.

In this article we will build a Zoom Chatbot that sends Unsplash photos! You can code along with me, or you can skip the coding and go straight to deploying the completed code to a Heroku server.

Prerequisites:

  1. A free Zoom Account
  2. A free Unsplash account
  3. A free Heroku account
  4. Ngrok.io (You will need this if you are coding along-Ngrok turns localhost into a free web server)

On the Chatbot app type, click Create. Provide an App Name and click Create.

STEP 2. Create an Unsplash App

Create an app on Unsplash.

Give your application a name and description, and click Create application.

STEP 3. Write Code for the Chatbot

If you would like to skip this step and just deploy the finished code to Heroku, click the Deploy to Heroku button. (You will still need to configure a few simple things, so skip to STEP 4.)

Deploy

Install Node.js if you don’t have it already

Create a new folder where you want your Zoom Chatbot to live,

$ mkdir zoom-chatbot
$ cd zoom-chatbot

Initialize git and npm. For the npm prompts just hit enter to use the default settings,

$ git init
$ npm init

Create your index.js, .gitignore, and .env files,

$ touch index.js .gitignore .env

Add these two lines to your .gitignore file,

.gitignore

.env
node_modules

Add the start script to your package.json file in the scripts object,

"start": "node index.js"

Add the following code to your .env file, replacing the placeholder values with your Unsplash Access Key (it’s the same for production and development, found on your Unsplash App Page), and your Zoom Development Client ID (found on your Zoom App Credentials page), Zoom Development Client Secret (found on your Zoom App Credentials page), Zoom Development Bot JID, and Zoom Verification Token (found on your Zoom Features page). This gives our app the credentials we need to make http requests to the Zoom and Unsplash API’s.

(We will fill in the DATABASE_URL later).

.env

unsplash_access_key=UNSPLASH_ACCESS_KEY_HERE
zoom_client_id=ZOOM_CLIENT_ID_HERE
zoom_client_secret=ZOOM_CLIENT_SECRET_HERE
zoom_bot_jid=ZOOM_BOT_JID_HERE
zoom_verification_token=ZOOM_VERIFICATION_TOKEN_HERE
DATABASE_URL=

Then install these dependencies,

$ npm install request pg express dotenv body-parser --save

Add the following code to your index.js file,

index.js

require('dotenv').config()
const express = require('express')
const bodyParser = require('body-parser')
const request = require('request')

const app = express()
const port = process.env.PORT || 4000

app.use(bodyParser.json())

app.get('/', (req, res) => {
  res.send('Welcome to the Unsplash Chatbot for Zoom!')
})

app.get('/authorize', (req, res) => {
  res.send('Thanks for installing the Unsplash Chatbot for Zoom!')
})

app.get('/support', (req, res) => {
  res.send('Contact {{EMAIL}} for support.')
})

app.get('/privacy', (req, res) => {
  res.send('The Unsplash Chatbot for Zoom does not store any user data.')
})

app.get('/zoomverify/verifyzoom.html', (req, res) => {
  res.send(process.env.zoom_verification_code)
})

app.post('/unsplash', (req, res) => {
  console.log(req.body)
  res.send('Chat received')
})

app.post('/deauthorize', (req, res) => {
  if (req.headers.authorization === process.env.zoom_verification_token) {
    res.status(200)
    res.send()
    request({
      url: 'https://api.zoom.us/oauth/data/compliance',
      method: 'POST',
      json: true,
      body: {
        'client_id': req.body.payload.client_id,
        'user_id': req.body.payload.user_id,
        'account_id': req.body.payload.account_id,
        'deauthorization_event_received': req.body.payload,
        'compliance_completed': true
      },
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + Buffer.from(process.env.zoom_client_id + ':' + process.env.zoom_client_secret).toString('base64'),
        'cache-control': 'no-cache'
      }
    }, (error, httpResponse, body) => {
      if (error) {
        console.log(error)
      } else {
        console.log(body)
      }
    })
  } else {
    res.send('Unauthorized request to Unsplash Chatbot for Zoom.')
  }
})

app.listen(port, () => console.log(`Unsplash Chatbot for Zoom listening on port ${port}!`))

This code is our Node.js skeleton, with all the required url paths for a Zoom App.

Now let’s run our app! I prefer to run my app with nodemon which reloads my index.js file every time I make a change and save.

$ npm install nodemon -g
$ nodemon

Go to localhost:4000 in your browser, and you should see this,

Great our Node.js server works!

Localhost is great, but it has its downfalls. One being we can’t access it outside of our own computer. We need our Zoom Chatbot to be able to send requests to our localhost server. To do that we can use a cool tool called ngrok.io to turn our localhost:4000 into a live web server.

Keeping your localhost server running, open a new terminal tab in your project root and run this command,

$ ngrok http 4000

Your terminal should look like this:

Copy the https url (for me it’s https://d4c4477b.ngrok.io), paste it in your browser, and hit the enter key. You should see the same page as when you went to localhost:4000, but now your server is accessible outside of your local machine.

Now we want to add the ngrok url to your Zoom App Dashboard and configure what our Zoom Chatbot can do by setting the scopes. Go to the Zoom App Credentials page,

Add the ngrok url into the Development Redirect URL for OAuth input with “/authorize” on the end. After a user installs our app, they will be taken to the “/authorize” path that we have in our code, where we thank them for installing our app! We also want to add our ngrok url into the Whitelist URL input so Zoom knows where requests will come from. Then click the Scopes link on the right side menu.

Click Add Scopes and find the “imchat:bot” scope which enables your Zoom Chatbot within Zoom Chat.

Now we can install our app to the Zoom Client! It doesn’t do much yet, but let’s see how it works! Click on the Local Test link on the left side menu.

Click the green Install button,

Click the blue Authorize button to install your Zoom Chatbot to your Zoom Client,

Our Chatbot is installed! Notice how we landed on our apps redirect page?

Now let’s go to the Zoom Client and check out our Chatbot!

If we type anything, nothing will happen though. Let’s change that!

Go back to the Zoom App Dashboard where we will add our slash command and Bot endpoint URL so that our Zoom Chatbot knows what to do with the messages we sent it.

Add the name you would like to use to talk to your Zoom Chatbot, like “unsplash” in the “Command” field.

NOTE: The “Command” field needs to be unique from all other Zoom Chatbots, so consider calling it “photo” or “unsplash1”. We will be improving this in the future.

Add your ngrok url with “/unsplash” on the end in both the Development and Production “Bot endpoint URL”. If you look at your index.js file you’ll notice we have a “/unsplash” path that will receive these requests. (The path has to be “/unsplash” unless you changed this in the code.)

Make sure to click the blue Save button.

Now lets try sending a message to our chatbot and see what it sends to our server. Type “hi” and hit enter.

{ 
  event: 'bot_notification',
  payload: {
    robotJid: 'v12qbexyetsaokavo42lclfa@xmpp.zoom.us',
    toJid: 'kdykjnimt4kpd8kkdqt9fq@xmpp.zoom.us',
    userJid: 'kdykjnimt4kpd8kkdqt9fq@xmpp.zoom.us',
    cmd: 'hi',
    accountId: 'gVcjZnWWRLWvv_GtyGuaxg',
    userId: 'KdYKjnimT4KPd8KKdQt9FQ',
    name: 'Tommy Gaessler',
    timestamp: 1558736172787
  }
}

Awesome, our Zoom Chatbot and Node.js Server are now connected! With this data and a Zoom access_token we can create a POST request to the Zoom send chatbot message endpoint. This http request allows us to send messages as the Chatbot.

All we need now is the Zoom access_token. Let’s make a call to get a Zoom access_token.

Replace the code inside your “/unsplash” route with this,

index.js

getChatbotToken()

function getChatbotToken () {
  request({
    url: `https://api.zoom.us/oauth/token?grant_type=client_credentials`,
    method: 'POST',
    headers: {
      'Authorization': 'Basic ' + Buffer.from(process.env.zoom_client_id + ':' + process.env.zoom_client_secret).toString('base64')
    }
  }, (error, httpResponse, body) => {
    if (error) {
      console.log('Error getting chatbot_token from Zoom.', error)
    } else {
      body = JSON.parse(body)
      sendChat(body.access_token)
    }
  })
}

function sendChat (chatbotToken) {
  request({
    url: 'https://api.zoom.us/v2/im/chat/messages',
    method: 'POST',
    json: true,
    body: {
      'robot_jid': process.env.zoom_bot_jid,
      'to_jid': req.body.payload.toJid,
      'account_id': req.body.payload.accountId,
      'content': {
        'head': {
          'text': 'Unsplash'
        },
        'body': [{
          'type': 'message',
          'text': 'You sent ' + req.body.payload.cmd
        }]
      }
    },
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + chatbotToken
    }
  }, (error, httpResponse, body) => {
    if (error) {
      console.log('Error sending chat.', error)
    } else {
      console.log(body)
    }
  })
}

Now test out your chatbot again, we should see a reply back!

Sending text back is cool, but sending a photo back is way cooler! Let’s integrate with Unsplash!

We will be making a get request to Unsplash’s random photo api endpoint, and passing in a query param of a search term! The search term will come from what we send to our Zoom Chatbot!

Replace the code inside your “/unsplash” route with this,

index.js

getChatbotToken()

function getPhoto (chatbotToken) {
  request(`https://api.unsplash.com/photos/random?query=${req.body.payload.cmd}&orientation=landscape&client_id=${process.env.unsplash_access_key}`, (error, body) => {
    if (error) {
      console.log('Error getting photo from Unsplash.', error)
    } else {
      body = JSON.parse(body.body)
      if (body.errors) {
        var errors = [
          {
            'type': 'section',
            'sidebar_color': '#D72638',
            'sections': body.errors.map((error) => {
              return { 'type': 'message', 'text': error }
            })
          }
        ]
        sendChat(errors, chatbotToken)
      } else {
        var photo = [
          {
            'type': 'section',
            'sidebar_color': body.color,
            'sections': [
              {
                'type': 'attachments',
                'img_url': body.urls.regular,
                'resource_url': body.links.html,
                'information': {
                  'title': {
                    'text': 'Photo by ' + body.user.name
                  },
                  'description': {
                    'text': 'Click to view on Unsplash'
                  }
                }
              }
            ]
          }
        ]
        sendChat(photo, chatbotToken)
      }
    }
  })
}

function sendChat (chatBody, chatbotToken) {
  request({
    url: 'https://api.zoom.us/v2/im/chat/messages',
    method: 'POST',
    json: true,
    body: {
      'robot_jid': process.env.zoom_bot_jid,
      'to_jid': req.body.payload.toJid,
      'account_id': req.body.payload.accountId,
      'content': {
        'head': {
          'text': '/unsplash ' + req.body.payload.cmd
        },
        'body': chatBody
      }
    },
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + chatbotToken
    }
  }, (error, httpResponse, body) => {
    if (error) {
      console.log('Error sending chat.', error)
    } else {
      console.log(body)
    }
  })
}

function getChatbotToken () {
  request({
    url: `https://api.zoom.us/oauth/token?grant_type=client_credentials`,
    method: 'POST',
    headers: {
      'Authorization': 'Basic ' + Buffer.from(process.env.zoom_client_id + ':' + process.env.zoom_client_secret).toString('base64')
    }
  }, (error, httpResponse, body) => {
    if (error) {
      console.log('Error getting chatbot_token from Zoom.', error)
    } else {
      body = JSON.parse(body)
      getPhoto(body.access_token)
    }
  })
}

This code requests a Zoom access_token, gets a photo from Unsplash, and sends the photo to Zoom Chat via the attachment message type.

Now you might be wondering why are we getting a new access_token every time we call our “/unsplash” endpoint? Zoom access_tokens last for one hour so we want to store it in a database and each time “/unsplash” is called we can check if the access token is expired. If the access_token is expired we can refresh it by calling our getChatbotToken function, but if it is still valid, we can skip that call and go straight to calling our getPhoto function, passing in our stored Zoom access_token. This makes our app faster, and allows us to make less requests to the Zoom API in order to achieve for our app to work.

Let’s finish up our Zoom Chatbot code by adding a PostgreSQL database to store our Zoom access_token, and adding logic in our code to get a new one if it is expired.

Download PostgreSQL here or if on a Mac install using Homebrew,

$ brew install postgresql

Once PostgreSQL is installed, follow these commands if you haven’t set it up before,

$ brew services start postgresql
$ psql postgres

You should be inside the PostgreSQL terminal now and see a postgres=# preifx. Now let’s create a database user called “me” with a password of “password”

postgres=# CREATE ROLE me WITH LOGIN PASSWORD 'password';
postgres=# ALTER ROLE me CREATEDB;
postgres=# \q

You have just added yourself as a user who has the create database permission. Now type this to connect to postgres as your user,

$ psql -d postgres -U me

Now that PostgreSQL is configured, let’s create a database, connect to it, and create a table to store our access_token. We will also seed our database with a blank access_token and an expires_on date of 1. That way, the first time we call our Zoom Chatbot it will think the access_token is expired. Then it will generate a new one for us, and save it. Run these postgres commands,

postgres=> CREATE DATABASE zoom_chatbot;
postgres=> \c zoom_chatbot
zoom_chatbot=> CREATE TABLE chatbot_token (token TEXT,  expires_on NUMERIC);
zoom_chatbot=> INSERT INTO chatbot_token (token, expires_on)  VALUES ('', '1');

Now that our database is setup, we need to connect PostgreSQL to our Node.js code. Add the following code under “const request = require(‘request’);”

index.js

const { Client } = require('pg')
const pg = new Client(process.env.DATABASE_URL)

pg.connect().catch((error) => {
  console.log('Error connecting to database', error)
})

Next, replace the “getChatbotToken()” call inside the “/unsplash” route with this,

index.js

if (req.headers.authorization === process.env.zoom_verification_token) {
  res.status(200)
  res.send()
  pg.query('SELECT * FROM chatbot_token', (error, results) => {
    if (error) {
      console.log('Error getting chatbot_token from database.', error)
    } else {
      if (results.rows[0].expires_on > (new Date().getTime() / 1000)) {
        getPhoto(results.rows[0].token)
      } else {
        getChatbotToken()
      }
    }
  })
} else {
  res.send('Unauthorized request to Unsplash Chatbot for Zoom.')
}

Lastly, replace the “getPhoto(body.access_token)” call inside the getChatbotToken() function with this,

index.js

pg.query(`UPDATE chatbot_token SET token = '${body.access_token}', expires_on = ${(new Date().getTime() / 1000) + body.expires_in}`, (error, results) => {
  if (error) {
    console.log('Error setting chatbot_token in database.', error)
  } else {
    getPhoto(body.access_token)
  }
})

Here is the completed index.js file for reference.

Now go to your .env file to add your DATABASE_URL. If you followed my instructions on setting up PostgreSQL it should be this,

postgres://me:password@localhost:5432/zoom_chatbot

If you have setup PostgreSQL before or set it up differently than me reference this image.

Image Source: http://www.javascriptpoint.com/nodejs-postgresql-tutorial-example/
Image Source: http://www.javascriptpoint.com/nodejs-postgresql-tutorial-example/

Now let’s test our chatbot to seed our database with a new access_token and expires_on date.

Awesome it worked!

And if we check our database, we now have a Zoom access_token, and an expires_on date,

zoom_chatbot=> SELECT * FROM chatbot_token;
token        | expires_on
-------------+--------------
asdfjkhqwei..|1559690220.871

Now that our code is complete, it’s time to create our Heroku server and push our database and code to it! If you haven’t setup the Heroku CLI yet, you can do that here.

$ heroku create
$ heroku addons:create heroku-postgresql:hobby-dev

We can run this magical command to copy our local PostgreSQL database to Heroku. Your Heroku database name will look something like “postgresql-concave-52656” and your Heroku app name should be something like “fast-gorge-64583”. You should be able to see these in your terminal after running the command above, but if not, run $ heroku addons to see the database name and app name in parentheses .

$ heroku pg:push zoom_chatbot HEROKU_DATABASE_NAME --app HEROKU_APP_NAME

Our database is now on Heroku! Let’s push our code up too!

$ git add -A
$ git commit -m "deploying to heroku"
$ git push heroku master

Now our code should be deployed to heroku! To open our landing page, type,

$ heroku open

You should see “Welcome to the Unsplash Bot for Zoom Chat!”

STEP 4. Configure Heroku

If you coded along, Navigate to your Heroku dashboard so we can insert our production keys and credentials into the Config Vars section on the Settings page. Copy these keys into your Heroku KEY inputs,

If you used the Deploy to Heroku button, enter a name for your app on the page the button took you to (or leave it blank to have a name generated for you), and fill in the values for these,

  • unsplash_access_key (Your Unsplash Access Key, it’s the same for production and development, found on your Unsplash App Page)
  • zoom_client_id (Your Zoom Production Client ID, found on your Zoom App Credentials page)
  • zoom_client_secret (Your Zoom Production Client Secret, found on your Zoom App Credentials page)
  • zoom_bot_jid (Your Zoom Production Bot JID, found on your Zoom Features page)
  • zoom_verification_token (Your Zoom Verification Token, found on your Zoom Features page)
  • zoom_verification_code (Optional, Your Zoom domain verification code, used to verify your domain name, found on your Zoom Submit page)

NOTE: Do not add or change the DATABASE_URL field. It is populated automatically.

If you coded, it should look like this, (the green being the values)

Or this if you clicked the Deploy to Heroku button,

The final step is to insert your Heroku url into the Zoom production inputs. Make sure you include the “/authorize” path param at the end of the base url because that will match the path in our code. For the Whitelist URL, just add the base Heroku url.

We need to add our Heroku url to a few more places. Go to the Information page and make sure to include the “/privacy” path param for the Privacy Policy URL, the “/support” path param for the Support URL, and the “/deauthorize” path param for the Endpoint URL because we have these paths in our code. Zoom requires us to have these urls so users using our app can get support, see our privacy policy, and uninstall our app.

Lastly go to the Features page, add the Heroku url and make sure to include the “/unsplash” path param so that it matches the path in our code.

Click the blue Save button.

And for good measure I added the Unsplash logo and a short / long description for the app.

Now go to the Submit page, click the Generate button, copy and paste the url in your browser, then click the blue Authorize button to install the production version of your chatbot to the Zoom Client!

NOTE: Doing this installs the app to your Zoom account only. People outside your account will not be able to install it using this link, unless you Submit it to the Zoom App Marketplace, which we will not do for this tutorial.

Now that our production Chatbot is finished and installed, let’s try it out!

Give yourself a high five, you made a Zoom Chatbot! You can talk to it directly without the your slash command, or use it in channels and direct messages with the your slash command.

You can find the completed code here.


Need Support?

The first place to look for help is on our Developer Forum, where Zoom Marketplace Developers can ask questions for public answers.

If you can’t find the answer in the Developer Forum or your request requires sensitive information to be relayed, please email us at developersupport@zoom.us.