nullstyle-contacts 0.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.
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
+