Weekend Project: NerdsAreDrinking -- Laravel, Twitter, and Commands

Joe • December 15, 2014

laravel php

Last Friday I was enjoying an after work beer at my favorite taproom with Brian and Nicole. Brian is one of my two cohosts of the Nerds Drinking Podcast. As we sat there talking each one of us checked into our beers via untappd and I made the comment: "Wouldn't it be amusing if the @nerdsdrinking twitter account retweeted our beer checkin whenever we had untappd tweet?" We had a decent chuckle about the thought of that.

So of course this past weekend I shrugged off most of what I had planned to do in order to crank out this application that would parse my twitter feed looking for a beer that I had checked into untappd.

I started out with Laravel and Homestead, as I typically start projects. I googled for a package that would do the twitter heavy lifting for me and came across j7mbo/twitter-api-php. I was somewhat concerned about using a package that hadn't been touched in so long and had the install instructions set to include the file even though the autoloader would have loaded the class. This could have just been an oversight, it has been a year since it was updated.

After about 3 hours of lazily poking around at the application while watching TV with the wife I managed to get a working example of what I wanted.

I knew I wanted this application to be run periodically from the command line. It has been about 4 years since I seriously wrote anything that was intended for the command line, so a refresher was in order. So much has changed in the short time since I've written PHP command line apps. Symfony's Console makes it ridiculously easy. There is an excellent Laracasts video that guides you through this. This method was a perfect choice for what I'd need, but I wanted to stretch my Laravel knowledge so I went down the path of adding a Laravel Command.

Laravel Commands are really easy to use and allow you to do some neat things.  At the day job, a coworker and I built a decently large application on Laravel and he built a few commands for us to automate some same data being added to the database. I never looked too far into the commands since we were under a tight deadline so this would be my chance to jump into commands.

Major Refactor (/salute) time. It was time to go from a long list of procedural PHP to functional PHP!

Once I had my command boilerplate set up I put my command code side by side with my procedural code and began to break it out into functions. I added a few features along the way as well: Multiple account checking and a test mode. The multiple account checking would allow me to add as many people as I wanted to have their feeds parsed for beer check ins via untappd. Test mode allows me to get everyone's tweets and see IF there has been something to retweet. To prevent retweeting the same tweets over and over and to prevent having to parse long lists of tweets every time,  I implemented the Twitter API's since_id functionality. This allows the application to keep track of the last tweet it has seen for each user. That way we only request the tweets since the last tweet id that we have previously seen. As long as we're saving the most recent tweet id and using that when we get tweets, We'll never see duplicate tweets and can feel confident that we won't retweet something we've already retweeted before.

Our command's fire method:

public function fire()
{
    $nerds = $this->getNerds();

    foreach ($nerds as $nerd)
    {
        $tweets = $this->getTweets($nerd);

        if (count($tweets) > 0)
        {
            // update the since_id with the latest tweet in $tweets
            if (!$this->option('test')) {
                $this->updateSince($tweets['0']->id, $nerd->name);
            }
        }

        foreach ($tweets as $tweet)
        {
            $this->parseTweets($tweet);
        }
    }
}

Our first method grabs all the people we want to check feeds for:

public function getNerds()
{
    return Nerds::all();
}

Then we loop through and get tweets for each user:

public function getTweets($user)
{
    $url = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
    $getField = '?screen_name=' . $user->twitter;

    $since = $this->getSince($user->name);
    if (!is_null($since))
    {
        $getField .= '&since_id=' . $since->since_id;
    }

    $twitter = new TwitterAPIExchange($this->getSettings());

    $response = $twitter->setGetfield($getField)
                        ->buildOauth($url, 'GET')
                        ->performRequest();

    return json_decode($response);
}

If we found new tweets, and we're NOT running in test mode, we update the since_id with the newest tweet:

public function updateSince($tweet_id, $name)
{
    $lastTweet = new LastTweet;
    $lastTweet->since_id = $tweet_id;
    $lastTweet->name = $name;
    $lastTweet->save();
}

Looping over each tweet we're going to pass each to our parsing method and look for the text "Drinking a" and "untappd" as the source of the tweet. If we find both of those then we build our status that We'll retweet. If we ARE in test mode, We'll output to the console that we should have tweeted this status.

public function parseTweets($tweet)
{
    if (strpos($tweet->text, 'Drinking a') !== false &&
            strpos($tweet->source, 'untappd') !== false)
        {
            $user = '@' . $tweet->user->screen_name;
            $status = '#NerdsDrinking RT: ' . $user . ' ' . $tweet->text;
            $regex = "@(https?://([-\w\.]+[-\w])+(:\d+)?(/([\w/_\.#-]*(\?\S+)?[^\.\s])?).*$)@";
            $status =  preg_replace($regex, ' ', $status);
            $status = str_replace(' —  ', '', $status);

            if (!$this->option('test'))
            {
                $this->postTweet($status, $tweet->id);
            }

            if ($this->option('test'))
            {
                $this->info('We should have tweeted: ' . $status);
            }
        }
}

If we're NOT in test mode, We'll pass the status and tweet id to the postTweet method.

public function postTweet($status, $tweet_id)
{
    $url = 'https://api.twitter.com/1.1/statuses/update.json';
    $postFields['status'] = json_encode($status);
    $postFields['in_reply_to_status_id'] = $tweet_id;

    $tweet = new TwitterAPIExchange($this->getSettings());
    $response = $tweet->setPostfields($postFields)
                      ->buildOauth($url, 'POST')
                      ->performRequest();
}

You can view the entire command on github.

Deploying this was very easy with Laravel Forge. If you're doing a lot of Laravel projects you should check out forge if you haven't already. I added the repo to forge and then I added a scheduled job to run our command hourly.

Recap: There are a lot of things I'm not doing / taking for granted. I'm not doing any error checking when posting tweets. Since this is not a terribly serious application, I just Don't care if we miss a tweet. The application could also be refactored and abstracted a bit more. It could also be refactored to where the command is using controllers instead of it's own methods so I could potentially run the methods via the website, but since I'm not really using this app as a website, it's not an issue. There are also likely some efficiencies that could be gained from the string checking and manipulation but again, with it not being a serious application I'm inclined to be content without min/maxing performance.

This application could also be easily rewritten to use Symfony's Console. It would be lighter weight since we wouldn't be invoking an entire framework but the point of this weekend's exercise was to explore Laravel Commands. A future weekend could be devoted to rewriting it to the console and other tweaks.

 

Thanks for reading.