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.



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

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)



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

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.



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

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



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

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



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

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



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
275
# File 'lib/contacts/google.rb', line 224

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



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

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

#batch_groups(&blk) ⇒ Object



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

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



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

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:



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

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



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

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

#new_contact(attr = {}) ⇒ Object



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

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

#new_group(attr = {}) ⇒ Object



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

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

#post(url, body, headers) ⇒ Object



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

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.



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

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



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

def updated_at_string
  @updated_string
end