import-pojo 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Mislav Marohnić
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,47 @@
1
+ == Basic usage instructions
2
+
3
+ Fetch users' contact lists from your web application without asking them to
4
+ provide their passwords.
5
+
6
+ First, register[http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html]
7
+ your application's domain. Then make users follow this URL:
8
+
9
+ Contacts::Google.authentication_url('http://mysite.com/invite')
10
+
11
+ They will authenticate on Google and it will send them back to the URL
12
+ provided. Google will add a token GET parameter to the query part of the URL.
13
+ Use that token in the next step:
14
+
15
+ gmail = Contacts::Google.new('example@gmail.com', params[:token])
16
+ contacts = gmail.contacts
17
+ #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
18
+ ['William Paginate', 'will.paginate@gmail.com'], ...
19
+ ]
20
+
21
+ Read more in Contacts::Google. I plan to support more APIs (Microsoft Live, for
22
+ starters); feel free to contribute.
23
+
24
+ Author: <b>Mislav Marohnić</b> (mislav.marohnic@gmail.com)
25
+
26
+ == Documentation auto-generated from specifications
27
+
28
+ Contacts::Google.authentication_url
29
+ - generates a URL for target with default parameters
30
+ - should handle boolean parameters
31
+ - skips parameters that have nil value
32
+ - should be able to exchange one-time for session token
33
+
34
+ Contacts::Google
35
+ - fetches contacts feed via HTTP GET
36
+ - handles a normal response body
37
+ - handles gzipped response
38
+ - raises a FetchingError when something goes awry
39
+ - parses the resulting feed into name/email pairs
40
+ - parses a complex feed into name/email pairs
41
+ - makes modification time available after parsing
42
+
43
+ Contacts::Google GET query parameter handling
44
+ - abstracts ugly parameters behind nicer ones
45
+ - should have implicit :descending with :order
46
+ - should have default :limit of 200
47
+ - should skip nil values in parameters
@@ -0,0 +1,55 @@
1
+ require 'spec/rake/spectask'
2
+ require 'rake/rdoctask'
3
+
4
+ task :default => :spec
5
+
6
+ spec_opts = 'spec/spec.opts'
7
+ spec_glob = FileList['spec/**/*_spec.rb']
8
+ libs = ['lib', 'spec', 'vendor/fakeweb/lib']
9
+
10
+ desc 'Run all specs in spec directory'
11
+ Spec::Rake::SpecTask.new(:spec) do |t|
12
+ t.libs = libs
13
+ t.spec_opts = ['--options', spec_opts]
14
+ t.spec_files = spec_glob
15
+ # t.warning = true
16
+ end
17
+
18
+ namespace :spec do
19
+ desc 'Analyze spec coverage with RCov'
20
+ Spec::Rake::SpecTask.new(:rcov) do |t|
21
+ t.libs = libs
22
+ t.spec_files = spec_glob
23
+ t.spec_opts = ['--options', spec_opts]
24
+ t.rcov = true
25
+ t.rcov_opts = lambda do
26
+ IO.readlines('spec/rcov.opts').map { |l| l.chomp.split(" ") }.flatten
27
+ end
28
+ end
29
+
30
+ desc 'Print Specdoc for all specs'
31
+ Spec::Rake::SpecTask.new(:doc) do |t|
32
+ t.libs = libs
33
+ t.spec_opts = ['--format', 'specdoc', '--dry-run']
34
+ t.spec_files = spec_glob
35
+ end
36
+
37
+ desc 'Generate HTML report'
38
+ Spec::Rake::SpecTask.new(:html) do |t|
39
+ t.libs = libs
40
+ t.spec_opts = ['--format', 'html:doc/spec.html', '--diff']
41
+ t.spec_files = spec_glob
42
+ t.fail_on_error = false
43
+ end
44
+ end
45
+
46
+ desc 'Generate RDoc documentation'
47
+ Rake::RDocTask.new(:rdoc) do |rdoc|
48
+ rdoc.rdoc_files.add ['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
49
+ rdoc.main = 'README.rdoc'
50
+ rdoc.title = 'Ruby Contacts library'
51
+
52
+ rdoc.rdoc_dir = 'doc'
53
+ rdoc.options << '--inline-source'
54
+ rdoc.options << '--charset=UTF-8'
55
+ end
@@ -0,0 +1,46 @@
1
+ require 'contacts/version'
2
+
3
+ module Contacts
4
+
5
+ Identifier = 'Ruby Contacts v' + VERSION::STRING
6
+
7
+ # An object that represents a single contact
8
+ class Contact
9
+ attr_reader :name, :username, :emails
10
+
11
+ def initialize(email, name = nil, username = nil)
12
+ @emails = []
13
+ @emails << email if email
14
+ @name = name
15
+ @username = username
16
+ end
17
+
18
+ def email
19
+ @emails.first
20
+ end
21
+
22
+ def inspect
23
+ %!#<Contacts::Contact "#{name}"#{email ? " (#{email})" : ''}>!
24
+ end
25
+ end
26
+
27
+ def self.verbose?
28
+ 'irb' == $0
29
+ end
30
+
31
+ class Error < StandardError
32
+ end
33
+
34
+ class TooManyRedirects < Error
35
+ attr_reader :response, :location
36
+
37
+ MAX_REDIRECTS = 2
38
+
39
+ def initialize(response)
40
+ @response = response
41
+ @location = @response['Location']
42
+ super "exceeded maximum of #{MAX_REDIRECTS} redirects (Location: #{location})"
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,133 @@
1
+ require 'contacts'
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'md5'
6
+ require 'cgi'
7
+ require 'time'
8
+ require 'zlib'
9
+ require 'stringio'
10
+ require 'net/http'
11
+
12
+ module Contacts
13
+
14
+ class Flickr
15
+ DOMAIN = 'api.flickr.com'
16
+ ServicesPath = '/services/rest/'
17
+
18
+ def self.frob_url(key, secret)
19
+ url_for(:api_key => key, :secret => secret, :method => 'flickr.auth.getFrob')
20
+ end
21
+
22
+ def self.frob_from_response(response)
23
+ doc = Hpricot::XML response.body
24
+ doc.at('frob').inner_text
25
+ end
26
+
27
+ def self.authentication_url_for_frob(frob, key, secret)
28
+ params = { :api_key => key, :secret => secret, :perms => 'read', :frob => frob }
29
+ 'http://www.flickr.com/services/auth/?' + query_string(params)
30
+ end
31
+
32
+ def self.authentication_url(key, secret)
33
+ response = http_start do |flickr|
34
+ flickr.get(frob_url(key, secret))
35
+ end
36
+ authentication_url_for_frob(frob_from_response(response), key, secret)
37
+ end
38
+
39
+ def self.token_url(key, secret, frob)
40
+ params = { :api_key => key, :secret => secret, :frob => frob, :method => 'flickr.auth.getToken' }
41
+ url_for(params)
42
+ end
43
+
44
+ def self.get_token_from_frob(key, secret, frob)
45
+ response = http_start do |flickr|
46
+ flickr.get(token_url(key, secret, frob))
47
+ end
48
+ doc = Hpricot::XML response.body
49
+ doc.at('token').inner_text
50
+ end
51
+
52
+ private
53
+ # Use the key-sorted version of the parameters to construct
54
+ # a string, to which the secret is prepended.
55
+
56
+ def self.sort_params(params)
57
+ params.sort do |a,b|
58
+ a.to_s <=> b.to_s
59
+ end
60
+ end
61
+
62
+ def self.string_to_sign(params, secret)
63
+ string_to_sign = secret + sort_params(params).inject('') do |str, pair|
64
+ key, value = pair
65
+ str + key.to_s + value.to_s
66
+ end
67
+ end
68
+
69
+ # Get the MD5 digest of the string to sign
70
+ def self.get_signature(params, secret)
71
+ ::Digest::MD5.hexdigest(string_to_sign(params, secret))
72
+ end
73
+
74
+ def self.query_string(params)
75
+ secret = params.delete(:secret)
76
+ params[:api_sig] = get_signature(params, secret)
77
+
78
+ params.inject([]) do |arr, pair|
79
+ key, value = pair
80
+ arr << "#{key}=#{value}"
81
+ end.join('&')
82
+ end
83
+
84
+ def self.url_for(params)
85
+ ServicesPath + '?' + query_string(params)
86
+ end
87
+
88
+ def self.http_start(ssl = false)
89
+ port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
90
+ http = Net::HTTP.new(DOMAIN, port)
91
+ redirects = 0
92
+ if ssl
93
+ http.use_ssl = true
94
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
95
+ end
96
+ http.start
97
+
98
+ begin
99
+ response = yield(http)
100
+
101
+ loop do
102
+ inspect_response(response) if Contacts::verbose?
103
+
104
+ case response
105
+ when Net::HTTPSuccess
106
+ break response
107
+ when Net::HTTPRedirection
108
+ if redirects == TooManyRedirects::MAX_REDIRECTS
109
+ raise TooManyRedirects.new(response)
110
+ end
111
+ location = URI.parse response['Location']
112
+ puts "Redirected to #{location}"
113
+ response = http.get(location.path)
114
+ redirects += 1
115
+ else
116
+ response.error!
117
+ end
118
+ end
119
+ ensure
120
+ http.finish
121
+ end
122
+ end
123
+
124
+ def self.inspect_response(response, out = $stderr)
125
+ out.puts response.inspect
126
+ for name, value in response
127
+ out.puts "#{name}: #{value}"
128
+ end
129
+ out.puts "----\n#{response.body}\n----" unless response.body.empty?
130
+ end
131
+ end
132
+
133
+ end
@@ -0,0 +1,308 @@
1
+ require 'contacts'
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'cgi'
6
+ require 'time'
7
+ require 'zlib'
8
+ require 'stringio'
9
+ require 'net/http'
10
+ require 'net/https'
11
+
12
+ module Contacts
13
+ # == Fetching Google Contacts
14
+ #
15
+ # First, get the user to follow the following URL:
16
+ #
17
+ # Contacts::Google.authentication_url('http://mysite.com/invite')
18
+ #
19
+ # After he authenticates successfully to Google, it will redirect him back to the target URL
20
+ # (specified as argument above) and provide the token GET parameter. Use it to create a
21
+ # new instance of this class and request the contact list:
22
+ #
23
+ # gmail = Contacts::Google.new(params[:token])
24
+ # contacts = gmail.contacts
25
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
26
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
27
+ # ]
28
+ #
29
+ # == Storing a session token
30
+ #
31
+ # The basic token that you will get after the user has authenticated on Google is valid
32
+ # for <b>only one request</b>. However, you can specify that you want a session token which
33
+ # doesn't expire:
34
+ #
35
+ # Contacts::Google.authentication_url('http://mysite.com/invite', :session => true)
36
+ #
37
+ # When the user authenticates, he will be redirected back with a token that can be exchanged
38
+ # for a session token with the following method:
39
+ #
40
+ # token = Contacts::Google.sesion_token(params[:token])
41
+ #
42
+ # Now you have a permanent token. Store it with other user data so you can query the API
43
+ # on his behalf without him having to authenticate on Google each time.
44
+ class Google
45
+ DOMAIN = 'www.google.com'
46
+ AuthSubPath = '/accounts/AuthSub' # all variants go over HTTPS
47
+ ClientLogin = '/accounts/ClientLogin'
48
+ FeedsPath = '/m8/feeds/contacts/'
49
+
50
+ # default options for #authentication_url
51
+ def self.authentication_url_options
52
+ @authentication_url_options ||= {
53
+ :scope => "http://#{DOMAIN}#{FeedsPath}",
54
+ :secure => false,
55
+ :session => false
56
+ }
57
+ end
58
+
59
+ # default options for #client_login
60
+ def self.client_login_options
61
+ @client_login_options ||= {
62
+ :accountType => 'GOOGLE',
63
+ :service => 'cp',
64
+ :source => 'Contacts-Ruby'
65
+ }
66
+ end
67
+
68
+ # URL to Google site where user authenticates. Afterwards, Google redirects to your
69
+ # site with the URL specified as +target+.
70
+ #
71
+ # Options are:
72
+ # * <tt>:scope</tt> -- the AuthSub scope in which the resulting token is valid
73
+ # (default: "http://www.google.com/m8/feeds/contacts/")
74
+ # * <tt>:secure</tt> -- boolean indicating whether the token will be secure. Only available
75
+ # for registered domains.
76
+ # (default: false)
77
+ # * <tt>:session</tt> -- boolean indicating if the token can be exchanged for a session token
78
+ # (default: false)
79
+ def self.authentication_url(target, options = {})
80
+ params = authentication_url_options.merge(options)
81
+ params[:next] = target
82
+ query = query_string(params)
83
+ "https://#{DOMAIN}#{AuthSubPath}Request?#{query}"
84
+ end
85
+
86
+ # Makes an HTTPS request to exchange the given token with a session one. Session
87
+ # tokens never expire, so you can store them in the database alongside user info.
88
+ #
89
+ # Returns the new token as string or nil if the parameter couldn't be found in response
90
+ # body.
91
+ def self.session_token(token)
92
+ response = http_start do |google|
93
+ google.get(AuthSubPath + 'SessionToken', authorization_header(token))
94
+ end
95
+
96
+ pair = response.body.split(/\n/).detect { |p| p.index('Token=') == 0 }
97
+ pair.split('=').last if pair
98
+ end
99
+
100
+ # Alternative to AuthSub: using email and password.
101
+ def self.client_login(email, password)
102
+ response = http_start do |google|
103
+ query = query_string(client_login_options.merge(:Email => email, :Passwd => password))
104
+ puts "posting #{query} to #{ClientLogin}" if Contacts::verbose?
105
+ google.post(ClientLogin, query)
106
+ end
107
+
108
+ pair = response.body.split(/\n/).detect { |p| p.index('Auth=') == 0 }
109
+ pair.split('=').last if pair
110
+ end
111
+
112
+ attr_reader :user, :token, :headers
113
+ attr_accessor :projection
114
+
115
+ # A token is required here. By default, an AuthSub token from
116
+ # Google is one-time only, which means you can only make a single request with it.
117
+ def initialize(token, user_id = 'default', client = false)
118
+ @user = user_id.to_s
119
+ @token = token.to_s
120
+ @headers = {
121
+ 'Accept-Encoding' => 'gzip',
122
+ 'User-Agent' => Identifier + ' (gzip)'
123
+ }.update(self.class.authorization_header(@token, client))
124
+ @projection = 'thin'
125
+ end
126
+
127
+ def get(params) # :nodoc:
128
+ self.class.http_start(false) do |google|
129
+ path = FeedsPath + CGI.escape(@user)
130
+ google_params = translate_parameters(params)
131
+ query = self.class.query_string(google_params)
132
+ google.get("#{path}/#{@projection}?#{query}", @headers)
133
+ end
134
+ end
135
+
136
+ # Timestamp of last update. This value is available only after the XML
137
+ # document has been parsed; for instance after fetching the contact list.
138
+ def updated_at
139
+ @updated_at ||= Time.parse @updated_string if @updated_string
140
+ end
141
+
142
+ # Timestamp of last update as it appeared in the XML document
143
+ def updated_at_string
144
+ @updated_string
145
+ end
146
+
147
+ # Fetches, parses and returns the contact list.
148
+ #
149
+ # ==== Options
150
+ # * <tt>:limit</tt> -- use a large number to fetch a bigger contact list (default: 200)
151
+ # * <tt>:offset</tt> -- 0-based value, can be used for pagination
152
+ # * <tt>:order</tt> -- currently the only value support by Google is "lastmodified"
153
+ # * <tt>:descending</tt> -- boolean
154
+ # * <tt>:updated_after</tt> -- string or time-like object, use to only fetch contacts
155
+ # that were updated after this date
156
+ def contacts(options = {})
157
+ params = { :limit => 200 }.update(options)
158
+ response = get(params)
159
+ parse_contacts response_body(response)
160
+ end
161
+
162
+ # Fetches contacts using multiple API calls when necessary
163
+ def all_contacts(options = {}, chunk_size = 200)
164
+ in_chunks(options, :contacts, chunk_size)
165
+ end
166
+
167
+ protected
168
+
169
+ def in_chunks(options, what, chunk_size)
170
+ returns = []
171
+ offset = 0
172
+
173
+ begin
174
+ chunk = send(what, options.merge(:offset => offset, :limit => chunk_size))
175
+ returns.push(*chunk)
176
+ offset += chunk_size
177
+ end while chunk.size == chunk_size
178
+
179
+ returns
180
+ end
181
+
182
+ def response_body(response)
183
+ unless response['Content-Encoding'] == 'gzip'
184
+ response.body
185
+ else
186
+ gzipped = StringIO.new(response.body)
187
+ Zlib::GzipReader.new(gzipped).read
188
+ end
189
+ end
190
+
191
+ def parse_contacts(body)
192
+ doc = Hpricot::XML body
193
+ contacts_found = []
194
+
195
+ if updated_node = doc.at('/feed/updated')
196
+ @updated_string = updated_node.inner_text
197
+ end
198
+
199
+ (doc / '/feed/entry').each do |entry|
200
+ email_nodes = entry / 'gd:email[@address]'
201
+
202
+ unless email_nodes.empty?
203
+ title_node = entry.at('/title')
204
+ name = title_node ? title_node.inner_text : nil
205
+ contact = Contact.new(nil, name)
206
+ contact.emails.concat email_nodes.map { |e| e['address'].to_s }
207
+ contacts_found << contact
208
+ end
209
+ end
210
+
211
+ contacts_found
212
+ end
213
+
214
+ # Constructs a query string from a Hash object
215
+ def self.query_string(params)
216
+ params.inject([]) do |all, pair|
217
+ key, value = pair
218
+ unless value.nil?
219
+ value = case value
220
+ when TrueClass; '1'
221
+ when FalseClass; '0'
222
+ else value
223
+ end
224
+
225
+ all << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
226
+ end
227
+ all
228
+ end.join('&')
229
+ end
230
+
231
+ def translate_parameters(params)
232
+ params.inject({}) do |all, pair|
233
+ key, value = pair
234
+ unless value.nil?
235
+ key = case key
236
+ when :limit
237
+ 'max-results'
238
+ when :offset
239
+ value = value.to_i + 1
240
+ 'start-index'
241
+ when :order
242
+ all['sortorder'] = 'descending' if params[:descending].nil?
243
+ 'orderby'
244
+ when :descending
245
+ value = value ? 'descending' : 'ascending'
246
+ 'sortorder'
247
+ when :updated_after
248
+ value = value.strftime("%Y-%m-%dT%H:%M:%S%Z") if value.respond_to? :strftime
249
+ 'updated-min'
250
+ else key
251
+ end
252
+
253
+ all[key] = value
254
+ end
255
+ all
256
+ end
257
+ end
258
+
259
+ def self.authorization_header(token, client = false)
260
+ type = client ? 'GoogleLogin auth' : 'AuthSub token'
261
+ { 'Authorization' => %(#{type}="#{token}") }
262
+ end
263
+
264
+ def self.http_start(ssl = true)
265
+ port = ssl ? Net::HTTP::https_default_port : Net::HTTP::http_default_port
266
+ http = Net::HTTP.new(DOMAIN, port)
267
+ redirects = 0
268
+ if ssl
269
+ http.use_ssl = true
270
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
271
+ end
272
+ http.start
273
+
274
+ begin
275
+ response = yield(http)
276
+
277
+ loop do
278
+ inspect_response(response) if Contacts::verbose?
279
+
280
+ case response
281
+ when Net::HTTPSuccess
282
+ break response
283
+ when Net::HTTPRedirection
284
+ if redirects == TooManyRedirects::MAX_REDIRECTS
285
+ raise TooManyRedirects.new(response)
286
+ end
287
+ location = URI.parse response['Location']
288
+ puts "Redirected to #{location}"
289
+ response = http.get(location.path)
290
+ redirects += 1
291
+ else
292
+ response.error!
293
+ end
294
+ end
295
+ ensure
296
+ http.finish
297
+ end
298
+ end
299
+
300
+ def self.inspect_response(response, out = $stderr)
301
+ out.puts response.inspect
302
+ for name, value in response
303
+ out.puts "#{name}: #{value}"
304
+ end
305
+ out.puts "----\n#{response_body response}\n----" unless response.body.empty?
306
+ end
307
+ end
308
+ end
@@ -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
+ http = http = Net::HTTP.new('livecontacts.services.live.com', 443)
117
+ http.use_ssl = true
118
+
119
+ response = nil
120
+ http.start do |http|
121
+ request = Net::HTTP::Get.new("/users/@L@#{@consent_token.locationid}/rest/invitationsbyemail", {"Authorization" => "DelegatedToken dt=\"#{@consent_token.delegationtoken}\""})
122
+ response = http.request(request)
123
+ end
124
+
125
+ return response.body
126
+ end
127
+
128
+ # This method parses the XML Contacts document and returns the contacts
129
+ # inside an Array
130
+ #
131
+ # ==== Paramaters
132
+ # * xml <String>:: A string containing the XML contacts document
133
+ #
134
+ def self.parse_xml(xml)
135
+ doc = Hpricot::XML(xml)
136
+
137
+ contacts = []
138
+ doc.search('/livecontacts/contacts/contact').each do |contact|
139
+ email = contact.at('/preferredemail').inner_text
140
+ email.strip!
141
+
142
+ first_name = last_name = nil
143
+ if first_name = contact.at('/profiles/personal/firstname')
144
+ first_name = first_name.inner_text.strip
145
+ end
146
+
147
+ if last_name = contact.at('/profiles/personal/lastname')
148
+ last_name = last_name.inner_text.strip
149
+ end
150
+
151
+ name = nil
152
+ if !first_name.nil? || !last_name.nil?
153
+ name = "#{first_name} #{last_name}"
154
+ name.strip!
155
+ end
156
+ new_contact = Contact.new(email, name)
157
+ contacts << new_contact
158
+ end
159
+
160
+ return contacts
161
+ end
162
+ end
163
+
164
+ end
@@ -0,0 +1,240 @@
1
+ require 'contacts'
2
+
3
+ require 'rubygems'
4
+ require 'hpricot'
5
+ require 'md5'
6
+ require 'net/https'
7
+ require 'uri'
8
+ require 'yaml'
9
+ require 'json' unless defined? ActiveSupport::JSON
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
+ # ==== Parameters
211
+ # * json <String>:: A String of user's contacts in JSON format
212
+ #
213
+ def self.parse_contacts(json)
214
+ contacts = []
215
+ people = if defined? ActiveSupport::JSON
216
+ ActiveSupport::JSON.decode(json)
217
+ else
218
+ JSON.parse(json)
219
+ end
220
+
221
+ people['contacts'].each do |contact|
222
+ name = nil
223
+ email = nil
224
+ contact['fields'].each do |field|
225
+ case field['type']
226
+ when 'email'
227
+ email = field['data']
228
+ email.strip!
229
+ when 'name'
230
+ name = "#{field['first']} #{field['last']}"
231
+ name.strip!
232
+ end
233
+ end
234
+ contacts.push Contact.new(email, name)
235
+ end
236
+ return contacts
237
+ end
238
+
239
+ end
240
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: import-pojo
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 5
8
+ - 1
9
+ version: 0.5.1
10
+ platform: ruby
11
+ authors:
12
+ - Josiah Kiehl
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-15 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: |-
22
+ Lib to pull contacts from Gmail, Yahoo!, Windows Live, and Flickr. This lib uses authentication tokens rather than requiring the user to enter their private credentials. This is a fork from rscarvalho/contacts (which is a fork of mislav/contacts).
23
+
24
+ I couldn't track down a published gem, so I made one.
25
+ email: bluepojo+rubygems@gmail.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files: []
31
+
32
+ files:
33
+ - lib/contacts/version.rb
34
+ - lib/contacts/windows_live.rb
35
+ - lib/contacts/yahoo.rb
36
+ - lib/contacts/google.rb
37
+ - lib/contacts/flickr.rb
38
+ - lib/contacts.rb
39
+ - Rakefile
40
+ - README.rdoc
41
+ - MIT-LICENSE
42
+ has_rdoc: true
43
+ homepage: http://github.com/bluepojo/contacts/
44
+ licenses: []
45
+
46
+ post_install_message:
47
+ rdoc_options: []
48
+
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ version: "0"
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.3.6
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Lib to pull contacts from Gmail, Yahoo!, Windows Live, and Flickr.
72
+ test_files: []
73
+