koala 1.2.0 → 1.3.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 (54) hide show
  1. data/.gitignore +3 -1
  2. data/.rspec +1 -0
  3. data/.travis.yml +4 -0
  4. data/.yardopts +3 -0
  5. data/CHANGELOG +43 -0
  6. data/Gemfile +14 -0
  7. data/Guardfile +6 -0
  8. data/koala.gemspec +4 -4
  9. data/lib/koala/api/batch_operation.rb +83 -0
  10. data/lib/koala/api/graph_api.rb +476 -0
  11. data/lib/koala/{graph_batch_api.rb → api/graph_batch_api.rb} +22 -17
  12. data/lib/koala/api/graph_collection.rb +107 -0
  13. data/lib/koala/api/legacy.rb +26 -0
  14. data/lib/koala/{rest_api.rb → api/rest_api.rb} +36 -10
  15. data/lib/koala/api.rb +93 -0
  16. data/lib/koala/http_service/multipart_request.rb +41 -0
  17. data/lib/koala/http_service/response.rb +18 -0
  18. data/lib/koala/http_service/uploadable_io.rb +187 -0
  19. data/lib/koala/http_service.rb +73 -23
  20. data/lib/koala/oauth.rb +173 -36
  21. data/lib/koala/realtime_updates.rb +89 -51
  22. data/lib/koala/test_users.rb +122 -32
  23. data/lib/koala/utils.rb +11 -4
  24. data/lib/koala/version.rb +1 -1
  25. data/lib/koala.rb +16 -95
  26. data/readme.md +30 -13
  27. data/spec/cases/api_spec.rb +28 -21
  28. data/spec/cases/error_spec.rb +10 -0
  29. data/spec/cases/graph_api_batch_spec.rb +100 -58
  30. data/spec/cases/graph_collection_spec.rb +23 -7
  31. data/spec/cases/http_service_spec.rb +14 -34
  32. data/spec/cases/koala_spec.rb +22 -4
  33. data/spec/cases/legacy_spec.rb +115 -0
  34. data/spec/cases/multipart_request_spec.rb +66 -0
  35. data/spec/cases/oauth_spec.rb +232 -108
  36. data/spec/cases/realtime_updates_spec.rb +154 -47
  37. data/spec/cases/test_users_spec.rb +276 -217
  38. data/spec/cases/uploadable_io_spec.rb +1 -1
  39. data/spec/cases/utils_spec.rb +29 -5
  40. data/spec/fixtures/mock_facebook_responses.yml +50 -26
  41. data/spec/spec_helper.rb +3 -0
  42. data/spec/support/custom_matchers.rb +28 -0
  43. data/spec/support/graph_api_shared_examples.rb +274 -70
  44. data/spec/support/koala_test.rb +27 -16
  45. data/spec/support/mock_http_service.rb +2 -2
  46. data/spec/support/rest_api_shared_examples.rb +46 -162
  47. metadata +33 -25
  48. data/lib/koala/batch_operation.rb +0 -74
  49. data/lib/koala/graph_api.rb +0 -270
  50. data/lib/koala/graph_collection.rb +0 -59
  51. data/lib/koala/uploadable_io.rb +0 -181
  52. data/spec/cases/graph_and_rest_api_spec.rb +0 -22
  53. data/spec/cases/graph_api_spec.rb +0 -22
  54. data/spec/cases/rest_api_spec.rb +0 -41
@@ -0,0 +1,476 @@
1
+ require 'koala/api/graph_collection'
2
+ require 'koala/http_service/uploadable_io'
3
+
4
+ module Koala
5
+ module Facebook
6
+ GRAPH_SERVER = "graph.facebook.com"
7
+
8
+ # Methods used to interact with the Facebook Graph API.
9
+ #
10
+ # See https://github.com/arsduo/koala/wiki/Graph-API for a general introduction to Koala
11
+ # and the Graph API.
12
+ #
13
+ # The Graph API is made up of the objects in Facebook (e.g., people, pages,
14
+ # events, photos, etc.) and the connections between them (e.g., friends,
15
+ # photo tags, event RSVPs, etc.). Koala provides access to those
16
+ # objects types in a generic way. For example, given an OAuth access
17
+ # token, this will fetch the profile of the active user and the list
18
+ # of the user's friends:
19
+ #
20
+ # @example
21
+ # graph = Koala::Facebook::API.new(access_token)
22
+ # user = graph.get_object("me")
23
+ # friends = graph.get_connections(user["id"], "friends")
24
+ #
25
+ # You can see a list of all of the objects and connections supported
26
+ # by the API at http://developers.facebook.com/docs/reference/api/.
27
+ #
28
+ # You can obtain an access token via OAuth or by using the Facebook JavaScript SDK.
29
+ # If you're using the JavaScript SDK, you can use the
30
+ # {Koala::Facebook::OAuth#get_user_from_cookie} method to get the OAuth access token
31
+ # for the active user from the cookie provided by Facebook.
32
+ # See the Koala and Facebook documentation for more information.
33
+ module GraphAPIMethods
34
+
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
+ #
44
+ # @raise [Koala::Facebook::APIError] if the ID is invalid or you don't have access to that object
45
+ #
46
+ # @return a hash of object data
47
+ def get_object(id, args = {}, options = {})
48
+ # Fetchs the given object from the graph.
49
+ graph_call(id, args, "get", options)
50
+ end
51
+
52
+ # Get information about multiple Facebook objects in one call.
53
+ #
54
+ # @param ids an array or comma-separated string of object IDs
55
+ # @param args (see #get_object)
56
+ # @param options (see Koala::Facebook::API#api)
57
+ #
58
+ # @raise [Koala::Facebook::APIError] if any ID is invalid or you don't have access to that object
59
+ #
60
+ # @return an array of object data hashes
61
+ def get_objects(ids, args = {}, options = {})
62
+ # Fetchs all of the given objects from the graph.
63
+ # If any of the IDs are invalid, they'll raise an exception.
64
+ return [] if ids.empty?
65
+ graph_call("", args.merge("ids" => ids.respond_to?(:join) ? ids.join(",") : ids), "get", options)
66
+ end
67
+
68
+ # Write an object to the Graph for a specific user.
69
+ # @see #put_connections
70
+ #
71
+ # @note put_object is (for historical reasons) the same as put_connections.
72
+ # Please use put_connections; in a future version of Koala (2.0?),
73
+ # put_object will issue a POST directly to an individual object, not to a connection.
74
+ def put_object(parent_object, connection_name, args = {}, options = {})
75
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
76
+ graph_call("#{parent_object}/#{connection_name}", args, "post", options)
77
+ end
78
+
79
+ # Delete an object from the Graph if you have appropriate permissions.
80
+ #
81
+ # @param id (see #get_object)
82
+ # @param options (see #get_object)
83
+ #
84
+ # @return true if successful, false (or an APIError) if not
85
+ def delete_object(id, options = {})
86
+ # Deletes the object with the given ID from the graph.
87
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Delete requires an access token"}) unless @access_token
88
+ graph_call(id, {}, "delete", options)
89
+ end
90
+
91
+ # Fetch information about a given connection (e.g. type of activity -- feed, events, photos, etc.)
92
+ # for a specific user.
93
+ # See {http://developers.facebook.com/docs/api Facebook's documentation} for a complete list of connections.
94
+ #
95
+ # @note to access connections like /user_id/CONNECTION/other_user_id,
96
+ # simply pass "CONNECTION/other_user_id" as the connection_name
97
+ #
98
+ # @param id (see #get_object)
99
+ # @param connection_name what
100
+ # @param args any additional arguments
101
+ # @param options (see #get_object)
102
+ #
103
+ # @return [Koala::Facebook::API::GraphCollection] an array of object hashes (in most cases)
104
+ def get_connections(id, connection_name, args = {}, options = {})
105
+ # Fetchs the connections for given object.
106
+ graph_call("#{id}/#{connection_name}", args, "get", options)
107
+ end
108
+
109
+
110
+ # Write an object to the Graph for a specific user.
111
+ # See {http://developers.facebook.com/docs/api#publishing Facebook's documentation}
112
+ # for all the supported writeable objects.
113
+ #
114
+ # @note (see #get_connections)
115
+ #
116
+ # @example
117
+ # graph.put_object("me", "feed", :message => "Hello, world")
118
+ # => writes "Hello, world" to the active user's wall
119
+ #
120
+ # Most write operations require extended permissions. For example,
121
+ # publishing wall posts requires the "publish_stream" permission. See
122
+ # http://developers.facebook.com/docs/authentication/ for details about
123
+ # extended permissions.
124
+ #
125
+ # @param id (see #get_object)
126
+ # @param connection_name (see #get_connections)
127
+ # @param args (see #get_connections)
128
+ # @param options (see #get_object)
129
+ #
130
+ # @return a hash containing the new object's id
131
+ def put_connections(id, connection_name, args = {}, options = {})
132
+ # Posts a certain connection
133
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
134
+ graph_call("#{id}/#{connection_name}", args, "post", options)
135
+ end
136
+
137
+ # Delete an object's connection (for instance, unliking the object).
138
+ #
139
+ # @note (see #get_connections)
140
+ #
141
+ # @param id (see #get_object)
142
+ # @param connection_name (see #get_connections)
143
+ # @args (see #get_connections)
144
+ # @param options (see #get_object)
145
+ #
146
+ # @return (see #delete_object)
147
+ def delete_connections(id, connection_name, args = {}, options = {})
148
+ # Deletes a given connection
149
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Delete requires an access token"}) unless @access_token
150
+ graph_call("#{id}/#{connection_name}", args, "delete", options)
151
+ end
152
+
153
+ # Fetches a photo.
154
+ # (Facebook returns the src of the photo as a response header; this method parses that properly,
155
+ # unlike using get_connections("photo").)
156
+ #
157
+ # @note to delete photos or videos, use delete_object(id)
158
+ #
159
+ # @return the URL to the image
160
+ def get_picture(object, args = {}, options = {})
161
+ # Gets a picture object, returning the URL (which Facebook sends as a header)
162
+ graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
163
+ result["Location"]
164
+ end
165
+ end
166
+
167
+ # Upload a photo.
168
+ #
169
+ # This can be called in multiple ways:
170
+ # put_picture(file, [content_type], ...)
171
+ # put_picture(path_to_file, [content_type], ...)
172
+ # put_picture(picture_url, ...)
173
+ #
174
+ # You can also pass in uploaded files directly from Rails or Sinatra.
175
+ # See {https://github.com/arsduo/koala/wiki/Uploading-Photos-and-Videos the Koala wiki} for more information.
176
+ #
177
+ # @param args (see #get_object)
178
+ # @param target_id the Facebook object to which to post the picture (default: "me")
179
+ # @param options (see #get_object)
180
+ #
181
+ # @example
182
+ # put_picture(file, content_type, {:message => "Message"}, 01234560)
183
+ # put_picture(params[:file], {:message => "Message"})
184
+ # # with URLs, there's no optional content type field
185
+ # put_picture(picture_url, {:message => "Message"}, my_page_id)
186
+ #
187
+ # @note to access the media after upload, you'll need the user_photos or user_videos permission as appropriate.
188
+ #
189
+ # @return (see #put_connections)
190
+ def put_picture(*picture_args)
191
+ put_object(*parse_media_args(picture_args, "photos"))
192
+ end
193
+
194
+ # Upload a video. Functions exactly the same as put_picture.
195
+ # @see #put_picture
196
+ def put_video(*video_args)
197
+ args = parse_media_args(video_args, "videos")
198
+ args.last[:video] = true
199
+ put_object(*args)
200
+ end
201
+
202
+ # Write directly to the user's wall.
203
+ # Convenience method equivalent to put_object(id, "feed").
204
+ #
205
+ # To get wall posts, use get_connections(user, "feed")
206
+ # To delete a wall post, use delete_object(post_id)
207
+ #
208
+ # @param message the message to write for the wall
209
+ # @param attachment a hash describing the wall post
210
+ # (see the {https://developers.facebook.com/docs/guides/attachments/ stream attachments} documentation.)
211
+ # @param target_id the target wall
212
+ # @param options (see #get_object)
213
+ #
214
+ # @example
215
+ # @api.put_wall_post("Hello there!", {
216
+ # "name" => "Link name"
217
+ # "link" => "http://www.example.com/",
218
+ # "caption" => "{*actor*} posted a new review",
219
+ # "description" => "This is a longer description of the attachment",
220
+ # "picture" => "http://www.example.com/thumbnail.jpg"
221
+ # })
222
+ #
223
+ # @see #put_connections
224
+ # @return (see #put_connections)
225
+ def put_wall_post(message, attachment = {}, target_id = "me", options = {})
226
+ self.put_object(target_id, "feed", attachment.merge({:message => message}), options)
227
+ end
228
+
229
+ # Comment on a given object.
230
+ # Convenience method equivalent to put_connection(id, "comments").
231
+ #
232
+ # To delete comments, use delete_object(comment_id).
233
+ # To get comments, use get_connections(object, "likes").
234
+ #
235
+ # @param id (see #get_object)
236
+ # @param message the comment to write
237
+ # @param options (see #get_object)
238
+ #
239
+ # @return (see #put_connections)
240
+ def put_comment(id, message, options = {})
241
+ # Writes the given comment on the given post.
242
+ self.put_object(id, "comments", {:message => message}, options)
243
+ end
244
+
245
+ # Like a given object.
246
+ # Convenience method equivalent to put_connections(id, "likes").
247
+ #
248
+ # To get a list of a user's or object's likes, use get_connections(id, "likes").
249
+ #
250
+ # @param id (see #get_object)
251
+ # @param options (see #get_object)
252
+ #
253
+ # @return (see #put_connections)
254
+ def put_like(id, options = {})
255
+ # Likes the given post.
256
+ self.put_object(id, "likes", {}, options)
257
+ end
258
+
259
+ # Unlike a given object.
260
+ # Convenience method equivalent to delete_connection(id, "likes").
261
+ #
262
+ # @param id (see #get_object)
263
+ # @param options (see #get_object)
264
+ #
265
+ # @return (see #delete_object)
266
+ def delete_like(id, options = {})
267
+ # Unlikes a given object for the logged-in user
268
+ raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Unliking requires an access token"}) unless @access_token
269
+ graph_call("#{id}/likes", {}, "delete", options)
270
+ end
271
+
272
+ # Search for a given query among visible Facebook objects.
273
+ # See {http://developers.facebook.com/docs/reference/api/#searching Facebook documentation} for more information.
274
+ #
275
+ # @param search_terms the query to search for
276
+ # @param args additional arguments, such as type, fields, etc.
277
+ # @param options (see #get_object)
278
+ #
279
+ # @return [Koala::Facebook::API::GraphCollection] an array of search results
280
+ def search(search_terms, args = {}, options = {})
281
+ args.merge!({:q => search_terms}) unless search_terms.nil?
282
+ graph_call("search", args, "get", options)
283
+ end
284
+
285
+ # Convenience Methods
286
+ # In general, we're trying to avoid adding convenience methods to Koala
287
+ # except to support cases where the Facebook API requires non-standard input
288
+ # such as JSON-encoding arguments, posts directly to objects, etc.
289
+
290
+ # Make an FQL query.
291
+ # Convenience method equivalent to get_object("fql", :q => query).
292
+ #
293
+ # @param query the FQL query to perform
294
+ # @param args (see #get_object)
295
+ # @param options (see #get_object)
296
+ def fql_query(query, args = {}, options = {})
297
+ get_object("fql", args.merge(:q => query), options)
298
+ end
299
+
300
+ # Make an FQL multiquery.
301
+ # This method simplifies the result returned from multiquery into a more logical format.
302
+ #
303
+ # @param queries a hash of query names => FQL queries
304
+ # @param args (see #get_object)
305
+ # @param options (see #get_object)
306
+ #
307
+ # @example
308
+ # @api.fql_multiquery({
309
+ # "query1" => "select post_id from stream where source_id = me()",
310
+ # "query2" => "select fromid from comment where post_id in (select post_id from #query1)"
311
+ # })
312
+ # # returns {"query1" => [obj1, obj2, ...], "query2" => [obj3, ...]}
313
+ # # instead of [{"name":"query1", "fql_result_set":[]},{"name":"query2", "fql_result_set":[]}]
314
+ #
315
+ # @return a hash of FQL results keyed to the appropriate query
316
+ def fql_multiquery(queries = {}, args = {}, options = {})
317
+ if results = get_object("fql", args.merge(:q => MultiJson.encode(queries)), options)
318
+ # simplify the multiquery result format
319
+ results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
320
+ end
321
+ end
322
+
323
+ # Get a page's access token, allowing you to act as the page.
324
+ # Convenience method for @api.get_object(page_id, :fields => "access_token").
325
+ #
326
+ # @param id the page ID
327
+ # @param args (see #get_object)
328
+ # @param options (see #get_object)
329
+ #
330
+ # @return the page's access token (discarding expiration and any other information)
331
+ def get_page_access_token(id, args = {}, options = {})
332
+ result = get_object(id, args.merge(:fields => "access_token"), options) do
333
+ result ? result["access_token"] : nil
334
+ end
335
+ end
336
+
337
+ # Fetchs the comments from fb:comments widgets for a given set of URLs (array or comma-separated string).
338
+ # See https://developers.facebook.com/blog/post/490.
339
+ #
340
+ # @param urls the URLs for which you want comments
341
+ # @param args (see #get_object)
342
+ # @param options (see #get_object)
343
+ #
344
+ # @returns a hash of urls => comment arrays
345
+ def get_comments_for_urls(urls = [], args = {}, options = {})
346
+ return [] if urls.empty?
347
+ args.merge!(:ids => urls.respond_to?(:join) ? urls.join(",") : urls)
348
+ get_object("comments", args, options)
349
+ end
350
+
351
+ def set_app_restrictions(app_id, restrictions_hash, args = {}, options = {})
352
+ graph_call(app_id, args.merge(:restrictions => MultiJson.encode(restrictions_hash)), "post", options)
353
+ end
354
+
355
+ # Certain calls such as {#get_connections} return an array of results which you can page through
356
+ # forwards and backwards (to see more feed stories, search results, etc.).
357
+ # Those methods use get_page to request another set of results from Facebook.
358
+ #
359
+ # @note You'll rarely need to use this method unless you're using Sinatra or another non-Rails framework
360
+ # (see {Koala::Facebook::GraphCollection GraphCollection} for more information).
361
+ #
362
+ # @param params an array of arguments to graph_call
363
+ # as returned by {Koala::Facebook::GraphCollection.parse_page_url}.
364
+ #
365
+ # @return Koala::Facebook::GraphCollection the appropriate page of results (an empty array if there are none)
366
+ def get_page(params)
367
+ graph_call(*params)
368
+ end
369
+
370
+ # Execute a set of Graph API calls as a batch.
371
+ # See {https://github.com/arsduo/koala/wiki/Batch-requests batch request documentation}
372
+ # for more information and examples.
373
+ #
374
+ # @param http_options HTTP options for the entire request.
375
+ #
376
+ # @yield batch_api [Koala::Facebook::GraphBatchAPI] an API subclass
377
+ # whose requests will be queued and executed together at the end of the block
378
+ #
379
+ # @raise [Koala::Facebook::APIError] only if there is a problem with the overall batch request
380
+ # (e.g. connectivity failure, an operation with a missing dependency).
381
+ # Individual calls that error out will be represented as an unraised
382
+ # APIError in the appropriate spot in the results array.
383
+ #
384
+ # @example
385
+ # results = @api.batch do |batch_api|
386
+ # batch_api.get_object('me')
387
+ # batch_api.get_object(KoalaTest.user1)
388
+ # end
389
+ # # => [{"id" => my_id, ...}, {"id"" => koppel_id, ...}]
390
+ #
391
+ # @return an array of results from your batch calls (as if you'd made them individually),
392
+ # arranged in the same order they're made.
393
+ def batch(http_options = {}, &block)
394
+ batch_client = GraphBatchAPI.new(access_token, self)
395
+ if block
396
+ yield batch_client
397
+ batch_client.execute(http_options)
398
+ else
399
+ batch_client
400
+ end
401
+ end
402
+
403
+ # Make a call directly to the Graph API.
404
+ # (See any of the other methods for example invocations.)
405
+ #
406
+ # @param path the Graph API path to query (no leading / needed)
407
+ # @param args (see #get_object)
408
+ # @param verb the type of HTTP request to make (get, post, delete, etc.)
409
+ # @options (see #get_object)
410
+ #
411
+ # @yield response when making a batch API call, you can pass in a block
412
+ # that parses the results, allowing for cleaner code.
413
+ # The block's return value is returned in the batch results.
414
+ # See the code for {#get_picture} or {#fql_multiquery} for examples.
415
+ # (Not needed in regular calls; you'll probably rarely use this.)
416
+ #
417
+ # @raise [Koala::Facebook::APIError] if Facebook returns an error
418
+ #
419
+ # @return the result from Facebook
420
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
421
+ result = api(path, args, verb, options) do |response|
422
+ error = check_response(response)
423
+ raise error if error
424
+ end
425
+
426
+ # turn this into a GraphCollection if it's pageable
427
+ result = GraphCollection.evaluate(result, self)
428
+
429
+ # now process as appropriate for the given call (get picture header, etc.)
430
+ post_processing ? post_processing.call(result) : result
431
+ end
432
+
433
+ private
434
+
435
+ def check_response(response)
436
+ # check for Graph API-specific errors
437
+ # this returns an error, which is immediately raised (non-batch)
438
+ # or added to the list of batch results (batch)
439
+ if response.is_a?(Hash) && error_details = response["error"]
440
+ APIError.new(error_details)
441
+ end
442
+ end
443
+
444
+ def parse_media_args(media_args, method)
445
+ # photo and video uploads can accept different types of arguments (see above)
446
+ # so here, we parse the arguments into a form directly usable in put_object
447
+ raise KoalaError.new("Wrong number of arguments for put_#{method == "photos" ? "picture" : "video"}") unless media_args.size.between?(1, 5)
448
+
449
+ args_offset = media_args[1].kind_of?(Hash) || media_args.size == 1 ? 0 : 1
450
+
451
+ args = media_args[1 + args_offset] || {}
452
+ target_id = media_args[2 + args_offset] || "me"
453
+ options = media_args[3 + args_offset] || {}
454
+
455
+ if url?(media_args.first)
456
+ # If media_args is a URL, we can upload without UploadableIO
457
+ args.merge!(:url => media_args.first)
458
+ else
459
+ args["source"] = Koala::UploadableIO.new(*media_args.slice(0, 1 + args_offset))
460
+ end
461
+
462
+ [target_id, method, args, options]
463
+ end
464
+
465
+ def url?(data)
466
+ return false unless data.is_a? String
467
+ begin
468
+ uri = URI.parse(data)
469
+ %w( http https ).include?(uri.scheme)
470
+ rescue URI::BadURIError
471
+ false
472
+ end
473
+ end
474
+ end
475
+ end
476
+ end
@@ -1,22 +1,17 @@
1
+ require 'koala/api'
2
+ require 'koala/api/batch_operation'
3
+
1
4
  module Koala
2
5
  module Facebook
3
- module GraphBatchAPIMethods
4
-
5
- def self.included(base)
6
- base.class_eval do
7
- attr_reader :original_api
8
-
9
- def initialize(access_token, api)
10
- super(access_token)
11
- @original_api = api
12
- end
13
-
14
- alias_method :graph_call_outside_batch, :graph_call
15
- alias_method :graph_call, :graph_call_in_batch
6
+ # @private
7
+ class GraphBatchAPI < API
8
+ # inside a batch call we can do anything a regular Graph API can do
9
+ include GraphAPIMethods
16
10
 
17
- alias_method :check_graph_api_response, :check_response
18
- alias_method :check_response, :check_graph_batch_api_response
19
- end
11
+ attr_reader :original_api
12
+ def initialize(access_token, api)
13
+ super(access_token)
14
+ @original_api = api
20
15
  end
21
16
 
22
17
  def batch_calls
@@ -38,12 +33,22 @@ module Koala
38
33
 
39
34
  def check_graph_batch_api_response(response)
40
35
  if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
41
- APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"])
36
+ # old error format -- see http://developers.facebook.com/blog/post/596/
37
+ APIError.new({"type" => "Error #{response["error"]}", "message" => response["error_description"]}.merge(response))
42
38
  else
43
39
  check_graph_api_response(response)
44
40
  end
45
41
  end
46
42
 
43
+ # redefine the graph_call and check_response methods
44
+ # so we can use this API inside the batch block just like any regular Graph API
45
+ alias_method :graph_call_outside_batch, :graph_call
46
+ alias_method :graph_call, :graph_call_in_batch
47
+
48
+ alias_method :check_graph_api_response, :check_response
49
+ alias_method :check_response, :check_graph_batch_api_response
50
+
51
+ # execute the queued batch calls
47
52
  def execute(http_options = {})
48
53
  return [] unless batch_calls.length > 0
49
54
  # Turn the call args collected into what facebook expects
@@ -0,0 +1,107 @@
1
+ module Koala
2
+ module Facebook
3
+ class API
4
+ # A light wrapper for collections returned from the Graph API.
5
+ # It extends Array to allow you to page backward and forward through
6
+ # result sets, and providing easy access to paging information.
7
+ class GraphCollection < Array
8
+
9
+ # The raw paging information from Facebook (next/previous URLs).
10
+ attr_reader :paging
11
+ # @return [Koala::Facebook::GraphAPI] the api used to make requests.
12
+ attr_reader :api
13
+ # The entire raw response from Facebook.
14
+ attr_reader :raw_response
15
+
16
+ # Initialize the array of results and store various additional paging-related information.
17
+ #
18
+ # @param response the response from Facebook (a hash whose "data" key is an array)
19
+ # @param api the Graph {Koala::Facebook::API API} instance to use to make calls
20
+ # (usually the API that made the original call).
21
+ #
22
+ # @return [Koala::Facebook::GraphCollection] an initialized GraphCollection
23
+ # whose paging, raw_response, and api attributes are populated.
24
+ def initialize(response, api)
25
+ super response["data"]
26
+ @paging = response["paging"]
27
+ @raw_response = response
28
+ @api = api
29
+ end
30
+
31
+ # @private
32
+ # Turn the response into a GraphCollection if they're pageable;
33
+ # if not, return the original response.
34
+ # The Ads API (uniquely so far) returns a hash rather than an array when queried
35
+ # with get_connections.
36
+ def self.evaluate(response, api)
37
+ response.is_a?(Hash) && response["data"].is_a?(Array) ? self.new(response, api) : response
38
+ end
39
+
40
+ # Retrieve the next page of results.
41
+ #
42
+ # @return a GraphCollection array of additional results (an empty array if there are no more results)
43
+ def next_page
44
+ base, args = next_page_params
45
+ base ? @api.get_page([base, args]) : nil
46
+ end
47
+
48
+ # Retrieve the previous page of results.
49
+ #
50
+ # @return a GraphCollection array of additional results (an empty array if there are no earlier results)
51
+ def previous_page
52
+ base, args = previous_page_params
53
+ base ? @api.get_page([base, args]) : nil
54
+ end
55
+
56
+ # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the next page of results.
57
+ #
58
+ # @example
59
+ # @api.graph_call(*collection.next_page_params)
60
+ #
61
+ # @return an array of arguments, or nil if there are no more pages
62
+ def next_page_params
63
+ @paging && @paging["next"] ? parse_page_url(@paging["next"]) : nil
64
+ end
65
+
66
+ # Arguments that can be sent to {Koala::Facebook::API#graph_call} to retrieve the previous page of results.
67
+ #
68
+ # @example
69
+ # @api.graph_call(*collection.previous_page_params)
70
+ #
71
+ # @return an array of arguments, or nil if there are no previous pages
72
+ def previous_page_params
73
+ @paging && @paging["previous"] ? parse_page_url(@paging["previous"]) : nil
74
+ end
75
+
76
+ # @private
77
+ def parse_page_url(url)
78
+ GraphCollection.parse_page_url(url)
79
+ end
80
+
81
+ # Parse the previous and next page URLs Facebook provides in pageable results.
82
+ # You'll mainly need to use this when using a non-Rails framework (one without url_for);
83
+ # to store paging information between page loads, pass the URL (from GraphCollection#paging)
84
+ # and use parse_page_url to turn it into parameters useful for {Koala::Facebook::API#get_page}.
85
+ #
86
+ # @param url the paging URL to turn into graph_call parameters
87
+ #
88
+ # @return an array of parameters that can be provided via graph_call(*parsed_params)
89
+ def self.parse_page_url(url)
90
+ match = url.match(/.com\/(.*)\?(.*)/)
91
+ base = match[1]
92
+ args = match[2]
93
+ params = CGI.parse(args)
94
+ new_params = {}
95
+ params.each_pair do |key,value|
96
+ new_params[key] = value.join ","
97
+ end
98
+ [base,new_params]
99
+ end
100
+ end
101
+ end
102
+
103
+ # @private
104
+ # legacy support for when GraphCollection lived directly under Koala::Facebook
105
+ GraphCollection = API::GraphCollection
106
+ end
107
+ end
@@ -0,0 +1,26 @@
1
+ require 'koala/api'
2
+ module Koala
3
+ module Facebook
4
+ # Legacy support for old pre-1.2 APIs
5
+
6
+ # A wrapper for the old APIs deprecated in 1.2.0, which triggers a deprecation warning when used.
7
+ # Otherwise, this class functions identically to API.
8
+ # @see API
9
+ # @private
10
+ class OldAPI < API
11
+ def initialize(*args)
12
+ Koala::Utils.deprecate("#{self.class.name} is deprecated and will be removed in a future version; please use the API class instead.")
13
+ super
14
+ end
15
+ end
16
+
17
+ # @private
18
+ class GraphAPI < OldAPI; end
19
+
20
+ # @private
21
+ class RestAPI < OldAPI; end
22
+
23
+ # @private
24
+ class GraphAndRestAPI < OldAPI; end
25
+ end
26
+ end