Site icon David Lozzi

Adding your Advent of Code [AoC] Leaderboard to Slack

Advertisements

I am very pleased to share this guest post written by my colleague and friend Connor Tyrrell. He is a fantastic developer, consultant, principal, in the Application Development practice out of Slalom Boston. He is the founder of BrewGene, creator of Slack Codenames, drinker of craft beer, skier of mountains, runner of marathons, #CatDad. Follow him on Twitter or LinkedIn.

See more about my advent of code here.


On Day 4 of Advent of Code more techies in our office started getting involved, and the desire to quickly-see who’s participating started to come up. I decided to build a simple Slack /Slash-Command to show us our private leaderboard, and used AWS Lambda so that it would all run serverless (low overhead / cost).

Follow along below to hear how it was built and you can find all of the code on github. There are no spoilers to any of the puzzles in this post.

Step 1: Fetching the Leaderboard Data

Advent of Code (AoC) has a JSON endpoint available for each private leaderboard, all you need to pass is your cookie which will stay good through the month of December. Using the Node package request-promise, fetching data is easy:

  const cookie = process.env.AOC_COOKIE;
  const url = process.env.AOC_URL;
  var options = {
    method: 'GET',
    uri: url,
    headers: {
      'Content-Type': 'application/json;charset=utf-8',
      'cookie': cookie
    },
  };
 
  let response;
  await rp(options)
    .then(function (parsedBody) {
      response = JSON.parse(parsedBody);
      // …Do something with that parsed body

Step 2: Caching the Response

Getting the data was so easy! …Except that AoC asks that you not hit the URL more than every 15 minutes, so we need to store the data, and check that cache before putting together the response for Slack. After some code refactoring the parent function looks like this:

const getLeaderboardJSON = async () => {
const timestamp = new Date().getTime();
const currentData = await dynamoScanAllRows(process.env.LEADERBOARD_TABLE, 'leaderboardId, leaderboardObject, createdTime, updatedTime', `activeFlag = :active`, { ':active': true }, 'leaderboardId');
if (currentData[0] !== undefined) { // Need to handle the first time it runs, gets no results, and so there's no timestamp to compare to
    const lastUpdatedTime = currentData[0].updatedTime;
    if (timestamp - lastUpdatedTime > 15 * 60 * 1000) { // 15 minutes (in milliseconds)
      return await getLatestLeaderboard(currentData[0].leaderboardId);
    } else {
      return JSON.parse(currentData[0].leaderboardObject);
    }
  } else {
    return await getLatestLeaderboard(‘’);
  }
};

What this code does:

I’m not going to detail the process of creating a record, deactivating a record, or querying a record from a DynamoDB table because that’s not unique to this project. But if you want to see the code you can find it here.

Step 3: Preparing the response for Slack

Now that we have all of the Leaderboard data, we need to get it into a format that Slack knows how to read. Slack has a well-documented API, but one thing most people don’t know about is Block Kit Builder. Using the Block Kit Builder you can easily format messages – it’s not full-HTML, but you have a lot more rich controls than when you’re just typing in Slack.

The data from AoC isn’t in an ideal format with userIds as Keys in an object instead of being a simple array of users. A quick Object.keys can convert it to a more friendly format.

    const leaderboardData = await getLeaderboardJSON();
    let leaderboard = [];
    const keys = Object.keys(leaderboardData.members);
    for (let i = 0; i < keys.length; i++) {
        const record = leaderboardData.members[keys[i]];
        leaderboard.push(record);
    }

Now that we have one object per user, we want to sort it so the highest-scorers are on top.

leaderboard.sort((a, b) => (a.local_score < b.local_score) ? 1 : -1);

Then you just loop through them and format your message for each user however you want. I did a simple “Connor Tyrrell has [35] points and [8] stars”. Once you have that message setup now you want to fit it into the Slack Block Kit format.

return callback(null, {
        statusCode: 200,
        body: JSON.stringify({
            response_type: 'in_channel',
            blocks: [
                {
                    type: 'section',
                    text: {
                        type: 'mrkdwn',
                        text: message
                    }
                },
                {
                    type: 'divider'
                },
                {
                    type: 'section',
                    text: {
                        type: 'mrkdwn',
                        text: `Visit <https://adventofcode.com/|Advent of Code> and join the Leaderboard ${process.env.AOC_LEADERBOARD}`
                    }
                }
            ]
        })
    });

That’s it! Assuming you have your Serverless YAML file and environment variables setup correctly, do a simple serverless deploy and your new lambda function will be published! Since it’s a simple GET command you can then put the URL in Postman or even just Chrome and make sure you see the JSON formatted response back.

Step 4: Connecting it to Slack

While Slack has an entire Directory of full applications that you can install (for instance, my Codenames Game), there’s also a lesser-known Integration you can use to setup a simple Slash Command – the functionality is limited, but for a simple “I type a slash command and get back a result” it works great.

To get started go to Slash Commands in the App Directory (if you have multiple workspaces, make sure you choose the correct one in the top-right), hit Add to Slack, then follow the prompts! The key things to fill out:

That’s it! Now when you run your slash command in Slack you should see something like this:

Step 5: Finishing Touches

It works, it’s functional, but surely it could be a little prettier! We’re using Slack, the king of Emojis, let’s take advantage! For instance, what if instead of just showing names or names and “1st, 2nd, 3rd” we used emojis – that’s easy enough.

let rankText = `${rank})  `;
if (rank === 1) {
  rankText = ':first_place_medal:';
} else if (rank === 2) {
  rankText = ':second_place_medal:';
} else if (rank === 3) {
  rankText = ':third_place_medal:';
}

message = message + `\n${rankText}${name} with ${score} points and ${stars} stars`;

That’s a nice easy thing to make the leaderboard a little more fun. I stopped there for now, but I’m sure there will be more ideas and requests over the next 3 weeks to keep making it more fun!

What do you think?

Was this interesting? Informative? Did you learn anything new? Anything that feels left out? I’d love your feedback, please comment below!


Thank you Connor! I look forward to learning the inner working of this and getting my own slash commands up and running!

Series Navigation<< Advent of Code, Day 5Advent of Code, Day 4 >>
Exit mobile version