koala 1.0.0.beta → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG +17 -3
  3. data/Gemfile +3 -0
  4. data/LICENSE +1 -1
  5. data/Manifest +5 -2
  6. data/Rakefile +13 -14
  7. data/koala.gemspec +35 -20
  8. data/lib/koala/graph_api.rb +183 -131
  9. data/lib/koala/http_services.rb +46 -55
  10. data/lib/koala/test_users.rb +21 -8
  11. data/lib/koala/uploadable_io.rb +115 -0
  12. data/lib/koala.rb +52 -84
  13. data/readme.md +19 -6
  14. data/spec/cases/api_base_spec.rb +101 -0
  15. data/spec/cases/graph_and_rest_api_spec.rb +31 -0
  16. data/spec/cases/graph_api_spec.rb +25 -0
  17. data/spec/cases/http_services/http_service_spec.rb +54 -0
  18. data/spec/cases/http_services/net_http_service_spec.rb +350 -0
  19. data/spec/cases/http_services/typhoeus_service_spec.rb +144 -0
  20. data/spec/cases/oauth_spec.rb +409 -0
  21. data/spec/cases/realtime_updates_spec.rb +184 -0
  22. data/spec/cases/rest_api_spec.rb +25 -0
  23. data/spec/{koala/test_users/test_users_tests.rb → cases/test_users_spec.rb} +46 -33
  24. data/spec/cases/uploadable_io_spec.rb +151 -0
  25. data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +12 -14
  26. data/spec/{mock_facebook_responses.yml → fixtures/mock_facebook_responses.yml} +313 -306
  27. data/spec/spec_helper.rb +18 -0
  28. data/spec/support/graph_api_shared_examples.rb +424 -0
  29. data/spec/{koala → support}/live_testing_data_helper.rb +39 -42
  30. data/spec/{mock_http_service.rb → support/mock_http_service.rb} +94 -95
  31. data/spec/{koala/rest_api/rest_api_tests.rb → support/rest_api_shared_examples.rb} +43 -0
  32. data/spec/support/setup_mocks_or_live.rb +52 -0
  33. data/spec/support/uploadable_io_shared_examples.rb +76 -0
  34. metadata +107 -48
  35. data/init.rb +0 -2
  36. data/spec/koala/api_base_tests.rb +0 -102
  37. data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -14
  38. data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -16
  39. data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -63
  40. data/spec/koala/graph_api/graph_api_tests.rb +0 -86
  41. data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -154
  42. data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
  43. data/spec/koala/net_http_service_tests.rb +0 -430
  44. data/spec/koala/oauth/oauth_tests.rb +0 -409
  45. data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
  46. data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -25
  47. data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -38
  48. data/spec/koala/typhoeus_service_tests.rb +0 -156
  49. data/spec/koala_spec.rb +0 -18
  50. data/spec/koala_spec_helper.rb +0 -70
  51. data/spec/koala_spec_without_mocks.rb +0 -19
  52. /data/spec/{koala/assets → fixtures}/beach.jpg +0 -0
@@ -15,26 +15,19 @@ module Koala
15
15
  class << self
16
16
  attr_accessor :always_use_ssl
17
17
  end
18
-
18
+
19
+ def self.server(options = {})
20
+ "#{options[:beta] ? "beta." : ""}#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
21
+ end
22
+
19
23
  protected
24
+
20
25
  def self.params_require_multipart?(param_hash)
21
- param_hash.any? { |key, value| is_valid_file_hash?(value) }
26
+ param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
22
27
  end
23
-
24
- # A file hash can take two forms:
25
- # - A hash with "content_type" and "path" keys where "path" is the local path
26
- # to the file to be uploaded.
27
- # - A hash with the "file" key containing an already-opened IO that responds to "read"
28
- # as well as "content_type" and the "path" key to the original file
29
- # ("path"" is required by multipart-post even for opened files)
30
- #
31
- # Valid inputs for a file to be posted via multipart/form-data
32
- # are based on the criteria for an UploadIO to be created
33
- # See : https://github.com/nicksieger/multipart-post/blob/master/lib/composite_io.rb
34
- def self.is_valid_file_hash?(value)
35
- value.kind_of?(Hash) and value.key?("content_type") and value.key?("path") and (
36
- !value.key?("file") or value["file"].respond_to?(:read)
37
- )
28
+
29
+ def self.multipart_requires_content_type?
30
+ true
38
31
  end
39
32
  end
40
33
  end
@@ -61,16 +54,9 @@ module Koala
61
54
  # if the verb isn't get or post, send it as a post argument
62
55
  args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
63
56
 
64
- server = options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER
65
- http = Net::HTTP.new(server, private_request ? 443 : nil)
57
+ http = create_http(server(options), private_request, options)
66
58
  http.use_ssl = true if private_request
67
59
 
68
- # we turn off certificate validation to avoid the
69
- # "warning: peer certificate won't be verified in this SSL session" warning
70
- # not sure if this is the right way to handle it
71
- # see http://redcorundum.blogspot.com/2008/03/ssl-certificates-and-nethttps.html
72
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
73
-
74
60
  result = http.start do |http|
75
61
  response, body = if verb == "post"
76
62
  if params_require_multipart? args
@@ -98,17 +84,25 @@ module Koala
98
84
 
99
85
  def self.encode_multipart_params(param_hash)
100
86
  Hash[*param_hash.collect do |key, value|
101
- if is_valid_file_hash?(value)
102
- if value.key?("file")
103
- value = UploadIO.new(value["file"], value["content_type"], value["path"])
104
- else
105
- value = UploadIO.new(value["path"], value['content_type'])
106
- end
107
- end
108
-
109
- [key, value]
87
+ [key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
110
88
  end.flatten]
111
89
  end
90
+
91
+ def self.create_http(server, private_request, options)
92
+ if options[:proxy]
93
+ proxy = URI.parse(options[:proxy])
94
+ http = Net::HTTP.new(server, private_request ? 443 : nil,
95
+ proxy.host, proxy.port, proxy.user, proxy.password)
96
+ else
97
+ http = Net::HTTP.new(server, private_request ? 443 : nil)
98
+ end
99
+ if options[:timeout]
100
+ http.open_timeout = options[:timeout]
101
+ http.read_timeout = options[:timeout]
102
+ end
103
+ http
104
+ end
105
+
112
106
  end
113
107
  end
114
108
  end
@@ -116,11 +110,6 @@ module Koala
116
110
  module TyphoeusService
117
111
  # this service uses Typhoeus to send requests to the graph
118
112
 
119
- # used for multipart file uploads (see below)
120
- class NetHTTPInterface
121
- include NetHTTPService
122
- end
123
-
124
113
  def self.included(base)
125
114
  base.class_eval do
126
115
  require "typhoeus" unless defined?(Typhoeus)
@@ -129,25 +118,27 @@ module Koala
129
118
  include Koala::HTTPService
130
119
 
131
120
  def self.make_request(path, args, verb, options = {})
132
- unless params_require_multipart?(args)
133
- # if the verb isn't get or post, send it as a post argument
134
- args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
135
- server = options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER
121
+ # if the verb isn't get or post, send it as a post argument
122
+ args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
136
123
 
137
- # you can pass arguments directly to Typhoeus using the :typhoeus_options key
138
- typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
124
+ # switch any UploadableIOs to the files Typhoeus expects
125
+ args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
139
126
 
140
- # by default, we use SSL only for private requests (e.g. with access token)
141
- # this makes public requests faster
142
- prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
127
+ # you can pass arguments directly to Typhoeus using the :typhoeus_options key
128
+ typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
143
129
 
144
- response = self.send(verb, "#{prefix}://#{server}#{path}", typhoeus_options)
145
- Koala::Response.new(response.code, response.body, response.headers_hash)
146
- else
147
- # we have to use NetHTTPService for multipart for file uploads
148
- # until Typhoeus integrates support
149
- Koala::TyphoeusService::NetHTTPInterface.make_request(path, args, verb, options)
150
- end
130
+ # by default, we use SSL only for private requests (e.g. with access token)
131
+ # this makes public requests faster
132
+ prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
133
+
134
+ response = self.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
135
+ Koala::Response.new(response.code, response.body, response.headers_hash)
136
+ end
137
+
138
+ private
139
+
140
+ def self.multipart_requires_content_type?
141
+ false # Typhoeus handles multipart file types, we don't have to require it
151
142
  end
152
143
  end # class_eval
153
144
  end
@@ -20,11 +20,11 @@ module Koala
20
20
  @graph_api = GraphAPI.new(@app_access_token)
21
21
  end
22
22
 
23
- def create(installed, permissions = nil)
23
+ def create(installed, permissions = nil, args = {}, options = {})
24
24
  # Creates and returns a test user
25
- args = {'installed' => installed}
25
+ args['installed'] = installed
26
26
  args['permissions'] = (permissions.is_a?(Array) ? permissions.join(",") : permissions) if installed
27
- result = @graph_api.graph_call(accounts_path, args, "post")
27
+ result = @graph_api.graph_call(accounts_path, args, "post", options)
28
28
  end
29
29
 
30
30
  def list
@@ -40,12 +40,25 @@ module Koala
40
40
  list.each {|u| delete u }
41
41
  end
42
42
 
43
- def befriend(user1, user2)
44
- user1 = user1["id"] if user1.is_a?(Hash)
45
- user2 = user2["id"] if user2.is_a?(Hash)
46
- @graph_api.graph_call("/#{user1}/friends/#{user2}") && @graph_api.graph_call("/#{user2}/friends/#{user1}")
43
+ def befriend(user1_hash, user2_hash)
44
+ user1_id = user1_hash["id"] || user1_hash[:id]
45
+ user2_id = user2_hash["id"] || user2_hash[:id]
46
+ user1_token = user1_hash["access_token"] || user1_hash[:access_token]
47
+ user2_token = user2_hash["access_token"] || user2_hash[:access_token]
48
+ unless user1_id && user2_id && user1_token && user2_token
49
+ # we explicitly raise an error here to minimize the risk of confusing output
50
+ # if you pass in a string (as was previously supported) no local exception would be raised
51
+ # but the Facebook call would fail
52
+ raise ArgumentError, "TestUsers#befriend requires hash arguments for both users with id and access_token"
53
+ end
54
+
55
+ u1_graph_api = GraphAPI.new(user1_token)
56
+ u2_graph_api = GraphAPI.new(user2_token)
57
+
58
+ u1_graph_api.graph_call("#{user1_id}/friends/#{user2_id}", {}, "post") &&
59
+ u2_graph_api.graph_call("#{user2_id}/friends/#{user1_id}", {}, "post")
47
60
  end
48
-
61
+
49
62
  def create_network(network_size, installed = true, permissions = '')
50
63
  network_size = 50 if network_size > 50 # FB's max is 50
51
64
  users = (0...network_size).collect { create(installed, permissions) }
@@ -0,0 +1,115 @@
1
+ require 'koala'
2
+
3
+ module Koala
4
+ class UploadableIO
5
+ attr_reader :io_or_path, :content_type
6
+
7
+ def initialize(io_or_path_or_mixed, content_type = nil)
8
+ # see if we got the right inputs
9
+ if content_type.nil?
10
+ parse_init_mixed_param io_or_path_or_mixed
11
+ elsif !content_type.nil? && (io_or_path_or_mixed.respond_to?(:read) or io_or_path_or_mixed.kind_of?(String))
12
+ @io_or_path = io_or_path_or_mixed
13
+ @content_type = content_type
14
+ end
15
+
16
+ raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
17
+ raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type && Koala.multipart_requires_content_type?
18
+ end
19
+
20
+ def to_upload_io
21
+ UploadIO.new(@io_or_path, @content_type, "koala-io-file.dum")
22
+ end
23
+
24
+ def to_file
25
+ @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
26
+ end
27
+
28
+ private
29
+ PARSE_STRATEGIES = [
30
+ :parse_rails_3_param,
31
+ :parse_sinatra_param,
32
+ :parse_file_object,
33
+ :parse_string_path
34
+ ]
35
+
36
+ def parse_init_mixed_param(mixed)
37
+ PARSE_STRATEGIES.each do |method|
38
+ send(method, mixed)
39
+ return if @io_or_path && @content_type
40
+ end
41
+ end
42
+
43
+ # Expects a parameter of type ActionDispatch::Http::UploadedFile
44
+ def parse_rails_3_param(uploaded_file)
45
+ if uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
46
+ @io_or_path = uploaded_file.tempfile.path
47
+ @content_type = uploaded_file.content_type
48
+ end
49
+ end
50
+
51
+ # Expects a Sinatra hash of file info
52
+ def parse_sinatra_param(file_hash)
53
+ if file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
54
+ @io_or_path = file_hash[:tempfile]
55
+ @content_type = file_hash[:type] || detect_mime_type(tempfile)
56
+ end
57
+ end
58
+
59
+ # takes a file object
60
+ def parse_file_object(file)
61
+ if file.kind_of?(File)
62
+ @io_or_path = file
63
+ @content_type = detect_mime_type(file.path)
64
+ end
65
+ end
66
+
67
+ def parse_string_path(path)
68
+ if path.kind_of?(String)
69
+ @io_or_path = path
70
+ @content_type = detect_mime_type(path)
71
+ end
72
+ end
73
+
74
+ MIME_TYPE_STRATEGIES = [
75
+ :use_mime_module,
76
+ :use_simple_detection
77
+ ]
78
+
79
+ def detect_mime_type(filename)
80
+ if filename
81
+ MIME_TYPE_STRATEGIES.each do |method|
82
+ result = send(method, filename)
83
+ return result if result
84
+ end
85
+ end
86
+ nil # if we can't find anything
87
+ end
88
+
89
+ def use_mime_module(filename)
90
+ # if the user has installed mime/types, we can use that
91
+ # if not, rescue and return nil
92
+ begin
93
+ type = MIME::Types.type_for(filename).first
94
+ type ? type.to_s : nil
95
+ rescue
96
+ nil
97
+ end
98
+ end
99
+
100
+ def use_simple_detection(filename)
101
+ # very rudimentary extension analysis for images
102
+ # first, get the downcased extension, or an empty string if it doesn't exist
103
+ extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
104
+ if extension == ""
105
+ nil
106
+ elsif extension == "jpg" || extension == "jpeg"
107
+ "image/jpeg"
108
+ elsif extension == "png"
109
+ "image/png"
110
+ elsif extension == "gif"
111
+ "image/gif"
112
+ end
113
+ end
114
+ end
115
+ end
data/lib/koala.rb CHANGED
@@ -1,55 +1,32 @@
1
1
  require 'cgi'
2
2
  require 'digest/md5'
3
3
 
4
- # rubygems is required to support json, how facebook returns data
5
- require 'rubygems'
6
4
  require 'json'
7
5
 
8
6
  # OpenSSL and Base64 are required to support signed_request
9
7
  require 'openssl'
10
8
  require 'base64'
11
9
 
12
- # include default http services
10
+ # include koala modules
13
11
  require 'koala/http_services'
14
-
15
- # add Graph API methods
16
12
  require 'koala/graph_api'
17
-
18
- # add REST API methods
19
13
  require 'koala/rest_api'
20
-
21
- # add realtime update methods
22
14
  require 'koala/realtime_updates'
23
-
24
- # add test user methods
25
15
  require 'koala/test_users'
26
16
 
17
+ # add KoalaIO class
18
+ require 'koala/uploadable_io'
19
+
27
20
  module Koala
28
21
 
29
22
  module Facebook
30
23
  # Ruby client library for the Facebook Platform.
31
- # Copyright 2010 Facebook
32
- # Adapted from the Python library by Alex Koppel, Rafi Jacoby, and the team at Context Optional
33
- #
34
- # Licensed under the Apache License, Version 2.0 (the "License"); you may
35
- # not use this file except in compliance with the License. You may obtain
36
- # a copy of the License at
37
- # http://www.apache.org/licenses/LICENSE-2.0
38
- #
39
- # Unless required by applicable law or agreed to in writing, software
40
- # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
41
- # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
42
- # License for the specific language governing permissions and limitations
43
- # under the License.
44
- #
45
- # This client library is designed to support the Graph API and the official
46
- # Facebook JavaScript SDK, which is the canonical way to implement
47
- # Facebook authentication. Read more about the Graph API at
48
- # http://developers.facebook.com/docs/api. You can download the Facebook
49
- # JavaScript SDK at http://github.com/facebook/connect-js/.
24
+ # Copyright 2010-2011 Alex Koppel
25
+ # Contributors: Alex Koppel, Chris Baclig, Rafi Jacoby, and the team at Context Optional
26
+ # http://github.com/arsduo/koala
50
27
 
51
28
  class API
52
- # initialize with an access token
29
+ # initialize with an access token
53
30
  def initialize(access_token = nil)
54
31
  @access_token = access_token
55
32
  end
@@ -58,7 +35,7 @@ module Koala
58
35
  def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
59
36
  # Fetches the given path in the Graph API.
60
37
  args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
61
-
38
+
62
39
  # add a leading /
63
40
  path = "/#{path}" unless path =~ /^\//
64
41
 
@@ -110,7 +87,7 @@ module Koala
110
87
  attr_reader :graph_api
111
88
  end
112
89
 
113
- class APIError < Exception
90
+ class APIError < StandardError
114
91
  attr_accessor :fb_error_type
115
92
  def initialize(details = {})
116
93
  self.fb_error_type = details["type"]
@@ -170,12 +147,13 @@ module Koala
170
147
  # for permissions, see http://developers.facebook.com/docs/authentication/permissions
171
148
  permissions = options[:permissions]
172
149
  scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
173
-
150
+ display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
151
+
174
152
  callback = options[:callback] || @oauth_callback_url
175
153
  raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
176
154
 
177
155
  # Creates the URL for oauth authorization for a given callback and optional set of permissions
178
- "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}"
156
+ "https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
179
157
  end
180
158
 
181
159
  def url_for_access_token(code, options = {})
@@ -185,70 +163,56 @@ module Koala
185
163
  "https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
186
164
  end
187
165
 
188
- def get_access_token_info(code)
166
+ def get_access_token_info(code, options = {})
189
167
  # convenience method to get a parsed token from Facebook for a given code
190
168
  # should this require an OAuth callback URL?
191
- get_token_from_server(:code => code, :redirect_uri => @oauth_callback_url)
169
+ get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
192
170
  end
193
171
 
194
- def get_access_token(code)
172
+ def get_access_token(code, options = {})
195
173
  # upstream methods will throw errors if needed
196
- if info = get_access_token_info(code)
174
+ if info = get_access_token_info(code, options)
197
175
  string = info["access_token"]
198
176
  end
199
177
  end
200
178
 
201
- def get_app_access_token_info
179
+ def get_app_access_token_info(options = {})
202
180
  # convenience method to get a the application's sessionless access token
203
- get_token_from_server({:type => 'client_cred'}, true)
181
+ get_token_from_server({:type => 'client_cred'}, true, options)
204
182
  end
205
183
 
206
- def get_app_access_token
207
- if info = get_app_access_token_info
184
+ def get_app_access_token(options = {})
185
+ if info = get_app_access_token_info(options)
208
186
  string = info["access_token"]
209
187
  end
210
188
  end
211
-
212
- # provided directly by Facebook
213
- # see https://github.com/facebook/crypto-request-examples/blob/master/sample.rb
214
- # and http://developers.facebook.com/docs/authentication/canvas/encryption_proposal
215
- def parse_signed_request(input, max_age = 3600)
189
+
190
+ # Originally provided directly by Facebook, however this has changed
191
+ # as their concept of crypto changed. For historic purposes, this is their proposal:
192
+ # https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
193
+ # Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
194
+ # for a more accurate reference implementation strategy.
195
+ def parse_signed_request(input)
216
196
  encoded_sig, encoded_envelope = input.split('.', 2)
197
+ signature = base64_url_decode(encoded_sig).unpack("H*").first
217
198
  envelope = JSON.parse(base64_url_decode(encoded_envelope))
218
- algorithm = envelope['algorithm']
219
-
220
- raise 'Invalid request. (Unsupported algorithm.)' \
221
- if algorithm != 'AES-256-CBC HMAC-SHA256' && algorithm != 'HMAC-SHA256'
222
-
223
- raise 'Invalid request. (Too old.)' \
224
- if algorithm == "AES-256-CBC HMAC-SHA256" && envelope['issued_at'].to_i < Time.now.to_i - max_age
225
-
226
- raise 'Invalid request. (Invalid signature.)' \
227
- if base64_url_decode(encoded_sig) !=
228
- OpenSSL::HMAC.hexdigest(
229
- 'sha256', @app_secret, encoded_envelope).split.pack('H*')
230
-
231
- # for requests that are signed, but not encrypted, we're done
232
- return envelope if algorithm == 'HMAC-SHA256'
233
-
234
- # otherwise, decrypt the payload
235
- cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
236
- cipher.decrypt
237
- cipher.key = @app_secret
238
- cipher.iv = base64_url_decode(envelope['iv'])
239
- cipher.padding = 0
240
- decrypted_data = cipher.update(base64_url_decode(envelope['payload']))
241
- decrypted_data << cipher.final
242
- return JSON.parse(decrypted_data.strip)
199
+
200
+ raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
201
+
202
+ # now see if the signature is valid (digest, key, data)
203
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
204
+ raise 'SignedRequest: Invalid signature' if (signature != hmac)
205
+
206
+ return envelope
243
207
  end
244
208
 
245
209
  # from session keys
246
- def get_token_info_from_session_keys(sessions)
210
+ def get_token_info_from_session_keys(sessions, options = {})
247
211
  # fetch the OAuth tokens from Facebook
248
212
  response = fetch_token_string({
249
213
  :type => 'client_cred',
250
214
  :sessions => sessions.join(",")
251
- }, true, "exchange_sessions")
215
+ }, true, "exchange_sessions", options)
252
216
 
253
217
  # Facebook returns an empty body in certain error conditions
254
218
  if response == ""
@@ -261,24 +225,24 @@ module Koala
261
225
  JSON.parse(response)
262
226
  end
263
227
 
264
- def get_tokens_from_session_keys(sessions)
228
+ def get_tokens_from_session_keys(sessions, options = {})
265
229
  # get the original hash results
266
- results = get_token_info_from_session_keys(sessions)
230
+ results = get_token_info_from_session_keys(sessions, options)
267
231
  # now recollect them as just the access tokens
268
232
  results.collect { |r| r ? r["access_token"] : nil }
269
233
  end
270
234
 
271
- def get_token_from_session_key(session)
235
+ def get_token_from_session_key(session, options = {})
272
236
  # convenience method for a single key
273
237
  # gets the overlaoded strings automatically
274
- get_tokens_from_session_keys([session])[0]
238
+ get_tokens_from_session_keys([session], options)[0]
275
239
  end
276
240
 
277
241
  protected
278
242
 
279
- def get_token_from_server(args, post = false)
243
+ def get_token_from_server(args, post = false, options = {})
280
244
  # fetch the result from Facebook's servers
281
- result = fetch_token_string(args, post)
245
+ result = fetch_token_string(args, post, "access_token", options)
282
246
 
283
247
  # if we have an error, parse the error JSON and raise an error
284
248
  raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
@@ -295,22 +259,24 @@ module Koala
295
259
  components
296
260
  end
297
261
 
298
- def fetch_token_string(args, post = false, endpoint = "access_token")
262
+ def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
299
263
  Koala.make_request("/oauth/#{endpoint}", {
300
264
  :client_id => @app_id,
301
265
  :client_secret => @app_secret
302
- }.merge!(args), post ? "post" : "get", :use_ssl => true).body
266
+ }.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
303
267
  end
304
268
 
305
269
  # base 64
306
270
  # directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
307
271
  def base64_url_decode(str)
308
272
  str += '=' * (4 - str.length.modulo(4))
309
- Base64.decode64(str.gsub('-', '+').gsub('_', '/'))
273
+ Base64.decode64(str.tr('-_', '+/'))
310
274
  end
311
275
  end
312
276
  end
313
277
 
278
+ class KoalaError< StandardError; end
279
+
314
280
  # finally, set up the http service Koala methods used to make requests
315
281
  # you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
316
282
  def self.http_service=(service)
@@ -318,6 +284,8 @@ module Koala
318
284
  end
319
285
 
320
286
  # by default, try requiring Typhoeus -- if that works, use it
287
+ # if you have Typheous and don't want to use it (or want another service),
288
+ # you can run Koala.http_service = NetHTTPService (or MyHTTPService)
321
289
  begin
322
290
  Koala.http_service = TyphoeusService
323
291
  rescue LoadError
data/readme.md CHANGED
@@ -1,12 +1,22 @@
1
1
  Koala
2
2
  ====
3
- Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.com/arsduo/koala</a>) is a new Facebook library for Ruby, supporting the Graph API, the old REST API, realtime updates, and OAuth validation. We wrote Koala with four goals:
3
+ Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.com/arsduo/koala</a>) is a new Facebook library for Ruby, supporting the Graph API (including photo uploads), the old REST API, realtime updates, and OAuth validation. We wrote Koala with four goals:
4
4
 
5
5
  * Lightweight: Koala should be as light and simple as Facebook’s own new libraries, providing API accessors and returning simple JSON. (We clock in, with comments, just over 750 lines of code.)
6
6
  * Fast: Koala should, out of the box, be quick. In addition to supporting the vanilla Ruby networking libraries, it natively supports Typhoeus, our preferred gem for making fast HTTP requests. Of course, That brings us to our next topic:
7
7
  * Flexible: Koala should be useful to everyone, regardless of their current configuration. (We have no dependencies beyond the JSON gem. Koala also has a built-in mechanism for using whichever HTTP library you prefer to make requests against the graph.)
8
8
  * Tested: Koala should have complete test coverage, so you can rely on it. (Our complete test coverage can be run against either mocked responses or the live Facebook servers.)
9
9
 
10
+ 1.0
11
+ ---
12
+ Version 1.0 is due out on May 1st, 2011 with a ton of great features.
13
+
14
+ sudo gem install koala
15
+
16
+ Until then, you can install the release candidate like so:
17
+
18
+ sudo gem install koala --pre
19
+
10
20
  Graph API
11
21
  ----
12
22
  The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
@@ -120,11 +130,14 @@ Testing
120
130
  -----
121
131
 
122
132
  Unit tests are provided for all of Koala's methods. By default, these tests run against mock responses and hence are ready out of the box:
123
- # From the spec directory
124
- spec koala_spec.rb
133
+
134
+ # From anywhere in the project directory:
135
+ rake spec
136
+
125
137
 
126
138
  You can also run live tests against Facebook's servers:
127
- # Again from the spec directory
128
- spec koala_spec_without_mocks.rb
139
+
140
+ # Again from anywhere in the project directory:
141
+ LIVE=true rake spec
129
142
 
130
- Important Note: to run the live tests, you have to provide some of your own data: a valid OAuth access token with publish\_stream and read\_stream permissions and an OAuth code that can be used to generate an access token. You can get these data at the OAuth Playground; if you want to use your own app, remember to swap out the app ID, secret, and other values. (The file also provides valid values for other tests, which you're welcome to swap out for data specific to your own application.)
143
+ Important Note: to run the live tests, you have to provide some of your own data in spec/fixtures/facebook_data.yml: a valid OAuth access token with publish\_stream, read\_stream, and user\_photos permissions and an OAuth code that can be used to generate an access token. You can get thisdata at the OAuth Playground; if you want to use your own app, remember to swap out the app ID, secret, and other values. (The file also provides valid values for other tests, which you're welcome to swap out for data specific to your own application.)