Hotaru Docs

 [həʊdɒks]

Making a Plugin - Case Study 3: Tweet This

About the Tweet This plugin

Adding a Tweet This! button so you can send your posts to Twitter is incredibly easy. All you would have to do is add this code to the sb_list.php and sb_post.php templates:

<a href="http://twitter.com/home?status=Currently reading <?php echo $h->url(array('page'=>$h->post->id)); ?>"' title="Click to send this post to Twitter!" target="_blank">Tweet This!</a>

The problem with that method is there is no check to see if you've gone over 140 characters, and the url isn't shortened until after you click "Update" on Twitter's end. That means you have very little room for comments around the url, such as the post's title, which is not included in the above snippet.

This Hotaru plugin is going to include the post's title, shrink the url with a choice of url shortening services, and make sure it all fits in 140 characters.

STEP 1 - The skeleton

To get started, make a folder called tweet_this and a .php file called tweet_this.php containing the following code. Notice that we've included three plugin hooks to begin with. The first we will use for installing the plugin and the other two are both used for building our settings page. We don't actually need to write functions for those last two hooks, because we can fall back on Hotaru's default plugin functions instead.

<?php
/**
 * name: Tweet This
 * description: Send posts to Twitter
 * version: 0.1
 * folder: tweet_this
 * class: TweetThis
 * hooks: install_plugin, admin_sidebar_plugin_settings, admin_plugin_settings
 */
 
class TweetThis
{
    /**
     * Install Tweet This
     */
    public function install_plugin($h)
    {
        // install code to go here
    }
}
?>

STEP 2 - Install function

What options should we give site admins? For now let's give them a choice of using TinyUrl, is.gd and Bitly. If they choose the last one, they will nedd to enter their Bitly login and API key.

Let's start with the install function. Rather than saving each item as individual settings, we'll combine them into one for efficiency. That's done with PHP's serialize function which takes our array of settings and squashes them into a string.

/**
 * Install Tweet This
 */
public function install_plugin($h)
{
    // Plugin settings
    $tweet_this_settings = $h->getSerializedSettings();
 
    if (!isset($tweet_this_settings['tt_shortener'])) { $tweet_this_settings['tt_shortener'] = "is.gd"; }
    if (!isset($tweet_this_settings['tt_bitly_login'])) { $tweet_this_settings['tt_bitly_login'] = ""; }
    if (!isset($tweet_this_settings['tt_bitly_api_key'])) { $tweet_this_settings['tt_bitly_api_key'] = ""; }
 
    $h->updateSetting('tweet_this_settings', serialize($tweet_this_settings));
}

Note: the settings will only be updated if they don't already exist. Otherwise the admin would lose his choices each time he installs or upgrades the plugin.

STEP 3 - Admin settings preparation

This is quite large, but hopefully quite logical.

First, make a new .php file called tweet_this_settings.php and put a class with two functions in it like this:

<?php
 
class TweetThisSettings
{
    /**
     * Admin settings for the Tweet This plugin
     */
    public function settings($h)
    {
 
    }
 
    /**
     * Save Tweet This settings
     */
    public function saveSettings($h)
    {
 
    }
}

In the first function, we will get the existing settings from the database and display them on the page. To do that, we will need to make a language file so we can label the various options.

Create a folder called languages and put a .php file with the name tweet_this_language.php inside it.

<?php
/**
 * Language file for Tweet This plugin
 */
 
/* Admin options */
$lang['tweet_this_settings_header'] = "Tweet This Settings";
$lang['tweet_this_settings_shortener'] = "Choose the url shortener you want to use. If you choose bit.ly, you must provide your bit.ly login and API key.";
$lang['tweet_this_settings_tinyurl'] = "tinyurl";
$lang['tweet_this_settings_isgd'] = "is.gd";
$lang['tweet_this_settings_bitly'] = "bit.ly";
$lang['tweet_this_settings_bitly_login'] = "bit.ly login";
$lang['tweet_this_settings_bitly_api_key'] = "bit.ly API key";
$lang['tweet_this_settings_error'] = "You must enter your bitly login and API key to use bitly";
 
?>

Go back to your tweet_this_settings.php file and display the header:

/**
 * Admin settings for the Tweet This plugin
 */
public function settings($h)
{
    // show header
    echo "<h1>" . $h->lang["tweet_this_settings_header"] . "</h1>\n";
}

With that done, you should be able to install the plugin, click a "Tweet This" link in the Admin sidebar and view the settings page with the "Tweet This Settings" header on display.

STEP 4 - Admin settings form

Before we can display the form, we need to get the existing settings from the database:

/**
 * Admin settings for the Tweet This plugin
 */
public function settings()
{
    // show header
    echo "<h1>" . $h->lang["tweet_this_settings_header"] . "</h1>\n";
 
    // Get settings from database if they exist...
    $tweet_this_settings = $h->getSerializedSettings(); 
Then prepare the values for our radio buttons...
<code>[php, N]
    // set choices to blank
    $tinyurl = "";
    $isgd = "";
    $bitly = "";
 
    // determine which is selected
    switch($tweet_this_settings['tt_shortener']) {
        case 'tinyurl':
            $tinyurl = "checked";
            break;
        case 'bitly':
            $bitly = "checked";
            break;
        default:
            $isgd = "checked";
}

And now we can show the whole form:

// start form
    echo "<form name='tweet_this_settings_form' ";
    echo "action='" . BASEURL . "admin_index.php?page=plugin_settings&amp;plugin=tweet_this' method='post'>\n";
 
    // instructions
    echo "<p>" . $h->lang['tweet_this_settings_shortener'] . "</p>";
 
    // radio buttons
 
    // is.gd
    echo "<p><input type='radio' name='tt_shortener' value='isgd' " . $isgd . " >";
    echo "&nbsp;&nbsp;" . $h->lang["tweet_this_settings_isgd"] . "</p>\n"; 
 
    // tinyurl
    echo "<p><input type='radio' name='tt_shortener' value='tinyurl' " . $tinyurl . " >";
    echo "&nbsp;&nbsp;" . $h->lang["tweet_this_settings_tinyurl"] . "</p>\n"; 
 
    // bit.ly
    echo "<p><input type='radio' name='tt_shortener' value='bitly' " . $bitly . " >";
    echo "&nbsp;&nbsp;" . $h->lang["tweet_this_settings_bitly"] . "</p>\n"; 
 
    // input fields
 
    // bitly login
    echo "<p>" . $h->lang['tweet_this_settings_bitly_login'];
    echo ": <input type='text' size=30 name='tt_bitly_login' value='" . $tweet_this_settings['tt_bitly_login'] . "' /></p>";
 
    // bit.ly api key
    echo "<p>" . $h->lang['tweet_this_settings_bitly_api_key'];
    echo ": <input type='text' size=30 name='tt_bitly_api_key' value='" . $tweet_this_settings['tt_bitly_api_key'] . "' /></p>";
 
    // end form
    echo "<br />";
    echo "<input type='hidden' name='submitted' value='true' />\n";
    echo "<input type='submit' value='" . $h->lang["main_form_save"] . "' />\n";
    echo "<input type='hidden' name='csrf' value='" . $h->csrfToken . "' />\n";
    echo "</form>\n";
}

Now that the form is complete, we need to tell Hotaru when it is submitted and to call the saveSettings function. To do that, add this below the <h1> tags near the top of the settings function you've just been working on:

// If the form has been submitted, go and save the data...
if ($h->cage->post->getAlpha('submitted') == 'true') { 
    $this->saveSettings($h); 
}

STEP 5 - Saving admin settings

So far, you've stored default settings in the database, created an Admin settings page, retrieved those settings and filled in a form using them. The next step is to save those settings.

You should still have this empty function below what you've just finished:

/**
 * Save Tweet This settings
 */
public function saveSettings()
{
 
}

Let's start by including the language file and getting the settings again.

/**
 * Save Tweet This Settings
 */
public function saveSettings($h)
{
    // Get settings from database if they exist...
    $tweet_this_settings = $h->getSerializedSettings();

Now let's store the user's choices in our $tweet_this_settings array:

// get result of radio buttons and bitly fields
    $tweet_this_settings['tt_shortener'] = $h->cage->post->testAlpha('tt_shortener');
    $tweet_this_settings['tt_bitly_login'] = $h->cage->post->testAlnumLines('tt_bitly_login');
    $tweet_this_settings['tt_bitly_api_key'] = $h->cage->post->testAlnumLines('tt_bitly_api_key');

Next we need to check whether the user has chosen bitly, and if so, did he enter both his login and api key? If not, we should show an error message and not save the settings. Otherwise, save everything and show a "Settings saved" message.

// if bitly is chosen but either of the login or api key fields are empty, set error, don't save
    if ($tweet_this_settings['tt_shortener'] == 'bitly' &&
        (!$tweet_this_settings['tt_bitly_login'] || !$tweet_this_settings['tt_bitly_api_key']))
    {
        // error message
        $h->message = $h->lang["tweet_this_settings_error"];
        $h->messageType = "red";
    } 
    else 
    {
        // update settings and set message
        $h->updateSetting('tweet_this_settings', serialize($tweet_this_settings));
        $h->message = $h->lang["main_settings_saved"];
        $h->messageType = "green";
    }
 
    // show message
    $h->showMessage();
 
    return true;
}

That concludes the admin section. You should be able to test it in your browser now.

STEP 6 - The Tweet This link

We're going to put our Tweet This link below each post description alongside the comments, tags and flag links. To do that, we need to find the same plugin hook used by Comments, Tags, and Vote Simple to place their links.

Take a look at sb_list.php and sb_post.php in the Submit plugin. They both have the same plugin hook:

<div class="show_post_extra_fields">
    <ul>
        <?php $h>pluginHook('sb_base_show_post_extra_fields'); ?>
    </ul>
</div>

So all we need to do is write a function for that spot. Let's see how the Comments plugin does it:

/**
 * Link to comments
 */
public function sb_base_show_post_extra_fields($h)
{
    echo '<li><a class="comment_link" href="' . $h->url(array('page'=>$h->post->id)) . '">' . $this->hotaru->comment->countComments() . '</a></li>' . "\n";
}

For us to use an edited version of this in our Tweet This plugin, first we need to add this hook at the top of our tweet_this.php file:

* hooks: install_plugin, admin_sidebar_plugin_settings, admin_plugin_settings, sb_base_show_post_extra_fields

Now lets copy the function from the Comments plugin and make a few changes:

/**
 * Tweet This link
 */
public function sb_base_show_post_extra_fields($h)
{
    echo "<li><a class='tweet_this_link' href='" . $h->url(array('page'=>'tweet_this', 'id'=>$h->post->id)) . "'>";
    echo $h->lang['tweet_this'] . "</a></li>\n";
}

Two things worth noting. First, we're using Hotaru's url function instead of a direct http:// link because Hotaru will automatically convert it into either a standard or friendly url, depending on the admin's settings. We're linking to a "tweet_this" page, which is actually our plugin, and we're including the ID of the post we want to tweet.

The second point is that rather than hard code the words "Tweet This!" into the link, your should use the language file. Open /languages/tweet_this_language.php and add this:

/* Tweet This link */
$lang['tweet_this'] = "Tweet This!";

Because you've added a new hook to the top of the file, you'll need to reinstall the Tweet This plugin. When you've done that, you should a link below post descriptions!

That completes the presentation side of the plugin. Now it's time to get into the nitty-gritty of manipulating the url for Twitter.

STEP 7 - When "Tweet This!" is clicked

When a user clicks "Tweet This", Hotaru fills the url with page=tweet_this and id=xxx. So the first thing to do is check whether page is in fact tweet_this. The best place to do that is just before HTML is output to the browser. This allows us to redirect elsewhere (in this case Twitter) without throwing any horrible "Headers already sent" errors that occur if you try redirecting after outputing HTML to the browser.

The theme_index_top plugin hook at the top of the default theme's index.php file is the perfect spot, so let's add that to our list of plugin hooks:

* hooks: install_plugin, admin_sidebar_plugin_settings, admin_plugin_settings, sb_base_show_post_extra_fields, theme_index_top

First, we'll set up a simple test to make sure the page parameter is being detected properly:

/**
 * Determine if the user has clicked Tweet This
 */
public function theme_index_top($h)
{
    if ($h->isPage('tweet_this')) {
        $this->tweetThisPost($h);
    }
}
 
/**
 * Build the link 
 */
public function tweetThisPost($h)
{
    echo "Testing. Hello, hello, testing";
}

When the theme_index_top hook is triggered, our plugin checks the page parameter, and if it's tweet_this, it calls a tweetThisPost function which outputs some text to the screen.

Reinstall the plugin to register that new hook, then load the front page of your site, click "Tweet This!" and you should see "Testing. Hello, hello, testing" appear at the top of the page.

Now let's change that last function so that it displays the page name and post id that should have been passed to the plugin:

/**
 * Build the link 
 */
public function tweetThisPost($h)
{
    echo "Page: " . $h->cage->get->testAlnumLines('page') . "<br />";
    echo "Post ID: " . $h->cage->get->testInt('id') . "<br />";
}

Give that a test, and if it works, we can move on.

STEP 8 - Compressing the post's url

Change that last function one last time to the following. This will call other functions to compress the post's url, append it to the Twitter status update link and redirect us to Twitter.

/**
 * Build the link 
 */
public function tweetThisPost($h)
{
    // get the post's id from the url
    $post_id = $h->cage->get->testInt('id');
 
    // get the compressed url for this link
    $shortened_url = $this->getShortUrl($h, $post_id);
 
    // add the compressed link to the Twitter status update url
    $twitter_url = $this->getTwitterUrl($h, $shortened_url); 
 
    // redirect to Twitter
    header("Location: " . $twitter_url);
    exit;
}

When a link is compressed, it will get stored in the database. The postmeta table is a suitable place to put it because not every post will be tweeted and we don't want to fill up the main posts table with too many columns if they aren't absolutely necessary. We'll put a compressed address in the database later, but first we need to check if one already exists before we shrink the post's url.

/**
 * Build the shortened link 
 *
 * @param int $post_id
 * @return string $url
 */
public function getShortUrl($h, $post_id)
{
    // Check the database to see if there's already a short link there.
    $query = "SELECT postmeta_value FROM " . TABLE_POSTMETA . " where postmeta_postid = %d AND postmeta_key = %s";
    $sql = $h->db->prepare($query, $post_id, 'compressed_url');
    $stored_short_link = $h->db->get_var($sql);

Immediately after that, add this:

if(!$stored_short_link) {
        // no compressed url in the database. We need to create one...
    } else {
        // compressed url found, let's use it...
        $url = $stored_short_link;
    }
    return trim($url);
}

Hopefully that makes sense. Here's the full function that includes the api calls to the url shortening service to compress the url. You can see that we will need another function for bit.ly because bit.ly needs a login and api key. You should also see that we're putting the newly created url in the database.

/**
 * Build the shortened link 
 *
 * @param int $post_id
 * @return string $url
 */
public function getShortUrl($h, $post_id)
{
     // Check the database to see if there's already a short link there.
     $query = "SELECT postmeta_value FROM " . TABLE_POSTMETA . " where postmeta_postid = %d AND postmeta_key = %s";
     $sql = $h->db->prepare($query, $post_id, 'compressed_url');
     $stored_short_link = $h->db->get_var($sql);
 
     if(!$stored_short_link) {
        // no short link in db. We need to create one:
 
        // get the post's url and encode it:
        $post_url = urlencode($h->url(array('page'=>$post_id)));
 
        // get settings so we know which shortener to use:
        $tweet_this_settings = $h->getSerializedSettings();
 
        switch($tweet_this_settings['tt_shortener']) {
            case 'tinyurl':
                $url = file_get_contents('http://tinyurl.com/api-create.php?url=' . $post_url);
                break;
            case 'bitly':
                $url = $this->getBitlyLink($h, $post_url, $tweet_this_settings);
                break;
            default:
                $url = file_get_contents('http://is.gd/api.php?longurl=' . $post_url);
        }
 
        // then store it in the database:
        $query = "INSERT INTO " . TABLE_POSTMETA . " (postmeta_postid, postmeta_key, postmeta_value, postmeta_updateby) VALUES(%d, %s, %s, %d)";
        $sql = $h->db->prepare($query, $post_id, 'compressed_url', urlencode(trim($url)), $h->current_user->id);
        $h->db->query($sql);
 
    } else {
        // we can use the existing one.
        $url = $stored_short_link;
    }
    return trim($url);
}

In the next post, we'll look at how to use bit.ly.

STEP 9 - Shortening urls with bit.ly

This step should be pretty self-explanatory. We're just getting our login and api key information and including them in the api call to bit.ly.

/**
 * Shorten url with bit.ly
 *
 * @param string $post_url
 * @param array $tweet_this_settings
 * @return string $url
 */
public function getBitlyLink($h, $post_url, $tweet_this_settings)
{
    // get our login and api key from the saved settings
    $bitly_login = $tweet_this_settings['tt_bitly_login'];
    $bitly_apikey = $tweet_this_settings['tt_bitly_api_key'];
 
    // build the api call:
    $api_call = file_get_contents("http://api.bit.ly/shorten?version=2.0.1&longUrl=" . $post_url . "&login=" . $bitly_login . "&apiKey=" . $bitly_apikey);
 
    // get the result of the api call
    $bitlyinfo = json_decode(utf8_encode($api_call),true);
 
    // return the shortened url if no error
    if ($bitlyinfo['errorCode'] == 0) {
        return $bitlyinfo['results'][urldecode($url)]['shortUrl'];
    } else {
        return false;
    }
}

STEP 10 - Include the post title and return the redirect link

Now that we've got our shortened url. The next step is to include it as part of a larger url that we use to redirect us to Twitter. That url will look something like this:

http://twitter.com/home/?status=STORY TITLE AND LINK TO STORY

This is the last function. It takes the post's title, strips it down to a maximum of 100 characters and includes it together with the compressed url. The final url is returned to the tweetThisPost function where we are redirected toTwitter.

/**
 * Build the Twitter status update link
 *
 * @param int $post_id
 * @param string $shortened_url
 * @return string $url
 */
public function getTwitterUrl($h, $post_id, $shortened_url)
{
    // build an object of Post class so we can get the post's title
    require_once(PLUGINS . 'submit/libs/Post.php');
    $h->readPost($post_id);
    $title = html_entity_decode($h->post->title, ENT_QUOTES, "UTF-8");
 
    $orig_length = strlen($title); // get original title length
    if ($orig_length > 110) {
        $title = substr($title, 0, 100); // keep only the first 100 characters
        $title = substr($title, 0, strrpos($title,' ')); // keep everything up to the last space
        $title .= "..."; // adds some dots to show we've truncated it
    }
    $title = $title . " "; // 100 chars + "..." + " " = 104 chars, leaving 36 chars for the url
    $title = urlencode($title);
 
    // The final Twitter URL:
    $url = 'http://twitter.com/home/?status=' . $title . '+' . $shortened_url ;
    return $url;
}

And that is nearly all, but there's one last step...

STEP 11 - Finishing touches

The plugin should be fully working by now, but there are a couple of loose ends to tidy up. First, in the default theme, the links below post descriptions have an image which brightens when hovered over. We can do that with our Tweet This! link, too.

Make a new "css" folder in the tweet_this folder and create a .css file with this code:

/* **************************************
     *     TWEET THIS CSS            *
     ************************************** */
 
    .tweet_this_link    { padding-left: 1.5em; background-image: url(content/plugins/tweet_this/images/tweet-this.png); }

You can see that pulls an image from a folder we don't have yet, so lets make an images folder, too: /tweet_this/images. Here's the image:

Tweet-this.png
In order to include this css file, all we have to do is add another hook, this time header_include, to the top of our tweet_this.php file:

* hooks: install_plugin, admin_sidebar_plugin_settings, admin_plugin_settings, sb_base_show_post_extra_fields, theme_index_top, header_include

Reinstall the plugin so that hook gets registered and view the front page. You may need to hard refresh with CTRL + F5 to make sure the css file gets loaded and merged with all the other css files Hotaru uses.

The last thing to do is make a readme.txt fileto put in your tweet_this folder. It might look something like this:

    Tweet This Plugin for Hotaru CMS
    ---------------------------------
    Created by: Nick Ramsay

    Description
    -----------
    This plugin adds a "Tweet This!" link below a post description, which when clicked, redirects you to your Twitter page with your status prefilled in with the post title and shortened url, pointing back to the post on your Hotaru site. Admins can choose from thre different url shortening services: is.gd (default), TinyUrl, and bit.ly (requires login and API key).

    Instructions
    ------------
    1. Upload the "tweet_this" folder to your plugins folder. Install it from Plugin Management in Admin.
    2. Access it from the Admin sidebar
    3. Choose your preferred url shortening service and enter your login and API key if you choose bit.ly.
    4. Save and view your site. A new "Tweet This!" link should be visible below each post.

    Changelog
    ---------
    v.0.1 2009/11/28 - Nick - Released first version

Well, if you've made it this far, congratulations! You are well on your way to becoming a Hotaru plugin guru.

Note: There is one problem with this plugin. If the post submitter or a moderator edits the post's title, the url will change. That means that if anyone has already tweeted it with the old url, the compressed url will no longer match. It's probably a very rare case, but still a problem nonetheless.

Getting StartedDesign and LayoutPlugin DevelopmentAdvanced TopicsFunction ReferenceTroubleshooting