Class: Contacts::Google

Inherits:
Object
  • Object
show all
Defined in:
lib/contacts/google.rb

Overview

Fetching Google Contacts

Web applications should use AuthSub proxy authentication to get an authentication token for a Google account.

First, get the user to follow the following URL:

Contacts::Google.authentication_url('http://mysite.com/invite')

After he authenticates successfully, Google will redirect him back to the target URL (specified as argument above) and provide the token GET parameter. Use it to create a new instance of this class and request the contact list:

gmail = Contacts::Google.new('[email protected]', params[:token])
contacts = gmail.contacts
#-> [ ['Fitzgerald', '[email protected]', '[email protected]'],
      ['William Paginate', '[email protected]'], ...
      ]

Storing a session token

The basic token that you will get after the user has authenticated on Google is valid for only one request. However, you can specify that you want a session token which doesn’t expire:

Contacts::Google.authentication_url('http://mysite.com/invite', :session => true)

When the user authenticates, he will be redirected back with a token which still isn’t a session token, but can be exchanged for one!

token = Contacts::Google.sesion_token(params[:token])

Now you have a permanent token. Store it with other user data so you can query the API on his behalf without him having to authenticate on Google each time.

Defined Under Namespace

Classes: Base, Contact, Group

Constant Summary collapse

DOMAIN =
'www.google.com'
AuthSubPath =

all variants go over HTTPS

'/accounts/AuthSub'
AuthScope =
"http://#{DOMAIN}/m8/feeds/"
PATH =
{
  'contacts_full' => '/m8/feeds/contacts/default/full',
  'contacts_batch' => '/m8/feeds/contacts/default/full/batch',
  'groups_full' => '/m8/feeds/groups/default/full',
  'groups_batch' => '/m8/feeds/groups/default/full/batch',
}

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user_id, token) ⇒ Google

User ID (email) and token are required here. By default, an AuthSub token from Google is one-time only, which means you can only make a single request with it.



101
102
103
104
105
106
107
108
# File 'lib/contacts/google.rb', line 101

def initialize(user_id, token)
  @user = user_id.to_s
  @headers = {
    'Accept-Encoding' => 'gzip',
    'User-Agent' => 'agent-that-accepts-gzip',
  }.update(self.class.auth_headers(token))
  @in_batch = false
end

Class Method Details

.authentication_url(target, options = {}) ⇒ Object

URL to Google site where user authenticates. Afterwards, Google redirects to your site with the URL specified as target.

Options are:

  • :scope – the AuthSub scope in which the resulting token is valid (default: “www.google.com/m8/feeds/”)

  • :secure – boolean indicating whether the token will be secure (default: false)

  • :session – boolean indicating if the token can be exchanged for a session token (default: false)



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/contacts/google.rb', line 61

def self.authentication_url(target, options = {})
  params = { :next => target,
             :scope => AuthScope,
             :secure => false,
             :session => false
           }.merge(options)
           
  query = params.inject [] do |url, pair|
    unless pair.last.nil?
      value = case pair.last
        when TrueClass; 1
        when FalseClass; 0
        else pair.last
        end
      
      url << "#{pair.first}=#{CGI.escape(value.to_s)}"
    end
    url
  end.join('&')

  "https://#{DOMAIN}#{AuthSubPath}Request?#{query}"
end

.session_token(token) ⇒ Object

Makes an HTTPS request to exchange the given token with a session one. Session tokens never expire, so you can store them in the database alongside user info.

Returns the new token as string or nil if the parameter couln’t be found in response body.



89
90
91
92
93
94
95
96
97
# File 'lib/contacts/google.rb', line 89

def self.session_token(token)
  http = Net::HTTP.new(DOMAIN, 443)
  http.use_ssl = true
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  response = http.request_get(AuthSubPath + 'SessionToken', auth_headers(token))

  pair = response.body.split(/\s+/).detect {|p| p.index('Token') == 0 }
  pair.split('=').last if pair
end

Instance Method Details

#all_contactsObject

Fetches all contacts in chunks of 200.

For example: if you have 1000 contacts, this will render in 5 GET requests



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/contacts/google.rb', line 180

def all_contacts
  ret = []
  chunk_size = 200
  offset = 0
  
  while (chunk = contacts(:limit => chunk_size, :offset => offset)).size != 0
    ret.push(*chunk)
    offset += chunk_size
    break if chunk.size < chunk_size
  end
  ret
end

#all_groupsObject



193
194
195
196
197
198
199
200
201
202
203
# File 'lib/contacts/google.rb', line 193

def all_groups
  ret = []
  chunk_size = 200
  offset = 0
  
  while (chunk = groups(:limit => chunk_size, :offset => offset)).size != 0
    ret.push(*chunk)
    offset += chunk_size
  end
  ret
end

#batch(url, &blk) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/contacts/google.rb', line 223

def batch(url, &blk)
  # Init
  limit = 512 * 1024
  @batch_request = []
  @in_batch = true

  # Execute the block
  yield

  # Pack post-request in batch job(s)
  while !@batch_request.empty?
    doc = Hpricot("<?xml version='1.0' encoding='UTF-8'?>\n<feed/>", :xml => true)
    root = doc.root
    root['xmlns'] = 'http://www.w3.org/2005/Atom'
    root['xmlns:gContact'] = 'http://schemas.google.com/contact/2008'
    root['xmlns:gd'] = 'http://schemas.google.com/g/2005'
    root['xmlns:batch'] = 'http://schemas.google.com/gdata/batch'

    size = doc.to_s.size
    100.times do
      break if size >= limit || @batch_request.empty?
      r = @batch_request.shift

      # Get stuff for request
      headers = r[1]
      xml = r[0]

      # Delete all namespace attributes
      xml.root.attributes.each { |a,v| xml.root.remove_attribute(a) if a =~ /^xmlns/ }

      # Find out what to do
      operation = case headers['X-HTTP-Method-Override']
      when 'PUT'
        'update'
      when 'DELETE'
        'delete'
      else
        'insert'
      end
      
      xml.root.children << Hpricot.make("<batch:operation type='#{operation}'/>").first
      root.children << xml.root
      size += xml.root.to_s.size
    end
    
    #puts "Doing POST... (#{size} bytes)"
    @in_batch = false
    post(url, doc, 'Content-Type' => 'application/atom+xml')
    @in_batch = true
  end
  @in_batch = false
end

#batch_contacts(&blk) ⇒ Object



215
216
217
# File 'lib/contacts/google.rb', line 215

def batch_contacts(&blk)
  batch(PATH['contacts_batch'], &blk)
end

#batch_groups(&blk) ⇒ Object



219
220
221
# File 'lib/contacts/google.rb', line 219

def batch_groups(&blk)
  batch(PATH['groups_batch'], &blk)
end

#contacts(options = {}) ⇒ Object

Fetches, parses and returns the contact list.

Options

  • :limit – use a large number to fetch a bigger contact list (default: 200)

  • :offset – 0-based value, can be used for pagination

  • :order – currently the only value support by Google is “lastmodified”

  • :descending – boolean

  • :updated_after – string or time-like object, use to only fetch contacts that were updated after this date



161
162
163
164
165
# File 'lib/contacts/google.rb', line 161

def contacts(options = {})
  params = { :limit => 200 }.update(options)
  response = get(PATH['contacts_full'], params)
  parse_contacts response_body(response)
end

#get(path, params) ⇒ Object

:nodoc:

Raises:



117
118
119
120
121
122
123
124
125
# File 'lib/contacts/google.rb', line 117

def get(path, params) #:nodoc:
  response = Net::HTTP.start(DOMAIN) do |google|
    google.get(path + '?' + query_string(params), @headers)
  end

  raise FetchingError.new(response) unless response.is_a? Net::HTTPSuccess

  response
end

#groups(options = {}) ⇒ Object

Fetches, parses and returns the group list.

Options

see contacts



171
172
173
174
175
# File 'lib/contacts/google.rb', line 171

def groups(options = {})
  params = { :limit => 200 }.update(options)
  response = get(PATH['groups_full'], params)
  parse_groups response_body(response)
end

#new_contact(attr = {}) ⇒ Object



205
206
207
208
# File 'lib/contacts/google.rb', line 205

def new_contact(attr = {})
  c = Contact.new(self)
  c.load_attributes(attr)
end

#new_group(attr = {}) ⇒ Object



210
211
212
213
# File 'lib/contacts/google.rb', line 210

def new_group(attr = {})
  g = Group.new(self)
  g.load_attributes(attr)
end

#post(url, body, headers) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/contacts/google.rb', line 138

def post(url, body, headers)
  if @in_batch
    @batch_request << [body, headers]
  else
    response = Net::HTTP.start(DOMAIN) do |google|
      google.post(url, body.to_s, @headers.merge(headers))
    end
    
    raise FetchingError.new(response) unless response.is_a? Net::HTTPSuccess

    response
  end
end

#updated_atObject

Timestamp of last update. This value is available only after the XML document has been parsed; for instance after fetching the contact list.



129
130
131
# File 'lib/contacts/google.rb', line 129

def updated_at
  @updated_at ||= Time.parse @updated_string if @updated_string
end

#updated_at_stringObject

Timestamp of last update as it appeared in the XML document



134
135
136
# File 'lib/contacts/google.rb', line 134

def updated_at_string
  @updated_string
end