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