nullstyle-contacts 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -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.
data/README.rdoc ADDED
@@ -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
data/Rakefile ADDED
@@ -0,0 +1,49 @@
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
+
9
+ desc 'Run all specs in spec directory'
10
+ Spec::Rake::SpecTask.new(:spec) do |t|
11
+ t.spec_opts = ['--options', spec_opts]
12
+ t.spec_files = spec_glob
13
+ end
14
+
15
+ namespace :spec do
16
+ desc 'Run all specs in spec directory with RCov'
17
+ Spec::Rake::SpecTask.new(:rcov) do |t|
18
+ t.spec_opts = ['--options', spec_opts]
19
+ t.spec_files = spec_glob
20
+ t.rcov = true
21
+ # t.rcov_opts = lambda do
22
+ # IO.readlines('spec/rcov.opts').map {|l| l.chomp.split " "}.flatten
23
+ # end
24
+ end
25
+
26
+ desc 'Print Specdoc for all specs'
27
+ Spec::Rake::SpecTask.new(:doc) do |t|
28
+ t.spec_opts = ['--format', 'specdoc', '--dry-run']
29
+ t.spec_files = spec_glob
30
+ end
31
+
32
+ desc 'Generate HTML report'
33
+ Spec::Rake::SpecTask.new(:html) do |t|
34
+ t.spec_opts = ['--format', 'html:doc/spec_results.html', '--diff']
35
+ t.spec_files = spec_glob
36
+ t.fail_on_error = false
37
+ end
38
+ end
39
+
40
+ desc 'Generate RDoc documentation'
41
+ Rake::RDocTask.new(:rdoc) do |rdoc|
42
+ rdoc.rdoc_files.add ['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
43
+ rdoc.main = 'README.rdoc'
44
+ rdoc.title = 'Google Contacts API'
45
+
46
+ rdoc.rdoc_dir = 'doc'
47
+ rdoc.options << '--inline-source'
48
+ rdoc.options << '--charset=UTF-8'
49
+ end
data/contacts.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "contacts"
3
+ s.version = "0.1"
4
+ s.date = "2008-04-29"
5
+ s.summary = "import contacts from google Contacts API"
6
+ s.email = "mislav.marohnic@gmail.com"
7
+ s.homepage = "http://test-spec.rubyforge.org"
8
+
9
+ s.description = <<EOF
10
+ Fetch users' contact lists from your web application without asking them to
11
+ provide their passwords.
12
+ EOF
13
+
14
+ s.has_rdoc = true
15
+ s.authors = ["Mislav Marohnić", "Scott Fleckenstein"]
16
+
17
+ s.files = [
18
+ "README.rdoc",
19
+ "MIT-LICENSE",
20
+ "Rakefile",
21
+ "contacts.gemspec",
22
+ "lib/contacts/google.rb",
23
+ "lib/contacts.rb",
24
+ ]
25
+
26
+ s.test_files = []
27
+
28
+ s.rdoc_options = ["--main", "README.rdoc"]
29
+ s.extra_rdoc_files = ["README.rdoc"]
30
+ end
data/lib/contacts.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Contacts
2
+ class FetchingError < RuntimeError
3
+ attr_reader :response
4
+
5
+ def initialize(response)
6
+ @response = response
7
+ super "expected HTTPSuccess, got #{response.class} (#{response.code} #{response.message})"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,213 @@
1
+ require 'contacts'
2
+ require 'cgi'
3
+ require 'net/http'
4
+ require 'net/https'
5
+ require 'rubygems'
6
+ require 'hpricot'
7
+ require 'time'
8
+ require 'zlib'
9
+ require 'stringio'
10
+
11
+ module Contacts
12
+ # == Fetching Google Contacts
13
+ #
14
+ # Web applications should use
15
+ # AuthSub[http://code.google.com/apis/contacts/developers_guide_protocol.html#auth_sub]
16
+ # proxy authentication to get an authentication token for a Google account.
17
+ #
18
+ # First, get the user to follow the following URL:
19
+ #
20
+ # Contacts::Google.authentication_url('http://mysite.com/invite')
21
+ #
22
+ # After he authenticates successfully, Google will redirect him back to the target URL
23
+ # (specified as argument above) and provide the token GET parameter. Use it to create a
24
+ # new instance of this class and request the contact list:
25
+ #
26
+ # gmail = Contacts::Google.new('example@gmail.com', params[:token])
27
+ # contacts = gmail.contacts
28
+ # #-> [ ['Fitzgerald', 'fubar@gmail.com', 'fubar@example.com'],
29
+ # ['William Paginate', 'will.paginate@gmail.com'], ...
30
+ # ]
31
+ #
32
+ # == Storing a session token
33
+ #
34
+ # The basic token that you will get after the user has authenticated on Google is valid
35
+ # for only one request. However, you can specify that you want a session token which
36
+ # doesn't expire:
37
+ #
38
+ # Contacts::Google.authentication_url('http://mysite.com/invite', :session => true)
39
+ #
40
+ # When the user authenticates, he will be redirected back with a token which still isn't
41
+ # a session token, but can be exchanged for one!
42
+ #
43
+ # token = Contacts::Google.sesion_token(params[:token])
44
+ #
45
+ # Now you have a permanent token. Store it with other user data so you can query the API
46
+ # on his behalf without him having to authenticate on Google each time.
47
+ class Google
48
+ DOMAIN = 'www.google.com'
49
+ AuthSubPath = '/accounts/AuthSub' # all variants go over HTTPS
50
+ AuthScope = "http://#{DOMAIN}/m8/feeds/"
51
+
52
+ # URL to Google site where user authenticates. Afterwards, Google redirects to your
53
+ # site with the URL specified as +target+.
54
+ #
55
+ # Options are:
56
+ # * <tt>:scope</tt> -- the AuthSub scope in which the resulting token is valid
57
+ # (default: "http://www.google.com/m8/feeds/")
58
+ # * <tt>:secure</tt> -- boolean indicating whether the token will be secure
59
+ # (default: false)
60
+ # * <tt>:session</tt> -- boolean indicating if the token can be exchanged for a session token
61
+ # (default: false)
62
+ def self.authentication_url(target, options = {})
63
+ params = { :next => target,
64
+ :scope => AuthScope,
65
+ :secure => false,
66
+ :session => false
67
+ }.merge(options)
68
+
69
+ query = params.inject [] do |url, pair|
70
+ unless pair.last.nil?
71
+ value = case pair.last
72
+ when TrueClass; 1
73
+ when FalseClass; 0
74
+ else pair.last
75
+ end
76
+
77
+ url << "#{pair.first}=#{CGI.escape(value.to_s)}"
78
+ end
79
+ url
80
+ end.join('&')
81
+
82
+ "https://#{DOMAIN}#{AuthSubPath}Request?#{query}"
83
+ end
84
+
85
+ # Makes an HTTPS request to exchange the given token with a session one. Session
86
+ # tokens never expire, so you can store them in the database alongside user info.
87
+ #
88
+ # Returns the new token as string or nil if the parameter couln't be found in response
89
+ # body.
90
+ def self.session_token(token)
91
+ response = Net::HTTP.start(DOMAIN) do |google|
92
+ google.use_ssl
93
+ google.verify_mode = OpenSSL::SSL::VERIFY_NONE
94
+ google.get(AuthSubPath + 'SessionToken', auth_headers(token))
95
+ end
96
+
97
+ pair = response.body.split(/\s+/).detect {|p| p.index('Token') == 0 }
98
+ pair.split('=').last if pair
99
+ end
100
+
101
+ # User ID (email) and token are required here. By default, an AuthSub token from
102
+ # Google is one-time only, which means you can only make a single request with it.
103
+ def initialize(user_id, token)
104
+ @user = user_id.to_s
105
+ @headers = { 'Accept-Encoding' => 'gzip' }.update(self.class.auth_headers(token))
106
+ @base_path = "/m8/feeds/contacts/#{CGI.escape(@user)}/base"
107
+ end
108
+
109
+ def get(params) #:nodoc:
110
+ response = Net::HTTP.start(DOMAIN) do |google|
111
+ google.get(@base_path + '?' + query_string(params), @headers)
112
+ end
113
+
114
+ raise FetchingError.new(response) unless response.is_a? Net::HTTPSuccess
115
+
116
+ response
117
+ end
118
+
119
+ # Timestamp of last update. This value is available only after the XML
120
+ # document has been parsed; for instance after fetching the contact list.
121
+ def updated_at
122
+ @updated_at ||= Time.parse @updated_string if @updated_string
123
+ end
124
+
125
+ # Timestamp of last update as it appeared in the XML document
126
+ def updated_at_string
127
+ @updated_string
128
+ end
129
+
130
+ # Fetches, parses and returns the contact list.
131
+ #
132
+ # ==== Options
133
+ # * <tt>:limit</tt> -- use a large number to fetch a bigger contact list (default: 200)
134
+ # * <tt>:offset</tt> -- 0-based value, can be used for pagination
135
+ # * <tt>:order</tt> -- currently the only value support by Google is "lastmodified"
136
+ # * <tt>:descending</tt> -- boolean
137
+ # * <tt>:updated_after</tt> -- string or time-like object, use to only fetch contacts
138
+ # that were updated after this date
139
+ def contacts(options = {})
140
+ params = { :limit => 200 }.update(options)
141
+ response = get(params)
142
+ parse_contacts response_body(response)
143
+ end
144
+
145
+ protected
146
+
147
+ def response_body(response)
148
+ unless response['Content-Encoding'] == 'gzip'
149
+ response.body
150
+ else
151
+ gzipped = StringIO.new(response.body)
152
+ Zlib::GzipReader.new(gzipped).read
153
+ end
154
+ end
155
+
156
+ def self.auth_headers(token)
157
+ { 'Authorization' => %(AuthSub token=#{token.to_s.inspect}) }
158
+ end
159
+
160
+ def parse_contacts(body)
161
+ doc = Hpricot::XML body
162
+ entries = []
163
+
164
+ if updated_node = doc.at('/feed/updated')
165
+ @updated_string = updated_node.inner_text
166
+ end
167
+
168
+ (doc / '/feed/entry').each do |entry|
169
+ email_nodes = entry / 'gd:email[@address]'
170
+
171
+ unless email_nodes.empty?
172
+ title_node = entry.at('/title')
173
+ name = title_node ? title_node.inner_text : nil
174
+
175
+ person = email_nodes.inject [name] do |p, e|
176
+ p << e['address'].to_s
177
+ end
178
+ entries << person
179
+ end
180
+ end
181
+
182
+ entries
183
+ end
184
+
185
+ def query_string(params)
186
+ params.inject [] do |url, pair|
187
+ value = pair.last
188
+ unless value.nil?
189
+ key = case pair.first
190
+ when :limit
191
+ 'max-results'
192
+ when :offset
193
+ value = value.to_i + 1
194
+ 'start-index'
195
+ when :order
196
+ url << 'sortorder=descending' if params[:descending].nil?
197
+ 'orderby'
198
+ when :descending
199
+ value = value ? 'descending' : 'ascending'
200
+ 'sortorder'
201
+ when :updated_after
202
+ value = value.strftime("%Y-%m-%dT%H:%M:%S%Z") if value.respond_to? :strftime
203
+ 'updated-min'
204
+ else pair.first
205
+ end
206
+
207
+ url << "#{key}=#{CGI.escape(value.to_s)}"
208
+ end
209
+ url
210
+ end.join('&')
211
+ end
212
+ end
213
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nullstyle-contacts
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - "Mislav Marohni\xC4\x87"
8
+ - Scott Fleckenstein
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2008-04-29 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Fetch users' contact lists from your web application without asking them to provide their passwords.
18
+ email: mislav.marohnic@gmail.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files:
24
+ - README.rdoc
25
+ files:
26
+ - README.rdoc
27
+ - MIT-LICENSE
28
+ - Rakefile
29
+ - contacts.gemspec
30
+ - lib/contacts/google.rb
31
+ - lib/contacts.rb
32
+ has_rdoc: true
33
+ homepage: http://test-spec.rubyforge.org
34
+ post_install_message:
35
+ rdoc_options:
36
+ - --main
37
+ - README.rdoc
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.0.1
56
+ signing_key:
57
+ specification_version: 2
58
+ summary: import contacts from google Contacts API
59
+ test_files: []
60
+