Tweets to WordPress Posts Plugin

In the first part of this tutorial, we covered the WordPress Cron engine, created our own scheduled task that used the WordPress HTTP API to query the Twitter Search API and create new Status posts out of new tweets in the search.

Today we’ll talk about some improvements that can be made to the plugin. We’ll create a plugin options page using the Settings API and talk about different search techniques available on Twitter.

Planning an Options Page

Let’s start off with the plugin options page and the first thing we’ll go through is a plan. We need to plan what kind of options we’re going to provide, how these options will be called and used. After that we’ll jump to implementing them on an options page using the Settings API. So, here’s what options we’ll cover in this tutorial:

  • Recurrence (daily, twice daily, hourly)
  • Search string
  • Posts author

Let’s keep it as simple as that for a while. We’ll store the options in an associative array called ttp_options (ttp for Tweets to Posts) and the array keys: recurrence, search and author for the respective options mentioned above.

We’ll continue where we left off in the previous part of this tutorial and since it’s going to get a little more complicated than before, let’s take a minute to rename all our functions and hooks from the my_ prefix to the less-generic ttp_ prefix, so my_create_schedule becomes ttp_create_schedule and my_fetch_tweets becomes ttp_fetch_tweets. Don’t forget to provide the correct function names when adding them to actions with add_action too!

Registering the Settings, Sections and Fields

You’re probably familiar with the Settings API already, so let’s use that to create our three settings together with the callback functions that render the form controls. So, appending to our plugin source file:

function ttp_register_settings() {
    register_setting( 'ttp_options', 'ttp_options' );

    add_settings_section( 'ttp_general', 'General Settings', 'ttp_section_general', 'ttp_options' );
    add_settings_field( 'recurrence', 'Recurrence', 'ttp_field_recurrence', 'ttp_options', 'ttp_general' );
    add_settings_field( 'search', 'Search', 'ttp_field_search', 'ttp_options', 'ttp_general' );
    add_settings_field( 'author', 'Author', 'ttp_field_author', 'ttp_options', 'ttp_general' );
}
add_action( 'admin_init', 'ttp_register_settings' );

There you go, a setting, a section and three fields. Note that our option name in the WordPress database is ttp_options, we’ll use that when retrieving options back to our plugin. The General section will hold all our three settings fields which is quite straightforward, so let’s now look at the callbacks implementation.

General Section Callback

This is very easy, we just output some information about the settings section and since we have only one, we called it General. Here’s the callback function for our section:

function ttp_section_general() {
    echo 'General Tweets to Posts settings that affect the workflow of the plugin.';
}

Nothing to discuss here, so let’s move over to the fields.

Recurrence Field Callback

This field will be a drop-down select box containing all available schedules provided by the WordPress cron module. Let’s take a look at the callback function first and go through it step by step afterwards.

function ttp_field_recurrence() {
    $schedules = wp_get_schedules();
    $options = get_option( 'ttp_options' );
    $recurrence = isset( $options['recurrence'] ) ? $options['recurrence'] : 'hourly';
?>
    <select name="ttp_options[recurrence]">
    <?php foreach ( $schedules as $key => $schedule ): ?>
        <option value="<?php echo $key; ?>" <?php selected( $key == $recurrence ); ?>><?php echo $schedule['display']; ?></option>
    <?php endforeach; ?>
    </select>
<?php
}

As you can see, we’re using the wp_get_schedules function to retrieve all available schedules. WordPress comes with three standard schedules — hourly, daily and twice daily, but you can easily filter in and provide your own. We’re also using the get_option function to retrieve our plugin options and then get the recurrence key from that.

We finally render our select element (note down the name attribute) and using a foreach loop, render each available option into our select box. Note how we’re using the selected function which sets the value of the selected option when rendering the control. You can also inspect the $schedules array using print_r to see what other keys are available.

Search Field Callback

This is fairly simple since our search field is a simple text box, the contents of which will then be passed on to the Twitter API as the search query.

function ttp_field_search() {
    $options = get_option( 'ttp_options' );
    $search = isset( $options['search'] ) ? $options['search'] : '';
?>
    <input type="text" name="ttp_options[search]" value="<?php echo esc_attr( $search ); ?>" />
<?php
}

Note the usage of the esc_attr WordPress function which makes sure that the value is escaped before squeezing it into the value attribute.

Author Field Callback

And finally, the author field, which is even easier than all the above, thanks to WordPress built-in function called wp_dropdown_users:

function ttp_field_author() {
    $options = get_option( 'ttp_options' );
    $author = isset( $options['author'] ) ? $options['author'] : '';
    wp_dropdown_users( array(
        'name' => 'ttp_options[author]',
        'selected' => $author
    ) );
}

This renders a select box, similar to the one in our recurrence field but mostly generated by that magic wp_dropdown_users function along with a few arguments to make sure the name attribute is correct, as well as the selected author remains selected.

Now that’s been taken care of, all fields and sections have now got callbacks but we aren’t really seeing them in action, are we? Right, we need to add an entry to our Settings menu and render the options page for our plugin.

Creating the Plugin Options Page

Again, thanks to the Settings API, this is extremely easy to do. You just have to make sure you’re hooking into the right place and providing the correct callback functions.

function ttp_options_page() {
    add_options_page( 'Tweets to Posts Options', 'Tweets to Posts', 'manage_options', 'ttp_options', 'ttp_options_page_contents' );
}
add_action( 'admin_menu', 'ttp_options_page' );

function ttp_options_page_contents() {
?>
<div class="wrap">
    <?php screen_icon(); ?>
    <h2>Tweets to Posts Options</h2>

    <form method="post" action="options.php">
        <?php wp_nonce_field( 'update-options' ); ?>
        <?php settings_fields( 'ttp_options' ); ?>
        <?php do_settings_sections( 'ttp_options' ); ?>
        <?php submit_button(); ?>
    </form>
</div>
<?php
}

The first function uses add_options_page to add an entry under the Settings menu. The function itself should be added during admin_menu ( and not admin_init ). The callback function uses some simple HTML markup to make our options page look as nice as all the rest of the admin pages in WordPress, and uses the settings_fields and do_settings_sections to render our sections and controls. The wp_nonce_field is important here too from the security point of view.

So as soon as that’s done, you’ll be left with a fully working plugin options page, that should look something like this:

Plugin Options Page

Implementing Our New Options

Let’s start off with the Recurrence option. As you recall from the first part of this tutorial, the scheduling magic happens in a function called ttp_create_schedule. Rewrite that to make it look something like this:

function ttp_create_schedule() {
    $options = get_option( 'ttp_options' );
    $recurrence = isset( $options['recurrence'] ) ? $options['recurrence'] : 'hourly';

    $schedule = wp_get_schedule( 'ttp_tweets_to_posts' );
    if ( $schedule != $recurrence )
        wp_clear_scheduled_hook( 'ttp_tweets_to_posts' );

    if ( ! wp_next_scheduled( 'ttp_tweets_to_posts' ) ) {
        wp_schedule_event( time(), $recurrence, 'ttp_tweets_to_posts' );
    }
}

It didn’t change much. All we did was grab the recurrence value from the database and used it when scheduling the event. We also compared that value to the currently scheduled one and cleared it out if they were different. This makes sure that the new schedule takes effect as soon as the the recurrence has been changed, rather than after the even has happened the next time.

Possible improvements here are around the $recurrence variable. We’re running a check to see if the option has been set, but what if it has been set to “three times a day” and WordPress doesn’t know about that kind of schedule? We should probably run that through an in_array call comparing to the output of wp_get_schedules. We can do the same when rendering our recurrence field.

Next up is the search query for the Twitter API and the status posts author on our website. These are quite easy to implement and should be done in our ttp_fetch_tweets function, which earlier had something like this:

$query = "from:themefm";
$author = get_user_by( 'email', 'konstantin@frumatic.com' ); // Replace with your own

Which explicitly set the search term and the author, but we now have these in our plugin options, so let’s use them:

$options = get_option( 'ttp_options' );
$query = isset( $options['search'] ) ? $options['search'] : '';
$author_id = isset( $options['author'] ) ? $options['author'] : 0;
$author = get_user_by( 'id', $author_id );

// Don't do anything if not configured.
if ( empty( $author ) || empty( $query ) )
    return;

As you can see, we’re setting the $query and $author variables like we did before, only this time we’re taking the values from the database. We’re also running a quick check to make sure both variables contain some values. The rest of the function remains as it was. Here’s the full source code for our ttp_fetch_tweets function which is getting a little bigger now:

// Fired during our scheduled event
function ttp_fetch_tweets() {
    $options = get_option( 'ttp_options' );
    $query = isset( $options['search'] ) ? $options['search'] : '';
    $author_id = isset( $options['author'] ) ? $options['author'] : 0;
    $author = get_user_by( 'id', $author_id );

    // Don't do anything if not configured.
    if ( empty( $author ) || empty( $query ) )
        return;

    $max_id = get_option( 'ttp_max_tweet_id', 0 );

    // Fire the HTTP request to Twitter
    $response = wp_remote_get( 'http://search.twitter.com/search.json?q=' . urlencode( $query ) . '&result_type=recent&rpp=10&since_id=' . $max_id );

    // Make sure no errors occured
    if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) == 200 ) {

        // Parse the response and grab the max_id
        $response = json_decode( wp_remote_retrieve_body( $response ) );
        if ( ! empty( $response->results ) ) {
            $max_id = $response->max_id_str;
            update_option( 'ttp_max_tweet_id', $max_id );
        }

        // Loop through the tweets (search results) and publish posts
        foreach ( $response->results as $tweet ) {
            $gmt_date = date( 'Y-m-d H:i:s', strtotime( $tweet->created_at ) );

            $post = array(
                'post_content' => $tweet->text,
                'post_status' => 'publish',
                'post_date_gmt' => $gmt_date,
                'post_date' => get_date_from_gmt( $gmt_date ),
                'post_author' => $author->ID
            );

            $post_id = wp_insert_post( $post );
            set_post_format( $post_id , 'status' );
        }
    }
}
add_action( 'ttp_tweets_to_posts', 'ttp_fetch_tweets' );

And you’re done! You’ve now got three options which you can use to configure the plugin without having to go into the source code, but there’s one more thing I’d like to cover.

Making Things Clickable

WordPress comes with a sweet function called make_clickable which transforms URLs and e-mail addresses into links, but as we know from Twitter, that’s not enough. We’ll add usernames, hashtags and lists to that using our own clickable function, like this:

function ttp_make_clickable( $text ) {
    $text = make_clickable( $text );

    // Hashtags
    $text = preg_replace(
        '/(^|[^0-9A-Z&\/]+)(#|\xef\xbc\x83)([0-9A-Z_]*[A-Z_]+[a-z0-9_\xc0-\xd6\xd8-\xf6\xf8\xff]*)/iu',
        '${1}<a href="http://twitter.com/search?q=%23${3}" title="#${3}">${2}${3}</a>',
        $text
    );

    // Mentions
    $text = preg_replace(
        '/([^a-zA-Z0-9_]|^)([@\xef\xbc\xa0]+)([a-zA-Z0-9_]{1,20})(\/[a-zA-Z][a-zA-Z0-9\x80-\xff-]{0,79})?/u',
        '${1}@<a href="http://twitter.com/intent/user?screen_name=${3}" class="twitter-action">${3}</a>',
        $text
    );

    // Lists
    $text = preg_replace(
        '$([@|@])([a-z0-9_]{1,20})(/[a-z][a-z0-9\x80-\xFF-]{0,79})?$i',
        '${1}<a href="http://twitter.com/${2}${3}">${2}${3}</a>',
        $text
    );

    return $text;
}

You don’t have to dig into the regular expressions themselves, but simply copy and paste them ;) Then use the function in a filter to the_content also making sure we’re operating on status posts, like this:

function ttp_filter_content( $content ) {
    global $post;
    if ( get_post_format( $post->ID ) === 'status' )
        $content = ttp_make_clickable( $content );

    return $content;
}
add_filter( 'the_content', 'ttp_filter_content' );

Which should bring us the following result:

Making Tweets Clickable

Twitter Search Techniques

If you publish a lot of tweets from your own Twitter account, or use a search query that’s quite popular on Twitter, you might end up with way too many status posts per hour. We recommend you inspect the search results for your specific query before actually using it.

You should definitely take a look at Twitter’s Advanced Search which can help you construct a better search query, filtering out things you don’t like.

For example, a search for “from:themefm -filter:replies” would return tweets from @themefm but would filter out tweets which are replies to other tweets. A search for “from:themefm filter:links” would return only tweets containing links. With a little bit of more coding, you can also move some of these filters into your plugin options page and append them to the search query when chosen, which would actually be very nice ;)

Conclusion

Of course there’s a lot of room for other improvements here, perhaps adding the Twitter widgets script for better popup-intents is one and multi-author (Twitter names to Author names mapping) is another one. Security improvements is always a must before you go public. You can grab the full source code of the plugin right here and compare to what you have done and see where we (or you) went wrong:

Download Source Code

Thanks for reading and hope you enjoyed it. Say, should we follow up on this tutorial with part three? If yes, then what features and improvements would you like to see next? Use the comment form below to leave us a note and thank you for staying tuned!