In this multi-phase project, you will build a client that interacts with the Twitter messaging service. Your client will both mimic functionality found through the twitter.com web interface as well as perform many new tasks.
Learning Goals:
If you haven’t already setup Ruby, visit http://jumpstartlab.com/resources/general/environment/ for instructions.
If you’re on Mac OS X, you can go to your command line and enter this
sudo gem install twitter
The latest versions of the twitter gem are not compatible with Windows, though, so Win users need a slightly older version. Use this instruction instead:
gem install twitter --version '= 0.6.15'
Test your installation by starting an IRB terminal and entering…
require 'rubygems'
require 'twitter'
username = "your_testing_account_username"
password = "your_testing_account_password"
auth = Twitter::HTTPAuth.new(username,password)
client = Twitter::Base.new(auth)
client.update("Sample tweet from JSTwitter project by (Your Name)")
You should then see a return value that starts like this:
=> <Mash created_at="Sat..."
Visit http://twitter.com/jsl_demo and look for your message
Next open RubyMine and…
Enter the code below into your new class file
require 'rubygems'
require 'twitter'
class JSTwitter
attr_accessor :client
def initialize(username, password)
puts "Initializing"
end
end
jst = JSTwitter.new("your_testing_account_username","your_testing_account_password")
Run your code and you should see some output like this:
Initializing Process finished with exit code 0
Posting tweets is pretty easy, but first we’ll have to create an authenticated connection to Twitter.
Inside the initialize method, add the following instructions below the puts statement:
auth = Twitter::HTTPAuth.new(username, password) @client = Twitter::Base.new(auth)
The auth line is setting up the HTTP-based authentication to your account. The @client line is creating the object that you’ll use to interact with Twitter.
tweet MethodNow add the the following method to your class:
def tweet(message) @client.update(message) end
Below the entire class, write the following:
jst.tweet("JS Twitter Initialized")
Then run your code by going to the RUN menu and clicking RUN. In the output window you should just see the line Initializing. Now go to http://twitter.com/your_testing_account_username and look for your
results!
Twitter messages are limited to 140 characters. Experiment with your current program and see what happens if your try to call tweet with a message longer than 140. Let’s create some error checking that will prevent the user from posting messages longer than 140.
Inside your tweet method, write an if/else block that performs the following logic:
Test your new tweet method with a message that is less than 140 characters, one that is exactly 140 characters, and one that’s longer than 140 characters.
How do you get a string that’s exactly 140 characters? Here’s how I did it in irb:
text = "" until text.length == 140 text = text + "asdf" end puts text
Our client is off to a good start, but the interface sucks. We have to change lines in the Ruby file for each tweet we want to send — that’s not reasonable!
Let’s build a simple prompt/command interface to run our program.
First, let’s define a method named run which will be the instruction that gets repeated over and over:
def run
puts "Welcome to the JSL Twitter Client!"
end
Then go to the last line of your program which currently says something like jsl.tweet("JSTwitter Initialized") and change it to jsl.run.
Run your program at the command line with ruby js_twitter.rb and you should just see the line “Welcome to the JSL Twitter Client!”
Underneath the puts line we’ll use a while loop to repeat instructions over and over. Add these lines below the puts but before the end:
command = ""
while command != "q"
printf "enter command: "
end
Now before you run that, remember that you can exit a running Ruby application by pressing Control-C.
Go ahead an run that program and you should see an infinite repetition of the "enter command: " string. The while loop will keep repeating until the variable command contains the value "q". Since we set command to the empty string and aren’t changing it, the loop continues forever.
Also, you might wonder what printf is about. Why not puts? The difference is that printf prints text and leaves the cursor there, while puts prints text then starts a new line. For our interface, we’ll have the prompt and the command on the same line.
In Ruby we can accept user string with the gets command. Add this line below your printf:
command = gets
Now run your program again and it’ll wait for you to enter commands. Try typing some things in. Try entering a q to quit.
It doesn’t work, right? There’s a little gotcha with using gets — it picks up the enter key too. So your command variable is actually getting the string "q\n" where that backslash-n is a new line. The fix is to change gets to gets.chomp. The chomp method will remove any whitespace (spaces, tabs, newlines) on either the front or back ends of a string.
After you add the chomp try your program again and you should be able to quit with just q.
We think we’re getting the instruction from the user, but we need to actually do something with it. We’ll use what’s called a case statement. Case statements in Ruby look like this:
case input
when "a" then puts "It's an A!"
when "b" then puts "It's a B!"
when "c" then puts "It's a C!"
else
puts "It's something else!"
end
end
Ruby will look at the variable input and see what value it holds. If the value matches one of the when lines, then it’ll do the instruction(s) that follow that line’s then. If it doesn’t match and of the when lines, it’ll run the else.
Start with this case statement in your method just below the command = gets.chomp line:
case command
when 'q' then puts "Goodbye!"
else
puts "Sorry, I don't know how to #{command}"
end
end
Run your program and test some commands.
Let’s make this thing work for our tweet method. Add a when line that is run when the command is "t". Have it call our tweet method.
Run your program and try entering t This is only a test!.
You should see output like Sorry, I don't know how to (t This is only a test!). I wanted it to call the tweet method because I started the line with t, but then the rest of the line was my message. Instead, it thought the whole line was the command. We need to divide up the input between the command and the text that should be sent to that command.
There are a few ways we could accomplish this, but we’ll use the most straightforward method.
The line command = gets.chomp is kind of telling a lie. It isn’t just getting the command, it’s getting a command and a message to send to that command. Lets change this line to input = gets.chomp then we’ll work with input to pull out the command.
Now that we have input we need to split it up into pieces. We’ll cut it up using the split method. Just below the input = gets.chomp add a line that says parts = input.split(" ") to chop input into parts.
We know that the first element in the parts array is our command, so let’s pull it out by saying command = parts[0].
Then what do we do with the rest of the parts? They’re our message. Our tweet method is expecting to be passed in a message, so we need to reassemble the message and add it to our when line. In order to get the whole message I’ll grab parts[1..-1] which gives me all the words in the message from index 1 to the end of the array (-1). That basically just skips the command that’s in parts[0].
But those parts[1..-1] are individual word strings, I need to join them into a single string. We can use the join method and tell it to connect the words with a space like this: parts[1..-1].join(" "). Using that idea in the when line for t, here’s what my method looks like right now:
def run
command = ""
while command != "q"
puts ""
printf "enter command: "
input = gets.chomp
parts = input.split
command = parts[0]
case command
when 'q' then puts "Goodbye!"
when 't' then tweet #tweet(parts[1..-1].join(" "))
else
puts "Sorry, I don't know how to (#{command})"
end
end
end
Try it out and you should finally be able to post tweets over and over!
Sending Direct Messages isn’t that different from posting a tweet. In fact, we can reuse our existing tweet method to do all the hard work.
def dm(target, message)
puts "Trying to send #{target} this direct message:"
puts message
end
And we need to add the command to our run method. We’ll enter the instruction like dm jumpstartlab Here is the text of the DM, so our when line should look like this:
when 'dm' then dm(parts[1], parts[2..-1].join(" "))
Remember that parts[0] is the command itself, here dm. Then parts[1] will be the target username, here jumpstartlab. Then everything else is the message, so we join them with spaces parts[2..-1].join(" ").
First, inside your dm method, create a new string that is a combination of “d”, a space, the target username, a space, then the message.
Then call the tweet method with this new string as the parameter message.
Try sending a DM to your personal Twitter account. Try sending yourself a DM. If the DM doesn’t show up it is probably because you can only send DMs to people who are following you. Start following your your_testing_account_username account from your personal account and try it again.
You can only DM people who are following you. As you saw in Step 1, if you try and DM someone else, though, it doesn’t give you an error message. It just fails silently.
Let’s add a way to verify that the target is following you before sending the message. In pseudo-code, it’d go something like this:
We can call @client.followers which gives us back a list of all our followers but includes lots of information we don’t need right now like their follower count, web address, last tweet. All we want is to find their screen_name. When you call client.followers you get an array where each element in the array is a hash. Each hash is for one user and has several keys:
["profile_background_tile", "name", "profile_sidebar_fill_color", "profile_sidebar_border_color", "created_at", "profile_image_url", "location", "profile_link_color", "url", "favourites_count", "utc_offset", "id", "profile_text_color", "followers_count", "protected", "notifications", "description", "verified", "profile_background_color", "time_zone", "profile_background_image_url", "friends_count", "status", "statuses_count", "following", "screen_name"]
What we need to do is pull out just the screen_name. We create an array of the followers’ screen names with this line of code:
screen_names = @client.followers.collect{|follower| follower.screen_name}
To read this line out loud it would be like "Call the followers method of @client, then take that array and, for each element in the array, collect together the value of screen_name.
Now you have an array of your followers’ screen names. Create a conditional block that follows this logic:
target username is in the screen_names list (use the .include? method), then send the DMTest your code by sending a DM to someone who does follow your demo account and someone who does not.
It would be cool to be able to send the same message out to all of our followers. We’ll accomplish this in two parts:
followers_list that returns an array containing the usernames of all my followersspam_my_friends that finds all followers from followers_list and tries to send them a Direct Message using the dm methodTo create the followers_list method…
followers_list with no parametersscreen_namesfollowers method of @client and iterate through each of them performing the instruction below:screen_names << follower["screen_name"]
screen_namesThen for the spam_my_followers method…
spam_my_followers with a parameter named messagefollowers_list methodeach of those followers and use your dm method to send them the messageThen create a when line in your run method for the command spam. It will look just like the tweet line, except it’ll send the message into the spam_my_friends method.
Test it out and see how many followers you can annoy at once!
So now you can post tweets and DMs. There are hundreds of clients that can do that. If you’re a normal twitter user you follow some people who post several times per day and some people who post once per week. It can be difficult to see everyone. Lets create a list of the last tweet for each person you follow.
Here it is in pseudocode:
each member of the list…
screen_name and latest tweetTurn that into code like this…
def everyones_last_tweet
friends = @client.friends
friends.each do |friend|
# find each friends last message
# print each friend's screen_name
# print each friend's last message
puts "" # Just print a blank line to separate people
end
end
Add a when line to your run method for this instruction. I’m using the instruction elt so my when line is just when 'elt' then everyones_last_tweet. Once added, restart your program and try it out.
When you call the friends method you get an array list where each element of the array is itself a hash. The hash has all the information about an individual friend such as :screen_name, :id, :follower_count, etc. Here are the useful names (or keys) that it has:
["created_at", "description", "favourites_count", "followers_count", "following", "friends_count", "id", "location", "name", "notifications", "profile_image_url", "protected", "screen_name", "status", "statuses_count", "time_zone", "url"]
So for each of those friend objects, if you call friend.followers_count you’ll get their number of followers. Or use friend.id to get their unique Twitter ID number.
status contains their last tweet, but there’s a catch — status is ANOTHER hash. The status hash has these keys:
["created_at", "favorited", "id", "in_reply_to_screen_name", "in_reply_to_status_id", "in_reply_to_user_id", "source", "text", "truncated"]
So if you want to access one of these pieces of data you’d call it like this: friend.status.created_at or friend.status.source.
Now that you understand the hashes available to you, implement code for the three commented lines in our everyones_last_tweet method. RUN your program and you should see output kinda like this:
JSTwitter Initialized rsturim said... the raw bar is open amyhoy said... along with our candlelit dinner in the garden, charming brass music drifted over the hills from nearby. as if it were just for us. wonderwillow said... @ChrisMacDen fab idea. Do you know of good resources?
Getting each friend’s last message was cool, but they’re in some random order. Sort them by the screen_name in alphabetical order! I want you to hack out the code, but the way I did it would read like this:
“take the friends list and use the sort_by method, then call each one friend and find the friend.screen_name”. You might look at how you used sort_by in JSAttend for syntax clues. (NOTE: Ruby considers all capital letters to come earlier in alphabetical order than lowercase letters. To keep all your letters together regardless of capitalization, change friend.screen_name to friend.screen_name.downcase when sorting)
Second, these messages are lacking any time context. The status hash has a key named created_at which holds a string like this one: Thu Jul 23 23:31:16 +0000 2009. That’s the information we need, but it’s in an ugly format. Use these steps to make the data more useable:
timestamp = friend.status.created_at
tweet_date = Date.parse(timestamp)
#Then, when you want to print the date, it can be formatted like this:
tweet_date.strftime("%A, %b %d")
strftime is my most hated method in Ruby because every time I use it I need to lookup the dumb parameters. The "%A, %b %d" that I gave you will cause it to output the date formatted like Wednesday, Jul 29. Implement the sorting and the timestamping to create output that looks like this:
JSTwitter Initialized abailin said this on Wednesday, Jul 29... RT @JackiMieler: @abailin Amen! Be sure to spread the word about the facts of this story - http://tinyurl.com/mt3gfx - vs. the fictitous AlexSenn said this on Tuesday, Jan 13... Is looking for a cute condo in Atlanta...2 bedroom, 2 bathroom...any suggestions, let me know! americasvoice said this on Wednesday, Jul 29... Frank Sharry on Huffington Post: GOP Latino Outreach Strategy: Oppose, Ignore, Aggravate and Scapegoat - http://bit.ly/1jbpxo #topptog #tcot amyhoy said this on Wednesday, Jul 29... along with our candlelit dinner in the garden, charming brass music drifted over the hills from nearby. as if it were just for us.
There’s a great library which can be used to automatically create shortened URLs. Let’s add this functionality into our project.
First, go into Terminal and run this line:
sudo gem install shorturl
Next, open irb and try out the following:
require 'rubygems'
require 'shorturl'
puts ShortURL.shorten('http://jumpstartlab.com/courses/ruby/')
It might take a few seconds, but you should now have a shortened URL from RubyURL.com. Try it out in your browser to make sure it works.
Create this method:
def shorten(original_url)
# Shortening Code
puts "Shortening this URL: #{original_url}"
end
Add a when line to your run method so that the command s will take one parameter and send it into the shorten method.
Look at the model for ShortURL that we used in Step 0 and use it to fill in the shortening code of your shorten method. Make sure that your method ends with a return statement so it send the shortened URL that called it.
How can we shorten a url while posting a tweet? There are a few ways to do it. Here’s an easy one:
Add a when line in your run method for the command turl which stands for “Tweet with URL”. Make it accept commands that look like this:
turl http://jumpstartlab.com I wrote this twitter client at:
You know that parts[0] is the command, parts[1] is the URL to be shortened, and parts[2..-1] are the message. You can put that all together like this:
tweet(parts[2..-1].join(" ") + " " + shorten(parts[1]))
Get that working and you’re done with the twitter client!