koala 0.9.0 → 1.1.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.
- data/.autotest +12 -0
- data/.gitignore +5 -0
- data/.travis.yml +8 -0
- data/CHANGELOG +71 -7
- data/Gemfile +7 -0
- data/LICENSE +1 -1
- data/Manifest +10 -15
- data/Rakefile +13 -13
- data/autotest/discover.rb +1 -0
- data/koala.gemspec +36 -16
- data/lib/koala/batch_operation.rb +74 -0
- data/lib/koala/graph_api.rb +200 -134
- data/lib/koala/graph_batch_api.rb +87 -0
- data/lib/koala/graph_collection.rb +54 -0
- data/lib/koala/http_services/net_http_service.rb +92 -0
- data/lib/koala/http_services/typhoeus_service.rb +37 -0
- data/lib/koala/http_services.rb +22 -47
- data/lib/koala/oauth.rb +181 -0
- data/lib/koala/realtime_updates.rb +5 -14
- data/lib/koala/rest_api.rb +82 -10
- data/lib/koala/test_users.rb +85 -0
- data/lib/koala/uploadable_io.rb +175 -0
- data/lib/koala.rb +67 -229
- data/readme.md +71 -38
- data/spec/cases/api_base_spec.rb +101 -0
- data/spec/cases/graph_and_rest_api_spec.rb +31 -0
- data/spec/cases/graph_api_batch_spec.rb +609 -0
- data/spec/cases/graph_api_spec.rb +25 -0
- data/spec/cases/http_services/http_service_spec.rb +129 -0
- data/spec/cases/http_services/net_http_service_spec.rb +532 -0
- data/spec/cases/http_services/typhoeus_service_spec.rb +152 -0
- data/spec/cases/koala_spec.rb +55 -0
- data/spec/cases/oauth_spec.rb +409 -0
- data/spec/cases/realtime_updates_spec.rb +184 -0
- data/spec/cases/rest_api_spec.rb +25 -0
- data/spec/cases/test_users_spec.rb +221 -0
- data/spec/cases/uploadable_io_spec.rb +193 -0
- data/spec/fixtures/beach.jpg +0 -0
- data/spec/fixtures/cat.m4v +0 -0
- data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +18 -14
- data/spec/fixtures/mock_facebook_responses.yml +409 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/graph_api_shared_examples.rb +495 -0
- data/spec/support/json_testing_fix.rb +18 -0
- data/spec/support/live_testing_data_helper.rb +40 -0
- data/spec/support/mock_http_service.rb +96 -0
- data/spec/support/rest_api_shared_examples.rb +285 -0
- data/spec/support/setup_mocks_or_live.rb +51 -0
- data/spec/support/uploadable_io_shared_examples.rb +76 -0
- metadata +131 -59
- data/examples/oauth_playground/Capfile +0 -2
- data/examples/oauth_playground/LICENSE +0 -22
- data/examples/oauth_playground/Rakefile +0 -4
- data/examples/oauth_playground/config/deploy.rb +0 -39
- data/examples/oauth_playground/config/facebook.yml +0 -13
- data/examples/oauth_playground/config.ru +0 -27
- data/examples/oauth_playground/lib/load_facebook.rb +0 -3
- data/examples/oauth_playground/lib/oauth_playground.rb +0 -187
- data/examples/oauth_playground/readme.md +0 -8
- data/examples/oauth_playground/spec/oauth_playground_spec.rb +0 -35
- data/examples/oauth_playground/spec/spec_helper.rb +0 -36
- data/examples/oauth_playground/tmp/restart.txt +0 -0
- data/examples/oauth_playground/views/index.erb +0 -206
- data/examples/oauth_playground/views/layout.erb +0 -39
- data/init.rb +0 -2
- data/spec/koala/api_base_tests.rb +0 -95
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -10
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -11
- data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -114
- data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -150
- data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
- data/spec/koala/live_testing_data_helper.rb +0 -15
- data/spec/koala/net_http_service_tests.rb +0 -181
- data/spec/koala/oauth/oauth_tests.rb +0 -440
- data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
- data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -94
- data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -36
- data/spec/koala_spec.rb +0 -18
- data/spec/koala_spec_helper.rb +0 -31
- data/spec/koala_spec_without_mocks.rb +0 -19
- data/spec/mock_facebook_responses.yml +0 -241
- data/spec/mock_http_service.rb +0 -81
data/lib/koala/graph_api.rb
CHANGED
|
@@ -1,65 +1,12 @@
|
|
|
1
1
|
module Koala
|
|
2
2
|
module Facebook
|
|
3
|
-
GRAPH_SERVER = "graph.facebook.com"
|
|
4
|
-
|
|
5
|
-
class GraphCollection < Array
|
|
6
|
-
#This class is a light wrapper for collections returned
|
|
7
|
-
#from the Graph API.
|
|
8
|
-
#
|
|
9
|
-
#It extends Array to allow direct access to the data colleciton
|
|
10
|
-
#which should allow it to drop in seamlessly.
|
|
11
|
-
#
|
|
12
|
-
#It also allows access to paging information and the
|
|
13
|
-
#ability to get the next/previous page in the collection
|
|
14
|
-
#by calling next_page or previous_page.
|
|
15
|
-
attr_reader :paging
|
|
16
|
-
attr_reader :api
|
|
17
|
-
|
|
18
|
-
def initialize(response, api)
|
|
19
|
-
super response["data"]
|
|
20
|
-
@paging = response["paging"]
|
|
21
|
-
@api = api
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# defines methods for NEXT and PREVIOUS pages
|
|
25
|
-
%w{next previous}.each do |this|
|
|
26
|
-
|
|
27
|
-
# def next_page
|
|
28
|
-
# def previous_page
|
|
29
|
-
define_method "#{this.to_sym}_page" do
|
|
30
|
-
base, args = send("#{this}_page_params")
|
|
31
|
-
base ? @api.get_page([base, args]) : nil
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# def next_page_params
|
|
35
|
-
# def previous_page_params
|
|
36
|
-
define_method "#{this.to_sym}_page_params" do
|
|
37
|
-
return nil unless @paging and @paging[this]
|
|
38
|
-
parse_page_url(@paging[this])
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def parse_page_url(url)
|
|
43
|
-
match = url.match(/.com\/(.*)\?(.*)/)
|
|
44
|
-
base = match[1]
|
|
45
|
-
args = match[2]
|
|
46
|
-
params = CGI.parse(args)
|
|
47
|
-
new_params = {}
|
|
48
|
-
params.each_pair do |key,value|
|
|
49
|
-
new_params[key] = value.join ","
|
|
50
|
-
end
|
|
51
|
-
[base,new_params]
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
|
|
3
|
+
GRAPH_SERVER = "graph.facebook.com"
|
|
57
4
|
|
|
58
5
|
module GraphAPIMethods
|
|
59
6
|
# A client for the Facebook Graph API.
|
|
60
7
|
#
|
|
61
|
-
# See http://
|
|
62
|
-
# for
|
|
8
|
+
# See http://github.com/arsduo/koala for Ruby/Koala documentation
|
|
9
|
+
# and http://developers.facebook.com/docs/api for Facebook API documentation
|
|
63
10
|
#
|
|
64
11
|
# The Graph API is made up of the objects in Facebook (e.g., people, pages,
|
|
65
12
|
# events, photos) and the connections between them (e.g., friends,
|
|
@@ -76,117 +23,236 @@ module Koala
|
|
|
76
23
|
# by the API at http://developers.facebook.com/docs/reference/api/.
|
|
77
24
|
#
|
|
78
25
|
# You can obtain an access token via OAuth or by using the Facebook
|
|
79
|
-
# JavaScript SDK. See
|
|
80
|
-
# for details.
|
|
26
|
+
# JavaScript SDK. See the Koala and Facebook documentation for more information.
|
|
81
27
|
#
|
|
82
28
|
# If you are using the JavaScript SDK, you can use the
|
|
83
29
|
# Koala::Facebook::OAuth.get_user_from_cookie() method below to get the OAuth access token
|
|
84
30
|
# for the active user from the cookie saved by the SDK.
|
|
85
|
-
|
|
86
|
-
|
|
31
|
+
|
|
32
|
+
# Objects
|
|
33
|
+
|
|
34
|
+
def get_object(id, args = {}, options = {})
|
|
87
35
|
# Fetchs the given object from the graph.
|
|
88
|
-
graph_call(id, args)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def get_objects(ids, args = {})
|
|
92
|
-
# Fetchs all of the given object from the graph.
|
|
93
|
-
# We return a map from ID to object. If any of the IDs are invalid,
|
|
94
|
-
# we raise an exception.
|
|
95
|
-
graph_call("", args.merge("ids" => ids.join(",")))
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def get_page(params)
|
|
99
|
-
result = graph_call(*params)
|
|
100
|
-
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
|
|
36
|
+
graph_call(id, args, "get", options)
|
|
101
37
|
end
|
|
102
38
|
|
|
103
|
-
def
|
|
104
|
-
# Fetchs
|
|
105
|
-
|
|
106
|
-
|
|
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)
|
|
107
44
|
end
|
|
108
45
|
|
|
109
|
-
|
|
110
|
-
def get_picture(object, args = {})
|
|
111
|
-
result = graph_call("#{object}/picture", args, "get", :http_component => :headers)
|
|
112
|
-
result["Location"]
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def put_object(parent_object, connection_name, args = {})
|
|
46
|
+
def put_object(parent_object, connection_name, args = {}, options = {})
|
|
116
47
|
# Writes the given object to the graph, connected to the given parent.
|
|
117
|
-
#
|
|
118
|
-
# For example,
|
|
119
|
-
#
|
|
120
|
-
# graph.put_object("me", "feed", :message => "Hello, world")
|
|
121
|
-
#
|
|
122
|
-
# writes "Hello, world" to the active user's wall. Likewise, this
|
|
123
|
-
# will comment on a the first post of the active user's feed:
|
|
124
|
-
#
|
|
125
|
-
# feed = graph.get_connections("me", "feed")
|
|
126
|
-
# post = feed["data"][0]
|
|
127
|
-
# graph.put_object(post["id"], "comments", :message => "First!")
|
|
128
|
-
#
|
|
129
48
|
# See http://developers.facebook.com/docs/api#publishing for all of
|
|
130
49
|
# the supported writeable objects.
|
|
131
50
|
#
|
|
51
|
+
# For example,
|
|
52
|
+
# graph.put_object("me", "feed", :message => "Hello, world")
|
|
53
|
+
# writes "Hello, world" to the active user's wall.
|
|
54
|
+
#
|
|
132
55
|
# Most write operations require extended permissions. For example,
|
|
133
56
|
# publishing wall posts requires the "publish_stream" permission. See
|
|
134
57
|
# http://developers.facebook.com/docs/authentication/ for details about
|
|
135
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
|
|
136
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) do |result|
|
|
75
|
+
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def get_comments_for_urls(urls = [], args = {}, options = {})
|
|
80
|
+
# Fetchs the comments for given URLs (array or comma-separated string)
|
|
81
|
+
# see https://developers.facebook.com/blog/post/490
|
|
82
|
+
return [] if urls.empty?
|
|
83
|
+
args.merge!(:ids => urls.respond_to?(:join) ? urls.join(",") : urls)
|
|
84
|
+
get_object("comments", args, options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def put_connections(id, connection_name, args = {}, options = {})
|
|
88
|
+
# Posts a certain connection
|
|
137
89
|
raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
|
|
138
|
-
graph_call("#{
|
|
90
|
+
graph_call("#{id}/#{connection_name}", args, "post", options)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def delete_connections(id, connection_name, args = {}, options = {})
|
|
94
|
+
# Deletes a given connection
|
|
95
|
+
raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Delete requires an access token"}) unless @access_token
|
|
96
|
+
graph_call("#{id}/#{connection_name}", args, "delete", options)
|
|
139
97
|
end
|
|
98
|
+
|
|
99
|
+
# Media (photos and videos)
|
|
100
|
+
# to delete photos or videos, use delete_object(object_id)
|
|
101
|
+
# note: you'll need the user_photos or user_videos permissions to actually access media after upload
|
|
140
102
|
|
|
141
|
-
def
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
103
|
+
def get_picture(object, args = {}, options = {})
|
|
104
|
+
# Gets a picture object, returning the URL (which Facebook sends as a header)
|
|
105
|
+
graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
|
|
106
|
+
result["Location"]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Can be called in multiple ways:
|
|
111
|
+
#
|
|
112
|
+
# put_picture(file, [content_type], ...)
|
|
113
|
+
# put_picture(path_to_file, [content_type], ...)
|
|
114
|
+
#
|
|
115
|
+
# You can pass in uploaded files directly from Rails or Sinatra.
|
|
116
|
+
# (See lib/koala/uploadable_io.rb for supported frameworks)
|
|
117
|
+
#
|
|
118
|
+
# Optional parameters can be added to the end of the argument list:
|
|
119
|
+
# - args: a hash of request parameters (default: {})
|
|
120
|
+
# - target_id: ID of the target where to post the picture (default: "me")
|
|
121
|
+
# - options: a hash of http options passed to the HTTPService module
|
|
122
|
+
#
|
|
123
|
+
# put_picture(file, content_type, {:message => "Message"}, 01234560)
|
|
124
|
+
# put_picture(params[:file], {:message => "Message"})
|
|
125
|
+
|
|
126
|
+
def put_picture(*picture_args)
|
|
127
|
+
put_object(*parse_media_args(picture_args, "photos"))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def put_video(*video_args)
|
|
131
|
+
args = parse_media_args(video_args, "videos")
|
|
132
|
+
args.last[:video] = true
|
|
133
|
+
put_object(*args)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Wall posts
|
|
137
|
+
# To get wall posts, use get_connections(user, "feed")
|
|
138
|
+
# To delete a wall post, just use delete_object(post_id)
|
|
139
|
+
|
|
140
|
+
def put_wall_post(message, attachment = {}, profile_id = "me", options = {})
|
|
141
|
+
# attachment is a hash describing the wall post
|
|
142
|
+
# (see X for more details)
|
|
143
|
+
# For instance,
|
|
149
144
|
#
|
|
150
|
-
# {"name"
|
|
151
|
-
# "link"
|
|
152
|
-
# "caption"
|
|
153
|
-
# "description"
|
|
154
|
-
# "picture"
|
|
145
|
+
# {"name" => "Link name"
|
|
146
|
+
# "link" => "http://www.example.com/",
|
|
147
|
+
# "caption" => "{*actor*} posted a new review",
|
|
148
|
+
# "description" => "This is a longer description of the attachment",
|
|
149
|
+
# "picture" => "http://www.example.com/thumbnail.jpg"}
|
|
155
150
|
|
|
156
|
-
self.put_object(profile_id, "feed", attachment.merge({:message => message}))
|
|
151
|
+
self.put_object(profile_id, "feed", attachment.merge({:message => message}), options)
|
|
157
152
|
end
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
|
|
154
|
+
# Comments
|
|
155
|
+
# to delete comments, use delete_object(comment_id)
|
|
156
|
+
# to get comments, use get_connections(object, "likes")
|
|
157
|
+
|
|
158
|
+
def put_comment(object_id, message, options = {})
|
|
160
159
|
# Writes the given comment on the given post.
|
|
161
|
-
self.put_object(object_id, "comments", {:message => message})
|
|
160
|
+
self.put_object(object_id, "comments", {:message => message}, options)
|
|
162
161
|
end
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
|
|
163
|
+
# Likes
|
|
164
|
+
# to get likes, use get_connections(user, "likes")
|
|
165
|
+
|
|
166
|
+
def put_like(object_id, options = {})
|
|
165
167
|
# Likes the given post.
|
|
166
|
-
self.put_object(object_id, "likes")
|
|
168
|
+
self.put_object(object_id, "likes", {}, options)
|
|
167
169
|
end
|
|
168
|
-
|
|
169
|
-
def
|
|
170
|
-
#
|
|
171
|
-
|
|
170
|
+
|
|
171
|
+
def delete_like(object_id, options = {})
|
|
172
|
+
# Unlikes a given object for the logged-in user
|
|
173
|
+
raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Unliking requires an access token"}) unless @access_token
|
|
174
|
+
graph_call("#{object_id}/likes", {}, "delete", options)
|
|
172
175
|
end
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
|
|
177
|
+
# Search
|
|
178
|
+
|
|
179
|
+
def search(search_terms, args = {}, options = {})
|
|
180
|
+
args.merge!({:q => search_terms}) unless search_terms.nil?
|
|
181
|
+
graph_call("search", args, "get", options) do |result|
|
|
182
|
+
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# GraphCollection support
|
|
188
|
+
def get_page(params)
|
|
189
|
+
# Pages through a set of results stored in a GraphCollection
|
|
190
|
+
# Used for connections and search results
|
|
191
|
+
graph_call(*params) do |result|
|
|
192
|
+
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
|
|
193
|
+
end
|
|
178
194
|
end
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Batch API
|
|
198
|
+
def batch(http_options = {}, &block)
|
|
199
|
+
batch_client = GraphBatchAPI.new(access_token)
|
|
200
|
+
if block
|
|
201
|
+
yield batch_client
|
|
202
|
+
batch_client.execute(http_options)
|
|
203
|
+
else
|
|
204
|
+
batch_client
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.included(base)
|
|
209
|
+
base.class_eval do
|
|
210
|
+
def self.batch
|
|
211
|
+
raise NoMethodError, "The BatchAPI signature has changed (the original implementation was not thread-safe). Please see https://github.com/arsduo/koala/wiki/Batch-requests. (This message will be removed in the final 1.1 release.)"
|
|
185
212
|
end
|
|
186
213
|
end
|
|
214
|
+
end
|
|
187
215
|
|
|
188
|
-
|
|
216
|
+
# Direct access to the Facebook API
|
|
217
|
+
# see any of the above methods for example invocations
|
|
218
|
+
def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
|
|
219
|
+
result = api(path, args, verb, options) do |response|
|
|
220
|
+
error = check_response(response)
|
|
221
|
+
raise error if error
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# now process as appropriate (get picture header, make GraphCollection, etc.)
|
|
225
|
+
post_processing ? post_processing.call(result) : result
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def check_response(response)
|
|
229
|
+
# check for Graph API-specific errors
|
|
230
|
+
# this returns an error, which is immediately raised (non-batch)
|
|
231
|
+
# or added to the list of batch results (batch)
|
|
232
|
+
if response.is_a?(Hash) && error_details = response["error"]
|
|
233
|
+
APIError.new(error_details)
|
|
234
|
+
end
|
|
189
235
|
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def parse_media_args(media_args, method)
|
|
240
|
+
# photo and video uploads can accept different types of arguments (see above)
|
|
241
|
+
# so here, we parse the arguments into a form directly usable in put_object
|
|
242
|
+
raise KoalaError.new("Wrong number of arguments for put_#{method == "photos" ? "picture" : "video"}") unless media_args.size.between?(1, 5)
|
|
243
|
+
|
|
244
|
+
args_offset = media_args[1].kind_of?(Hash) || media_args.size == 1 ? 0 : 1
|
|
245
|
+
|
|
246
|
+
args = media_args[1 + args_offset] || {}
|
|
247
|
+
target_id = media_args[2 + args_offset] || "me"
|
|
248
|
+
options = media_args[3 + args_offset] || {}
|
|
249
|
+
|
|
250
|
+
args["source"] = Koala::UploadableIO.new(*media_args.slice(0, 1 + args_offset))
|
|
251
|
+
|
|
252
|
+
options[:http_service] = Koala.base_http_service if args["source"].requires_base_http_service
|
|
253
|
+
|
|
254
|
+
[target_id, method, args, options]
|
|
255
|
+
end
|
|
190
256
|
end
|
|
191
257
|
end
|
|
192
|
-
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Koala
|
|
2
|
+
module Facebook
|
|
3
|
+
module GraphBatchAPIMethods
|
|
4
|
+
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.class_eval do
|
|
7
|
+
alias_method :graph_call_outside_batch, :graph_call
|
|
8
|
+
alias_method :graph_call, :graph_call_in_batch
|
|
9
|
+
|
|
10
|
+
alias_method :check_graph_api_response, :check_response
|
|
11
|
+
alias_method :check_response, :check_graph_batch_api_response
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def batch_calls
|
|
16
|
+
@batch_calls ||= []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def graph_call_in_batch(path, args = {}, verb = "get", options = {}, &post_processing)
|
|
20
|
+
# for batch APIs, we queue up the call details (incl. post-processing)
|
|
21
|
+
batch_calls << BatchOperation.new(
|
|
22
|
+
:url => path,
|
|
23
|
+
:args => args,
|
|
24
|
+
:method => verb,
|
|
25
|
+
:access_token => options['access_token'] || access_token,
|
|
26
|
+
:http_options => options,
|
|
27
|
+
:post_processing => post_processing
|
|
28
|
+
)
|
|
29
|
+
nil # batch operations return nothing immediately
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check_graph_batch_api_response(response)
|
|
33
|
+
if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
|
|
34
|
+
APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"])
|
|
35
|
+
else
|
|
36
|
+
check_graph_api_response(response)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def execute(http_options = {})
|
|
41
|
+
return [] unless batch_calls.length > 0
|
|
42
|
+
# Turn the call args collected into what facebook expects
|
|
43
|
+
args = {}
|
|
44
|
+
args["batch"] = MultiJson.encode(batch_calls.map { |batch_op|
|
|
45
|
+
args.merge!(batch_op.files) if batch_op.files
|
|
46
|
+
batch_op.to_batch_params(access_token)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
graph_call_outside_batch('/', args, 'post', http_options) do |response|
|
|
50
|
+
# map the results with post-processing included
|
|
51
|
+
index = 0 # keep compat with ruby 1.8 - no with_index for map
|
|
52
|
+
response.map do |call_result|
|
|
53
|
+
# Get the options hash
|
|
54
|
+
batch_op = batch_calls[index]
|
|
55
|
+
index += 1
|
|
56
|
+
|
|
57
|
+
if call_result
|
|
58
|
+
# (see note in regular api method about JSON parsing)
|
|
59
|
+
body = MultiJson.decode("[#{call_result['body'].to_s}]")[0]
|
|
60
|
+
|
|
61
|
+
unless call_result["code"].to_i >= 500 || error = check_response(body)
|
|
62
|
+
# Get the HTTP component they want
|
|
63
|
+
data = case batch_op.http_options[:http_component]
|
|
64
|
+
when :status
|
|
65
|
+
call_result["code"].to_i
|
|
66
|
+
when :headers
|
|
67
|
+
# facebook returns the headers as an array of k/v pairs, but we want a regular hash
|
|
68
|
+
call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
|
|
69
|
+
else
|
|
70
|
+
body
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# process it if we are given a block to process with
|
|
74
|
+
batch_op.post_processing ? batch_op.post_processing.call(data) : data
|
|
75
|
+
else
|
|
76
|
+
error || APIError.new({"type" => "HTTP #{call_result["code"].to_s}", "message" => "Response body: #{body}"})
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
|
14
|
+
attr_reader :api
|
|
15
|
+
|
|
16
|
+
def initialize(response, api)
|
|
17
|
+
super response["data"]
|
|
18
|
+
@paging = response["paging"]
|
|
19
|
+
@api = api
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# defines methods for NEXT and PREVIOUS pages
|
|
23
|
+
%w{next previous}.each do |this|
|
|
24
|
+
|
|
25
|
+
# def next_page
|
|
26
|
+
# def previous_page
|
|
27
|
+
define_method "#{this.to_sym}_page" do
|
|
28
|
+
base, args = send("#{this}_page_params")
|
|
29
|
+
base ? @api.get_page([base, args]) : nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# def next_page_params
|
|
33
|
+
# def previous_page_params
|
|
34
|
+
define_method "#{this.to_sym}_page_params" do
|
|
35
|
+
return nil unless @paging and @paging[this]
|
|
36
|
+
parse_page_url(@paging[this])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse_page_url(url)
|
|
41
|
+
match = url.match(/.com\/(.*)\?(.*)/)
|
|
42
|
+
base = match[1]
|
|
43
|
+
args = match[2]
|
|
44
|
+
params = CGI.parse(args)
|
|
45
|
+
new_params = {}
|
|
46
|
+
params.each_pair do |key,value|
|
|
47
|
+
new_params[key] = value.join ","
|
|
48
|
+
end
|
|
49
|
+
[base,new_params]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require "net/http" unless defined?(Net::HTTP)
|
|
2
|
+
require "net/https"
|
|
3
|
+
require "net/http/post/multipart"
|
|
4
|
+
|
|
5
|
+
module Koala
|
|
6
|
+
module NetHTTPService
|
|
7
|
+
# this service uses Net::HTTP to send requests to the graph
|
|
8
|
+
include Koala::HTTPService
|
|
9
|
+
|
|
10
|
+
# Net::HTTP-specific values
|
|
11
|
+
class << self
|
|
12
|
+
attr_accessor :ca_file, :ca_path, :verify_mode
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.make_request(path, args, verb, options = {})
|
|
16
|
+
# We translate args to a valid query string. If post is specified,
|
|
17
|
+
# we send a POST request to the given path with the given arguments.
|
|
18
|
+
|
|
19
|
+
# by default, we use SSL only for private requests
|
|
20
|
+
# this makes public requests faster
|
|
21
|
+
private_request = args["access_token"] || @always_use_ssl || options[:use_ssl]
|
|
22
|
+
|
|
23
|
+
# if the verb isn't get or post, send it as a post argument
|
|
24
|
+
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
|
25
|
+
|
|
26
|
+
http = create_http(server(options), private_request, options)
|
|
27
|
+
|
|
28
|
+
response = http.start do |http|
|
|
29
|
+
if verb == "post"
|
|
30
|
+
if params_require_multipart? args
|
|
31
|
+
http.request Net::HTTP::Post::Multipart.new path, encode_multipart_params(args)
|
|
32
|
+
else
|
|
33
|
+
http.post(path, encode_params(args))
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
http.get("#{path}?#{encode_params(args)}")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Koala::Response.new(response.code.to_i, response.body, response)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
def self.encode_params(param_hash)
|
|
45
|
+
# unfortunately, we can't use to_query because that's Rails, not Ruby
|
|
46
|
+
# if no hash (e.g. no auth token) return empty string
|
|
47
|
+
((param_hash || {}).collect do |key_and_value|
|
|
48
|
+
key_and_value[1] = MultiJson.encode(key_and_value[1]) if key_and_value[1].class != String
|
|
49
|
+
"#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
|
|
50
|
+
end).join("&")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.encode_multipart_params(param_hash)
|
|
54
|
+
Hash[*param_hash.collect do |key, value|
|
|
55
|
+
[key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
|
|
56
|
+
end.flatten]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.create_http(server, private_request, options)
|
|
60
|
+
if proxy_server = options[:proxy] || proxy
|
|
61
|
+
proxy = URI.parse(proxy_server)
|
|
62
|
+
http = Net::HTTP.new(server, private_request ? 443 : nil,
|
|
63
|
+
proxy.host, proxy.port, proxy.user, proxy.password)
|
|
64
|
+
else
|
|
65
|
+
http = Net::HTTP.new(server, private_request ? 443 : nil)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if timeout_value = options[:timeout] || timeout
|
|
69
|
+
http.open_timeout = timeout_value
|
|
70
|
+
http.read_timeout = timeout_value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# For HTTPS requests, set the proper CA certificates
|
|
74
|
+
if private_request
|
|
75
|
+
http.use_ssl = true
|
|
76
|
+
http.verify_mode = options[:verify_mode] || verify_mode || OpenSSL::SSL::VERIFY_PEER
|
|
77
|
+
|
|
78
|
+
if cert_file = options[:ca_file] || ca_file
|
|
79
|
+
raise Errno::ENOENT, "Certificate file #{cert_file.inspect} does not exist!" unless File.exists?(cert_file)
|
|
80
|
+
http.ca_file = cert_file
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if cert_path = options[:ca_path] || ca_path
|
|
84
|
+
raise Errno::ENOENT, "Certificate path #{cert_path.inspect} does not exist!" unless File.directory?(cert_path)
|
|
85
|
+
http.ca_path = cert_path
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
http
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "typhoeus" unless defined?(Typhoeus)
|
|
2
|
+
|
|
3
|
+
module Koala
|
|
4
|
+
module TyphoeusService
|
|
5
|
+
# this service uses Typhoeus to send requests to the graph
|
|
6
|
+
include Typhoeus
|
|
7
|
+
include Koala::HTTPService
|
|
8
|
+
|
|
9
|
+
def self.make_request(path, args, verb, options = {})
|
|
10
|
+
# if the verb isn't get or post, send it as a post argument
|
|
11
|
+
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
|
12
|
+
|
|
13
|
+
# switch any UploadableIOs to the files Typhoeus expects
|
|
14
|
+
args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
|
|
15
|
+
|
|
16
|
+
# you can pass arguments directly to Typhoeus using the :typhoeus_options key
|
|
17
|
+
typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
|
|
18
|
+
|
|
19
|
+
# if proxy/timeout options aren't passed, check if defaults are set
|
|
20
|
+
typhoeus_options[:proxy] ||= proxy
|
|
21
|
+
typhoeus_options[:timeout] ||= timeout
|
|
22
|
+
|
|
23
|
+
# by default, we use SSL only for private requests (e.g. with access token)
|
|
24
|
+
# this makes public requests faster
|
|
25
|
+
prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
|
|
26
|
+
|
|
27
|
+
response = Typhoeus::Request.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
|
|
28
|
+
Koala::Response.new(response.code, response.body, response.headers_hash)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
protected
|
|
32
|
+
|
|
33
|
+
def self.multipart_requires_content_type?
|
|
34
|
+
false # Typhoeus handles multipart file types, we don't have to require it
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|