2015 twenty four merry days of Perl Feed

Escaping the Basement For Christmas

Mojo::UserAgent - 2015-12-23

The Christmas Party on the fourth floor of Reynholm Industries was in full swing. It was the party legends would be made of, something that would be talked about around the water cooler for years to come. Anyone who wasn't there was sorely missing out. And right now Roy was certainly missing out.

At his desk in the IT department in the basement, Roy stared at his computer screen in dismay at the endless support requests that kept coming in on Slack. Moss was out at some sort of Countdown related Christmas party, and no-one had seen Richmond since he left for that job as a zoo keeper, leaving his boss Jen the choice between going to the office party or making Roy stay behind and answer support queries...she'd obviously chosen the latter option. And now Roy was forced to endure things like this all evening long:

If only there was some way he could automate all of this so he could go upstairs and join the frivolity?

Slack's API

Slack has a fully featured customer accessible API that is able to do pretty much anything Roy could do in the web client. In addition to a collection of normal HTTP GET / PUT / POST API calls Slack supports a websocket API to allow the program to retrieve and answer messages in real time just like the web client does.

The normal way to use the rtm real time message service is:

  1. Make a standard HTTP GET call to the rtm.start API method to get a unique websocket URL
  2. Connect to that bi-directional websocket URL
  3. Read JSON from / send JSON to that websocket to receive and send messages respectivly

Handling asynchronous programming where your code is both listening and sending messages at the same time used to be complex, but now Perl has a bunch of really good event loop modules that abstract all this away.

One such framework, Mojolicious, has excellent websocket handling right out of the box, supporting all the idiosyncrasies of connecting to the socket, handling the necessary HTTP socket upgrades specified in the protocols, and allows Roy to simply register callbacks that are triggered whenever the server has sent us a complete chunk of JSON down the socket.

Given all of this, it shouldn't be too hard for Roy to use it to write something with it to pretend to be him in Perl:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 

 

#!/usr/bin/perl

use 5.022;
use warnings;
use feature qw(signatures);
no warnings qw(experimental::signatures);

use Mojo::UserAgent;

# The per-user token is availble to logged-in users from the slack website
# to authenticate API users as them. See https://api.slack.com/web
my $TOKEN = 'xoxp-3234159231-8529214522-1345812313-143531';

my $ua = Mojo::UserAgent->new;

# Use the standard synchronous HTTP API get a websocket URL to connect to
sub rtm_ws_url {
  my $tx = $ua->get('https://slack.com/api/rtm.start' => form => { token => $TOKEN });
  my $res = $tx->success or die "Can't connect!";
  return $res->json->{url};
}

# connect to the websocket
sub rtm_start($url) {
  my $id = 1;

# connect to the websocket URL asynchronously and then call callback
my $tx = $ua->websocket($url => sub ($ua, $tx) {
    say 'WebSocket handshake failed!' and return unless $tx->is_websocket;

# handle JSON messages sent to us from the server
$tx->on(json => sub ($tx,$msg) {
      ...
    });

# keep pinging every five seconds to not get disconnected
Mojo::IOLoop->recurring(5 => sub {
      $tx->send({ json => { id => $id++, type => "ping" } });
    });
  });
}

my $url = rtm_ws_url();
rtm_start( $url );
Mojo::IOLoop->start unless Mojo::IOLoop->is_running;

 

With the basics of connecting to the server (and staying connected by sending a ping every five seconds no matter what) done Roy needed to work out how to handle messages sent from the server. Slack sends all kinds of messages: people leaving or joining channels, ping responses, even notifications that people are starting to type. But in reality there was only one message Roy was interested in...someone starting to chat to him in a private one-on-one chat, presumably seeking support. He decided to script his standard one-line response to the first time anyone ever asked him anything that seemed to solve almost every problem that came his way:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 

 

# handle JSON messages sent to us from the server
$tx->on(json => sub ($tx,$msg) {
# ignore anything but actual messages
return unless $msg->{type} eq "message";

# ignore anything but direct messages
my $channel = $msg->{channel};
  return unless $channel && $channel =~ /\AD/;

# don't speak twice
state %already_replied;
  return if $already_replied{ $channel };
  $already_replied{ $channel } = 1;

  $tx->send({ json => {
    id => $id++,
    type => 'message',
    channel => $channel,
    text => 'Have you tried turning it off and on again?'
  }});
});

 

Roy didn't have to wait long before someone triggered his script:

It worked! Roy ran gleefully upstairs to see if there was any eggnog left.

Hey You $#%! Fix My Computer!

Jen didn't seem to appreciate his clever solution when he bumped into her next to the punch bowl. She thought it would make people very angry. Normally Roy didn't listen to Jen too much, but he had to admit that she might be right about that. Maybe he should have the script let him know if someone was truly upset?

Roy figured if they were swearing at him, he'd better pay attention!


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 

 

# handle JSON messages sent to us from the server
$tx->on(json => sub ($tx,$msg) {
  ...

  use Regexp::Common qw(RE_profanity);
  if ($msg->{text} =~ RE_profanity) {
    $tx->send({ json => {
      id => $id++,
      type => 'message',
      channel => $channel,
      text => "Hold on, I'm looking into it",
    }});
    notify('CODE RED ROY! YOU NEED TO DEAL WITH THIS!');
    return;
  }

  ...
});

 

Now when the boss man complained, he should be tempoarly soothed while his script could summon him.

All that was needed was some way to notify himself. Of course! The Slack app on his mobile phone could be configured to buzz whenever a particular user - like slackbot, the Slack bot - messaged him. Now all he had to do was lookup the secret slackbot URL for his account on the Slack web pages and make asynchronous posts to that whenever he wanted to notify himself.


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 

 

my $SLACKBOT_URL =
  'https://reynholm-industries.slack.com/services/hooks/slackbot'
   . '?token=hWa13Ndj32Ajoz2jnaoqu42X'
   . '&channel=%23roytrenneman';

sub notify($message) {
# send non-blocking post
my $tx = $ua->build_tx(POST => $SLACKBOT_URL => $message);
  $ua->start($tx => sub {});
}

 

If he could just avoid drinking enough to lose his phone all would be great!

SEE ALSO

Gravatar Image This article contributed by: Mark Fowler <mark@twoshortplanks.com>