koala 1.3.0rc1 → 1.3.0rc2

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 (45) hide show
  1. data/.gitignore +3 -1
  2. data/.travis.yml +4 -0
  3. data/.yardopts +3 -0
  4. data/CHANGELOG +7 -0
  5. data/Gemfile +14 -0
  6. data/Guardfile +6 -0
  7. data/lib/koala.rb +16 -97
  8. data/lib/koala/api.rb +93 -0
  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} +20 -16
  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} +33 -3
  15. data/lib/koala/http_service.rb +69 -19
  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/oauth.rb +117 -14
  20. data/lib/koala/realtime_updates.rb +89 -51
  21. data/lib/koala/test_users.rb +109 -33
  22. data/lib/koala/utils.rb +4 -0
  23. data/lib/koala/version.rb +1 -1
  24. data/spec/cases/api_spec.rb +19 -12
  25. data/spec/cases/graph_api_batch_spec.rb +41 -41
  26. data/spec/cases/http_service_spec.rb +1 -22
  27. data/spec/cases/legacy_spec.rb +107 -0
  28. data/spec/cases/multipart_request_spec.rb +5 -5
  29. data/spec/cases/oauth_spec.rb +9 -9
  30. data/spec/cases/realtime_updates_spec.rb +154 -47
  31. data/spec/cases/test_users_spec.rb +268 -219
  32. data/spec/fixtures/mock_facebook_responses.yml +10 -6
  33. data/spec/support/graph_api_shared_examples.rb +17 -12
  34. data/spec/support/koala_test.rb +1 -1
  35. data/spec/support/mock_http_service.rb +2 -2
  36. data/spec/support/rest_api_shared_examples.rb +1 -1
  37. metadata +82 -104
  38. data/lib/koala/batch_operation.rb +0 -74
  39. data/lib/koala/graph_api.rb +0 -289
  40. data/lib/koala/graph_collection.rb +0 -63
  41. data/lib/koala/multipart_request.rb +0 -35
  42. data/lib/koala/uploadable_io.rb +0 -181
  43. data/spec/cases/graph_and_rest_api_spec.rb +0 -22
  44. data/spec/cases/graph_api_spec.rb +0 -22
  45. data/spec/cases/rest_api_spec.rb +0 -22
@@ -1,289 +0,0 @@
1
- module Koala
2
- module Facebook
3
- GRAPH_SERVER = "graph.facebook.com"
4
-
5
- module GraphAPIMethods
6
- # A client for the Facebook Graph API.
7
- #
8
- # See http://github.com/arsduo/koala for Ruby/Koala documentation
9
- # and http://developers.facebook.com/docs/api for Facebook API documentation
10
- #
11
- # The Graph API is made up of the objects in Facebook (e.g., people, pages,
12
- # events, photos) and the connections between them (e.g., friends,
13
- # photo tags, and event RSVPs). This client provides access to those
14
- # primitive types in a generic way. For example, given an OAuth access
15
- # token, this will fetch the profile of the active user and the list
16
- # of the user's friends:
17
- #
18
- # graph = Koala::Facebook::API.new(access_token)
19
- # user = graph.get_object("me")
20
- # friends = graph.get_connections(user["id"], "friends")
21
- #
22
- # You can see a list of all of the objects and connections supported
23
- # by the API at http://developers.facebook.com/docs/reference/api/.
24
- #
25
- # You can obtain an access token via OAuth or by using the Facebook
26
- # JavaScript SDK. See the Koala and Facebook documentation for more information.
27
- #
28
- # If you are using the JavaScript SDK, you can use the
29
- # Koala::Facebook::OAuth.get_user_from_cookie() method below to get the OAuth access token
30
- # for the active user from the cookie saved by the SDK.
31
-
32
- # Objects
33
-
34
- def get_object(id, args = {}, options = {})
35
- # Fetchs the given object from the graph.
36
- graph_call(id, args, "get", options)
37
- end
38
-
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)
44
- end
45
-
46
- def put_object(parent_object, connection_name, args = {}, options = {})
47
- # Writes the given object to the graph, connected to the given parent.
48
- # See http://developers.facebook.com/docs/api#publishing for all of
49
- # the supported writeable objects.
50
- #
51
- # For example,
52
- # graph.put_object("me", "feed", :message => "Hello, world")
53
- # writes "Hello, world" to the active user's wall.
54
- #
55
- # Most write operations require extended permissions. For example,
56
- # publishing wall posts requires the "publish_stream" permission. See
57
- # http://developers.facebook.com/docs/authentication/ for details about
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
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)
75
- end
76
-
77
- def put_connections(id, connection_name, args = {}, options = {})
78
- # Posts a certain connection
79
- raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
80
- graph_call("#{id}/#{connection_name}", args, "post", options)
81
- end
82
-
83
- def delete_connections(id, connection_name, args = {}, options = {})
84
- # Deletes a given connection
85
- raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Delete requires an access token"}) unless @access_token
86
- graph_call("#{id}/#{connection_name}", args, "delete", options)
87
- end
88
-
89
- # Media (photos and videos)
90
- # to delete photos or videos, use delete_object(object_id)
91
- # note: you'll need the user_photos or user_videos permissions to actually access media after upload
92
-
93
- def get_picture(object, args = {}, options = {})
94
- # Gets a picture object, returning the URL (which Facebook sends as a header)
95
- graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
96
- result["Location"]
97
- end
98
- end
99
-
100
- # Can be called in multiple ways:
101
- #
102
- # put_picture(file, [content_type], ...)
103
- # put_picture(path_to_file, [content_type], ...)
104
- # put_picture(picture_url, ...)
105
- #
106
- # You can pass in uploaded files directly from Rails or Sinatra.
107
- # (See lib/koala/uploadable_io.rb for supported frameworks)
108
- #
109
- # Optional parameters can be added to the end of the argument list:
110
- # - args: a hash of request parameters (default: {})
111
- # - target_id: ID of the target where to post the picture (default: "me")
112
- # - options: a hash of http options passed to the HTTPService module
113
- #
114
- # put_picture(file, content_type, {:message => "Message"}, 01234560)
115
- # put_picture(params[:file], {:message => "Message"})
116
- #
117
- # (Note that with URLs, there's no optional content type field)
118
- # put_picture(picture_url, {:message => "Message"}, my_page_id)
119
-
120
- def put_picture(*picture_args)
121
- put_object(*parse_media_args(picture_args, "photos"))
122
- end
123
-
124
- def put_video(*video_args)
125
- args = parse_media_args(video_args, "videos")
126
- args.last[:video] = true
127
- put_object(*args)
128
- end
129
-
130
- # Wall posts
131
- # To get wall posts, use get_connections(user, "feed")
132
- # To delete a wall post, just use delete_object(post_id)
133
-
134
- def put_wall_post(message, attachment = {}, profile_id = "me", options = {})
135
- # attachment is a hash describing the wall post
136
- # (see X for more details)
137
- # For instance,
138
- #
139
- # {"name" => "Link name"
140
- # "link" => "http://www.example.com/",
141
- # "caption" => "{*actor*} posted a new review",
142
- # "description" => "This is a longer description of the attachment",
143
- # "picture" => "http://www.example.com/thumbnail.jpg"}
144
-
145
- self.put_object(profile_id, "feed", attachment.merge({:message => message}), options)
146
- end
147
-
148
- # Comments
149
- # to delete comments, use delete_object(comment_id)
150
- # to get comments, use get_connections(object, "likes")
151
-
152
- def put_comment(object_id, message, options = {})
153
- # Writes the given comment on the given post.
154
- self.put_object(object_id, "comments", {:message => message}, options)
155
- end
156
-
157
- # Likes
158
- # to get likes, use get_connections(user, "likes")
159
-
160
- def put_like(object_id, options = {})
161
- # Likes the given post.
162
- self.put_object(object_id, "likes", {}, options)
163
- end
164
-
165
- def delete_like(object_id, options = {})
166
- # Unlikes a given object for the logged-in user
167
- raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Unliking requires an access token"}) unless @access_token
168
- graph_call("#{object_id}/likes", {}, "delete", options)
169
- end
170
-
171
- # Search
172
-
173
- def search(search_terms, args = {}, options = {})
174
- args.merge!({:q => search_terms}) unless search_terms.nil?
175
- graph_call("search", args, "get", options)
176
- end
177
-
178
- # Convenience Methods
179
- #
180
- # in general, we're trying to avoid adding convenience methods to Koala
181
- # except to support cases where the Facebook API requires non-standard input
182
- # such as JSON-encoding arguments, posts directly to objects, etc.
183
-
184
- def fql_query(query, args = {}, options = {})
185
- get_object("fql", args.merge(:q => query), options)
186
- end
187
-
188
- def fql_multiquery(queries = {}, args = {}, options = {})
189
- if results = get_object("fql", args.merge(:q => MultiJson.encode(queries)), options)
190
- # simplify the multiquery result format
191
- results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
192
- end
193
- end
194
-
195
- def get_page_access_token(object_id, args = {}, options = {})
196
- result = get_object(object_id, args.merge(:fields => "access_token"), options) do
197
- result ? result["access_token"] : nil
198
- end
199
- end
200
-
201
- def get_comments_for_urls(urls = [], args = {}, options = {})
202
- # Fetchs the comments for given URLs (array or comma-separated string)
203
- # see https://developers.facebook.com/blog/post/490
204
- return [] if urls.empty?
205
- args.merge!(:ids => urls.respond_to?(:join) ? urls.join(",") : urls)
206
- get_object("comments", args, options)
207
- end
208
-
209
- def set_app_restrictions(app_id, restrictions_hash, args = {}, options = {})
210
- graph_call(app_id, args.merge(:restrictions => MultiJson.encode(restrictions_hash)), "post", options)
211
- end
212
-
213
- # GraphCollection support
214
- def get_page(params)
215
- # Pages through a set of results stored in a GraphCollection
216
- # Used for connections and search results
217
- graph_call(*params)
218
- end
219
-
220
- # Batch API
221
- def batch(http_options = {}, &block)
222
- batch_client = GraphBatchAPI.new(access_token, self)
223
- if block
224
- yield batch_client
225
- batch_client.execute(http_options)
226
- else
227
- batch_client
228
- end
229
- end
230
-
231
- # Direct access to the Facebook API
232
- # see any of the above methods for example invocations
233
- def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
234
- result = api(path, args, verb, options) do |response|
235
- error = check_response(response)
236
- raise error if error
237
- end
238
-
239
- # turn this into a GraphCollection if it's pageable
240
- result = GraphCollection.evaluate(result, self)
241
-
242
- # now process as appropriate for the given call (get picture header, etc.)
243
- post_processing ? post_processing.call(result) : result
244
- end
245
-
246
- private
247
-
248
- def check_response(response)
249
- # check for Graph API-specific errors
250
- # this returns an error, which is immediately raised (non-batch)
251
- # or added to the list of batch results (batch)
252
- if response.is_a?(Hash) && error_details = response["error"]
253
- APIError.new(error_details)
254
- end
255
- end
256
-
257
- def parse_media_args(media_args, method)
258
- # photo and video uploads can accept different types of arguments (see above)
259
- # so here, we parse the arguments into a form directly usable in put_object
260
- raise KoalaError.new("Wrong number of arguments for put_#{method == "photos" ? "picture" : "video"}") unless media_args.size.between?(1, 5)
261
-
262
- args_offset = media_args[1].kind_of?(Hash) || media_args.size == 1 ? 0 : 1
263
-
264
- args = media_args[1 + args_offset] || {}
265
- target_id = media_args[2 + args_offset] || "me"
266
- options = media_args[3 + args_offset] || {}
267
-
268
- if url?(media_args.first)
269
- # If media_args is a URL, we can upload without UploadableIO
270
- args.merge!(:url => media_args.first)
271
- else
272
- args["source"] = Koala::UploadableIO.new(*media_args.slice(0, 1 + args_offset))
273
- end
274
-
275
- [target_id, method, args, options]
276
- end
277
-
278
- def url?(data)
279
- return false unless data.is_a? String
280
- begin
281
- uri = URI.parse(data)
282
- %w( http https ).include?(uri.scheme)
283
- rescue URI::BadURIError
284
- false
285
- end
286
- end
287
- end
288
- end
289
- end
@@ -1,63 +0,0 @@
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, :api, :raw_response
14
-
15
- def self.evaluate(response, api)
16
- # turn the response into a GraphCollection if it's pageable; if not, return the original response
17
- response.is_a?(Hash) && response["data"].is_a?(Array) ? self.new(response, api) : response
18
- end
19
-
20
- def initialize(response, api)
21
- super response["data"]
22
- @paging = response["paging"]
23
- @raw_response = response
24
- @api = api
25
- end
26
-
27
- # defines methods for NEXT and PREVIOUS pages
28
- %w{next previous}.each do |this|
29
-
30
- # def next_page
31
- # def previous_page
32
- define_method "#{this.to_sym}_page" do
33
- base, args = send("#{this}_page_params")
34
- base ? @api.get_page([base, args]) : nil
35
- end
36
-
37
- # def next_page_params
38
- # def previous_page_params
39
- define_method "#{this.to_sym}_page_params" do
40
- return nil unless @paging and @paging[this]
41
- parse_page_url(@paging[this])
42
- end
43
- end
44
-
45
- def parse_page_url(url)
46
- GraphCollection.parse_page_url(url)
47
- end
48
-
49
- def self.parse_page_url(url)
50
- match = url.match(/.com\/(.*)\?(.*)/)
51
- base = match[1]
52
- args = match[2]
53
- params = CGI.parse(args)
54
- new_params = {}
55
- params.each_pair do |key,value|
56
- new_params[key] = value.join ","
57
- end
58
- [base,new_params]
59
- end
60
-
61
- end
62
- end
63
- end
@@ -1,35 +0,0 @@
1
- require 'faraday'
2
-
3
- module Koala
4
- class MultipartRequest < Faraday::Request::Multipart
5
- # Facebook expects nested parameters to be passed in a certain way
6
- # Based on our testing (https://github.com/arsduo/koala/issues/125),
7
- # Faraday needs two changes to make that work:
8
- # 1) [] need to be escaped (e.g. params[foo]=bar ==> params%5Bfoo%5D=bar)
9
- # 2) such messages need to be multipart-encoded
10
-
11
- self.mime_type = 'multipart/form-data'.freeze
12
-
13
- def process_request?(env)
14
- # if the request values contain any hashes or arrays, multipart it
15
- super || !!(env[:body].respond_to?(:values) && env[:body].values.find {|v| v.is_a?(Hash) || v.is_a?(Array)})
16
- end
17
-
18
-
19
- def process_params(params, prefix = nil, pieces = nil, &block)
20
- params.inject(pieces || []) do |all, (key, value)|
21
- key = "#{prefix}%5B#{key}%5D" if prefix
22
-
23
- case value
24
- when Array
25
- values = value.inject([]) { |a,v| a << [nil, v] }
26
- process_params(values, key, all, &block)
27
- when Hash
28
- process_params(value, key, all, &block)
29
- else
30
- all << block.call(key, value)
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,181 +0,0 @@
1
- require "net/http/post/multipart"
2
-
3
- module Koala
4
- class UploadableIO
5
- attr_reader :io_or_path, :content_type, :filename
6
-
7
- def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
8
- # see if we got the right inputs
9
- parse_init_mixed_param io_or_path_or_mixed, content_type
10
-
11
- # filename is used in the Ads API
12
- # if it's provided, take precedence over the detected filename
13
- # otherwise, fall back to a dummy name
14
- @filename = filename || @filename || "koala-io-file.dum"
15
-
16
- raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
17
- raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type
18
- end
19
-
20
- def to_upload_io
21
- UploadIO.new(@io_or_path, @content_type, @filename)
22
- end
23
-
24
- def to_file
25
- @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
26
- end
27
-
28
- def self.binary_content?(content)
29
- content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
30
- end
31
-
32
- private
33
- DETECTION_STRATEGIES = [
34
- :sinatra_param?,
35
- :rails_3_param?,
36
- :file_param?
37
- ]
38
-
39
- PARSE_STRATEGIES = [
40
- :parse_rails_3_param,
41
- :parse_sinatra_param,
42
- :parse_file_object,
43
- :parse_string_path,
44
- :parse_io
45
- ]
46
-
47
- def parse_init_mixed_param(mixed, content_type = nil)
48
- PARSE_STRATEGIES.each do |method|
49
- send(method, mixed, content_type)
50
- return if @io_or_path && @content_type
51
- end
52
- end
53
-
54
- # Expects a parameter of type ActionDispatch::Http::UploadedFile
55
- def self.rails_3_param?(uploaded_file)
56
- uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
57
- end
58
-
59
- def parse_rails_3_param(uploaded_file, content_type = nil)
60
- if UploadableIO.rails_3_param?(uploaded_file)
61
- @io_or_path = uploaded_file.tempfile.path
62
- @content_type = content_type || uploaded_file.content_type
63
- @filename = uploaded_file.original_filename
64
- end
65
- end
66
-
67
- # Expects a Sinatra hash of file info
68
- def self.sinatra_param?(file_hash)
69
- file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
70
- end
71
-
72
- def parse_sinatra_param(file_hash, content_type = nil)
73
- if UploadableIO.sinatra_param?(file_hash)
74
- @io_or_path = file_hash[:tempfile]
75
- @content_type = content_type || file_hash[:type] || detect_mime_type(tempfile)
76
- @filename = file_hash[:filename]
77
- end
78
- end
79
-
80
- # takes a file object
81
- def self.file_param?(file)
82
- file.kind_of?(File)
83
- end
84
-
85
- def parse_file_object(file, content_type = nil)
86
- if UploadableIO.file_param?(file)
87
- @io_or_path = file
88
- @content_type = content_type || detect_mime_type(file.path)
89
- @filename = File.basename(file.path)
90
- end
91
- end
92
-
93
- def parse_string_path(path, content_type = nil)
94
- if path.kind_of?(String)
95
- @io_or_path = path
96
- @content_type = content_type || detect_mime_type(path)
97
- @filename = File.basename(path)
98
- end
99
- end
100
-
101
- def parse_io(io, content_type = nil)
102
- if io.respond_to?(:read)
103
- @io_or_path = io
104
- @content_type = content_type
105
- end
106
- end
107
-
108
- MIME_TYPE_STRATEGIES = [
109
- :use_mime_module,
110
- :use_simple_detection
111
- ]
112
-
113
- def detect_mime_type(filename)
114
- if filename
115
- MIME_TYPE_STRATEGIES.each do |method|
116
- result = send(method, filename)
117
- return result if result
118
- end
119
- end
120
- nil # if we can't find anything
121
- end
122
-
123
- def use_mime_module(filename)
124
- # if the user has installed mime/types, we can use that
125
- # if not, rescue and return nil
126
- begin
127
- type = MIME::Types.type_for(filename).first
128
- type ? type.to_s : nil
129
- rescue
130
- nil
131
- end
132
- end
133
-
134
- def use_simple_detection(filename)
135
- # very rudimentary extension analysis for images
136
- # first, get the downcased extension, or an empty string if it doesn't exist
137
- extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
138
- case extension
139
- when ""
140
- nil
141
- # images
142
- when "jpg", "jpeg"
143
- "image/jpeg"
144
- when "png"
145
- "image/png"
146
- when "gif"
147
- "image/gif"
148
-
149
- # video
150
- when "3g2"
151
- "video/3gpp2"
152
- when "3gp", "3gpp"
153
- "video/3gpp"
154
- when "asf"
155
- "video/x-ms-asf"
156
- when "avi"
157
- "video/x-msvideo"
158
- when "flv"
159
- "video/x-flv"
160
- when "m4v"
161
- "video/x-m4v"
162
- when "mkv"
163
- "video/x-matroska"
164
- when "mod"
165
- "video/mod"
166
- when "mov", "qt"
167
- "video/quicktime"
168
- when "mp4", "mpeg4"
169
- "video/mp4"
170
- when "mpe", "mpeg", "mpg", "tod", "vob"
171
- "video/mpeg"
172
- when "nsv"
173
- "application/x-winamp"
174
- when "ogm", "ogv"
175
- "video/ogg"
176
- when "wmv"
177
- "video/x-ms-wmv"
178
- end
179
- end
180
- end
181
- end