Sending a Slack invite with a Perl CGI script

Sending a Slack invite with a Perl CGI script

We wanted to invite people to a Slack channel, but Slack provides no automated means of doing so. We found implementations of Slack sign-up forms, but they were in languages we didn’t prefer and would not integrate with our existing marketing tools.

Approaching it from the opposite end, we could have them fill out a form that would be processed by our marketing tools. That gets us part of the way there, providing link tracking and even allowing users to sign up to an email list using the same form.

But the tool didn’t have any mechanism to send a Slack invitation. It could call out to a URL with a fixed format that includes the Slack sign-up data as parameters. But we still needed some type of shim that proxies the request, reformatting the data as Slack requires.

The answer was slack_invite.cgi, a simple CGI script that accepts query parameters and calls the Slack API to generate an invitation, using the undocumented users.admin.invite API call, which developers of one of the other Slack sign-up scripts reverse engineered from the Slack web UI. And in fact, our code relied on reverse engineering some of those scripts.

In the process of developing this Slack-sign-up shim, I noticed a number of tips and some indications that CGI is not dead yet.

  • CGI is appropriate for small apps that don’t require a full framework. If you only have a small number of rarely invoked URL paths, then loading a whole framework would be overkill. You don’t need most of the features a framework provides, and you don’t need to keep the app preloaded in memory. That was the case here, as it’s a simple proxy script—bridging between the marketing API and Slack API. We also didn’t expect a rush of users to be invoking it, but if we did, we could’ve installed it as a FastCGI script, because we used Plack (which I go into below).

  • Don’t use CGI. (CGI is so passé.) Rather, use Plack::Handler::CGI, and implement your app in its own class. That way you can easily test your app with Plack::Test or Test::WWW::Mechanize::PSGI, and you can easily migrate to FastCGI, mod_perl, or any other PSGI-capable interface in the future. You could even someday install the app as a path in a larger Plack app, using Plack::Builder. For this project, all the functionality fits into a single module, SlackInvite::App, which slack_invite.cgi simply calls.

  • It’s okay to skimp on infrastructure. That’s also why we skimped on a full framework. Prefer a simple solution, knowing that you can always extend it later. For this project, I hard-coded my configuration in a SlackInvite::Config module. However, the module returns a hashref from a config method, so it would be fairly easy to migrate to using a YAML or JSON configuration file, or even to using a configuration service.

  • Don’t skimp on process! Just because it’s a small app, that isn’t an excuse to be haphazard and sloppy. Do create a Git repository. Do write tests. Do show your code to peers for review. We actually prepared this project for publishing on GitHub, so I included full POD. Then I passed it by one of my colleagues for a code review and refactored the code based on his comments.

  • Do think through your architecture. Maybe you don’t need separate controller, view, business-logic, model, and storage classes, but do at least put separate concerns in separate methods. I ended up with different concerns handled by highly cohesive subroutines: _send_slack_invite() to interface to the back-end Slack API, _success() and _error() to generate user responses, _log() to log requests, and so forth.

  • You may not need unit tests, but do write tests. I actually prefer to start with broad feature tests, as they help me think through and demonstrate the overarching interface to the app. Then I drill down to unit tests of specific modules and methods. However, a tiny app that fits completely into a single class may well be served by feature testing alone. That’s how things shook out for this project. I injected a fake Slack API service using Test::LWP::UserAgent, injected a custom configuration for testing, and tested the app using Plack::Test. That was enough to test the entire app. (I’ve written more about our approach to testing in Testing Strategies for Modern Perl.)

  • And of course, use CPAN! I myself tend to associate CPAN with using a framework, and then forget to use it when I’m not using a framework. But whatever you need that you would normally associate with a framework, there’s probably a module for that. In this project, I used Path::Tiny for file IO: it provides a powerful, UTF-8 aware API and automatically locks files for concurrency support. I used Template::Mustache for lightweight template formatting. I used LWP::UserAgent to interact with the downstream Slack API. And I used, JSON to log structured data to a text file.

May all your text turn from red to green…

P.S. When it comes to using UTF-8 with Plack… Here’s a bonus story that didn’t really fit in above, but I spent so much time tracking it down, that it feels a waste not to share it. I wanted to make sure my script handled query parameters with non-ASCII characters. What if someone was named “José”? I wanted my script to faithfully pass it along uncorrupted. So I included non-alphabetic characters in my tests.

And the tests failed.

As it turns out, the code under test was working fine. But I was using HTTP::Request::Common::POST to encode the form data in my test, and that calls URI::query_form(), which doesn’t properly handle UTF-8.

The solution was to use URI::Escape::uri_escape() to encode the form values and then have the test do the work of assembling the query form. See commit d02d2d9 for the full diff.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.