PageRenderTime 70ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/facebook_session.rb

https://github.com/NoJster/rfacebook
Ruby | 376 lines | 170 code | 68 blank | 138 comment | 15 complexity | 62a32aef55a466f66233fa7db97000fe MD5 | raw file
  1. # Copyright (c) 2007, Matt Pizzimenti (www.livelearncode.com)
  2. # Copyright (c) 2009, Nils o. Janus (we.bnam.es)
  3. # All rights reserved.
  4. #
  5. # Redistribution and use in source and binary forms, with or without modification,
  6. # are permitted provided that the following conditions are met:
  7. #
  8. # Redistributions of source code must retain the above copyright notice,
  9. # this list of conditions and the following disclaimer.
  10. #
  11. # Redistributions in binary form must reproduce the above copyright notice,
  12. # this list of conditions and the following disclaimer in the documentation
  13. # and/or other materials provided with the distribution.
  14. #
  15. # Neither the name of the original author nor the names of contributors
  16. # may be used to endorse or promote products derived from this software
  17. # without specific prior written permission.
  18. #
  19. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  20. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  21. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  22. # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  23. # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  24. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  25. # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  26. # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  27. # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. #
  30. require "digest/md5"
  31. require "net/https"
  32. require "cgi"
  33. require "facepricot"
  34. module RFacebook
  35. # TODO: better handling of session expiration
  36. # TODO: support Bebo's API
  37. API_VERSION = "1.0"
  38. API_HOST = "api.facebook.com"
  39. API_PATH_REST = "/restserver.php"
  40. WWW_HOST = "www.facebook.com"
  41. WWW_PATH_LOGIN = "/login.php"
  42. WWW_PATH_PERMISSION = "/connect/prompt_permissions.php"
  43. WWW_PATH_ADD = "/add.php"
  44. WWW_PATH_INSTALL = "/install.php"
  45. class FacebookSession
  46. ################################################################################################
  47. ################################################################################################
  48. # :section: Error classes
  49. ################################################################################################
  50. # TODO: better exception classes in v1.0?
  51. class RemoteStandardError < StandardError
  52. attr_reader :code
  53. def initialize(message, code)
  54. @code = code
  55. end
  56. end
  57. class ExpiredSessionStandardError < RemoteStandardError; end
  58. class NotActivatedStandardError < StandardError; end
  59. ################################################################################################
  60. ################################################################################################
  61. # :section: Properties
  62. ################################################################################################
  63. # The user id of the user associated with this sesssion.
  64. attr_reader :session_user_id
  65. # The key for this session. You will need to save this for infinite sessions.
  66. attr_reader :session_key
  67. # The expiration time of this session, as given from Facebook API login.
  68. attr_reader :session_expires
  69. # Can be set to any valid logger (for example, RAIL_DEFAULT_LOGGER)
  70. attr_accessor :logger
  71. ################################################################################################
  72. ################################################################################################
  73. # :section: Public Interface
  74. ################################################################################################
  75. # Constructs a FacebookSession.
  76. #
  77. # api_key:: your API key
  78. # api_secret:: your API secret
  79. # quiet:: boolean, set to true if you don't want exceptions to be thrown (defaults to false)
  80. def initialize(api_key, api_secret, quiet = false)
  81. # required parameters
  82. @api_key = api_key
  83. @api_secret = api_secret
  84. # optional parameters
  85. @quiet = quiet
  86. # initialize internal state
  87. @last_error_message = nil # DEPRECATED
  88. @last_error_code = nil # DEPRECATED
  89. @expired = false
  90. end
  91. # Template method. Returns true when the session is definitely prepared to make API calls.
  92. def ready?
  93. raise NotImplementedError
  94. end
  95. # Returns true if the session is expired (will often mean that the session is not ready as well)
  96. def expired?
  97. return @expired
  98. end
  99. # Returns true if exceptions are being suppressed in favor of log messages
  100. def quiet?
  101. return @quiet
  102. end
  103. # Sets whether or not we suppress exceptions from being thrown
  104. def quiet=(val)
  105. @quiet = val
  106. end
  107. # Template method. Used for signing a set of parameters in the way that Facebook
  108. # specifies: <http://developers.facebook.com/documentation.php?v=1.0&doc=auth>
  109. #
  110. # params:: a Hash containing the parameters to sign
  111. def signature(params)
  112. raise NotImplementedError
  113. end
  114. ################################################################################################
  115. ################################################################################################
  116. # :section: Utility methods
  117. ################################################################################################
  118. private
  119. # This allows *any* Facebook method to be called, using the Ruby
  120. # mechanism for responding to unimplemented methods. Basically,
  121. # this converts a call to "auth_getSession" to "auth.getSession"
  122. # and does the proper API call using the parameter hash given.
  123. #
  124. # This allows you to call an API method such as facebook.users.getInfo
  125. # by calling "fbsession.users_getInfo"
  126. def method_missing(methodSymbol, *params)
  127. # get the remote method name
  128. remoteMethod = methodSymbol.to_s.gsub("_", ".")
  129. if methodSymbol.to_s.match(/cached_(.*)/)
  130. log_debug "** RFACEBOOK(GEM) - DEPRECATION NOTICE - cached methods are deprecated, making a raw call without caching."
  131. tokens.shift
  132. end
  133. # there can only be one parameter, a Hash, for remote methods
  134. unless (params.size == 1 and params.first.is_a?(Hash))
  135. log_debug "** RFACEBOOK(GEM) - when you call a remote Facebook method"
  136. end
  137. # make the remote method call
  138. return remote_call(remoteMethod, params.first)
  139. end
  140. # Sets everything up to make a POST request to Facebook's API servers.
  141. #
  142. # method:: i.e. "users.getInfo"
  143. # params:: hash of key,value pairs for the parameters to this method
  144. # useSSL:: set to true if the call will be made over SSL
  145. def remote_call(method, params={}, useSSL=false) # :nodoc:
  146. log_debug "** RFACEBOOK(GEM) - RFacebook::FacebookSession\#remote_call - #{method}(#{params.inspect}) - making remote call"
  147. # set up the parameters
  148. params = (params || {}).dup
  149. params[:method] = "facebook.#{method}"
  150. params[:api_key] = @api_key
  151. params[:v] = API_VERSION
  152. # params[:format] ||= @response_format # TODO: consider JSON capability
  153. # non-auth methods get special consideration
  154. unless(method == "auth.getSession" or method == "auth.createToken")
  155. # session must be activated for non-auth methods
  156. raise NotActivatedStandardError, "You must activate the session before using it." unless ready?
  157. # secret and call ID must be set for non-auth methods
  158. params[:session_key] = session_key
  159. params[:call_id] = Time.now.to_f.to_s
  160. end
  161. # in the parameters, all arrays must be converted to comma-separated lists
  162. params.each{|k,v| params[k] = v.join(",") if v.is_a?(Array)}
  163. # sign the parameter list by adding a proper sig
  164. params[:sig] = signature(params)
  165. # make the remote call and contain the results in a Facepricot XML object
  166. xml = post_request(params, useSSL)
  167. return handle_xml_response(xml)
  168. end
  169. # Wraps an XML response in a Facepricot XML document, and checks for
  170. # an error response (raising or logging errors as needed)
  171. #
  172. # NOTE: Facepricot chaining may be deprecated in the 1.0 release
  173. def handle_xml_response(rawXML)
  174. facepricotXML = Facepricot.new(rawXML)
  175. # error checking
  176. if facepricotXML.at("error_response")
  177. # get the error code
  178. errorCode = facepricotXML.at("error_code").inner_html.to_i
  179. errorMessage = facepricotXML.at("error_msg").inner_html
  180. log_debug "** RFACEBOOK(GEM) - RFacebook::FacebookSession\#remote_call - remote call failed (#{errorCode}: #{errorMessage})"
  181. # TODO: remove these 2 lines
  182. @last_error_message = "ERROR #{errorCode}: #{errorMessage}" # DEPRECATED
  183. @last_error_code = errorCode # DEPRECATED
  184. # check to see if this error was an expired session error
  185. case errorCode
  186. # the remote method did not exist, convert that to a standard Ruby no-method error
  187. when 3
  188. raise NoMethodError, errorMessage unless quiet? == true
  189. # the parameters were wrong, or not enough parameters...convert that to a standard Ruby argument error
  190. when 100,606
  191. raise ArgumentError, errorMessage unless quiet? == true
  192. # when the session expires, we need to record that internally
  193. when 102
  194. @expired = true
  195. raise ExpiredSessionStandardError.new(errorMessage, errorCode) unless quiet? == true
  196. # otherwise, just raise a regular remote error with the error code
  197. else
  198. raise RemoteStandardError.new(errorMessage, errorCode) unless quiet? == true
  199. end
  200. # since the quiet flag may have been activated, we may not have thrown
  201. # an actual exception, so we still need to return nil here
  202. return nil
  203. end
  204. # everything was just fine, return the Facepricot XML response
  205. return facepricotXML
  206. end
  207. # Posts a request to the remote Facebook API servers, and returns the
  208. # raw text body of the result
  209. #
  210. # params:: a Hash of the post parameters to send to the REST API
  211. # useSSL:: defaults to false, set to true if you want to use SSL for the POST
  212. def post_request(params, useSSL=false)
  213. # get a server handle
  214. port = (useSSL == true) ? 443 : 80
  215. http_server = Net::HTTP.new(API_HOST, port)
  216. http_server.use_ssl = useSSL
  217. # build a request
  218. http_request = Net::HTTP::Post.new(API_PATH_REST)
  219. http_request.form_data = params
  220. # get the response XML
  221. return http_server.start{|http| http.request(http_request)}.body
  222. end
  223. # Generates a proper Facebook signature.
  224. #
  225. # params:: a Hash containing the parameters to sign
  226. # secret:: the secret to use to sign the parameters
  227. def signature_helper(params, secret) # :nodoc:
  228. args = []
  229. params.each{|k,v| args << "#{k}=#{v}"}
  230. sortedArray = args.sort
  231. requestStr = sortedArray.join("")
  232. return Digest::MD5.hexdigest("#{requestStr}#{secret}")
  233. end
  234. # log a debug message
  235. def log_debug(message) # :nodoc:
  236. @logger.debug(message) if @logger
  237. end
  238. # log an informational message
  239. def log_info(message) # :nodoc:
  240. @logger.info(message) if @logger
  241. end
  242. ################################################################################################
  243. ################################################################################################
  244. # :section: Serialization
  245. ################################################################################################
  246. public
  247. # dump to a serialized string, removing the logger object (which cannot be serialized)
  248. def _dump(depth) # :nodoc:
  249. instanceVarHash = {}
  250. self.instance_variables.each { |k| instanceVarHash[k] = self.instance_variable_get(k) }
  251. return Marshal.dump(instanceVarHash.delete_if{|k,v| k == "@logger"})
  252. end
  253. # load from a serialized string
  254. def self._load(dumpedStr) # :nodoc:
  255. instance = self.new(nil,nil)
  256. dumped = Marshal.load(dumpedStr)
  257. dumped.each do |k,v|
  258. instance.instance_variable_set(k,v)
  259. end
  260. return instance
  261. end
  262. ################################################################################################
  263. ################################################################################################
  264. # :section: Deprecated Methods
  265. ################################################################################################
  266. public
  267. # DEPRECATED
  268. def is_expired? # :nodoc:
  269. RAILS_DEFAULT_LOGGER.info "** RFACEBOOK(GEM) DEPRECATION WARNING: is_expired? is deprecated, use expired? instead"
  270. return expired?
  271. end
  272. # DEPRECATED
  273. def is_activated? # :nodoc:
  274. RAILS_DEFAULT_LOGGER.info "** RFACEBOOK(GEM) DEPRECATION WARNING: is_activated? is deprecated, use ready? instead"
  275. return ready?
  276. end
  277. # DEPRECATED
  278. def is_valid? # :nodoc:
  279. RAILS_DEFAULT_LOGGER.info "** RFACEBOOK(GEM) DEPRECATION WARNING: is_valid? is deprecated, use ready? instead"
  280. return ready?
  281. end
  282. # DEPRECATED
  283. def is_ready? # :nodoc:
  284. RAILS_DEFAULT_LOGGER.info "** RFACEBOOK(GEM) DEPRECATION WARNING: is_valid? is deprecated, use ready? instead"
  285. return ready?
  286. end
  287. # DEPRECATED
  288. def last_error_message # :nodoc:
  289. RAILS_DEFAULT_LOGGER.info "** RFACEBOOK(GEM) DEPRECATION WARNING: last_error_message is deprecated"
  290. return @last_error_message
  291. end
  292. # DEPRECATED
  293. def last_error_code # :nodoc:
  294. RAILS_DEFAULT_LOGGER.info "** RFACEBOOK(GEM) DEPRECATION WARNING: last_error_code is deprecated"
  295. return @last_error_code
  296. end
  297. # DEPRECATED
  298. def suppress_errors # :nodoc:
  299. RAILS_DEFAULT_LOGGER.info "** RFACEBOOK(GEM) DEPRECATION WARNING: suppress_errors is deprecated, use quiet? instead"
  300. return quiet?
  301. end
  302. # DEPRECATED
  303. def suppress_errors=(val) # :nodoc:
  304. RAILS_DEFAULT_LOGGER.info "** RFACEBOOK(GEM) DEPRECATION WARNING: suppress_errors= is deprecated, use quiet= instead"
  305. @quiet=val
  306. end
  307. end
  308. end