import-pojo 0.5.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,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
+