Make Twitter::Trendline embody trends collection
[twitter4r-core.git] / lib / twitter / model.rb
blob03bd16f190100845bcc00defc6b2a1d552392eef
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   class Trendline
159     include ModelMixin
160     include Enumerable
161     include Comparable
163     @@ATTRIBUTES = [:as_of, :type]
164     attr_accessor(*@@ATTRIBUTES)
166     class << self
167       def attributes; @@ATTRIBUTES; end
168     end
170     # Spaceship operator definition needed by Comparable mixin
171     # for sort, etc.
172     def <=>(other)
173       self.type === other.type && self.as_of <=> other.as_of
174     end
176     # each definition needed by Enumerable mixin for first, ...
177     def each
178       trends.each do |t|
179         yield t
180       end
181     end
183     # index operator definition needed to iterate over trends 
184     # in the +::Twitter::Trendline+ object using for or otherwise
185     def [](index)
186       trends[index]
187     end
189     protected
190       attr_accessor(:trends)
191       # Constructor callback
192       def init
193         @trends = @trends.collect do |trend|
194           ::Twitter::Trend.new(trend) if trend.is_a?(Hash)
195         end if @trends.is_a?(Array)
196       end
197   end
199   class Trend
200     include ModelMixin
201     @@ATTRIBUTES = [:name, :url]
202     attr_accessor(*@@ATTRIBUTES)
204     class << self
205       def attributes; @@ATTRIBUTES; end
206     end
207   end
209   # Represents a <tt>Twitter</tt> user
210   class User
211     include ModelMixin
212     @@ATTRIBUTES = [:id, :name, :description, :location, :screen_name, :url, 
213       :protected, :profile_image_url, :profile_background_color, 
214       :profile_text_color, :profile_link_color, :profile_sidebar_fill_color, 
215       :profile_sidebar_border_color, :profile_background_image_url, 
216       :profile_background_tile, :utc_offset, :time_zone, 
217       :following, :notifications, :favourites_count, :followers_count, 
218       :friends_count, :statuses_count, :created_at ]
219     attr_accessor(*@@ATTRIBUTES)
221     class << self
222       # Used as factory method callback
223       def attributes; @@ATTRIBUTES; end
225       # Returns user model object with given <tt>id</tt> using the configuration
226       # and credentials of the <tt>client</tt> object passed in.
227       # 
228       # You can pass in either the user's unique integer ID or the user's 
229       # screen name.
230       def find(id, client)
231         client.user(id)
232       end
233     end
234     
235     # Override of ModelMixin#bless method.
236     # 
237     # Adds #followers instance method when user object represents 
238     # authenticated user.  Otherwise just do basic bless.
239     # 
240     # This permits applications using <tt>Twitter4R</tt> to write 
241     # Rubyish code like this:
242     #  followers = user.followers if user.is_me?
243     # Or:
244     #  followers = user.followers if user.respond_to?(:followers)
245     def bless(client)
246       basic_bless(client)
247       self.instance_eval(%{
248         self.class.send(:include, Twitter::AuthenticatedUserMixin)
249       }) if self.is_me? and not self.respond_to?(:followers)
250       self
251     end
252     
253     # Returns whether this <tt>Twitter::User</tt> model object
254     # represents the authenticated user of the <tt>client</tt>
255     # that blessed it.
256     def is_me?
257       # TODO: Determine whether we should cache this or not?
258       # Might be dangerous to do so, but do we want to support
259       # the edge case where this would cause a problem?  i.e. 
260       # changing authenticated user after initial use of 
261       # authenticated API.
262       # TBD: To cache or not to cache.  That is the question!
263       # Since this is an implementation detail we can leave this for 
264       # subsequent 0.2.x releases.  It doesn't have to be decided before 
265       # the 0.2.0 launch.
266       @screen_name == @client.instance_eval("@login")
267     end
268     
269     # Returns an Array of user objects that represents the authenticated
270     # user's friends on Twitter.
271     def friends
272       @client.user(@id, :friends)
273     end
274   end # User
275   
276   # Represents a status posted to <tt>Twitter</tt> by a <tt>Twitter</tt> user.
277   class Status
278     include ModelMixin
279     @@ATTRIBUTES = [:id, :id_str, :text, :source, :truncated, :created_at, :user, 
280                     :from_user, :to_user, :favorited, :in_reply_to_status_id, 
281                     :in_reply_to_user_id, :in_reply_to_screen_name, :geo]
282     attr_accessor(*@@ATTRIBUTES)
284     class << self
285       # Used as factory method callback
286       def attributes; @@ATTRIBUTES; end
287       
288       # Returns status model object with given <tt>status</tt> using the 
289       # configuration and credentials of the <tt>client</tt> object passed in.
290       def find(id, client)
291         client.status(:get, id)
292       end
293       
294       # Creates a new status for the authenticated user of the given 
295       # <tt>client</tt> context.
296       # 
297       # You MUST include a valid/authenticated <tt>client</tt> context 
298       # in the given <tt>params</tt> argument.
299       # 
300       # For example:
301       #  status = Twitter::Status.create(
302       #    :text => 'I am shopping for flip flops',
303       #    :client => client)
304       # 
305       # An <tt>ArgumentError</tt> will be raised if no valid client context
306       # is given in the <tt>params</tt> Hash.  For example,
307       #  status = Twitter::Status.create(:text => 'I am shopping for flip flops')
308       # The above line of code will raise an <tt>ArgumentError</tt>.
309       # 
310       # The same is true when you do not provide a <tt>:text</tt> key-value
311       # pair in the <tt>params</tt> argument given.
312       # 
313       # The Twitter::Status object returned after the status successfully
314       # updates on the Twitter server side is returned from this method.
315       def create(params)
316         client, text = params[:client], params[:text]
317         raise ArgumentError, 'Valid client context must be provided' unless client.is_a?(Twitter::Client)
318         raise ArgumentError, 'Must provide text for the status to update' unless text.is_a?(String)
319         client.status(:post, text)
320       end
321     end
323     def reply?
324       !!@in_reply_to_status_id
325     end
327     def reply(status)
328       client.status(:reply, :status => status, :in_reply_to_status_id => @id)
329     end
330     
331     protected
332       # Constructor callback
333       def init
334         @user = User.new(@user) if @user.is_a?(Hash)
335         @created_at = Time.parse(@created_at) if @created_at.is_a?(String)
336       end    
337   end # Status
338   
339   # Represents a direct message on <tt>Twitter</tt> between <tt>Twitter</tt> users.
340   class Message
341     include ModelMixin
342     @@ATTRIBUTES = [:id, :recipient, :sender, :text, :geo, :created_at]
343     attr_accessor(*@@ATTRIBUTES)
344     
345     class << self
346       # Used as factory method callback
347       def attributes; @@ATTRIBUTES; end
348       
349       # Raises <tt>NotImplementedError</tt> because currently 
350       # <tt>Twitter</tt> doesn't provide a facility to retrieve 
351       # one message by unique ID.
352       def find(id, client)
353         raise NotImplementedError, 'Twitter has yet to implement a REST API for this.  This is not a Twitter4R library limitation.'
354       end
355       
356       # Creates a new direct message from the authenticated user of the 
357       # given <tt>client</tt> context.
358       # 
359       # You MUST include a valid/authenticated <tt>client</tt> context 
360       # in the given <tt>params</tt> argument.
361       # 
362       # For example:
363       #  status = Twitter::Message.create(
364       #    :text => 'I am shopping for flip flops',
365       #    :recipient => 'anotherlogin',
366       #    :client => client)
367       # 
368       # An <tt>ArgumentError</tt> will be raised if no valid client context
369       # is given in the <tt>params</tt> Hash.  For example,
370       #  status = Twitter::Status.create(:text => 'I am shopping for flip flops')
371       # The above line of code will raise an <tt>ArgumentError</tt>.
372       # 
373       # The same is true when you do not provide any of the following
374       # key-value pairs in the <tt>params</tt> argument given:
375       # * <tt>text</tt> - the String that will be the message text to send to <tt>user</tt>
376       # * <tt>recipient</tt> - the user ID, screen_name or Twitter::User object representation of the recipient of the direct message
377       # 
378       # The Twitter::Message object returned after the direct message is 
379       # successfully sent on the Twitter server side is returned from 
380       # this method.
381       def create(params)
382         client, text, recipient = params[:client], params[:text], params[:recipient]
383         raise ArgumentError, 'Valid client context must be given' unless client.is_a?(Twitter::Client)
384         raise ArgumentError, 'Message text must be supplied to send direct message' unless text.is_a?(String)
385         raise ArgumentError, 'Recipient user must be specified to send direct message' unless [Twitter::User, Integer, String].member?(recipient.class)
386         client.message(:post, text, recipient)
387       end
388     end
389     
390     protected
391       # Constructor callback
392       def init
393         @sender = User.new(@sender) if @sender.is_a?(Hash)
394         @recipient = User.new(@recipient) if @recipient.is_a?(Hash)
395         @created_at = Time.parse(@created_at) if @created_at.is_a?(String)
396       end
397   end # Message
398   
399   # RateLimitStatus provides information about how many requests you have left 
400   # and when you can resume more requests if your remaining_hits count is zero.
401   class RateLimitStatus
402     include ModelMixin
403     @@ATTRIBUTES = [:remaining_hits, :hourly_limit, :reset_time_in_seconds, :reset_time]
404     attr_accessor(*@@ATTRIBUTES)
405     
406     class << self
407       # Used as factory method callback
408       def attributes; @@ATTRIBUTES; end
409     end
410   end
411 end # Twitter