koala 1.0.0.beta → 1.0.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.
- data/.gitignore +3 -0
- data/CHANGELOG +17 -3
- data/Gemfile +3 -0
- data/LICENSE +1 -1
- data/Manifest +5 -2
- data/Rakefile +13 -14
- data/koala.gemspec +35 -20
- data/lib/koala/graph_api.rb +183 -131
- data/lib/koala/http_services.rb +46 -55
- data/lib/koala/test_users.rb +21 -8
- data/lib/koala/uploadable_io.rb +115 -0
- data/lib/koala.rb +52 -84
- data/readme.md +19 -6
- data/spec/cases/api_base_spec.rb +101 -0
- data/spec/cases/graph_and_rest_api_spec.rb +31 -0
- data/spec/cases/graph_api_spec.rb +25 -0
- data/spec/cases/http_services/http_service_spec.rb +54 -0
- data/spec/cases/http_services/net_http_service_spec.rb +350 -0
- data/spec/cases/http_services/typhoeus_service_spec.rb +144 -0
- data/spec/cases/oauth_spec.rb +409 -0
- data/spec/cases/realtime_updates_spec.rb +184 -0
- data/spec/cases/rest_api_spec.rb +25 -0
- data/spec/{koala/test_users/test_users_tests.rb → cases/test_users_spec.rb} +46 -33
- data/spec/cases/uploadable_io_spec.rb +151 -0
- data/spec/{facebook_data.yml → fixtures/facebook_data.yml} +12 -14
- data/spec/{mock_facebook_responses.yml → fixtures/mock_facebook_responses.yml} +313 -306
- data/spec/spec_helper.rb +18 -0
- data/spec/support/graph_api_shared_examples.rb +424 -0
- data/spec/{koala → support}/live_testing_data_helper.rb +39 -42
- data/spec/{mock_http_service.rb → support/mock_http_service.rb} +94 -95
- data/spec/{koala/rest_api/rest_api_tests.rb → support/rest_api_shared_examples.rb} +43 -0
- data/spec/support/setup_mocks_or_live.rb +52 -0
- data/spec/support/uploadable_io_shared_examples.rb +76 -0
- metadata +107 -48
- data/init.rb +0 -2
- data/spec/koala/api_base_tests.rb +0 -102
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_no_token_tests.rb +0 -14
- data/spec/koala/graph_and_rest_api/graph_and_rest_api_with_token_tests.rb +0 -16
- data/spec/koala/graph_api/graph_api_no_access_token_tests.rb +0 -63
- data/spec/koala/graph_api/graph_api_tests.rb +0 -86
- data/spec/koala/graph_api/graph_api_with_access_token_tests.rb +0 -154
- data/spec/koala/graph_api/graph_collection_tests.rb +0 -104
- data/spec/koala/net_http_service_tests.rb +0 -430
- data/spec/koala/oauth/oauth_tests.rb +0 -409
- data/spec/koala/realtime_updates/realtime_updates_tests.rb +0 -187
- data/spec/koala/rest_api/rest_api_no_access_token_tests.rb +0 -25
- data/spec/koala/rest_api/rest_api_with_access_token_tests.rb +0 -38
- data/spec/koala/typhoeus_service_tests.rb +0 -156
- data/spec/koala_spec.rb +0 -18
- data/spec/koala_spec_helper.rb +0 -70
- data/spec/koala_spec_without_mocks.rb +0 -19
- /data/spec/{koala/assets → fixtures}/beach.jpg +0 -0
data/lib/koala/http_services.rb
CHANGED
|
@@ -15,26 +15,19 @@ module Koala
|
|
|
15
15
|
class << self
|
|
16
16
|
attr_accessor :always_use_ssl
|
|
17
17
|
end
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
def self.server(options = {})
|
|
20
|
+
"#{options[:beta] ? "beta." : ""}#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
protected
|
|
24
|
+
|
|
20
25
|
def self.params_require_multipart?(param_hash)
|
|
21
|
-
param_hash.any? { |key, value|
|
|
26
|
+
param_hash.any? { |key, value| value.kind_of?(Koala::UploadableIO) }
|
|
22
27
|
end
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# to the file to be uploaded.
|
|
27
|
-
# - A hash with the "file" key containing an already-opened IO that responds to "read"
|
|
28
|
-
# as well as "content_type" and the "path" key to the original file
|
|
29
|
-
# ("path"" is required by multipart-post even for opened files)
|
|
30
|
-
#
|
|
31
|
-
# Valid inputs for a file to be posted via multipart/form-data
|
|
32
|
-
# are based on the criteria for an UploadIO to be created
|
|
33
|
-
# See : https://github.com/nicksieger/multipart-post/blob/master/lib/composite_io.rb
|
|
34
|
-
def self.is_valid_file_hash?(value)
|
|
35
|
-
value.kind_of?(Hash) and value.key?("content_type") and value.key?("path") and (
|
|
36
|
-
!value.key?("file") or value["file"].respond_to?(:read)
|
|
37
|
-
)
|
|
28
|
+
|
|
29
|
+
def self.multipart_requires_content_type?
|
|
30
|
+
true
|
|
38
31
|
end
|
|
39
32
|
end
|
|
40
33
|
end
|
|
@@ -61,16 +54,9 @@ module Koala
|
|
|
61
54
|
# if the verb isn't get or post, send it as a post argument
|
|
62
55
|
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
http = Net::HTTP.new(server, private_request ? 443 : nil)
|
|
57
|
+
http = create_http(server(options), private_request, options)
|
|
66
58
|
http.use_ssl = true if private_request
|
|
67
59
|
|
|
68
|
-
# we turn off certificate validation to avoid the
|
|
69
|
-
# "warning: peer certificate won't be verified in this SSL session" warning
|
|
70
|
-
# not sure if this is the right way to handle it
|
|
71
|
-
# see http://redcorundum.blogspot.com/2008/03/ssl-certificates-and-nethttps.html
|
|
72
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
73
|
-
|
|
74
60
|
result = http.start do |http|
|
|
75
61
|
response, body = if verb == "post"
|
|
76
62
|
if params_require_multipart? args
|
|
@@ -98,17 +84,25 @@ module Koala
|
|
|
98
84
|
|
|
99
85
|
def self.encode_multipart_params(param_hash)
|
|
100
86
|
Hash[*param_hash.collect do |key, value|
|
|
101
|
-
|
|
102
|
-
if value.key?("file")
|
|
103
|
-
value = UploadIO.new(value["file"], value["content_type"], value["path"])
|
|
104
|
-
else
|
|
105
|
-
value = UploadIO.new(value["path"], value['content_type'])
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
[key, value]
|
|
87
|
+
[key, value.kind_of?(Koala::UploadableIO) ? value.to_upload_io : value]
|
|
110
88
|
end.flatten]
|
|
111
89
|
end
|
|
90
|
+
|
|
91
|
+
def self.create_http(server, private_request, options)
|
|
92
|
+
if options[:proxy]
|
|
93
|
+
proxy = URI.parse(options[:proxy])
|
|
94
|
+
http = Net::HTTP.new(server, private_request ? 443 : nil,
|
|
95
|
+
proxy.host, proxy.port, proxy.user, proxy.password)
|
|
96
|
+
else
|
|
97
|
+
http = Net::HTTP.new(server, private_request ? 443 : nil)
|
|
98
|
+
end
|
|
99
|
+
if options[:timeout]
|
|
100
|
+
http.open_timeout = options[:timeout]
|
|
101
|
+
http.read_timeout = options[:timeout]
|
|
102
|
+
end
|
|
103
|
+
http
|
|
104
|
+
end
|
|
105
|
+
|
|
112
106
|
end
|
|
113
107
|
end
|
|
114
108
|
end
|
|
@@ -116,11 +110,6 @@ module Koala
|
|
|
116
110
|
module TyphoeusService
|
|
117
111
|
# this service uses Typhoeus to send requests to the graph
|
|
118
112
|
|
|
119
|
-
# used for multipart file uploads (see below)
|
|
120
|
-
class NetHTTPInterface
|
|
121
|
-
include NetHTTPService
|
|
122
|
-
end
|
|
123
|
-
|
|
124
113
|
def self.included(base)
|
|
125
114
|
base.class_eval do
|
|
126
115
|
require "typhoeus" unless defined?(Typhoeus)
|
|
@@ -129,25 +118,27 @@ module Koala
|
|
|
129
118
|
include Koala::HTTPService
|
|
130
119
|
|
|
131
120
|
def self.make_request(path, args, verb, options = {})
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
|
135
|
-
server = options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER
|
|
121
|
+
# if the verb isn't get or post, send it as a post argument
|
|
122
|
+
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
|
136
123
|
|
|
137
|
-
|
|
138
|
-
|
|
124
|
+
# switch any UploadableIOs to the files Typhoeus expects
|
|
125
|
+
args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
|
|
139
126
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
|
|
127
|
+
# you can pass arguments directly to Typhoeus using the :typhoeus_options key
|
|
128
|
+
typhoeus_options = {:params => args}.merge(options[:typhoeus_options] || {})
|
|
143
129
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
130
|
+
# by default, we use SSL only for private requests (e.g. with access token)
|
|
131
|
+
# this makes public requests faster
|
|
132
|
+
prefix = (args["access_token"] || @always_use_ssl || options[:use_ssl]) ? "https" : "http"
|
|
133
|
+
|
|
134
|
+
response = self.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
|
|
135
|
+
Koala::Response.new(response.code, response.body, response.headers_hash)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def self.multipart_requires_content_type?
|
|
141
|
+
false # Typhoeus handles multipart file types, we don't have to require it
|
|
151
142
|
end
|
|
152
143
|
end # class_eval
|
|
153
144
|
end
|
data/lib/koala/test_users.rb
CHANGED
|
@@ -20,11 +20,11 @@ module Koala
|
|
|
20
20
|
@graph_api = GraphAPI.new(@app_access_token)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def create(installed, permissions = nil)
|
|
23
|
+
def create(installed, permissions = nil, args = {}, options = {})
|
|
24
24
|
# Creates and returns a test user
|
|
25
|
-
args
|
|
25
|
+
args['installed'] = installed
|
|
26
26
|
args['permissions'] = (permissions.is_a?(Array) ? permissions.join(",") : permissions) if installed
|
|
27
|
-
result = @graph_api.graph_call(accounts_path, args, "post")
|
|
27
|
+
result = @graph_api.graph_call(accounts_path, args, "post", options)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def list
|
|
@@ -40,12 +40,25 @@ module Koala
|
|
|
40
40
|
list.each {|u| delete u }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def befriend(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
def befriend(user1_hash, user2_hash)
|
|
44
|
+
user1_id = user1_hash["id"] || user1_hash[:id]
|
|
45
|
+
user2_id = user2_hash["id"] || user2_hash[:id]
|
|
46
|
+
user1_token = user1_hash["access_token"] || user1_hash[:access_token]
|
|
47
|
+
user2_token = user2_hash["access_token"] || user2_hash[:access_token]
|
|
48
|
+
unless user1_id && user2_id && user1_token && user2_token
|
|
49
|
+
# we explicitly raise an error here to minimize the risk of confusing output
|
|
50
|
+
# if you pass in a string (as was previously supported) no local exception would be raised
|
|
51
|
+
# but the Facebook call would fail
|
|
52
|
+
raise ArgumentError, "TestUsers#befriend requires hash arguments for both users with id and access_token"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
u1_graph_api = GraphAPI.new(user1_token)
|
|
56
|
+
u2_graph_api = GraphAPI.new(user2_token)
|
|
57
|
+
|
|
58
|
+
u1_graph_api.graph_call("#{user1_id}/friends/#{user2_id}", {}, "post") &&
|
|
59
|
+
u2_graph_api.graph_call("#{user2_id}/friends/#{user1_id}", {}, "post")
|
|
47
60
|
end
|
|
48
|
-
|
|
61
|
+
|
|
49
62
|
def create_network(network_size, installed = true, permissions = '')
|
|
50
63
|
network_size = 50 if network_size > 50 # FB's max is 50
|
|
51
64
|
users = (0...network_size).collect { create(installed, permissions) }
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require 'koala'
|
|
2
|
+
|
|
3
|
+
module Koala
|
|
4
|
+
class UploadableIO
|
|
5
|
+
attr_reader :io_or_path, :content_type
|
|
6
|
+
|
|
7
|
+
def initialize(io_or_path_or_mixed, content_type = nil)
|
|
8
|
+
# see if we got the right inputs
|
|
9
|
+
if content_type.nil?
|
|
10
|
+
parse_init_mixed_param io_or_path_or_mixed
|
|
11
|
+
elsif !content_type.nil? && (io_or_path_or_mixed.respond_to?(:read) or io_or_path_or_mixed.kind_of?(String))
|
|
12
|
+
@io_or_path = io_or_path_or_mixed
|
|
13
|
+
@content_type = content_type
|
|
14
|
+
end
|
|
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 && Koala.multipart_requires_content_type?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_upload_io
|
|
21
|
+
UploadIO.new(@io_or_path, @content_type, "koala-io-file.dum")
|
|
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
|
+
private
|
|
29
|
+
PARSE_STRATEGIES = [
|
|
30
|
+
:parse_rails_3_param,
|
|
31
|
+
:parse_sinatra_param,
|
|
32
|
+
:parse_file_object,
|
|
33
|
+
:parse_string_path
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def parse_init_mixed_param(mixed)
|
|
37
|
+
PARSE_STRATEGIES.each do |method|
|
|
38
|
+
send(method, mixed)
|
|
39
|
+
return if @io_or_path && @content_type
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Expects a parameter of type ActionDispatch::Http::UploadedFile
|
|
44
|
+
def parse_rails_3_param(uploaded_file)
|
|
45
|
+
if uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
|
|
46
|
+
@io_or_path = uploaded_file.tempfile.path
|
|
47
|
+
@content_type = uploaded_file.content_type
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Expects a Sinatra hash of file info
|
|
52
|
+
def parse_sinatra_param(file_hash)
|
|
53
|
+
if file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
|
|
54
|
+
@io_or_path = file_hash[:tempfile]
|
|
55
|
+
@content_type = file_hash[:type] || detect_mime_type(tempfile)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# takes a file object
|
|
60
|
+
def parse_file_object(file)
|
|
61
|
+
if file.kind_of?(File)
|
|
62
|
+
@io_or_path = file
|
|
63
|
+
@content_type = detect_mime_type(file.path)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def parse_string_path(path)
|
|
68
|
+
if path.kind_of?(String)
|
|
69
|
+
@io_or_path = path
|
|
70
|
+
@content_type = detect_mime_type(path)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
MIME_TYPE_STRATEGIES = [
|
|
75
|
+
:use_mime_module,
|
|
76
|
+
:use_simple_detection
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def detect_mime_type(filename)
|
|
80
|
+
if filename
|
|
81
|
+
MIME_TYPE_STRATEGIES.each do |method|
|
|
82
|
+
result = send(method, filename)
|
|
83
|
+
return result if result
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
nil # if we can't find anything
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def use_mime_module(filename)
|
|
90
|
+
# if the user has installed mime/types, we can use that
|
|
91
|
+
# if not, rescue and return nil
|
|
92
|
+
begin
|
|
93
|
+
type = MIME::Types.type_for(filename).first
|
|
94
|
+
type ? type.to_s : nil
|
|
95
|
+
rescue
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def use_simple_detection(filename)
|
|
101
|
+
# very rudimentary extension analysis for images
|
|
102
|
+
# first, get the downcased extension, or an empty string if it doesn't exist
|
|
103
|
+
extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
|
|
104
|
+
if extension == ""
|
|
105
|
+
nil
|
|
106
|
+
elsif extension == "jpg" || extension == "jpeg"
|
|
107
|
+
"image/jpeg"
|
|
108
|
+
elsif extension == "png"
|
|
109
|
+
"image/png"
|
|
110
|
+
elsif extension == "gif"
|
|
111
|
+
"image/gif"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/koala.rb
CHANGED
|
@@ -1,55 +1,32 @@
|
|
|
1
1
|
require 'cgi'
|
|
2
2
|
require 'digest/md5'
|
|
3
3
|
|
|
4
|
-
# rubygems is required to support json, how facebook returns data
|
|
5
|
-
require 'rubygems'
|
|
6
4
|
require 'json'
|
|
7
5
|
|
|
8
6
|
# OpenSSL and Base64 are required to support signed_request
|
|
9
7
|
require 'openssl'
|
|
10
8
|
require 'base64'
|
|
11
9
|
|
|
12
|
-
# include
|
|
10
|
+
# include koala modules
|
|
13
11
|
require 'koala/http_services'
|
|
14
|
-
|
|
15
|
-
# add Graph API methods
|
|
16
12
|
require 'koala/graph_api'
|
|
17
|
-
|
|
18
|
-
# add REST API methods
|
|
19
13
|
require 'koala/rest_api'
|
|
20
|
-
|
|
21
|
-
# add realtime update methods
|
|
22
14
|
require 'koala/realtime_updates'
|
|
23
|
-
|
|
24
|
-
# add test user methods
|
|
25
15
|
require 'koala/test_users'
|
|
26
16
|
|
|
17
|
+
# add KoalaIO class
|
|
18
|
+
require 'koala/uploadable_io'
|
|
19
|
+
|
|
27
20
|
module Koala
|
|
28
21
|
|
|
29
22
|
module Facebook
|
|
30
23
|
# Ruby client library for the Facebook Platform.
|
|
31
|
-
# Copyright 2010
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
35
|
-
# not use this file except in compliance with the License. You may obtain
|
|
36
|
-
# a copy of the License at
|
|
37
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
38
|
-
#
|
|
39
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
40
|
-
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
41
|
-
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
42
|
-
# License for the specific language governing permissions and limitations
|
|
43
|
-
# under the License.
|
|
44
|
-
#
|
|
45
|
-
# This client library is designed to support the Graph API and the official
|
|
46
|
-
# Facebook JavaScript SDK, which is the canonical way to implement
|
|
47
|
-
# Facebook authentication. Read more about the Graph API at
|
|
48
|
-
# http://developers.facebook.com/docs/api. You can download the Facebook
|
|
49
|
-
# JavaScript SDK at http://github.com/facebook/connect-js/.
|
|
24
|
+
# Copyright 2010-2011 Alex Koppel
|
|
25
|
+
# Contributors: Alex Koppel, Chris Baclig, Rafi Jacoby, and the team at Context Optional
|
|
26
|
+
# http://github.com/arsduo/koala
|
|
50
27
|
|
|
51
28
|
class API
|
|
52
|
-
# initialize with an access token
|
|
29
|
+
# initialize with an access token
|
|
53
30
|
def initialize(access_token = nil)
|
|
54
31
|
@access_token = access_token
|
|
55
32
|
end
|
|
@@ -58,7 +35,7 @@ module Koala
|
|
|
58
35
|
def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
|
|
59
36
|
# Fetches the given path in the Graph API.
|
|
60
37
|
args["access_token"] = @access_token || @app_access_token if @access_token || @app_access_token
|
|
61
|
-
|
|
38
|
+
|
|
62
39
|
# add a leading /
|
|
63
40
|
path = "/#{path}" unless path =~ /^\//
|
|
64
41
|
|
|
@@ -110,7 +87,7 @@ module Koala
|
|
|
110
87
|
attr_reader :graph_api
|
|
111
88
|
end
|
|
112
89
|
|
|
113
|
-
class APIError <
|
|
90
|
+
class APIError < StandardError
|
|
114
91
|
attr_accessor :fb_error_type
|
|
115
92
|
def initialize(details = {})
|
|
116
93
|
self.fb_error_type = details["type"]
|
|
@@ -170,12 +147,13 @@ module Koala
|
|
|
170
147
|
# for permissions, see http://developers.facebook.com/docs/authentication/permissions
|
|
171
148
|
permissions = options[:permissions]
|
|
172
149
|
scope = permissions ? "&scope=#{permissions.is_a?(Array) ? permissions.join(",") : permissions}" : ""
|
|
173
|
-
|
|
150
|
+
display = options.has_key?(:display) ? "&display=#{options[:display]}" : ""
|
|
151
|
+
|
|
174
152
|
callback = options[:callback] || @oauth_callback_url
|
|
175
153
|
raise ArgumentError, "url_for_oauth_code must get a callback either from the OAuth object or in the options!" unless callback
|
|
176
154
|
|
|
177
155
|
# Creates the URL for oauth authorization for a given callback and optional set of permissions
|
|
178
|
-
"https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}"
|
|
156
|
+
"https://#{GRAPH_SERVER}/oauth/authorize?client_id=#{@app_id}&redirect_uri=#{callback}#{scope}#{display}"
|
|
179
157
|
end
|
|
180
158
|
|
|
181
159
|
def url_for_access_token(code, options = {})
|
|
@@ -185,70 +163,56 @@ module Koala
|
|
|
185
163
|
"https://#{GRAPH_SERVER}/oauth/access_token?client_id=#{@app_id}&redirect_uri=#{callback}&client_secret=#{@app_secret}&code=#{code}"
|
|
186
164
|
end
|
|
187
165
|
|
|
188
|
-
def get_access_token_info(code)
|
|
166
|
+
def get_access_token_info(code, options = {})
|
|
189
167
|
# convenience method to get a parsed token from Facebook for a given code
|
|
190
168
|
# should this require an OAuth callback URL?
|
|
191
|
-
get_token_from_server(:code => code, :redirect_uri => @oauth_callback_url)
|
|
169
|
+
get_token_from_server({:code => code, :redirect_uri => @oauth_callback_url}, false, options)
|
|
192
170
|
end
|
|
193
171
|
|
|
194
|
-
def get_access_token(code)
|
|
172
|
+
def get_access_token(code, options = {})
|
|
195
173
|
# upstream methods will throw errors if needed
|
|
196
|
-
if info = get_access_token_info(code)
|
|
174
|
+
if info = get_access_token_info(code, options)
|
|
197
175
|
string = info["access_token"]
|
|
198
176
|
end
|
|
199
177
|
end
|
|
200
178
|
|
|
201
|
-
def get_app_access_token_info
|
|
179
|
+
def get_app_access_token_info(options = {})
|
|
202
180
|
# convenience method to get a the application's sessionless access token
|
|
203
|
-
get_token_from_server({:type => 'client_cred'}, true)
|
|
181
|
+
get_token_from_server({:type => 'client_cred'}, true, options)
|
|
204
182
|
end
|
|
205
183
|
|
|
206
|
-
def get_app_access_token
|
|
207
|
-
if info = get_app_access_token_info
|
|
184
|
+
def get_app_access_token(options = {})
|
|
185
|
+
if info = get_app_access_token_info(options)
|
|
208
186
|
string = info["access_token"]
|
|
209
187
|
end
|
|
210
188
|
end
|
|
211
|
-
|
|
212
|
-
# provided directly by Facebook
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
|
|
189
|
+
|
|
190
|
+
# Originally provided directly by Facebook, however this has changed
|
|
191
|
+
# as their concept of crypto changed. For historic purposes, this is their proposal:
|
|
192
|
+
# https://developers.facebook.com/docs/authentication/canvas/encryption_proposal/
|
|
193
|
+
# Currently see https://github.com/facebook/php-sdk/blob/master/src/facebook.php#L758
|
|
194
|
+
# for a more accurate reference implementation strategy.
|
|
195
|
+
def parse_signed_request(input)
|
|
216
196
|
encoded_sig, encoded_envelope = input.split('.', 2)
|
|
197
|
+
signature = base64_url_decode(encoded_sig).unpack("H*").first
|
|
217
198
|
envelope = JSON.parse(base64_url_decode(encoded_envelope))
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
raise 'Invalid
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
raise 'Invalid request. (Invalid signature.)' \
|
|
227
|
-
if base64_url_decode(encoded_sig) !=
|
|
228
|
-
OpenSSL::HMAC.hexdigest(
|
|
229
|
-
'sha256', @app_secret, encoded_envelope).split.pack('H*')
|
|
230
|
-
|
|
231
|
-
# for requests that are signed, but not encrypted, we're done
|
|
232
|
-
return envelope if algorithm == 'HMAC-SHA256'
|
|
233
|
-
|
|
234
|
-
# otherwise, decrypt the payload
|
|
235
|
-
cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
|
|
236
|
-
cipher.decrypt
|
|
237
|
-
cipher.key = @app_secret
|
|
238
|
-
cipher.iv = base64_url_decode(envelope['iv'])
|
|
239
|
-
cipher.padding = 0
|
|
240
|
-
decrypted_data = cipher.update(base64_url_decode(envelope['payload']))
|
|
241
|
-
decrypted_data << cipher.final
|
|
242
|
-
return JSON.parse(decrypted_data.strip)
|
|
199
|
+
|
|
200
|
+
raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
|
|
201
|
+
|
|
202
|
+
# now see if the signature is valid (digest, key, data)
|
|
203
|
+
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @app_secret, encoded_envelope.tr("-_", "+/"))
|
|
204
|
+
raise 'SignedRequest: Invalid signature' if (signature != hmac)
|
|
205
|
+
|
|
206
|
+
return envelope
|
|
243
207
|
end
|
|
244
208
|
|
|
245
209
|
# from session keys
|
|
246
|
-
def get_token_info_from_session_keys(sessions)
|
|
210
|
+
def get_token_info_from_session_keys(sessions, options = {})
|
|
247
211
|
# fetch the OAuth tokens from Facebook
|
|
248
212
|
response = fetch_token_string({
|
|
249
213
|
:type => 'client_cred',
|
|
250
214
|
:sessions => sessions.join(",")
|
|
251
|
-
}, true, "exchange_sessions")
|
|
215
|
+
}, true, "exchange_sessions", options)
|
|
252
216
|
|
|
253
217
|
# Facebook returns an empty body in certain error conditions
|
|
254
218
|
if response == ""
|
|
@@ -261,24 +225,24 @@ module Koala
|
|
|
261
225
|
JSON.parse(response)
|
|
262
226
|
end
|
|
263
227
|
|
|
264
|
-
def get_tokens_from_session_keys(sessions)
|
|
228
|
+
def get_tokens_from_session_keys(sessions, options = {})
|
|
265
229
|
# get the original hash results
|
|
266
|
-
results = get_token_info_from_session_keys(sessions)
|
|
230
|
+
results = get_token_info_from_session_keys(sessions, options)
|
|
267
231
|
# now recollect them as just the access tokens
|
|
268
232
|
results.collect { |r| r ? r["access_token"] : nil }
|
|
269
233
|
end
|
|
270
234
|
|
|
271
|
-
def get_token_from_session_key(session)
|
|
235
|
+
def get_token_from_session_key(session, options = {})
|
|
272
236
|
# convenience method for a single key
|
|
273
237
|
# gets the overlaoded strings automatically
|
|
274
|
-
get_tokens_from_session_keys([session])[0]
|
|
238
|
+
get_tokens_from_session_keys([session], options)[0]
|
|
275
239
|
end
|
|
276
240
|
|
|
277
241
|
protected
|
|
278
242
|
|
|
279
|
-
def get_token_from_server(args, post = false)
|
|
243
|
+
def get_token_from_server(args, post = false, options = {})
|
|
280
244
|
# fetch the result from Facebook's servers
|
|
281
|
-
result = fetch_token_string(args, post)
|
|
245
|
+
result = fetch_token_string(args, post, "access_token", options)
|
|
282
246
|
|
|
283
247
|
# if we have an error, parse the error JSON and raise an error
|
|
284
248
|
raise APIError.new((JSON.parse(result)["error"] rescue nil) || {}) if result =~ /error/
|
|
@@ -295,22 +259,24 @@ module Koala
|
|
|
295
259
|
components
|
|
296
260
|
end
|
|
297
261
|
|
|
298
|
-
def fetch_token_string(args, post = false, endpoint = "access_token")
|
|
262
|
+
def fetch_token_string(args, post = false, endpoint = "access_token", options = {})
|
|
299
263
|
Koala.make_request("/oauth/#{endpoint}", {
|
|
300
264
|
:client_id => @app_id,
|
|
301
265
|
:client_secret => @app_secret
|
|
302
|
-
}.merge!(args), post ? "post" : "get", :use_ssl => true).body
|
|
266
|
+
}.merge!(args), post ? "post" : "get", {:use_ssl => true}.merge!(options)).body
|
|
303
267
|
end
|
|
304
268
|
|
|
305
269
|
# base 64
|
|
306
270
|
# directly from https://github.com/facebook/crypto-request-examples/raw/master/sample.rb
|
|
307
271
|
def base64_url_decode(str)
|
|
308
272
|
str += '=' * (4 - str.length.modulo(4))
|
|
309
|
-
Base64.decode64(str.
|
|
273
|
+
Base64.decode64(str.tr('-_', '+/'))
|
|
310
274
|
end
|
|
311
275
|
end
|
|
312
276
|
end
|
|
313
277
|
|
|
278
|
+
class KoalaError< StandardError; end
|
|
279
|
+
|
|
314
280
|
# finally, set up the http service Koala methods used to make requests
|
|
315
281
|
# you can use your own (for HTTParty, etc.) by calling Koala.http_service = YourModule
|
|
316
282
|
def self.http_service=(service)
|
|
@@ -318,6 +284,8 @@ module Koala
|
|
|
318
284
|
end
|
|
319
285
|
|
|
320
286
|
# by default, try requiring Typhoeus -- if that works, use it
|
|
287
|
+
# if you have Typheous and don't want to use it (or want another service),
|
|
288
|
+
# you can run Koala.http_service = NetHTTPService (or MyHTTPService)
|
|
321
289
|
begin
|
|
322
290
|
Koala.http_service = TyphoeusService
|
|
323
291
|
rescue LoadError
|
data/readme.md
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
Koala
|
|
2
2
|
====
|
|
3
|
-
Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.com/arsduo/koala</a>) is a new Facebook library for Ruby, supporting the Graph API, the old REST API, realtime updates, and OAuth validation. We wrote Koala with four goals:
|
|
3
|
+
Koala (<a href="http://github.com/arsduo/koala" target="_blank">http://github.com/arsduo/koala</a>) is a new Facebook library for Ruby, supporting the Graph API (including photo uploads), the old REST API, realtime updates, and OAuth validation. We wrote Koala with four goals:
|
|
4
4
|
|
|
5
5
|
* Lightweight: Koala should be as light and simple as Facebook’s own new libraries, providing API accessors and returning simple JSON. (We clock in, with comments, just over 750 lines of code.)
|
|
6
6
|
* Fast: Koala should, out of the box, be quick. In addition to supporting the vanilla Ruby networking libraries, it natively supports Typhoeus, our preferred gem for making fast HTTP requests. Of course, That brings us to our next topic:
|
|
7
7
|
* Flexible: Koala should be useful to everyone, regardless of their current configuration. (We have no dependencies beyond the JSON gem. Koala also has a built-in mechanism for using whichever HTTP library you prefer to make requests against the graph.)
|
|
8
8
|
* Tested: Koala should have complete test coverage, so you can rely on it. (Our complete test coverage can be run against either mocked responses or the live Facebook servers.)
|
|
9
9
|
|
|
10
|
+
1.0
|
|
11
|
+
---
|
|
12
|
+
Version 1.0 is due out on May 1st, 2011 with a ton of great features.
|
|
13
|
+
|
|
14
|
+
sudo gem install koala
|
|
15
|
+
|
|
16
|
+
Until then, you can install the release candidate like so:
|
|
17
|
+
|
|
18
|
+
sudo gem install koala --pre
|
|
19
|
+
|
|
10
20
|
Graph API
|
|
11
21
|
----
|
|
12
22
|
The Graph API is the simple, slick new interface to Facebook's data. Using it with Koala is quite straightforward:
|
|
@@ -120,11 +130,14 @@ Testing
|
|
|
120
130
|
-----
|
|
121
131
|
|
|
122
132
|
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:
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
|
|
134
|
+
# From anywhere in the project directory:
|
|
135
|
+
rake spec
|
|
136
|
+
|
|
125
137
|
|
|
126
138
|
You can also run live tests against Facebook's servers:
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
|
|
140
|
+
# Again from anywhere in the project directory:
|
|
141
|
+
LIVE=true rake spec
|
|
129
142
|
|
|
130
|
-
Important Note: to run the live tests, you have to provide some of your own data: a valid OAuth access token with publish\_stream
|
|
143
|
+
Important Note: to run the live tests, you have to provide some of your own data in spec/fixtures/facebook_data.yml: a valid OAuth access token with publish\_stream, read\_stream, and user\_photos permissions and an OAuth code that can be used to generate an access token. You can get thisdata at the OAuth Playground; if you want to use your own app, remember to swap out the app ID, secret, and other values. (The file also provides valid values for other tests, which you're welcome to swap out for data specific to your own application.)
|