Fix Twitter::Status#reply(text) method to prefix @#{screen_name} to replies
[twitter4r-core.git] / lib / twitter / model.rb
bloba01a8d689605ed2fca9bd8ebcbe2bdc9b93bf812
1 # Contains Twitter4R Model API.
3 module Twitter
4   # Mixin module for model classes.  Includes generic class methods like
5   # unmarshal.
6   # 
7   # To create a new model that includes this mixin's features simply:
8   #  class NewModel
9   #    include Twitter::ModelMixin
10   #  end
11   # 
12   # This mixin module automatically includes <tt>Twitter::ClassUtilMixin</tt>
13   # features.
14   # 
15   # The contract for models to use this mixin correctly is that the class 
16   # including this mixin must provide an class method named <tt>attributes</tt>
17   # that will return an Array of attribute symbols that will be checked 
18   # in #eql? override method.  The following would be sufficient:
19   #  def self.attributes; @@ATTRIBUTES; end
20   module ModelMixin #:nodoc:
21     def self.included(base) #:nodoc:
22       base.send(:include, Twitter::ClassUtilMixin)
23       base.send(:include, InstanceMethods)
24       base.extend(ClassMethods)
25     end
27     # Class methods defined for <tt>Twitter::ModelMixin</tt> module.
28     module ClassMethods #:nodoc:
29       # Unmarshal object singular or plural array of model objects
30       # from JSON serialization.  Currently JSON is only supported 
31       # since this is all <tt>Twitter4R</tt> needs.
32       def unmarshal(raw)
33         input = JSON.parse(raw) if raw.is_a?(String)
34         def unmarshal_model(hash)
35           self.new(hash)
36         end
37         return unmarshal_model(input) if input.is_a?(Hash) # singular case
38         result = []
39         input.each do |hash|
40           model = unmarshal_model(hash) if hash.is_a?(Hash)
41           result << model
42         end if input.is_a?(Array)
43         result # plural case
44       end
45     end
46     
47     # Instance methods defined for <tt>Twitter::ModelMixin</tt> module.
48     module InstanceMethods #:nodoc:
49       attr_accessor :client
50       # Equality method override of Object#eql? default.
51       # 
52       # Relies on the class using this mixin to provide a <tt>attributes</tt>
53       # class method that will return an Array of attributes to check are 
54       # equivalent in this #eql? override.
55       # 
56       # It is by design that the #eql? method will raise a NoMethodError
57       # if no <tt>attributes</tt> class method exists, to alert you that 
58       # you must provide it for a meaningful result from this #eql? override.
59       # Otherwise this will return a meaningless result.
60       def eql?(other)
61         attrs = self.class.attributes
62         attrs.each do |att|
63           return false unless self.send(att).eql?(other.send(att))
64         end
65         true
66       end
67       
68       # Returns integer representation of model object instance.
69       # 
70       # For example,
71       #  status = Twitter::Status.new(:id => 234343)
72       #  status.to_i #=> 234343
73       def to_i
74         @id
75       end
76       
77       # Returns string representation of model object instance.
78       # 
79       # For example,
80       #  status = Twitter::Status.new(:text => 'my status message')
81       #  status.to_s #=> 'my status message'
82       # 
83       # If a model class doesn't have a @text attribute defined 
84       # the default Object#to_s will be returned as the result.
85       def to_s
86         self.respond_to?(:text) ? @text : super.to_s
87       end
88       
89       # Returns hash representation of model object instance.
90       # 
91       # For example,
92       #  u = Twitter::User.new(:id => 2342342, :screen_name => 'tony_blair_is_the_devil')
93       #  u.to_hash #=> {:id => 2342342, :screen_name => 'tony_blair_is_the_devil'}
94       # 
95       # This method also requires that the class method <tt>attributes</tt> be 
96       # defined to return an Array of attributes for the class.
97       def to_hash
98         attrs = self.class.attributes
99         result = {}
100         attrs.each do |att|
101           value = self.send(att)
102           value = value.to_hash if value.respond_to?(:to_hash)
103           result[att] = value if value
104         end
105         result
106       end
107       
108       # "Blesses" model object.
109       # 
110       # Should be overridden by model class if special behavior is expected
111       # 
112       # Expected to return blessed object (usually <tt>self</tt>)
113       def bless(client)
114         self.basic_bless(client)
115       end
116       
117       protected
118         # Basic "blessing" of model object 
119         def basic_bless(client)
120           self.client = client
121           self
122         end
123     end
124   end
125   
126   module AuthenticatedUserMixin
127     def self.included(base)
128       base.send(:include, InstanceMethods)
129     end
130         
131     module InstanceMethods
132       # Returns an Array of user objects that represents the authenticated
133       # user's friends on Twitter.
134       def followers(options = {})
135         @client.my(:followers, options)
136       end
137       
138       # Adds given user as a friend.  Returns user object as given by 
139       # <tt>Twitter</tt> REST server response.
140       # 
141       # For <tt>user</tt> argument you may pass in the unique integer 
142       # user ID, screen name or Twitter::User object representation.
143       def befriend(user)
144         @client.friend(:add, user)
145       end
146       
147       # Removes given user as a friend.  Returns user object as given by 
148       # <tt>Twitter</tt> REST server response.
149       # 
150       # For <tt>user</tt> argument you may pass in the unique integer 
151       # user ID, screen name or Twitter::User object representation.
152       def defriend(user)
153         @client.friend(:remove, user)
154       end
155     end
156   end
158   # Represents a location in Twitter
159   class Location
160     include ModelMixin
162     @@ATTRIBUTES = [:name, :woeid, :country, :url, :countryCode, :parentid, :placeType]
163     attr_accessor(*@@ATTRIBUTES)
165     class << self
166       def attributes; @@ATTRIBUTES; end
167     end
169     # Alias to +countryCode+ for those wanting to use consistent naming 
170     # convention for attribute
171     def country_code
172       @countryCode
173     end
175     # Alias to +parentid+ for those wanting to use consistent naming 
176     # convention for attribute
177     def parent_id
178       @parentid
179     end
181     # Alias to +placeType+ for those wanting to use consistent naming 
182     # convention for attribute
183     def place_type
184       @place_type
185     end
187     # Convenience method to output meaningful representation to STDOUT as per 
188     # Ruby convention
189     def inspect
190       "#{name} / #{woeid} / #{countryCode}\n#{url}\n"
191     end
193     protected
194       def init
195         puts @placeType
196         @placeType = ::Twitter::PlaceType.new(:name => @placeType["name"], 
197                                               :code => @placeType["code"]) if @placeType.is_a?(Hash)
198       end
199   end
201   # Represents a type of a place.
202   class PlaceType
203     include ModelMixin
205     @@ATTRIBUTES = [:name, :code]
206     attr_accessor(*@@ATTRIBUTES)
208     class << self
209       def attributes; @@ATTRIBUTES; end
210     end
211   end
213   # Represents a sorted, dated and typed list of trends.
214   #
215   # To find out when this +Trendline+ was created query the +as_of+ attribute.
216   # To find out what type +Trendline+ is use the +type+ attribute.
217   # You can iterator over the trends in the +Trendline+ with +each+ or by 
218   # index, whichever you prefer.
219   class Trendline
220     include ModelMixin
221     include Enumerable
222     include Comparable
224     @@ATTRIBUTES = [:as_of, :type]
225     attr_accessor(*@@ATTRIBUTES)
227     class << self
228       def attributes; @@ATTRIBUTES; end
229     end
231     # Spaceship operator definition needed by Comparable mixin
232     # for sort, etc.
233     def <=>(other)
234       self.type === other.type && self.as_of <=> other.as_of
235     end
237     # each definition needed by Enumerable mixin for first, ...
238     def each
239       trends.each do |t|
240         yield t
241       end
242     end
244     # index operator definition needed to iterate over trends 
245     # in the +::Twitter::Trendline+ object using for or otherwise
246     def [](index)
247       trends[index]
248     end
250     protected
251       attr_accessor(:trends)
252       # Constructor callback
253       def init
254         @trends = @trends.collect do |trend|
255           ::Twitter::Trend.new(trend) if trend.is_a?(Hash)
256         end if @trends.is_a?(Array)
257       end
258   end
260   class Trend
261     include ModelMixin
262     @@ATTRIBUTES = [:name, :url]
263     attr_accessor(*@@ATTRIBUTES)
265     class << self
266       def attributes; @@ATTRIBUTES; end
267     end
268   end
270   # Represents a <tt>Twitter</tt> user
271   class User
272     include ModelMixin
273     @@ATTRIBUTES = [:id, :name, :description, :location, :screen_name, :url, 
274       :protected, :profile_image_url, :profile_background_color, 
275       :profile_text_color, :profile_link_color, :profile_sidebar_fill_color, 
276       :profile_sidebar_border_color, :profile_background_image_url, 
277       :profile_background_tile, :utc_offset, :time_zone, 
278       :following, :notifications, :favourites_count, :followers_count, 
279       :friends_count, :statuses_count, :created_at ]
280     attr_accessor(*@@ATTRIBUTES)
282     class << self
283       # Used as factory method callback
284       def attributes; @@ATTRIBUTES; end
286       # Returns user model object with given <tt>id</tt> using the configuration
287       # and credentials of the <tt>client</tt> object passed in.
288       # 
289       # You can pass in either the user's unique integer ID or the user's 
290       # screen name.
291       def find(id, client)
292         client.user(id)
293       end
294     end
295     
296     # Override of ModelMixin#bless method.
297     # 
298     # Adds #followers instance method when user object represents 
299     # authenticated user.  Otherwise just do basic bless.
300     # 
301     # This permits applications using <tt>Twitter4R</tt> to write 
302     # Rubyish code like this:
303     #  followers = user.followers if user.is_me?
304     # Or:
305     #  followers = user.followers if user.respond_to?(:followers)
306     def bless(client)
307       basic_bless(client)
308       self.instance_eval(%{
309         self.class.send(:include, Twitter::AuthenticatedUserMixin)
310       }) if self.is_me? and not self.respond_to?(:followers)
311       self
312     end
313     
314     # Returns whether this <tt>Twitter::User</tt> model object
315     # represents the authenticated user of the <tt>client</tt>
316     # that blessed it.
317     def is_me?
318       # TODO: Determine whether we should cache this or not?
319       # Might be dangerous to do so, but do we want to support
320       # the edge case where this would cause a problem?  i.e. 
321       # changing authenticated user after initial use of 
322       # authenticated API.
323       # TBD: To cache or not to cache.  That is the question!
324       # Since this is an implementation detail we can leave this for 
325       # subsequent 0.2.x releases.  It doesn't have to be decided before 
326       # the 0.2.0 launch.
327       @screen_name == @client.instance_eval("@login")
328     end
329     
330     # Returns an Array of user objects that represents the authenticated
331     # user's friends on Twitter.
332     def friends
333       @client.user(@id, :friends)
334     end
335   end # User
336   
337   # Represents a status posted to <tt>Twitter</tt> by a <tt>Twitter</tt> user.
338   class Status
339     include ModelMixin
340     @@ATTRIBUTES = [:id, :id_str, :text, :source, :truncated, :created_at, :user, 
341                     :from_user, :to_user, :favorited, :in_reply_to_status_id, 
342                     :in_reply_to_user_id, :in_reply_to_screen_name, :geo]
343     attr_accessor(*@@ATTRIBUTES)
345     class << self
346       # Used as factory method callback
347       def attributes; @@ATTRIBUTES; end
348       
349       # Returns status model object with given <tt>status</tt> using the 
350       # configuration and credentials of the <tt>client</tt> object passed in.
351       def find(id, client)
352         client.status(:get, id)
353       end
354       
355       # Creates a new status for the authenticated user of the given 
356       # <tt>client</tt> context.
357       # 
358       # You MUST include a valid/authenticated <tt>client</tt> context 
359       # in the given <tt>params</tt> argument.
360       # 
361       # For example:
362       #  status = Twitter::Status.create(
363       #    :text => 'I am shopping for flip flops',
364       #    :client => client)
365       # 
366       # An <tt>ArgumentError</tt> will be raised if no valid client context
367       # is given in the <tt>params</tt> Hash.  For example,
368       #  status = Twitter::Status.create(:text => 'I am shopping for flip flops')
369       # The above line of code will raise an <tt>ArgumentError</tt>.
370       # 
371       # The same is true when you do not provide a <tt>:text</tt> key-value
372       # pair in the <tt>params</tt> argument given.
373       # 
374       # The Twitter::Status object returned after the status successfully
375       # updates on the Twitter server side is returned from this method.
376       def create(params)
377         client, text = params[:client], params[:text]
378         raise ArgumentError, 'Valid client context must be provided' unless client.is_a?(Twitter::Client)
379         raise ArgumentError, 'Must provide text for the status to update' unless text.is_a?(String)
380         client.status(:post, text)
381       end
382     end
384     def reply?
385       !!@in_reply_to_status_id
386     end
388     # Convenience method to allow client developers to not have to worry about 
389     # setting the +in_reply_to_status_id+ attribute or prefixing the status 
390     # text with the +screen_name+ being replied to.
391     def reply(reply)
392       status_reply = "@#{user.screen_name} #{reply}"
393       client.status(:reply, :status => status_reply, 
394                     :in_reply_to_status_id => @id)
395     end
396     
397     protected
398       # Constructor callback
399       def init
400         @user = User.new(@user) if @user.is_a?(Hash)
401         @created_at = Time.parse(@created_at) if @created_at.is_a?(String)
402       end    
403   end # Status
404   
405   # Represents a direct message on <tt>Twitter</tt> between <tt>Twitter</tt> users.
406   class Message
407     include ModelMixin
408     @@ATTRIBUTES = [:id, :recipient, :sender, :text, :geo, :created_at]
409     attr_accessor(*@@ATTRIBUTES)
410     
411     class << self
412       # Used as factory method callback
413       def attributes; @@ATTRIBUTES; end
414       
415       # Raises <tt>NotImplementedError</tt> because currently 
416       # <tt>Twitter</tt> doesn't provide a facility to retrieve 
417       # one message by unique ID.
418       def find(id, client)
419         raise NotImplementedError, 'Twitter has yet to implement a REST API for this.  This is not a Twitter4R library limitation.'
420       end
421       
422       # Creates a new direct message from the authenticated user of the 
423       # given <tt>client</tt> context.
424       # 
425       # You MUST include a valid/authenticated <tt>client</tt> context 
426       # in the given <tt>params</tt> argument.
427       # 
428       # For example:
429       #  status = Twitter::Message.create(
430       #    :text => 'I am shopping for flip flops',
431       #    :recipient => 'anotherlogin',
432       #    :client => client)
433       # 
434       # An <tt>ArgumentError</tt> will be raised if no valid client context
435       # is given in the <tt>params</tt> Hash.  For example,
436       #  status = Twitter::Status.create(:text => 'I am shopping for flip flops')
437       # The above line of code will raise an <tt>ArgumentError</tt>.
438       # 
439       # The same is true when you do not provide any of the following
440       # key-value pairs in the <tt>params</tt> argument given:
441       # * <tt>text</tt> - the String that will be the message text to send to <tt>user</tt>
442       # * <tt>recipient</tt> - the user ID, screen_name or Twitter::User object representation of the recipient of the direct message
443       # 
444       # The Twitter::Message object returned after the direct message is 
445       # successfully sent on the Twitter server side is returned from 
446       # this method.
447       def create(params)
448         client, text, recipient = params[:client], params[:text], params[:recipient]
449         raise ArgumentError, 'Valid client context must be given' unless client.is_a?(Twitter::Client)
450         raise ArgumentError, 'Message text must be supplied to send direct message' unless text.is_a?(String)
451         raise ArgumentError, 'Recipient user must be specified to send direct message' unless [Twitter::User, Integer, String].member?(recipient.class)
452         client.message(:post, text, recipient)
453       end
454     end
455     
456     protected
457       # Constructor callback
458       def init
459         @sender = User.new(@sender) if @sender.is_a?(Hash)
460         @recipient = User.new(@recipient) if @recipient.is_a?(Hash)
461         @created_at = Time.parse(@created_at) if @created_at.is_a?(String)
462       end
463   end # Message
464   
465   # RateLimitStatus provides information about how many requests you have left 
466   # and when you can resume more requests if your remaining_hits count is zero.
467   class RateLimitStatus
468     include ModelMixin
469     @@ATTRIBUTES = [:remaining_hits, :hourly_limit, :reset_time_in_seconds, :reset_time]
470     attr_accessor(*@@ATTRIBUTES)
471     
472     class << self
473       # Used as factory method callback
474       def attributes; @@ATTRIBUTES; end
475     end
476   end
477 end # Twitter