Easiest Way to Retrieve Twitter Timeline and Hashtags (Twitter OAuth API 1.1)

Written by Kevin Liew on 19 Mar 2013
218,283 Views • Tutorials

Introduction

Twitter terminated its old API, and all of my Twitter tutorials have stopped working! So, here is a follow up to show you how easy is it to retrieve user timeline and hashtag with Twitter REST API 1.1. Of course, I don't just stop there. I integrated my previously written script and modified it to work with Grid-A-Licious plugin to create something that's similar with Pinterest.

Before we start, we need to have all the ingredients ready.

  1. You need to create an application. https://dev.twitter.com/apps.
  2. Once you've done that, you need to get your consumer key, consumer secret, access token and access token secret.
  3. For security reason, Twitter OAuth is done through server side, in this tutorial, we're using PHP. To keep things simple, we are using Twitter Library called CodeBird-PHP.
  4. To make all the tweets we retrieve pretty, we will be using Grid-A-Licious

Once you have all that, we are good to good. This tutorial is a modified version of my previous old Twitter API - Create a Twitter Feed With Hash Tag And Cache Support. However, we won't implement cache control here just to make it easier to understand.

HTML

Alright, first stop. Since all HTML Markup will be generated by jQuery, we only need a div with an id called #jstwitter

<div id="jstwitter"></div>
<div class="item">
{IMG}
    <div class="tweet-wrapper">
        <span class="text">{TEXT}</span>
        <span class="time">
            <a href="{URL}" target="_blank">{AGO}</a>
        </span>
        by 
        <span class="user">{USER}</span>
    </div>
</div>

CSS

I keep CSS really simple, because in the end, I don't think you will be using mine, you probably will style it up according to your website design. So, I spice thing up a little bit. I'm going to create Pinterest style with Grid-A-Licious.

#jstwitter {
    position: relative;
}
#jstwitter .item {
    -webkit-border-radius:5px;
    -moz-border-radius:5px;
    border-radius:5px;
    -webkit-box-shadow:0 0 3px 1px rgba(100,100,100,0.2);
    -moz-box-shadow:0 0 3px 1px rgba(100,100,100,0.2);
    box-shadow:0 0 3px 1px rgba(100,100,100,0.2);
    overflow:hidden;
    background: #fff;
}
#jstwitter .tweet-wrapper {
    padding:10px;
    -webkit-box-sizing:border-box;
    -moz-box-sizing:border-box;
    box-sizing:border-box;
    line-height:16px;
}
#jstwitter .item a {
    text-decoration: none;
    color: #03a8e5;
}
#jstwitter .item img {
    width:100%;
}
#jstwitter .item a:hover {
    text-decoration: underline;
}
#jstwitter .item .text {
    display:block;
}
#jstwitter .item .time, #jstwitter .tweet .user {
    font-style: italic;
    color: #666666;    
}

PHP

I keep it really simple. We're not going to write our own PHP OAuth authentication, we will be using a Twitter Library called CodeBird-PHP. CodeBird makes it ridiculously easy to get authenticated. You may want to read more about it - CodeBird Documentation. As long as you get all the keys, tokens and secrets right, you should have any problems at all.

I have write inline comment in the following PHP script. If you want to add cache control to beat the rate limits in Twitter API. Here is the place you need to modify it.

<?
//We use already made Twitter OAuth library
//https://github.com/mynetx/codebird-php
require_once ('codebird.php');

//Twitter OAuth Settings
$CONSUMER_KEY = '...';
$CONSUMER_SECRET = '...';
$ACCESS_TOKEN = '...';
$ACCESS_TOKEN_SECRET = '...';

//Get authenticated
Codebird::setConsumerKey($CONSUMER_KEY, $CONSUMER_SECRET);
$cb = Codebird::getInstance();
$cb->setToken($ACCESS_TOKEN, $ACCESS_TOKEN_SECRET);

//retrieve posts
$q = $_POST['q'];
$count = $_POST['count'];
$api = $_POST['api'];

//https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline
//https://dev.twitter.com/docs/api/1.1/get/search/tweets
$params = array(
'screen_name' => $q,
'q' => $q,
'count' => $count
);

//Make the REST call
$data = (array) $cb->$api($params);

//Output result in JSON, getting it ready for jQuery to process
echo json_encode($data);
?>

Javascript / jQuery

Lastly, the Javascript. We called the PHP we just created. Depend on the parameters, I have made it so you can either search for hashtag, or load a user timeline. It send the parameters to the PHP file above, and PHP script get us authenticated, and PHP returns Twitter data in JSON format. The following scripts read the data and parse them in to HTML markup.

For more information about the Twitter Object, you can read it here.

$(function() {		
			
JQTWEET = {
     
    // Set twitter hash/user, number of tweets & id/class to append tweets
    // You need to clear tweet-date.txt before toggle between hash and user
    // for multiple hashtags, you can separate the hashtag with OR, eg:
    // hash: '%23jquery OR %23css'			    
    search: '%23heroes2013', //leave this blank if you want to show user's tweet
    user: 'quenesstestacc', //username
    numTweets: 21, //number of tweets
    appendTo: '#jstwitter',
    useGridalicious: true,
    template: '<div class="item">{IMG}<div class="tweet-wrapper"><span class="text">{TEXT}</span>\
               <span class="time"><a href="{URL}" target="_blank">{AGO}</a></span>\
               by <span class="user">{USER}</span></div></div>',
     
    // core function of jqtweet
    // https://dev.twitter.com/docs/using-search
    loadTweets: function() {

        var request;
         
        // different JSON request {hash|user}
        if (JQTWEET.search) {
          request = {
              q: JQTWEET.search,
              count: JQTWEET.numTweets,
              api: 'search_tweets'
          }
        } else {
          request = {
              q: JQTWEET.user,
              count: JQTWEET.numTweets,
              api: 'statuses_userTimeline'
          }
        }

        $.ajax({
            url: 'grabtweets.php',
            type: 'POST',
            dataType: 'json',
            data: request,
            success: function(data, textStatus, xhr) {
	            
	            if (data.httpstatus == 200) {
	            	if (JQTWEET.search) data = data.statuses;

                var text, name, img;	         
                	                
                try {
                  // append tweets into page
                  for (var i = 0; i < JQTWEET.numTweets; i++) {		
                  
                    img = '';
                    url = 'http://twitter.com/' + data[i].user.screen_name + '/status/' + data[i].id_str;
                    try {
                      if (data[i].entities['media']) {
                        img = '<a href="' + url + '" target="_blank"><img data-src="' + data[i].entities['media'][0].media_url + '" /></a>';
                      }
                    } catch (e) {  
                      //no media
                    }
                  
                    $(JQTWEET.appendTo).append( JQTWEET.template.replace('{TEXT}', JQTWEET.ify.clean(data[i].text) )
                        .replace('{USER}', data[i].user.screen_name)
                        .replace('{IMG}', img)                                
                        .replace('{AGO}', JQTWEET.timeAgo(data[i].created_at) )
                        .replace('{URL}', url )			                            
                        );
                  }
                
                } catch (e) {
                  //item is less than item count
                }
                
	             if (JQTWEET.useGridalicious) {                
	                //run grid-a-licious
			$(JQTWEET.appendTo).gridalicious({
				gutter: 13, 
				width: 200, 
				animate: true
			});	                   
		     }                  
                    
               } else alert('no data returned');
             
            }   
 
        });
 
    }, 
     
         
    /**
      * relative time calculator FROM TWITTER
      * @param {string} twitter date string returned from Twitter API
      * @return {string} relative time like "2 minutes ago"
      */
    timeAgo: function(dateString) {
        var rightNow = new Date();
        var then = new Date(dateString);
         
        if ($.browser.msie) {
            // IE can't parse these crazy Ruby dates
            then = Date.parse(dateString.replace(/( \+)/, ' UTC$1'));
        }
 
        var diff = rightNow - then;
 
        var second = 1000,
        minute = second * 60,
        hour = minute * 60,
        day = hour * 24,
        week = day * 7;
 
        if (isNaN(diff) || diff < 0) {
            return ""; // return blank string if unknown
        }
 
        if (diff < second * 2) {
            // within 2 seconds
            return "right now";
        }
 
        if (diff < minute) {
            return Math.floor(diff / second) + " seconds ago";
        }
 
        if (diff < minute * 2) {
            return "about 1 minute ago";
        }
 
        if (diff < hour) {
            return Math.floor(diff / minute) + " minutes ago";
        }
 
        if (diff < hour * 2) {
            return "about 1 hour ago";
        }
 
        if (diff < day) {
            return  Math.floor(diff / hour) + " hours ago";
        }
 
        if (diff > day && diff < day * 2) {
            return "yesterday";
        }
 
        if (diff < day * 365) {
            return Math.floor(diff / day) + " days ago";
        }
 
        else {
            return "over a year ago";
        }
    }, // timeAgo()
     
     
    /**
      * The Twitalinkahashifyer!
      * http://www.dustindiaz.com/basement/ify.html
      * Eg:
      * ify.clean('your tweet text');
      */
    ify:  {
      link: function(tweet) {
        return tweet.replace(/\b(((https*\:\/\/)|www\.)[^\"\']+?)(([!?,.\)]+)?(\s|$))/g, function(link, m1, m2, m3, m4) {
          var http = m2.match(/w/) ? 'http://' : '';
          return '<a class="twtr-hyperlink" target="_blank" href="' + http + m1 + '">' + ((m1.length > 25) ? m1.substr(0, 24) + '...' : m1) + '</a>' + m4;
        });
      },
 
      at: function(tweet) {
        return tweet.replace(/\B[@ï¼ ]([a-zA-Z0-9_]{1,20})/g, function(m, username) {
          return '<a target="_blank" class="twtr-atreply" href="http://twitter.com/intent/user?screen_name=' + username + '">@' + username + '</a>';
        });
      },
 
      list: function(tweet) {
        return tweet.replace(/\B[@ï¼ ]([a-zA-Z0-9_]{1,20}\/\w+)/g, function(m, userlist) {
          return '<a target="_blank" class="twtr-atreply" href="http://twitter.com/' + userlist + '">@' + userlist + '</a>';
        });
      },
 
      hash: function(tweet) {
        return tweet.replace(/(^|\s+)#(\w+)/gi, function(m, before, hash) {
          return before + '<a target="_blank" class="twtr-hashtag" href="http://twitter.com/search?q=%23' + hash + '">#' + hash + '</a>';
        });
      },
 
      clean: function(tweet) {
        return this.hash(this.at(this.list(this.link(tweet))));
      }
    } // ify
 
     
};		

});

And finally, you run the script like this:

$(function () {
    // start jqtweet!
    JQTWEET.loadTweets();
});

Conclusion

That's it my friend. If you have knowledge in both backend and frontend, web development can be easier. We can reuse a lot of scripts, for example, we use CodeBird, Gridalicious, my previous Twitter scripts. What we did right here, we use it, modify it, and bang, we have a working beautiful twitter feed interface.

I hope you will find this tutorial useful, if you have any question, don't hesitate to drop us a comment. :)

Demo Download
Join the discussion

Comments will be moderated and rel="nofollow" will be added to all links. You can wrap your coding with [code][/code] to make use of built-in syntax highlighter.

185 comments
Dave 11 years ago
I'm getting an error 500 on this, but I can't figure out where it's coming from.

All the keys are correct
Reply
Tom 11 years ago
I am too, can you let me know if you fix this ?
Reply
Kevin Admin 11 years ago
I think theres something wrong with your PHP code, make sure you close all quotes and end with semicolon etc. You can comment everything out and enabling them one by one to find out what causing it.
Reply
Hans 11 years ago
I see also my Retweet's. Can i hide that?! let me know. Thanks!
Reply
Damien 11 years ago
Hey Kevin,

I just implemented this within a wordpress theme; however, I am receiving the following php error:

"Fatal error: Class 'Codebird' not found in grabtweets.php on line 14"

line 14:
Codebird::setConsumerKey($CONSUMER_KEY, $CONSUMER_SECRET);


Note: I have also updated codebird.php to the most recent version (2.5.0-dev).

Thoughts?
Reply
kevin Admin 11 years ago
Seems like missing file here. Make sure PHP line 25 - require_once ('codebird.php'); is pointing to the right file.
Reply
Kevin Admin 11 years ago
A solution from Atir Javid.

Update the code with Codebird to:


Codebird\Codebird::setConsumerKey($CONSUMER_KEY, $CONSUMER_SECRET);
$cb = Codebird\Codebird::getInstance();
Reply
Kasia 11 years ago
Hi,
I've created the application in my twitter account, changed the keys in grabtweets.php and the username in the js file. I get no errors and a blank page.
Any ideas, or can you tell me how I could troubleshoot?
Reply
Kevin Admin 11 years ago
Okay, try to run grabtweet.php directly to make sure it's contain no error.

You can also use console.log(data) (chrome and firefox) to check if it returns any data in line 45.
Reply
Jessie 11 years ago
I'm getting a blank page as well. I get these errors when I load up grabtweets.php

( ! ) Notice: Undefined index: q in C:\wamp\www\sandbox\vcard\twitter\grabtweets.php on line 20

( ! ) Notice: Undefined index: count in C:\wamp\www\sandbox\vcard\twitter\grabtweets.php on line 21

( ! ) Notice: Undefined index: api in C:\wamp\www\sandbox\vcard\twitter\grabtweets.php on line 22

( ! ) Fatal error: Method name must be a string in C:\wamp\www\sandbox\vcard\twitter\grabtweets.php on line 33
Reply
Kasia 11 years ago
Thanks Kevin, that's a very obvious troubleshooting suggestion. I feel like such a noob.
I ran grabtweets.php directly and got a parse error (Parse error: syntax error, unexpected T_STRING). It's referring to this line "namespace Codebird;".
I've since found out namespace requires php 5.3. I'm not sure I can upgrade our php so might need to find another solution. Thanks.
Reply
Kevin Admin 11 years ago
That could be it.

@Kasia & @Jessie: The version of PHP you're running could be the reason why it doesn't work. I'm running PHP5.3.6.

@Jessie, you might want to disable PHP error notices.
Reply
Kevin Admin 11 years ago
Hi Kasia, Atir Javid has the solution for your issue:

Update the code to:


Codebird\Codebird::setConsumerKey($CONSUMER_KEY, $CONSUMER_SECRET);
$cb = Codebird\Codebird::getInstance();
Reply
Paddy 11 years ago
I can't seem to get this to work, for the demo is it just a case of downloading it and putting the correct consumer keys etc. into the js file?
Reply
annisa 11 years ago
Hi,
I've created the application in my twitter account, i wanna make program application for get Trending Topic World Wide (TTWW) and count how much tweet on TTWW.
Anybody help me how to count incoming tweets on TTWW ? I'm using PHP 5.2.4 and Twitter API v1.1. Thanks.
Reply
Atir Javid 11 years ago
Hi,

Just wanted to add that this code didn't work for me at all. Upon further inspection of Codebird.php the author has started to use namespaces, and as such your code needs modification.

Codebird\Codebird::setConsumerKey($CONSUMER_KEY, $CONSUMER_SECRET);
$cb = Codebird\Codebird::getInstance();

If you notice, I have prepended "Codebird\" before line #13 and #14.

I hope it helps people who were unable to get it working due to the Fatal Error: class Codebird not found. If you look closely at the error message, it's not about the include, or not being able to find the file, it's about the class not being found. Class can't be found because classpath is incorrect. You must include the namespace in the entire classpath. I hope that helps.
Reply
Kevin Admin 11 years ago
Awesome! Thanks for the tips Atir :)
Reply
Erik 11 years ago
So what should i change in codebird.php?
Reply
Alejo 11 years ago
Awesome api, solves a big problem in my project and added a great visual identity for the community. Thanks a lot
Reply
ArrivalCreative 11 years ago
Hey Kevin-

Awesome post- I was wondering if there is a way to do the same thing through instagrams api. I know you can use instagram to post via twitter, however since the photos are not hosted on twitter it just comes up with a post and link rather than a photo.
Reply
Sean Lang 11 years ago
How do i get this to show the profile image next to the users tweets?
Reply
Tatters 11 years ago
This would be good...I tried this but it's coming up as 'undefined'

.replace('{USER}', data[i].user.screen_name)
.replace('{PHO}', data[i].user.profile_image)
.replace('{IMG}', img)
.replace('{AGO}', JQTWEET.timeAgo(data[i].created_at) )
.replace('{URL}', url )
Reply
Kevin Admin 11 years ago
Should be this. I haven't tested it though.


.replace('{PHO}', data[i].user.profile_image_url)

Reply
Tatters 11 years ago
Hi Kevin, Sorry to bombard you with questions.

It worked, however when I tried to add a link (href) to the profile {PHO} thumbnail it stopped working, see code below:

I had to include another CSS div (items) as the profile thumbnail was stretching to 100% of the item div. Also how would you align the profile image and user name inline on this code?


template: '<div class="item">{IMG}<div class="tweet-wrapper"><span class="user"><div class="items"><img href="{PHO}" src="{PHO}" /></span><span class="user">{USER}</span></div><span class="text">{TEXT}</span>\
<span class="time"><a href="{URL}" target="_blank">{AGO}</a></span>\
</div></div>',


I've also hit the rate limit for the API, how do I get around this?

Thanks for all your help
Reply
Kevin Admin 11 years ago
Hi Tatters, to create a link, you need to wrap the image with A tag. Should look like this:


template: '<div class="item">{IMG}<div class="tweet-wrapper"><span class="user"><div class="items"><a href="http://www.twitter.com/{PRO_URL}"><img src="{PHO}" /></a></span><span class="user">{USER}</span></div><span class="text">{TEXT}</span>\
<span class="time"><a href="{URL}" target="_blank">{AGO}</a></span>\
</div></div>',


And add this two replaces:


.replace('{PHO}', data[i].user.profile_image_url)
.replace('{PHO_URL}', data[i].user.screen_name)
Reply
Kevin Admin 11 years ago
For avoiding rate limit, please go to page 4 of this tutorial's comments.

Look for Jonas' answer posted on Mon, 17th June 2013.
Reply
Tatters 11 years ago
Hi, is it also possible to search for a hashtag from an input/text area box (like a standard search on a webpage) instead of hardcoding the search into the jquery.jstwitter.js file? Great code by the way. Thanks
Reply
Matt 11 years ago
Hi Kevin,
I've done everything I can possibly think to try and get this to work but to no avail. Am I right in thinking that jquery.jstwitter.js must be located in the same folder as codebird.php and grabtweets.php?

I'm running php 5.3.25 http://www.mdwoodman.co.uk/phpinfo.php and cannot seem to get anything but error500's when attempting to run www.mdwoodman.co.uk/grabtweets.php

Any help would be hugely appreciated!
Thanks
Reply
Kevin Admin 11 years ago
Yes, all the files should be in the same folder as it is when you unzip the demo.

You can go to page 4 and read previous comment, apparently if you are using PHP5.3 you need to modify the following line (add slashes):


Codebird\Codebird::setConsumerKey($CONSUMER_KEY, $CONSUMER_SECRET);
$cb = Codebird\Codebird::getInstance();
Reply
Matt 11 years ago
Hi Kevin, thanks for the quick reply. I have already made an amendment to lines 01 and 02 with your previous suggestion and still couldn't get the code to work. So I contacted the help desk of my web host and they are telling me that according to the error log there is a PHP fatal error on this line of grabtweets.php:

$data = (array) $cb->$api($params);

I have no experience with php so wouldn't know where to begin as far as correcting that code, do you have any suggestions? Thanks,
Matt
Reply
Matt 11 years ago
Here's the error code if it helps!

PHP Fatal error: Method name must be a string in /var/www/vhosts/mdwoodman.co.uk/httpdocs/grabtweets.php on line 33
Reply
Kevin Admin 11 years ago
I have no ideas what's wrong as well.

Try to comment line33 out see if it still throw an error and start debugging it from there.
Reply
Pat 11 years ago
i got the same problem in
$data = (array) $cb->$api($params);

it seems that it is not allowed to use $api as the name of the function.
Any workaround of $api?
Reply
Matt 11 years ago
Seconding Pat's point, the $api call seems to be causing the problem. Any ideas of a work around would be gratefully received, I'd love to get this working!
Matt
Reply
Tan Nguyen 11 years ago
Is there a way to stream multiple feeds?
Reply