koala 0.9.0 → 1.1.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 (82) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +5 -0
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG +71 -7
  5. data/Gemfile +7 -0
  6. data/LICENSE +1 -1
  7. data/Manifest +10 -15
  8. data/Rakefile +13 -13
  9. data/autotest/discover.rb +1 -0
  10. data/koala.gemspec +36 -16
  11. data/lib/koala/batch_operation.rb +74 -0
  12. data/lib/koala/graph_api.rb +200 -134
  13. data/lib/koala/graph_batch_api.rb +87 -0
  14. data/lib/koala/graph_collection.rb +54 -0
  15. data/lib/koala/http_services/net_http_service.rb +92 -0
  16. data/lib/koala/http_services/typhoeus_service.rb +37 -0
  17. data/lib/koala/http_services.rb +22 -47
  18. data/lib/koala/oauth.rb +181 -0
  19. data/lib/koala/realtime_updates.rb +5 -14
  20. data/lib/koala/rest_api.rb +82 -10
  21. data/lib/koala/test_users.rb +85 -0
  22. data/lib/koala/uploadable_io.rb +175 -0
  23. data/lib/koala.rb +67 -229
  24. data/readme.md +71 -38
  25. data/spec/cases/api_base_spec.rb +101 -0
  26. data/spec/cases/graph_and_rest_api_spec.rb +31 -0
  27. data/spec/cases/graph_api_batch_spec.rb +609 -0
  28. data/spec/cases/graph_api_spec.rb +25 -0
  29. data/spec/cases/http_services/http_service_spec.rb +129 -0
  30. data/spec/cases/http_services/net_http_service_spec.rb +532 -0
  31. data/spec/cases/http_services/typhoeus_service_spec.rb +152 -0
  32. data/spec/cases/koala_spec.rb +55 -0
  33. data/spec/cases/oauth_spec.rb +409 -0
  34. data/spec/cases/realtime_updates_spec.rb +184 -0
  35. data/spec/cases/rest_api_spec.rb +25 -0
  36. data/spec/cases/test_users_spec.rb +221 -0
  37. data/spec/cases/uploadable_io_spec.rb +193 -0
  38. data/spec/fixtures/beach.jpg +0 -0
  39. data/spec/fixtures/cat.m4v +0 -0
  40. data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +18 -14
  41. data/spec/fixtures/mock_facebook_responses.yml +409 -0
  42. data/spec/spec_helper.rb +19 -0
  43. data/spec/support/graph_api_shared_examples.rb +495 -0
  44. data/spec/support/json_testing_fix.rb +18 -0
  45. data/spec/support/live_testing_data_helper.rb +40 -0
  46. data/spec/support/mock_http_service.rb +96 -0
  47. data/spec/support/rest_api_shared_examples.rb +285 -0
  48. data/spec/support/setup_mocks_or_live.rb +51 -0
  49. data/spec/support/uploadable_io_shared_examples.rb +76 -0
  50. metadata +131 -59
  51. data/examples/oauth_playground/Capfile +0 -2
  52. data/examples/oauth_playground/LICENSE +0 -22
  53. data/examples/oauth_playground/Rakefile +0 -4
  54. data/examples/oauth_playground/config/deploy.rb +0 -39
  55. data/examples/oauth_playground/config/facebook.yml +0 -13
  56. data/examples/oauth_playground/config.ru +0 -27
  57. data/examples/oauth_playground/lib/load_facebook.rb +0 -3
  58. data/examples/oauth_playground/lib/oauth_playground.rb +0 -187
  59. data/examples/oauth_playground/readme.md +0 -8
  60. data/examples/oauth_playground/spec/oauth_playground_spec.rb +0 -35
  61. data/examples/oauth_playground/spec/spec_helper.rb +0 -36
  62. data/examples/oauth_playground/tmp/restart.txt +0 -0
  63. data/examples/oauth_playground/views/index.erb +0 -206
  64. data/examples/oauth_playground/views/layout.erb +0 -39
  65. data/init.rb +0 -2
  66. data/spec/koala/api_base_tests.rb +0 -95
  67. data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -10
  68. data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -11
  69. data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -114
  70. data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -150
  71. data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
  72. data/spec/koala/live_testing_data_helper.rb +0 -15
  73. data/spec/koala/net_http_service_tests.rb +0 -181
  74. data/spec/koala/oauth/oauth_tests.rb +0 -440
  75. data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
  76. data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -94
  77. data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -36
  78. data/spec/koala_spec.rb +0 -18
  79. data/spec/koala_spec_helper.rb +0 -31
  80. data/spec/koala_spec_without_mocks.rb +0 -19
  81. data/spec/mock_facebook_responses.yml +0 -241
  82. data/spec/mock_http_service.rb +0 -81
@@ -1,65 +1,12 @@
1
1
  module Koala
2
2
  module Facebook
3
- GRAPH_SERVER = "graph.facebook.com"
4
-
5
- class GraphCollection < Array
6
- #This class is a light wrapper for collections returned
7
- #from the Graph API.
8
- #
9
- #It extends Array to allow direct access to the data colleciton
10
- #which should allow it to drop in seamlessly.
11
- #
12
- #It also allows access to paging information and the
13
- #ability to get the next/previous page in the collection
14
- #by calling next_page or previous_page.
15
- attr_reader :paging
16
- attr_reader :api
17
-
18
- def initialize(response, api)
19
- super response["data"]
20
- @paging = response["paging"]
21
- @api = api
22
- end
23
-
24
- # defines methods for NEXT and PREVIOUS pages
25
- %w{next previous}.each do |this|
26
-
27
- # def next_page
28
- # def previous_page
29
- define_method "#{this.to_sym}_page" do
30
- base, args = send("#{this}_page_params")
31
- base ? @api.get_page([base, args]) : nil
32
- end
33
-
34
- # def next_page_params
35
- # def previous_page_params
36
- define_method "#{this.to_sym}_page_params" do
37
- return nil unless @paging and @paging[this]
38
- parse_page_url(@paging[this])
39
- end
40
- end
41
-
42
- def parse_page_url(url)
43
- match = url.match(/.com\/(.*)\?(.*)/)
44
- base = match[1]
45
- args = match[2]
46
- params = CGI.parse(args)
47
- new_params = {}
48
- params.each_pair do |key,value|
49
- new_params[key] = value.join ","
50
- end
51
- [base,new_params]
52
- end
53
-
54
- end
55
-
56
-
3
+ GRAPH_SERVER = "graph.facebook.com"
57
4
 
58
5
  module GraphAPIMethods
59
6
  # A client for the Facebook Graph API.
60
7
  #
61
- # See http://developers.facebook.com/docs/api for complete documentation
62
- # for the API.
8
+ # See http://github.com/arsduo/koala for Ruby/Koala documentation
9
+ # and http://developers.facebook.com/docs/api for Facebook API documentation
63
10
  #
64
11
  # The Graph API is made up of the objects in Facebook (e.g., people, pages,
65
12
  # events, photos) and the connections between them (e.g., friends,
@@ -76,117 +23,236 @@ module Koala
76
23
  # by the API at http://developers.facebook.com/docs/reference/api/.
77
24
  #
78
25
  # You can obtain an access token via OAuth or by using the Facebook
79
- # JavaScript SDK. See http://developers.facebook.com/docs/authentication/
80
- # for details.
26
+ # JavaScript SDK. See the Koala and Facebook documentation for more information.
81
27
  #
82
28
  # If you are using the JavaScript SDK, you can use the
83
29
  # Koala::Facebook::OAuth.get_user_from_cookie() method below to get the OAuth access token
84
30
  # for the active user from the cookie saved by the SDK.
85
-
86
- def get_object(id, args = {})
31
+
32
+ # Objects
33
+
34
+ def get_object(id, args = {}, options = {})
87
35
  # Fetchs the given object from the graph.
88
- graph_call(id, args)
89
- end
90
-
91
- def get_objects(ids, args = {})
92
- # Fetchs all of the given object from the graph.
93
- # We return a map from ID to object. If any of the IDs are invalid,
94
- # we raise an exception.
95
- graph_call("", args.merge("ids" => ids.join(",")))
96
- end
97
-
98
- def get_page(params)
99
- result = graph_call(*params)
100
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
36
+ graph_call(id, args, "get", options)
101
37
  end
102
38
 
103
- def get_connections(id, connection_name, args = {})
104
- # Fetchs the connections for given object.
105
- result = graph_call("#{id}/#{connection_name}", args)
106
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
39
+ def get_objects(ids, args = {}, options = {})
40
+ # Fetchs all of the given objects from the graph.
41
+ # If any of the IDs are invalid, they'll raise an exception.
42
+ return [] if ids.empty?
43
+ graph_call("", args.merge("ids" => ids.respond_to?(:join) ? ids.join(",") : ids), "get", options)
107
44
  end
108
45
 
109
-
110
- def get_picture(object, args = {})
111
- result = graph_call("#{object}/picture", args, "get", :http_component => :headers)
112
- result["Location"]
113
- end
114
-
115
- def put_object(parent_object, connection_name, args = {})
46
+ def put_object(parent_object, connection_name, args = {}, options = {})
116
47
  # Writes the given object to the graph, connected to the given parent.
117
- #
118
- # For example,
119
- #
120
- # graph.put_object("me", "feed", :message => "Hello, world")
121
- #
122
- # writes "Hello, world" to the active user's wall. Likewise, this
123
- # will comment on a the first post of the active user's feed:
124
- #
125
- # feed = graph.get_connections("me", "feed")
126
- # post = feed["data"][0]
127
- # graph.put_object(post["id"], "comments", :message => "First!")
128
- #
129
48
  # See http://developers.facebook.com/docs/api#publishing for all of
130
49
  # the supported writeable objects.
131
50
  #
51
+ # For example,
52
+ # graph.put_object("me", "feed", :message => "Hello, world")
53
+ # writes "Hello, world" to the active user's wall.
54
+ #
132
55
  # Most write operations require extended permissions. For example,
133
56
  # publishing wall posts requires the "publish_stream" permission. See
134
57
  # http://developers.facebook.com/docs/authentication/ for details about
135
58
  # extended permissions.
59
+
60
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
61
+ graph_call("#{parent_object}/#{connection_name}", args, "post", options)
62
+ end
136
63
 
64
+ def delete_object(id, options = {})
65
+ # Deletes the object with the given ID from the graph.
66
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Delete requires an access token"}) unless @access_token
67
+ graph_call(id, {}, "delete", options)
68
+ end
69
+
70
+ # Connections
71
+
72
+ def get_connections(id, connection_name, args = {}, options = {})
73
+ # Fetchs the connections for given object.
74
+ graph_call("#{id}/#{connection_name}", args, "get", options) do |result|
75
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
76
+ end
77
+ end
78
+
79
+ def get_comments_for_urls(urls = [], args = {}, options = {})
80
+ # Fetchs the comments for given URLs (array or comma-separated string)
81
+ # see https://developers.facebook.com/blog/post/490
82
+ return [] if urls.empty?
83
+ args.merge!(:ids => urls.respond_to?(:join) ? urls.join(",") : urls)
84
+ get_object("comments", args, options)
85
+ end
86
+
87
+ def put_connections(id, connection_name, args = {}, options = {})
88
+ # Posts a certain connection
137
89
  raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
138
- graph_call("#{parent_object}/#{connection_name}", args, "post")
90
+ graph_call("#{id}/#{connection_name}", args, "post", options)
91
+ end
92
+
93
+ def delete_connections(id, connection_name, args = {}, options = {})
94
+ # Deletes a given connection
95
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Delete requires an access token"}) unless @access_token
96
+ graph_call("#{id}/#{connection_name}", args, "delete", options)
139
97
  end
98
+
99
+ # Media (photos and videos)
100
+ # to delete photos or videos, use delete_object(object_id)
101
+ # note: you'll need the user_photos or user_videos permissions to actually access media after upload
140
102
 
141
- def put_wall_post(message, attachment = {}, profile_id = "me")
142
- # Writes a wall post to the given profile's wall.
143
- #
144
- # We default to writing to the authenticated user's wall if no
145
- # profile_id is specified.
146
- #
147
- # attachment adds a structured attachment to the status message being
148
- # posted to the Wall. It should be a dictionary of the form:
103
+ def get_picture(object, args = {}, options = {})
104
+ # Gets a picture object, returning the URL (which Facebook sends as a header)
105
+ graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
106
+ result["Location"]
107
+ end
108
+ end
109
+
110
+ # Can be called in multiple ways:
111
+ #
112
+ # put_picture(file, [content_type], ...)
113
+ # put_picture(path_to_file, [content_type], ...)
114
+ #
115
+ # You can pass in uploaded files directly from Rails or Sinatra.
116
+ # (See lib/koala/uploadable_io.rb for supported frameworks)
117
+ #
118
+ # Optional parameters can be added to the end of the argument list:
119
+ # - args: a hash of request parameters (default: {})
120
+ # - target_id: ID of the target where to post the picture (default: "me")
121
+ # - options: a hash of http options passed to the HTTPService module
122
+ #
123
+ # put_picture(file, content_type, {:message => "Message"}, 01234560)
124
+ # put_picture(params[:file], {:message => "Message"})
125
+
126
+ def put_picture(*picture_args)
127
+ put_object(*parse_media_args(picture_args, "photos"))
128
+ end
129
+
130
+ def put_video(*video_args)
131
+ args = parse_media_args(video_args, "videos")
132
+ args.last[:video] = true
133
+ put_object(*args)
134
+ end
135
+
136
+ # Wall posts
137
+ # To get wall posts, use get_connections(user, "feed")
138
+ # To delete a wall post, just use delete_object(post_id)
139
+
140
+ def put_wall_post(message, attachment = {}, profile_id = "me", options = {})
141
+ # attachment is a hash describing the wall post
142
+ # (see X for more details)
143
+ # For instance,
149
144
  #
150
- # {"name": "Link name"
151
- # "link": "http://www.example.com/",
152
- # "caption": "{*actor*} posted a new review",
153
- # "description": "This is a longer description of the attachment",
154
- # "picture": "http://www.example.com/thumbnail.jpg"}
145
+ # {"name" => "Link name"
146
+ # "link" => "http://www.example.com/",
147
+ # "caption" => "{*actor*} posted a new review",
148
+ # "description" => "This is a longer description of the attachment",
149
+ # "picture" => "http://www.example.com/thumbnail.jpg"}
155
150
 
156
- self.put_object(profile_id, "feed", attachment.merge({:message => message}))
151
+ self.put_object(profile_id, "feed", attachment.merge({:message => message}), options)
157
152
  end
158
-
159
- def put_comment(object_id, message)
153
+
154
+ # Comments
155
+ # to delete comments, use delete_object(comment_id)
156
+ # to get comments, use get_connections(object, "likes")
157
+
158
+ def put_comment(object_id, message, options = {})
160
159
  # Writes the given comment on the given post.
161
- self.put_object(object_id, "comments", {:message => message})
160
+ self.put_object(object_id, "comments", {:message => message}, options)
162
161
  end
163
-
164
- def put_like(object_id)
162
+
163
+ # Likes
164
+ # to get likes, use get_connections(user, "likes")
165
+
166
+ def put_like(object_id, options = {})
165
167
  # Likes the given post.
166
- self.put_object(object_id, "likes")
168
+ self.put_object(object_id, "likes", {}, options)
167
169
  end
168
-
169
- def delete_object(id)
170
- # Deletes the object with the given ID from the graph.
171
- graph_call(id, {}, "delete")
170
+
171
+ def delete_like(object_id, options = {})
172
+ # Unlikes a given object for the logged-in user
173
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Unliking requires an access token"}) unless @access_token
174
+ graph_call("#{object_id}/likes", {}, "delete", options)
172
175
  end
173
-
174
- def search(search_terms, args = {})
175
- # Searches for a given term
176
- result = graph_call("search", args.merge({:q => search_terms}))
177
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
176
+
177
+ # Search
178
+
179
+ def search(search_terms, args = {}, options = {})
180
+ args.merge!({:q => search_terms}) unless search_terms.nil?
181
+ graph_call("search", args, "get", options) do |result|
182
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
183
+ end
184
+ end
185
+
186
+
187
+ # GraphCollection support
188
+ def get_page(params)
189
+ # Pages through a set of results stored in a GraphCollection
190
+ # Used for connections and search results
191
+ graph_call(*params) do |result|
192
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
193
+ end
178
194
  end
179
-
180
- def graph_call(*args)
181
- response = api(*args) do |response|
182
- # check for Graph API-specific errors
183
- if response.is_a?(Hash) && error_details = response["error"]
184
- raise APIError.new(error_details)
195
+
196
+
197
+ # Batch API
198
+ def batch(http_options = {}, &block)
199
+ batch_client = GraphBatchAPI.new(access_token)
200
+ if block
201
+ yield batch_client
202
+ batch_client.execute(http_options)
203
+ else
204
+ batch_client
205
+ end
206
+ end
207
+
208
+ def self.included(base)
209
+ base.class_eval do
210
+ def self.batch
211
+ raise NoMethodError, "The BatchAPI signature has changed (the original implementation was not thread-safe). Please see https://github.com/arsduo/koala/wiki/Batch-requests. (This message will be removed in the final 1.1 release.)"
185
212
  end
186
213
  end
214
+ end
187
215
 
188
- response
216
+ # Direct access to the Facebook API
217
+ # see any of the above methods for example invocations
218
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
219
+ result = api(path, args, verb, options) do |response|
220
+ error = check_response(response)
221
+ raise error if error
222
+ end
223
+
224
+ # now process as appropriate (get picture header, make GraphCollection, etc.)
225
+ post_processing ? post_processing.call(result) : result
226
+ end
227
+
228
+ def check_response(response)
229
+ # check for Graph API-specific errors
230
+ # this returns an error, which is immediately raised (non-batch)
231
+ # or added to the list of batch results (batch)
232
+ if response.is_a?(Hash) && error_details = response["error"]
233
+ APIError.new(error_details)
234
+ end
189
235
  end
236
+
237
+ private
238
+
239
+ def parse_media_args(media_args, method)
240
+ # photo and video uploads can accept different types of arguments (see above)
241
+ # so here, we parse the arguments into a form directly usable in put_object
242
+ raise KoalaError.new("Wrong number of arguments for put_#{method == "photos" ? "picture" : "video"}") unless media_args.size.between?(1, 5)
243
+
244
+ args_offset = media_args[1].kind_of?(Hash) || media_args.size == 1 ? 0 : 1
245
+
246
+ args = media_args[1 + args_offset] || {}
247
+ target_id = media_args[2 + args_offset] || "me"
248
+ options = media_args[3 + args_offset] || {}
249
+
250
+ args["source"] = Koala::UploadableIO.new(*media_args.slice(0, 1 + args_offset))
251
+
252
+ options[:http_service] = Koala.base_http_service if args["source"].requires_base_http_service
253
+
254
+ [target_id, method, args, options]
255
+ end
190
256
  end
191
257
  end
192
- end
258
+ end
@@ -0,0 +1,87 @@
1
+ module Koala
2
+ module Facebook
3
+ module GraphBatchAPIMethods
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ alias_method :graph_call_outside_batch, :graph_call
8
+ alias_method :graph_call, :graph_call_in_batch
9
+
10
+ alias_method :check_graph_api_response, :check_response
11
+ alias_method :check_response, :check_graph_batch_api_response
12
+ end
13
+ end
14
+
15
+ def batch_calls
16
+ @batch_calls ||= []
17
+ end
18
+
19
+ def graph_call_in_batch(path, args = {}, verb = "get", options = {}, &post_processing)
20
+ # for batch APIs, we queue up the call details (incl. post-processing)
21
+ batch_calls << BatchOperation.new(
22
+ :url => path,
23
+ :args => args,
24
+ :method => verb,
25
+ :access_token => options['access_token'] || access_token,
26
+ :http_options => options,
27
+ :post_processing => post_processing
28
+ )
29
+ nil # batch operations return nothing immediately
30
+ end
31
+
32
+ def check_graph_batch_api_response(response)
33
+ if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
34
+ APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"])
35
+ else
36
+ check_graph_api_response(response)
37
+ end
38
+ end
39
+
40
+ def execute(http_options = {})
41
+ return [] unless batch_calls.length > 0
42
+ # Turn the call args collected into what facebook expects
43
+ args = {}
44
+ args["batch"] = MultiJson.encode(batch_calls.map { |batch_op|
45
+ args.merge!(batch_op.files) if batch_op.files
46
+ batch_op.to_batch_params(access_token)
47
+ })
48
+
49
+ graph_call_outside_batch('/', args, 'post', http_options) do |response|
50
+ # map the results with post-processing included
51
+ index = 0 # keep compat with ruby 1.8 - no with_index for map
52
+ response.map do |call_result|
53
+ # Get the options hash
54
+ batch_op = batch_calls[index]
55
+ index += 1
56
+
57
+ if call_result
58
+ # (see note in regular api method about JSON parsing)
59
+ body = MultiJson.decode("[#{call_result['body'].to_s}]")[0]
60
+
61
+ unless call_result["code"].to_i >= 500 || error = check_response(body)
62
+ # Get the HTTP component they want
63
+ data = case batch_op.http_options[:http_component]
64
+ when :status
65
+ call_result["code"].to_i
66
+ when :headers
67
+ # facebook returns the headers as an array of k/v pairs, but we want a regular hash
68
+ call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
69
+ else
70
+ body
71
+ end
72
+
73
+ # process it if we are given a block to process with
74
+ batch_op.post_processing ? batch_op.post_processing.call(data) : data
75
+ else
76
+ error || APIError.new({"type" => "HTTP #{call_result["code"].to_s}", "message" => "Response body: #{body}"})
77
+ end
78
+ else
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,54 @@
1
+ module Koala
2
+ module Facebook
3
+ class GraphCollection < Array
4
+ # This class is a light wrapper for collections returned
5
+ # from the Graph API.
6
+ #
7
+ # It extends Array to allow direct access to the data colleciton
8
+ # which should allow it to drop in seamlessly.
9
+ #
10
+ # It also allows access to paging information and the
11
+ # ability to get the next/previous page in the collection
12
+ # by calling next_page or previous_page.
13
+ attr_reader :paging
14
+ attr_reader :api
15
+
16
+ def initialize(response, api)
17
+ super response["data"]
18
+ @paging = response["paging"]
19
+ @api = api
20
+ end
21
+
22
+ # defines methods for NEXT and PREVIOUS pages
23
+ %w{next previous}.each do |this|
24
+
25
+ # def next_page
26
+ # def previous_page
27
+ define_method "#{this.to_sym}_page" do
28
+ base, args = send("#{this}_page_params")
29
+ base ? @api.get_page([base, args]) : nil
30
+ end
31
+
32
+ # def next_page_params
33
+ # def previous_page_params
34
+ define_method "#{this.to_sym}_page_params" do
35
+ return nil unless @paging and @paging[this]
36
+ parse_page_url(@paging[this])
37
+ end
38
+ end
39
+
40
+ def parse_page_url(url)
41
+ match = url.match(/.com\/(.*)\?(.*)/)
42
+ base = match[1]
43
+ args = match[2]
44
+ params = CGI.parse(args)
45
+ new_params = {}
46
+ params.each_pair do |key,value|
47
+ new_params[key] = value.join ","
48
+ end
49
+ [base,new_params]
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,92 @@
1
+ require "net/http" unless defined?(Net::HTTP)
2
+ require "net/https"
3
+ require "net/http/post/multipart"
4
+
5
+ module Koala
6
+ module NetHTTPService
7
+ # this service uses Net::HTTP to send requests to the graph
8
+ include Koala::HTTPService
9
+
10
+ # Net::HTTP-specific values
11
+ class << self
12
+ attr_accessor :ca_file, :ca_path, :verify_mode
13
+ end
14
+
15
+ def self.make_request(path, args, verb, options = {})
16
+ # We translate args to a valid query string. If post is specified,
17
+ # we send a POST request to the given path with the given arguments.
18
+
19
+ # by default, we use SSL only for private requests
20
+ # this makes public requests faster
21
+ private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
22
+
23
+ # if the verb isn't get or post, send it as a post argument
24
+ args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
25
+
26
+ http = create_http(server(options), private_request, options)
27
+
28
+ response = http.start do |http|
29
+ if verb == "post"
30
+ if params_require_multipart? args
31
+ http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
32
+ else
33
+ http.post(path, encode_params(args))
34
+ end
35
+ else
36
+ http.get("#{path}?#{encode_params(args)}")
37
+ end
38
+ end
39
+
40
+ Koala::Response.new(response.code.to_i, response.body, response)
41
+ end
42
+
43
+ protected
44
+ def self.encode_params(param_hash)
45
+ # unfortunately, we can't use to_query because that's Rails, not Ruby
46
+ # if no hash (e.g. no auth token) return empty string
47
+ ((param_hash || {}).collect do |key_and_value|
48
+ key_and_value[1] = MultiJson.encode(key_and_value[1]) if key_and_value[1].class != String
49
+ "#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
50
+ end).join("&")
51
+ end
52
+
53
+ def self.encode_multipart_params(param_hash)
54
+ Hash[*param_hash.collect do |key, value|
55
+ [key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
56
+ end.flatten]
57
+ end
58
+
59
+ def self.create_http(server, private_request, options)
60
+ if proxy_server = options[:proxy] || proxy
61
+ proxy = URI.parse(proxy_server)
62
+ http = Net::HTTP.new(server, private_request ? 443 : nil,
63
+ proxy.host, proxy.port, proxy.user, proxy.password)
64
+ else
65
+ http = Net::HTTP.new(server, private_request ? 443 : nil)
66
+ end
67
+
68
+ if timeout_value = options[:timeout] || timeout
69
+ http.open_timeout = timeout_value
70
+ http.read_timeout = timeout_value
71
+ end
72
+
73
+ # For HTTPS requests, set the proper CA certificates
74
+ if private_request
75
+ http.use_ssl = true
76
+ http.verify_mode = options[:verify_mode] || verify_mode || OpenSSL::SSL::VERIFY_PEER
77
+
78
+ if cert_file = options[:ca_file] || ca_file
79
+ raise Errno::ENOENT, "Certificate file #{cert_file.inspect} does not exist!" unless File.exists?(cert_file)
80
+ http.ca_file = cert_file
81
+ end
82
+
83
+ if cert_path = options[:ca_path] || ca_path
84
+ raise Errno::ENOENT, "Certificate path #{cert_path.inspect} does not exist!" unless File.directory?(cert_path)
85
+ http.ca_path = cert_path
86
+ end
87
+ end
88
+
89
+ http
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,37 @@
1
+ require "typhoeus" unless defined?(Typhoeus)
2
+
3
+ module Koala
4
+ module TyphoeusService
5
+ # this service uses Typhoeus to send requests to the graph
6
+ include Typhoeus
7
+ include Koala::HTTPService
8
+
9
+ def self.make_request(path, args, verb, options = {})
10
+ # if the verb isn't get or post, send it as a post argument
11
+ args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
12
+
13
+ # switch any UploadableIOs to the files Typhoeus expects
14
+ args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
15
+
16
+ # you can pass arguments directly to Typhoeus using the :typhoeus_options key
17
+ typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
18
+
19
+ # if proxy/timeout options aren't passed, check if defaults are set
20
+ typhoeus_options[:proxy] ||= proxy
21
+ typhoeus_options[:timeout] ||= timeout
22
+
23
+ # by default, we use SSL only for private requests (e.g. with access token)
24
+ # this makes public requests faster
25
+ prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
26
+
27
+ response = Typhoeus::Request.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
28
+ Koala::Response.new(response.code, response.body, response.headers_hash)
29
+ end
30
+
31
+ protected
32
+
33
+ def self.multipart_requires_content_type?
34
+ false # Typhoeus handles multipart file types, we don't have to require it
35
+ end
36
+ end
37
+ end