koala 1.3.0rc1 → 1.3.0rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +3 -1
  2. data/.travis.yml +4 -0
  3. data/.yardopts +3 -0
  4. data/CHANGELOG +7 -0
  5. data/Gemfile +14 -0
  6. data/Guardfile +6 -0
  7. data/lib/koala.rb +16 -97
  8. data/lib/koala/api.rb +93 -0
  9. data/lib/koala/api/batch_operation.rb +83 -0
  10. data/lib/koala/api/graph_api.rb +476 -0
  11. data/lib/koala/{graph_batch_api.rb → api/graph_batch_api.rb} +20 -16
  12. data/lib/koala/api/graph_collection.rb +107 -0
  13. data/lib/koala/api/legacy.rb +26 -0
  14. data/lib/koala/{rest_api.rb → api/rest_api.rb} +33 -3
  15. data/lib/koala/http_service.rb +69 -19
  16. data/lib/koala/http_service/multipart_request.rb +41 -0
  17. data/lib/koala/http_service/response.rb +18 -0
  18. data/lib/koala/http_service/uploadable_io.rb +187 -0
  19. data/lib/koala/oauth.rb +117 -14
  20. data/lib/koala/realtime_updates.rb +89 -51
  21. data/lib/koala/test_users.rb +109 -33
  22. data/lib/koala/utils.rb +4 -0
  23. data/lib/koala/version.rb +1 -1
  24. data/spec/cases/api_spec.rb +19 -12
  25. data/spec/cases/graph_api_batch_spec.rb +41 -41
  26. data/spec/cases/http_service_spec.rb +1 -22
  27. data/spec/cases/legacy_spec.rb +107 -0
  28. data/spec/cases/multipart_request_spec.rb +5 -5
  29. data/spec/cases/oauth_spec.rb +9 -9
  30. data/spec/cases/realtime_updates_spec.rb +154 -47
  31. data/spec/cases/test_users_spec.rb +268 -219
  32. data/spec/fixtures/mock_facebook_responses.yml +10 -6
  33. data/spec/support/graph_api_shared_examples.rb +17 -12
  34. data/spec/support/koala_test.rb +1 -1
  35. data/spec/support/mock_http_service.rb +2 -2
  36. data/spec/support/rest_api_shared_examples.rb +1 -1
  37. metadata +82 -104
  38. data/lib/koala/batch_operation.rb +0 -74
  39. data/lib/koala/graph_api.rb +0 -289
  40. data/lib/koala/graph_collection.rb +0 -63
  41. data/lib/koala/multipart_request.rb +0 -35
  42. data/lib/koala/uploadable_io.rb +0 -181
  43. data/spec/cases/graph_and_rest_api_spec.rb +0 -22
  44. data/spec/cases/graph_api_spec.rb +0 -22
  45. data/spec/cases/rest_api_spec.rb +0 -22
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