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
data/.gitignore CHANGED
@@ -2,4 +2,6 @@ pkg
2
2
  .project
3
3
  Gemfile.lock
4
4
  .rvmrc
5
- *.rbc
5
+ *.rbc
6
+ *~
7
+ .yardoc/
@@ -7,3 +7,7 @@ rvm:
7
7
  - rbx-2.0
8
8
  - ree
9
9
  - jruby
10
+
11
+ branches:
12
+ except:
13
+ - rdoc
@@ -0,0 +1,3 @@
1
+ --readme readme.md
2
+ --title "Koala Documentation"
3
+ --no-private
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
@@ -0,0 +1,6 @@
1
+ guard 'rspec', :version => 2 do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
@@ -1,122 +1,40 @@
1
- require 'cgi'
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
- module Facebook
34
- # Ruby client library for the Facebook Platform.
35
- # Copyright 2010-2011 Alex Koppel
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
@@ -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