koala 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +62 -2
  5. data/Gemfile +8 -0
  6. data/Rakefile +0 -1
  7. data/autotest/discover.rb +1 -0
  8. data/koala.gemspec +13 -14
  9. data/lib/koala/batch_operation.rb +74 -0
  10. data/lib/koala/graph_api.rb +145 -132
  11. data/lib/koala/graph_batch_api.rb +97 -0
  12. data/lib/koala/graph_collection.rb +59 -0
  13. data/lib/koala/http_service.rb +176 -0
  14. data/lib/koala/oauth.rb +191 -0
  15. data/lib/koala/realtime_updates.rb +23 -29
  16. data/lib/koala/rest_api.rb +13 -8
  17. data/lib/koala/test_users.rb +33 -17
  18. data/lib/koala/uploadable_io.rb +153 -87
  19. data/lib/koala/utils.rb +11 -0
  20. data/lib/koala/version.rb +3 -0
  21. data/lib/koala.rb +59 -217
  22. data/readme.md +92 -53
  23. data/spec/cases/{api_base_spec.rb → api_spec.rb} +31 -6
  24. data/spec/cases/error_spec.rb +32 -0
  25. data/spec/cases/graph_and_rest_api_spec.rb +12 -21
  26. data/spec/cases/graph_api_batch_spec.rb +582 -0
  27. data/spec/cases/graph_api_spec.rb +11 -14
  28. data/spec/cases/graph_collection_spec.rb +116 -0
  29. data/spec/cases/http_service_spec.rb +446 -0
  30. data/spec/cases/koala_spec.rb +54 -0
  31. data/spec/cases/oauth_spec.rb +319 -213
  32. data/spec/cases/realtime_updates_spec.rb +45 -31
  33. data/spec/cases/rest_api_spec.rb +23 -7
  34. data/spec/cases/test_users_spec.rb +123 -75
  35. data/spec/cases/uploadable_io_spec.rb +120 -37
  36. data/spec/cases/utils_spec.rb +10 -0
  37. data/spec/fixtures/cat.m4v +0 -0
  38. data/spec/fixtures/facebook_data.yml +26 -24
  39. data/spec/fixtures/mock_facebook_responses.yml +203 -78
  40. data/spec/spec_helper.rb +30 -5
  41. data/spec/support/graph_api_shared_examples.rb +149 -118
  42. data/spec/support/json_testing_fix.rb +42 -0
  43. data/spec/support/koala_test.rb +187 -0
  44. data/spec/support/mock_http_service.rb +62 -58
  45. data/spec/support/ordered_hash.rb +205 -0
  46. data/spec/support/rest_api_shared_examples.rb +139 -15
  47. data/spec/support/uploadable_io_shared_examples.rb +2 -8
  48. metadata +90 -114
  49. data/lib/koala/http_services.rb +0 -146
  50. data/spec/cases/http_services/http_service_spec.rb +0 -54
  51. data/spec/cases/http_services/net_http_service_spec.rb +0 -350
  52. data/spec/cases/http_services/typhoeus_service_spec.rb +0 -144
  53. data/spec/support/live_testing_data_helper.rb +0 -40
  54. data/spec/support/setup_mocks_or_live.rb +0 -52
data/.autotest ADDED
@@ -0,0 +1,12 @@
1
+ # Override autotest default magic to rerun all tests every time a
2
+ # change is detected on the file system.
3
+ class Autotest
4
+
5
+ def get_to_green
6
+ begin
7
+ rerun_all_tests
8
+ wait_for_changes unless all_good
9
+ end until all_good
10
+ end
11
+
12
+ end
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  pkg
2
2
  .project
3
- Gemfile.lock
3
+ Gemfile.lock
4
+ .rvmrc
5
+ *.rbc
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ rvm:
2
+ - 1.8.7 # (current default)
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - ruby-head
6
+ - rbx
7
+ - rbx-2.0
8
+ - ree
9
+ - jruby
data/CHANGELOG CHANGED
@@ -1,3 +1,62 @@
1
+ v1.2
2
+ New methods:
3
+ -- API is now the main API class, contains both Graph and REST methods
4
+ -- Old classes are aliased with deprecation warnings (non-breaking change)
5
+ -- TestUsers#update lets you update the name or password of an existing test user
6
+ -- API.get_page_access_token lets you easily fetch the access token for a page you manage (thanks, marcgg!)
7
+ -- Added version.rb (Koala::VERSION)
8
+ Updated methods:
9
+ -- OAuth now parses Facebook's new signed cookie format
10
+ -- API.put_picture now accepts URLs to images (thanks, marcgg!)
11
+ -- Bug fixes to put_picture, parse_signed_request, and the test suite (thanks, johnbhall and Will S.!)
12
+ -- Smarter GraphCollection use
13
+ -- Any pageable result will now become a GraphCollection
14
+ -- Non-pageable results from get_connections no longer error
15
+ -- GraphCollection.raw_results allows access to original result data
16
+ -- Koala no longer enforces any limits on the number of test users you create at once
17
+ Internal improvements:
18
+ -- Koala now uses Faraday to make requests, replacing the HTTPServices (see wiki)
19
+ -- Koala::HTTPService.http_options allows specification of default Faraday connection options
20
+ -- Koala::HTTPService.faraday_middleware allows custom middleware configurations
21
+ -- Koala now defaults to Net::HTTP rather than Typhoeus
22
+ -- Koala::NetHTTPService and Koala::TyphoeusService modules no longer exist
23
+ -- Koala no longer automatically switches to Net::HTTP when uploading IO objects to Facebook
24
+ -- RealTimeUpdates and TestUsers are no longer subclasses of API, but have their own .api objects
25
+ -- The old .graph_api accessor is aliases to .api with a deprecation warning
26
+ -- Removed deprecation warnings for pre-1.1 batch interface
27
+ Testing improvements:
28
+ -- Live test suites now run against test users by default
29
+ -- Test suite can be repeatedly run live without having to update facebook_data.yml
30
+ -- OAuth code and session key tests cannot be run against test users
31
+ -- Faraday adapter for live tests can be specified with ADAPTER=[your adapter] in the rspec command
32
+ -- Live tests can be run against the beta server by specifying BETA=true in the rspec command
33
+ -- Tests now pass against all rubies on Travis CI
34
+ -- Expanded and refactored test coverage
35
+ -- Fixed bug with YAML parsing in Ruby 1.9
36
+
37
+ v1.1
38
+ New methods:
39
+ -- Added Batch API support (thanks, seejohnrun and spiegela!)
40
+ -- includes file uploads, error handling, and FQL
41
+ -- Added GraphAPI#put_video
42
+ -- Added GraphAPI#get_comments_for_urls (thanks, amrnt!)
43
+ -- Added RestAPI#fql_multiquery, which simplifies the results (thanks, amrnt!)
44
+ -- HTTP services support global proxy and timeout settings (thanks, itchy!)
45
+ -- Net::HTTP supports global ca_path, ca_file, and verify_mode settings (thanks, spiegela!)
46
+ Updated methods:
47
+ -- RealtimeUpdates now uses a GraphAPI object instead of its own API
48
+ -- RestAPI#rest_call now has an optional last argument for method, for calls requiring POST, DELETE, etc. (thanks, sshilo!)
49
+ -- Filename can now be specified when uploading (e.g. for Ads API) (thanks, sshilo!)
50
+ -- get_objects([]) returns [] instead of a Facebook error in non-batch mode (thanks, aselder!)
51
+ Internal improvements:
52
+ -- Koala is now more compatible with other Rubies (JRuby, Rubinius, etc.)
53
+ -- HTTP services are more modular and can be changed on the fly (thanks, chadk!)
54
+ -- Includes support for uploading StringIOs and other non-files via Net::HTTP even when using TyphoeusService
55
+ -- Koala now uses multi_json to improve compatibility with Rubinius and other Ruby versions
56
+ -- Koala now uses the modern Typhoeus API (thanks, aselder!)
57
+ -- Koala now uses the current modern Net::HTTP interface (thanks, romanbsd!)
58
+ -- Fixed bugs and typos (thanks, waynn, mokevnin, and tikh!)
59
+
1
60
  v1.0
2
61
  New methods:
3
62
  -- Photo and file upload now supported through #put_picture
@@ -6,13 +65,13 @@ New methods:
6
65
  -- Added put_connection and delete_connection convenience methods
7
66
  Updated methods:
8
67
  -- Search can now search places, checkins, etc. (thanks, rickyc!)
9
- -- You can now pass :beta => true in the http options to use Facebook's beta tier.
68
+ -- You can now pass :beta => true in the http options to use Facebook's beta tier
10
69
  -- TestUser#befriend now requires user info hashes (id and access token) due to Facebook API changes (thanks, pulsd and kbighorse!)
11
70
  -- All methods now accept an http_options hash as their optional last parameter (thanks, spiegela!)
12
71
  -- url_for_oauth_code can now take a :display option (thanks, netbe!)
13
72
  -- Net::HTTP can now accept :timeout and :proxy options (thanks, gilles!)
14
73
  -- Test users now supports using test accounts across multiple apps
15
- Internal improvements:
74
+ Internal improvements:
16
75
  -- For public requests, Koala now uses http by default (instead of https) to improve speed
17
76
  -- This can be overridden through Koala.always_use_ssl= or by passing :use_ssl => true in the options hash for an api call
18
77
  -- Read-only REST API requests now go through the faster api-read server
@@ -22,6 +81,7 @@ Internal improvements:
22
81
  -- Updated parse_signed_request to match Facebook's current implementation (thanks, imajes!)
23
82
  -- APIError is now < StandardError, not Exception
24
83
  -- Added KoalaError for non-API errors
84
+ -- Net::HTTP's SSL verification is no longer disabled by default
25
85
  Test improvements:
26
86
  -- Incorporated joshk's awesome rewrite of the entire Koala test suite (thanks, joshk!)
27
87
  -- Expanded HTTP service tests (added Typhoeus test suite and additional Net::HTTP test cases)
data/Gemfile CHANGED
@@ -1,3 +1,11 @@
1
1
  source :rubygems
2
2
 
3
+ group :development, :test do
4
+ gem "typhoeus"
5
+ end
6
+
7
+ if defined? JRUBY_VERSION
8
+ gem "jruby-openssl"
9
+ end
10
+
3
11
  gemspec
data/Rakefile CHANGED
@@ -7,7 +7,6 @@ rescue LoadError
7
7
  puts 'although not required, bundler is recommened for running the tests'
8
8
  end
9
9
 
10
-
11
10
  task :default => :spec
12
11
 
13
12
  require 'rspec/core/rake_task'
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
data/koala.gemspec CHANGED
@@ -1,9 +1,11 @@
1
1
  # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
3
+ require 'koala/version'
2
4
 
3
5
  Gem::Specification.new do |s|
4
6
  s.name = %q{koala}
5
- s.version = "1.0.0"
6
- s.date = %q{2011-05-01}
7
+ s.version = Koala::VERSION
8
+ s.date = %q{2011-09-27}
7
9
 
8
10
  s.summary = %q{A lightweight, flexible library for Facebook with support for the Graph API, the REST API, realtime updates, and OAuth authentication.}
9
11
  s.description = %q{Koala is a lightweight, flexible Ruby SDK for Facebook. It allows read/write access to the social graph via the Graph and REST APIs, as well as support for realtime updates and OAuth and Facebook Connect authentication. Koala is fully tested and supports Net::HTTP and Typhoeus connections out of the box and can accept custom modules for other services.}
@@ -28,23 +30,20 @@ Gem::Specification.new do |s|
28
30
  s.specification_version = 3
29
31
 
30
32
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
31
- s.add_runtime_dependency(%q<json>, ["~> 1.0"])
32
- s.add_runtime_dependency(%q<multipart-post>, ["~> 1.0"])
33
- s.add_development_dependency(%q<rspec>, ["~> 2.5.0"])
33
+ s.add_runtime_dependency(%q<multi_json>, ["~> 1.0"])
34
+ s.add_runtime_dependency(%q<faraday>, ["~> 0.7.0"])
35
+ s.add_development_dependency(%q<rspec>, ["~> 2.5"])
34
36
  s.add_development_dependency(%q<rake>, ["~> 0.8.7"])
35
- s.add_development_dependency(%q<typhoeus>, ["~> 0.2.4"])
36
37
  else
37
- s.add_dependency(%q<json>, ["~> 1.0"])
38
- s.add_dependency(%q<multipart-post>, ["~> 1.0"])
39
- s.add_dependency(%q<rspec>, ["~> 2.5.0"])
38
+ s.add_dependency(%q<multi_json>, ["~> 1.0"])
39
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
40
40
  s.add_dependency(%q<rake>, ["~> 0.8.7"])
41
- s.add_dependency(%q<typhoeus>, ["~> 0.2.4"])
41
+ s.add_dependency(%q<faraday>, ["~> 0.7.0"])
42
42
  end
43
43
  else
44
- s.add_dependency(%q<json>, ["~> 1.0"])
45
- s.add_dependency(%q<multipart-post>, ["~> 1.0"])
46
- s.add_dependency(%q<rspec>, ["~> 2.5.0"])
44
+ s.add_dependency(%q<multi_json>, ["~> 1.0"])
45
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
47
46
  s.add_dependency(%q<rake>, ["~> 0.8.7"])
48
- s.add_dependency(%q<typhoeus>, ["~> 0.2.4"])
47
+ s.add_dependency(%q<faraday>, ["~> 0.7.0"])
49
48
  end
50
49
  end
@@ -0,0 +1,74 @@
1
+ module Koala
2
+ module Facebook
3
+ class BatchOperation
4
+ attr_reader :access_token, :http_options, :post_processing, :files, :batch_api, :identifier
5
+
6
+ @identifier = 0
7
+
8
+ def self.next_identifier
9
+ @identifier += 1
10
+ end
11
+
12
+ def initialize(options = {})
13
+ @identifier = self.class.next_identifier
14
+ @args = (options[:args] || {}).dup # because we modify it below
15
+ @access_token = options[:access_token]
16
+ @http_options = (options[:http_options] || {}).dup # dup because we modify it below
17
+ @batch_args = @http_options.delete(:batch_args) || {}
18
+ @url = options[:url]
19
+ @method = options[:method].to_sym
20
+ @post_processing = options[:post_processing]
21
+
22
+ process_binary_args
23
+
24
+ raise Koala::KoalaError, "Batch operations require an access token, none provided." unless @access_token
25
+ end
26
+
27
+ def to_batch_params(main_access_token)
28
+ # set up the arguments
29
+ args_string = Koala.http_service.encode_params(@access_token == main_access_token ? @args : @args.merge(:access_token => @access_token))
30
+
31
+ response = {
32
+ :method => @method.to_s,
33
+ :relative_url => @url,
34
+ }
35
+
36
+ # handle batch-level arguments, such as name, depends_on, and attached_files
37
+ @batch_args[:attached_files] = @files.keys.join(",") if @files
38
+ response.merge!(@batch_args) if @batch_args
39
+
40
+ # for get and delete, we append args to the URL string
41
+ # otherwise, they go in the body
42
+ if args_string.length > 0
43
+ if args_in_url?
44
+ response[:relative_url] += (@url =~ /\?/ ? "&" : "?") + args_string if args_string.length > 0
45
+ else
46
+ response[:body] = args_string if args_string.length > 0
47
+ end
48
+ end
49
+
50
+ response
51
+ end
52
+
53
+ protected
54
+
55
+ def process_binary_args
56
+ # collect binary files
57
+ @args.each_pair do |key, value|
58
+ if UploadableIO.binary_content?(value)
59
+ @files ||= {}
60
+ # we use a class-level counter to ensure unique file identifiers across multiple batch operations
61
+ # (this is thread safe, since we just care about uniqueness)
62
+ # so remove the file from the original hash and add it to the file store
63
+ id = "op#{identifier}_file#{@files.keys.length}"
64
+ @files[id] = @args.delete(key).is_a?(UploadableIO) ? value : UploadableIO.new(value)
65
+ end
66
+ end
67
+ end
68
+
69
+ def args_in_url?
70
+ @method == :get || @method == :delete
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,53 +1,53 @@
1
1
  module Koala
2
2
  module Facebook
3
- GRAPH_SERVER = "graph.facebook.com"
3
+ GRAPH_SERVER = "graph.facebook.com"
4
4
 
5
5
  module GraphAPIMethods
6
6
  # A client for the Facebook Graph API.
7
- #
7
+ #
8
8
  # See http://github.com/arsduo/koala for Ruby/Koala documentation
9
9
  # and http://developers.facebook.com/docs/api for Facebook API documentation
10
- #
10
+ #
11
11
  # The Graph API is made up of the objects in Facebook (e.g., people, pages,
12
12
  # events, photos) and the connections between them (e.g., friends,
13
13
  # photo tags, and event RSVPs). This client provides access to those
14
14
  # primitive types in a generic way. For example, given an OAuth access
15
15
  # token, this will fetch the profile of the active user and the list
16
16
  # of the user's friends:
17
- #
18
- # graph = Koala::Facebook::GraphAPI.new(access_token)
17
+ #
18
+ # graph = Koala::Facebook::API.new(access_token)
19
19
  # user = graph.get_object("me")
20
20
  # friends = graph.get_connections(user["id"], "friends")
21
- #
21
+ #
22
22
  # You can see a list of all of the objects and connections supported
23
23
  # by the API at http://developers.facebook.com/docs/reference/api/.
24
- #
24
+ #
25
25
  # You can obtain an access token via OAuth or by using the Facebook
26
26
  # JavaScript SDK. See the Koala and Facebook documentation for more information.
27
- #
27
+ #
28
28
  # If you are using the JavaScript SDK, you can use the
29
29
  # Koala::Facebook::OAuth.get_user_from_cookie() method below to get the OAuth access token
30
30
  # for the active user from the cookie saved by the SDK.
31
-
31
+
32
32
  # Objects
33
33
 
34
34
  def get_object(id, args = {}, options = {})
35
35
  # Fetchs the given object from the graph.
36
36
  graph_call(id, args, "get", options)
37
37
  end
38
-
38
+
39
39
  def get_objects(ids, args = {}, options = {})
40
- # Fetchs all of the given object from the graph.
41
- # We return a map from ID to object. If any of the IDs are invalid,
42
- # we raise an exception.
43
- graph_call("", args.merge("ids" => ids.join(",")), "get", options)
40
+ # Fetchs all of the given objects from the graph.
41
+ # If any of the IDs are invalid, they'll raise an exception.
42
+ return [] if ids.empty?
43
+ graph_call("", args.merge("ids" => ids.respond_to?(:join) ? ids.join(",") : ids), "get", options)
44
44
  end
45
-
45
+
46
46
  def put_object(parent_object, connection_name, args = {}, options = {})
47
47
  # Writes the given object to the graph, connected to the given parent.
48
48
  # See http://developers.facebook.com/docs/api#publishing for all of
49
49
  # the supported writeable objects.
50
- #
50
+ #
51
51
  # For example,
52
52
  # graph.put_object("me", "feed", :message => "Hello, world")
53
53
  # writes "Hello, world" to the active user's wall.
@@ -56,7 +56,7 @@ module Koala
56
56
  # publishing wall posts requires the "publish_stream" permission. See
57
57
  # http://developers.facebook.com/docs/authentication/ for details about
58
58
  # extended permissions.
59
-
59
+
60
60
  raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
61
61
  graph_call("#{parent_object}/#{connection_name}", args, "post", options)
62
62
  end
@@ -66,13 +66,12 @@ module Koala
66
66
  raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Delete requires an access token"}) unless @access_token
67
67
  graph_call(id, {}, "delete", options)
68
68
  end
69
-
69
+
70
70
  # Connections
71
-
71
+
72
72
  def get_connections(id, connection_name, args = {}, options = {})
73
73
  # Fetchs the connections for given object.
74
- result = graph_call("#{id}/#{connection_name}", args, "get", options)
75
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
74
+ graph_call("#{id}/#{connection_name}", args, "get", options)
76
75
  end
77
76
 
78
77
  def put_connections(id, connection_name, args = {}, options = {})
@@ -87,55 +86,56 @@ module Koala
87
86
  graph_call("#{id}/#{connection_name}", args, "delete", options)
88
87
  end
89
88
 
90
- # Pictures
91
- # to delete pictures, use delete_object(photo_id)
92
- # note: you'll need the user_photos permission to actually access photos after uploading them
93
-
89
+ # Media (photos and videos)
90
+ # to delete photos or videos, use delete_object(object_id)
91
+ # note: you'll need the user_photos or user_videos permissions to actually access media after upload
92
+
94
93
  def get_picture(object, args = {}, options = {})
95
94
  # Gets a picture object, returning the URL (which Facebook sends as a header)
96
- result = graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers))
97
- result["Location"]
98
- end
99
-
95
+ graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
96
+ result["Location"]
97
+ end
98
+ end
99
+
100
+ # Can be called in multiple ways:
101
+ #
102
+ # put_picture(file, [content_type], ...)
103
+ # put_picture(path_to_file, [content_type], ...)
104
+ # put_picture(picture_url, ...)
105
+ #
106
+ # You can pass in uploaded files directly from Rails or Sinatra.
107
+ # (See lib/koala/uploadable_io.rb for supported frameworks)
108
+ #
109
+ # Optional parameters can be added to the end of the argument list:
110
+ # - args: a hash of request parameters (default: {})
111
+ # - target_id: ID of the target where to post the picture (default: "me")
112
+ # - options: a hash of http options passed to the HTTPService module
113
+ #
114
+ # put_picture(file, content_type, {:message => "Message"}, 01234560)
115
+ # put_picture(params[:file], {:message => "Message"})
116
+ #
117
+ # (Note that with URLs, there's no optional content type field)
118
+ # put_picture(picture_url, {:message => "Message"}, my_page_id)
119
+
100
120
  def put_picture(*picture_args)
101
- # Can be called in multiple ways:
102
- #
103
- # put_picture(file, [content_type], ...)
104
- # put_picture(path_to_file, [content_type], ...)
105
- #
106
- # You can pass in uploaded files directly from Rails or Sinatra.
107
- # (See lib/koala/uploadable_io.rb for supported frameworks)
108
- #
109
- # Optional parameters can be added to the end of the argument list:
110
- # - args: a hash of request parameters (default: {})
111
- # - target_id: ID of the target where to post the picture (default: "me")
112
- # - options: a hash of http options passed to the HTTPService module
113
- #
114
- # put_picture(file, content_type, {:message => "Message"}, 01234560)
115
- # put_picture(params[:file], {:message => "Message"})
116
-
117
- raise KoalaError.new("Wrong number of arguments for put_picture") unless picture_args.size.between?(1, 5)
118
-
119
- args_offset = picture_args[1].kind_of?(Hash) || picture_args.size == 1 ? 0 : 1
120
-
121
- args = picture_args[1 + args_offset] || {}
122
- target_id = picture_args[2 + args_offset] || "me"
123
- options = picture_args[3 + args_offset] || {}
124
-
125
- args["source"] = Koala::UploadableIO.new(*picture_args.slice(0, 1 + args_offset))
126
-
127
- self.put_object(target_id, "photos", args, options)
121
+ put_object(*parse_media_args(picture_args, "photos"))
122
+ end
123
+
124
+ def put_video(*video_args)
125
+ args = parse_media_args(video_args, "videos")
126
+ args.last[:video] = true
127
+ put_object(*args)
128
128
  end
129
-
129
+
130
130
  # Wall posts
131
131
  # To get wall posts, use get_connections(user, "feed")
132
132
  # To delete a wall post, just use delete_object(post_id)
133
-
133
+
134
134
  def put_wall_post(message, attachment = {}, profile_id = "me", options = {})
135
135
  # attachment is a hash describing the wall post
136
136
  # (see X for more details)
137
- # For instance,
138
- #
137
+ # For instance,
138
+ #
139
139
  # {"name" => "Link name"
140
140
  # "link" => "http://www.example.com/",
141
141
  # "caption" => "{*actor*} posted a new review",
@@ -144,19 +144,19 @@ module Koala
144
144
 
145
145
  self.put_object(profile_id, "feed", attachment.merge({:message => message}), options)
146
146
  end
147
-
147
+
148
148
  # Comments
149
149
  # to delete comments, use delete_object(comment_id)
150
150
  # to get comments, use get_connections(object, "likes")
151
-
151
+
152
152
  def put_comment(object_id, message, options = {})
153
153
  # Writes the given comment on the given post.
154
154
  self.put_object(object_id, "comments", {:message => message}, options)
155
155
  end
156
-
156
+
157
157
  # Likes
158
158
  # to get likes, use get_connections(user, "likes")
159
-
159
+
160
160
  def put_like(object_id, options = {})
161
161
  # Likes the given post.
162
162
  self.put_object(object_id, "likes", {}, options)
@@ -169,89 +169,102 @@ module Koala
169
169
  end
170
170
 
171
171
  # Search
172
-
172
+
173
173
  def search(search_terms, args = {}, options = {})
174
174
  args.merge!({:q => search_terms}) unless search_terms.nil?
175
- result = graph_call("search", args, "get", options)
176
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
177
- end
178
-
179
- # API access
180
-
181
- def graph_call(*args)
182
- # Direct access to the Facebook API
183
- # see any of the above methods for example invocations
184
- response = api(*args) do |response|
185
- # check for Graph API-specific errors
186
- if response.is_a?(Hash) && error_details = response["error"]
187
- raise APIError.new(error_details)
188
- end
175
+ graph_call("search", args, "get", options)
176
+ end
177
+
178
+ # Convenience Methods
179
+
180
+ def get_page_access_token(object_id)
181
+ result = get_object(object_id, :fields => "access_token") do
182
+ result ? result["access_token"] : nil
189
183
  end
190
-
191
- response
192
- end
193
-
184
+ end
185
+
186
+ def get_comments_for_urls(urls = [], args = {}, options = {})
187
+ # Fetchs the comments for given URLs (array or comma-separated string)
188
+ # see https://developers.facebook.com/blog/post/490
189
+ return [] if urls.empty?
190
+ args.merge!(:ids => urls.respond_to?(:join) ? urls.join(",") : urls)
191
+ get_object("comments", args, options)
192
+ end
193
+
194
194
  # GraphCollection support
195
-
196
195
  def get_page(params)
197
196
  # Pages through a set of results stored in a GraphCollection
198
197
  # Used for connections and search results
199
- result = graph_call(*params)
200
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
198
+ graph_call(*params)
201
199
  end
202
-
203
- end
204
-
205
-
206
- class GraphCollection < Array
207
- #This class is a light wrapper for collections returned
208
- #from the Graph API.
209
- #
210
- #It extends Array to allow direct access to the data colleciton
211
- #which should allow it to drop in seamlessly.
212
- #
213
- #It also allows access to paging information and the
214
- #ability to get the next/previous page in the collection
215
- #by calling next_page or previous_page.
216
- attr_reader :paging
217
- attr_reader :api
218
-
219
- def initialize(response, api)
220
- super response["data"]
221
- @paging = response["paging"]
222
- @api = api
200
+
201
+ # Batch API
202
+ def batch(http_options = {}, &block)
203
+ batch_client = GraphBatchAPI.new(access_token, self)
204
+ if block
205
+ yield batch_client
206
+ batch_client.execute(http_options)
207
+ else
208
+ batch_client
209
+ end
223
210
  end
224
-
225
- # defines methods for NEXT and PREVIOUS pages
226
- %w{next previous}.each do |this|
227
-
228
- # def next_page
229
- # def previous_page
230
- define_method "#{this.to_sym}_page" do
231
- base, args = send("#{this}_page_params")
232
- base ? @api.get_page([base, args]) : nil
211
+
212
+ # Direct access to the Facebook API
213
+ # see any of the above methods for example invocations
214
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
215
+ result = api(path, args, verb, options) do |response|
216
+ error = check_response(response)
217
+ raise error if error
233
218
  end
219
+
220
+ # turn this into a GraphCollection if it's pageable
221
+ result = GraphCollection.evaluate(result, self)
234
222
 
235
- # def next_page_params
236
- # def previous_page_params
237
- define_method "#{this.to_sym}_page_params" do
238
- return nil unless @paging and @paging[this]
239
- parse_page_url(@paging[this])
240
- end
223
+ # now process as appropriate for the given call (get picture header, etc.)
224
+ post_processing ? post_processing.call(result) : result
241
225
  end
226
+
227
+ private
242
228
 
243
- def parse_page_url(url)
244
- match = url.match(/.com\/(.*)\?(.*)/)
245
- base = match[1]
246
- args = match[2]
247
- params = CGI.parse(args)
248
- new_params = {}
249
- params.each_pair do |key,value|
250
- new_params[key] = value.join ","
229
+ def check_response(response)
230
+ # check for Graph API-specific errors
231
+ # this returns an error, which is immediately raised (non-batch)
232
+ # or added to the list of batch results (batch)
233
+ if response.is_a?(Hash) && error_details = response["error"]
234
+ APIError.new(error_details)
235
+ end
236
+ end
237
+
238
+ def parse_media_args(media_args, method)
239
+ # photo and video uploads can accept different types of arguments (see above)
240
+ # so here, we parse the arguments into a form directly usable in put_object
241
+ raise KoalaError.new("Wrong number of arguments for put_#{method == "photos" ? "picture" : "video"}") unless media_args.size.between?(1, 5)
242
+
243
+ args_offset = media_args[1].kind_of?(Hash) || media_args.size == 1 ? 0 : 1
244
+
245
+ args = media_args[1 + args_offset] || {}
246
+ target_id = media_args[2 + args_offset] || "me"
247
+ options = media_args[3 + args_offset] || {}
248
+
249
+ if url?(media_args.first)
250
+ # If media_args is a URL, we can upload without UploadableIO
251
+ args.merge!(:url => media_args.first)
252
+ else
253
+ args["source"] = Koala::UploadableIO.new(*media_args.slice(0, 1 + args_offset))
254
+ end
255
+
256
+ [target_id, method, args, options]
257
+ end
258
+
259
+ def url?(data)
260
+ return false unless data.is_a? String
261
+ begin
262
+ uri = URI.parse(data)
263
+ %w( http https ).include?(uri.scheme)
264
+ rescue URI::BadURIError
265
+ false
251
266
  end
252
- [base,new_params]
253
267
  end
254
-
255
268
  end
256
269
  end
257
270
  end