slack_webhook_json_to_rocketchat_webhook_json

Rocket.Chat Webhooks Processing Slack Formatted Requests

Rocket.Chat is an open-source communication platform offering a plethora of features, including persistent chat rooms (channels) organized by topic, private groups, and direct messaging. It is a serious open-source alternative to Slack.

Many organizations and businesses choose Rocket.Chat for the control that self-hosting provides, as well as for its privacy and security benefits. Over the past 10 years, (almost) all companies and organizations I have worked with have deployed Rocket.Chat based on my recommendations. Consequently, I feel a sense of responsibility when things do not work perfectly.

For instance, it is a common scenario to want services such as Grafana to send alerts to a dedicated #alerts channel in your Rocket.Chat instance. However, these services often support integration with Slack but not with Rocket.Chat. This compatibility issue hinders the broader adoption of Rocket.Chat and presents a challenge for me, especially when my colleagues express frustration over the lack of wide-support for Rocket.Chat

In this blog post, I outline a method that enables Rocket.Chat to process webhook requests initially designed for Slack. This approach leverages a feature in Rocket.Chat’s webhook integration that allows for the reformatting of incoming data ensuring it complies with Rocket.Chat standards before the platform processes it, using a javascript snippet that can is stored as part of the webhook entry.

I will create a webhook on Rocket.Chat that expects a POST request from Ansible Semaphore, which is an open-source, modern UI for Ansible that “lets you easily run Ansible playbooks, get notifications about fails, control access to deployment system.” Ansible Semaphore has built-in support for Slack but not for Rocket.Chat. The Rocket.Chat webhook includes javascript code to reformat the Slack compatible request content sent by Ansible Semaphore and turn it into a rocket.chat compatible request content.

Rocket.Chat Incoming Webhook Integrations

Rocket.Chat supports integrations of external services via incoming and outgoing webhooks. These Incoming integration webhooks can be configured to trigger messages within chat channels or direct messages based on the configuration of the webhooks.

Creating a new webhook is well documented in the RocketChat admin documentation: https://docs.rocket.chat/use-rocket.chat/workspace-administration/integrations.

To setup an incoming integration webhook, we navigate to: https://{{rocketchat_hostname}}/admin/integrations then click +New and the following form appears:

Then as mentioned in the documentations:

  • Turn on the Enabled toggle.
  • Name: Let’s call the webhook: “ansible-semaphore-webhook”
  • Post to Channel: Let’s select a suitable channel, such as #ansible_alerts. Yes we have to create it beforehand.
  • Post as: Let’s choose the username that this integration posts as.

After clicking on save, the platform generates a webhook URL (and a Token), the webhook Url has the format: https://{{rocketchat_hostname}}/hooks/{{hook_id}}/{{hook_token}}.

Ansible Semaphore Slack Alerts

Ansible Semaphore, has support for Slack Alerts out of the box. Sadly, at the time of writing this post, it does not provide support for Rocket.Chat out of the box, although there is an issue on GitHub suggesting that enhancement, and a pull-request, that adds that support. (Update: My pull-request was merged and included in the release v2.9.56, So Ansible Semaphore now has support for Rocket.Chat.)

To receive Ansible Semaphore alerts on Slack the following configurations needs to be defined correctly in the /etc/semaphore/config.json file:

"slack_alert": true,
"slack_url": "https://hooks.slack.com/services/T{{company}}/B{{hook_id}}/{{token}}",

Instead of the Slack webhook URL, we will put our Rocket.Chat webhook URL.

To make sure Ansible Semaphore sends alerts, alerts has to be enabled for the Project. They have to be enabled for the user, and they have to be enabled for successful tasks. Once that has been done, we can start a task and expect an alert at the end of the task run. But instead we get an error from the Rocket.Chat server with response.status_code: 400 Bad Gateway.

Crafting the Rocket.Chat Webhook

Let’s go back to edit the webhook and edit it. We will add the following script to process incoming requests, and print out the request.content.

/* exported Script */
/* globals console, _, s */

/** Global Helpers
 *
 * console - A normal console instance
 * _       - An underscore instance
 * s       - An underscore string instance
 */

class Script {
  /**
   * @param {object} request
   */
  process_incoming_request({ request }) {
    // request.url.hash
    // request.url.search
    // request.url.query
    // request.url.pathname
    // request.url.path
    // request.url_raw
    // request.url_params
    // request.headers
    // request.user._id
    // request.user.name
    // request.user.username
    // request.content_raw
    // request.content

    // console is a global helper to improve debug
    console.log(request.content);
  }
}

Next time the hook is triggered the request.content will be printed out to the logs, and we can see where the divergence to the format Rocket.Chat expects:

{
  "text": "Example message",
  "attachments": [
    {
      "title": "Rocket.Chat",
      "title_link": "https://rocket.chat",
      "text": "Rocket.Chat, the best open source chat",
      "image_url": "https://rocketchat.mydomain.com/images/integration-attachment-example.png",
      "color": "#764FA5"
    }
  ]
}

In the case of Ansible Semaphore, the webhook payload (request.content) contains the following json file:

{
  "attachments": [
    {
      "title": "Task: it-routines-infrastructure",
      "title_link": "https://semaphore.mydomain.com/project/1/templates/2?t=268",
      "text": "execution ID #268, status: SUCCESS!",
      "color": "good",
      "mrkdwn_in": [...],
      "fields": [...]
    }
  ]
}

The format of the Ansible Semaphore Slack payload differs from the format Rocket.Chat is expecting. The “mrkdwn_in”, and “fields” keys in the attachments should be removed. The “color” value in the attachments should be an HTML color such as “#00FF00”, and It would be nice to have a “text” key and an associated value, and not only attachments.

Now all we need to do is to adjust the JavaScript function process_incoming_request() to re-format the payload, and return a Rocket.Chat conform payload. Here is what I use:

/* exported Script */
/* globals console, _, s */

/** Global Helpers
 *
 * console - A normal console instance
 * _       - An underscore instance
 * s       - An underscore string instance
 */

class Script {
  /**
   * @param {object} request
   */
  process_incoming_request({ request }) {
    // request.url.hash
    // request.url.search
    // request.url.query
    // request.url.pathname
    // request.url.path
    // request.url_raw
    // request.url_params
    // request.headers
    // request.user._id
    // request.user.name
    // request.user.username
    // request.content_raw
    // request.content

    // console is a global helper to improve debug
    console.log(request.content);
    
    // Define a mapping for color replacements
    const colorMapping = {
      'good': '#00EE00',
      'danger': '#EE0000'
    };
   const content = request.content;
   
   // Initialize content.text if it doesn't exist
   if (!content.text) {
      content.text = '';
    }

    // Check if attachments exist
    if (content.attachments && Array.isArray(content.attachments)) {
      // Iterate over attachments and modify them
      content.attachments.forEach(attachment => {
        console.log(attachment);
        // Remove "fields" and "mrkdwn_in" properties
        delete attachment.fields;
        delete attachment.mrkdwn_in;
        // Check and replace attachment.color if necessary
        if (attachment.color && colorMapping[attachment.color]) {
          attachment.color = colorMapping[attachment.color];}
        // Add attachment.text to content.text
        if (attachment.text) {
          // If it's the first addition and content.text is empty, don't prepend "; "
          if (content.text === '' || index === 0) {
            content.text += attachment.text;
          } else {
            content.text += "; " + attachment.text;
          }
        }
      });
    }
    
    console.log(content);

    // Return the modified content as response
    return {
      content: content // Return the modified content
    };
  }
}

Click Save. That’s it.

Conclusion

In this blog post, we devised a method for Rocket.Chat to receive 3rd party App messages, that were originally intended for a Slack webhook. We achieve that by, defining a Rocket.Chat Incoming webhook and adding a pre-processing script that transform the payload from the Slack conform format to a Rocket.Chat conform format.

We showed how to setup the webhook for Ansible Semaphore Slack integration, however, this can be done to any other 3rd party App, that uses Slack Webhooks to send messages to Slack, by analyzing the payload and crafting the process_incoming_request() function.

References