koala 0.4 → 3.7.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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +32 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +1 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +25 -0
  7. data/ISSUE_TEMPLATE +25 -0
  8. data/LICENSE +22 -0
  9. data/Manifest +32 -5
  10. data/PULL_REQUEST_TEMPLATE +11 -0
  11. data/Rakefile +12 -12
  12. data/changelog.md +781 -0
  13. data/code_of_conduct.md +74 -0
  14. data/koala.gemspec +28 -24
  15. data/lib/koala/api/batch_operation.rb +86 -0
  16. data/lib/koala/api/graph_api_methods.rb +504 -0
  17. data/lib/koala/api/graph_batch_api.rb +167 -0
  18. data/lib/koala/api/graph_collection.rb +129 -0
  19. data/lib/koala/api/graph_error_checker.rb +72 -0
  20. data/lib/koala/api.rb +159 -0
  21. data/lib/koala/configuration.rb +56 -0
  22. data/lib/koala/errors.rb +126 -0
  23. data/lib/koala/http_service/request.rb +133 -0
  24. data/lib/koala/http_service/response.rb +20 -0
  25. data/lib/koala/http_service/uploadable_io.rb +183 -0
  26. data/lib/koala/http_service.rb +108 -0
  27. data/lib/koala/oauth.rb +342 -0
  28. data/lib/koala/realtime_updates.rb +151 -0
  29. data/lib/koala/test_users.rb +189 -0
  30. data/lib/koala/utils.rb +41 -0
  31. data/lib/koala/version.rb +3 -0
  32. data/lib/koala.rb +51 -291
  33. data/readme.md +269 -21
  34. data/spec/cases/api_spec.rb +362 -0
  35. data/spec/cases/configuration_spec.rb +11 -0
  36. data/spec/cases/error_spec.rb +143 -0
  37. data/spec/cases/graph_api_batch_spec.rb +788 -0
  38. data/spec/cases/graph_api_spec.rb +76 -0
  39. data/spec/cases/graph_collection_spec.rb +192 -0
  40. data/spec/cases/graph_error_checker_spec.rb +147 -0
  41. data/spec/cases/http_service/request_spec.rb +250 -0
  42. data/spec/cases/http_service/response_spec.rb +24 -0
  43. data/spec/cases/http_service_spec.rb +280 -0
  44. data/spec/cases/koala_spec.rb +57 -0
  45. data/spec/cases/koala_test_spec.rb +5 -0
  46. data/spec/cases/oauth_spec.rb +647 -0
  47. data/spec/cases/realtime_updates_spec.rb +327 -0
  48. data/spec/cases/test_users_spec.rb +383 -0
  49. data/spec/cases/uploadable_io_spec.rb +266 -0
  50. data/spec/cases/utils_spec.rb +55 -0
  51. data/spec/fixtures/beach.jpg +0 -0
  52. data/spec/fixtures/cat.m4v +0 -0
  53. data/spec/fixtures/facebook_data.yml +63 -0
  54. data/spec/fixtures/mock_facebook_responses.yml +483 -0
  55. data/spec/fixtures/vcr_cassettes/app_test_accounts.yml +97 -0
  56. data/spec/fixtures/vcr_cassettes/friend_list_next_page.yml +121 -0
  57. data/spec/integration/graph_collection_spec.rb +24 -0
  58. data/spec/spec_helper.rb +25 -0
  59. data/spec/support/custom_matchers.rb +28 -0
  60. data/spec/support/graph_api_shared_examples.rb +534 -0
  61. data/spec/support/koala_test.rb +251 -0
  62. data/spec/support/mock_http_service.rb +140 -0
  63. data/spec/support/uploadable_io_shared_examples.rb +70 -0
  64. metadata +206 -62
  65. data/CHANGELOG +0 -24
  66. data/init.rb +0 -2
  67. data/lib/http_services.rb +0 -60
  68. data/test/facebook_data.yml +0 -5
  69. data/test/koala/facebook_no_access_token_tests.rb +0 -119
  70. data/test/koala/facebook_with_access_token_tests.rb +0 -106
  71. data/test/koala_tests.rb +0 -30
@@ -0,0 +1,504 @@
1
+ require 'addressable/uri'
2
+
3
+ require 'koala/api/graph_collection'
4
+ require 'koala/http_service/uploadable_io'
5
+ require 'koala/api/graph_error_checker'
6
+
7
+ module Koala
8
+ module Facebook
9
+ # Methods used to interact with the Facebook Graph API.
10
+ #
11
+ # See https://github.com/arsduo/koala/wiki/Graph-API for a general introduction to Koala
12
+ # and the Graph API.
13
+ #
14
+ # The Graph API is made up of the objects in Facebook (e.g., people, pages,
15
+ # events, photos, etc.) and the connections between them (e.g., friends,
16
+ # photo tags, event RSVPs, etc.). Koala provides access to those
17
+ # objects types in a generic way. For example, given an OAuth access
18
+ # token, this will fetch the profile of the active user and the list
19
+ # of the user's friends:
20
+ #
21
+ # @example
22
+ # graph = Koala::Facebook::API.new(access_token)
23
+ # user = graph.get_object("me")
24
+ # friends = graph.get_connections(user["id"], "friends")
25
+ #
26
+ # You can see a list of all of the objects and connections supported
27
+ # by the API at http://developers.facebook.com/docs/reference/api/.
28
+ #
29
+ # You can obtain an access token via OAuth or by using the Facebook JavaScript SDK.
30
+ # If you're using the JavaScript SDK, you can use the
31
+ # {Koala::Facebook::OAuth#get_user_from_cookie} method to get the OAuth access token
32
+ # for the active user from the cookie provided by Facebook.
33
+ # See the Koala and Facebook documentation for more information.
34
+ module GraphAPIMethods
35
+ # Objects
36
+
37
+ # Get information about a Facebook object.
38
+ #
39
+ # @param id the object ID (string or number)
40
+ # @param args any additional arguments
41
+ # (fields, metadata, etc. -- see {http://developers.facebook.com/docs/reference/api/ Facebook's documentation})
42
+ # @param options (see Koala::Facebook::API#api)
43
+ # @param block for post-processing. It receives the result data; the
44
+ # return value of the method is the result of the block, if
45
+ # provided. (see Koala::Facebook::API#api)
46
+ #
47
+ # @raise [Koala::Facebook::APIError] if the ID is invalid or you don't have access to that object
48
+ #
49
+ # @example
50
+ # get_object("me") # => {"id" => ..., "name" => ...}
51
+ # get_object("me") {|data| data['education']} # => only education section of profile
52
+ #
53
+ # @return a hash of object data
54
+ def get_object(id, args = {}, options = {}, &block)
55
+ # Fetches the given object from the graph.
56
+ graph_call(id, args, "get", options, &block)
57
+ end
58
+
59
+ # Get the metadata of a Facebook object, including its type.
60
+ #
61
+ # @param id the object ID (string or number)
62
+ #
63
+ # @raise [Koala::Facebook::ClientError] if the ID is invalid
64
+ # @example
65
+ # get_object_metadata("442575165800306")=>{"metadata" => "page", ...}
66
+ # get_object_metadata("190822584430113")=>{"metadata" => "status", ...}
67
+ # @return a string of Facebook object type
68
+ def get_object_metadata(id, &block)
69
+ result = graph_call(id, {"metadata" => "1"}, "get", {}, &block)
70
+ result["metadata"]
71
+ end
72
+
73
+ # Get information about multiple Facebook objects in one call.
74
+ #
75
+ # @param ids an array or comma-separated string of object IDs
76
+ # @param args (see #get_object)
77
+ # @param options (see Koala::Facebook::API#api)
78
+ # @param block (see Koala::Facebook::API#api)
79
+ #
80
+ # @raise [Koala::Facebook::APIError] if any ID is invalid or you don't have access to that object
81
+ #
82
+ # @return an array of object data hashes
83
+ def get_objects(ids, args = {}, options = {}, &block)
84
+ # Fetches all of the given objects from the graph.
85
+ # If any of the IDs are invalid, they'll raise an exception.
86
+ return [] if ids.empty?
87
+ graph_call("", args.merge("ids" => ids.respond_to?(:join) ? ids.join(",") : ids), "get", options, &block)
88
+ end
89
+
90
+ # Write an object to the Graph for a specific user.
91
+ # @see #put_connections
92
+ #
93
+ # @note put_object is (for historical reasons) the same as put_connections.
94
+ # Please use put_connections; in a future version of Koala (2.0?),
95
+ # put_object will issue a POST directly to an individual object, not to a connection.
96
+ def put_object(parent_object, connection_name, args = {}, options = {}, &block)
97
+ put_connections(parent_object, connection_name, args, options, &block)
98
+ end
99
+
100
+ # Delete an object from the Graph if you have appropriate permissions.
101
+ #
102
+ # @param id (see #get_object)
103
+ # @param options (see #get_object)
104
+ # @param block (see Koala::Facebook::API#api)
105
+ #
106
+ # @return true if successful, false (or an APIError) if not
107
+ def delete_object(id, options = {}, &block)
108
+ # Deletes the object with the given ID from the graph.
109
+ raise AuthenticationError.new(nil, nil, "Delete requires an access token") unless access_token
110
+ graph_call(id, {}, "delete", options, &block)
111
+ end
112
+
113
+ # Fetch information about a given connection (e.g. type of activity -- feed, events, photos, etc.)
114
+ # for a specific user.
115
+ # See {http://developers.facebook.com/docs/api Facebook's documentation} for a complete list of connections.
116
+ #
117
+ # @note to access connections like /user_id/CONNECTION/other_user_id,
118
+ # simply pass "CONNECTION/other_user_id" as the connection_name
119
+ #
120
+ # @param id (see #get_object)
121
+ # @param connection_name what
122
+ # @param args any additional arguments
123
+ # @param options (see #get_object)
124
+ # @param block (see Koala::Facebook::API#api)
125
+ #
126
+ # @return [Koala::Facebook::API::GraphCollection] an array of object hashes (in most cases)
127
+ def get_connection(id, connection_name, args = {}, options = {}, &block)
128
+ # Fetches the connections for given object.
129
+ graph_call("#{id}/#{connection_name}", args, "get", options, &block)
130
+ end
131
+ alias_method :get_connections, :get_connection
132
+
133
+
134
+ # Write an object to the Graph for a specific user.
135
+ # See {http://developers.facebook.com/docs/api#publishing Facebook's documentation}
136
+ # for all the supported writeable objects. It is important to note that objects
137
+ # take the singular form, i.e. "event" when using put_connections.
138
+ #
139
+ # @note (see #get_connection)
140
+ #
141
+ # @example
142
+ # graph.put_connections("me", "feed", :message => "Hello, world")
143
+ # => writes "Hello, world" to the active user's wall
144
+ #
145
+ # Most write operations require extended permissions. For example,
146
+ # publishing wall posts requires the "publish_stream" permission. See
147
+ # http://developers.facebook.com/docs/authentication/ for details about
148
+ # extended permissions.
149
+ #
150
+ # @param id (see #get_object)
151
+ # @param connection_name (see #get_connection)
152
+ # @param args (see #get_connection)
153
+ # @param options (see #get_object)
154
+ # @param block (see Koala::Facebook::API#api)
155
+ #
156
+ # @return a hash containing the new object's id
157
+ def put_connections(id, connection_name, args = {}, options = {}, &block)
158
+ # Posts a certain connection
159
+ raise AuthenticationError.new(nil, nil, "Write operations require an access token") unless access_token
160
+
161
+ graph_call("#{id}/#{connection_name}", args, "post", options, &block)
162
+ end
163
+
164
+ # Delete an object's connection (for instance, unliking the object).
165
+ #
166
+ # @note (see #get_connection)
167
+ #
168
+ # @param id (see #get_object)
169
+ # @param connection_name (see #get_connection)
170
+ # @args (see #get_connection)
171
+ # @param options (see #get_object)
172
+ # @param block (see Koala::Facebook::API#api)
173
+ #
174
+ # @return (see #delete_object)
175
+ def delete_connections(id, connection_name, args = {}, options = {}, &block)
176
+ # Deletes a given connection
177
+ raise AuthenticationError.new(nil, nil, "Delete requires an access token") unless access_token
178
+ graph_call("#{id}/#{connection_name}", args, "delete", options, &block)
179
+ end
180
+
181
+ # Fetches a photo url.
182
+ # Note that this method returns the picture url, not the full API
183
+ # response. For the hash containing the full metadata for the photo, use
184
+ # #get_user_picture_data instead.
185
+ #
186
+ # @param options options for Facebook (see #get_object).
187
+ # To get a different size photo, pass :type => size (small, normal, large, square).
188
+ # @param block (see Koala::Facebook::API#api)
189
+ #
190
+ # @note to delete photos or videos, use delete_object(id)
191
+ #
192
+ # @return the URL to the image
193
+ def get_picture(object, args = {}, options = {}, &block)
194
+ Koala::Utils.deprecate("API#get_picture will be removed in a future version. Please use API#get_picture_data, which returns a hash including the url.")
195
+
196
+ get_user_picture_data(object, args, options) do |result|
197
+ # Try to extract the URL
198
+ result = result.fetch('data', {})['url'] if result.respond_to?(:fetch)
199
+ block ? block.call(result) : result
200
+ end
201
+ end
202
+
203
+ # Fetches a photo data hash.
204
+ #
205
+ # @param args (see #get_object)
206
+ # @param options (see Koala::Facebook::API#api)
207
+ # @param block (see Koala::Facebook::API#api)
208
+ #
209
+ # @return a hash of object data
210
+ def get_picture_data(object, args = {}, options = {}, &block)
211
+ # The default response for a Graph API query like GET /me/picture is to
212
+ # return a 302 redirect. This is a surprising difference from the
213
+ # common return type, so we add the `redirect: false` parameter to get
214
+ # a RESTful API response instead.
215
+ args = args.merge(:redirect => false)
216
+ graph_call("#{object}/picture", args, "get", options, &block)
217
+ end
218
+
219
+ def get_user_picture_data(*args, &block)
220
+ Koala::Utils.deprecate("API#get_user_picture_data is deprecated and will be removed in a future version. Please use API#get_picture_data, which has the same signature.")
221
+ get_picture_data(*args, &block)
222
+ end
223
+
224
+ # Upload a photo.
225
+ #
226
+ # This can be called in multiple ways:
227
+ # put_picture(file, [content_type], ...)
228
+ # put_picture(path_to_file, [content_type], ...)
229
+ # put_picture(picture_url, ...)
230
+ #
231
+ # You can also pass in uploaded files directly from Rails or Sinatra.
232
+ # See {https://github.com/arsduo/koala/wiki/Uploading-Photos-and-Videos the Koala wiki} for more information.
233
+ #
234
+ # @param args (see #get_object)
235
+ # @param target_id the Facebook object to which to post the picture (default: "me")
236
+ # @param options (see #get_object)
237
+ # @param block (see Koala::Facebook::API#api)
238
+ #
239
+ # @example
240
+ # put_picture(file, content_type, {:message => "Message"}, 01234560)
241
+ # put_picture(params[:file], {:message => "Message"})
242
+ # # with URLs, there's no optional content type field
243
+ # put_picture(picture_url, {:message => "Message"}, my_page_id)
244
+ #
245
+ # @note to access the media after upload, you'll need the user_photos or user_videos permission as appropriate.
246
+ #
247
+ # @return (see #put_connections)
248
+ def put_picture(*picture_args, &block)
249
+ put_connections(*parse_media_args(picture_args, "photos"), &block)
250
+ end
251
+
252
+ # Upload a video. Functions exactly the same as put_picture (URLs supported as of Facebook
253
+ # API version 2.3).
254
+ # @see #put_picture
255
+ def put_video(*video_args, &block)
256
+ args = parse_media_args(video_args, "videos")
257
+ args.last[:video] = true
258
+ put_connections(*args, &block)
259
+ end
260
+
261
+ # Write directly to the user's wall.
262
+ # Convenience method equivalent to put_connections(id, "feed").
263
+ #
264
+ # To get wall posts, use get_connections(user, "feed")
265
+ # To delete a wall post, use delete_object(post_id)
266
+ #
267
+ # @param message the message to write for the wall
268
+ # @param attachment a hash describing the wall post
269
+ # (see the {https://developers.facebook.com/docs/guides/attachments/ stream attachments} documentation.)
270
+ # If attachment contains a properties key, this will be turned to
271
+ # JSON (if it's a hash) since Facebook's API, oddly, requires
272
+ # this.
273
+ # @param target_id the target wall
274
+ # @param options (see #get_object)
275
+ # @param block (see Koala::Facebook::API#api)
276
+ #
277
+ # @example
278
+ # @api.put_wall_post("Hello there!", {
279
+ # "name" => "Link name",
280
+ # "link" => "http://www.example.com/",
281
+ # "caption" => "{*actor*} posted a new review",
282
+ # "description" => "This is a longer description of the attachment",
283
+ # "picture" => "http://www.example.com/thumbnail.jpg"
284
+ # })
285
+ #
286
+ # @see #put_connections
287
+ # @return (see #put_connections)
288
+ def put_wall_post(message, attachment = {}, target_id = "me", options = {}, &block)
289
+ if properties = attachment.delete(:properties) || attachment.delete("properties")
290
+ properties = JSON.dump(properties) if properties.is_a?(Hash) || properties.is_a?(Array)
291
+ attachment["properties"] = properties
292
+ end
293
+ put_connections(target_id, "feed", attachment.merge({:message => message}), options, &block)
294
+ end
295
+
296
+ # Comment on a given object.
297
+ # Convenience method equivalent to put_connection(id, "comments").
298
+ #
299
+ # To delete comments, use delete_object(comment_id).
300
+ # To get comments, use get_connections(object, "likes").
301
+ #
302
+ # @param id (see #get_object)
303
+ # @param message the comment to write
304
+ # @param options (see #get_object)
305
+ # @param block (see Koala::Facebook::API#api)
306
+ #
307
+ # @return (see #put_connections)
308
+ def put_comment(id, message, options = {}, &block)
309
+ # Writes the given comment on the given post.
310
+ put_connections(id, "comments", {:message => message}, options, &block)
311
+ end
312
+
313
+ # Like a given object.
314
+ # Convenience method equivalent to put_connections(id, "likes").
315
+ #
316
+ # To get a list of a user's or object's likes, use get_connections(id, "likes").
317
+ #
318
+ # @param id (see #get_object)
319
+ # @param options (see #get_object)
320
+ # @param block (see Koala::Facebook::API#api)
321
+ #
322
+ # @return (see #put_connections)
323
+ def put_like(id, options = {}, &block)
324
+ # Likes the given post.
325
+ put_connections(id, "likes", {}, options, &block)
326
+ end
327
+
328
+ # Unlike a given object.
329
+ # Convenience method equivalent to delete_connection(id, "likes").
330
+ #
331
+ # @param id (see #get_object)
332
+ # @param options (see #get_object)
333
+ # @param block (see Koala::Facebook::API#api)
334
+ #
335
+ # @return (see #delete_object)
336
+ def delete_like(id, options = {}, &block)
337
+ # Unlikes a given object for the logged-in user
338
+ raise AuthenticationError.new(nil, nil, "Unliking requires an access token") unless access_token
339
+ graph_call("#{id}/likes", {}, "delete", options, &block)
340
+ end
341
+
342
+ # Search for a given query among visible Facebook objects.
343
+ # See {http://developers.facebook.com/docs/reference/api/#searching Facebook documentation} for more information.
344
+ #
345
+ # @param search_terms the query to search for
346
+ # @param args object type and any additional arguments, such as fields, etc.
347
+ # @param options (see #get_object)
348
+ # @param block (see Koala::Facebook::API#api)
349
+ #
350
+ # @return [Koala::Facebook::API::GraphCollection] an array of search results
351
+ def search(search_terms, args = {}, options = {}, &block)
352
+ # Normally we wouldn't enforce Facebook API behavior, but the API fails with cryptic error
353
+ # messages if you fail to include a type term. For a convenience method, that is valuable.
354
+ raise ArgumentError, "type must be includedin args when searching" unless args[:type] || args["type"]
355
+ graph_call("search", args.merge("q" => search_terms), "get", options, &block)
356
+ end
357
+
358
+ # Convenience Methods
359
+ # In general, we're trying to avoid adding convenience methods to Koala
360
+ # except to support cases where the Facebook API requires non-standard input
361
+ # such as JSON-encoding arguments, posts directly to objects, etc.
362
+
363
+ # Get a page's access token, allowing you to act as the page.
364
+ # Convenience method for @api.get_object(page_id, :fields => "access_token").
365
+ #
366
+ # @param id the page ID
367
+ # @param args (see #get_object)
368
+ # @param options (see #get_object)
369
+ # @param block (see Koala::Facebook::API#api)
370
+ #
371
+ # @return the page's access token (discarding expiration and any other information)
372
+ def get_page_access_token(id, args = {}, options = {}, &block)
373
+ access_token = get_object(id, args.merge(:fields => "access_token"), options) do |result|
374
+ result ? result["access_token"] : nil
375
+ end
376
+
377
+ block ? block.call(access_token) : access_token
378
+ end
379
+
380
+ # Get an access token information
381
+ # The access token used to instantiate the API object needs to be
382
+ # the app access token or a valid User Access Token from a developer of the app.
383
+ # See https://developers.facebook.com/docs/howtos/login/debugging-access-tokens/#step1
384
+ #
385
+ # @param input_token the access token you want to inspect
386
+ # @param block (see Koala::Facebook::API#api)
387
+ #
388
+ # @return a JSON array containing data and a map of fields
389
+ def debug_token(input_token, &block)
390
+ access_token_info = graph_call("debug_token", {:input_token => input_token})
391
+
392
+ block ? block.call(access_token_info) : access_token_info
393
+ end
394
+
395
+ # App restrictions require you to JSON-encode the restriction value. This
396
+ # is neither obvious nor intuitive, so this convenience method is
397
+ # provided.
398
+ #
399
+ # @params app_id the application to apply the restrictions to
400
+ # @params restrictions_hash the restrictions to apply
401
+ # @param args (see #get_object)
402
+ # @param options (see #get_object)
403
+ # @param block (see Koala::Facebook::API#api)
404
+ def set_app_restrictions(app_id, restrictions_hash, args = {}, options = {}, &block)
405
+ graph_call(app_id, args.merge(:restrictions => JSON.dump(restrictions_hash)), "post", options, &block)
406
+ end
407
+
408
+ # Certain calls such as {#get_connections} return an array of results which you can page through
409
+ # forwards and backwards (to see more feed stories, search results, etc.).
410
+ # Those methods use get_page to request another set of results from Facebook.
411
+ #
412
+ # @note You'll rarely need to use this method unless you're using Sinatra or another non-Rails framework
413
+ # (see {Koala::Facebook::API::GraphCollection GraphCollection} for more information).
414
+ #
415
+ # @param params an array of arguments to graph_call
416
+ # as returned by {Koala::Facebook::API::GraphCollection.parse_page_url}.
417
+ # @param block (see Koala::Facebook::API#api)
418
+ #
419
+ # @return Koala::Facebook::API::GraphCollection the appropriate page of results (an empty array if there are none)
420
+ def get_page(params, &block)
421
+ graph_call(*params, &block)
422
+ end
423
+
424
+ # Execute a set of Graph API calls as a batch.
425
+ # See {https://github.com/arsduo/koala/wiki/Batch-requests batch request documentation}
426
+ # for more information and examples.
427
+ #
428
+ # @param http_options HTTP options for the entire request.
429
+ #
430
+ # @yield batch_api [Koala::Facebook::GraphBatchAPI] an API subclass
431
+ # whose requests will be queued and executed together at the end of the block
432
+ #
433
+ # @raise [Koala::Facebook::APIError] only if there is a problem with the overall batch request
434
+ # (e.g. connectivity failure, an operation with a missing dependency).
435
+ # Individual calls that error out will be represented as an unraised
436
+ # APIError in the appropriate spot in the results array.
437
+ #
438
+ # @example
439
+ # results = @api.batch do |batch_api|
440
+ # batch_api.get_object('me')
441
+ # batch_api.get_object(KoalaTest.user1)
442
+ # end
443
+ # # => [{'id' => my_id, ...}, {'id' => koppel_id, ...}]
444
+ #
445
+ # # You can also provide blocks to your operations to process the
446
+ # # results, which is often useful if you're constructing batch
447
+ # # requests in various locations and want to keep the code
448
+ # # together in logical places.
449
+ # # See readme.md and the wiki for more examples.
450
+ # @api.batch do |batch_api|
451
+ # batch_api.get_object('me') {|data| data["id"] }
452
+ # batch_api.get_object(KoalaTest.user1) {|data| data["name"] }
453
+ # end
454
+ # # => [my_id, "Alex Koppel"]
455
+ #
456
+ # @return an array of results from your batch calls (as if you'd made them individually),
457
+ # arranged in the same order they're made.
458
+ def batch(http_options = {}, &block)
459
+ batch_client = GraphBatchAPI.new(self)
460
+ if block
461
+ yield batch_client
462
+ batch_client.execute(http_options)
463
+ else
464
+ batch_client
465
+ end
466
+ end
467
+
468
+ private
469
+
470
+ def parse_media_args(media_args, method)
471
+ # photo and video uploads can accept different types of arguments (see above)
472
+ # so here, we parse the arguments into a form directly usable in put_connections
473
+ raise KoalaError.new("Wrong number of arguments for put_#{method == "photos" ? "picture" : "video"}") unless media_args.size.between?(1, 5)
474
+
475
+ args_offset = media_args[1].kind_of?(Hash) || media_args.size == 1 ? 0 : 1
476
+
477
+ args = media_args[1 + args_offset] || {}
478
+ target_id = media_args[2 + args_offset] || "me"
479
+ options = media_args[3 + args_offset] || {}
480
+
481
+ if url?(media_args.first)
482
+ # If media_args is a URL, we can upload without UploadableIO
483
+ # Video: https://developers.facebook.com/docs/graph-api/video-uploads
484
+ fb_expected_arg_name = method == "photos" ? :url : :file_url
485
+ args.merge!(fb_expected_arg_name => media_args.first)
486
+ else
487
+ args["source"] = Koala::HTTPService::UploadableIO.new(*media_args.slice(0, 1 + args_offset))
488
+ end
489
+
490
+ [target_id, method, args, options]
491
+ end
492
+
493
+ def url?(data)
494
+ return false unless data.is_a? String
495
+ begin
496
+ uri = Addressable::URI.parse(data)
497
+ %w( http https ).include?(uri.scheme)
498
+ rescue Addressable::URI::InvalidURIError
499
+ false
500
+ end
501
+ end
502
+ end
503
+ end
504
+ end
@@ -0,0 +1,167 @@
1
+ require "koala/api"
2
+ require "koala/api/batch_operation"
3
+
4
+ module Koala
5
+ module Facebook
6
+ # @private
7
+ class GraphBatchAPI
8
+ # inside a batch call we can do anything a regular Graph API can do
9
+ include GraphAPIMethods
10
+
11
+ # Limits from @see https://developers.facebook.com/docs/marketing-api/batch-requests/v2.8
12
+ MAX_CALLS = 50
13
+
14
+ attr_reader :original_api
15
+ def initialize(api)
16
+ @original_api = api
17
+ end
18
+
19
+ def batch_calls
20
+ @batch_calls ||= []
21
+ end
22
+
23
+ # Enqueue a call into the batch for later processing.
24
+ # See API#graph_call
25
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
26
+ # normalize options for consistency
27
+ options = Koala::Utils.symbolize_hash(options)
28
+
29
+ # for batch APIs, we queue up the call details (incl. post-processing)
30
+ batch_calls << BatchOperation.new(
31
+ :url => path,
32
+ :args => args,
33
+ :method => verb,
34
+ :access_token => options[:access_token] || access_token,
35
+ :http_options => options,
36
+ :post_processing => post_processing
37
+ )
38
+ nil # batch operations return nothing immediately
39
+ end
40
+
41
+ # execute the queued batch calls. limits it to 50 requests per call.
42
+ # NOTE: if you use `name` and JsonPath references, you should ensure to call `execute` for each
43
+ # co-reference group and that the group size is not greater than the above limits.
44
+ def execute(http_options = {})
45
+ return [] if batch_calls.empty?
46
+
47
+ batch_results = []
48
+ batch_calls.each_slice(MAX_CALLS) do |batch|
49
+ # Turn the call args collected into what facebook expects
50
+ args = {"batch" => batch_args(batch)}
51
+ batch.each do |call|
52
+ args.merge!(call.files || {})
53
+ end
54
+
55
+ original_api.graph_call("/", args, "post", http_options) do |response|
56
+ raise bad_response('Facebook returned an empty body') if response.nil?
57
+
58
+ # when http_component is set we receive Koala::Http_service response object
59
+ # from graph_call so this needs to be parsed
60
+ # as generate_results method handles only JSON response
61
+ if http_options[:http_component] && http_options[:http_component] == :response
62
+ response = json_body(response.body)
63
+
64
+ raise bad_response('Facebook returned an invalid body') unless response.is_a?(Array)
65
+ end
66
+
67
+ batch_results += generate_results(response, batch)
68
+ end
69
+ end
70
+
71
+ batch_results
72
+ end
73
+
74
+ def generate_results(response, batch)
75
+ index = 0
76
+ response.map do |call_result|
77
+ batch_op = batch[index]
78
+ index += 1
79
+ post_process = batch_op.post_processing
80
+
81
+ # turn any results that are pageable into GraphCollections
82
+ result = result_from_response(call_result, batch_op)
83
+
84
+ # and pass to post-processing callback if given
85
+ if post_process
86
+ post_process.call(result)
87
+ else
88
+ result
89
+ end
90
+ end
91
+ end
92
+
93
+ def bad_response(message)
94
+ # Facebook sometimes reportedly returns an empty body at times
95
+ BadFacebookResponse.new(200, '', message)
96
+ end
97
+
98
+ def result_from_response(response, options)
99
+ return nil if response.nil?
100
+
101
+ headers = headers_from_response(response)
102
+ error = error_from_response(response, headers)
103
+ component = options.http_options[:http_component]
104
+
105
+ error || desired_component(
106
+ component: component,
107
+ response: response,
108
+ headers: headers
109
+ )
110
+ end
111
+
112
+ def headers_from_response(response)
113
+ headers = response.fetch("headers", [])
114
+
115
+ headers.inject({}) do |compiled_headers, header|
116
+ compiled_headers.merge(header.fetch("name") => header.fetch("value"))
117
+ end
118
+ end
119
+
120
+ def error_from_response(response, headers)
121
+ code = response["code"]
122
+ body = response["body"].to_s
123
+
124
+ GraphErrorChecker.new(code, body, headers).error_if_appropriate
125
+ end
126
+
127
+ def batch_args(calls_for_batch)
128
+ calls = calls_for_batch.map do |batch_op|
129
+ batch_op.to_batch_params(access_token, app_secret)
130
+ end
131
+
132
+ JSON.dump calls
133
+ end
134
+
135
+ def json_body(body)
136
+ return if body.nil?
137
+
138
+ JSON.parse(body)
139
+ rescue JSON::ParserError => e
140
+ Koala::Utils.logger.error("#{e.class}: #{e.message} while parsing #{body}")
141
+ nil
142
+ end
143
+
144
+ def desired_component(component:, response:, headers:)
145
+ result = Koala::HTTPService::Response.new(response['code'], response['body'], headers)
146
+
147
+ # Get the HTTP component they want
148
+ case component
149
+ when :status then response["code"].to_i
150
+ # facebook returns the headers as an array of k/v pairs, but we want a regular hash
151
+ when :headers then headers
152
+ # (see note in regular api method about JSON parsing)
153
+ when :response then result
154
+ else GraphCollection.evaluate(result, original_api)
155
+ end
156
+ end
157
+
158
+ def access_token
159
+ original_api.access_token
160
+ end
161
+
162
+ def app_secret
163
+ original_api.app_secret
164
+ end
165
+ end
166
+ end
167
+ end