koala 1.3.0rc1 → 1.3.0rc2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -1
- data/.travis.yml +4 -0
- data/.yardopts +3 -0
- data/CHANGELOG +7 -0
- data/Gemfile +14 -0
- data/Guardfile +6 -0
- data/lib/koala.rb +16 -97
- data/lib/koala/api.rb +93 -0
- data/lib/koala/api/batch_operation.rb +83 -0
- data/lib/koala/api/graph_api.rb +476 -0
- data/lib/koala/{graph_batch_api.rb → api/graph_batch_api.rb} +20 -16
- data/lib/koala/api/graph_collection.rb +107 -0
- data/lib/koala/api/legacy.rb +26 -0
- data/lib/koala/{rest_api.rb → api/rest_api.rb} +33 -3
- data/lib/koala/http_service.rb +69 -19
- data/lib/koala/http_service/multipart_request.rb +41 -0
- data/lib/koala/http_service/response.rb +18 -0
- data/lib/koala/http_service/uploadable_io.rb +187 -0
- data/lib/koala/oauth.rb +117 -14
- data/lib/koala/realtime_updates.rb +89 -51
- data/lib/koala/test_users.rb +109 -33
- data/lib/koala/utils.rb +4 -0
- data/lib/koala/version.rb +1 -1
- data/spec/cases/api_spec.rb +19 -12
- data/spec/cases/graph_api_batch_spec.rb +41 -41
- data/spec/cases/http_service_spec.rb +1 -22
- data/spec/cases/legacy_spec.rb +107 -0
- data/spec/cases/multipart_request_spec.rb +5 -5
- data/spec/cases/oauth_spec.rb +9 -9
- data/spec/cases/realtime_updates_spec.rb +154 -47
- data/spec/cases/test_users_spec.rb +268 -219
- data/spec/fixtures/mock_facebook_responses.yml +10 -6
- data/spec/support/graph_api_shared_examples.rb +17 -12
- data/spec/support/koala_test.rb +1 -1
- data/spec/support/mock_http_service.rb +2 -2
- data/spec/support/rest_api_shared_examples.rb +1 -1
- metadata +82 -104
- data/lib/koala/batch_operation.rb +0 -74
- data/lib/koala/graph_api.rb +0 -289
- data/lib/koala/graph_collection.rb +0 -63
- data/lib/koala/multipart_request.rb +0 -35
- data/lib/koala/uploadable_io.rb +0 -181
- data/spec/cases/graph_and_rest_api_spec.rb +0 -22
- data/spec/cases/graph_api_spec.rb +0 -22
- data/spec/cases/rest_api_spec.rb +0 -22
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/.yardopts
ADDED
data/CHANGELOG
CHANGED
@@ -3,13 +3,20 @@ New methods:
|
|
3
3
|
-- OAuth#url_for_dialog creates URLs for Facebook dialog pages
|
4
4
|
-- API#set_app_restrictions handles JSON-encoding app restrictions
|
5
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
|
6
7
|
Updated methods:
|
7
8
|
-- OAuth#url_for_access_token and #url_for_oauth_code now include any provided options as URL parameters
|
8
9
|
-- APIError#raw_response allows access to the raw error response received from Facebook
|
9
10
|
-- Utils.deprecate only prints each message once (no more spamming)
|
10
11
|
-- API#get_page_access_token now accepts additional arguments and HTTP options (like other calls)
|
12
|
+
-- TestUsers and RealtimeUpdates methods now take http_options arguments
|
13
|
+
-- All methods with http_options can now take :http_component => :response for the complete response
|
11
14
|
Internal improvements:
|
12
15
|
-- FQL queries now use the Graph API behind-the-scenes
|
16
|
+
-- Cleaned up file and class organization, with aliases for backward compatibility
|
17
|
+
-- Added YARD documentation throughout
|
18
|
+
-- Fixed bugs in RealtimeUpdates, TestUsers, elsewhere
|
19
|
+
-- Reorganized file and class structure non-destructively
|
13
20
|
Testing improvements:
|
14
21
|
-- Expanded/improved test coverage
|
15
22
|
-- The test suite no longer users any hard-coded user IDs
|
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
data/lib/koala.rb
CHANGED
@@ -1,122 +1,40 @@
|
|
1
|
-
|
1
|
+
# useful tools
|
2
2
|
require 'digest/md5'
|
3
|
-
|
4
3
|
require 'multi_json'
|
5
4
|
|
6
|
-
# OpenSSL and Base64 are required to support signed_request
|
7
|
-
require 'openssl'
|
8
|
-
require 'base64'
|
9
|
-
|
10
5
|
# include koala modules
|
6
|
+
require 'koala/api'
|
11
7
|
require 'koala/oauth'
|
12
|
-
require 'koala/graph_api'
|
13
|
-
require 'koala/graph_batch_api'
|
14
|
-
require 'koala/batch_operation'
|
15
|
-
require 'koala/graph_collection'
|
16
|
-
require 'koala/rest_api'
|
17
8
|
require 'koala/realtime_updates'
|
18
9
|
require 'koala/test_users'
|
19
10
|
|
20
11
|
# HTTP module so we can communicate with Facebook
|
21
12
|
require 'koala/http_service'
|
22
|
-
require 'koala/multipart_request'
|
23
|
-
|
24
|
-
# add KoalaIO class
|
25
|
-
require 'koala/uploadable_io'
|
26
13
|
|
27
14
|
# miscellaneous
|
28
15
|
require 'koala/utils'
|
29
16
|
require 'koala/version'
|
30
17
|
|
31
18
|
module Koala
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# Contributors: Alex Koppel, Chris Baclig, Rafi Jacoby, and the team at Context Optional
|
37
|
-
# http://github.com/arsduo/koala
|
38
|
-
|
39
|
-
# APIs
|
40
|
-
class API
|
41
|
-
# initialize with an access token
|
42
|
-
def initialize(access_token = nil)
|
43
|
-
@access_token = access_token
|
44
|
-
end
|
45
|
-
attr_reader :access_token
|
46
|
-
|
47
|
-
include GraphAPIMethods
|
48
|
-
include RestAPIMethods
|
49
|
-
|
50
|
-
def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
|
51
|
-
# Fetches the given path in the Graph API.
|
52
|
-
args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
|
53
|
-
|
54
|
-
# add a leading /
|
55
|
-
path = "/#{path}" unless path =~ /^\//
|
56
|
-
|
57
|
-
# make the request via the provided service
|
58
|
-
result = Koala.make_request(path, args, verb, options)
|
59
|
-
|
60
|
-
# Check for any 500 errors before parsing the body
|
61
|
-
# since we're not guaranteed that the body is valid JSON
|
62
|
-
# in the case of a server error
|
63
|
-
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
|
64
|
-
|
65
|
-
# parse the body as JSON and run it through the error checker (if provided)
|
66
|
-
# Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
|
67
|
-
# and cause MultiJson.decode to fail -- so we account for that by wrapping the result in []
|
68
|
-
body = MultiJson.decode("[#{result.body.to_s}]")[0]
|
69
|
-
yield body if error_checking_block
|
70
|
-
|
71
|
-
# if we want a component other than the body (e.g. redirect header for images), return that
|
72
|
-
options[:http_component] ? result.send(options[:http_component]) : body
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# special enhanced APIs
|
77
|
-
class GraphBatchAPI < API
|
78
|
-
include GraphBatchAPIMethods
|
79
|
-
end
|
80
|
-
|
81
|
-
class RealtimeUpdates
|
82
|
-
include RealtimeUpdateMethods
|
83
|
-
end
|
84
|
-
|
85
|
-
class TestUsers
|
86
|
-
include TestUserMethods
|
87
|
-
end
|
88
|
-
|
89
|
-
# legacy support for old APIs
|
90
|
-
class OldAPI < API;
|
91
|
-
def initialize(*args)
|
92
|
-
Koala::Utils.deprecate("#{self.class.name} is deprecated and will be removed in a future version; please use the API class instead.")
|
93
|
-
super
|
94
|
-
end
|
95
|
-
end
|
96
|
-
class GraphAPI < OldAPI; end
|
97
|
-
class RestAPI < OldAPI; end
|
98
|
-
class GraphAndRestAPI < OldAPI; end
|
99
|
-
|
100
|
-
# Errors
|
101
|
-
|
102
|
-
class APIError < StandardError
|
103
|
-
attr_accessor :fb_error_type, :raw_response
|
104
|
-
def initialize(details = {})
|
105
|
-
self.raw_response = details
|
106
|
-
self.fb_error_type = details["type"]
|
107
|
-
super("#{fb_error_type}: #{details["message"]}")
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
19
|
+
# A Ruby client library for the Facebook Platform.
|
20
|
+
# See http://github.com/arsduo/koala/wiki for a general introduction to Koala
|
21
|
+
# and the Graph API.
|
22
|
+
|
112
23
|
class KoalaError < StandardError; end
|
113
24
|
|
114
|
-
|
115
|
-
# finally, the few things defined on the Koala module itself
|
25
|
+
# Making HTTP requests
|
116
26
|
class << self
|
27
|
+
# Control which HTTP service framework Koala uses.
|
28
|
+
# Primarily used to switch between the mock-request framework used in testing
|
29
|
+
# and the live framework used in real life (and live testing).
|
30
|
+
# In theory, you could write your own HTTPService module if you need different functionality,
|
31
|
+
# but since the switch to {https://github.com/arsduo/koala/wiki/HTTP-Services Faraday} almost all such goals can be accomplished with middleware.
|
117
32
|
attr_accessor :http_service
|
118
33
|
end
|
119
34
|
|
35
|
+
# @private
|
36
|
+
# For current HTTPServices, switch the service as expected.
|
37
|
+
# For deprecated services (Typhoeus and Net::HTTP), print a warning and set the default Faraday adapter appropriately.
|
120
38
|
def self.http_service=(service)
|
121
39
|
if service.respond_to?(:deprecated_interface)
|
122
40
|
# if this is a deprecated module, support the old interface
|
@@ -129,6 +47,7 @@ module Koala
|
|
129
47
|
end
|
130
48
|
end
|
131
49
|
|
50
|
+
# An convenenient alias to Koala.http_service.make_request.
|
132
51
|
def self.make_request(path, args, verb, options = {})
|
133
52
|
http_service.make_request(path, args, verb, options)
|
134
53
|
end
|
data/lib/koala/api.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# graph_batch_api and legacy are required at the bottom, since they depend on API being defined
|
2
|
+
require 'koala/api/graph_api'
|
3
|
+
require 'koala/api/rest_api'
|
4
|
+
|
5
|
+
module Koala
|
6
|
+
module Facebook
|
7
|
+
class API
|
8
|
+
# Creates a new API client.
|
9
|
+
# @param [String] access_token access token
|
10
|
+
# @note If no access token is provided, you can only access some public information.
|
11
|
+
# @return [Koala::Facebook::API] the API client
|
12
|
+
def initialize(access_token = nil)
|
13
|
+
@access_token = access_token
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :access_token
|
17
|
+
|
18
|
+
include GraphAPIMethods
|
19
|
+
include RestAPIMethods
|
20
|
+
|
21
|
+
# Makes a request to the appropriate Facebook API.
|
22
|
+
# @note You'll rarely need to call this method directly.
|
23
|
+
#
|
24
|
+
# @see GraphAPIMethods#graph_call
|
25
|
+
# @see RestAPIMethods#rest_call
|
26
|
+
#
|
27
|
+
# @param path the server path for this request (leading / is prepended if not present)
|
28
|
+
# @param args arguments to be sent to Facebook
|
29
|
+
# @param verb the HTTP method to use
|
30
|
+
# @param options request-related options for Koala and Faraday.
|
31
|
+
# See https://github.com/arsduo/koala/wiki/HTTP-Services for additional options.
|
32
|
+
# @option options [Symbol] :http_component which part of the response (headers, body, or status) to return
|
33
|
+
# @option options [Boolean] :beta use Facebook's beta tier
|
34
|
+
# @option options [Boolean] :use_ssl force SSL for this request, even if it's tokenless.
|
35
|
+
# (All API requests with access tokens use SSL.)
|
36
|
+
# @param error_checking_block a block to evaluate the response status for additional JSON-encoded errors
|
37
|
+
#
|
38
|
+
# @yield The response body for evaluation
|
39
|
+
#
|
40
|
+
# @raise [Koala::Facebook::APIError] if Facebook returns an error (response status >= 500)
|
41
|
+
#
|
42
|
+
# @return the body of the response from Facebook (unless another http_component is requested)
|
43
|
+
def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
|
44
|
+
# Fetches the given path in the Graph API.
|
45
|
+
args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
|
46
|
+
|
47
|
+
# add a leading /
|
48
|
+
path = "/#{path}" unless path =~ /^\//
|
49
|
+
|
50
|
+
# make the request via the provided service
|
51
|
+
result = Koala.make_request(path, args, verb, options)
|
52
|
+
|
53
|
+
# Check for any 500 errors before parsing the body
|
54
|
+
# since we're not guaranteed that the body is valid JSON
|
55
|
+
# in the case of a server error
|
56
|
+
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status >= 500
|
57
|
+
|
58
|
+
# parse the body as JSON and run it through the error checker (if provided)
|
59
|
+
# Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
|
60
|
+
# and cause MultiJson.decode to fail -- so we account for that by wrapping the result in []
|
61
|
+
body = MultiJson.decode("[#{result.body.to_s}]")[0]
|
62
|
+
yield body if error_checking_block
|
63
|
+
|
64
|
+
# if we want a component other than the body (e.g. redirect header for images), return that
|
65
|
+
if component = options[:http_component]
|
66
|
+
component == :response ? result : result.send(options[:http_component])
|
67
|
+
else
|
68
|
+
body
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class APIError < StandardError
|
74
|
+
attr_accessor :fb_error_type, :raw_response
|
75
|
+
|
76
|
+
# Creates a new APIError.
|
77
|
+
#
|
78
|
+
# Assigns the error type (as reported by Facebook) to #fb_error_type
|
79
|
+
# and the raw error response available to #raw_response.
|
80
|
+
#
|
81
|
+
# @param details error details containing "type" and "message" keys.
|
82
|
+
def initialize(details = {})
|
83
|
+
self.raw_response = details
|
84
|
+
self.fb_error_type = details["type"]
|
85
|
+
super("#{fb_error_type}: #{details["message"]}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
require 'koala/api/graph_batch_api'
|
92
|
+
# legacy support for old pre-1.2 API interfaces
|
93
|
+
require 'koala/api/legacy'
|
@@ -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
|