Making Work Slack More Fun: PHPBot and YakBot

I’ve been toying around with Slackbots a bit lately.  Back in January I wrote about publishing a Git log to Slack via one.  They’re dirt simple to implement and can be really useful.

They can also be fun.  I’ve added a couple bots in our office Slack just for posting randomness, running as cron jobs.  I figured I’d take a look at them here.


A couple weeks ago one of my coworkers posted a link to the documentation for a seemingly-random PHP function in our dev Slack channel.  It wasn’t random, it just pertained to an offline conversation, but we still made some cracks about how he was doing it to spread awareness of the function.

At nearly the same time, he and I both said, “That sounds like a great idea for a Slackbot.”  So I made it, and it looks like this:

<?php
$funcs = get_defined_functions();

do {
   $index = array_rand($funcs['internal']);
   $url = 'http://php.net/manual/en/function.' . str_replace('_', '-', $funcs['internal'][$index]) . '.php';
   $headers = @get_headers($url);

   if (strpos(implode(';', $headers), 'rel=shorturl') === false) {
      unset($url);
      sleep(2);
   } else {
      $attachments = [];
      $fields = [];

      $html = preg_replace("|\n[ ]*|is", '', file_get_contents($url));

      $doc = new DOMDocument();
      libxml_use_internal_errors(true);
      $doc->loadHTML($html);
      libxml_clear_errors();

      $xpath = new DOMXpath($doc);

      // "HEADER" INFORMATION
      $el = $xpath->query("//*/div[@class='refentry']")->item(0);
      $name = $xpath->query("./div[@class='refnamediv']/h1", $el)->item(0)->textContent;
      $verinfo = $xpath->query("./div[@class='refnamediv']/p[@class='verinfo']", $el)->item(0)->textContent;
      $refpurpose = $xpath->query("./div[@class='refnamediv']/p[@class='refpurpose']", $el)->item(0)->textContent;

      if (!$name) {
         sleep(2);
         continue;
      }

      // DESCRIPTION
      $el = $xpath->query("//*/div[@class='refsect1 description']")->item(0);
      $title = $xpath->query("./h3", $el)->item(0)->textContent;
      $example = $xpath->query("./div", $el)->item(0)->textContent;
      $description = $xpath->query("./p", $el)->item(0)->textContent;
      $note = $xpath->query("./blockquote", $el)->item(0)->textContent;

      $text = '';
      $text .= '```' . $example . '```' . PHP_EOL;
      $text .= $description . PHP_EOL;
      if ($note) $text .= '>>>' . $note . PHP_EOL;

      $fields[] = [
         'title' => $title,
         'value' => trim($text)
      ];

      $attachments[] = [
         'title' => $name,
         'title_link' => $url,
         'text' => $verinfo . PHP_EOL . $refpurpose,
         'fields' => $fields,
         'mrkdwn_in' => ['fields'],
      ];

      $msg = [
         'text' => '',
         'attachments' => $attachments,
         'username' => 'PHPBot',
         'icon_url' => 'https://example.com/slackbots/avatars/phpbot.png'
      ];
   }
} while (!$msg);

// send message
$c = curl_init();
curl_setopt($c, CURLOPT_URL, 'https://hooks.slack.com/services/TTTTTTTTTT/BBBBBBBBBB/ddddddddddddddddddddddd');
curl_setopt($c, CURLOPT_POST, 1);
curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($msg));
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($c, CURLOPT_HEADER, 0);
curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
curl_exec($c);
curl_close($c);

As you can see, it’s a little more advanced than just posting a link to a PHP.net URL.  If PHP.net had metadata on its pages that allowed for a nice-looking Slack unfurl, I might have just done that.  Instead I decided to pull content from the page we’re linking to and make the Slack post a little prettier with it.

So how does that all work?

<?php
$funcs = get_defined_functions();

do {

Simple thing here.  Get the defined PHP functions and then start a do…while loop.  By using get_defined_functions() I’m limiting what our PHPBot can link to but I figured there are enough functions available there that it doesn’t really matter.

$index = array_rand($funcs['internal']);
$url = 'http://php.net/manual/en/function.' . str_replace('_', '-', $funcs['internal'][$index]) . '.php';
$headers = @get_headers($url);

if (strpos(implode(';', $headers), 'rel=shorturl') === false) {
   unset($url);
   sleep(2);
} else {

We jump into our do…while loop, where we grab a random function from our array of functions and build what should be a PHP.net documentation URL from the name.  For some reason, not every function has documentation (or at least not that matches this template), which is why we’re in a do…while loop.

We get the headers for that URL and if no shorturl is defined there, we know we don’t have a valid function documentation page.  In that case, we sleep (I like to sleep in situations like this) and unset the URL, then we’ll take another run at the do…while loop.

If we do have a shorturl, we get into the heavy lifting.

$attachments = [];
$fields = [];

$html = preg_replace("|\n[ ]*|is", '', file_get_contents($url));

$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML($html);
libxml_clear_errors();

$xpath = new DOMXpath($doc);

We initialize a couple arrays that we’ll use for building our Slack message, then we pull in the HTML from the documentation page and strip out line breaks and any spaces that might start a line.  We do that stripping so that the text we use in our message later on looks better.

Then we load that HTML into a DOMDocument and fire up XPath for it so we can target the elements we want a little easier.

// "HEADER" INFORMATION
$el = $xpath->query("//*/div[@class='refentry']")->item(0);
$name = $xpath->query("./div[@class='refnamediv']/h1", $el)->item(0)->textContent;
$verinfo = $xpath->query("./div[@class='refnamediv']/p[@class='verinfo']", $el)->item(0)->textContent;
$refpurpose = $xpath->query("./div[@class='refnamediv']/p[@class='refpurpose']", $el)->item(0)->textContent;

if (!$name) {
   sleep(2);
   continue;
}

We grab the element of the page that contains all of the information about the function by targeting the refentry class.

Inside that element is a div with class refnamediv, which contains an H1 with the function name, a paragraph classed as verinfo with information about what versions of PHP support the function, and a paragraph defining the function classed as refpurpose.  We grab each of these as we’ll use them in our message.

If – for some reason – we didn’t get a function name, we sleep (like I said, I like to sleep in these situations) and then head back to the start of our do…while loop.

// DESCRIPTION
$el = $xpath->query("//*/div[@class='refsect1 description']")->item(0);
$title = $xpath->query("./h3", $el)->item(0)->textContent;
$example = $xpath->query("./div", $el)->item(0)->textContent;
$description = $xpath->query("./p", $el)->item(0)->textContent;
$note = $xpath->query("./blockquote", $el)->item(0)->textContent;

$text = '';
$text .= '```' . $example . '```' . PHP_EOL;
$text .= $description . PHP_EOL;
if ($note) $text .= '>>>' . $note . PHP_EOL;

Having advanced this far, we target the “Description” section of the function documentation page, which has a class of refsect1 description.

Inside that div is an H3 that serves as the section title, a div that contains an example use of the function, a paragraph with a description, and an optional blockquote with notes about the function.  We target each of these and pull that content in, then we use them to build the test of our message.

It should be noted that we use markup to make sure that the example is displayed as a code block while the note(s) (if present) are displayed as a quote, mimicking their appearance on PHP.net.

       $fields[] = [
         'title' => $title,
         'value' => trim($text)
      ];

      $attachments[] = [
         'title' => $name,
         'title_link' => $url,
         'text' => $verinfo . PHP_EOL . $refpurpose,
         'fields' => $fields,
         'mrkdwn_in' => ['fields'],
      ];

      $msg = [
         'text' => '',
         'attachments' => $attachments,
         'username' => 'PHPBot',
         'icon_url' => 'https://example.com/slackbots/avatars/phpbot.png'
      ];
   }
} while (!$msg);

With all of the information we need acquired, we build our message.

The description section title and our formatted text from it are added as a Slack attachment field.  That attachment gets the function name as a title (linked to the documentation URL) and it’s text is the version and purpose text we grabbed early on.

We post this all as user “PHPBot” with a hosted avatar.  These two steps aren’t necessary as you can define your incoming webhook’s name and avatar, but we’re reusing a webhook for multiple purposes and, as such, define these for each.

Then we hit the end of our do…while loop.  We’ve assembled our message so we can move on.

// send message
$c = curl_init();
curl_setopt($c, CURLOPT_URL, 'https://hooks.slack.com/services/TTTTTTTTTT/BBBBBBBBBB/ddddddddddddddddddddddd');
curl_setopt($c, CURLOPT_POST, 1);
curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($msg));
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($c, CURLOPT_HEADER, 0);
curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
curl_exec($c);
curl_close($c);

Lastly, we actually send that message.  It’s just a cURL post to the Slack webhook URL (obscured here) and ends up posting a message that looks like this:

 

PHPBot example

As I said, there’s nothing too complex here.  If anything, this is probably over-complicated.  But it gives us something to talk about at 2:05 every day.


Last week another coworker posted to our developer channel asking that we all send a random photo of a Yak to one of our non-dev coworkers.  My immediate thought was that this was begging to be made into a Slackbot, and thus the YakBot was created.

<?php
$api_key = 'AbCdEfGhIjKlMnOpQrS1TuVw2xYzAbC3DeFgHiJk';
$search_engine_id = '012345678901234567890:abc0defghijk';

$channels = [
   '#developer-channel',
   'U197UV7HV',
   'U1989NZ12',
   'U198JH8GL',
   'U99EYR89S',
   'U198L05JG',
   'U198GH6H7',
   'U74JZ1KAY',
   'UAPFKFAHG',
   'U4U7VSABA',
];

$index = rand(1, 91);

$url = 'https://www.googleapis.com/customsearch/v1?q=yak&cx=' . $search_engine_id . '&imgSize=medium&safe=high&searchType=image&key=' . $api_key . '&start=' . $index;
$json = file_get_contents($url);
$data = json_decode($json);

$image_url = $data->items[0]->link;

$msg = [
   'text' => '',
   'attachments' => [
      [
         'image_url' => $image_url
      ]
   ],
   'username' => 'YakBot',
   'icon_url' => 'https://example.com/slackbots/avatars/yakbot.png'
];

$recipients = [];
foreach ($channels AS $channel) {
   $rand = rand(0, 9);
   if (!$rand) {
      $recipients[] = $channel;
   }
}

foreach ($recipients AS $recipient) {
   $msg['channel'] = $recipient;

   // send message
   $c = curl_init();
   curl_setopt($c, CURLOPT_URL, 'https://hooks.slack.com/services/TTTTTTTTTT/BBBBBBBBBB/ddddddddddddddddddddddd');
   curl_setopt($c, CURLOPT_POST, 1);
   curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($msg));
   curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
   curl_setopt($c, CURLOPT_HEADER, 0);
   curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
   curl_exec($c);
   curl_close($c);
}

We bring in the Google search API for this one in addition to the Slack API but overall it’s a bit simpler because there’s less of a message to build.

<?php
$api_key = 'AbCdEfGhIjKlMnOpQrS1TuVw2xYzAbC3DeFgHiJk';
$search_engine_id = '012345678901234567890:abc0defghijk';

$channels = [
   '#developer-channel',
   'U197UV7HV',
   'U1989NZ12',
   'U198JH8GL',
   'U99EYR89S',
   'U198L05JG',
   'U198GH6H7',
   'U74JZ1KAY',
   'UAPFKFAHG',
   'U4U7VSABA',
];

$index = rand(1, 91);

The first thing we do is define our Google search API key and the ID of the custom search engine that we’re using.  Then we build an array of possible recipients of our yak.  The recipients include our developer channel as well as individual user IDs, to whom the message would be sent in the form of a direct message from Slackbot.

Next we select a random number between 1 and 91.  This is because the Google search API won’t let you request a start point higher than 91 for some reason.  The search results return ten items, which means the most results you can get is 100.  We only need one and one 1/91 is close enough to 1/100 so I do the randomizing up front and then do the lookup and take the first response.

$url = 'https://www.googleapis.com/customsearch/v1?q=yak&cx=' . $search_engine_id . '&imgSize=medium&safe=high&searchType=image&key=' . $api_key . '&start=' . $index;
$json = file_get_contents($url);
$data = json_decode($json);

$image_url = $data->items[0]->link;

Here we actually do that lookup.  We get JSON back and decode it to pull out the first item listed.

$msg = [
   'text' => '',
   'attachments' => [
      [
         'image_url' => $image_url
      ]
   ],
   'username' => 'YakBot',
   'icon_url' => 'https://example.com/slackbots/avatars/yakbot.png'
];

Once we have our image URL, we build our message, which is relatively simple compared to the PHPBot message because all we’re doing is posting an attachment with an image.  As I mentioned above, we also define a custom username and avatar, though we don’t need to.

$recipients = [];
foreach ($channels AS $channel) {
   $rand = rand(0, 9);
   if (!$rand) {
      $recipients[] = $channel;
   }
}

foreach ($recipients AS $recipient) {
   $msg['channel'] = $recipient;

   // send message
   $c = curl_init();
   curl_setopt($c, CURLOPT_URL, 'https://hooks.slack.com/services/TTTTTTTTTT/BBBBBBBBBB/ddddddddddddddddddddddd');
   curl_setopt($c, CURLOPT_POST, 1);
   curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($msg));
   curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
   curl_setopt($c, CURLOPT_HEADER, 0);
   curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
   curl_exec($c);
   curl_close($c);
}

Finally, we wrap it up by determining who to send our yak to.  We loop through each of our channels and generate a random number between zero and nine.  If the number is zero, we add that channel to the list of recipients.

We do that because it allows for some randomness.  It’d get noisy if we were posting yaks too often or too regularly.

Then we loop through the list of recipients and send the message, defining the channel along the way.  The lucky winners end up getting something like this:

YakBot example