pezra-contacts 0.1.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.
@@ -0,0 +1,9 @@
1
+ module Contacts
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 2
5
+ TINY = 5
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,164 @@
1
+ require 'contacts'
2
+ require File.join(File.dirname(__FILE__), %w{.. .. vendor windowslivelogin})
3
+
4
+ require 'rubygems'
5
+ require 'hpricot'
6
+ require 'uri'
7
+ require 'yaml'
8
+
9
+ module Contacts
10
+ # = How I can fetch Windows Live Contacts?
11
+ # To gain access to a Windows Live user's data in the Live Contacts service,
12
+ # a third-party developer first must ask the owner for permission. You must
13
+ # do that through Windows Live Delegated Authentication.
14
+ #
15
+ # This library give you access to Windows Live Delegated Authentication System
16
+ # and Windows Live Contacts API. Just follow the steps below and be happy!
17
+ #
18
+ # === Registering your app
19
+ # First of all, follow the steps in this
20
+ # page[http://msdn.microsoft.com/en-us/library/cc287659.aspx] to register your
21
+ # app.
22
+ #
23
+ # === Configuring your Windows Live YAML
24
+ # After registering your app, you will have an *appid*, a <b>secret key</b> and
25
+ # a <b>return URL</b>. Use their values to fill in the config/contacts.yml file.
26
+ # The policy URL field inside the YAML config file must contain the URL
27
+ # of the privacy policy of your Web site for Delegated Authentication.
28
+ #
29
+ # === Authenticating your user and fetching his contacts
30
+ #
31
+ # wl = Contacts::WindowsLive.new
32
+ # auth_url = wl.get_authentication_url
33
+ #
34
+ # Use that *auth_url* to redirect your user to Windows Live. He will authenticate
35
+ # there and Windows Live will POST to your return URL. You have to get the
36
+ # body of that POST, let's call it post_body. (if you're using Rails, you can
37
+ # get the POST body through request.raw_post, in the context of an action inside
38
+ # ActionController)
39
+ #
40
+ # Now, to fetch his contacts, just do this:
41
+ #
42
+ # contacts = wl.contacts(post_body)
43
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
44
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
45
+ # ]
46
+ #--
47
+ # This class has two responsibilities:
48
+ # 1. Access the Windows Live Contacts API through Delegated Authentication
49
+ # 2. Import contacts from Windows Live and deliver it inside an Array
50
+ #
51
+ class WindowsLive
52
+ CONFIG_FILE = File.dirname(__FILE__) + '/../config/contacts.yml'
53
+
54
+ # Initialize a new WindowsLive object.
55
+ #
56
+ # ==== Paramaters
57
+ # * config_file <String>:: The contacts YAML config file name
58
+ #--
59
+ # You can check an example of a config file inside config/ directory
60
+ #
61
+ def initialize(config_file=CONFIG_FILE)
62
+ confs = YAML.load_file(config_file)['windows_live']
63
+ @wll = WindowsLiveLogin.new(confs['appid'], confs['secret'], confs['security_algorithm'],
64
+ nil, confs['policy_url'], confs['return_url'])
65
+ end
66
+
67
+
68
+ # Windows Live Contacts API need to authenticate the user that is giving you
69
+ # access to his contacts. To do that, you must give him a URL. That method
70
+ # generates that URL. The user must access that URL, and after he has done
71
+ # authentication, hi will be redirected to your application.
72
+ #
73
+ def get_authentication_url
74
+ @wll.getConsentUrl("Contacts.Invite")
75
+ end
76
+
77
+ # After the user has been authenticaded, Windows Live Delegated Authencation
78
+ # Service redirects to your application, through a POST HTTP method. Along
79
+ # with the POST, Windows Live send to you a Consent that you must process
80
+ # to access the user's contacts. This method process the Consent
81
+ # to you.
82
+ #
83
+ # ==== Paramaters
84
+ # * consent <String>:: A string containing the Consent given to you inside
85
+ # the redirection POST from Windows Live
86
+ #
87
+ def process_consent(consent)
88
+ consent.strip!
89
+ consent = URI.unescape(consent)
90
+ @consent_token = @wll.processConsent(consent)
91
+ end
92
+
93
+ # This method return the user's contacts inside an Array in the following
94
+ # format:
95
+ #
96
+ # [
97
+ # ['Brad Fitzgerald', 'fubar@gmail.com'],
98
+ # [nil, 'nagios@hotmail.com'],
99
+ # ['William Paginate', 'will.paginate@yahoo.com'] ...
100
+ # ]
101
+ #
102
+ # ==== Paramaters
103
+ # * consent <String>:: A string containing the Consent given to you inside
104
+ # the redirection POST from Windows Live
105
+ #
106
+ def contacts(consent)
107
+ process_consent(consent)
108
+ contacts_xml = access_live_contacts_api()
109
+ contacts_list = WindowsLive.parse_xml(contacts_xml)
110
+ end
111
+
112
+ # This method access the Windows Live Contacts API Web Service to get
113
+ # the XML contacts document
114
+ #
115
+ def access_live_contacts_api
116
+ Net::HTTP.version_1_1
117
+ http = http = Net::HTTP.new('livecontacts.services.live.com', 443)
118
+ http.use_ssl = true
119
+
120
+ response = nil
121
+ http.start do |http|
122
+ request = Net::HTTP::Get.new("/users/@L@#{@consent_token.locationid}/rest/invitationsbyemail", {"Authorization" => "DelegatedToken dt=\"#{@consent_token.delegationtoken}\""})
123
+ response = http.request(request)
124
+ end
125
+
126
+ return response.body
127
+ end
128
+
129
+ # This method parses the XML Contacts document and returns the contacts
130
+ # inside an Array
131
+ #
132
+ # ==== Paramaters
133
+ # * xml <String>:: A string containing the XML contacts document
134
+ #
135
+ def self.parse_xml(xml)
136
+ doc = Hpricot::XML(xml)
137
+
138
+ contacts = []
139
+ doc.search('/livecontacts/contacts/contact').each do |contact|
140
+ email = contact.at('/preferredemail').inner_text
141
+ email.strip!
142
+
143
+ first_name = last_name = nil
144
+ if first_name = contact.at('/profiles/personal/firstname')
145
+ first_name = first_name.inner_text.strip
146
+ end
147
+
148
+ if last_name = contact.at('/profiles/personal/lastname')
149
+ last_name = last_name.inner_text.strip
150
+ end
151
+
152
+ name = nil
153
+ if !first_name.nil? || !last_name.nil?
154
+ name = "#{first_name} #{last_name}"
155
+ name.strip!
156
+ end
157
+ contacts.push([name, email])
158
+ end
159
+
160
+ return contacts
161
+ end
162
+ end
163
+
164
+ end
@@ -0,0 +1,236 @@
1
+ require 'contacts'
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'md5'
6
+ require 'net/https'
7
+ require 'uri'
8
+ require 'json'
9
+ require 'yaml'
10
+
11
+ module Contacts
12
+ # = How I can fetch Yahoo Contacts?
13
+ # To gain access to a Yahoo user's data in the Yahoo Address Book Service,
14
+ # a third-party developer first must ask the owner for permission. You must
15
+ # do that through Yahoo Browser Based Authentication (BBAuth).
16
+ #
17
+ # This library give you access to Yahoo BBAuth and Yahoo Address Book API.
18
+ # Just follow the steps below and be happy!
19
+ #
20
+ # === Registering your app
21
+ # First of all, follow the steps in this
22
+ # page[http://developer.yahoo.com/wsregapp/] to register your app. If you need
23
+ # some help with that form, you can get it
24
+ # here[http://developer.yahoo.com/auth/appreg.html]. Just two tips: inside
25
+ # <b>Required access scopes</b> in that registration form, choose
26
+ # <b>Yahoo! Address Book with Read Only access</b>. Inside
27
+ # <b>Authentication method</b> choose <b>Browser Based Authentication</b>.
28
+ #
29
+ # === Configuring your Yahoo YAML
30
+ # After registering your app, you will have an <b>application id</b> and a
31
+ # <b>shared secret</b>. Use their values to fill in the config/contacts.yml
32
+ # file.
33
+ #
34
+ # === Authenticating your user and fetching his contacts
35
+ #
36
+ # yahoo = Contacts::Yahoo.new
37
+ # auth_url = yahoo.get_authentication_url
38
+ #
39
+ # Use that *auth_url* to redirect your user to Yahoo BBAuth. He will authenticate
40
+ # there and Yahoo will redirect to your application entrypoint URL (that you provided
41
+ # while registering your app with Yahoo). You have to get the path of that
42
+ # redirect, let's call it path (if you're using Rails, you can get it through
43
+ # request.request_uri, in the context of an action inside ActionController)
44
+ #
45
+ # Now, to fetch his contacts, just do this:
46
+ #
47
+ # contacts = wl.contacts(path)
48
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
49
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
50
+ # ]
51
+ #--
52
+ # This class has two responsibilities:
53
+ # 1. Access the Yahoo Address Book API through Delegated Authentication
54
+ # 2. Import contacts from Yahoo Mail and deliver it inside an Array
55
+ #
56
+ class Yahoo
57
+ AUTH_DOMAIN = "https://api.login.yahoo.com"
58
+ AUTH_PATH = "/WSLogin/V1/wslogin?appid=#appid&ts=#ts"
59
+ CREDENTIAL_PATH = "/WSLogin/V1/wspwtoken_login?appid=#appid&ts=#ts&token=#token"
60
+ ADDRESS_BOOK_DOMAIN = "address.yahooapis.com"
61
+ ADDRESS_BOOK_PATH = "/v1/searchContacts?format=json&fields=name,email&appid=#appid&WSSID=#wssid"
62
+ CONFIG_FILE = File.dirname(__FILE__) + '/../config/contacts.yml'
63
+
64
+ attr_reader :appid, :secret, :token, :wssid, :cookie
65
+
66
+ # Initialize a new Yahoo object.
67
+ #
68
+ # ==== Paramaters
69
+ # * config_file <String>:: The contacts YAML config file name
70
+ #--
71
+ # You can check an example of a config file inside config/ directory
72
+ #
73
+ def initialize(config_file=CONFIG_FILE)
74
+ confs = YAML.load_file(config_file)['yahoo']
75
+ @appid = confs['appid']
76
+ @secret = confs['secret']
77
+ end
78
+
79
+ # Yahoo Address Book API need to authenticate the user that is giving you
80
+ # access to his contacts. To do that, you must give him a URL. This method
81
+ # generates that URL. The user must access that URL, and after he has done
82
+ # authentication, hi will be redirected to your application.
83
+ #
84
+ def get_authentication_url
85
+ path = AUTH_PATH.clone
86
+ path.sub!(/#appid/, @appid)
87
+
88
+ timestamp = Time.now.utc.to_i
89
+ path.sub!(/#ts/, timestamp.to_s)
90
+
91
+ signature = MD5.hexdigest(path + @secret)
92
+ return AUTH_DOMAIN + "#{path}&sig=#{signature}"
93
+ end
94
+
95
+ # This method return the user's contacts inside an Array in the following
96
+ # format:
97
+ #
98
+ # [
99
+ # ['Brad Fitzgerald', 'fubar@gmail.com'],
100
+ # [nil, 'nagios@hotmail.com'],
101
+ # ['William Paginate', 'will.paginate@yahoo.com'] ...
102
+ # ]
103
+ #
104
+ # ==== Paramaters
105
+ # * path <String>:: The path of the redirect request that Yahoo sent to you
106
+ # after authenticating the user
107
+ #
108
+ def contacts(path)
109
+ begin
110
+ validate_signature(path)
111
+ credentials = access_user_credentials()
112
+ parse_credentials(credentials)
113
+ contacts_json = access_address_book_api()
114
+ Yahoo.parse_contacts(contacts_json)
115
+ rescue Exception => e
116
+ "Error #{e.class}: #{e.message}."
117
+ end
118
+ end
119
+
120
+ # This method processes and validates the redirect request that Yahoo send to
121
+ # you. Validation is done to verify that the request was really made by
122
+ # Yahoo. Processing is done to get the token.
123
+ #
124
+ # ==== Paramaters
125
+ # * path <String>:: The path of the redirect request that Yahoo sent to you
126
+ # after authenticating the user
127
+ #
128
+ def validate_signature(path)
129
+ path.match(/^(.+)&sig=(\w{32})$/)
130
+ path_without_sig = $1
131
+ sig = $2
132
+
133
+ if sig == MD5.hexdigest(path_without_sig + @secret)
134
+ path.match(/token=(.+?)&/)
135
+ @token = $1
136
+ return true
137
+ else
138
+ raise 'Signature not valid. This request may not have been sent from Yahoo.'
139
+ end
140
+ end
141
+
142
+ # This method accesses Yahoo to retrieve the user's credentials.
143
+ #
144
+ def access_user_credentials
145
+ url = get_credential_url()
146
+ uri = URI.parse(url)
147
+
148
+ http = http = Net::HTTP.new(uri.host, uri.port)
149
+ http.use_ssl = true
150
+
151
+ response = nil
152
+ http.start do |http|
153
+ request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
154
+ response = http.request(request)
155
+ end
156
+
157
+ return response.body
158
+ end
159
+
160
+ # This method generates the URL that you must access to get user's
161
+ # credentials.
162
+ #
163
+ def get_credential_url
164
+ path = CREDENTIAL_PATH.clone
165
+ path.sub!(/#appid/, @appid)
166
+
167
+ path.sub!(/#token/, @token)
168
+
169
+ timestamp = Time.now.utc.to_i
170
+ path.sub!(/#ts/, timestamp.to_s)
171
+
172
+ signature = MD5.hexdigest(path + @secret)
173
+ return AUTH_DOMAIN + "#{path}&sig=#{signature}"
174
+ end
175
+
176
+ # This method parses the user's credentials to generate the WSSID and
177
+ # Coookie that are needed to give you access to user's address book.
178
+ #
179
+ # ==== Paramaters
180
+ # * xml <String>:: A String containing the user's credentials
181
+ #
182
+ def parse_credentials(xml)
183
+ doc = Hpricot::XML(xml)
184
+ @wssid = doc.at('/BBAuthTokenLoginResponse/Success/WSSID').inner_text.strip
185
+ @cookie = doc.at('/BBAuthTokenLoginResponse/Success/Cookie').inner_text.strip
186
+ end
187
+
188
+ # This method accesses the Yahoo Address Book API and retrieves the user's
189
+ # contacts in JSON.
190
+ #
191
+ def access_address_book_api
192
+ http = http = Net::HTTP.new(ADDRESS_BOOK_DOMAIN, 80)
193
+
194
+ response = nil
195
+ http.start do |http|
196
+ path = ADDRESS_BOOK_PATH.clone
197
+ path.sub!(/#appid/, @appid)
198
+ path.sub!(/#wssid/, @wssid)
199
+
200
+ request = Net::HTTP::Get.new(path, {'Cookie' => @cookie})
201
+ response = http.request(request)
202
+ end
203
+
204
+ return response.body
205
+ end
206
+
207
+ # This method parses the JSON contacts document and returns an array
208
+ # contaning all the user's contacts.
209
+ #
210
+ # ==== Paramaters
211
+ # * json <String>:: A String the user's contacts ni JSON format
212
+ #
213
+ def self.parse_contacts(json)
214
+ people = JSON.parse(json)
215
+ contacts = []
216
+
217
+ people['contacts'].each do |contact|
218
+ name = nil
219
+ email = nil
220
+ contact['fields'].each do |field|
221
+ case field['type']
222
+ when 'email'
223
+ email = field['data']
224
+ email.strip!
225
+ when 'name'
226
+ name = "#{field['first']} #{field['last']}"
227
+ name.strip!
228
+ end
229
+ end
230
+ contacts.push([name, email])
231
+ end
232
+ return contacts
233
+ end
234
+
235
+ end
236
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'contacts'
3
+
4
+ describe Contacts::Contact do
5
+ describe 'instance' do
6
+ before do
7
+ @contact = Contacts::Contact.new('max@example.com', 'Max Power', 'maxpower')
8
+ end
9
+
10
+ it "should have email" do
11
+ @contact.email.should == 'max@example.com'
12
+ end
13
+
14
+ it "should have name" do
15
+ @contact.name.should == 'Max Power'
16
+ end
17
+
18
+ it "should support multiple emails" do
19
+ @contact.emails << 'maxpower@example.com'
20
+ @contact.email.should == 'max@example.com'
21
+ @contact.emails.should == ['max@example.com', 'maxpower@example.com']
22
+ end
23
+
24
+ it "should have username" do
25
+ @contact.username.should == 'maxpower'
26
+ end
27
+
28
+ it "should have nice inspect" do
29
+ @contact.inspect.should == '#<Contacts::Contact "Max Power" (max@example.com)>'
30
+ end
31
+ end
32
+
33
+ it "should fail without first argument (email)" do
34
+ lambda {
35
+ Contacts::Contact.new()
36
+ }.should raise_error(ArgumentError)
37
+ end
38
+ end