Google Maps based UGC Approval using Slack via Node.js

Moderating is something you need to have in place once allowing for user generated content (UGC). It can be painful to work with if the approval process is not made smooth. Here I share our approach for a client using Slack.

The content we are working with is GEO based UGC for their App. In order to streamline the process of receiving notifications and also approving content in one go we decided to use Slack.

Content approval with Slack

The example I use shows how you can push a Slack message from Node.js that contains a Google Map. By using the Google API and static maps you can easily generate an image that can be used as a preview for the Slack messages.

In addition to get a preview of the actual map representation of the GEO location we are also adding an interactive button in our Slack message. This gives the user the possibility to directly perform the action of approving or rejecting the user generated content.

Initializing the npm Node.js environment

For this example we needed a couple of external libraries. I decided to go for express to simplify the creation of our server. Node fetch with form data is used to send our message to Slack. Finally the body parser component makes it easy for us to work with the response from Slack as it manages parsing of the JSON body.

To get everything set up and downloaded you simply need to run the following commands in your project directory.

npm initnpm install node-fetch form-data express body-parser --save

The project directory is simple and we are just containing a file index.js.

Running and testing our server

To start your server you simply run the command "node index.js" as shown below. To test the postMessage function you can simply call "http://localhost:3000/postMessage" in your browser. Of course the postMessage function is what you would connect to whenever a user actually submits some GEO based content.

$ node index.jsSlack notification App listening on port 3000!

Generating the maps

Generating static maps with the Google API is pretty straight forward. You basically construct a URL with latitude and longitude and the zoom level. You can add markers on the map also at a given lat long. Google does a good job of documenting the Google Maps API.

Registering and setting up your Slack App

Before you can start sending interactive messages to Slack you need to:

  • Register your App on Slack
  • Define permissions: chat:write:bot
  • Install the App for your team
  • Receive OAuth Access token for sending messages
  • Receive Verification Token for verifying messages from Slack
  • Register your interactive message endpoint

Once you have your OAuth access token you can send messages. There is also the Verification Token you find under Basic Information when you create your Slack App. This is basically a secret Slack sends to your endpoint so you can verify that it is indeed valid messages.

no-alt

Sending a Slack message with approval buttons

I created a function that simply takes three arguments; the Slack Channel ID, latitude and longitude. The function is shown below and it consist of the following parts:

  • Constructing the Slack message JSON with action buttons
  • Initializing a FormData component with to send to Slack
  • The actual sending of the message using fetch

Slack has good API documentation for interactive message buttons that you can review for options on how to set up your message.

function postSlackMap(channel, lat, long) {  return new Promise(function(resolve, reject) {    console.log('Posting message to Slack');    var token = 'xxx-xxx-xxx';    var message = 'Do you want to approve this location?';    var attachments = [      {        fallback: 'Location approval',        callback_id: 'location_approve_42',        image_url:          'http://maps.googleapis.com/maps/api/staticmap?center=' +          lat +          ',' +          long +          '&zoom=12&size=1024x768&sensor=false&maptype=hybrid&markers=color:red%7Clabel:K%7C' +          lat +          ',' +          long +          '',        fields: [          {            title: 'Latitude',            value: lat,            short: true          },          {            title: 'Longitude',            value: long,            short: true          }        ],        actions: [          {            name: 'action',            text: 'Approve',            type: 'button',            value: 'approve'          },          {            name: 'action',            text: 'Reject',            style: 'danger',            type: 'button',            value: 'delete',            confirm: {              title: 'Are you sure?',              text: 'Are you sure you want to delete this submission?',              ok_text: 'Yes',              dismiss_text: 'No'            }          }        ]      }    ];    var form = new FormData();    form.append('token', token);    form.append('channel', channel);    form.append('text', message);    form.append('attachments', JSON.stringify(attachments));    fetch('https://slack.com/api/chat.postMessage', {      method: 'POST',      headers: {        Accept: 'application/json'      },      body: form    })      .then(response => response.json())      .then(responseData => {        resolve(responseData);      });  });}

The screenshot below shows how the message will be shown on Slack.

no-alt

Using ngrok for testing

Since Slack needs a public encrypted endpoint to work with you should get familiar with ngrok https://ngrok.com . It is a small utility that makes it very simple to make a local port available publicly. To forward port 3000 locally you can simply run ngrok http 3000.

$ ngrok http 3000Session Status onlineAccount SnowballVersion 2.1.18Region United States (us)Web Interface http://127.0.0.1:4040Forwarding http://4b53b898.ngrok.io -> localhost:3000Forwarding https://4b53b898.ngrok.io -> localhost:3000Connections ttl opn rt1 rt5 p50 p90 3 0 0.00 0.00 0.31 0.31HTTP Requests-------------POST /approve 200 OK

Approving with Interactive messages

Once you have been able to send your message to Slack the users has the two action buttons to approve or reject. When the user presses one of these buttons Slack will send a request to your defined endpoint. There are basically four points in this function that I named approveMessage:

  • Verify the token sent from Slack
  • Check the type of action is requested by the user
  • Perform your action
  • Return and updated message

The screenshot below shows the Slack message after the user has pressed approve, the request has been sent from Slack to your server, and an updated message has been received and displayed by Slack.

no-alt

Connecting the dots

In addition to the two functions described above I am connecting the get action /postMessage to the postSlackMessage() function. The GET endpoint /approve is connected to approveMessage(). This is done with Express.

You can see everything connected in the complete code below.

var fetch = require('node-fetch');var FormData = require('form-data');var express = require('express');var app = express();var bodyParser = require('body-parser');app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));function postSlackMap(channel, lat, long) {  return new Promise(function(resolve, reject) {    console.log('Posting message to Slack');    var token = 'xxx-xxx-xxx';    var message = 'Do you want to approve this location?';    var attachments = [      {        fallback: 'Location approval',        callback_id: 'location_approve_42',        image_url:          'http://maps.googleapis.com/maps/api/staticmap?center=' +          lat +          ',' +          long +          '&zoom=12&size=1024x768&sensor=false&maptype=hybrid&markers=color:red%7Clabel:K%7C' +          lat +          ',' +          long +          '',        fields: [          {            title: 'Latitude',            value: lat,            short: true          },          {            title: 'Longitude',            value: long,            short: true          }        ],        actions: [          {            name: 'action',            text: 'Approve',            type: 'button',            value: 'approve'          },          {            name: 'action',            text: 'Reject',            style: 'danger',            type: 'button',            value: 'delete',            confirm: {              title: 'Are you sure?',              text: 'Are you sure you want to delete this submission?',              ok_text: 'Yes',              dismiss_text: 'No'            }          }        ]      }    ];    var form = new FormData();    form.append('token', token);    form.append('channel', channel);    form.append('text', message);    form.append('attachments', JSON.stringify(attachments));    fetch('https://slack.com/api/chat.postMessage', {      method: 'POST',      headers: {        Accept: 'application/json'      },      body: form    })      .then(response => response.json())      .then(responseData => {        resolve(responseData);      });  });}function approveMessage(payload) {  return new Promise(function(resolve, reject) {    // Check verification token from the App credentials    if (payload.token == 'xyz') {      var originalMessage = payload.original_message;      // Remove the buttons      originalMessage.attachments[0].actions = [];      if (payload.actions[0].value == 'approve') {        console.log('Approving...');        // Add message about action        originalMessage.text = 'Message approved by ' + payload.user.name;      } else if (payload.actions[0].value == 'delete') {        console.log('Rejecting...');        // Add message about action        originalMessage.text = 'Message rejected by ' + payload.user.name;      }      // Return updated message      resolve(originalMessage);    } else {      reject('Verification token is not valid.');    }  });}app.get('/postMessage', function(req, res) {  postSlackMap('CIDHERE', 40.73042, -73.997507)    .then(result => {      res.status(200).end();    })    .catch(function(err) {      res.status(500).send(err);    });});app.post('/approve', function(req, res) {  approveMessage(JSON.parse(req.body.payload))    .then(message => {      res.status(200).json(message);    })    .catch(function(err) {      res.status(500).send(err);    });});app.listen(3000, function() {  console.log('Slack notification App listening on port 3000!');});