tyler_koala 1.2.0beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +5 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +185 -0
  5. data/Gemfile +11 -0
  6. data/LICENSE +22 -0
  7. data/Manifest +39 -0
  8. data/Rakefile +16 -0
  9. data/autotest/discover.rb +1 -0
  10. data/koala.gemspec +50 -0
  11. data/lib/koala.rb +119 -0
  12. data/lib/koala/batch_operation.rb +74 -0
  13. data/lib/koala/graph_api.rb +281 -0
  14. data/lib/koala/graph_batch_api.rb +87 -0
  15. data/lib/koala/graph_collection.rb +54 -0
  16. data/lib/koala/http_service.rb +161 -0
  17. data/lib/koala/oauth.rb +181 -0
  18. data/lib/koala/realtime_updates.rb +89 -0
  19. data/lib/koala/rest_api.rb +95 -0
  20. data/lib/koala/test_users.rb +102 -0
  21. data/lib/koala/uploadable_io.rb +180 -0
  22. data/lib/koala/utils.rb +7 -0
  23. data/readme.md +160 -0
  24. data/spec/cases/api_base_spec.rb +101 -0
  25. data/spec/cases/error_spec.rb +30 -0
  26. data/spec/cases/graph_and_rest_api_spec.rb +48 -0
  27. data/spec/cases/graph_api_batch_spec.rb +600 -0
  28. data/spec/cases/graph_api_spec.rb +42 -0
  29. data/spec/cases/http_service_spec.rb +420 -0
  30. data/spec/cases/koala_spec.rb +21 -0
  31. data/spec/cases/oauth_spec.rb +428 -0
  32. data/spec/cases/realtime_updates_spec.rb +198 -0
  33. data/spec/cases/rest_api_spec.rb +41 -0
  34. data/spec/cases/test_users_spec.rb +281 -0
  35. data/spec/cases/uploadable_io_spec.rb +206 -0
  36. data/spec/cases/utils_spec.rb +8 -0
  37. data/spec/fixtures/beach.jpg +0 -0
  38. data/spec/fixtures/cat.m4v +0 -0
  39. data/spec/fixtures/facebook_data.yml +61 -0
  40. data/spec/fixtures/mock_facebook_responses.yml +439 -0
  41. data/spec/spec_helper.rb +43 -0
  42. data/spec/support/graph_api_shared_examples.rb +502 -0
  43. data/spec/support/json_testing_fix.rb +42 -0
  44. data/spec/support/koala_test.rb +163 -0
  45. data/spec/support/mock_http_service.rb +98 -0
  46. data/spec/support/ordered_hash.rb +205 -0
  47. data/spec/support/rest_api_shared_examples.rb +285 -0
  48. data/spec/support/uploadable_io_shared_examples.rb +70 -0
  49. metadata +221 -0
@@ -0,0 +1,102 @@
1
+ require 'koala'
2
+
3
+ module Koala
4
+ module Facebook
5
+ module TestUserMethods
6
+
7
+ def self.included(base)
8
+ base.class_eval do
9
+ # make the Graph API accessible in case someone wants to make other calls to interact with their users
10
+ attr_reader :api, :app_id, :app_access_token, :secret
11
+ end
12
+ end
13
+
14
+ def initialize(options = {})
15
+ @app_id = options[:app_id]
16
+ @app_access_token = options[:app_access_token]
17
+ @secret = options[:secret]
18
+ unless @app_id && (@app_access_token || @secret) # make sure we have what we need
19
+ raise ArgumentError, "Initialize must receive a hash with :app_id and either :app_access_token or :secret! (received #{options.inspect})"
20
+ end
21
+
22
+ # fetch the access token if we're provided a secret
23
+ if @secret && !@app_access_token
24
+ oauth = Koala::Facebook::OAuth.new(@app_id, @secret)
25
+ @app_access_token = oauth.get_app_access_token
26
+ end
27
+ @api = API.new(@app_access_token)
28
+ end
29
+
30
+ def create(installed, permissions = nil, args = {}, options = {})
31
+ # Creates and returns a test user
32
+ args['installed'] = installed
33
+ args['permissions'] = (permissions.is_a?(Array) ? permissions.join(",") : permissions) if installed
34
+ result = @api.graph_call(accounts_path, args, "post", options)
35
+ end
36
+
37
+ def list
38
+ @api.graph_call(accounts_path)["data"]
39
+ end
40
+
41
+ def delete(test_user)
42
+ test_user = test_user["id"] if test_user.is_a?(Hash)
43
+ @api.delete_object(test_user)
44
+ end
45
+
46
+ def delete_all
47
+ list.each {|u| delete u}
48
+ end
49
+
50
+ def update(test_user, args = {}, http_options = {})
51
+ test_user = test_user["id"] if test_user.is_a?(Hash)
52
+ @api.graph_call(test_user, args, "post", http_options)
53
+ end
54
+
55
+ def befriend(user1_hash, user2_hash)
56
+ user1_id = user1_hash["id"] || user1_hash[:id]
57
+ user2_id = user2_hash["id"] || user2_hash[:id]
58
+ user1_token = user1_hash["access_token"] || user1_hash[:access_token]
59
+ user2_token = user2_hash["access_token"] || user2_hash[:access_token]
60
+ unless user1_id && user2_id && user1_token && user2_token
61
+ # we explicitly raise an error here to minimize the risk of confusing output
62
+ # if you pass in a string (as was previously supported) no local exception would be raised
63
+ # but the Facebook call would fail
64
+ raise ArgumentError, "TestUsers#befriend requires hash arguments for both users with id and access_token"
65
+ end
66
+
67
+ u1_graph_api = API.new(user1_token)
68
+ u2_graph_api = API.new(user2_token)
69
+
70
+ u1_graph_api.graph_call("#{user1_id}/friends/#{user2_id}", {}, "post") &&
71
+ u2_graph_api.graph_call("#{user2_id}/friends/#{user1_id}", {}, "post")
72
+ end
73
+
74
+ def create_network(network_size, installed = true, permissions = '')
75
+ network_size = 50 if network_size > 50 # FB's max is 50
76
+ users = (0...network_size).collect { create(installed, permissions) }
77
+ friends = users.clone
78
+ users.each do |user|
79
+ # Remove this user from list of friends
80
+ friends.delete_at(0)
81
+ # befriend all the others
82
+ friends.each do |friend|
83
+ befriend(user, friend)
84
+ end
85
+ end
86
+ return users
87
+ end
88
+
89
+ def graph_api
90
+ Koala::Utils.deprecate("the TestUsers.graph_api accessor is deprecated and will be removed in a future version; please use .api instead.")
91
+ @api
92
+ end
93
+
94
+ protected
95
+
96
+ def accounts_path
97
+ @accounts_path ||= "/#{@app_id}/accounts/test-users"
98
+ end
99
+
100
+ end # TestUserMethods
101
+ end # Facebook
102
+ end # Koala
@@ -0,0 +1,180 @@
1
+ require "net/http/post/multipart"
2
+
3
+ module Koala
4
+ class UploadableIO
5
+ attr_reader :io_or_path, :content_type, :filename
6
+
7
+ def initialize(io_or_path_or_mixed, content_type = nil, filename = nil)
8
+ # see if we got the right inputs
9
+ parse_init_mixed_param io_or_path_or_mixed, content_type
10
+
11
+ # filename is used in the Ads API
12
+ # if it's provided, take precedence over the detected filename
13
+ # otherwise, fall back to a dummy name
14
+ @filename = filename || @filename || "koala-io-file.dum"
15
+
16
+ raise KoalaError.new("Invalid arguments to initialize an UploadableIO") unless @io_or_path
17
+ raise KoalaError.new("Unable to determine MIME type for UploadableIO") if !@content_type
18
+ end
19
+
20
+ def to_upload_io
21
+ UploadIO.new(@io_or_path, @content_type, @filename)
22
+ end
23
+
24
+ def to_file
25
+ @io_or_path.is_a?(String) ? File.open(@io_or_path) : @io_or_path
26
+ end
27
+
28
+ def self.binary_content?(content)
29
+ content.is_a?(UploadableIO) || DETECTION_STRATEGIES.detect {|method| send(method, content)}
30
+ end
31
+
32
+ private
33
+ DETECTION_STRATEGIES = [
34
+ :sinatra_param?,
35
+ :rails_3_param?,
36
+ :file_param?
37
+ ]
38
+
39
+ PARSE_STRATEGIES = [
40
+ :parse_rails_3_param,
41
+ :parse_sinatra_param,
42
+ :parse_file_object,
43
+ :parse_string_path
44
+ ]
45
+
46
+ def parse_init_mixed_param(mixed, content_type = nil)
47
+ PARSE_STRATEGIES.each do |method|
48
+ send(method, mixed, content_type)
49
+ return if @io_or_path && @content_type
50
+ end
51
+ end
52
+
53
+ # Expects a parameter of type ActionDispatch::Http::UploadedFile
54
+ def self.rails_3_param?(uploaded_file)
55
+ uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
56
+ end
57
+
58
+ def parse_rails_3_param(uploaded_file, content_type = nil)
59
+ if UploadableIO.rails_3_param?(uploaded_file)
60
+ @io_or_path = uploaded_file.tempfile.path
61
+ @content_type = content_type || uploaded_file.content_type
62
+ @filename = uploaded_file.original_filename
63
+ end
64
+ end
65
+
66
+ # Expects a Sinatra hash of file info
67
+ def self.sinatra_param?(file_hash)
68
+ file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
69
+ end
70
+
71
+ def parse_sinatra_param(file_hash, content_type = nil)
72
+ if UploadableIO.sinatra_param?(file_hash)
73
+ @io_or_path = file_hash[:tempfile]
74
+ @content_type = content_type || file_hash[:type] || detect_mime_type(tempfile)
75
+ @filename = file_hash[:filename]
76
+ end
77
+ end
78
+
79
+ # takes a file object
80
+ def self.file_param?(file)
81
+ file.kind_of?(File)
82
+ end
83
+
84
+ def parse_file_object(file, content_type = nil)
85
+ if UploadableIO.file_param?(file)
86
+ @io_or_path = file
87
+ @content_type = content_type || detect_mime_type(file.path)
88
+ @filename = File.basename(file.path)
89
+ end
90
+ end
91
+
92
+ def parse_string_path(path, content_type = nil)
93
+ if path.kind_of?(String)
94
+ @io_or_path = path
95
+ @content_type = content_type || detect_mime_type(path)
96
+ @filename = File.basename(path)
97
+ end
98
+ end
99
+
100
+ def parse_io(io, content_type = nil)
101
+ if io.respond_to?(:read)
102
+ @io_or_path = io
103
+ @content_type = content_type
104
+ end
105
+ end
106
+
107
+ MIME_TYPE_STRATEGIES = [
108
+ :use_mime_module,
109
+ :use_simple_detection
110
+ ]
111
+
112
+ def detect_mime_type(filename)
113
+ if filename
114
+ MIME_TYPE_STRATEGIES.each do |method|
115
+ result = send(method, filename)
116
+ return result if result
117
+ end
118
+ end
119
+ nil # if we can't find anything
120
+ end
121
+
122
+ def use_mime_module(filename)
123
+ # if the user has installed mime/types, we can use that
124
+ # if not, rescue and return nil
125
+ begin
126
+ type = MIME::Types.type_for(filename).first
127
+ type ? type.to_s : nil
128
+ rescue
129
+ nil
130
+ end
131
+ end
132
+
133
+ def use_simple_detection(filename)
134
+ # very rudimentary extension analysis for images
135
+ # first, get the downcased extension, or an empty string if it doesn't exist
136
+ extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
137
+ case extension
138
+ when ""
139
+ nil
140
+ # images
141
+ when "jpg", "jpeg"
142
+ "image/jpeg"
143
+ when "png"
144
+ "image/png"
145
+ when "gif"
146
+ "image/gif"
147
+
148
+ # video
149
+ when "3g2"
150
+ "video/3gpp2"
151
+ when "3gp", "3gpp"
152
+ "video/3gpp"
153
+ when "asf"
154
+ "video/x-ms-asf"
155
+ when "avi"
156
+ "video/x-msvideo"
157
+ when "flv"
158
+ "video/x-flv"
159
+ when "m4v"
160
+ "video/x-m4v"
161
+ when "mkv"
162
+ "video/x-matroska"
163
+ when "mod"
164
+ "video/mod"
165
+ when "mov", "qt"
166
+ "video/quicktime"
167
+ when "mp4", "mpeg4"
168
+ "video/mp4"
169
+ when "mpe", "mpeg", "mpg", "tod", "vob"
170
+ "video/mpeg"
171
+ when "nsv"
172
+ "application/x-winamp"
173
+ when "ogm", "ogv"
174
+ "video/ogg"
175
+ when "wmv"
176
+ "video/x-ms-wmv"
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,7 @@
1
+ module Koala
2
+ module Utils
3
+ def self.deprecate(message)
4
+ send(:warn, "KOALA: Deprecation warning: #{message}")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,160 @@
1
+ Koala
2
+ ====
3
+ [Koala](http://github.com/arsduo/koala) is a Facebook library for Ruby, supporting the Graph API (including the batch requests and photo uploads), the REST API, realtime updates, test users, and OAuth validation. We wrote Koala with four goals:
4
+
5
+ * Lightweight: Koala should be as light and simple as Facebook’s own libraries, providing API accessors and returning simple JSON.
6
+ * Fast: Koala should, out of the box, be quick. Out of the box, we use Facebook's faster read-only servers when possible and if available, the Typhoeus gem to make snappy Facebook requests. Of course, that brings us to our next topic:
7
+ * Flexible: Koala should be useful to everyone, regardless of their current configuration. (We support JRuby, Rubinius, and REE as well as vanilla Ruby, and use the Faraday library to provide complete flexibility over how HTTP requests are made.)
8
+ * Tested: Koala should have complete test coverage, so you can rely on it. (Our test coverage is complete and can be run against either mocked responses or the live Facebook servers.)
9
+
10
+ Installation
11
+ ---
12
+
13
+ Easy:
14
+
15
+ [sudo|rvm] gem install koala --pre # for 1.2 beta
16
+ [sudo|rvm] gem install koala # for 1.1
17
+
18
+ Or in Bundler:
19
+
20
+ gem "koala", "~> 1.2.0beta"
21
+ gem "koala" # for 1.1
22
+
23
+ Graph API
24
+ ----
25
+ The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
26
+
27
+ @graph = Koala::Facebook::API.new(oauth_access_token)
28
+ profile = @graph.get_object("me")
29
+ friends = @graph.get_connections("me", "friends")
30
+ @graph.put_object("me", "feed", :message => "I am writing on my wall!")
31
+
32
+ The response of most requests is the JSON data returned from the Facebook servers as a Hash.
33
+
34
+ When retrieving data that returns an array of results (for example, when calling API#get_connections or API#search) a GraphCollection object will be returned, which makes it easy to page through the results:
35
+
36
+ # Returns the feed items for the currently logged-in user as a GraphCollection
37
+ feed = @graph.get_connections("me", "feed")
38
+ feed.each {|f| do_something_with_item(f) } # it's a subclass of Array
39
+ next_feed = feed.next_page
40
+
41
+ # You can also get an array describing the URL for the next page: [path, arguments]
42
+ # This is useful for storing page state across multiple browser requests
43
+ next_page_params = feed.next_page_params
44
+ page = @graph.get_page(next_page_params)
45
+
46
+ You can also make multiple calls at once using Facebook's batch API:
47
+
48
+ # Returns an array of results as if they were called non-batch
49
+ @graph.batch do |batch_api|
50
+ batch_api.get_object('me')
51
+ batch_api.put_wall_post('Making a post in a batch.')
52
+ end
53
+
54
+ Check out the wiki for more details and examples.
55
+
56
+ The REST API
57
+ -----
58
+ Where the Graph API and the old REST API overlap, you should choose the Graph API. Unfortunately, that overlap is far from complete, and there are many important API calls that can't yet be done via the Graph.
59
+
60
+ Fortunately, Koala supports the REST API using the very same interface; to use this, instantiate an API:
61
+
62
+ @rest = Koala::Facebook::API.new(oauth_access_token)
63
+ @rest.fql_query(my_fql_query) # convenience method
64
+ @rest.fql_multiquery(fql_query_hash) # convenience method
65
+ @rest.rest_call("stream.publish", arguments_hash) # generic version
66
+
67
+ Of course, you can use the Graph API methods on the same object -- the power of two APIs right in the palm of your hand.
68
+
69
+ @api = Koala::Facebook::API.new(oauth_access_token)
70
+ fql = @api.fql_query(my_fql_query)
71
+ @api.put_wall_post(process_result(fql))
72
+
73
+
74
+ OAuth
75
+ -----
76
+ You can use the Graph and REST APIs without an OAuth access token, but the real magic happens when you provide Facebook an OAuth token to prove you're authenticated. Koala provides an OAuth class to make that process easy:
77
+ @oauth = Koala::Facebook::OAuth.new(app_id, app_secret, callback_url)
78
+
79
+ If your application uses Koala and the Facebook [JavaScript SDK](http://github.com/facebook/connect-js) (formerly Facebook Connect), you can use the OAuth class to parse the cookies:
80
+ @oauth.get_user_from_cookies(cookies) # gets the user's ID
81
+ @oauth.get_user_info_from_cookies(cookies) # parses and returns the entire hash
82
+
83
+ And if you have to use the more complicated [redirect-based OAuth process](http://developers.facebook.com/docs/authentication/), Koala helps out there, too:
84
+ # generate authenticating URL
85
+ @oauth.url_for_oauth_code
86
+ # fetch the access token once you have the code
87
+ @oauth.get_access_token(code)
88
+
89
+ You can also get your application's own access token, which can be used without a user session for subscriptions and certain other requests:
90
+ @oauth.get_app_access_token
91
+
92
+ For those building apps on Facebook, parsing signed requests is simple:
93
+ @oauth.parse_signed_request(request)
94
+
95
+ Or, if for some horrible reason, you're still using session keys, despair not! It's easy to turn them into shiny, modern OAuth tokens:
96
+ @oauth.get_token_from_session_key(session_key)
97
+ @oauth.get_tokens_from_session_keys(array_of_session_keys)
98
+
99
+ That's it! It's pretty simple once you get the hang of it. If you're new to OAuth, though, check out the wiki and the OAuth Playground example site (see below).
100
+
101
+ Real-time Updates
102
+ -----
103
+ Sometimes, reaching out to Facebook is a pain -- let it reach out to you instead. The Graph API allows your application to subscribe to real-time updates for certain objects in the graph; check the [official Facebook documentation](http://developers.facebook.com/docs/api/realtime) for more details on what objects you can subscribe to and what limitations may apply.
104
+
105
+ Koala makes it easy to interact with your applications using the RealtimeUpdates class:
106
+
107
+ @updates = Koala::Facebook::RealtimeUpdates.new(:app_id => app_id, :secret => secret)
108
+
109
+ You can do just about anything with your real-time update subscriptions using the RealtimeUpdates class:
110
+
111
+ # Add/modify a subscription to updates for when the first_name or last_name fields of any of your users is changed
112
+ @updates.subscribe("user", "first_name, last_name", callback_token, verify_token)
113
+
114
+ # Get an array of your current subscriptions (one hash for each object you've subscribed to)
115
+ @updates.list_subscriptions
116
+
117
+ # Unsubscribe from updates for an object
118
+ @updates.unsubscribe("user")
119
+
120
+ And to top it all off, RealtimeUpdates provides a static method to respond to Facebook servers' verification of your callback URLs:
121
+
122
+ # Returns the hub.challenge parameter in params if the verify token in params matches verify_token
123
+ Koala::Facebook::RealtimeUpdates.meet_challenge(params, your_verify_token)
124
+
125
+ For more information about meet_challenge and the RealtimeUpdates class, check out the Real-Time Updates page on the wiki.
126
+
127
+ Test Users
128
+ -----
129
+
130
+ We also support the test users API, allowing you to conjure up fake users and command them to do your bidding using the Graph or REST API:
131
+
132
+ @test_users = Koala::Facebook::TestUsers.new(:app_id => id, :secret => secret)
133
+ user = @test_users.create(is_app_installed, desired_permissions)
134
+ user_graph_api = Koala::Facebook::API.new(user["access_token"])
135
+ # or, if you want to make a whole community:
136
+ @test_users.create_network(network_size, is_app_installed, common_permissions)
137
+
138
+ See examples, ask questions
139
+ -----
140
+ Some resources to help you as you play with Koala and the Graph API:
141
+
142
+ * Complete Koala documentation <a href="http://wiki.github.com/arsduo/koala/">on the wiki</a>
143
+ * The <a href="http://groups.google.com/group/koala-users">Koala users group</a> on Google Groups, the place for your Koala and API questions
144
+ * The Koala-powered <a href="http://oauth.twoalex.com" target="_blank">OAuth Playground</a>, where you can easily generate OAuth access tokens and any other data needed to test out the APIs or OAuth
145
+
146
+ Testing
147
+ -----
148
+
149
+ Unit tests are provided for all of Koala's methods. By default, these tests run against mock responses and hence are ready out of the box:
150
+
151
+ # From anywhere in the project directory:
152
+ bundle exec rake spec
153
+
154
+
155
+ You can also run live tests against Facebook's servers:
156
+
157
+ # Again from anywhere in the project directory:
158
+ LIVE=true bundle exec rake spec
159
+
160
+ By default, the live tests are run against test users, so you can run them as frequently as you want. If you want to run them against a real user, however, you can fill in the OAuth token, code, and access\_token values in spec/fixtures/facebook_data.yml. See the wiki for more details.