# This is the base class for the twitter library. It makes all the requests
# to twitter, parses the xml (using hpricot) and returns ruby objects to play with.
#
# For complete documentation on the options, check out the twitter api docs.
# http://groups.google.com/group/twitter-development-talk/web/api-documentation
module Twitter
class Base
# Initializes the configuration for making requests to twitter
# Twitter example:
# Twitter.new('email/username', 'password')
#
# Identi.ca example:
# Twitter.new('email/username', 'password', :api_host => 'identi.ca/api')
def initialize(email, password, options={})
@api_host = options.delete(:api_host) || 'twitter.com'
@config, @config[:email], @config[:password] = options, email, password
@proxy_host = options[:proxy_host]
@proxy_port = options[:proxy_port]
end
# Returns an array of statuses for a timeline; Defaults to your friends timeline.
def timeline(which=:friends, options={})
raise UnknownTimeline unless [:friends, :public, :user].include?(which)
auth = which.to_s.include?('public') ? false : true
statuses(call("#{which}_timeline", :auth => auth, :since => options[:since], :args => parse_options(options)))
end
# Returns an array of users who are in your friends list
def friends(options={})
users(call(:friends, {:args => parse_options(options)}))
end
# Returns an array of users who are friends for the id or username passed in
def friends_for(id, options={})
friends(options.merge({:id => id}))
end
# Returns an array of users who are following you
def followers(options={})
users(call(:followers, {:args => parse_options(options)}))
end
def followers_for(id, options={})
followers(options.merge({:id => id}))
end
# Returns a single status for a given id
def status(id)
statuses(call("show/#{id}")).first
end
# returns all the profile information and the last status for a user
def user(id_or_screenname)
users(request("users/show/#{id_or_screenname}.xml", :auth => true)).first
end
# Returns an array of statuses that are replies
def replies(options={})
statuses(call(:replies, :since => options[:since], :args => parse_options(options)))
end
# Destroys a status by id
def destroy(id)
call("destroy/#{id}")
end
def rate_limit_status
RateLimitStatus.new_from_xml request("account/rate_limit_status.xml", :auth => true)
end
# waiting for twitter to correctly implement this in the api as it is documented
def featured
users(call(:featured))
end
# Returns an array of all the direct messages for the authenticated user
def direct_messages(options={})
doc = request(build_path('direct_messages.xml', parse_options(options)), {:auth => true, :since => options[:since]})
(doc/:direct_message).inject([]) { |dms, dm| dms << DirectMessage.new_from_xml(dm); dms }
end
alias :received_messages :direct_messages
# Returns direct messages sent by auth user
def sent_messages(options={})
doc = request(build_path('direct_messages/sent.xml', parse_options(options)), {:auth => true, :since => options[:since]})
(doc/:direct_message).inject([]) { |dms, dm| dms << DirectMessage.new_from_xml(dm); dms }
end
# destroys a give direct message by id if the auth user is a recipient
def destroy_direct_message(id)
DirectMessage.new_from_xml(request("direct_messages/destroy/#{id}.xml", :auth => true, :method => :post))
end
# Sends a direct message text
to user
def d(user, text)
DirectMessage.new_from_xml(request('direct_messages/new.xml', :auth => true, :method => :post, :form_data => {'text' => text, 'user' => user}))
end
# Befriends id_or_screenname for the auth user
def create_friendship(id_or_screenname)
users(request("friendships/create/#{id_or_screenname}.xml", :auth => true, :method => :post)).first
end
# Defriends id_or_screenname for the auth user
def destroy_friendship(id_or_screenname)
users(request("friendships/destroy/#{id_or_screenname}.xml", :auth => true, :method => :post)).first
end
# Returns true if friendship exists, false if it doesn't.
def friendship_exists?(user_a, user_b)
doc = request(build_path("friendships/exists.xml", {:user_a => user_a, :user_b => user_b}), :auth => true)
doc.at('friends').innerHTML == 'true' ? true : false
end
# Updates your location and returns Twitter::User object
def update_location(location)
users(request(build_path('account/update_location.xml', {'location' => location}), :auth => true, :method => :post)).first
end
# Updates your deliver device and returns Twitter::User object
def update_delivery_device(device)
users(request(build_path('account/update_delivery_device.xml', {'device' => device}), :auth => true, :method => :post)).first
end
# Turns notifications by id_or_screenname on for auth user.
def follow(id_or_screenname)
users(request("notifications/follow/#{id_or_screenname}.xml", :auth => true, :method => :post)).first
end
# Turns notifications by id_or_screenname off for auth user.
def leave(id_or_screenname)
users(request("notifications/leave/#{id_or_screenname}.xml", :auth => true, :method => :post)).first
end
# Returns the most recent favorite statuses for the autenticating user
def favorites(options={})
statuses(request(build_path('favorites.xml', parse_options(options)), :auth => true))
end
# Favorites the status specified by id for the auth user
def create_favorite(id)
statuses(request("favorites/create/#{id}.xml", :auth => true, :method => :post)).first
end
# Un-favorites the status specified by id for the auth user
def destroy_favorite(id)
statuses(request("favorites/destroy/#{id}.xml", :auth => true, :method => :post)).first
end
# Blocks the user specified by id for the auth user
def block(id)
users(request("blocks/create/#{id}.xml", :auth => true, :method => :post)).first
end
# Unblocks the user specified by id for the auth user
def unblock(id)
users(request("blocks/destroy/#{id}.xml", :auth => true, :method => :post)).first
end
# Posts a new update to twitter for auth user.
def post(status, options={})
form_data = {'status' => status}
form_data.merge!({'source' => options[:source]}) if options[:source]
form_data.merge!({'in_reply_to_status_id' => options[:in_reply_to_status_id]}) if options[:in_reply_to_status_id]
Status.new_from_xml(request('statuses/update.xml', :auth => true, :method => :post, :form_data => form_data))
end
alias :update :post
# Verifies the credentials for the auth user.
# raises Twitter::CantConnect on failure.
def verify_credentials
request('account/verify_credentials.xml', :auth => true)
end
private
# Converts an hpricot doc to an array of statuses
def statuses(doc)
(doc/:status).inject([]) { |statuses, status| statuses << Status.new_from_xml(status); statuses }
end
# Converts an hpricot doc to an array of users
def users(doc)
(doc/:user).inject([]) { |users, user| users << User.new_from_xml(user); users }
end
# Calls whatever api method requested that deals with statuses
#
# ie: call(:public_timeline, :auth => false)
def call(method, options={})
options = { :auth => true, :args => {} }.merge(options)
# Following line needed as lite=false doesn't work in the API: http://tinyurl.com/yo3h5d
options[:args].delete(:lite) unless options[:args][:lite]
args = options.delete(:args)
request(build_path("statuses/#{method.to_s}.xml", args), options)
end
def response(path, options={})
uri = URI.parse("http://#{@api_host}")
begin
response = Net::HTTP::Proxy(@proxy_host, @proxy_port).start(uri.host, uri.port) do |http|
klass = Net::HTTP.const_get options[:method].to_s.downcase.capitalize
req = klass.new("#{uri.path}/#{path}", options[:headers])
req.basic_auth(@config[:email], @config[:password]) if options[:auth]
if options[:method].to_s == 'post' && options[:form_data]
req.set_form_data(options[:form_data])
end
http.request(req)
end
rescue => error
raise CantConnect, error.message
end
end
# Makes a request to twitter.
def request(path, options={})
options = {
:headers => { "User-Agent" => @config[:email] },
:method => :get,
}.merge(options)
unless options[:since].nil?
since = options[:since].kind_of?(Date) ? options[:since].strftime('%a, %d-%b-%y %T GMT') : options[:since].to_s
options[:headers]["If-Modified-Since"] = since
end
handle_response!(response(path, options))
end
def handle_response!(response)
if %w[200 304].include?(response.code)
response = parse(response.body)
raise RateExceeded if (response/:hash/:error).text =~ /Rate limit exceeded/
response
elsif response.code == '503'
raise Unavailable, response.message
elsif response.code == '400'
raise RateExceeded, response.message
elsif response.code == '401'
raise CantConnect, 'Authentication failed. Check your username and password'
elsif response.code == '403'
error_message = (parse(response.body)/:hash/:error).text
raise CantFindUsers, error_message if error_message =~ /Could not find both specified users/
raise AlreadyFollowing, error_message if error_message =~ /already on your list/
raise CantFollowUser, "Response code #{response.code}: #{response.message} #{error_message}"
else
raise CantConnect, "Twitter is returning a #{response.code}: #{response.message}"
end
end
# Given a path and a hash, build a full path with the hash turned into a query string
def build_path(path, options)
unless options.nil?
query = options.inject('') { |str, h| str += "#{CGI.escape(h[0].to_s)}=#{CGI.escape(h[1].to_s)}&"; str }
path += "?#{query}"
end
path
end
# Tries to get all the options in the correct format before making the request
def parse_options(options)
options[:since] = options[:since].kind_of?(Date) ? options[:since].strftime('%a, %d-%b-%y %T GMT') : options[:since].to_s if options[:since]
options
end
# Converts a string response into an Hpricot xml element.
def parse(response)
Hpricot.XML(response || '')
end
end
end