live_contacts 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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