Empact-rpx_now 0.7.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 +5 -0
- data/CHANGELOG +38 -0
- data/Empact-rpx_now.gemspec +67 -0
- data/MIGRATION +9 -0
- data/README.markdown +156 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/certs/ssl_cert.pem +3154 -0
- data/init.rb +2 -0
- data/lib/rpx_now.rb +163 -0
- data/lib/rpx_now/api.rb +81 -0
- data/lib/rpx_now/contacts_collection.rb +20 -0
- data/lib/rpx_now/user_integration.rb +9 -0
- data/lib/rpx_now/user_proxy.rb +19 -0
- data/spec/fixtures/get_contacts_response.json +58 -0
- data/spec/integration/mapping_spec.rb +40 -0
- data/spec/rpx_now/api_spec.rb +89 -0
- data/spec/rpx_now/contacts_collection_spec.rb +32 -0
- data/spec/rpx_now/user_proxy_spec.rb +33 -0
- data/spec/rpx_now_spec.rb +361 -0
- data/spec/spec_helper.rb +18 -0
- metadata +98 -0
data/init.rb
ADDED
data/lib/rpx_now.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'rpx_now/api'
|
2
|
+
require 'rpx_now/contacts_collection'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module RPXNow
|
6
|
+
extend self
|
7
|
+
|
8
|
+
attr_accessor :api_key
|
9
|
+
attr_accessor :api_version
|
10
|
+
attr_accessor :ssl
|
11
|
+
self.api_version = 2
|
12
|
+
self.ssl = true
|
13
|
+
|
14
|
+
VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
|
15
|
+
|
16
|
+
# retrieve the users data
|
17
|
+
# - cleaned Hash
|
18
|
+
# - complete/unclean response when block was given user_data{|response| ...; return hash }
|
19
|
+
# - nil when token was invalid / data was not found
|
20
|
+
def user_data(token, options={})
|
21
|
+
begin
|
22
|
+
data = Api.call("auth_info", options.merge(:token => token))
|
23
|
+
result = (block_given? ? yield(data) : parse_user_data(data, options))
|
24
|
+
result.respond_to?(:with_indifferent_access) ? result.with_indifferent_access : result
|
25
|
+
rescue ServerError
|
26
|
+
return nil if $!.to_s=~/Data not found/
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# set the users status
|
32
|
+
def set_status(identifier, status, options={})
|
33
|
+
options = options.merge(:identifier => identifier, :status => status)
|
34
|
+
data = Api.call("set_status", options)
|
35
|
+
rescue ServerError
|
36
|
+
return nil if $!.to_s=~/Data not found/
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
|
40
|
+
# Post an activity update to the user's activity stream.
|
41
|
+
# See more: https://rpxnow.com/docs#api_activity
|
42
|
+
def activity(identifier, activity_options, options={})
|
43
|
+
options = options.merge(:identifier => identifier, :activity => activity_options.to_json)
|
44
|
+
Api.call("activity", options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# maps an identifier to an primary-key (e.g. user.id)
|
48
|
+
def map(identifier, primary_key, options={})
|
49
|
+
Api.call("map", options.merge(:identifier => identifier, :primaryKey => primary_key))
|
50
|
+
end
|
51
|
+
|
52
|
+
# un-maps an identifier to an primary-key (e.g. user.id)
|
53
|
+
def unmap(identifier, primary_key, options={})
|
54
|
+
Api.call("unmap", options.merge(:identifier => identifier, :primaryKey => primary_key))
|
55
|
+
end
|
56
|
+
|
57
|
+
# returns an array of identifiers which are mapped to one of your primary-keys (e.g. user.id)
|
58
|
+
def mappings(primary_key, options={})
|
59
|
+
Api.call("mappings", options.merge(:primaryKey => primary_key))['identifiers']
|
60
|
+
end
|
61
|
+
|
62
|
+
def all_mappings(options={})
|
63
|
+
Api.call("all_mappings", options)['mappings']
|
64
|
+
end
|
65
|
+
|
66
|
+
def contacts(identifier, options={})
|
67
|
+
data = Api.call("get_contacts", options.merge(:identifier => identifier))
|
68
|
+
RPXNow::ContactsCollection.new(data['response'])
|
69
|
+
end
|
70
|
+
alias get_contacts contacts
|
71
|
+
|
72
|
+
# embedded rpx login (via iframe)
|
73
|
+
# options: :width, :height, :language, :flags, :api_version, :default_provider
|
74
|
+
def embed_code(subdomain, url, options={})
|
75
|
+
options = {:width => '400', :height => '240'}.merge(options)
|
76
|
+
<<-EOF
|
77
|
+
<iframe src="#{Api.host(subdomain)}/openid/embed?#{embed_params(url, options)}"
|
78
|
+
scrolling="no" frameBorder="no" style="width:#{options[:width]}px;height:#{options[:height]}px;">
|
79
|
+
</iframe>
|
80
|
+
EOF
|
81
|
+
end
|
82
|
+
|
83
|
+
# popup window for rpx login
|
84
|
+
# options: :language, :flags, :unobtrusive, :api_version, :default_provider
|
85
|
+
def popup_code(text, subdomain, url, options = {})
|
86
|
+
if options[:unobtrusive]
|
87
|
+
unobtrusive_popup_code(text, subdomain, url, options)
|
88
|
+
else
|
89
|
+
obtrusive_popup_code(text, subdomain, url, options)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# javascript for popup
|
94
|
+
# only needed in combination with popup_code(x,y,z, :unobtrusive => true)
|
95
|
+
def popup_source(subdomain, url, options={})
|
96
|
+
<<-EOF
|
97
|
+
<script src="#{Api.host}/openid/v#{extract_version(options)}/widget" type="text/javascript"></script>
|
98
|
+
<script type="text/javascript">
|
99
|
+
//<![CDATA[
|
100
|
+
RPXNOW.token_url = '#{url}';
|
101
|
+
RPXNOW.realm = '#{subdomain}';
|
102
|
+
RPXNOW.overlay = true;
|
103
|
+
RPXNOW.ssl = #{ssl};
|
104
|
+
#{ "RPXNOW.language_preference = '#{options[:language]}';" if options[:language] }
|
105
|
+
#{ "RPXNOW.default_provider = '#{options[:default_provider]}';" if options[:default_provider] }
|
106
|
+
#{ "RPXNOW.flags = '#{options[:flags]}';" if options[:flags] }
|
107
|
+
//]]>
|
108
|
+
</script>
|
109
|
+
EOF
|
110
|
+
end
|
111
|
+
|
112
|
+
# url for unobtrusive popup window
|
113
|
+
# options: :language, :flags, :api_version, :default_provider
|
114
|
+
def popup_url(subdomain, url, options={})
|
115
|
+
"#{Api.host(subdomain)}/openid/v#{extract_version(options)}/signin?#{embed_params(url, options)}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def extract_version(options)
|
119
|
+
options[:api_version] || api_version
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def self.embed_params(url, options)
|
125
|
+
{
|
126
|
+
:token_url => CGI::escape( url ),
|
127
|
+
:language_preference => options[:language],
|
128
|
+
:flags => options[:flags],
|
129
|
+
:default_provider => options[:default_provider]
|
130
|
+
}.map{|k,v| "#{k}=#{v}" if v}.compact.join('&')
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.parse_user_data(response, options)
|
134
|
+
user_data = response['profile']
|
135
|
+
data = {}
|
136
|
+
data[:identifier] = user_data['identifier']
|
137
|
+
data[:email] = user_data['verifiedEmail'] || user_data['email']
|
138
|
+
data[:username] = user_data['preferredUsername'] || data[:email].to_s.sub(/@.*/,'')
|
139
|
+
data[:name] = user_data['displayName'] || data[:username]
|
140
|
+
data[:id] = user_data['primaryKey'] unless user_data['primaryKey'].to_s.empty?
|
141
|
+
(options[:additional] || []).each do |key|
|
142
|
+
if key == :raw
|
143
|
+
data[key] = user_data
|
144
|
+
else
|
145
|
+
data[key] = user_data[key.to_s]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
data
|
149
|
+
end
|
150
|
+
|
151
|
+
def unobtrusive_popup_code(text, subdomain, url, options={})
|
152
|
+
%Q(<a class="rpxnow" href="#{popup_url(subdomain, url, options)}">#{text}</a>)
|
153
|
+
end
|
154
|
+
|
155
|
+
def obtrusive_popup_code(text, subdomain, url, options = {})
|
156
|
+
unobtrusive_popup_code(text, subdomain, url, options) +
|
157
|
+
popup_source(subdomain, url, options)
|
158
|
+
end
|
159
|
+
|
160
|
+
class ServerError < RuntimeError; end #backwards compatibility / catch all
|
161
|
+
class ApiError < ServerError; end
|
162
|
+
class ServiceUnavailableError < ServerError; end
|
163
|
+
end
|
data/lib/rpx_now/api.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module RPXNow
|
6
|
+
# low-level interaction with rpxnow.com api
|
7
|
+
# - send requests
|
8
|
+
# - parse response
|
9
|
+
# - handle server errors
|
10
|
+
class Api
|
11
|
+
HOST = 'rpxnow.com'
|
12
|
+
SSL_CERT = File.join(File.dirname(__FILE__), '..', '..', 'certs', 'ssl_cert.pem')
|
13
|
+
|
14
|
+
def self.call(method, data)
|
15
|
+
data = data.dup
|
16
|
+
version = RPXNow.extract_version(data)
|
17
|
+
data.delete(:api_version)
|
18
|
+
|
19
|
+
path = "/api/v#{version}/#{method}"
|
20
|
+
response = request(path, {:apiKey => RPXNow.api_key}.merge(data))
|
21
|
+
parse_response(response)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.host(realm=nil)
|
25
|
+
protocol = RPXNow.ssl ? 'https' : 'http'
|
26
|
+
domain =
|
27
|
+
if realm.nil?
|
28
|
+
Api::HOST
|
29
|
+
elsif realm.include?('.')
|
30
|
+
realm
|
31
|
+
else
|
32
|
+
"#{realm}.#{Api::HOST}"
|
33
|
+
end
|
34
|
+
"#{protocol}://#{domain}"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def self.request(path, data)
|
40
|
+
client.request(request_object(path, data))
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.request_object(path, data)
|
44
|
+
request = Net::HTTP::Post.new(path)
|
45
|
+
request.form_data = stringify_keys(data)
|
46
|
+
request
|
47
|
+
end
|
48
|
+
|
49
|
+
# symbol keys -> string keys
|
50
|
+
# because of ruby 1.9.x bug in Net::HTTP
|
51
|
+
# http://redmine.ruby-lang.org/issues/show/1351
|
52
|
+
def self.stringify_keys(hash)
|
53
|
+
hash.map{|k,v| [k.to_s,v]}
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.client
|
57
|
+
client = Net::HTTP.new(HOST, 443)
|
58
|
+
client.use_ssl = true
|
59
|
+
client.ca_file = SSL_CERT
|
60
|
+
client.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
61
|
+
client.verify_depth = 5
|
62
|
+
client
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.parse_response(response)
|
66
|
+
if response.code.to_i >= 400
|
67
|
+
raise ServiceUnavailableError, "The RPX service is temporarily unavailable. (4XX)"
|
68
|
+
else
|
69
|
+
result = JSON.parse(response.body)
|
70
|
+
return result unless result['err']
|
71
|
+
|
72
|
+
code = result['err']['code']
|
73
|
+
if code == -1
|
74
|
+
raise ServiceUnavailableError, "The RPX service is temporarily unavailable."
|
75
|
+
else
|
76
|
+
raise ApiError, "Got error: #{result['err']['msg']} (code: #{code}), HTTP status: #{response.code}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RPXNow
|
2
|
+
# Makes returned contacts feel like a array
|
3
|
+
class ContactsCollection < Array
|
4
|
+
def initialize(list)
|
5
|
+
@raw = list
|
6
|
+
@additional_info = list.reject{|k,v|k=='entry'}
|
7
|
+
list['entry'].each{|item| self << parse_data(item)}
|
8
|
+
end
|
9
|
+
|
10
|
+
def additional_info;@additional_info;end
|
11
|
+
def raw;@raw;end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def parse_data(entry)
|
16
|
+
entry['emails'] = (entry['emails'] ? entry['emails'].map{|email| email['value']} : [])
|
17
|
+
entry
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RPXNow
|
2
|
+
class UserProxy
|
3
|
+
def initialize(id)
|
4
|
+
@id = id
|
5
|
+
end
|
6
|
+
|
7
|
+
def identifiers
|
8
|
+
RPXNow.mappings(@id)
|
9
|
+
end
|
10
|
+
|
11
|
+
def map(identifier)
|
12
|
+
RPXNow.map(identifier, @id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def unmap(identifier)
|
16
|
+
RPXNow.unmap(identifier, @id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
{
|
2
|
+
"response": {
|
3
|
+
"itemsPerPage": 5,
|
4
|
+
"totalResults": 5,
|
5
|
+
"entry": [
|
6
|
+
{
|
7
|
+
"displayName": "Bob Johnson",
|
8
|
+
"emails": [
|
9
|
+
{
|
10
|
+
"type": "other",
|
11
|
+
"value": "bob@example.com"
|
12
|
+
}
|
13
|
+
]
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"displayName": "Cindy Smith",
|
17
|
+
"emails": [
|
18
|
+
{
|
19
|
+
"type": "other",
|
20
|
+
"value": "cindy.smith@example.com"
|
21
|
+
}
|
22
|
+
]
|
23
|
+
},
|
24
|
+
{
|
25
|
+
"displayName": "Fred Williams",
|
26
|
+
"emails": [
|
27
|
+
{
|
28
|
+
"type": "other",
|
29
|
+
"value": "fred.williams@example.com"
|
30
|
+
},
|
31
|
+
{
|
32
|
+
"type": "other",
|
33
|
+
"value": "fred@example.com"
|
34
|
+
}
|
35
|
+
]
|
36
|
+
},
|
37
|
+
{
|
38
|
+
"emails": [
|
39
|
+
{
|
40
|
+
"type": "other",
|
41
|
+
"value": "j.lewis@example.com"
|
42
|
+
}
|
43
|
+
]
|
44
|
+
},
|
45
|
+
{
|
46
|
+
"displayName": "Mary Jones",
|
47
|
+
"emails": [
|
48
|
+
{
|
49
|
+
"type": "other",
|
50
|
+
"value": "mary.jones@example.com"
|
51
|
+
}
|
52
|
+
]
|
53
|
+
}
|
54
|
+
],
|
55
|
+
"startIndex": 1
|
56
|
+
},
|
57
|
+
"stat": "ok"
|
58
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe RPXNow do
|
4
|
+
describe :mapping_integration do
|
5
|
+
before do
|
6
|
+
@k1 = 'http://test.myopenid.com'
|
7
|
+
RPXNow.unmap(@k1, 1)
|
8
|
+
@k2 = 'http://test-2.myopenid.com'
|
9
|
+
RPXNow.unmap(@k2, 1)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "has no mappings when nothing was mapped" do
|
13
|
+
RPXNow.mappings(1).should == []
|
14
|
+
end
|
15
|
+
|
16
|
+
it "unmaps mapped keys" do
|
17
|
+
RPXNow.map(@k2, 1)
|
18
|
+
RPXNow.unmap(@k2, 1)
|
19
|
+
RPXNow.mappings(1).should == []
|
20
|
+
end
|
21
|
+
|
22
|
+
it "maps keys to a primary key and then retrieves them" do
|
23
|
+
RPXNow.map(@k1, 1)
|
24
|
+
RPXNow.map(@k2, 1)
|
25
|
+
RPXNow.mappings(1).sort.should == [@k2,@k1]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "does not add duplicate mappings" do
|
29
|
+
RPXNow.map(@k1, 1)
|
30
|
+
RPXNow.map(@k1, 1)
|
31
|
+
RPXNow.mappings(1).should == [@k1]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "finds all mappings" do
|
35
|
+
RPXNow.map(@k1, 1)
|
36
|
+
RPXNow.map(@k2, 2)
|
37
|
+
RPXNow.all_mappings.sort.should == [["1", ["http://test.myopenid.com"]], ["2", ["http://test-2.myopenid.com"]]]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe RPXNow::Api do
|
4
|
+
describe 'ssl cert' do
|
5
|
+
it "has an absolute path" do
|
6
|
+
RPXNow::Api::SSL_CERT[0..0].should == File.expand_path( RPXNow::Api::SSL_CERT )[0..0] # start with '/' on *nix, drive letter on win
|
7
|
+
end
|
8
|
+
|
9
|
+
it "exists" do
|
10
|
+
File.read(RPXNow::Api::SSL_CERT).to_s.should_not be_empty
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe :host do
|
15
|
+
after do
|
16
|
+
RPXNow.ssl = true
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when the realm is a domain" do
|
20
|
+
it "returns the domain itself" do
|
21
|
+
RPXNow::Api.host("login.example.com").should == 'https://login.example.com'
|
22
|
+
end
|
23
|
+
|
24
|
+
it "honors the ssl setting" do
|
25
|
+
RPXNow.ssl = false
|
26
|
+
RPXNow::Api.host("login.example.com").should == 'http://login.example.com'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when the realm is a subdomain" do
|
31
|
+
it "returns the qualified rpx domain" do
|
32
|
+
RPXNow::Api.host("example").should == 'https://example.rpxnow.com'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "honors the ssl setting" do
|
36
|
+
RPXNow.ssl = false
|
37
|
+
RPXNow::Api.host("example").should == 'http://example.rpxnow.com'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "when no realm is provided" do
|
42
|
+
it "returns the rpx domain" do
|
43
|
+
RPXNow::Api.host.should == 'https://rpxnow.com'
|
44
|
+
end
|
45
|
+
|
46
|
+
it "honors the ssl setting" do
|
47
|
+
RPXNow.ssl = false
|
48
|
+
RPXNow::Api.host.should == 'http://rpxnow.com'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe :parse_response do
|
54
|
+
it "parses json when status is ok" do
|
55
|
+
response = mock(:code=>'200', :body=>%Q({"stat":"ok","data":"xx"}))
|
56
|
+
RPXNow::Api.send(:parse_response, response)['data'].should == "xx"
|
57
|
+
end
|
58
|
+
|
59
|
+
it "raises when there is a communication error" do
|
60
|
+
response = stub(:code=>'200', :body=>%Q({"err":"wtf","stat":"ok"}))
|
61
|
+
lambda{
|
62
|
+
RPXNow::Api.send(:parse_response,response)
|
63
|
+
}.should raise_error(RPXNow::ApiError)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "raises when service has downtime" do
|
67
|
+
response = stub(:code=>'200', :body=>%Q({"err":{"code":-1},"stat":"ok"}))
|
68
|
+
lambda{
|
69
|
+
RPXNow::Api.send(:parse_response,response)
|
70
|
+
}.should raise_error(RPXNow::ServiceUnavailableError)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "raises when service is down" do
|
74
|
+
response = stub(:code=>'400',:body=>%Q({"stat":"err"}))
|
75
|
+
lambda{
|
76
|
+
RPXNow::Api.send(:parse_response,response)
|
77
|
+
}.should raise_error(RPXNow::ServiceUnavailableError)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe :request_object do
|
82
|
+
it "converts symbols to string keys" do
|
83
|
+
mock = ''
|
84
|
+
mock.should_receive(:form_data=).with([['symbol', 'value']])
|
85
|
+
Net::HTTP::Post.should_receive(:new).and_return(mock)
|
86
|
+
RPXNow::Api.send(:request_object, 'something', :symbol=>'value')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|