Trello as an Interface via Webhooks

So I’m a bit particular about how I keep my finances in order.  To the point that I wrote my own web-based ledger software to help myself keep it all straight.  Yeah, there are third-party solutions out there, but I wanted something that worked exactly the way I wanted.

Every couple days I pull receipts out of my pocket and go through my email inbox and drop new entries in my ledger.  Every couple weeks I manually reconcile the ledger with my banking statements.  Manual processes – ew – but it’s important enough to me that I do it.

The issue I ran into was transactions that I didn’t have a receipt for.  Tim Horton’s drive-thru or gas station pumps with a broken receipt printer or the Flint Firebirds’ souvenir shop using SquareCash.  No receipt means no ledger entry means confusion when I go to reconcile.

I could have made a mobile version of the ledger entry form but I really didn’t want to. As such I decided I could fix the issue by keeping faux-receipts electronically in Trello.  A single list.  Card title is the place I spent the money, description is the amount, a label to represent the account.  Once there’s a ledger entry, archive the card.

And that would have been enough.  I decided to take it a step further and automate things.

I use a webhook subscribed to that single list to look for new cards, get the data from them, automatically add the record to my ledger, then archive the card. I’m essentially using Trello’s app as an interface for my own so that I don’t have to make a mobile interface.

It’s a bit hacky but I figured I’d throw some of the code out here since I feel like there’s not a lot of Trello webhook documentation out there.  Unlike my usual, I’m going to redact some of the code as it deals with my financial system and I’d prefer not to put that out there.

<?php
  require_once 'trello_api.php';
  $trello = new trello_api($trello_config['key'], $trello_config['secret'], $trello_config['token']);

  $data = json_decode(file_get_contents('php://input'));

  if (($data->action->type == 'createCard') AND ($data->model->id == $data->action->data->list->id)) {
    // CREATE WEBHOOK ON CARD
    $webhook = $trello->request('POST', '/1/webhook', array('description' => ('Temporary callback for Card ' . $data->action->data->card->id), 'callbackURL' => 'http://www.example.com/webhook_callback.php', 'idModel' => $data->action->data->card->id));
    $trello->request('POST', ('/1/card/' . $data->action->data->card->id . '/actions/comments'), array('text' => ('Webhook ID: ' . $webhook->id)));
  }

  if (($data->model->id == $data->action->data->card->id) AND (($data->action->type == 'createCard') OR ($data->action->type == 'updateCard') OR ($data->action->type == 'addLabelToCard'))) {
    // ASSIGN VARIABLES
    $date = date('Y-m-d', strtotime($data->action->date));
    $label = trim($data->model->name);
    $amt = trim(str_replace('$', '', $data->model->desc));

    $category = get_category_id($label);
    $account_id = get_account_name($data->model->labels[0]->name);

    $details = array(array('category_id' => $category, 'amt' => $amt));

    if (($account_id) AND ($category) AND (is_numeric($amt))) {
      // DATA LOOKS GOOD, REMOVE WEBHOOK, ARCHIVE CARD, AND CREATE ENTRY
      $comments = $trello->request('GET', ('/1/cards/' . $data->model->id . '/actions'), array('filter' => 'commentCard', 'fields' => 'data'));

      $webhook_id = 0;
      foreach ($comments AS $comment) {
        if (strstr($comment->data->text, 'Webhook ID: ')) {
          $webhook_id = str_replace('Webhook ID: ', '', $comment->data->text);
          break;
        }
      }

      if ($webhook_id) {
        // FOUND A WEBHOOK, EVERYTHING WORKING AS IT SHOULD
        $trello->request('DELETE', ('/1/webhook/' . $webhook_id));
        $trello->request('PUT', ('/1/card/' . $data->model->id . '/closed'), array('value' => true));
        create_ledger_entry($account_id, $date, $label, $amt, $details);
      } else {
        // DIDN'T FIND A WEBHOOK, STUFF IS BROKEN
        $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => 'No webhook found.'));
      }
    } else {
      // DATA LOOKS NOT GOOD, NOTE IN CARD
      $msg = '';
      if (!$account_id) $msg .= 'Invalid account selected' . "\n\n";
      if (!is_numeric($amt)) $msg .= 'Invalid amount selected: ' . $amt . "\n\n";
      if (!$category) $msg .= 'No category found' . "\n\n";

      if ($msg) $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => trim($msg)));
    }
  }

  unset($trello);
?>

As I said, I’ve replaced some of the actual code with obscured-away psudo-functions. I’ll point those out those spots as necessary while going through this piece-by-piece.

require_once 'trello_api.php';
$trello = new trello_api($trello_config['key'], $trello_config['secret'], $trello_config['token']);

$data = json_decode(file_get_contents('php://input'));

Right off the bat we pull in my Trello API wrapper class (which really could use some love, maybe I’ll get to that sooner or later) and instantiate an object that we’ll use later. Then we pull in the data Trello posted to us via the input stream.

if (($data->action->type == 'createCard') AND ($data->model->id == $data->action->data->list->id)) {
  // CREATE WEBHOOK ON CARD
  $webhook = $trello->request('POST', '/1/webhook', array('description' => ('Temporary callback for Card ' . $data->action->data->card->id), 'callbackURL' => 'http://www.example.com/webhook_callback.php', 'idModel' => $data->action->data->card->id));
  $trello->request('POST', ('/1/card/' . $data->action->data->card->id . '/actions/comments'), array('text' => ('Webhook ID: ' . $webhook->id)));
}

Webhooks subscribed to a list don’t give us all the details of cards on the list, so if we detect a card creation and we’re certain we’re getting data from the webhook attached to the list, we add a webhook to the new card. This is done with a POST to /1/webhook passing in a description (which is optional and doesn’t really matter), callbackURL (same as the URL of this script, though obscured here), and idModel (the ID of the card). For future reference, we then post the ID of the newly-created webhook to a comment on the card via POST to /1/card/<card_id>/actions/comments with text set as needed.

if (($data->model->id == $data->action->data->card->id) AND (($data->action->type == 'createCard') OR ($data->action->type == 'updateCard') OR ($data->action->type == 'addLabelToCard'))) {
  // ASSIGN VARIABLES
  $date = date('Y-m-d', strtotime($data->action->date));
  $label = trim($data->model->name);
  $amt = trim(str_replace('$', '', $data->model->desc));

  $category = get_category_id($label);
  $account_id = get_account_name($data->model->labels[0]->name);

  $details = array(array('category_id' => $category, 'amt' => $amt));

The rest of the code only fires if we’re receiving from the new webhook attached to the card, and if it’s triggered by either a card creation (which should never happen since the webhook won’t have been created yet), card update (which happens when I set the card’s description), or label addition (which is how I define what account the transaction is for). We only care about those actions because they match the ones I take on the front-end.

From there, we use attributes of the card to build the transaction. The transaction date is whatever the action is taking place. The label is the name of the card. The amount comes from the card description. The category is determined based on the label. The account_id is determined based on the first label selected. A single-item, multidimensional array (the actual interface handles more complex data) is assembled from this data.

if (($account_id) AND ($category) AND (is_numeric($amt))) {
  // DATA LOOKS GOOD, REMOVE WEBHOOK, ARCHIVE CARD, AND CREATE ENTRY
  $comments = $trello->request('GET', ('/1/cards/' . $data->model->id . '/actions'), array('filter' => 'commentCard', 'fields' => 'data'));

  $webhook_id = 0;
  foreach ($comments AS $comment) {
    if (strstr($comment->data->text, 'Webhook ID: ')) {
      $webhook_id = str_replace('Webhook ID: ', '', $comment->data->text);
      break;
    }
  }

If we got an account ID and a category and the amount is numeric, we know we got good data and we’re ready to try to add the record. First, though, we make a GET call to /1/cards/<card_id>/actions with filter set to commentCard and fields set to data, so that we can get all of the comments posted to the card. We loop through them until we find the one where we stored the card webhook ID and then we save that ID off. There’s probably a better way to do this.

if ($webhook_id) {
  // FOUND A WEBHOOK, EVERYTHING WORKING AS IT SHOULD
  $trello->request('DELETE', ('/1/webhook/' . $webhook_id));
  $trello->request('PUT', ('/1/card/' . $data->model->id . '/closed'), array('value' => true));
  create_ledger_entry($account_id, $date, $label, $amt, $details);

If we found a webhook ID (and we always should), we delete the webhook, archive the card, and create the ledger entry. The first is accomplished with a DELETE request to /1/webhook/<webhook_id>. The second is a PUT to /1/card/<card_id>/closed with value set to true.

} else {
  // DIDN'T FIND A WEBHOOK, STUFF IS BROKEN
  $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => 'No webhook found.'));
}

If we didn’t get a webhook ID, something is seriously broken so we log that finding to the card as a comment via a POST to /1/card/<card_id>/actions/comments with text set to “No webhook found.”

    } else {
      // DATA LOOKS NOT GOOD, NOTE IN CARD
      $msg = '';
      if (!$account_id) $msg .= 'Invalid account selected' . "\n\n";
      if (!is_numeric($amt)) $msg .= 'Invalid amount selected: ' . $amt . "\n\n";
      if (!$category) $msg .= 'No category found' . "\n\n";

      if ($msg) $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => trim($msg)));
    }
  }

  unset($trello);
?>

Lastly, if we don’t have all of the things we need to make a ledger entry we log that to the card as a comment. Again, that’s done with a POST to /1/card/<card_id>/actions/comments.

As I said, it’s hacky. It gave me a chance to play with Trello webhooks a bit, though, and was a lot of fun. As I use it more, we’ll see what I did wrong.

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.