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.
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.
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 3000
Session Status onlineAccount SnowballVersion 2.1.18
Region United States (us)
Web Interface http://127.0.0.1:4040Forwarding http://4b53b898.ngrok.io -> localhost:3000
// Forwarding https://4b53b898.ngrok.io -> localhost:3000
// Connections ttl opn rt1 rt5 p50 p90 3 0 0.00 0.00 0.31 0.31
// HTTP 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.
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!');
})
}