koala 1.2.1 → 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 (55) 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 +28 -0
  6. data/Gemfile +14 -0
  7. data/Guardfile +6 -0
  8. data/koala.gemspec +3 -3
  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} +34 -13
  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 +69 -20
  20. data/lib/koala/oauth.rb +170 -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 -96
  26. data/readme.md +9 -9
  27. data/spec/cases/api_spec.rb +19 -12
  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 +5 -26
  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 +7 -7
  35. data/spec/cases/oauth_spec.rb +134 -48
  36. data/spec/cases/realtime_updates_spec.rb +154 -47
  37. data/spec/cases/test_users_spec.rb +276 -219
  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 +41 -30
  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 +192 -14
  44. data/spec/support/koala_test.rb +10 -1
  45. data/spec/support/mock_http_service.rb +2 -2
  46. data/spec/support/rest_api_shared_examples.rb +5 -165
  47. metadata +75 -99
  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/multipart_request.rb +0 -35
  52. data/lib/koala/uploadable_io.rb +0 -181
  53. data/spec/cases/graph_and_rest_api_spec.rb +0 -22
  54. data/spec/cases/graph_api_spec.rb +0 -22
  55. data/spec/cases/rest_api_spec.rb +0 -22
data/.gitignore CHANGED
@@ -2,4 +2,6 @@ pkg
2
2
  .project
3
3
  Gemfile.lock
4
4
  .rvmrc
5
- *.rbc
5
+ *.rbc
6
+ *~
7
+ .yardoc/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --order rand
data/.travis.yml CHANGED
@@ -7,3 +7,7 @@ rvm:
7
7
  - rbx-2.0
8
8
  - ree
9
9
  - jruby
10
+
11
+ branches:
12
+ except:
13
+ - rdoc
data/.yardopts ADDED
@@ -0,0 +1,3 @@
1
+ --readme readme.md
2
+ --title "Koala Documentation"
3
+ --no-private
data/CHANGELOG CHANGED
@@ -1,3 +1,31 @@
1
+ v1.3
2
+ New methods:
3
+ -- OAuth#url_for_dialog creates URLs for Facebook dialog pages
4
+ -- API#set_app_restrictions handles JSON-encoding app restrictions
5
+ -- GraphCollection.parse_page_url now exposes useful functionality for non-Rails apps
6
+ -- RealtimeUpdates#subscription_path and TestUsers#test_user_accounts_path are now public
7
+ Updated methods:
8
+ -- REST API methods are now deprecated (see http://developers.facebook.com/blog/post/616/)
9
+ -- OAuth#url_for_access_token and #url_for_oauth_code now include any provided options as URL parameters
10
+ -- APIError#raw_response allows access to the raw error response received from Facebook
11
+ -- Utils.deprecate only prints each message once (no more spamming)
12
+ -- API#get_page_access_token now accepts additional arguments and HTTP options (like other calls)
13
+ -- TestUsers and RealtimeUpdates methods now take http_options arguments
14
+ -- All methods with http_options can now take :http_component => :response for the complete response
15
+ -- OAuth#get_user_info_from_cookies returns nil rather than an error if the cookies are expired (thanks, herzio)
16
+ -- TestUsers#delete_all now uses the Batch API and is much faster
17
+ Internal improvements:
18
+ -- FQL queries now use the Graph API behind-the-scenes
19
+ -- Cleaned up file and class organization, with aliases for backward compatibility
20
+ -- Added YARD documentation throughout
21
+ -- Fixed bugs in RealtimeUpdates, TestUsers, elsewhere
22
+ -- Reorganized file and class structure non-destructively
23
+ Testing improvements:
24
+ -- Expanded/improved test coverage
25
+ -- The test suite no longer users any hard-coded user IDs
26
+ -- KoalaTest.test_user_api allows access to the TestUsers instance
27
+ -- Configured tests to run in random order using RSpec 2.8.0rc1
28
+
1
29
  v1.2.1
2
30
  New methods:
3
31
  -- RestAPI.set_app_properties handles JSON-encoding application properties
data/Gemfile CHANGED
@@ -1,7 +1,21 @@
1
1
  source :rubygems
2
2
 
3
+ group :development do
4
+ gem "yard"
5
+ end
6
+
3
7
  group :development, :test do
4
8
  gem "typhoeus"
9
+
10
+ # Testing infrastructure
11
+ gem 'guard'
12
+ gem 'guard-rspec'
13
+
14
+ if RUBY_PLATFORM =~ /darwin/
15
+ # OS X integration
16
+ gem "ruby_gntp"
17
+ gem "rb-fsevent", "~> 0.4.3.1"
18
+ end
5
19
  end
6
20
 
7
21
  if defined? JRUBY_VERSION
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard 'rspec', :version => 2 do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
data/koala.gemspec CHANGED
@@ -32,17 +32,17 @@ Gem::Specification.new do |s|
32
32
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
33
33
  s.add_runtime_dependency(%q<multi_json>, ["~> 1.0"])
34
34
  s.add_runtime_dependency(%q<faraday>, ["~> 0.7.0"])
35
- s.add_development_dependency(%q<rspec>, ["~> 2.5"])
35
+ s.add_development_dependency(%q<rspec>, ["~> 2.8.0rc1"])
36
36
  s.add_development_dependency(%q<rake>, ["~> 0.8.7"])
37
37
  else
38
38
  s.add_dependency(%q<multi_json>, ["~> 1.0"])
39
- s.add_dependency(%q<rspec>, ["~> 2.5"])
39
+ s.add_dependency(%q<rspec>, ["~> 2.8.0rc1"])
40
40
  s.add_dependency(%q<rake>, ["~> 0.8.7"])
41
41
  s.add_dependency(%q<faraday>, ["~> 0.7.0"])
42
42
  end
43
43
  else
44
44
  s.add_dependency(%q<multi_json>, ["~> 1.0"])
45
- s.add_dependency(%q<rspec>, ["~> 2.5"])
45
+ s.add_dependency(%q<rspec>, ["~> 2.8.0rc1"])
46
46
  s.add_dependency(%q<rake>, ["~> 0.8.7"])
47
47
  s.add_dependency(%q<faraday>, ["~> 0.7.0"])
48
48
  end
@@ -0,0 +1,83 @@
1
+ require 'koala/api'
2
+
3
+ module Koala
4
+ module Facebook
5
+ class GraphBatchAPI < API
6
+ # @private
7
+ class BatchOperation
8
+ attr_reader :access_token, :http_options, :post_processing, :files, :batch_api, :identifier
9
+
10
+ @identifier = 0
11
+
12
+ def self.next_identifier
13
+ @identifier += 1
14
+ end
15
+
16
+ def initialize(options = {})
17
+ @identifier = self.class.next_identifier
18
+ @args = (options[:args] || {}).dup # because we modify it below
19
+ @access_token = options[:access_token]
20
+ @http_options = (options[:http_options] || {}).dup # dup because we modify it below
21
+ @batch_args = @http_options.delete(:batch_args) || {}
22
+ @url = options[:url]
23
+ @method = options[:method].to_sym
24
+ @post_processing = options[:post_processing]
25
+
26
+ process_binary_args
27
+
28
+ raise Koala::KoalaError, "Batch operations require an access token, none provided." unless @access_token
29
+ end
30
+
31
+ def to_batch_params(main_access_token)
32
+ # set up the arguments
33
+ args_string = Koala.http_service.encode_params(@access_token == main_access_token ? @args : @args.merge(:access_token => @access_token))
34
+
35
+ response = {
36
+ :method => @method.to_s,
37
+ :relative_url => @url,
38
+ }
39
+
40
+ # handle batch-level arguments, such as name, depends_on, and attached_files
41
+ @batch_args[:attached_files] = @files.keys.join(",") if @files
42
+ response.merge!(@batch_args) if @batch_args
43
+
44
+ # for get and delete, we append args to the URL string
45
+ # otherwise, they go in the body
46
+ if args_string.length > 0
47
+ if args_in_url?
48
+ response[:relative_url] += (@url =~ /\?/ ? "&" : "?") + args_string if args_string.length > 0
49
+ else
50
+ response[:body] = args_string if args_string.length > 0
51
+ end
52
+ end
53
+
54
+ response
55
+ end
56
+
57
+ protected
58
+
59
+ def process_binary_args
60
+ # collect binary files
61
+ @args.each_pair do |key, value|
62
+ if UploadableIO.binary_content?(value)
63
+ @files ||= {}
64
+ # we use a class-level counter to ensure unique file identifiers across multiple batch operations
65
+ # (this is thread safe, since we just care about uniqueness)
66
+ # so remove the file from the original hash and add it to the file store
67
+ id = "op#{identifier}_file#{@files.keys.length}"
68
+ @files[id] = @args.delete(key).is_a?(UploadableIO) ? value : UploadableIO.new(value)
69
+ end
70
+ end
71
+ end
72
+
73
+ def args_in_url?
74
+ @method == :get || @method == :delete
75
+ end
76
+ end
77
+ end
78
+
79
+ # @private
80
+ # legacy support for when BatchOperation lived directly under Koala::Facebook
81
+ BatchOperation = GraphBatchAPI::BatchOperation
82
+ end
83
+ end
@@ -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