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 ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2008-07-23
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,6 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/live_contacts.rb
6
+ test/test_live_contacts.rb
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