# 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