diff --git a/.gitignore b/.gitignore index 05ab796..b220307 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ marketing/samples/twitter4rails.post-0_2_4/log marketing/samples/twitter4rails.post-0_2_4/tmp twitter*.gem coverage.data +coverage +gemfile.lock +*.DS_Store \ No newline at end of file diff --git a/Gemfile b/Gemfile index 59a4a48..af4dea2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source :gemcutter -gem "oauth", ">=0.4.1" +gem "oauth", ">=0.4.5" gem "rake" if RUBY_VERSION < "1.9.0" diff --git a/README.md b/README.md index 81506e3..fb229d7 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Code: * Filipe Giusti - fixed users/show issue that Twitter.com changed from under us, also inspired the v0.5.2 bugfix release by submitting great issue example code. * Seth Cousins - added HTTP timeout option and provided a patch that inspired the OAuth support for Twitter4R * John McKerrell <@mcknut on twitter> - added geo attribute to Twitter::Message. +* domrout on GitHub - added Tweet Entities. Design Suggestions: diff --git a/bin/t4rsh b/bin/t4rsh index 4ff2b3d..6d49bdb 100755 --- a/bin/t4rsh +++ b/bin/t4rsh @@ -2,19 +2,19 @@ require("irb") require("irb/completion") -require("rubygems") begin - gem('twitter4r', '>0.3.0') + gem('twitter4r', '>=0.5.0') require("twitter") require("twitter/console") rescue Gem::LoadError begin - gem("mbbx6spp-twitter4r", '>=0.3.1') + require("rubygems") + gem("twitter4r", '>=0.5.0') require("twitter") require("twitter/console") rescue Gem::LoadError - abort("Error: You must install either twitter4r gem from Rubyforge with version 0.3.1 or greater or the mbbx6spp-twitter4r gem from GitHub's servers with version 0.3.1 or greater (and make sure it is a recent version of the gem).") + abort("Error: You must install a twitter4r gem of version 0.5.0 or greater.") end end diff --git a/lib/twitter/client/base.rb b/lib/twitter/client/base.rb index 429b310..3e375f3 100644 --- a/lib/twitter/client/base.rb +++ b/lib/twitter/client/base.rb @@ -35,16 +35,31 @@ def search_oauth_connect(method, path, params = {}, headers = {}, require_auth = response end + # Returns the response of the OAuth/HTTP(s) request for Media/Upload API requests + def media_oauth_connect(method, path, params = {}, headers = {}, require_auth = true) + atoken = media_access_token + uri = media_request_uri(path, params) + puts uri + body = ::Twitter::MultiPartBody.new(params.delete(:parts)) + headers['Accept'] = "application/json" + headers['Content-Type'] = "multipart/form-data; boundary=#{body.send(:boundary)}" + headers['Content-Length'] = body.to_s.bytesize.to_s + puts body + response = atoken.send(method, uri, body.to_s, http_header.merge(headers)) + handle_rest_response(response) + response + end + # "Blesses" model object with client information def bless_model(model) model.bless(self) if model end - + def bless_models(list) return bless_model(list) if list.respond_to?(:client=) list.collect { |model| bless_model(model) } if list.respond_to?(:collect) end - + private @@http_header = nil @@ -58,7 +73,7 @@ def rest_consumer cfg = self.class.config key ||= cfg.oauth_consumer_token secret ||= cfg.oauth_consumer_secret - @rest_consumer = OAuth::Consumer.new(key, secret, + @rest_consumer = OAuth::Consumer.new(key, secret, :site => construct_site_url, :proxy => construct_proxy_url) http = @rest_consumer.http @@ -67,32 +82,18 @@ def rest_consumer @rest_consumer end - def rest_access_token - unless @rest_access_token - access = @oauth_access - if access - key = access[:key] || access["key"] - secret = access[:secret] || access["secret"] - else - raise Error, "No access tokens are set" - end - @rest_access_token = OAuth::AccessToken.new(rest_consumer, key, secret) - end - @rest_access_token - end - def search_consumer unless @search_consumer cfg = self.class.config consumer = @oauth_consumer if consumer - key = consumer[:key] || consumer["key"] + key = consumer[:key] || consumer["key"] secret = consumer[:secret] || consumer["secret"] end cfg = self.class.config key ||= cfg.oauth_consumer_token secret ||= cfg.oauth_consumer_secret - @search_consumer = OAuth::Consumer.new(key, secret, + @search_consumer = OAuth::Consumer.new(key, secret, :site => construct_site_url(:search), :proxy => construct_proxy_url) http = @search_consumer.http @@ -101,6 +102,40 @@ def search_consumer @search_consumer end + def media_consumer + unless @media_consumer + cfg = self.class.config + consumer = @oauth_consumer + if consumer + key = consumer[:key] || consumer["key"] + secret = consumer[:secret] || consumer["secret"] + end + cfg = self.class.config + key ||= cfg.oauth_consumer_token + secret ||= cfg.oauth_consumer_secret + @media_consumer = OAuth::Consumer.new(key, secret, + :site => construct_site_url(:media), + :proxy => construct_proxy_url) + http = @media_consumer.http + http.read_timeout = cfg.timeout + end + @media_consumer + end + + def rest_access_token + unless @rest_access_token + access = @oauth_access + if access + key = access[:key] || access["key"] + secret = access[:secret] || access["secret"] + @rest_access_token = OAuth::AccessToken.new(rest_consumer, key, secret) + else + raise ::Twitter::Twitter4rError.new(:message => "No access tokens are set") + end + end + @rest_access_token + end + def search_access_token unless @search_access_token key = @oauth_access[:key] || @oauth_access["key"] @@ -110,28 +145,37 @@ def search_access_token @search_access_token end + def media_access_token + unless @media_access_token + key = @oauth_access[:key] || @oauth_access["key"] + secret = @oauth_access[:secret] || @oauth_access["secret"] + @media_access_token = OAuth::AccessToken.new(media_consumer, key, secret) + end + @media_access_token + end + def raise_rest_error(response, uri = nil) map = JSON.parse(response.body) error = Twitter::RESTError.registry[response.code] - raise error.new(:code => response.code, + raise error.new(:code => response.code, :message => response.message, :error => map["error"], - :uri => uri) + :uri => uri) end - + def handle_rest_response(response, uri = nil) unless response.is_a?(Net::HTTPSuccess) raise_rest_error(response, uri) end end - + def http_header - # can cache this in class variable since all "variables" used to - # create the contents of the HTTP header are determined by other + # can cache this in class variable since all "variables" used to + # create the contents of the HTTP header are determined by other # class variables that are not designed to change after instantiation. - @@http_header ||= { + @@http_header ||= { 'User-Agent' => "Twitter4R v#{Twitter::Version.to_version} [#{self.class.config.user_agent}]", - 'Accept' => 'text/x-json', + 'Accept' => 'application/json', 'X-Twitter-Client' => self.class.config.application_name, 'X-Twitter-Client-Version' => self.class.config.application_version, 'X-Twitter-Client-URL' => self.class.config.application_url, @@ -144,21 +188,28 @@ def rest_request_uri(path, params = nil) uri << "?#{params.to_http_str}" if params uri end - + def search_request_uri(path, params = nil) uri = "#{self.class.config.search_path_prefix}#{path}" uri << "?#{params.to_http_str}" if params uri end - + + def media_request_uri(path, params = nil) + "#{self.class.config.media_path_prefix}#{path}" + end + def uri_components(service = :rest) case service when :rest - return self.class.config.protocol, self.class.config.host, self.class.config.port, - self.class.config.path_prefix + return self.class.config.protocol, self.class.config.host, + self.class.config.port, self.class.config.path_prefix when :search - return self.class.config.search_protocol, self.class.config.search_host, + return self.class.config.search_protocol, self.class.config.search_host, self.class.config.search_port, self.class.config.search_path_prefix + when :media + return self.class.config.media_protocol, self.class.config.media_host, + self.class.config.media_port, self.class.config.media_path_prefix end end diff --git a/lib/twitter/client/status.rb b/lib/twitter/client/status.rb index 7413b24..4177009 100644 --- a/lib/twitter/client/status.rb +++ b/lib/twitter/client/status.rb @@ -2,41 +2,44 @@ class Twitter::Client @@STATUS_URIS = { :get => '/statuses/show.json', :post => '/statuses/update.json', + :post_multipart => '/statuses/update_with_media.json', :delete => '/statuses/destroy.json', :reply => '/statuses/update.json' } - + # Provides access to individual statuses via Twitter's Status APIs - # + # # action can be of the following values: # * :get to retrieve status content. Assumes value given responds to :to_i message in meaningful way to yield intended status id. # * :post to publish a new status # * :delete to remove an existing status. Assumes value given responds to :to_i message in meaningful way to yield intended status id. # * :reply to reply to an existing status. Assumes value given is Hash which contains :in_reply_to_status_id and :status - # + # # value should be set to: # * the status identifier for :get case # * the status text message for :post case # * none necessary for :delete case - # + # # Examples: # twitter.status(:get, 107786772) # twitter.status(:post, "New Ruby open source project Twitter4R version 0.2.0 released.") # twitter.status(:delete, 107790712) # twitter.status(:reply, :in_reply_to_status_id => 1390482942342, :status => "@t4ruby This new v0.7.0 release is da bomb! #ruby #twitterapi #twitter4r") # twitter.status(:post, "My brand new status in all its glory here tweeted from Greenwich (the real one). #withawesomehashtag #booyah", :lat => 0, :long => 0) - # - # An ArgumentError will be raised if an invalid action + # twitter.status(:post, :media => {:filename => "awesome_photo.png", :content_type => "image/png"}, :status => "My brand new status in all its glory here tweeted from Greenwich (the real one). #withawesomehashtag #booyah", :lat => 0, :long => 0) + # + # An ArgumentError will be raised if an invalid action # is given. Valid actions are: # * +:get+ # * +:post+ # * +:delete+ # - # The third argument +options+ sends on a Hash to the Twitter API with the following keys allowed: + # The options argument sends on a Hash to the Twitter API with the following keys allowed: # * +:lat+ - latitude (for posting geolocation) # * +:long+ - longitude (for posting geolocation) # * +:place_id+ - using a place ID give by geo/reverse_geocode # * +:display_coordinates+ - whether or not to put a pin in the exact coordinates + # * +:media+ - media enclosures to upload and embed in status def status(action, value = nil) return self.timeline_for(action, value || {}) if :replies == action raise ArgumentError, "Invalid status action: #{action}" unless @@STATUS_URIS.keys.member?(action) @@ -47,14 +50,31 @@ def status(action, value = nil) when :get response = rest_oauth_connect(:get, uri, {:id => value.to_i}) when :post - if value.is_a?(Hash) + if value.is_a?(Hash) && value.key?(:media) + params = { + :parts => [ + Twitter::MediaPart.new(value[:media].merge(:name => "media[]")), + Twitter::MediaPart.new( + :body => value[:status], + :content_type => "text/plain", + :name => "status") + ] + } + action = :post_multipart + response = media_oauth_connect(:post, @@STATUS_URIS[action], params) + elsif value.is_a?(Hash) params = value.delete_if { |k, v| ![:status, :lat, :long, :place_id, :display_coordinates].member?(k) } + action = :post + response = rest_oauth_connect(action, uri, + params.merge(:source => self.class.config.source)) else params = {:status => value} + action = :post + response = rest_oauth_connect(action, uri, + params.merge(:source => self.class.config.source)) end - response = rest_oauth_connect(:post, uri, params.merge(:source => self.class.config.source)) when :delete response = rest_oauth_connect(:delete, uri, {:id => value.to_i}) when :reply diff --git a/lib/twitter/config.rb b/lib/twitter/config.rb index 373199a..18ac153 100644 --- a/lib/twitter/config.rb +++ b/lib/twitter/config.rb @@ -1,15 +1,15 @@ -# config.rb contains classes, methods and extends existing Twitter4R classes +# config.rb contains classes, methods and extends existing Twitter4R classes # to provide easy configuration facilities. module Twitter # Represents global configuration for Twitter::Client. # Can override the following configuration options: # * protocol - :http, :https or :ssl supported. :ssl is an alias for :https. Defaults to :ssl - # * host - hostname to connect to for the Twitter service. Defaults to 'twitter.com'. + # * host - hostname to connect to for the Twitter service. Defaults to 'api.twitter.com'. # * port - port to connect to for the Twitter service. Defaults to 443. - # * path_prefix - path to prefix URIs of REST API calls. Defaults to "". + # * path_prefix - path to prefix URIs of REST API calls. Defaults to "/1". # * search_protocol - :http, :https or :ssl supported. :ssl is an alias for :https. Defaults to :ssl - # * search_host - hostname to connect to for the Twitter Search service. Defaults to 'twitter.com'. + # * search_host - hostname to connect to for the Twitter Search service. Defaults to 'search.twitter.com'. # * search_port - port to connect to for the Twitter Search service. Defaults to 443. # * search_path_prefix - path to prefix URIs of Search API calls. Defaults to "". # * proxy_protocol - proxy protocol to use. Defaults to http. @@ -17,6 +17,10 @@ module Twitter # * proxy_port - proxy host to use. Defaults to 8080. # * proxy_user - proxy username to use. Defaults to nil. # * proxy_pass - proxy password to use. Defaults to nil. + # * media_protocol - :http, :https or :ssl supported. :ssl is an alias for :https. Defaults to :ssl + # * media_host - hostname to connect to for the Twitter Upload service. Defaults to 'twitter.com'. + # * media_port - port to connect to for the Twitter Upload service. Defaults to 443. + # * media_path_prefix - path to prefix URIs of Upload API calls. Defaults to "/1". # * user_agent - user agent string to use for each request of the HTTP header. # * application_name - name of your client application. Defaults to 'Twitter4R'. # * application_version - version of your client application. Defaults to current Twitter::Version.to_version. @@ -31,19 +35,23 @@ module Twitter class Config include ClassUtilMixin @@ATTRIBUTES = [ - :protocol, - :host, - :port, + :protocol, + :host, + :port, :path_prefix, :search_protocol, :search_host, :search_port, :search_path_prefix, :proxy_protocol, - :proxy_host, - :proxy_port, - :proxy_user, - :proxy_pass, + :proxy_host, + :proxy_port, + :proxy_user, + :proxy_pass, + :media_protocol, + :media_host, + :media_port, + :media_path_prefix, :user_agent, :application_name, :application_version, @@ -59,8 +67,8 @@ class Config ] attr_accessor(*@@ATTRIBUTES) - - # Override of Object#eql? to ensure RSpec specifications run + + # Override of Object#eql? to ensure RSpec specifications run # correctly. Also done to follow Ruby best practices. def eql?(other) return true if self == other @@ -72,17 +80,21 @@ def eql?(other) end class Client - @@defaults = { :host => 'twitter.com', - :port => 443, + @@defaults = { :host => 'api.twitter.com', + :port => 443, :protocol => :ssl, - :path_prefix => "", + :path_prefix => "/1", :search_host => 'search.twitter.com', - :search_port => 80, - :search_protocol => :http, + :search_port => 443, + :search_protocol => :ssl, :search_path_prefix => "", :proxy_protocol => "http", :proxy_host => nil, :proxy_port => 8080, + :media_host => 'upload.twitter.com', + :media_port => 443, + :media_protocol => :ssl, + :media_path_prefix => "/1", :user_agent => "default", :application_name => 'Twitter4R', :application_version => Twitter::Version.to_version, @@ -108,6 +120,6 @@ def configure(&block) raise ArgumentError, "Block must be provided to configure" unless block_given? yield config end # configure - end # class << self + end # class << self end # Client class end # Twitter module diff --git a/lib/twitter/core.rb b/lib/twitter/core.rb index a6e4f82..e224fa3 100644 --- a/lib/twitter/core.rb +++ b/lib/twitter/core.rb @@ -1,4 +1,4 @@ -# The Twitter4R API provides a nicer Ruby object API to work with +# The Twitter4R API provides a nicer Ruby object API to work with # instead of coding around the REST API. # Module to encapsule the Twitter4R API. @@ -6,7 +6,7 @@ module Twitter # Mixin module for classes that need to have a constructor similar to # Rails' models, where a Hash is provided to set attributes # appropriately. - # + # # To define a class that uses this mixin, use the following code: # class FilmActor # include ClassUtilMixin @@ -15,58 +15,58 @@ module ClassUtilMixin #:nodoc: def self.included(base) #:nodoc: base.send(:include, InstanceMethods) end - + # Instance methods defined for Twitter::ModelMixin module. module InstanceMethods #:nodoc: - # Constructor/initializer that takes a hash of parameters that - # will initialize *members* or instance attributes to the + # Constructor/initializer that takes a hash of parameters that + # will initialize *members* or instance attributes to the # values given. For example, - # + # # class FilmActor # include Twitter::ClassUtilMixin # attr_accessor :name # end - # + # # class Production # include Twitter::ClassUtilMixin # attr_accessor :title, :year, :actors # end - # + # # # Favorite actress... # jodhi = FilmActor.new(:name => "Jodhi May") # jodhi.name # => "Jodhi May" - # + # # # Favorite actor... # robert = FilmActor.new(:name => "Robert Lindsay") # robert.name # => "Robert Lindsay" - # + # # # Jane is also an excellent pick...gotta love her accent! # jane = FilmActor.new(name => "Jane Horrocks") # jane.name # => "Jane Horrocks" - # + # # # Witty BBC series... - # mrs_pritchard = Production.new(:title => "The Amazing Mrs. Pritchard", - # :year => 2005, + # mrs_pritchard = Production.new(:title => "The Amazing Mrs. Pritchard", + # :year => 2005, # :actors => [jodhi, jane]) # mrs_pritchard.title # => "The Amazing Mrs. Pritchard" # mrs_pritchard.year # => 2005 - # mrs_pritchard.actors # => [#, + # mrs_pritchard.actors # => [#, # ] # # Any Ros Pritchard's out there to save us from the Tony Blair - # # and Gordon Brown *New Labour* debacle? You've got my vote! - # - # jericho = Production.new(:title => "Jericho", - # :year => 2005, + # # and Gordon Brown *New Labour* debacle? You've got my vote! + # + # jericho = Production.new(:title => "Jericho", + # :year => 2005, # :actors => [robert]) # jericho.title # => "Jericho" # jericho.year # => 2005 # jericho.actors # => [#] - # - # Assuming class FilmActor includes - # Twitter::ClassUtilMixin in the class definition - # and has an attribute of name, then that instance - # attribute will be set to "Jodhi May" for the actress - # object during object initialization (aka construction for + # + # Assuming class FilmActor includes + # Twitter::ClassUtilMixin in the class definition + # and has an attribute of name, then that instance + # attribute will be set to "Jodhi May" for the actress + # object during object initialization (aka construction for # you Java heads). def initialize(params = {}) params.each do |key,val| @@ -74,19 +74,82 @@ def initialize(params = {}) end self.send(:init) if self.respond_to? :init end - + protected - # Helper method to provide an easy and terse way to require + # Helper method to provide an easy and terse way to require # a block is provided to a method. def require_block(block_given) raise ArgumentError, "Must provide a block" unless block_given end end end # ClassUtilMixin - - # Exception API base class raised when there is an error encountered upon + + # Representation that generates HTTP spec multi part form body format from + # multiple +MediaPart+s + class MultiPartBody + @@ATTRIBUTES = [:boundary, :parts] + @@CRLF = "\r\n" + attr_accessor *@@ATTRIBUTES + + def initialize(*parts) + @parts = parts + @boundary = Digest::SHA1.hexdigest(@parts.join("/")) + end + + def to_s + header = "--#{@boundary}" + payload = @parts.join("--#{@boundary}") + footer = "--#{@boundary}--" + (header + payload + footer) + end + end + + class MediaPart + include ClassUtilMixin + + @@ATTRIBUTES = [:name, :filename, :content_type, :body] + @@CRLF = "\r\n" + attr_accessor *@@ATTRIBUTES + + def disposition + disposition = %{form-data; name="#{self.name}"} + disposition += %{; filename="#{self.filename}"} if self.filename + disposition + end + + def to_s + content = %{#{@@CRLF}Content-Disposition: #{disposition}#{@@CRLF}} + content << %{Content-Type: #{self.content_type}#{@@CRLF}} + if self.name === "status" + content << %{#{@@CRLF}#{self.body}#{@@CRLF}} + else + content << %{Content-Transfer-Encoding: Base64#{@@CRLF*2}} + content << %{#{Base64.encode64(self.body)}#{@@CRLF}} + end + content + end + end # MediaPart + + # Exception class raised when preconditions not met. + # e.g. when no OAuth tokens are provided + class Twitter4rError < RuntimeError + include ClassUtilMixin + @@ATTRIBUTES = [:message] + attr_accessor *@@ATTRIBUTES + + # Returns string in following format: + # "HTTP #{@code}: #{@message} at #{@uri}" + # For example, + # "HTTP 404: Resource Not Found at /i_am_crap.json + # >This is the error message sent back by the Twitter.com API" + def to_s + "Twitter4R: #{@message}" + end + end + + # Exception API base class raised when there is an error encountered upon # querying or posting to the remote Twitter REST API. - # + # # To consume and query any RESTError raised by Twitter4R: # begin # # Do something with your instance of Twitter::Client. @@ -99,7 +162,7 @@ def require_block(block_given) # 404 # Resource Not Found # /i_am_crap.json - class RESTError < RuntimeError + class RESTError < RuntimeError class << self @@REGISTRY = {} @@ -114,8 +177,8 @@ def register(status_code) include ClassUtilMixin @@ATTRIBUTES = [:code, :message, :uri, :error] - attr_accessor :code, :message, :uri, :error - + attr_accessor *@@ATTRIBUTES + # Returns string in following format: # "HTTP #{@code}: #{@message} at #{@uri}" # For example, @@ -126,7 +189,7 @@ def to_s end end # RESTError - # Runtime error leaf class raised when Twitter.com API has no new results + # Runtime error leaf class raised when Twitter.com API has no new results # to return from the last query. HTTP code: 304 (aka Not Modified). # # To handle specifically you would do the following: @@ -136,7 +199,7 @@ def to_s # timeline = [] # end class NotModifiedError < RESTError; register('304'); end - + # Runtime error leaf class raised when client has reached rate limits. # HTTP code: 400 (aka Bad Request). # @@ -147,9 +210,9 @@ class NotModifiedError < RESTError; register('304'); end # # do something here... # end class RateLimitError < RESTError; register('400'); end - - # Runtime error leaf class raised when user and/or client credentials - # are missing or invalid. + + # Runtime error leaf class raised when user and/or client credentials + # are missing or invalid. # HTTP code: 401 (aka Unauthorized). # # To handle specifically you would do the following: @@ -159,8 +222,8 @@ class RateLimitError < RESTError; register('400'); end # # do something to prompt for valid credentials to user here. # end class UnauthorizedError < RESTError; register('401'); end - - # Runtime error leaf class raised when update limit reached. + + # Runtime error leaf class raised when update limit reached. # HTTP code: 403 (aka Forbidden). # # To handle specifically you would do the following: @@ -170,7 +233,7 @@ class UnauthorizedError < RESTError; register('401'); end # # do something to notify user that update limit has been reached # end class ForbiddenError < RESTError; register('403'); end - + # Runtime error leaf class raised when a resource requested was not found. # HTTP code: 404 (aka Not Found). # @@ -181,19 +244,19 @@ class ForbiddenError < RESTError; register('403'); end # # do something to notify user that resource was not found. # end class NotFoundError < RESTError; register('404'); end - + # Runtime error leaf class raised when the format specified in the request - # is not understood by the Twitter.com API. + # is not understood by the Twitter.com API. # HTTP code: 406 (aka Not Acceptable). # # To handle specifically you would do the following: # begin # timeline = twitter.timeline_for(:friends, :since => tweet) # rescue NotAcceptableError => nae - # # + # # # end class NotAcceptableError < RESTError; register('406'); end - + # Runtime error leaf class raised when search rate limit reached. # HTTP code: 420. # @@ -201,11 +264,11 @@ class NotAcceptableError < RESTError; register('406'); end # begin # timeline = twitter.timeline_for(:friends, :since => tweet) # rescue SearchRateLimitError => nme - # # + # # # end class SearchRateLimitError < RESTError; register('420'); end - - # Runtime error leaf class raised when Twitter.com API is borked for + + # Runtime error leaf class raised when Twitter.com API is borked for # an unknown reason. # HTTP code: 500 (aka Internal Server Error). # @@ -217,8 +280,8 @@ class SearchRateLimitError < RESTError; register('420'); end # # has arisen. # end class InternalServerError < RESTError; register('500'); end - - # Runtime error leaf class raised when Twitter.com servers are being + + # Runtime error leaf class raised when Twitter.com servers are being # upgraded. # HTTP code: 502 (aka Bad Gateway). # @@ -229,8 +292,8 @@ class InternalServerError < RESTError; register('500'); end # # # end class BadGatewayError < RESTError; register('502'); end - - # Runtime error leaf class raised when Twitter.com servers are unable + + # Runtime error leaf class raised when Twitter.com servers are unable # to respond to the current load. # HTTP code: 502 (aka Service Unavailable). # diff --git a/lib/twitter/model.rb b/lib/twitter/model.rb index a01a8d6..c0bafce 100644 --- a/lib/twitter/model.rb +++ b/lib/twitter/model.rb @@ -3,18 +3,18 @@ module Twitter # Mixin module for model classes. Includes generic class methods like # unmarshal. - # + # # To create a new model that includes this mixin's features simply: # class NewModel # include Twitter::ModelMixin # end - # + # # This mixin module automatically includes Twitter::ClassUtilMixin # features. - # - # The contract for models to use this mixin correctly is that the class + # + # The contract for models to use this mixin correctly is that the class # including this mixin must provide an class method named attributes - # that will return an Array of attribute symbols that will be checked + # that will return an Array of attribute symbols that will be checked # in #eql? override method. The following would be sufficient: # def self.attributes; @@ATTRIBUTES; end module ModelMixin #:nodoc: @@ -27,10 +27,11 @@ def self.included(base) #:nodoc: # Class methods defined for Twitter::ModelMixin module. module ClassMethods #:nodoc: # Unmarshal object singular or plural array of model objects - # from JSON serialization. Currently JSON is only supported + # from JSON serialization. Currently JSON is only supported # since this is all Twitter4R needs. def unmarshal(raw) input = JSON.parse(raw) if raw.is_a?(String) + def unmarshal_model(hash) self.new(hash) end @@ -43,18 +44,18 @@ def unmarshal_model(hash) result # plural case end end - + # Instance methods defined for Twitter::ModelMixin module. module InstanceMethods #:nodoc: attr_accessor :client # Equality method override of Object#eql? default. - # + # # Relies on the class using this mixin to provide a attributes - # class method that will return an Array of attributes to check are + # class method that will return an Array of attributes to check are # equivalent in this #eql? override. - # + # # It is by design that the #eql? method will raise a NoMethodError - # if no attributes class method exists, to alert you that + # if no attributes class method exists, to alert you that # you must provide it for a meaningful result from this #eql? override. # Otherwise this will return a meaningless result. def eql?(other) @@ -64,35 +65,35 @@ def eql?(other) end true end - + # Returns integer representation of model object instance. - # + # # For example, # status = Twitter::Status.new(:id => 234343) # status.to_i #=> 234343 def to_i @id end - + # Returns string representation of model object instance. - # + # # For example, # status = Twitter::Status.new(:text => 'my status message') # status.to_s #=> 'my status message' - # - # If a model class doesn't have a @text attribute defined + # + # If a model class doesn't have a @text attribute defined # the default Object#to_s will be returned as the result. def to_s self.respond_to?(:text) ? @text : super.to_s end - + # Returns hash representation of model object instance. - # + # # For example, # u = Twitter::User.new(:id => 2342342, :screen_name => 'tony_blair_is_the_devil') # u.to_hash #=> {:id => 2342342, :screen_name => 'tony_blair_is_the_devil'} - # - # This method also requires that the class method attributes be + # + # This method also requires that the class method attributes be # defined to return an Array of attributes for the class. def to_hash attrs = self.class.attributes @@ -104,50 +105,50 @@ def to_hash end result end - + # "Blesses" model object. - # + # # Should be overridden by model class if special behavior is expected - # + # # Expected to return blessed object (usually self) def bless(client) self.basic_bless(client) end - + protected - # Basic "blessing" of model object + # Basic "blessing" of model object def basic_bless(client) self.client = client self end end end - + module AuthenticatedUserMixin def self.included(base) base.send(:include, InstanceMethods) end - + module InstanceMethods # Returns an Array of user objects that represents the authenticated # user's friends on Twitter. def followers(options = {}) @client.my(:followers, options) end - - # Adds given user as a friend. Returns user object as given by + + # Adds given user as a friend. Returns user object as given by # Twitter REST server response. - # - # For user argument you may pass in the unique integer + # + # For user argument you may pass in the unique integer # user ID, screen name or Twitter::User object representation. def befriend(user) @client.friend(:add, user) end - - # Removes given user as a friend. Returns user object as given by + + # Removes given user as a friend. Returns user object as given by # Twitter REST server response. - # - # For user argument you may pass in the unique integer + # + # For user argument you may pass in the unique integer # user ID, screen name or Twitter::User object representation. def defriend(user) @client.friend(:remove, user) @@ -166,25 +167,25 @@ class << self def attributes; @@ATTRIBUTES; end end - # Alias to +countryCode+ for those wanting to use consistent naming + # Alias to +countryCode+ for those wanting to use consistent naming # convention for attribute def country_code @countryCode end - # Alias to +parentid+ for those wanting to use consistent naming + # Alias to +parentid+ for those wanting to use consistent naming # convention for attribute def parent_id @parentid end - # Alias to +placeType+ for those wanting to use consistent naming + # Alias to +placeType+ for those wanting to use consistent naming # convention for attribute def place_type @place_type end - # Convenience method to output meaningful representation to STDOUT as per + # Convenience method to output meaningful representation to STDOUT as per # Ruby convention def inspect "#{name} / #{woeid} / #{countryCode}\n#{url}\n" @@ -192,8 +193,7 @@ def inspect protected def init - puts @placeType - @placeType = ::Twitter::PlaceType.new(:name => @placeType["name"], + @placeType = ::Twitter::PlaceType.new(:name => @placeType["name"], :code => @placeType["code"]) if @placeType.is_a?(Hash) end end @@ -214,7 +214,7 @@ def attributes; @@ATTRIBUTES; end # # To find out when this +Trendline+ was created query the +as_of+ attribute. # To find out what type +Trendline+ is use the +type+ attribute. - # You can iterator over the trends in the +Trendline+ with +each+ or by + # You can iterator over the trends in the +Trendline+ with +each+ or by # index, whichever you prefer. class Trendline include ModelMixin @@ -241,7 +241,7 @@ def each end end - # index operator definition needed to iterate over trends + # index operator definition needed to iterate over trends # in the +::Twitter::Trendline+ object using for or otherwise def [](index) trends[index] @@ -270,12 +270,12 @@ def attributes; @@ATTRIBUTES; end # Represents a Twitter user class User include ModelMixin - @@ATTRIBUTES = [:id, :name, :description, :location, :screen_name, :url, - :protected, :profile_image_url, :profile_background_color, - :profile_text_color, :profile_link_color, :profile_sidebar_fill_color, - :profile_sidebar_border_color, :profile_background_image_url, - :profile_background_tile, :utc_offset, :time_zone, - :following, :notifications, :favourites_count, :followers_count, + @@ATTRIBUTES = [:id, :name, :description, :location, :screen_name, :url, + :protected, :profile_image_url, :profile_background_color, + :profile_text_color, :profile_link_color, :profile_sidebar_fill_color, + :profile_sidebar_border_color, :profile_background_image_url, + :profile_background_tile, :utc_offset, :time_zone, + :following, :notifications, :favourites_count, :followers_count, :friends_count, :statuses_count, :created_at ] attr_accessor(*@@ATTRIBUTES) @@ -285,20 +285,20 @@ def attributes; @@ATTRIBUTES; end # Returns user model object with given id using the configuration # and credentials of the client object passed in. - # - # You can pass in either the user's unique integer ID or the user's + # + # You can pass in either the user's unique integer ID or the user's # screen name. def find(id, client) client.user(id) end end - + # Override of ModelMixin#bless method. - # - # Adds #followers instance method when user object represents + # + # Adds #followers instance method when user object represents # authenticated user. Otherwise just do basic bless. - # - # This permits applications using Twitter4R to write + # + # This permits applications using Twitter4R to write # Rubyish code like this: # followers = user.followers if user.is_me? # Or: @@ -310,67 +310,164 @@ def bless(client) }) if self.is_me? and not self.respond_to?(:followers) self end - + # Returns whether this Twitter::User model object # represents the authenticated user of the client # that blessed it. def is_me? # TODO: Determine whether we should cache this or not? # Might be dangerous to do so, but do we want to support - # the edge case where this would cause a problem? i.e. - # changing authenticated user after initial use of + # the edge case where this would cause a problem? i.e. + # changing authenticated user after initial use of # authenticated API. # TBD: To cache or not to cache. That is the question! - # Since this is an implementation detail we can leave this for - # subsequent 0.2.x releases. It doesn't have to be decided before + # Since this is an implementation detail we can leave this for + # subsequent 0.2.x releases. It doesn't have to be decided before # the 0.2.0 launch. @screen_name == @client.instance_eval("@login") end - + # Returns an Array of user objects that represents the authenticated # user's friends on Twitter. def friends @client.user(@id, :friends) end end # User - + + # Represents the entities from a Twitter post + # + # Can include media, user mentions, URLs and hashtags. For more details see: + # https://dev.twitter.com/docs/tweet-entities + # + # Must enable include_entities as an option in the request to receive these. + class Entities + include ModelMixin + @@ATTRIBUTES = [:urls, :media, :user_mentions, :hashtags ] + attr_accessor(*@@ATTRIBUTES) + + class << self + # Used as factory method callback + def attributes; @@ATTRIBUTES; end + end + + protected + # Constructor callback + def init + @urls = @urls.collect {|e| e.is_a?(Hash) ? Url.new(e) : e } if @urls.is_a?(Array) + @media = @media.collect {|e| e.is_a?(Hash) ? Media.new(e) : e } if @media.is_a?(Array) + @user_mentions = @user_mentions.collect {|e| e.is_a?(Hash) ? UserMention.new(e) : e } if @user_mentions.is_a?(Array) + @hashtags = @hashtags.collect {|e| e.is_a?(Hash) ? HashTag.new(e) : e } if @hashtags.is_a?(Array) + + end + end + + # Represents a URL entity. + # + # For details, see: + # https://dev.twitter.com/docs/tweet-entities + class Url + include ModelMixin + @@ATTRIBUTES = [:url, :display_url, :expanded_url, :indices ] + attr_accessor(*@@ATTRIBUTES) + + class << self + # Used as factory method callback + def attributes; @@ATTRIBUTES; end + end + + def to_s + @display_url + end + end # Url + + # Represents a media entity. + # + # For details, see: + # https://dev.twitter.com/docs/tweet-entities + class Media + include ModelMixin + @@ATTRIBUTES = [:id, :id_str, :media_url, :media_url_https, :url, :display_url, + :expanded_url, :sizes, :type, :indices ] + attr_accessor(*@@ATTRIBUTES) + + class << self + # Used as factory method callback + def attributes; @@ATTRIBUTES; end + end + end # Media + + # Represents a user mention entity. + # + # For details, see: + # https://dev.twitter.com/docs/tweet-entities + class UserMention + include ModelMixin + @@ATTRIBUTES = [:id, :id_str, :screen_name, :name, :indices ] + attr_accessor(*@@ATTRIBUTES) + + class << self + # Used as factory method callback + def attributes; @@ATTRIBUTES; end + end + + def to_s + "@#{@screen_name}" + end + end # UserMention + + + # Represents a hashtag entity. + # + # For details, see: + # https://dev.twitter.com/docs/tweet-entities + class HashTag + include ModelMixin + @@ATTRIBUTES = [:text, :indices ] + attr_accessor(*@@ATTRIBUTES) + + class << self + # Used as factory method callback + def attributes; @@ATTRIBUTES; end + end + end # Hashtag + # Represents a status posted to Twitter by a Twitter user. class Status include ModelMixin - @@ATTRIBUTES = [:id, :id_str, :text, :source, :truncated, :created_at, :user, - :from_user, :to_user, :favorited, :in_reply_to_status_id, - :in_reply_to_user_id, :in_reply_to_screen_name, :geo] + @@ATTRIBUTES = [:id, :id_str, :text, :source, :truncated, :created_at, :user, + :from_user, :to_user, :favorited, :in_reply_to_status_id, + :in_reply_to_user_id, :in_reply_to_screen_name, :geo, :entities] attr_accessor(*@@ATTRIBUTES) class << self # Used as factory method callback def attributes; @@ATTRIBUTES; end - - # Returns status model object with given status using the + + # Returns status model object with given status using the # configuration and credentials of the client object passed in. def find(id, client) client.status(:get, id) end - - # Creates a new status for the authenticated user of the given + + # Creates a new status for the authenticated user of the given # client context. - # - # You MUST include a valid/authenticated client context + # + # You MUST include a valid/authenticated client context # in the given params argument. - # + # # For example: # status = Twitter::Status.create( # :text => 'I am shopping for flip flops', # :client => client) - # + # # An ArgumentError will be raised if no valid client context # is given in the params Hash. For example, # status = Twitter::Status.create(:text => 'I am shopping for flip flops') # The above line of code will raise an ArgumentError. - # + # # The same is true when you do not provide a :text key-value # pair in the params argument given. - # + # # The Twitter::Status object returned after the status successfully # updates on the Twitter server side is returned from this method. def create(params) @@ -385,64 +482,66 @@ def reply? !!@in_reply_to_status_id end - # Convenience method to allow client developers to not have to worry about - # setting the +in_reply_to_status_id+ attribute or prefixing the status + # Convenience method to allow client developers to not have to worry about + # setting the +in_reply_to_status_id+ attribute or prefixing the status # text with the +screen_name+ being replied to. def reply(reply) status_reply = "@#{user.screen_name} #{reply}" - client.status(:reply, :status => status_reply, + client.status(:reply, :status => status_reply, :in_reply_to_status_id => @id) end - + protected # Constructor callback def init @user = User.new(@user) if @user.is_a?(Hash) + @entities = Entities.new(@entities) if @entities.is_a?(Hash) + @created_at = Time.parse(@created_at) if @created_at.is_a?(String) - end + end end # Status - + # Represents a direct message on Twitter between Twitter users. class Message include ModelMixin @@ATTRIBUTES = [:id, :recipient, :sender, :text, :geo, :created_at] attr_accessor(*@@ATTRIBUTES) - + class << self # Used as factory method callback def attributes; @@ATTRIBUTES; end - - # Raises NotImplementedError because currently - # Twitter doesn't provide a facility to retrieve + + # Raises NotImplementedError because currently + # Twitter doesn't provide a facility to retrieve # one message by unique ID. def find(id, client) raise NotImplementedError, 'Twitter has yet to implement a REST API for this. This is not a Twitter4R library limitation.' end - - # Creates a new direct message from the authenticated user of the + + # Creates a new direct message from the authenticated user of the # given client context. - # - # You MUST include a valid/authenticated client context + # + # You MUST include a valid/authenticated client context # in the given params argument. - # + # # For example: # status = Twitter::Message.create( # :text => 'I am shopping for flip flops', # :recipient => 'anotherlogin', # :client => client) - # + # # An ArgumentError will be raised if no valid client context # is given in the params Hash. For example, # status = Twitter::Status.create(:text => 'I am shopping for flip flops') # The above line of code will raise an ArgumentError. - # + # # The same is true when you do not provide any of the following # key-value pairs in the params argument given: # * text - the String that will be the message text to send to user # * recipient - the user ID, screen_name or Twitter::User object representation of the recipient of the direct message - # - # The Twitter::Message object returned after the direct message is - # successfully sent on the Twitter server side is returned from + # + # The Twitter::Message object returned after the direct message is + # successfully sent on the Twitter server side is returned from # this method. def create(params) client, text, recipient = params[:client], params[:text], params[:recipient] @@ -452,7 +551,7 @@ def create(params) client.message(:post, text, recipient) end end - + protected # Constructor callback def init @@ -461,14 +560,14 @@ def init @created_at = Time.parse(@created_at) if @created_at.is_a?(String) end end # Message - - # RateLimitStatus provides information about how many requests you have left + + # RateLimitStatus provides information about how many requests you have left # and when you can resume more requests if your remaining_hits count is zero. class RateLimitStatus include ModelMixin @@ATTRIBUTES = [:remaining_hits, :hourly_limit, :reset_time_in_seconds, :reset_time] attr_accessor(*@@ATTRIBUTES) - + class << self # Used as factory method callback def attributes; @@ATTRIBUTES; end diff --git a/lib/twitter/version.rb b/lib/twitter/version.rb index c90d0a6..d38691f 100644 --- a/lib/twitter/version.rb +++ b/lib/twitter/version.rb @@ -3,14 +3,14 @@ module Twitter::Version #:nodoc: MAJOR = 0 - MINOR = 7 - REVISION = 1 + MINOR = 8 + REVISION = 0 class << self # Returns X.Y.Z formatted version string def to_version "#{MAJOR}.#{MINOR}.#{REVISION}" end - + # Returns X-Y-Z formatted version name def to_name "#{MAJOR}_#{MINOR}_#{REVISION}" diff --git a/pkg-info.yml b/pkg-info.yml index c350301..14d92ee 100644 --- a/pkg-info.yml +++ b/pkg-info.yml @@ -6,26 +6,27 @@ spec: require_path: lib has_rdoc: true extra_rdoc_files: - - README + - "README.md" - CHANGES - TODO - MIT-LICENSE autorequire: twitter bindir: bin - executables: + executables: - t4rsh - - t4r-oauth-access + - "t4r-oauth-access" + add_dependency: - json: >=1.1.1 - oauth: >=0.4.1 + json: ">=1.6.5" + oauth: ">=0.4.5" requirements: - Ruby 1.8.6+ - json gem, version 0.4.3 or higher - jcode (for unicode support) - required_ruby_version: >=1.8.6 + required_ruby_version: ">=1.8.7" author: Susan Potter email: twitter4r-users@googlegroups.com - homepage: http://twitter4r.rubyforge.org + homepage: "http://twitter4r.rubyforge.org" rubyforge_project: twitter4r files: <% (self.project_files + self.spec_files).each do |file| %> - <%= Pathname.new(file).relative_path_from(Pathname.new(@root_dir)) %> diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 37ec9b6..41c769b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,7 +8,7 @@ end def require_project_file(file) - require(File.join(File.dirname(__FILE__), '..', 'lib', file)) + require(File.join(File.dirname(__FILE__), '..', 'lib', file)) end require_project_file('twitter') @@ -17,7 +17,7 @@ def require_project_file(file) # Add helper methods here if relevant to multiple _spec.rb files -# Spec helper that sets attribute att for given objects obj +# Spec helper that sets attribute att for given objects obj # and other to given value. def equalizer(obj, other, att, value) setter = "#{att}=" @@ -36,7 +36,7 @@ def client_context(file = 'config/twitter.yml') end # Spec helper that returns a mocked Twitter::Config object -# with stubbed attributes and attrs for overriding attribute +# with stubbed attributes and attrs for overriding attribute # values. def stubbed_twitter_config(config, attrs = {}) defaults = Twitter::Client.class_eval("@@defaults") @@ -75,9 +75,9 @@ def project_root_dir File.expand_path(File.join(File.dirname(__FILE__), '..')) end -# Spec helper that returns stubbed Net::HTTP object +# Spec helper that returns stubbed Net::HTTP object # with given response and obj_stubs. -# The host and port are used to initialize +# The host and port are used to initialize # the Net::HTTP object. def stubbed_net_http(response, obj_stubs = {}, host = 'twitter.com', port = 80) http = Net::HTTP.new(host, port) @@ -86,8 +86,8 @@ def stubbed_net_http(response, obj_stubs = {}, host = 'twitter.com', port = 80) http end -# Spec helper that returns a mocked Net::HTTP object and -# stubs out the request method to return the given +# Spec helper that returns a mocked Net::HTTP object and +# stubs out the request method to return the given # response def mas_net_http(response, obj_stubs = {}) access_token = mock(OAuth::AccessToken, obj_stubs) @@ -103,25 +103,25 @@ def mas_net_http(response, obj_stubs = {}) access_token end -# Spec helper that returns a mocked Net::HTTP::Get object and -# stubs relevant class methods and given obj_stubs +# Spec helper that returns a mocked Net::HTTP::Get object and +# stubs relevant class methods and given obj_stubs # for endo-specing def mas_net_http_get(obj_stubs = {}) mas_net_http(nil, obj_stubs) end -# Spec helper that returns a mocked Net::HTTP::Post object and -# stubs relevant class methods and given obj_stubs +# Spec helper that returns a mocked Net::HTTP::Post object and +# stubs relevant class methods and given obj_stubs # for endo-specing def mas_net_http_post(obj_stubs = {}) mas_net_http(nil, obj_stubs) end -# Spec helper that returns a mocked Net::HTTPResponse object and +# Spec helper that returns a mocked Net::HTTPResponse object and # stubs given obj_stubs for endo-specing. -# -def mas_net_http_response(status = :success, - body = '{}', +# +def mas_net_http_response(status = :success, + body = '{}', obj_stubs = {}) response = RSpec::Mocks::Mock.new(Net::HTTPResponse) response.stub!(:body).and_return(body) diff --git a/spec/twitter/client/base_spec.rb b/spec/twitter/client/base_spec.rb index c44d114..a228d5c 100644 --- a/spec/twitter/client/base_spec.rb +++ b/spec/twitter/client/base_spec.rb @@ -34,7 +34,7 @@ client = Twitter::Client.new(@init_hash) end.should_not raise_error client.send(:login).should eql(@init_hash[:login]) - end + end end describe Twitter::Client, "#inspect" do @@ -60,7 +60,7 @@ conf.application_url = @application_url end @expected_headers = { - 'Accept' => 'text/x-json', + 'Accept' => 'application/json', 'X-Twitter-Client' => @application_name, 'X-Twitter-Client-Version' => @application_version, 'X-Twitter-Client-URL' => @application_url, @@ -70,12 +70,12 @@ # reset @@http_header class variable in Twitter::Client class Twitter::Client.class_eval("@@http_header = nil") end - + it "should always return expected HTTP headers" do headers = @twitter.send(:http_header) headers.should === @expected_headers end - + it "should cache HTTP headers Hash in class variable after first invocation" do cache = Twitter::Client.class_eval("@@http_header") cache.should be_nil @@ -84,7 +84,7 @@ cache.should_not be_nil cache.should === @expected_headers end - + after(:each) do nilize(@user_agent, @application_name, @application_version, @application_url, @twitter, @expected_headers) end @@ -95,12 +95,12 @@ @twitter = client_context @model = Twitter::User.new end - + it "should recieve #client= message on given model to self" do @model.should_receive(:client=).with(@twitter) model = @twitter.send(:bless_model, @model) end - + it "should set client attribute on given model to self" do model = @twitter.send(:bless_model, @model) model.client.should eql(@twitter) @@ -111,14 +111,14 @@ model = @twitter.send(:bless_model, nil) model.should be_nil end - + # needed to alert developer that the model needs to respond to #client= messages appropriately. it "should raise an error if passing in a non-nil object that doesn't not respond to the :client= message" do lambda { - @twitter.send(:bless_model, Object.new) + @twitter.send(:bless_model, Object.new) }.should raise_error(NoMethodError) end - + after(:each) do nilize(@twitter) end @@ -137,25 +137,25 @@ models = @twitter.send(:bless_models, @models) models.each {|model| model.client.should eql(@twitter) } end - + it "should set client attribute for singular model given to self" do model = @twitter.send(:bless_models, @models[0]) model.client.should eql(@twitter) end - + it "should delegate to bless_model for singular model case" do model = @models[0] @twitter.should_receive(:bless_model).with(model).and_return(model) @twitter.send(:bless_models, model) end - + it "should return nil when receiving nil and not raise any exceptions" do lambda { value = @twitter.send(:bless_models, nil) value.should be_nil }.should_not raise_error end - + after(:each) do nilize(@twitter, @models) end @@ -211,6 +211,14 @@ def get_consumer(client) end end +describe Twitter::Client, "media consumer token" do + it_should_behave_like "consumer token overrides" + + def get_consumer(client) + client.send(:media_consumer) + end +end + describe Twitter::Client, "#construct_proxy_url" do before(:each) do @host = "localhost" @@ -290,3 +298,18 @@ def get_consumer @client.send(:search_consumer) end end + +describe Twitter::Client, "#media_consumer" do + it_should_behave_like "consumer initialization with timeout" + before(:each) do + @timeout = 96 + end + + def timeout + @timeout + end + + def get_consumer + @client.send(:media_consumer) + end +end diff --git a/spec/twitter/client/status_spec.rb b/spec/twitter/client/status_spec.rb index 813b0df..e0695c4 100644 --- a/spec/twitter/client/status_spec.rb +++ b/spec/twitter/client/status_spec.rb @@ -13,33 +13,42 @@ @status = Twitter::Status.new(:id => 2349343) @reply_to_status_id = 3495293 @source = Twitter::Client.class_eval("@@defaults[:source]") + @image_data = "----IMAGEDATA######" + @image_part = Twitter::MediaPart.new( + :name => "media[]", :filename => "image.png", + :content_type => "application/octet-stream", + :body => @image_data) + @status_part = Twitter::MediaPart.new( + :name => "status", + :content_type => "text/plain", + :body => @message) end it "should return nil if nil is passed as value argument for :get case" do status = @twitter.status(:get, nil) status.should be_nil end - + it "should not call @twitter#http_connect when passing nil for value argument in :get case" do @twitter.should_not_receive(:http_connect) @twitter.status(:get, nil) end - + it "should create expected HTTP GET request for :get case" do @twitter.should_receive(:rest_oauth_connect).with(:get, @uris[:get], @options).and_return(@response) @twitter.status(:get, @options[:id]) end - + it "should invoke @twitter#rest_oauth_connect with given parameters equivalent to {:id => value.to_i} for :get case" do # Float case @twitter.should_receive(:rest_oauth_connect).with(:get, @uris[:get], {:id => @float.to_i}).and_return(@response) @twitter.status(:get, @float) # Twitter::Status object case - @twitter.should_receive(:rest_oauth_connect).with(:get, @uris[:get], {:id => @status.to_i}).and_return(@response) + @twitter.should_receive(:rest_oauth_connect).with(:get, @uris[:get], {:id => @status.to_i}).and_return(@response) @twitter.status(:get, @status) end - + it "should create expected HTTP POST request for :post case" do @twitter.should_receive(:rest_oauth_connect).with(:post, @uris[:post], :status => @message, :source => @source).and_return(@response) @twitter.status(:post, @message) @@ -50,36 +59,49 @@ @twitter.status(:post, :status => @message, :lat => 0, :long => 0) end - it "should create expected HTTP POST request for :post case when passing Hash with place_idinstead of String" do + it "should create expected HTTP POST request for :post case when passing Hash with place_id instead of String" do @twitter.should_receive(:rest_oauth_connect).with(:post, @uris[:post], :place_id => 1234, :status => @message, :source => @source).and_return(@response) @twitter.status(:post, :status => @message, :place_id => 1234) end + it "should create expected HTTP POST request for :post case when passing Hash with media entries" do + @twitter.should_receive(:media_oauth_connect). + with(:post, @uris[:post_multipart], hash_including(:parts)). + and_return(@response) + @twitter.status(:post, + :media => { + :filename => "image.png", + :content_type => "application/octet-stream", + :body => @image_data}, + :status => @message + ) + end + it "should return nil if nil is passed as value argument for :post case" do status = @twitter.status(:post, nil) status.should be_nil end - + it "should return nil if no :status key-value given in the value argument for :reply case" do status = @twitter.status(:reply, {}) status.should be_nil end - + it "should return nil if nil is passed as value argument for :reply case" do status = @twitter.status(:reply, nil) status.should be_nil end - + it "should create expected HTTP POST request for :reply case" do @twitter.should_receive(:rest_oauth_connect).with(:post, @uris[:reply], :status => @message, :source => @source, :in_reply_to_status_id => @reply_to_status_id).and_return(@response) @twitter.status(:reply, :status => @message, :in_reply_to_status_id => @reply_to_status_id) end - + it "should return nil if nil is passed as value argument for :delete case" do status = @twitter.status(:delete, nil) status.should be_nil end - + it "should create expected HTTP DELETE request for :delete case" do @twitter.should_receive(:rest_oauth_connect).with(:delete, @uris[:delete], @options).and_return(@response) @twitter.status(:delete, @options[:id]) @@ -94,13 +116,13 @@ @twitter.should_receive(:rest_oauth_connect).with(:delete, @uris[:delete], {:id => @status.to_i}).and_return(@response) @twitter.status(:delete, @status) end - + it "should raise an ArgumentError when given an invalid status action" do lambda { @twitter.status(:crap, nil) }.should raise_error(ArgumentError) end - + after(:each) do nilize(@twitter) end diff --git a/spec/twitter/config_spec.rb b/spec/twitter/config_spec.rb index 97e28dc..1a93ba2 100644 --- a/spec/twitter/config_spec.rb +++ b/spec/twitter/config_spec.rb @@ -3,13 +3,13 @@ describe Twitter::Client, ".configure" do it "should respond to :configure class method" do Twitter::Client.respond_to?(:configure).should be(true) - end + end it "should not accept calls that do not specify blocks" do lambda { Twitter::Client.configure() }.should raise_error(ArgumentError) - end + end end describe Twitter::Client, ".configure with mocked @config" do @@ -17,25 +17,25 @@ @block_invoked = false @conf_yielded = false @conf = mock(Twitter::Config) - @block = Proc.new do |conf| + @block = Proc.new do |conf| @block_invoked = true @conf_yielded = true if conf.is_a?(Twitter::Config) end Twitter::Config.stub!(:new).and_return(@conf) end - + it "should not raise an error when passing block" do lambda { Twitter::Client.configure(&@block) }.should_not raise_error end - + it "should yield a Twitter::Client object to block" do Twitter::Client.configure(&@block) @block_invoked.should be(true) @conf_yielded.should be(true) end - + after(:each) do nilize(@block, @block_invoked, @conf, @conf_yielded) end @@ -57,29 +57,29 @@ } @obj = Twitter::Config.new(attrs) @other = Twitter::Config.new(attrs) - + @different = stubbed_twitter_config(Twitter::Config.new, attrs.merge(:proxy_host => 'different.proxy')) @same = @obj end - + it "should return true for two logically equivalent objects" do @obj.should be_eql(@other) @other.should be_eql(@obj) end - + it "should return false for two logically different objects" do @obj.should_not be_eql(@different) @different.should_not be_eql(@obj) @other.should_not be_eql(@different) @different.should_not be_eql(@other) end - + it "should return true for references to the same object in memory" do @obj.should eql(@same) @same.should eql(@obj) @other.should eql(@other) end - + after(:each) do nilize(@protocol, @host, @port, @proxy_host, @proxy_port, @obj, @other, @different, @same) end diff --git a/spec/twitter/core_spec.rb b/spec/twitter/core_spec.rb index 359fa5c..6c64018 100644 --- a/spec/twitter/core_spec.rb +++ b/spec/twitter/core_spec.rb @@ -8,7 +8,7 @@ class TestClass end @init_hash = { :var1 => 'val1', :var2 => 'val2', :var3 => 'val3' } end - + it "should have Twitter::ClassUtilMixin as an included module" do TestClass.included_modules.member?(Twitter::ClassUtilMixin).should be(true) end @@ -19,7 +19,7 @@ class TestClass test.send(key).should eql(val) end end - + it "should not set attributes passed in the hash that are not attributes in TestClass.new" do test = nil lambda { test = TestClass.new(@init_hash.merge(:var4 => 'val4')) }.should_not raise_error @@ -34,7 +34,7 @@ class TestClass @error = Twitter::RESTError.new(@hash) @expected_message = "HTTP #{@hash[:code]}: #{@hash[:message]} at #{@hash[:uri]}" end - + it "should return @expected_message" do @error.to_s.should eql(@expected_message) end @@ -55,18 +55,18 @@ class MyCustomError < Twitter::RESTError; register('999'); end describe "Twitter::Status#eql?" do before(:each) do @id = 34329594003 - @attr_hash = { :text => 'Status', :id => @id, + @attr_hash = { :text => 'Status', :id => @id, :user => { :name => 'Tess', :description => "Unfortunate D'Urberville", :location => 'Dorset', :url => nil, :id => 34320304, - :screen_name => 'maiden_no_more' }, + :screen_name => 'maiden_no_more' }, :created_at => 'Wed May 02 03:04:54 +0000 2007'} @obj = Twitter::Status.new @attr_hash @other = Twitter::Status.new @attr_hash end - + it "should return true when non-transient object attributes are eql?" do @obj.should eql(@other) end @@ -75,7 +75,7 @@ class MyCustomError < Twitter::RESTError; register('999'); end @other.created_at = Time.now.to_s @obj.should_not eql(@other) end - + it "should return true when comparing same object to itself" do @obj.should eql(@obj) @other.should eql(@other) @@ -93,17 +93,17 @@ class MyCustomError < Twitter::RESTError; register('999'); end @obj = Twitter::User.new @attr_hash @other = Twitter::User.new @attr_hash end - + it "should return true when non-transient object attributes are eql?" do @obj.should eql(@other) end - + it "should return false when not all non-transient object attributes are eql?" do @other.id = 1 @obj.should_not eql(@other) @obj.eql?(@other).should be(false) end - + it "should return true when comparing same object to itself" do @obj.should eql(@obj) @other.should eql(@other) @@ -117,23 +117,23 @@ class TestClass end @test_subject = TestClass.new end - + it "should respond to :require_block" do @test_subject.should respond_to(:require_block) end - + it "should raise ArgumentError when block not given" do lambda { @test_subject.send(:require_block, false) }.should raise_error(ArgumentError) end - + it "should not raise ArgumentError when block is given" do lambda { @test_subject.send(:require_block, true) }.should_not raise_error(ArgumentError) end - + after(:each) do @test_subject = nil end @@ -202,3 +202,52 @@ def error_response_code; :service_unavailable; end it_should_behave_like "REST error returned" end +describe Twitter::MediaPart do + before(:each) do + @text_content = "My awesome tweet here!" + @image_content = "(FC@JOCWEOFHEWRGHWEOVAVOCNWDQ" + @base64_content = Base64.encode64(@image_content) + @text_content_type = "text/plain; charset=utf-8" + @image_content_type = "application/octet-stream" + @image_part = Twitter::MediaPart.new( + :name => "media[]", + :filename => "image.png", + :content_type => @image_content_type, + :body => @image_content) + @text_part = Twitter::MediaPart.new( + :name => "status", + :content_type => @text_content_type, + :body => @text_content) + @text_output = %{\r\nContent-Disposition: form-data; name="status"\r\nContent-Type: #{@text_content_type}\r\n\r\n#{@text_content}\r\n} + @image_output = %{\r\nContent-Disposition: form-data; name="media[]"; filename="image.png"\r\nContent-Type: #{@image_content_type}\r\nContent-Transfer-Encoding: Base64\r\n\r\n#{@base64_content}\r\n} + end + + it "should generate body part string per multipart format" do + @text_part.to_s.should === @text_output + @image_part.to_s.should === @image_output + end +end + +describe Twitter::MultiPartBody do + before(:each) do + @status_type = "text/plain" + @status_text = "Can't wait for my Ninja Blocks to arrive!;)" + @status_part = Twitter::MediaPart.new( + :name => "status", + :content_type => @status_type, + :body => @status_text) + @image_part = Twitter::MediaPart.new( + :name => "media[]", + :body => "----BODY-----", + :content_type => "image/png") + @digest = "34922jgfejfwef" + @boundary = @digest + Digest::SHA1.stub(:hexdigest).and_return(@digest) # ignore the fact it isn't a hexdigest + @multipart = Twitter::MultiPartBody.new(@image_part, @status_part) + @expected = %{--#{@boundary}#{@image_part.to_s}--#{@boundary}#{@status_part.to_s}--#{@boundary}--} + end + + it "shold generate the multipart format expected by HTTP specs" do + @multipart.to_s.should eql(@expected) + end +end diff --git a/spec/twitter/model_spec.rb b/spec/twitter/model_spec.rb index 48a8e37..470de1d 100644 --- a/spec/twitter/model_spec.rb +++ b/spec/twitter/model_spec.rb @@ -23,17 +23,17 @@ class Model @status = Twitter::Status.new @json_hash @status.user = @user end - + it "should respond to unmarshal class method" do Twitter::Status.should respond_to(:unmarshal) end - + it "should return expected Twitter::Status object for singular case" do status = Twitter::Status.unmarshal(JSON.unparse(@json_hash)) status.should_not be(nil) status.should eql(@status) end - + it "should return expected array of Twitter::Status objects for plural case" do statuses = Twitter::Status.unmarshal(JSON.unparse([@json_hash])) statuses.should_not be(nil) @@ -53,17 +53,17 @@ class Model "screen_name" => "LucyDominatrix", } @user = Twitter::User.new @json_hash end - + it "should respond to unmarshal class method" do Twitter::User.should respond_to(:unmarshal) end - + it "should return expected arry of Twitter::User objects for plural case" do users = Twitter::User.unmarshal(JSON.unparse([@json_hash])) users.should have(1).entries users.first.should eql(@user) end - + it "should return expected Twitter::User object for singular case" do user = Twitter::User.unmarshal(JSON.unparse(@json_hash)) user.should_not be(nil) @@ -79,7 +79,7 @@ class Model attr_accessor *@@ATTRIBUTES def self.attributes; @@ATTRIBUTES; end end - + class Hash def eql?(other) return false unless other # trivial nil case. @@ -96,11 +96,11 @@ def eql?(other) @attributes = {:id => 14, :name => 'State', :value => 'Illinois'} @model = Model.new(@attributes) end - + it "should return expected hash representation of given model object" do @model.to_hash.should eql(@attributes) end - + after(:each) do nilize(@attributes, @model) end @@ -113,19 +113,19 @@ def eql?(other) @screen_name = 'ascreenname' @expected_user = Twitter::User.new(:id => @id, :screen_name => @screen_name) end - + it "should invoke given Twitter::Client's #user method with expected arguments" do # case where id => @id @twitter.should_receive(:user).with(@id).and_return(@expected_user) user = Twitter::User.find(@id, @twitter) user.should eql(@expected_user) - + # case where id => @screen_name, which is also valid @twitter.should_receive(:user).with(@screen_name).and_return(@expected_user) user = Twitter::User.find(@screen_name, @twitter) user.should eql(@expected_user) end - + after(:each) do nilize(@twitter, @id, @screen_name, @expected_user) end @@ -139,13 +139,13 @@ def eql?(other) @user = Twitter::User.new(:id => @id, :screen_name => @screen_name) @expected_status = Twitter::Status.new(:id => @id, :text => @text, :user => @user) end - + it "should invoke given Twitter::Client's #status method with expected arguments" do @twitter.should_receive(:status).with(:get, @id).and_return(@expected_status) status = Twitter::Status.find(@id, @twitter) status.should eql(@expected_status) end - + after(:each) do nilize(@twitter, @id, @text, @user, @expected_status) end @@ -156,17 +156,17 @@ def eql?(other) @twitter = Twitter::Client.from_config('config/twitter.yml') @model = Test::Model.new end - + it "should delegate to #basic_bless" do @model.should_receive(:basic_bless).and_return(@twitter) @model.bless(@twitter) end - + it "should set client attribute of self" do @model.should_receive(:client=).once @model.bless(@twitter) end - + after(:each) do nilize(@model, @twitter) end @@ -180,15 +180,15 @@ def eql?(other) @user_not_me.bless(@twitter) @user_me.bless(@twitter) end - + it "should return true when Twitter::User object represents authenticated user of client context" do @user_me.is_me?.should be_true end - + it "should return false when Twitter::User object does not represent authenticated user of client context" do @user_not_me.is_me?.should be_false end - + after(:each) do nilize(@twitter, @user_not_me, @user_me) end @@ -201,12 +201,12 @@ def eql?(other) @user = Twitter::User.new(:id => @id, :screen_name => 'twitter4r') @user.bless(@twitter) end - + it "should delegate to @client.user(@id, :friends)" do @twitter.should_receive(:user).with(@id, :friends) @user.friends end - + after(:each) do nilize(@twitter, @id, @user) end @@ -216,17 +216,17 @@ def eql?(other) before(:each) do @twitter = Twitter::Client.from_config('config/twitter.yml') @id = 5701682 - @user = Twitter::User.new(:id => @id, :screen_name => 'twitter4r') + @user = Twitter::User.new(:id => @id, :screen_name => 'twitter4r') @user.bless(@twitter) end - + it "should delegate to @client.my(:followers)" do @twitter.should_receive(:my).with(:followers, {}) @user.followers end - + after(:each) do - nilize(@twitter, @id, @user) + nilize(@twitter, @id, @user) end end @@ -238,11 +238,11 @@ class Test::Model end @model = Test::Model.new(:id => @id) end - + it "should return @id attribute" do @model.to_i.should eql(@id) end - + after(:each) do nilize(@model, @id) end @@ -256,11 +256,11 @@ class Test::Model @text = 'Some text for the message body here' @model = Test::Model.new(:text => @text) end - + it "should return expected text when a @text attribute exists for the model" do @model.to_s.should eql(@text) end - + after(:each) do nilize(@model) end @@ -280,37 +280,37 @@ class Test::Model @text = 'My status update' @status = Twitter::Status.new(:text => @text, :client => @twitter) end - + it "should invoke #status(:post, text) on client context given" do @twitter.should_receive(:status).with(:post, @text).and_return(@status) Twitter::Status.create(:text => @text, :client => @twitter) end - + it "should raise an ArgumentError when no client is given in params" do lambda { Twitter::Status.create(:text => @text) }.should raise_error(ArgumentError) end - + it "should raise an ArgumentError when no text is given in params" do @twitter.should_receive(:is_a?).with(Twitter::Client) lambda { Twitter::Status.create(:client => @twitter) }.should raise_error(ArgumentError) end - + it "should raise an ArgumentError when text given in params is not a String" do lambda { Twitter::Status.create(:client => @twitter, :text => 234493) }.should raise_error(ArgumentError) end - + it "should raise an ArgumentError when client context given in params is not a Twitter::Client object" do lambda { Twitter::Status.create(:client => 'a string instead of a Twitter::Client', :text => @text) }.should raise_error(ArgumentError) end - + after(:each) do nilize(@twitter, @text, @status) end @@ -323,27 +323,27 @@ class Test::Model @recipient = Twitter::User.new(:id => 234958) @message = Twitter::Message.new(:text => @text, :recipient => @recipient) end - + it "should invoke #message(:post, text, recipient) on client context given" do @twitter.should_receive(:message).with(:post, @text, @recipient).and_return(@message) Twitter::Message.create(:client => @twitter, :text => @text, :recipient => @recipient) end - + it "should raise an ArgumentError if no client context is given in params" do lambda { Twitter::Message.create(:text => @text, :recipient => @recipient) }.should raise_error(ArgumentError) end - + it "should raise an ArgumentError if client conext given in params is not a Twitter::Client object" do lambda { Twitter::Message.create( - :client => 3.14159, - :text => @text, + :client => 3.14159, + :text => @text, :recipient => @recipient) }.should raise_error(ArgumentError) end - + it "should raise an ArgumentError if no text is given in params" do @twitter.should_receive(:is_a?).with(Twitter::Client) lambda { @@ -352,7 +352,7 @@ class Test::Model :recipient => @recipient) }.should raise_error(ArgumentError) end - + it "should raise an ArgumentError if text given in params is not a String" do @twitter.should_receive(:is_a?).with(Twitter::Client) lambda { @@ -362,7 +362,7 @@ class Test::Model :recipient => @recipient) }.should raise_error(ArgumentError) end - + it "should raise an ArgumentError if no recipient is given in params" do @text.should_receive(:is_a?).with(String) lambda { @@ -371,7 +371,7 @@ class Test::Model :text => @text) }.should raise_error(ArgumentError) end - + it "should raise an ArgumentError if recipient given in params is not a Twitter::User, Integer or String object" do @text.should_receive(:is_a?).with(String) lambda { @@ -381,7 +381,7 @@ class Test::Model :recipient => 3.14159) }.should raise_error(ArgumentError) end - + after(:each) do nilize(@twitter, @text, @recipient, @message) end @@ -391,20 +391,20 @@ class Test::Model before(:each) do @twitter = client_context @user = Twitter::User.new( - :id => 1234, + :id => 1234, :screen_name => 'mylogin', :client => @twitter) @friend = Twitter::User.new( - :id => 5678, + :id => 5678, :screen_name => 'friend', :client => @twitter) end - + it "should invoke #friend(:add, user) on client context" do @twitter.should_receive(:friend).with(:add, @friend).and_return(@friend) @user.befriend(@friend) end - + after(:each) do nilize(@twitter, @user, @friend) end @@ -414,20 +414,20 @@ class Test::Model before(:each) do @twitter = client_context @user = Twitter::User.new( - :id => 1234, + :id => 1234, :screen_name => 'mylogin', :client => @twitter) @friend = Twitter::User.new( - :id => 5678, + :id => 5678, :screen_name => 'friend', :client => @twitter) end - + it "should invoke #friend(:remove, user) on client context" do @twitter.should_receive(:friend).with(:remove, @friend).and_return(@friend) @user.defriend(@friend) end - + after(:each) do nilize(@twitter, @user, @friend) end @@ -437,7 +437,7 @@ class Test::Model before(:each) do @status = Twitter::Status.new( :id => 123456789, - :text => "Wazzup?", + :text => "Wazzup?", :user => mock(Twitter::User) ) @reply = Twitter::Status.new( @@ -477,7 +477,7 @@ class Test::Model after(:each) do nilize(@twitter, @status) - end + end end describe Twitter::Status, "#to_s" do @@ -485,11 +485,11 @@ class Test::Model @text = 'Aloha' @status = Twitter::Status.new(:text => @text) end - + it "should render text attribute" do @status.to_s.should be(@text) end - + after(:each) do nilize(@text, @status) end @@ -500,11 +500,11 @@ class Test::Model @text = 'Aloha' @message = Twitter::Message.new(:text => @text) end - + it "should render text attribute" do @message.to_s.should be(@text) end - + after(:each) do nilize(@text, @message) end diff --git a/tasks/doc.rake b/tasks/doc.rake index c726f61..7c98477 100644 --- a/tasks/doc.rake +++ b/tasks/doc.rake @@ -5,8 +5,8 @@ Rake::RDocTask.new do |rdoc| rdoc.rdoc_dir = 'doc/rdoc' rdoc.title = "Twitter4R v#{Twitter::Version.to_version}: Idiomatic Ruby Open Source Library for the Twitter REST and Search APIs" # rdoc.template = File.join(File.dirname(__FILE__), '..', 'config', 'rdoc_template.rb') - rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README' << '--line-numbers' - rdoc.rdoc_files.include('README') + rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README.md' << '--line-numbers' + rdoc.rdoc_files.include('README.md') rdoc.rdoc_files.include('CHANGES') rdoc.rdoc_files.include('MIT-LICENSE') rdoc.rdoc_files.include('lib/**/*.rb')