DEV Community

Cover image for How to set up custom Github webhook notifications on Discord (via Firebase)
Yuan Gao
Yuan Gao

Posted on

How to set up custom Github webhook notifications on Discord (via Firebase)

When working in a team, or a public project, sometimes you want to see github activity notifications in your discord chats (because who pays attention to github notification emails? got too many emails as it is). Discord actually provides this functionality out of the box as part of webhooks, you can set up a webhook with github and immediately start receiving notifications, which look like this:

Screenshot of github webhook notification on discord

However, you don't have much control over how this notification is presented, and you only get coarse-grain control over what notifications are sent, or people to ping. So I threw up a small firebase function (written in Javascript) to serve as an intermediary between GitHub and Discord to give me this control. Here's how I did it:

Step 1: Set up new/existing Firebase project

I won't go into it here, but you can pretty much drive the whole thing from the firebase CLI (docs), and goes something like this:

  1. With node.js and NPM installed, npm install -g firebase-tools to install the firebase CLI tools
  2. Then firebase login to log in (following the prompts)
  3. Then firebase init functions in a new folder to set up a new project with Firebase functions. You can either select an existing firebase project from the menu (if you created a new firebase project online or previously), or the CLI will let you create a new firebase project. I selected Javascript, and some other default settings.

Step 2: Install some stuff

Github provides a convenient library for webhooks called @octokit/webhooks to make processing them easily, which can be installed using npm install @octokit/webhooks from inside the new functions folder that firebase creates.

It's also possible to install discord.js library which helps you format the outgoing webhooks to Discord API, but I've decided not to use this as the use-case is relatively simple, and I didn't want to install quite a large library unnecessarily. Had the use-case been more complicated (if we had more than one type/format of notification to send), I would have gone with it.

Step 3: Add a github webhook receiving endpoint

Inside the index.js file I added the following boilerplate for handling github webhooks using the octokit webhooks library:

const functions = require("firebase-functions");
const octokit = require("@octokit/webhooks");

const webhooks = new octokit.Webhooks({
  secret: functions.config().github_webhook.secret,
});

const runtimeOpts = {
  memory: "128MB",
};

exports.githubNotifier = functions.runWith(runtimeOpts)
    .https.onRequest((request, response) => {
      const event = request.headers["x-github-event"];
      const hook = {
        id: request.headers["x-github-delivery"],
        name: event,
        payload: request.rawBody.toString(),
        signature: request.headers["x-hub-signature-256"],
      };

      return webhooks.verifyAndReceive(hook).then(() => {
        response.status(200).send("Webhook handled");
      }).catch((err) => {
        functions.logger.error(err);
        response.status(500).send("Webhook not processed");
      });
    });
Enter fullscreen mode Exit fullscreen mode

This code by itself will validate webhooks from github, checking that the secret value is what is provided. At the moment there's no actual code to handle the webhooks, but it will at least receive and validate them. The secret is important, as it will prevent other people who don't know your secret from arbitrarily pinging your webhook and sending random unauthorized notifications to your discord.

The secret value in the code above uses functions.config().github_webhook.secret which reads a secret value from firebase config. You can set the value using cli by running the command (come up with some suitable secret/password here):

firebase functions:config:set github_webhook.secret="<insert your secret here>"
Enter fullscreen mode Exit fullscreen mode

This avoids having to paste secret values into your code, perfect for when your code might be open source or a community project - other contributors or the public will be able to see your code but won't be able to see your secrets.

At this point, you can deploy this function to check everythings good. This can be done from the cli by using the command:

firebase deploy
Enter fullscreen mode Exit fullscreen mode

OR if you're in a project with other firebase stuff in it, you can deploy just this function, rather than everything else.

firebase deploy --only "functions:githubNotifier"
Enter fullscreen mode Exit fullscreen mode

Step 4: Set up webhooks on Github

Once the function is deployed, you can find its HTTP url on the Firebase web console:

screenshot of firebase web console

This is the URL that you want Github to send webhooks to. Copy this, then go over to your github hooks in your repository (Settings > Webhooks, then Add webhook)

screenshot of github webhook settings

Set the Payload URL to the address from firebase. Set the content type to application/json. Set the Secret to the secret set in the previous step.

Optionally, select Let me select individual events and then select just the events you want to notify on. You don't have to do this as you can filter out the events in the functions in a sec. But it may be nicer to restrict it here to save on how many times your function will have to trigger. I have it set to notify on: "Issues", "Issue comments", "Pull requests", and "Releases".

Step 5: Send some test webhooks

Once you've set it up, you can try create a PR or an issue, or anything that triggers the webhook, and go over to the "Recent Deliveries" tab of the webhook, where you can inspect the delivery status and payload of the webhook

screenshot of recent deliveries tab in github

Over on Firebase functions, you can inspect the logs for the function that was deployed to check that it correctly handled the incoming webhook. You should see the standard set of logs that firebase produces whenever it handles an event

Screenshot of Firebase function logs

Step 6: Set up incoming webhooks on Discord

Now that it's confirmed that github webhooks can trigger firebase functions correctly, it's time to hook up actual functionality and send hooks to Discord. Discord API allows incoming webhooks to send messages into a specific channel on Discord. You can set these up in the Server settings > Integrations > Webhooks. Create one, and copy the webhook URL:

Screenshot of Discord webhooks

I'm going to store this URL as a Firebase config as well, to hide it from prying eyes.

firebase functions:config:set discord_hook.url="<webhook url>"
Enter fullscreen mode Exit fullscreen mode

Step 7: Draw the rest of the owl

Next, since this is an HTTP requets that needs to be made, I install axios to make it, with npm install axios.

I can add a simple function for sending webhooks to discord:

const axios = require("axios");

function sendWebhook(author, icon, description, title, body, url) {
  const request = {
    "username": "Larbot Gitrold",
    "avatar_url": "<snip>",
    "embeds": [{
      "color": 0xFFC89C,
      "author": {
        "name": author,
        "icon_url": icon,
      },
      "title": description,
      "fields": [{
        "name": title,
        "value": body,
      }],
      "url": url,
    }],
  };

  functions.logger.info("Sending discord webhook", {request});
  return axios.post(functions.config().discord_hook.url, request)
      .then((res) => {
        functions.logger.info("Discord webhook send successful", {res});
      }).catch((err) => {
        functions.logger.error("Discord webhook failed", {err});
      });
}
Enter fullscreen mode Exit fullscreen mode

This function, when invoked, will send an HTTP request to Discord, causing a message to show up in the configured webhooks channel!

Next, I just need to trigger this function from a github webhook.
Octokit/webhooks uses an event system to handle the webhooks, which is actually very convenient, so I'm able to add a handler for specific:

webhooks.on(
    ["pull_request.opened", "pull_request.reopened", "pull_request.closed"],
    ({id, name, payload}) => {
      functions.logger.info("Handling Pull Request", {id, name, payload});
      sendWebhook(
          payload.pull_request.user.login,
          payload.pull_request.user.avatar_url,
          `Pull Request #${payload.pull_request.number} ${payload.action}`,
          payload.pull_request.title,
          payload.pull_request.body,
          payload.pull_request.html_url
      );
    }
);
Enter fullscreen mode Exit fullscreen mode

Here, I'm telling octokit/webhooks that I want to trigger this whenever pull requests are opened, reopened, or closed. I pull out all the relevant user info, avatar URLs, and pull requests details from the incoming github payload, and then trigger the outgoing discord webhook function.

The result looks something like this. I since added pings to a discord role so that certain people will be notified, and a few other goodies.

Screenshot

It's easy to add additional handlers for the different events, by registering more events/handlers with the webhook. here's a comparison between Discord's built-in github webhook handler for closing an issue, which doesn't show the contents of an issue, and this custom one that does:

image


And that's it! The final code looks like this:

const axios = require("axios");
const functions = require("firebase-functions");
const octokit = require("@octokit/webhooks");

const webhooks = new octokit.Webhooks({
  secret: functions.config().github_webhook.secret,
});

const runtimeOpts = {
  memory: "128MB",
};

exports.githubNotifier = functions.runWith(runtimeOpts)
    .https.onRequest((request, response) => {
      const event = request.headers["x-github-event"];
      const hook = {
        id: request.headers["x-github-delivery"],
        name: event,
        payload: request.rawBody.toString(),
        signature: request.headers["x-hub-signature-256"],
      };

      return webhooks.verifyAndReceive(hook).then(() => {
        response.status(200).send("Webhook handled");
      }).catch((err) => {
        functions.logger.error(err);
        response.status(500).send("Webhook not processed");
      });
    });

function sendWebhook(author, icon, description, title, body, url) {
  const request = {
    "username": "Larbot Gitrold",
    "avatar_url": "<snip>",
    "embeds": [{
      "color": 0xFFC89C,
      "author": {
        "name": author,
        "icon_url": icon,
      },
      "title": description,
      "fields": [{
        "name": title,
        "value": body,
      }],
      "url": url,
    }],
  };

  functions.logger.info("Sending discord webhook", {request});
  return axios.post(functions.config().discord_hook.url, request)
      .then((res) => {
        functions.logger.info("Discord webhook send successful", {res});
      }).catch((err) => {
        functions.logger.error("Discord webhook failed", {err});
      });
}

webhooks.on(
    ["pull_request.opened", "pull_request.reopened", "pull_request.closed"],
    ({id, name, payload}) => {
      functions.logger.info("Handling Pull Request", {id, name, payload});
      sendWebhook(
          payload.pull_request.user.login,
          payload.pull_request.user.avatar_url,
          `Pull Request #${payload.pull_request.number} ${payload.action}`,
          payload.pull_request.title,
          payload.pull_request.body,
          payload.pull_request.html_url
      );
    }
);
Enter fullscreen mode Exit fullscreen mode

Discussion (0)