koala 1.3.0rc1 → 1.3.0rc2

Sign up to get free protection for your applications and to get access to all the features.
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