Recurse Center, week 2: iCalendar Proxy

November 20, 2020 • 5 minutes

This is my first real project week at Recurse Center. For other Recursers, it was generative art and music week, but that didn't really jive with my own goals (though it was a lot of fun to be a fly on the wall for the presentations). Instead, I built something completely different from what I promised to build last week: a little proxy to allow you to modify iCalendar feeds "in flight". (No relation to the Apple app, BTW.)

My week started out by missing the morning checkins. Recurse Center has its own online calendar, which I had added into Thunderbird, but neither of them popped up a reminder that the event was about to start. Hmm, I said. Normally I would chalk that up as a minor inconvenience or find a simple workaround, but I'm at Recurse Center. If this isn't the place for unnecessarily complex workarounds, what is?

So I embarked on a quick warmup project that would sit as a middleware between the calendar feed offered by Recurse Center, and my own calendar app. It could run locally, and I would add its URL to my calendar app, at which point it would transparently forward the request on to the remote server, returning the response to the calendar. I could then modify that response to my heart's content, in this case, injecting a 10-minute reminder on each event.

This was supposed to be a JavaScript week, so I forced myself to do it in Node.js instead of Rust, which was my first impulse. Setting up a proxy was actually pretty straightforward, and by the end of Monday I was able to do a simple find-and-replace in the feed that looked something like this:

/**
 * @param {string} input
 * @return {string}
 */
function addReminders(input) {
  return input.replace(
      /^SUMMARY:(.*)$/gm,
      'SUMMARY:$1\r\n' +
      'BEGIN:VALARM\r\n' +
      'ACTION:DISPLAY\r\n' +
      'TRIGGER;VALUE=DURATION:-PT' + argv.minutes + 'M\r\n' +
      'DESCRIPTION:$1\r\n' +
      'END:VALARM',
  );
}

It doesn't actually know what's being summarized, but wherever it sees a line with a SUMMARY: field, it injects an event called VALARM, which can trigger a bunch of different actions related to an event, including displaying a pop-up reminder. Cool. Ready to go back to my original project.

But wait, why is Thunderbird a) showing me reminders for every past event, and b) showing me reminders every half-hour? Time to dive into this further. Turns out that dismissing an event involves a POST action to the calendar server, telling it that the reminder has been dismissed and not to display it on any other devices that might be listening to the feed. My server wasn't smart enough to know a GET from a POST, so it was happily acknowledging the request from Thunderbird, and responding with the same data again.

Right, the point of Recurse Center is to learn, right? There's actually some good scope for learning about Node, JavaScript, TypeScript, and of course iCalendar. (I had in fact encountered the latter a few years back, so I knew a bit of what to expect.) So I consciously abandoned my planned project for the week and decided to pivot to this one instead. It's a bit less ambitious in that I'm back to my nice comfortable backend development instead of striking out in the frontend like I'd planned, but getting more comfortable with JavaScript and its ecosystem is still a good stepping stone.

So, time to make things smarter. I abandoned vanilla Node.js and adopted Express to handle the routing and Axios to handle the proxied request, but I couldn't find a library that was well-maintained and supported both parsing and generating iCalendar data. Dealing with these things more intelligently would require me to understand the data I was getting, rather than doing a simple find-and-replace as I had been.

So I spent the next few days writing a parser for the iCalendar format. I felt a little dirty creating a Component object that could contain an array of Component objects to represent the nesting possible in the data type, something which Rust explicitly forbids (although of course it does have a workaround). I also noticed myself thinking a lot more critically about exactly how my code and especially my memory usage were being handled under the hood, and ended up with a much more efficient product as a result.

By Thursday, I had the parser more or less working and decided the time had come to migrate to TypeScript and start writing some tests. I did get things migrated to TypeScript, but learning automated testing in JS/TS is going to have to wait for another week, another project.

My lowly addReminders() function from above ended up looking more like this:

/**
 * @param {string} input
 * @return {string}
 */
function addReminders(input) {
  const parsedInput = icalendar.Component.fromString(input);

  for (const event of parsedInput.components) {
    if (event.name == 'VEVENT') {
      let summary = '';
      for (const property of event.properties) {
        if (property.name == 'SUMMARY') {
          summary = property.value;
        }
      }

      event.components.push(new icalendar.Component(
          'VALARM',
          [
            new icalendar.Property('ACTION', {}, 'DISPLAY'),
            new icalendar.Property('TRIGGER', {VALUE: 'DURATION'}, '-PT10M'),
            new icalendar.Property('DESCRIPTION', {}, summary),
          ],
          [],
      ));
    }
  }

  return parsedInput.toString();
}

Still garbage, doesn't actually do anything better than the version I had on Monday, but it's in a place where I can add features.

However, that's going to have to wait. It's Friday, and it's time to step back from that project and move onto something completely different. I have some ideas in mind, but I've learned my lesson: I'm not going to commit just yet. We'll see which way the winds of unschooling are blowing on Monday.

GitHub repository