live_contacts 0.0.3
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/History.txt +6 -0
- data/Manifest.txt +6 -0
- data/README.txt +48 -0
- data/Rakefile +15 -0
- data/lib/live_contacts.rb +184 -0
- data/test/test_live_contacts.rb +51 -0
- metadata +80 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= LiveContacts
|
2
|
+
|
3
|
+
* FIX (url)
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
FIX (describe your package)
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* FIX (list of features or problems)
|
12
|
+
|
13
|
+
== SYNOPSIS:
|
14
|
+
|
15
|
+
FIX (code sample of usage)
|
16
|
+
|
17
|
+
== REQUIREMENTS:
|
18
|
+
|
19
|
+
* FIX (list of requirements)
|
20
|
+
|
21
|
+
== INSTALL:
|
22
|
+
|
23
|
+
* FIX (sudo gem install, anything else)
|
24
|
+
|
25
|
+
== LICENSE:
|
26
|
+
|
27
|
+
(The MIT License)
|
28
|
+
|
29
|
+
Copyright (c) 2008 FIX
|
30
|
+
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
32
|
+
a copy of this software and associated documentation files (the
|
33
|
+
'Software'), to deal in the Software without restriction, including
|
34
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
35
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
36
|
+
permit persons to whom the Software is furnished to do so, subject to
|
37
|
+
the following conditions:
|
38
|
+
|
39
|
+
The above copyright notice and this permission notice shall be
|
40
|
+
included in all copies or substantial portions of the Software.
|
41
|
+
|
42
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
43
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
44
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
45
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
46
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
47
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
48
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/live_contacts.rb'
|
6
|
+
|
7
|
+
Hoe.new('live_contacts', LiveContacts::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'live-contacts' # if different than lowercase project name
|
9
|
+
# p.developer('FIX', 'FIX@example.com')
|
10
|
+
p.email = 'kenfodder@gmail.com'
|
11
|
+
p.author = 'Kenneth Lee'
|
12
|
+
p.extra_deps << ['ruby-openid', '= 1.1.4']
|
13
|
+
end
|
14
|
+
|
15
|
+
# vim: syntax=Ruby
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'cgi'
|
3
|
+
require 'base64'
|
4
|
+
require 'net/https'
|
5
|
+
require 'digest/sha2'
|
6
|
+
require 'openssl'
|
7
|
+
require 'hmac'
|
8
|
+
require 'hmac-sha2'
|
9
|
+
|
10
|
+
# Goto https://msm.live.com/app/default.aspx to configure your application
|
11
|
+
#
|
12
|
+
# For testin purposes in my example me.diary.com is a virtual host in the /etc/hosts file pointing at 127.0.0.1
|
13
|
+
#
|
14
|
+
# The APP_DETAILS hash has most of all the information I have registered my application with
|
15
|
+
#
|
16
|
+
# Example rails controller code:
|
17
|
+
#
|
18
|
+
# require 'live_contacts'
|
19
|
+
#
|
20
|
+
# protect_from_forgery :except => :windows_live_authentication
|
21
|
+
#
|
22
|
+
# APP_DETAILS = {
|
23
|
+
# :application_name => 'RLiveContacts',
|
24
|
+
# :app_id => '0016BFFD80013411',
|
25
|
+
# :secret => 'b623d64e8f1d18bfe8b281d385ad461c1e8bbebb',
|
26
|
+
# :security_algorithm => 'wsignin1.0',
|
27
|
+
# :return_url => 'http://me.diary.com:3000/home/windows_live_authentication',
|
28
|
+
# :privacy_policy_url => "http://me.diary.com:3000/privacy",
|
29
|
+
# :application_verifier_required => false
|
30
|
+
# }
|
31
|
+
#
|
32
|
+
# def windows_live_authentication
|
33
|
+
# lc = LiveContacts.new(APP_DETAILS)
|
34
|
+
# if request.post?
|
35
|
+
# lc.process_consent_params(params)
|
36
|
+
# xml = lc.retrieve_address_book_xml
|
37
|
+
# render :xml => xml and return
|
38
|
+
# else
|
39
|
+
# redirect_to lc.generate_delegation_url and return
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
class LiveContacts
|
43
|
+
|
44
|
+
VERSION = '0.0.3'
|
45
|
+
|
46
|
+
# Live application attibutes
|
47
|
+
attr_accessor :application_name, :app_id,:secret, :security_algorithm, :return_url, :privacy_policy_url, :application_verifier_required, :timestamp
|
48
|
+
|
49
|
+
# delegated authentication attributes
|
50
|
+
attr_reader :consent_token, :parsed_token, :eact, :cryptkey, :decrypted_token, :parsed_decrypted_token, :lid, :int_lid, :delt
|
51
|
+
|
52
|
+
# Step 1 - Initialize the object with the corrent details
|
53
|
+
def initialize(app_details)
|
54
|
+
self.application_name = app_details[:application_name]
|
55
|
+
self.app_id = app_details[:app_id]
|
56
|
+
self.secret = app_details[:secret]
|
57
|
+
self.security_algorithm = app_details[:security_algorithm]
|
58
|
+
self.return_url = app_details[:return_url]
|
59
|
+
self.privacy_policy_url = app_details[:privacy_policy_url]
|
60
|
+
self.application_verifier_required = app_details[:application_verifier_required]
|
61
|
+
|
62
|
+
# mainly used for testing purposes to check the signature generated
|
63
|
+
self.timestamp = app_details[:timestamp]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Step 2 - Get authentication consent from Live.com
|
67
|
+
def generate_delegation_url
|
68
|
+
url = "https://consent.live.com/Delegation.aspx?RU=#{self.return_url}&ps=Contacts.View&pl=#{self.privacy_policy_url}"
|
69
|
+
url += "&app=#{generate_app_verifier}" if self.application_verifier_required
|
70
|
+
url
|
71
|
+
end
|
72
|
+
|
73
|
+
# Step 3 - Process what was returned from Step 2, so we are ready to talk to the API
|
74
|
+
def process_consent_params(params)
|
75
|
+
return false unless params['ResponseCode'] == 'RequestApproved'
|
76
|
+
return false unless params['ConsentToken']
|
77
|
+
@consent_successful = false
|
78
|
+
@consent_successful = true if process_consent_token(params['ConsentToken'])
|
79
|
+
@consent_successful
|
80
|
+
end
|
81
|
+
|
82
|
+
# Step 4 - Actually request some information from the API, at the moment, simply just getting all contacts from the address book
|
83
|
+
# TODO timeout, error responses
|
84
|
+
def retrieve_address_book_xml
|
85
|
+
return unless @consent_successful
|
86
|
+
url = URI.parse("https://livecontacts.services.live.com" + "/users/@C@" + self.int_lid.to_s + "/REST/LiveContacts/Contacts")
|
87
|
+
|
88
|
+
req = Net::HTTP::Get.new(url.path)
|
89
|
+
req.add_field('Authorization', "DelegatedToken dt=\"" + self.delt + "\"")
|
90
|
+
req.set_content_type('application/xml', :charset => 'utf-8')
|
91
|
+
|
92
|
+
con = Net::HTTP.new(url.host, url.port)
|
93
|
+
con.use_ssl = true
|
94
|
+
|
95
|
+
res = con.start { |http| http.request(req) }
|
96
|
+
res.body
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def process_consent_token(token)
|
102
|
+
begin
|
103
|
+
@parsed_token = parse(CGI.unescape(token))
|
104
|
+
@eact = self.parsed_token['eact']
|
105
|
+
@cryptkey = Digest::SHA256.digest('ENCRYPTION' + self.secret)[0..15]
|
106
|
+
@decrypted_token = decode_token(self.eact, self.cryptkey)
|
107
|
+
@parsed_decrypted_token = parse(self.decrypted_token)
|
108
|
+
@lid = self.parsed_decrypted_token['lid']
|
109
|
+
@int_lid = self.lid.hex
|
110
|
+
@delt = self.parsed_decrypted_token['delt']
|
111
|
+
return true
|
112
|
+
rescue Exception => e
|
113
|
+
return false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# FIXME copied from Microsoft demo code, I'm sure this can be done in 2 lines of code, tests first though
|
118
|
+
def parse(input)
|
119
|
+
if (input.nil? or input.empty?)
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
|
123
|
+
pairs = {}
|
124
|
+
if (input.class == String)
|
125
|
+
input = input.split('&')
|
126
|
+
input.each{|pair|
|
127
|
+
k, v = pair.split('=')
|
128
|
+
pairs[k] = v
|
129
|
+
}
|
130
|
+
else
|
131
|
+
input.each{|k, v|
|
132
|
+
v = v[0] if (v.class == Array)
|
133
|
+
pairs[k.to_s] = v.to_s
|
134
|
+
}
|
135
|
+
end
|
136
|
+
return pairs
|
137
|
+
end
|
138
|
+
|
139
|
+
# FIXME copied from Microsoft demo
|
140
|
+
def decode_token(token, cryptkey)
|
141
|
+
if (cryptkey.nil? or cryptkey.empty?)
|
142
|
+
return false
|
143
|
+
end
|
144
|
+
token = Base64.decode64(CGI.unescape(token))
|
145
|
+
if (token.nil? or (token.size <= 16) or !(token.size % 16).zero?)
|
146
|
+
return false
|
147
|
+
end
|
148
|
+
iv = token[0..15]
|
149
|
+
crypted = token[16..-1]
|
150
|
+
begin
|
151
|
+
aes128cbc = OpenSSL::Cipher::AES128.new("CBC")
|
152
|
+
aes128cbc.decrypt
|
153
|
+
aes128cbc.iv = iv
|
154
|
+
aes128cbc.key = cryptkey
|
155
|
+
decrypted = aes128cbc.update(crypted) + aes128cbc.final
|
156
|
+
rescue Exception => e
|
157
|
+
return false
|
158
|
+
end
|
159
|
+
decrypted
|
160
|
+
end
|
161
|
+
|
162
|
+
# FIXME copied from Microsoft demo
|
163
|
+
def generate_app_verifier(ip = nil)
|
164
|
+
token = "appid=#{self.app_id}&ts=#{self.timestamp.to_i.to_s || Time.now.to_i.to_s}"
|
165
|
+
token += "&ip=#{ip}" if ip
|
166
|
+
token += "&sig=#{CGI.escape(Base64.encode64((signToken(token))))}"
|
167
|
+
CGI.escape token
|
168
|
+
end
|
169
|
+
|
170
|
+
# FIXME copied from Microsoft demo
|
171
|
+
def signToken(token)
|
172
|
+
begin
|
173
|
+
# TODO using the openssl libraries, they may not be up-to-date, e.g. Mac OSX Leopard doesn't have SHA256 in openssl
|
174
|
+
# digest = OpenSSL::Digest::SHA256.new
|
175
|
+
# return OpenSSL::HMAC.digest(digest, Digest::SHA256.digest('SIGNATURE' + SECRET)[0..15], token)
|
176
|
+
|
177
|
+
# using thw hmac from the ruby-openid libraries
|
178
|
+
return HMAC::SHA256.digest(Digest::SHA256.digest('SIGNATURE' + self.secret)[0..15], token)
|
179
|
+
rescue Exception => e
|
180
|
+
return false
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'live_contacts'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class LiveContactsTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
LIVE_APP_DETAILS = {
|
7
|
+
:application_name => 'RLiveContacts',
|
8
|
+
:app_id => '0016BFFD80013411',
|
9
|
+
:secret => 'b623d64e8f1d18bfe8b281d385ad461c1e8bbebb',
|
10
|
+
:security_algorithm => 'wsignin1.0',
|
11
|
+
:return_url => 'http://me.diary.com:3000',
|
12
|
+
:privacy_policy_url => "http://me.diary.com:3000/privacy",
|
13
|
+
:application_verifier_required => true
|
14
|
+
}
|
15
|
+
|
16
|
+
def test_initialize
|
17
|
+
lc = LiveContacts.new(LIVE_APP_DETAILS)
|
18
|
+
|
19
|
+
LIVE_APP_DETAILS.keys.each do |k|
|
20
|
+
assert_equal lc.send(k), LIVE_APP_DETAILS[k]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_generate_delegation_url
|
25
|
+
lc = LiveContacts.new(LIVE_APP_DETAILS.merge(:timestamp => Time.parse('Thu Jul 24 12:07:34 +0100 2008')))
|
26
|
+
assert_equal 'https://consent.live.com/Delegation.aspx?RU=http://me.diary.com:3000&ps=Contacts.View&pl=http://me.diary.com:3000/privacy&app=appid%3D0016BFFD80013411%26ts%3D1216897654%26sig%3DNJ3VHx6F9YMBZbCODuNcimejCqurCBXpEbWAclN4EZU%253D%250A', lc.generate_delegation_url
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_generate_delegation_url_without_app_verification
|
30
|
+
lc = LiveContacts.new(LIVE_APP_DETAILS.merge(:timestamp => Time.parse('Thu Jul 24 12:07:34 +0100 2008'), :application_verifier_required => false))
|
31
|
+
assert_equal 'https://consent.live.com/Delegation.aspx?RU=http://me.diary.com:3000&ps=Contacts.View&pl=http://me.diary.com:3000/privacy', lc.generate_delegation_url
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_process_consent_params
|
35
|
+
lc = LiveContacts.new(LIVE_APP_DETAILS.merge(:timestamp => Time.parse('Thu Jul 24 12:07:34 +0100 2008')))
|
36
|
+
assert lc.process_consent_params({"action"=>"delauth", "ResponseCode"=>"RequestApproved", "ConsentToken"=>"eact%3DwWAWABR%252Fr8S0kHilwbO3zEw8yn%252FJxvLnBeuneexie1Qm%252FdyzS438Jfnqk97wcNh4cDKf83L6KwzjVpHSnZOzlyJ0Rw8KG7sl%252F5nxTLRe7j5nKpn%252Bg%252FmRltAF9z%252BciCckPnDllO0%252FJ4%252BMvkb2YmGzo3SntFhMtyGc9WPTFtLKk%252F1INVSG6ssB%252FO4nC4riEKwqm662cfu4WOA0CPzVveh5VY7YknWM9zsJmSIfJ%252B0hxibpLzG7nLQHNF77xAscujc0OgPJVgYWEirCIqA2QUSkCDOmK%252B%252FkhUGr%252BIGlVxg1%252FunpGGrecDLyVtnlCm8tanRF8kZ3t47Hcg5SYkQbsPyv3S8prjMWDelUSus63yaTVsx10UrwhGM%252FfErSccsJUpYMVi1%252F%252FkhJNdlMCzHe2eJGNDyQhgZKZTXUW036fU5ZaY9CT8ARjSGlzc2uGf2FqQl2fr0g8%252FS6P%252BSz9G%252FP6fbsDM1HqcnyOcciR6R6sKEZ0dyBa4Md1HQPinJuO7sJhWxKV%252FpIRsvNX8u02ixcj63MmsQlb%252FOYRCOlz0D7Q%252BZzdUeP%252FUxh3z5XgBNJvQRpEyIWPTdXMbtuY0aGjNGVTujSqgtqnk2%252FqeHFbyk5iBHXrB1zMTFgM1lt9Yn5pppUDDTDZIeirWErgV2BMzxbIiAjjJYp0Fp6oHnepNro658vScgAlM8%252FuQzB31bbubvUWEZwZQAy7WmvZoxT33z8D8oiis8RPrCe0vIdYop1FNlKC163fRl3WtnGIJgaQZTGlONaCOtK5WAPm8Gy82P387FkdtD5FNJFgu0Ow4AMmvR8s4usOe7lFzLYzCUj0pYrIDFzY6UYJbEiJPohXwyzveu9y2HUxbsApkG36cmaa5Bzuxg5ECQE%252FsKFeIRwW23XI%252FrWL6XcRuuwL2pf4K%252F4eBolJQBIwgzPO4DPL7pf6FNjfKop2yWp9V5QJBY6K8ueqSCluwAFUNSHvoGaODdWINqTFB6haD0Gqmb9br0Po8%252BeExEYohF57X3Kv%252BXStGy5Of0jiCxcQfo0VZTooK17njLCtqFo%252B9I%252Fx53qis3AwaSpGXeiALHnyPK6oe2rtU77dQ8nT%252B45CgpKmOKdV%252Fk0gbzfQu3XTt%252F7Y%252BcvAIHy3NIqxZsE7fiyKRbHb0U9dXXhHh89dQds5Yd1lU%252BoNq0Dh8g7ZMO3hE%252FV%252FuZ8Jsz%252FQUYwWsMFamClI7zcfhh6qucxmV%252B%252BQxDnO2huiJVMRSXxsZXPYw%253D%253D", "appctx"=>""})
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_location_id
|
40
|
+
lc = LiveContacts.new(LIVE_APP_DETAILS.merge(:timestamp => Time.parse('Thu Jul 24 12:07:34 +0100 2008')))
|
41
|
+
lc.process_consent_params({"action"=>"delauth", "ResponseCode"=>"RequestApproved", "ConsentToken"=>"eact%3DwWAWABR%252Fr8S0kHilwbO3zEw8yn%252FJxvLnBeuneexie1Qm%252FdyzS438Jfnqk97wcNh4cDKf83L6KwzjVpHSnZOzlyJ0Rw8KG7sl%252F5nxTLRe7j5nKpn%252Bg%252FmRltAF9z%252BciCckPnDllO0%252FJ4%252BMvkb2YmGzo3SntFhMtyGc9WPTFtLKk%252F1INVSG6ssB%252FO4nC4riEKwqm662cfu4WOA0CPzVveh5VY7YknWM9zsJmSIfJ%252B0hxibpLzG7nLQHNF77xAscujc0OgPJVgYWEirCIqA2QUSkCDOmK%252B%252FkhUGr%252BIGlVxg1%252FunpGGrecDLyVtnlCm8tanRF8kZ3t47Hcg5SYkQbsPyv3S8prjMWDelUSus63yaTVsx10UrwhGM%252FfErSccsJUpYMVi1%252F%252FkhJNdlMCzHe2eJGNDyQhgZKZTXUW036fU5ZaY9CT8ARjSGlzc2uGf2FqQl2fr0g8%252FS6P%252BSz9G%252FP6fbsDM1HqcnyOcciR6R6sKEZ0dyBa4Md1HQPinJuO7sJhWxKV%252FpIRsvNX8u02ixcj63MmsQlb%252FOYRCOlz0D7Q%252BZzdUeP%252FUxh3z5XgBNJvQRpEyIWPTdXMbtuY0aGjNGVTujSqgtqnk2%252FqeHFbyk5iBHXrB1zMTFgM1lt9Yn5pppUDDTDZIeirWErgV2BMzxbIiAjjJYp0Fp6oHnepNro658vScgAlM8%252FuQzB31bbubvUWEZwZQAy7WmvZoxT33z8D8oiis8RPrCe0vIdYop1FNlKC163fRl3WtnGIJgaQZTGlONaCOtK5WAPm8Gy82P387FkdtD5FNJFgu0Ow4AMmvR8s4usOe7lFzLYzCUj0pYrIDFzY6UYJbEiJPohXwyzveu9y2HUxbsApkG36cmaa5Bzuxg5ECQE%252FsKFeIRwW23XI%252FrWL6XcRuuwL2pf4K%252F4eBolJQBIwgzPO4DPL7pf6FNjfKop2yWp9V5QJBY6K8ueqSCluwAFUNSHvoGaODdWINqTFB6haD0Gqmb9br0Po8%252BeExEYohF57X3Kv%252BXStGy5Of0jiCxcQfo0VZTooK17njLCtqFo%252B9I%252Fx53qis3AwaSpGXeiALHnyPK6oe2rtU77dQ8nT%252B45CgpKmOKdV%252Fk0gbzfQu3XTt%252F7Y%252BcvAIHy3NIqxZsE7fiyKRbHb0U9dXXhHh89dQds5Yd1lU%252BoNq0Dh8g7ZMO3hE%252FV%252FuZ8Jsz%252FQUYwWsMFamClI7zcfhh6qucxmV%252B%252BQxDnO2huiJVMRSXxsZXPYw%253D%253D", "appctx"=>""})
|
42
|
+
assert_equal 7682662888421099853, lc.int_lid
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_delegation_token
|
46
|
+
lc = LiveContacts.new(LIVE_APP_DETAILS.merge(:timestamp => Time.parse('Thu Jul 24 12:07:34 +0100 2008')))
|
47
|
+
lc.process_consent_params({"action"=>"delauth", "ResponseCode"=>"RequestApproved", "ConsentToken"=>"eact%3DwWAWABR%252Fr8S0kHilwbO3zEw8yn%252FJxvLnBeuneexie1Qm%252FdyzS438Jfnqk97wcNh4cDKf83L6KwzjVpHSnZOzlyJ0Rw8KG7sl%252F5nxTLRe7j5nKpn%252Bg%252FmRltAF9z%252BciCckPnDllO0%252FJ4%252BMvkb2YmGzo3SntFhMtyGc9WPTFtLKk%252F1INVSG6ssB%252FO4nC4riEKwqm662cfu4WOA0CPzVveh5VY7YknWM9zsJmSIfJ%252B0hxibpLzG7nLQHNF77xAscujc0OgPJVgYWEirCIqA2QUSkCDOmK%252B%252FkhUGr%252BIGlVxg1%252FunpGGrecDLyVtnlCm8tanRF8kZ3t47Hcg5SYkQbsPyv3S8prjMWDelUSus63yaTVsx10UrwhGM%252FfErSccsJUpYMVi1%252F%252FkhJNdlMCzHe2eJGNDyQhgZKZTXUW036fU5ZaY9CT8ARjSGlzc2uGf2FqQl2fr0g8%252FS6P%252BSz9G%252FP6fbsDM1HqcnyOcciR6R6sKEZ0dyBa4Md1HQPinJuO7sJhWxKV%252FpIRsvNX8u02ixcj63MmsQlb%252FOYRCOlz0D7Q%252BZzdUeP%252FUxh3z5XgBNJvQRpEyIWPTdXMbtuY0aGjNGVTujSqgtqnk2%252FqeHFbyk5iBHXrB1zMTFgM1lt9Yn5pppUDDTDZIeirWErgV2BMzxbIiAjjJYp0Fp6oHnepNro658vScgAlM8%252FuQzB31bbubvUWEZwZQAy7WmvZoxT33z8D8oiis8RPrCe0vIdYop1FNlKC163fRl3WtnGIJgaQZTGlONaCOtK5WAPm8Gy82P387FkdtD5FNJFgu0Ow4AMmvR8s4usOe7lFzLYzCUj0pYrIDFzY6UYJbEiJPohXwyzveu9y2HUxbsApkG36cmaa5Bzuxg5ECQE%252FsKFeIRwW23XI%252FrWL6XcRuuwL2pf4K%252F4eBolJQBIwgzPO4DPL7pf6FNjfKop2yWp9V5QJBY6K8ueqSCluwAFUNSHvoGaODdWINqTFB6haD0Gqmb9br0Po8%252BeExEYohF57X3Kv%252BXStGy5Of0jiCxcQfo0VZTooK17njLCtqFo%252B9I%252Fx53qis3AwaSpGXeiALHnyPK6oe2rtU77dQ8nT%252B45CgpKmOKdV%252Fk0gbzfQu3XTt%252F7Y%252BcvAIHy3NIqxZsE7fiyKRbHb0U9dXXhHh89dQds5Yd1lU%252BoNq0Dh8g7ZMO3hE%252FV%252FuZ8Jsz%252FQUYwWsMFamClI7zcfhh6qucxmV%252B%252BQxDnO2huiJVMRSXxsZXPYw%253D%253D", "appctx"=>""})
|
48
|
+
assert_equal 'EwCoARAnAAAUaiat%2Fx8TEXYT53ezUhJ8EEISAEWAANKjIpGuJPNQTsQn42llKNW4vXUOBv0v1FDcbBb2%2FsITUZBfSg7Cp1LoNDNn0paH4WAQrwHHMm5oPIOryQ3T9sGpZR3k%2BxewhYMzJja3XsMLNraZqufqrvhi6yjDHYyqtUK%2F0K5ZkDYBgdQNIqZrUIdbdqOkQ4VavTPg4sGikrq0A2YAAAihe2Xb8%2FZYXvgA0VMMFi3QfWAjj0jToUCYuyMteWu2l5jmkOaI5kxp%2FTV2YcrBIkKitlv8N1MBSZQZ67vWmNYNHIiwCN8PXnq5bfjK5RlIklFyDW0gA319xuxtU3A434A%2BuMwVtsMP9RGvfgyJ9eb1CiJmmo7ebl2%2FlHnAMQ7Mp02I7iHaNWeZVcCI9zZERKeb1yrDZAU3N4ykkKWMxdy1Z0Kp7%2FyZRJAD28P%2FuWEaEJl4TTy4b%2F0RMJ75VzuKKkf7u5bdmpV6t9IJgnz%2FtKhCXMQvdZGZVk4OT6QcGwP3Tm%2Fn9emRNpQki6EJhZ4Uwdx3OZ%2BLILfBT%2BDyta6DaudAFGQAAA%3D%3D', lc.delt
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: live_contacts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kenneth Lee
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-07-30 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: ruby-openid
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - "="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.1.4
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.7.0
|
34
|
+
version:
|
35
|
+
description: FIX (describe your package)
|
36
|
+
email: kenfodder@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- History.txt
|
43
|
+
- Manifest.txt
|
44
|
+
- README.txt
|
45
|
+
files:
|
46
|
+
- History.txt
|
47
|
+
- Manifest.txt
|
48
|
+
- README.txt
|
49
|
+
- Rakefile
|
50
|
+
- lib/live_contacts.rb
|
51
|
+
- test/test_live_contacts.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: FIX (url)
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options:
|
56
|
+
- --main
|
57
|
+
- README.txt
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project: live-contacts
|
75
|
+
rubygems_version: 1.2.0
|
76
|
+
signing_key:
|
77
|
+
specification_version: 2
|
78
|
+
summary: FIX (describe your package)
|
79
|
+
test_files:
|
80
|
+
- test/test_live_contacts.rb
|