aurelian-contacts 0.3.1

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,91 @@
1
+ require 'oauth'
2
+ require 'contacts/google'
3
+
4
+ # An extension to the standard OAuth library so we can nicely use Google APIs
5
+ module GoogleOAuth
6
+ class RequestToken < OAuth::RequestToken
7
+ def authorize_url(params={})
8
+ params.merge! :oauth_token => token
9
+ params = params.map { |k,v| "%s=%s" % [CGI.escape(k.to_s), CGI.escape(v)] }
10
+ consumer.authorize_url + "?" + params.join("&")
11
+ end
12
+ end
13
+
14
+ class Consumer < OAuth::Consumer
15
+ def initialize(consumer_key, consumer_secret)
16
+ super(consumer_key,
17
+ consumer_secret,
18
+ {:site => "https://www.google.com",
19
+ :request_token_path => "/accounts/OAuthGetRequestToken",
20
+ :access_token_path => "/accounts/OAuthGetAccessToken",
21
+ :authorize_path => "/accounts/OAuthAuthorizeToken"})
22
+ end
23
+
24
+ def marshal_load(data)
25
+ initialize(data[:key], data[:secret])
26
+ end
27
+
28
+ def marshal_dump
29
+ {:key => self.key, :secret => self.secret}
30
+ end
31
+
32
+ def get_request_token(params={})
33
+ params_str = params.map { |k,v| "%s=%s" % [CGI.escape(k.to_s), CGI.escape(v)] }.join("&")
34
+ uri = URI.parse(request_token_url? ? request_token_url : request_token_path)
35
+ if !uri.query || uri.query == ''
36
+ uri.query = params_str
37
+ else
38
+ uri.query = uri.query + "&" + params_str
39
+ end
40
+
41
+ response=token_request(http_method, uri.to_s, nil, {})
42
+ GoogleOAuth::RequestToken.new(self, response[:oauth_token], response[:oauth_token_secret])
43
+ end
44
+ end
45
+ end
46
+
47
+ module Contacts
48
+ class GoogleOAuth < Google
49
+ def initialize(consumer_key, consumer_secret, user_id = 'default')
50
+ @consumer = ::GoogleOAuth::Consumer.new(consumer_key, consumer_secret)
51
+ @request_token = @consumer.get_request_token :scope => "https://www.google.com/m8/feeds/"
52
+ @projection = 'thin'
53
+ @user = user_id.to_s
54
+ end
55
+
56
+ def marshal_load(data)
57
+ @consumer = data[:consumer]
58
+ @request_token = data[:request_token]
59
+ @projection = 'thin'
60
+ @user = data[:user]
61
+ end
62
+
63
+ def marshal_dump
64
+ {:consumer => @consumer,
65
+ :request_token => @request_token,
66
+ :user => @user}
67
+ end
68
+
69
+ # Available parameters:
70
+ # - hd: Google Apps domain that should be requested (default nil)
71
+ # - oauth_callback: The URL that the user should be redirected to when he successfully authorized us.
72
+ def authentication_url(params={})
73
+ @request_token.authorize_url params
74
+ end
75
+
76
+ def access_token
77
+ return @access_token if @access_token
78
+ begin
79
+ @access_token = @request_token.get_access_token
80
+ rescue Net::HTTPServerException
81
+ end
82
+ end
83
+
84
+ def get(params={})
85
+ path = FeedsPath + CGI.escape(@user)
86
+ google_params = translate_parameters(params)
87
+ query = self.class.query_string(google_params)
88
+ access_token.get("#{path}/#{@projection}?#{query}")
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,9 @@
1
+ module Contacts
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 3
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,186 @@
1
+ require 'contacts'
2
+
3
+ require File.join(File.dirname(__FILE__), %w{.. .. vendor windowslivelogin})
4
+ require 'hpricot'
5
+ require 'uri'
6
+
7
+ module Contacts
8
+ # = How I can fetch Windows Live Contacts?
9
+ # To gain access to a Windows Live user's data in the Live Contacts service,
10
+ # a third-party developer first must ask the owner for permission. You must
11
+ # do that through Windows Live Delegated Authentication.
12
+ #
13
+ # This library give you access to Windows Live Delegated Authentication System
14
+ # and Windows Live Contacts API. Just follow the steps below and be happy!
15
+ #
16
+ # === Registering your app
17
+ # First of all, follow the steps in this
18
+ # page[http://msdn.microsoft.com/en-us/library/cc287659.aspx] to register your
19
+ # app.
20
+ #
21
+ # === Configuring your Windows Live YAML
22
+ # After registering your app, you will have an *appid*, a <b>secret key</b> and
23
+ # a <b>return URL</b>. Use their values to fill in the config/contacts.yml file.
24
+ # The policy URL field inside the YAML config file must contain the URL
25
+ # of the privacy policy of your Web site for Delegated Authentication.
26
+ #
27
+ # === Authenticating your user and fetching his contacts
28
+ #
29
+ # wl = Contacts::WindowsLive.new
30
+ # auth_url = wl.get_authentication_url
31
+ #
32
+ # Use that *auth_url* to redirect your user to Windows Live. He will authenticate
33
+ # there and Windows Live will POST to your return URL. You have to get the
34
+ # body of that POST, let's call it post_body. (if you're using Rails, you can
35
+ # get the POST body through request.raw_post, in the context of an action inside
36
+ # ActionController)
37
+ #
38
+ # Now, to fetch his contacts, just do this:
39
+ #
40
+ # contacts = wl.contacts(post_body)
41
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
42
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
43
+ # ]
44
+ #--
45
+ # This class has two responsibilities:
46
+ # 1. Access the Windows Live Contacts API through Delegated Authentication
47
+ # 2. Import contacts from Windows Live and deliver it inside an Array
48
+ #
49
+ class WindowsLive
50
+
51
+ attr_accessor :wll
52
+ # Initialize a new WindowsLive object.
53
+ #
54
+ # ==== Paramaters
55
+ # * config_file <String>:: The contacts YAML config file name
56
+ #--
57
+ # You can check an example of a config file inside config/ directory
58
+ #
59
+ def initialize(config_file)
60
+ confs = YAML.load_file(config_file)['windows_live']
61
+ @wll = WindowsLiveLogin.new(confs['appid'], confs['secret'], confs['security_algorithm'],
62
+ nil, confs['policy_url'], confs['return_url'])
63
+ end
64
+
65
+
66
+ # Windows Live Contacts API need to authenticate the user that is giving you
67
+ # access to his contacts. To do that, you must give him a URL. That method
68
+ # generates that URL. The user must access that URL, and after he has done
69
+ # authentication, hi will be redirected to your application.
70
+ #
71
+ def get_authentication_url(context=nil)
72
+ @wll.getConsentUrl("Contacts.View", context)
73
+ end
74
+
75
+ # After the user has been authenticaded, Windows Live Delegated Authencation
76
+ # Service redirects to your application, through a POST HTTP method. Along
77
+ # with the POST, Windows Live send to you a Consent that you must process
78
+ # to access the user's contacts. This method process the Consent
79
+ # to you.
80
+ #
81
+ # ==== Paramaters
82
+ # * consent <String>:: A string containing the Consent given to you inside
83
+ # the redirection POST from Windows Live
84
+ #
85
+ def process_consent(consent)
86
+ consent.strip!
87
+ consent = URI.unescape(consent)
88
+ @consent_token = @wll.processConsent(consent)
89
+ end
90
+
91
+ def process_consent_token(consent_token)
92
+ @wll.processConsentToken consent_token
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
+ # * consent <String>:: A string containing the Consent given to you inside
106
+ # the redirection POST from Windows Live
107
+ #
108
+ def contacts(consent)
109
+ if consent.is_a? WindowsLiveLogin::ConsentToken
110
+ @consent_token = consent
111
+ else
112
+ process_consent(consent)
113
+ end
114
+ contacts_xml = access_live_contacts_api()
115
+ contacts_list = WindowsLive.parse_xml(contacts_xml)
116
+ end
117
+
118
+ # This method access the Windows Live Contacts API Web Service to get
119
+ # the XML contacts document
120
+ #
121
+ def access_live_contacts_api
122
+ http = http = Net::HTTP.new('livecontacts.services.live.com', 443)
123
+ http.use_ssl = true
124
+
125
+ response = nil
126
+ http.start do |http|
127
+ request = Net::HTTP::Get.new("/users/@L@#{@consent_token.locationid}/rest/LiveContacts", {"Authorization" => "DelegatedToken dt=\"#{@consent_token.delegationtoken}\""})
128
+ response = http.request(request)
129
+ end
130
+ response.body
131
+ end
132
+
133
+ # This method parses the XML Contacts document and returns the contacts
134
+ # inside an Array
135
+ #
136
+ # ==== Paramaters
137
+ # * xml <String>:: A string containing the XML contacts document
138
+ #
139
+ def self.parse_xml(xml)
140
+ doc = Hpricot::XML(xml)
141
+ contacts = []
142
+ doc.search('/LiveContacts/Contacts/Contact') do |contact|
143
+
144
+ contact_id = text_value contact, "ID"
145
+
146
+ first_name = text_value contact, "Profiles/Personal/FirstName"
147
+ last_name = text_value contact, "Profiles/Personal/LastName"
148
+ name = "#{first_name} #{last_name}".strip
149
+ emails = contact.search('Emails/Email').collect {|e| text_value e, "Address"}
150
+
151
+ phones = contact.search('Phones/Phone').collect do |e|
152
+ type=convert_type(text_value(e, "PhoneType"))
153
+ { "type" => type, "value" => (text_value e, "Number") }
154
+ end
155
+
156
+ addresses = contact.search('Locations/Location').collect do |e|
157
+ street = text_value(e, "StreetLine")
158
+ postal_code = text_value(e, "PostalCode")
159
+ sub_division = text_value(e, "Subdivision")
160
+ city = text_value(e, "PrimaryCity")
161
+ country_code = text_value(e, "CountryRegion")
162
+ formatted = [street, city, sub_division, postal_code, country_code].compact.join(", ")
163
+ type = convert_type(text_value(e, "LocationType"))
164
+ { "formatted" => formatted, "type" => type, "streetAddress" => street, "locality" => city, "region" => sub_division, "postalCode" => postal_code, "country" => country_code}
165
+ end
166
+
167
+ new_contact = Contact.new(nil, name, nil, first_name, last_name)
168
+ new_contact.emails = emails
169
+ new_contact.phones = phones
170
+ new_contact.addresses = addresses
171
+ new_contact.service_id = contact_id
172
+ contacts << new_contact
173
+ end
174
+ return contacts
175
+ end
176
+
177
+ def self.text_value(elem,path)
178
+ elem.at(path).inner_text rescue nil
179
+ end
180
+
181
+ def self.convert_type(t)
182
+ {"personal" => "home", "business" => "work"}[t.downcase] || "other"
183
+ end
184
+ end
185
+
186
+ end
@@ -0,0 +1,331 @@
1
+ require 'contacts'
2
+
3
+ require 'hpricot'
4
+ require 'md5'
5
+ require 'net/https'
6
+ require 'uri'
7
+ require 'json'
8
+
9
+ module Contacts
10
+ # = How I can fetch Yahoo Contacts?
11
+ # To gain access to a Yahoo user's data in the Yahoo Address Book Service,
12
+ # a third-party developer first must ask the owner for permission. You must
13
+ # do that through Yahoo Browser Based Authentication (BBAuth).
14
+ #
15
+ # This library give you access to Yahoo BBAuth and Yahoo Address Book API.
16
+ # 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://developer.yahoo.com/wsregapp/] to register your app. If you need
21
+ # some help with that form, you can get it
22
+ # here[http://developer.yahoo.com/auth/appreg.html]. Just two tips: inside
23
+ # <b>Required access scopes</b> in that registration form, choose
24
+ # <b>Yahoo! Address Book with Read Only access</b>. Inside
25
+ # <b>Authentication method</b> choose <b>Browser Based Authentication</b>.
26
+ #
27
+ # === Configuring your Yahoo YAML
28
+ # After registering your app, you will have an <b>application id</b> and a
29
+ # <b>shared secret</b>. Use their values to fill in the config/contacts.yml
30
+ # file.
31
+ #
32
+ # === Authenticating your user and fetching his contacts
33
+ #
34
+ # yahoo = Contacts::Yahoo.new
35
+ # auth_url = yahoo.get_authentication_url
36
+ #
37
+ # Use that *auth_url* to redirect your user to Yahoo BBAuth. He will authenticate
38
+ # there and Yahoo will redirect to your application entrypoint URL (that you provided
39
+ # while registering your app with Yahoo). You have to get the path of that
40
+ # redirect, let's call it path (if you're using Rails, you can get it through
41
+ # request.request_uri, in the context of an action inside ActionController)
42
+ #
43
+ # Now, to fetch his contacts, just do this:
44
+ #
45
+ # contacts = wl.contacts(path)
46
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
47
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
48
+ # ]
49
+ #--
50
+ # This class has two responsibilities:
51
+ # 1. Access the Yahoo Address Book API through Delegated Authentication
52
+ # 2. Import contacts from Yahoo Mail and deliver it inside an Array
53
+ #
54
+ class Yahoo
55
+ AUTH_DOMAIN = "https://api.login.yahoo.com"
56
+ AUTH_PATH = "/WSLogin/V1/wslogin?appid=#appid&ts=#ts"
57
+ CREDENTIAL_PATH = "/WSLogin/V1/wspwtoken_login?appid=#appid&ts=#ts&token=#token"
58
+ ADDRESS_BOOK_DOMAIN = "address.yahooapis.com"
59
+ ADDRESS_BOOK_PATH = "/v1/searchContacts?format=json&fields=all&appid=#appid&WSSID=#wssid"
60
+ CONFIG_FILE = File.dirname(__FILE__) + '/../config/contacts.yml'
61
+
62
+ attr_reader :appid, :secret, :token, :wssid, :cookie
63
+
64
+ # Initialize a new Yahoo object.
65
+ #
66
+ # ==== Paramaters
67
+ # * config_file <String>:: The contacts YAML config file name
68
+ #--
69
+ # You can check an example of a config file inside config/ directory
70
+ #
71
+ def initialize(config_file=CONFIG_FILE)
72
+ confs = YAML.load_file(config_file)['yahoo']
73
+ @appid = confs['appid']
74
+ @secret = confs['secret']
75
+ end
76
+
77
+ # Yahoo Address Book API need to authenticate the user that is giving you
78
+ # access to his contacts. To do that, you must give him a URL. This method
79
+ # generates that URL. The user must access that URL, and after he has done
80
+ # authentication, hi will be redirected to your application.
81
+ #
82
+ def get_authentication_url(appdata= nil)
83
+ path = AUTH_PATH.clone
84
+ path.sub!(/#appid/, @appid)
85
+
86
+ timestamp = Time.now.utc.to_i
87
+ path.sub!(/#ts/, timestamp.to_s)
88
+
89
+ path<< "&appdata=#{appdata}" unless appdata.nil?
90
+
91
+ signature = MD5.hexdigest(path + @secret)
92
+ "#{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(token)
109
+ begin
110
+ if token.is_a?(YahooToken)
111
+ @token = token.token
112
+ else
113
+ validate_signature(token)
114
+ end
115
+ credentials = access_user_credentials()
116
+ parse_credentials(credentials)
117
+ contacts_json = access_address_book_api()
118
+ Yahoo.parse_contacts(contacts_json)
119
+ rescue Exception => e
120
+ "Error #{e.class}: #{e.message}."
121
+ end
122
+ end
123
+
124
+ # This method processes and validates the redirect request that Yahoo send to
125
+ # you. Validation is done to verify that the request was really made by
126
+ # Yahoo. Processing is done to get the token.
127
+ #
128
+ # ==== Paramaters
129
+ # * path <String>:: The path of the redirect request that Yahoo sent to you
130
+ # after authenticating the user
131
+ #
132
+ def validate_signature(path)
133
+ path.match(/^(.+)&sig=(\w{32})$/)
134
+ path_without_sig = $1
135
+ sig = $2
136
+
137
+ if sig == MD5.hexdigest(path_without_sig + @secret)
138
+ path.match(/token=(.+?)&/)
139
+ @token = $1
140
+ return true
141
+ else
142
+ raise 'Signature not valid. This request may not have been sent from Yahoo.'
143
+ end
144
+ end
145
+
146
+ # This method accesses Yahoo to retrieve the user's credentials.
147
+ #
148
+ def access_user_credentials
149
+ url = get_credential_url()
150
+ uri = URI.parse(url)
151
+
152
+ http = http = Net::HTTP.new(uri.host, uri.port)
153
+ http.use_ssl = true
154
+
155
+ response = nil
156
+ http.start do |http|
157
+ request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
158
+ response = http.request(request)
159
+ end
160
+
161
+ return response.body
162
+ end
163
+
164
+ # This method generates the URL that you must access to get user's
165
+ # credentials.
166
+ #
167
+ def get_credential_url
168
+ path = CREDENTIAL_PATH.clone
169
+ path.sub!(/#appid/, @appid)
170
+
171
+ path.sub!(/#token/, @token)
172
+
173
+ timestamp = Time.now.utc.to_i
174
+ path.sub!(/#ts/, timestamp.to_s)
175
+
176
+ signature = MD5.hexdigest(path + @secret)
177
+ return AUTH_DOMAIN + "#{path}&sig=#{signature}"
178
+ end
179
+
180
+ # This method parses the user's credentials to generate the WSSID and
181
+ # Coookie that are needed to give you access to user's address book.
182
+ #
183
+ # ==== Paramaters
184
+ # * xml <String>:: A String containing the user's credentials
185
+ #
186
+ def parse_credentials(xml)
187
+ doc = Hpricot::XML(xml)
188
+ @wssid = doc.at('/BBAuthTokenLoginResponse/Success/WSSID').inner_text.strip
189
+ @cookie = doc.at('/BBAuthTokenLoginResponse/Success/Cookie').inner_text.strip
190
+ end
191
+
192
+ # This method accesses the Yahoo Address Book API and retrieves the user's
193
+ # contacts in JSON.
194
+ #
195
+ def access_address_book_api
196
+ http = http = Net::HTTP.new(ADDRESS_BOOK_DOMAIN, 80)
197
+
198
+ response = nil
199
+ http.start do |http|
200
+ path = ADDRESS_BOOK_PATH.clone
201
+ path.sub!(/#appid/, @appid)
202
+ path.sub!(/#wssid/, @wssid)
203
+
204
+ request = Net::HTTP::Get.new(path, {'Cookie' => @cookie})
205
+ response = http.request(request)
206
+ end
207
+
208
+ response.body
209
+ end
210
+
211
+ # This method parses the JSON contacts document and returns an array
212
+ # contaning all the user's contacts.
213
+ #
214
+ # ==== Parameters
215
+ # * json <String>:: A String of user's contacts in JSON format
216
+ # "fields": [
217
+ # {
218
+ # "type": "phone",
219
+ # "data": "808 123 1234",
220
+ # "home": true,
221
+ # },
222
+ # {
223
+ # "type": "email",
224
+ # "data": "martin.berner@mail.com",
225
+ # },
226
+ #
227
+ # {
228
+ # "type": "otherid",
229
+ # "data": "windowslive@msn.com",
230
+ # "msn": true,
231
+ # }
232
+ # ]
233
+ #
234
+ def self.parse_contacts(json)
235
+ contacts = []
236
+ people = JSON.parse(json)
237
+ people['contacts'].each do |contact|
238
+ name = nil
239
+ email = nil
240
+ firstname = nil
241
+ lastname = nil
242
+
243
+ contact_fields=Yahoo.array_to_hash contact['fields']
244
+
245
+ emails = (contact_fields['email'] || []).collect {|e| e['data']}
246
+ ims = (contact_fields['otherid'] || []).collect { |im| get_type_value(im) }
247
+ phones = (contact_fields['phone'] || []).collect { |phone| get_type_value(phone) }
248
+ addresses = (contact_fields['address'] || []).collect do |address|
249
+ type=get_type(address)
250
+ type = {"home" => "home", "work" => "work"}[type.downcase] || "other"
251
+ value = [address['street'], address['city'], address['state'], address['zip'], address['country']].compact.join(", ")
252
+ {"type" => type, "value" => value}
253
+ end
254
+
255
+ name_field=(contact_fields['name'] || [])
256
+
257
+ # if name is blank, try go for the yahoo id, and if that's blank too, ignore the record altogether (probably a mailing list)
258
+ if (name_field.empty?)
259
+ if contact_fields['yahooid']
260
+ name = contact_fields['yahooid'][0]['data']
261
+ else
262
+ next
263
+ end
264
+ else
265
+ name_field = name_field[0]
266
+ name = "#{name_field['first']} #{name_field['last']}"
267
+ name.strip!
268
+ lastname = name_field['last']
269
+ firstname = name_field['first']
270
+ end
271
+
272
+ yahoo_contact = Contact.new(nil, name, nil, firstname, lastname)
273
+ yahoo_contact.emails = emails
274
+ yahoo_contact.ims = ims
275
+ yahoo_contact.phones = phones
276
+ yahoo_contact.addresses = addresses
277
+ yahoo_contact.service_id = contact['cid']
278
+
279
+ contacts.push yahoo_contact
280
+ end
281
+ contacts
282
+ end
283
+
284
+ #
285
+ # grab the type field from each array item
286
+ # and turn it into a "email"=>{}, "phone"=>{} array
287
+ #
288
+ private
289
+ def self.array_to_hash(a)
290
+ (a || []).inject({}) {|x,y|
291
+ x[y['type']] ||= []
292
+ x[y['type']] << y
293
+ x
294
+ }
295
+ end
296
+
297
+ #
298
+ # return type/value from a datastructure like
299
+ # {
300
+ # "data": "808 456 7890",
301
+ # "mobile": true
302
+ # }
303
+ # -----> "type"=>"mobile", "value"=>"808 456 7890"
304
+ #
305
+ def self.get_type_value(hash)
306
+ type_field = hash.find{ |x| x[1] == true }
307
+ type = type_field ? type_field[0] : nil
308
+ {"type" => type, "value" => hash["data"]}
309
+ end
310
+
311
+ #
312
+ # return just the type from a datastructure like
313
+ # {
314
+ # "data": "808 456 7890",
315
+ # "mobile": true
316
+ # }
317
+ # -----> "mobile"
318
+ #
319
+ def self.get_type(hash)
320
+ type_field = hash.find{ |x| x[1] == true }
321
+ type = type_field ? type_field[0] : nil
322
+ end
323
+
324
+ end
325
+ class YahooToken
326
+ attr_reader :token
327
+ def initialize(token)
328
+ @token = token
329
+ end
330
+ end
331
+ end