Socialbus Webhook-Manager Documentation

The purpose of this documentation is to understand how to develop for Socialbus. It is only aimed for developers.

Access tutorials for documentation such as functional, structural or deployment documentation.

Access modules for the code's detailed documentation.

The documentation for the website can be accessed here.

Summary

- 1. Implementing an OSNS

- 2. Create the docker container

- 3. Deploying the webhook

- 4. Implementation of the OSNS in the website

1. Implementing an OSNS

This section will explain how to implement a new OSNS from the beginning.

1.1 Defining the environnement variables

A port needs to be allocated for the OSNS’ webhook that will be listening on the host of Socialbus. The port can be added in the .env file locally or in the gitlab ci enviornnement variables for production deployment. The variables required depend on the OSNS. The environnement variables below are an example of environnement variables.

OSNS_PORT=5000
OSNS_CLIENT_ID="1234_ID"
OSNS_CLIENT_SECRET="SECRET_5689_ID"
...

NOTE: Credential information should not be accessible publically and should be restrained to the developers that will deploy the application.

1.2 Creating the server file

The server file contains the webhook which will receive JSON messages from the OSNS. A server file located in the server directory of the webhook-manager repository should be created, conventionally in the format: "server[OSNS_Name].js".

Set up express

First, we need to get the express dependency and retrieve the environnement variables. Then we need to listen on that port such as the code block below:

const express = require('express');
const parser = require('body-parser');
const app = express().use(parser.json());

require('dotenv').config({path:"../.env"})

//listen on the port in the .env file, as a back up measure if the variable could not be loaded, it will take the 2nd argument in this case 5005
app.listen(process.env.EXAMPLEPORT || 5005, {});

Listen to specific routes

Conventionally, the route used to listen for messages should be located in the "/webhook" route. The "/button" route should be used for button callbacks if the OSNS requires a different route for them.

app.post('/webhook', (req,res) => onPost(req, res));
app.post('/button', (req,res) => onPostButton(req,res));
app.get('/webhook',(req,res) => onGet(req,res));

We now need to implement the callback functions called when a request is received. The get of the /webhook route is commonly used to verify tokens:


function onGet(req, res)
{
	//Verify a token received and validate the request or not depending on if it's the correct token
	if(req.query.token === expectedToken) //the expected token should be stored in the .env file
	{
		res.status(200);
		//some OSNS may wait for a specific response
	}
	else
	{
		res.status(400);
		res.send("Invalid token.");
	}
}

The default post route is the one used to receive regular messages from the OSNS. The json received in the OSNS' format is translated to the universal socialbus format here. It is then sent to the data translator.

First, we add the data translator dependency.

const messageHandler = require('../back/dataTranslator.js');

Then, we translate the JSON. We need to verify if the message isn't received from the bot used by Socialbus, otherwhise everytime a message is sent a new one will be attempted to be sent again.

function onPost(req,res)
{
    /*User messages will be received in post requests.the parameter req will contain the JSON sent by the OSNS that needs to be translated into socialbus' JSON format.
	*/

	let body = req.body;


	if(body.senderID === process.env.OSNS_BOT_ID) return;


	let response = 
	{
		connectors:{
			socialbusID: body.receiverID;
			userID: body.senderID;
		}
		text:body.message;
		origin:"Example OSNS";
	}

	messageHandler.sendMessage(response)
}

Finally, the callback of the button needs to be implemented in the "onPostButton" function. Similarly, we translate the JSON in the universal socialbus format. We also retrieve the button pressed

function onPostButton(req,res)
{

	/*Handle a button press from user
	  In this case, we do not need to handle the bot sending the message since the bot can't press on a button.
	*/
	let response = {
		connectors:{
			socialbusID: body.receiverID;
			userID: body.senderID;
		}
		buttonID:req.body.action_id;
		origin:"Example OSNS";
	}

	messageHandler.handleButtonDatabase(response);
}

dataTranslator.js will process the formatted JSON, affect the user's state if needed and send it to the targetted RabbitMQ queue. If the queue doesn’t exist, it will automatically be created.

The server file is now fully implemented. Here is the full example code:

const express = require('express');
const parser = require('body-parser');
const app = express().use(parser.json());
const messageHandler = require('../back/dataTranslator.js');

require('dotenv').config({path:"../.env"})

//listen on the port in the .env file, as a back up measure if the variable could not be loaded, it will take the 2nd argument in this case 5005
app.listen(process.env.EXAMPLEPORT || 5005, {});
app.post('/webhook', (req,res) => onPost(req, res));
app.post('/button', (req,res) => onPostButton(req,res));
app.get('/webhook',(req,res) => onGet(req,res));

function onGet(req, res)
{

	//Verify a token received and validate the request or not depending on if it's the correct token
	if(req.query.token === expectedToken) //the expected token should be stored in the .env file
	{
		res.status(200);
		//some OSNS may wait for a specific response
	}
	else
	{
		res.status(400);
		res.send("Invalid token.");
	}
}

function onPost(req,res)
{
    /*User messages will be received in post requests.the parameter req will contain the JSON sent by the OSNS that needs to be translated into socialbus' JSON format.
	*/

	let body = req.body;


	if(body.senderID === process.env.OSNS_BOT_ID) return;


	let response = 
	{
		connectors:{
			socialbusID: body.receiverID;
			userID: body.senderID;
		}
		text:body.message;
		origin:"Example OSNS";
	}

	messageHandler.sendMessage(response)
}

function onPostButton(req,res)
{

	/*Handle a button press from user
	  In this case, we do not need to handle the bot sending the message since the bot can't press on a button.
	*/
	let response = {
		connectors:{
			socialbusID: body.receiverID;
			userID: body.senderID;
		}
		buttonID:req.body.action_id;
		origin:"Example OSNS";
	}

	messageHandler.handleButtonDatabase(response);
}

1.3 Implementation of sending a request to the OSNS

Next, we need to implement the ability to send requests to the OSNS in order for the Socialbus bot to be able to send messages to the users.

Conventionally, the method should be called postTo[name of OSNS]. The code below shows an example implementation.

async function postToExampleOSNS(messageJson)
{
    let data;

    data = {
        form: {
            "token":process.env.EXAMPLE_OSNS_TOKEN,
            "conversation": messageJson.connectors.channel
        }
    };
    
    //If there is a button template, add it to the content to send
    messageJson.buttonTemplate !== undefined?
        data.form.blocks = JSON.stringify(await template.buildButtonTemplate(messageJson.buttonTemplate, messageJson.origin, messageJson.connectors.user)):
        data.form.text = messageJson.text;

    //post request to the OSNS
    request.post('https://exampleOSNS.com/api/chat.postMessage', data, function (err, res, body) {
        if (!err && res.statusCode === 200) {
            console.log("Succesffully sent message!");
        } else {
            console.error("Could not send message: " + err);
        }
    });
}

Finally, add the method call in the already existing "postToPlatform" method add a condition to call the method created.

    if(targetQueue === "Slack")
    {
        postToSlack(messageJson);
    }
    else if(targetQueue === "Example OSNS")
    {
        postToExampleOSNS(messageJson);
    }

1.4 implementing button templates

To display buttons in the final message, a JSON needs to be prepared for every action possible for Socialbus.

In the "buildTemplate.js" file, only one method is exported. It verifies what type of button template is asked and then returns the JSON for the specific OSNS. Add an if in each button template type with the JSON format of the suggested button template.

function getButtonTemplateDatabase(media)
{
    if(media === "messenger")
    {
        ...
    }
    else if(media === "Example OSNS")
    {
        json = {
            "type": "template",
            "payload": {
                "template_type": "generic",
                "elements": [{
                    "title": title,
                    "subtitle": "Tap a button to answer.",
                    "buttons": [...]
                }]
            }
        }

    }
}

2. Create the docker container

For the webhook to be used and ran, it has to pass through a container so it's an independent process from the rest of the application.

2.1 Create a dockerfile

In the docker directory, create a dockerfile with the name: "Dockerfile.OSNS_Name".

FROM node:17.0.0

COPY package.json package.json #get the dependencies

#Get the source core files of webhook-manager
COPY src/buildTemplate.js src/buildTemplate.js
COPY src/dataTranslator.js src/dataTranslator.js
COPY src/mongoose.js src/mongoose.js
COPY src/postToService.js src/postToService.js
COPY src/rabbitMQ/send.js src/rabbitMQ/send.js
COPY scripts/wait-for-it.sh wait-for-it.sh

#Copy the implemented server file
COPY src/servers/serverFacebook.js src/servers/serverFacebook.js

#Expose the port used for the webhook
ARG OSNS_Port
EXPOSE ${OSNS_Port}

#Install the dependencies
RUN source /root/.bashrc && npm install

2.2 Create a container in Docker Compose

In the base of the project, modify the docker-compose.yml to add a container for the new OSNS.

First, we need to create the container and retrieve the dockerfile we just created.

 example_OSNS:
    container_name: "Example OSNS"
    build:
      context: ./webhook-manager
      args:
        messengerPort: "${OSNS_PORT:-5000}"
      dockerfile: ./docker/Dockerfile.Example_OSNS

The context key determines in which context directory the dockerfile is ran.

The args key lets defines arguments. This is where we can set the port the webhook will run to.

Now we need to add the exposed port, the dependencies and hostname.

 example_OSNS:
    container_name: "Example OSNS"
    build:
      context: ./webhook-manager
      args:
        messengerPort: "${OSNS_PORT:-5000}"
      dockerfile: ./docker/Dockerfile.Example_OSNS
    env_file:
      - .env
    depends_on:
      - rabbitmq
    hostname: 'example_OSNS'
    ports:
      - "${OSNS_PORT:-5000}:${OSNS_PORT:-5000}"

The depends_on key makes sure we wait for the rabbitMQ container to be ready since the container will need it to run.

The hostname key defines how the other containers can access this one. This is important for the deployment of the container

The ports key lets us defined ports linked to the host machine.

Finally, we add the command to launch the application along with the docker network

 example_OSNS:
    container_name: "Example OSNS"
    build:
      context: ./webhook-manager
      args:
        messengerPort: "${OSNS_PORT:-5000}"
      dockerfile: ./docker/Dockerfile.Example_OSNS
    env_file:
      - .env
    depends_on:
      - rabbitmq
    hostname: 'example_OSNS'
    ports:
      - "${OSNS_PORT:-5000}:${OSNS_PORT:-5000}"
    command: ["./wait-for-it.sh", "rabbitmq:5672", "-t", "120", "--", "node", "src/servers/serverFacebook.js"]
    networks:
      - socialbus_shared

The "wait-for-it" bash file lets us make sure that rabbitMQ container is up and ready to be used. Once it is, we launch the serverfile for the OSNS in the context of the container.

The "networks" key lets us define a docker network. This lets docker containers communicate between each other.

The container will now be launched upon using docker-compose.

3. Deploying the webhook

Finally, we need to deploy the webhook so that it can be accessed by the OSNS. We add the route to the NGINX container. The NGINX file is stored in webhook-manager/docker/nginx/nginx.conf.

Add a location such as the example below:

location /exampleOSNS {
    resolver 127.0.0.11 valid=30s;
    set $exampleOSNS_up exampleOSNS:5002;
    proxy_pass          http://$exampleOSNS_up/webhook?$args;
    proxy_set_header    X-Forwarded-For $remote_addr;
}

When NGINX is running, the webhook can now be accessed at the /exampleOSNS endpoint.

4. Implementation of the OSNS in the website

The OSNS can now be used with a bot. However, it also needs to be implemented in the website for users to be able to connect to the bot. Please refer to the website's documentation for an example.