Tag Archives: Bitly

Sharing feedback with Twitter using Bit.ly – Part 2

In our previous installment we looked at setting our backend up so it could automatically retrieve the bit.ly URL for a given URL and store it as part of the data for a given instance of a Content model.  What we're going to do this time is take a look at the front end components.

Sometimes I find that doing things backwards can actually make things a little more clear.  That way you can see the end result and then, as you work backwards, see how all the pieces work together.

With that in mind, let's start with our view code, since that's the most important part of the whole thing.  The first thing we are going to do is define our HTML.

<div id="sliderMessage">Message is previewed before it is sent</div>
<div id="sliderContainer">
<div id="slider" style="width: 80%; margin: auto;"></div>
<div style="width: 191px; margin-right: 40px;">
<div id="customTwitterMessage">
<textarea id="twitterMessage" name="twitterMessage"></textarea>
<font size="1">(Maximum characters: 140)
You have characters left.</font>
 </div>
<div id="kudoTweetButton">
<a href="" target="_blank"><span style="color: white;">Tweet!</span></a></div>
<div class="kudos" id="slider-5" style="display: block;">Great Post!</div>
<div class="kudos" id="slider-4">Good Post</div>
<div class="kudos" id="slider-3">Decent Post</div>
<div class="kudos" id="slider-2">Didn't Like</div>
<div class="kudos" id="slider-1">Not Good</div>
</div>
</div>
<div id="sliderThanks">Thanks!</div>

There are a few elements in here.  The first is the slider with the ID of "slider".  The slider allows you to choose how high you want to rate the individual posting.  After that we have some code for writing custom Twitter messages if the review is really low.  It has the requisite 140 character limitation on it.  That is relatively simple to do, so I won't go into counting the characters.

Below that is the Tweet button. It floats to the right, so it is printed before our ratings.  After that are DIV tags that contain the individual messages.  They all have an ID that corresponds to the value of the slider and are all hidden, to start out with, except for "slider-5".  As the slider moves, each box will be displayed.

Rating the post

We have a couple of page-specific JavaScript variables that we need to have.  None of them are "required" to do this, but they are what makes it a little more automated.  All of the view script values are set in a controller.

var currentSlider = 5;
var twitterUser = "<php echo $this->twitterUser ?>";
var bitLy = "<php echo $this->content->getBitly() ?>";
$tags = array();
foreach ($this->content->getTags() as $t) {
    $t = (string) $t;
    $tags[] = '#' . preg_replace('/[W_]/', '', $t);
}
?>
var contentTags = ;
var twitterText = "";

currentSlider is the default value for the rating.  twitterUser is for if you rate a posting badly you can mention the Twitter user instead of just saying it sucked.  In other wordsd, it gives them a chance to redeem themselves.  bitLy is the variable that contains the bit.ly URL that we had before.  After that we echo out all of the tags that we have, but making them a little friendlier to Twitter but removing an non-white space and the underscore, since tags on Twitter generally don't have underscores.  It also adds the hash on the front of each tag.  They are then rendered as JSON because that's the easiest way to pass the information to the JavaScript in the view.  twitterText contains the full message that will be sent.

Speaking of twitterText we need to be able to set it.  That is done via the writeNormalTwitterMessage() function.  Is there an "abnormal" Twitter message?  Yep, but we'll look at that later.

function writeNormalTwitterMessage()
{
    $("#customTwitterMessage").hide();
    count = 0;
    twitterText = $("#slider-" + currentSlider).text() + " " + bitLy + " ";
    while (twitterText.length < 140 && count < contentTags.length

           && twitterText.length + contentTags[count].length < 140) {
        twitterText += " " + contentTags[count++];
    }
    twitterText = escape(twitterText);
}

Because this function is only called when the slider is moved, the custom message box is first hidden.  It is only used for non-kudos.  Then it takes the value of the currently selected DIV element and starts the string with that value, appending the bit.ly value to the end of it.  Then it iterates over a loop, adding the tags that we had created previously until we reach the 140 character limit or run out of tags.  Then we escape that value and store it on the twitterText varialbe.

Now we have to implement the functionality in the slider so that when we slide it, it is able to actually set the message in the function we had just defined.

if (twitterUser && bitLy) {
    $("#slider").slider({
        min: 1,
        max: 5,
        value:5,
        slide: function(event, ui) {
            $("#slider-" + currentSlider).hide();
            $("#slider-" + ui.value).show();
            currentSlider = ui.value;
            if (ui.value >= 3) {
                writeNormalTwitterMessage();    
            } else {
                if (ui.value == 1 ) {
                    $("#twitterMessage").val("@" + twitterUser + " " + bitLy + " wasn't good because ");
                } else {
                    $("#twitterMessage").val("@" + twitterUser + " I didn't like " + bitLy + " because ");
                }
                setTwitterMessageLength();
                $("#customTwitterMessage").show();
            }
        }
    });
    writeNormalTwitterMessage();
}

It looks like a bunch of code, but it's not.  What we do is bind to the slider and use some JSON to configure it.  We set the min as 1, the max as 5 and the default value as 5, or fully awesome kudos.  Then, for the slide event we define our functionality.  We first hide the previous slider caption DIV and then show the new one, resetting the previous value for the new one so we can hide it when we slide it again.  Then we check the value of the slider that was passed.  If it is greater or equal to 3 then the author did a good job and all we want to do is post the kudos.  If the value is 2 or 1, we want to give the author the chance to redeem him, or herself.  So we set it to give you  a text box.

The last thing to do from this side is to actually submit the text.  However, Twitter, for very good reasons, does not allow a web page to kick off some JavaScript and post a status update.  Otherwise you'd be seeing Twitter accounts being used as a spambot the likes of which you have never seen.  You could do it via an API, but your blog post isn't so important that someone will grant your website permission to do anything for them. So, to post this to Twitter, rather than using a form, we simply present a URL to be clicked on.  And the way we present that is via this code.

$("#kudoTweetButton").mouseover(function(){
    url = "http:///home?status="+twitterText;
    $("#kudoTweetButton a").attr("href", url);
});

What this does is set the href attribute to our twitterText value so that when the user clicks on it they will be brought to the Twitter page with kudos pre-populated.  It will look something like this.

Kudos post

The Twitter user then clicks "update" and the kudo is delivered.

Try it yourself a little bit and see what you think.

New changes made to the site

Well another week, another set of changes.  There are 4 primary changes that I've made to the site since last week.  They are, in no particular order

  1. Email subscriptions
  2. The addition of comments.
  3. A Twitter-based rating widget
  4. Related links

Related Links

The first is related links.  What it basically does is allow me to enter in links that I think might be pertinent to various articles on this site.  Each link can be tagged and any place where an article is displayed that has the same tags the related links will be displayed.  But it doesn't end there.  When I submit a link I make a request off to bit.ly to get the short URL for it.  This allows you to share that URL easily over Twitter or Facebook.  But my purpose is actually tracking.  Bit.ly tracks individual URLs according to who submitted them, not just based off of the URL.  So what that allows me to do is see how many times someone went to a given page because of me.  Perhaps it's narcissistic, but this is the web where narcissism abounds.

But because I am depending on a third party web service I don't want to be handling errors or slow web service requests in my main web request.  If there's a timeout or something on the web service end it could end up timing out on the browser end and I don't want that.  To solve that problem I used, TADA!, the Job Queue.  I swear, after having implemented my earlier task system I have gone Job Queue crazy.  The code for making this call is

class Admin_Task_InsertLink extends Esc_Queue_TaskAbstract
{
    
    private $_link;
    private $_tags;
    
    public function __construct($link, $tags)
    {
        $this->_link = $link;
        $this->_tags = $tags;
    }
    
    public function _execute(Zend_Application $app)
    {
        $dom = new DOMDocument();
        $dom->loadHTML(file_get_contents($this->_link));
        $xpath = new DOMXPath($dom);
        $ns = $xpath->query('/html/head/title');
        if ($ns->length) {
            $title = $ns->item(0);
            if (!$title) {
                return;
            }
            $title = $title->nodeValue;
            $api = new Esc_Api_Bitly($app->getOption('bitly'));
            
            $lt = new Model_DbTable_Link();
            $l = $lt->fetchNew();
            $l->setBitly($api->getShortUrl($this->_link));
            $l->setTitle($title);
            $l->save();
            

... Plus a bunch of stuff for saving the tags
           
        }
    }
    
}

The API class is defined as
 

class Esc_Api_Bitly
{
    private $_options = array();
    
    public function __construct(array $options)
    {
        if (!isset($options['login']) || !isset($options['key']) ) {
            throw new Esc_ApiException('login and key are required options');
        }
        $this->_options = $options;
    }
    
    public function getShortUrl($url)
    {
        $url = 'http://api.bit.ly/shorten?version=2.0.1&longUrl='
               . urlencode($url)
               . '&login='
               . $this->_options['login']
               . '&apiKey='
               . $this->_options['key']
               . '&format=json';
        $results = json_decode(
            file_get_contents($url),
            true
        );
        if (!$results['errorCode']) {
            $res = array_shift($results['results']);
            return $res['shortUrl'];
        }
        return null;
    }
}

I found myself using the bit.ly API all over the place so I just created a simple API class that I can re-use.

So when I post a link I simply invoke the code

$ilTask = new Admin_Task_InsertLink(
    $this->_request->getPost('link'),
    $this->_request->getPost('tags')
);
$ilTask->execute($this->getInvokeArg('bootstrap')->getApplication());

It sends it off to the Job Queue and does all the necessary processing offline.

Twitter based rating widget

If you look at the top part of the side bar you will see a slider.

Rating with a tweet

When you tweet it will open up a new window with the shortened URL and the tags all set up.

Twitter output

Since I will be doing another post on this later on, that's the extent of the details I will provide at this time.  Do, however, use it.   But be kind.

Comments

Comments are easy.  Comments with @#^$@#$^ CSS is hard.  If you notice, all of the comments have a "notify".  So basically, if you make a comment, after a comment has been approved, an email is going to be sent out to everyone who asked to be notified.  But do we want to wait while all of the commenters are notified?  Of course not!  Job Queue!!

class Admin_Task_SendArticleCommentNotification extends  Esc_Queue_TaskAbstract
{
    private $_commentKey;
    
    public function __construct($commentKey)
    {
        $this->_commentKey = $commentKey;
    }
    
    protected function _execute (Zend_Application $app)
    {
        $ct = new Model_DbTable_Comment();
        $c = $ct->find($this->_commentKey)->current();
        if ($c) {
            $options = $app->getOption('site');
            if (!$options['baseurl']) return;
            /* @var $c Model_Comment */
            $content = $c->getContent();
            /* @var $content Model_Content */
            $mail = new Zend_Mail();
            $link = $content->getPageId();
            $title = $content->getTitle();
            $mail->setSubject('New comment: ' . $title);
            $text = $c->getText();
            $text = "

A new comment has been made

 

{$title}

 

{$text}


Read more at {$title}
            
";
            $mail->setBodyHtml(
                $text
            );
            $mail->setFrom($options['email'], $options['title']);
            $subRs = $content->getCommentsWithNotifications();
            $addr = array();
            foreach ($subRs as $sub) {
                /* @var $sub Model_Comment */
                $addr[$sub->getEmail()] = 1;
            }
            $thisEmail = $c->getEmail();
            foreach (array_keys($addr) as $addr) {
                if ($addr == $thisEmail) continue;
                $email = clone $mail;
                $email->addTo($addr);
                $email->send();
            }
        }
    } 
}

If you get email notifications don't worry.  All of the text is filtered.

Subscriptions

Last but not least, some people like to get emailed when a new post is made instead of getting it with Atom.  I'm cool with that.  So I added the ability to subscribe to the site via email.  So as soon as a new post goes out it will be sent to everyone who is a subscriber.  Once again; Job Queue!!!  In fact, one of the reasons for this post is to test this functionality out.  I've already tested it in dev and staging but now production.

class Admin_Task_SendNewArticleNotification extends  Esc_Queue_TaskAbstract
{
    private $_contentKey;
    
    public function __construct($contentKey)
    {
        $this->_contentKey = $contentKey;
    }
    
    protected function _execute (Zend_Application $app)
    {
        $ct = new Model_DbTable_Content();
        $c = $ct->find($this->_contentKey)->current();
        if ($c) {
            $options = $app->getOption('site');
            if (!$options['baseurl']) return;
            /* @var $c Model_Content */
            $mail = new Zend_Mail();
            $link = $c->getPageId();
            $title = $c->getTitle();
            $mail->setSubject('New post: ' . $title);
            $text = $c->getContentSnip(2048);
            $text = "

A new posting has been made

 

{$title}

 

Article Preview

 

{$text}


Read more at {$title}
            
";
            $mail->setBodyHtml(
                $text
            );
            $mail->setFrom($options['email'], $options['title']);
            $st = new Model_DbTable_Subscriber();
            $subRs = $st->fetchAll();
            foreach ($subRs as $sub) {
                /* @var $sub Model_Subscriber */
                $email = clone $mail;
                $email->addTo($sub->getEmail());
                $email->send();
            }
        }
    }
}

Whew!  That's a lot of stuff.  Plus, it's relatively new.  So there might be some bugs in it.  But even so it was fun to build and more fun will be had in the near future.

Sharing feedback with Twitter using Bit.ly – Part 1

Over here there is a good article on sharing page feedback on Twitter.  I'll end up doing something similar but in a different manner.  A little while I posted an article (Do you queue?) on how you could use the Zend Server Job Queue to run individual tasks.  Well you can do the same thing here.  I've made a few changes since that article, namely that Zend_Application is passed in both to execute() and to run() so I could easily retrieve application settings.  Don't know why I didn't think of that earlier.  Oh well.

The way this is different is that I want to pre-populate the bit.ly URL for an individual page so I only have to make one call to bit.ly.  This is because I don't want to slow the performance of my web page down when someone is viewing the page.  Additionally, I don't want to have a time out when I submit a page.  Therefore I need to create a task.

class Admin_Task_Bitly extends Esc_Queue_TaskAbstract
{

    private $_contentKey;
    
    public function __construct($contentKey)
    {
        $this->_contentKey = $contentKey;
    }
    
    protected function _execute (Zend_Application $app)
    {
        $ct = new Model_DbTable_Content();
        $c = $ct->find($this->_contentKey)->current();
        if (!$c) return;
        // Don't need to do it again
        if ($c->getBitly()) return;
        $options = $app->getOption('bitly');
        $url = 'http://api.bit.ly/shorten?version=2.0.1&longUrl='
               . urlencode(
                       $options['url']
                    . $c->getPageId())
               . '&login='
               . $options['login']
               . '&apiKey='
               . $options['key']
               . '&format=json';
        $results = json_decode(
            file_get_contents($url),
            true
        );
        if (!$results['errorCode']) {
            $res = array_shift($results['results']);
            $c->setBitly($res['shortUrl']);
            $c->save();
        }
    }
}

What this code does is take the primary key of the content being shortened and stores it for serialization when being passed to the Job Queue.  Then when the Job Queue executes it will get a new instance of the object, check to see if it already has a bit.ly URL and then build the URL, sending it to the bit.ly API servers.  When it gets the response back it checks to see if it's an error and if not, saves the result to the database for later usage.

But we're not quite there yet.  We still have some application.ini settings to set.

bitly.login = "xxxxxxxxxx"
bitly.url = http://www.eschrade.com/page/
bitly.key = "xxxxxxxxxxxxxx"

… not that some of those are hard to find out.  And then we need to add the task to the code that saves the article after I have finished working on it.

$mod->save();
$task = new Admin_Task_Bitly($mod->getId());
$task->execute($this->getInvokeArg('bootstrap')->getApplication());

That's it, from the functional perspective.  All that's left to do now is test it.

class Admin_Task_BitlyTest extends PHPUnit_Framework_TestCase
{
 
    public function testGetBitly()
    {
        $ct  = new Model_DbTable_Content();
        $c = $ct->fetchAll()->current();
        /* @var $c Model_Content */
        $id = $c->getId();
        $c->setBitly('');
        $c->save();
        
        $task = new Admin_Task_Bitly($c->getId());
        $task->run(Zend_Registry::get('Zend_Application'));
        
        $c = $ct->find($id)->current();
        $this->assertNotEquals(
            '',
            $c->getBitly()
        );
        
    }
}

And we're done.  Except for the actual part about posting to Twitter.  But we'll do that in a bit.

Web Analytics