How to set up custom Github webhook notifications on Discord (via Firebase)
Tags: github, javascript, firebase
Published: 2021-08-24 19:27:59

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qo5f8n0vokr4gedevtyt.png)
 
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](https://firebase.google.com/docs/cli)), 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](https://www.npmjs.com/package/@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:

```js
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");
      });
    });
```

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>"
```

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
```

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"
```

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgyt0yzhq3f23uz7zwem.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0f0ka351cb9lipx8ssms.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/anlfvbwq6a6tmgssgf38.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3p7mcuoplfi3x6dooq3z.png)
 
## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1x0oim8s89cokz651wco.png)

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>"
```

## 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:
```js
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});
      });
}
```

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:

```js
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
      );
    }
);
```

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 ](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dr72r9y4em1shuw45xgz.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qhugm5lozjddam8rmf6p.png)
___

And that's it! The final code looks like this:
```js
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
      );
    }
);
```