PageRenderTime 74ms CodeModel.GetById 36ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/mini_fb.rb

https://github.com/imorente/mini_fb
Ruby | 294 lines | 186 code | 54 blank | 54 comment | 25 complexity | 375bab4962c71facc2a106f73aa0f587 MD5 | raw file
  1. require 'digest/md5'
  2. require 'erb'
  3. require 'json' unless defined? JSON
  4. module MiniFB
  5. # Global constants
  6. FB_URL = "http://api.facebook.com/restserver.php"
  7. FB_API_VERSION = "1.0"
  8. @@logging = false
  9. def self.enable_logging
  10. @@logging = true
  11. end
  12. def self.disable_logging
  13. @@logging = false
  14. end
  15. class FaceBookError < StandardError
  16. attr_accessor :code
  17. # Error that happens during a facebook call.
  18. def initialize( error_code, error_msg )
  19. @code = error_code
  20. super("Facebook error #{error_code}: #{error_msg}" )
  21. end
  22. end
  23. class Session
  24. attr_accessor :api_key, :secret_key, :session_key, :uid
  25. def initialize(api_key, secret_key, session_key, uid)
  26. @api_key = api_key
  27. @secret_key = FaceBookSecret.new secret_key
  28. @session_key = session_key
  29. @uid = uid
  30. end
  31. # returns current user
  32. def user
  33. return @user unless @user.nil?
  34. @user = User.new(MiniFB.call(@api_key, @secret_key, "Users.getInfo", "session_key"=>@session_key, "uids"=>@uid, "fields"=>User.all_fields)[0], self)
  35. @user
  36. end
  37. def photos
  38. Photos.new(self)
  39. end
  40. def call(method, params={})
  41. return MiniFB.call(api_key, secret_key, method, params.update("session_key"=>session_key))
  42. end
  43. end
  44. class User
  45. FIELDS = [:uid, :status, :political, :pic_small, :name, :quotes, :is_app_user, :tv, :profile_update_time, :meeting_sex, :hs_info, :timezone, :relationship_status, :hometown_location, :about_me, :wall_count, :significant_other_id, :pic_big, :music, :work_history, :sex, :religion, :notes_count, :activities, :pic_square, :movies, :has_added_app, :education_history, :birthday, :birthday_date, :first_name, :meeting_for, :last_name, :interests, :current_location, :pic, :books, :affiliations, :locale, :profile_url, :proxied_email, :email, :email_hashes, :allowed_restrictions, :pic_with_logo, :pic_big_with_logo, :pic_small_with_logo, :pic_square_with_logo]
  46. STANDARD_FIELDS = [:uid, :first_name, :last_name, :name, :timezone, :birthday, :sex, :affiliations, :locale, :profile_url, :proxied_email, :email]
  47. def self.all_fields
  48. FIELDS.join(",")
  49. end
  50. def self.standard_fields
  51. STANDARD_FIELDS.join(",")
  52. end
  53. def initialize(fb_hash, session)
  54. @fb_hash = fb_hash
  55. @session = session
  56. end
  57. def [](key)
  58. @fb_hash[key]
  59. end
  60. def uid
  61. return self["uid"]
  62. end
  63. def profile_photos
  64. @session.photos.get("uid"=>uid, "aid"=>profile_pic_album_id)
  65. end
  66. def profile_pic_album_id
  67. merge_aid(-3, uid)
  68. end
  69. def merge_aid(aid, uid)
  70. uid = uid.to_i
  71. ret = (uid << 32) + (aid & 0xFFFFFFFF)
  72. # puts 'merge_aid=' + ret.inspect
  73. return ret
  74. end
  75. end
  76. class Photos
  77. def initialize(session)
  78. @session = session
  79. end
  80. def get(params)
  81. pids = params["pids"]
  82. if !pids.nil? && pids.is_a?(Array)
  83. pids = pids.join(",")
  84. params["pids"] = pids
  85. end
  86. @session.call("photos.get", params)
  87. end
  88. end
  89. BAD_JSON_METHODS = ["users.getloggedinuser", "auth.promotesession", "users.hasapppermission", "Auth.revokeExtendedPermission", "pages.isAdmin"].collect { |x| x.downcase }
  90. # Call facebook server with a method request. Most keyword arguments
  91. # are passed directly to the server with a few exceptions.
  92. # The 'sig' value will always be computed automatically.
  93. # The 'v' version will be supplied automatically if needed.
  94. # The 'call_id' defaults to True, which will generate a valid
  95. # number. Otherwise it should be a valid number or False to disable.
  96. # The default return is a parsed json object.
  97. # Unless the 'format' and/or 'callback' arguments are given,
  98. # in which case the raw text of the reply is returned. The string
  99. # will always be returned, even during errors.
  100. # If an error occurs, a FacebookError exception will be raised
  101. # with the proper code and message.
  102. # The secret argument should be an instance of FacebookSecret
  103. # to hide value from simple introspection.
  104. def MiniFB.call( api_key, secret, method, kwargs )
  105. puts 'kwargs=' + kwargs.inspect if @@logging
  106. if secret.is_a? String
  107. secret = FaceBookSecret.new(secret)
  108. end
  109. # Prepare arguments for call
  110. call_id = kwargs.fetch("call_id", true)
  111. if call_id == true then
  112. kwargs["call_id"] = Time.now.tv_sec.to_s
  113. else
  114. kwargs.delete("call_id")
  115. end
  116. custom_format = kwargs.include?("format") or kwargs.include?("callback")
  117. kwargs["format"] ||= "JSON"
  118. kwargs["v"] ||= FB_API_VERSION
  119. kwargs["api_key"]||= api_key
  120. kwargs["method"] ||= method
  121. # Hash with secret
  122. arg_string = String.new
  123. # todo: convert symbols to strings, symbols break the next line
  124. kwargs.sort.each { |kv| arg_string << kv[0] << "=" << kv[1].to_s }
  125. kwargs["sig"] = Digest::MD5.hexdigest( arg_string + secret.value.call )
  126. # Call website with POST request
  127. begin
  128. response = Net::HTTP.post_form( URI.parse(FB_URL), kwargs )
  129. rescue SocketError => err
  130. raise IOError.new( "Cannot connect to the facebook server: " + err )
  131. end
  132. # Handle response
  133. return response.body if custom_format
  134. fb_method = kwargs["method"].downcase
  135. body = response.body
  136. puts 'response=' + body.inspect if @@logging
  137. begin
  138. data = JSON.parse( body )
  139. if data.include?( "error_msg" ) then
  140. raise FaceBookError.new( data["error_code"] || 1, data["error_msg"] )
  141. end
  142. rescue JSON::ParserError => ex
  143. if BAD_JSON_METHODS.include?(fb_method) # Little hack because this response isn't valid JSON
  144. if body == "0" || body == "false"
  145. return false
  146. end
  147. return body
  148. else
  149. raise ex
  150. end
  151. end
  152. return data
  153. end
  154. # Returns true is signature is valid, false otherwise.
  155. def MiniFB.verify_signature( secret, arguments )
  156. signature = arguments.delete( "fb_sig" )
  157. return false if signature.nil?
  158. unsigned = Hash.new
  159. signed = Hash.new
  160. arguments.each do |k, v|
  161. if k =~ /^fb_sig_(.*)/ then
  162. signed[$1] = v
  163. else
  164. unsigned[k] = v
  165. end
  166. end
  167. arg_string = String.new
  168. signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
  169. if Digest::MD5.hexdigest( arg_string + secret ) == signature
  170. return true
  171. end
  172. return false
  173. end
  174. # The cookie format has changed with the new open source
  175. # javascript SDK. See "Has the Cookie format changed" in
  176. # the Facebook Connect JavaScript SDK FAQ:
  177. # http://wiki.github.com/facebook/connect-js/faq
  178. # Returns true is signature is valid, false otherwise.
  179. def MiniFB.verify_session_signature(secret, arguments)
  180. signature = arguments.delete('sig')
  181. signature == Digest::MD5.hexdigest(arguments.sort.map {|kv| kv.join('=') }.join << secret)
  182. end
  183. # Returns the login/add app url for your application.
  184. #
  185. # options:
  186. # - :next => a relative next page to go to. relative to your facebook connect url or if :canvas is true, then relative to facebook app url
  187. # - :canvas => true/false - to say whether this is a canvas app or not
  188. def self.login_url(api_key, options={})
  189. login_url = "http://api.facebook.com/login.php?api_key=#{api_key}"
  190. login_url << "&next=#{options[:next]}" if options[:next]
  191. login_url << "&canvas" if options[:canvas]
  192. login_url
  193. end
  194. # This function expects arguments as a hash, so
  195. # it is agnostic to different POST handling variants in ruby.
  196. #
  197. # Validate the arguments received from facebook. This is usually
  198. # sent for the iframe in Facebook's canvas. It is not necessary
  199. # to use this on the auth_token and uid passed to callbacks like
  200. # post-add and post-remove.
  201. #
  202. # The arguments must be a mapping of to string keys and values
  203. # or a string of http request data.
  204. #
  205. # If the data is invalid or not signed properly, an empty
  206. # dictionary is returned.
  207. #
  208. # The secret argument should be an instance of FacebookSecret
  209. # to hide value from simple introspection.
  210. #
  211. # DEPRECATED, use verify_signature instead
  212. def MiniFB.validate( secret, arguments )
  213. signature = arguments.delete( "fb_sig" )
  214. return arguments if signature.nil?
  215. unsigned = Hash.new
  216. signed = Hash.new
  217. arguments.each do |k, v|
  218. if k =~ /^fb_sig_(.*)/ then
  219. signed[$1] = v
  220. else
  221. unsigned[k] = v
  222. end
  223. end
  224. arg_string = String.new
  225. signed.sort.each { |kv| arg_string << kv[0] << "=" << kv[1] }
  226. if Digest::MD5.hexdigest( arg_string + secret ) != signature
  227. unsigned # Hash is incorrect, return only unsigned fields.
  228. else
  229. unsigned.merge signed
  230. end
  231. end
  232. class FaceBookSecret
  233. # Simple container that stores a secret value.
  234. # Proc cannot be dumped or introspected by normal tools.
  235. attr_reader :value
  236. def initialize( value )
  237. @value = Proc.new { value }
  238. end
  239. end
  240. end