bradfitz / addressbooker
- Source
- Commits
- Network (1)
- Issues (0)
- Downloads (0)
- Wiki (1)
- Graphs
-
Tree:
1517662
Brad Fitzpatrick (author)
Sat Nov 29 17:05:13 -0800 2008
addressbooker / addressbooker.py
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 1 | # Copyright (C) 2008 Brad Fitzpatrick | |
| 2 | # | ||||
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| 4 | # you may not use this file except in compliance with the License. | ||||
| 5 | # You may obtain a copy of the License at | ||||
| 6 | # | ||||
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 8 | # | ||||
| 9 | # Unless required by applicable law or agreed to in writing, software | ||||
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| 12 | # See the License for the specific language governing permissions and | ||||
| 13 | # limitations under the License. | ||||
| 14 | |||||
| 15 | |||||
| 16 | __author__ = 'brad@danga.com (Brad Fitzpatrick)' | ||||
| 17 | |||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 18 | # Core Python | |
| f3be64f3 » | Brad Fitzpatrick | 2008-11-29 | 19 | import cgi | |
| 20 | import logging | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 21 | import pprint | |
| f3be64f3 » | Brad Fitzpatrick | 2008-11-29 | 22 | import random | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 23 | import re | |
| f3be64f3 » | Brad Fitzpatrick | 2008-11-29 | 24 | import urllib | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 25 | ||
| 26 | # Core/AppEngine stuff | ||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 27 | import wsgiref.handlers | |
| 28 | from google.appengine.api import users | ||||
| 29 | from google.appengine.ext import webapp | ||||
| 30 | from google.appengine.ext import db | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 31 | from google.appengine.ext.webapp import template | |
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 32 | from google.appengine.api import urlfetch | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 33 | ||
| 34 | # Libraries included w/ app | ||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 35 | import atom | |
| 36 | import atom.http_interface | ||||
| 37 | import atom.token_store | ||||
| 38 | import atom.url | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 39 | import gdata.alt.appengine | |
| 40 | import gdata.auth | ||||
| 41 | import gdata.contacts.service as contactsservice | ||||
| 42 | import gdata.service | ||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 43 | import simplejson | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 44 | ||
| 45 | # App stuff | ||||
| 46 | import settings | ||||
| 47 | import models | ||||
| 48 | |||||
| 49 | VALID_HANDLE = re.compile(r"^\w+$") | ||||
| 50 | |||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 51 | ||
| f2b7da9b » | Brad Fitzpatrick | 2008-11-29 | 52 | def NumberSuffixesMatch(num1, num2): | |
| 53 | """Given two phone numbers, return bool if they match. | ||||
| 54 | |||||
| 55 | Numbers are strings. A match is 7 matching final | ||||
| 56 | numbers, ignoring punctuations and space and stuff. | ||||
| 57 | """ | ||||
| 58 | num1 = re.sub(r"[^\d]", "", num1) | ||||
| 59 | num2 = re.sub(r"[^\d]", "", num2) | ||||
| 60 | if len(num1) < 6 or len(num2) < 6: | ||||
| 61 | return False | ||||
| 62 | return num1[-7:] == num2[-7:] | ||||
| 63 | |||||
| 64 | |||||
| 65 | def FindEntryToMergeInto(contact, feed): | ||||
| 66 | """Finds Entry (or None) in feed to merge contact into.""" | ||||
| 67 | contact_name = contact["name"] | ||||
| 68 | for entry in feed.entry: | ||||
| 69 | if entry.title and entry.title.text and \ | ||||
| 70 | entry.title.text == contact_name: | ||||
| 71 | return entry | ||||
| 72 | |||||
| 73 | for phone_number in entry.phone_number: | ||||
| 74 | google_number = phone_number.text | ||||
| 75 | for number_rec in contact["numbers"]: | ||||
| 76 | contact_number = number_rec["number"] | ||||
| 77 | if NumberSuffixesMatch(google_number, contact_number): | ||||
| 78 | return entry | ||||
| 79 | |||||
| 80 | return None | ||||
| 81 | |||||
| f3be64f3 » | Brad Fitzpatrick | 2008-11-29 | 82 | ||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 83 | def PhoneRelType(text): | |
| 84 | """Given some free-formish text, map that to the GData phone rel.""" | ||||
| 85 | if re.match(r"^\s*(mobile|cell)", text, re.I): | ||||
| 86 | return "http://schemas.google.com/g/2005#mobile" | ||||
| 87 | if re.match(r"^\s*(work|office)", text, re.I): | ||||
| 88 | return "http://schemas.google.com/g/2005#work" | ||||
| 89 | if re.match(r"^\s*(house|home)", text, re.I): | ||||
| 90 | return "http://schemas.google.com/g/2005#home" | ||||
| 91 | return "http://schemas.google.com/g/2005#other" | ||||
| 92 | |||||
| 93 | |||||
| 94 | def NewContactEntry(contact, group=None): | ||||
| 95 | """Make a new GData Contact Entry from a submitted contact dict.""" | ||||
| 96 | new_entry = gdata.contacts.ContactEntry() | ||||
| 97 | if contact["name"]: | ||||
| 98 | new_entry.title = atom.Title(text=contact["name"]) | ||||
| 99 | |||||
| 100 | for number_rec in contact["numbers"]: | ||||
| 101 | new_entry.phone_number.append(gdata.contacts.PhoneNumber( | ||||
| 102 | rel=PhoneRelType(number_rec["type"]), | ||||
| 103 | text=number_rec["number"])) | ||||
| 104 | |||||
| 105 | if group: | ||||
| 106 | new_entry.group_membership_info.append(group) | ||||
| 107 | |||||
| 108 | return new_entry | ||||
| 109 | |||||
| 110 | |||||
| f3be64f3 » | Brad Fitzpatrick | 2008-11-29 | 111 | class Updater(object): | |
| 112 | """Queues up updates and flushes them to gdata batch as needed.""" | ||||
| 113 | |||||
| 114 | def __init__(self, client=None): | ||||
| 115 | self.client = client | ||||
| 116 | self.batch_feed = gdata.contacts.ContactsFeed() | ||||
| 117 | |||||
| 118 | def AddInsert(self, entry): | ||||
| 119 | self.batch_feed.AddInsert(entry) | ||||
| 120 | self.FlushIfNeeded() | ||||
| 121 | |||||
| 122 | def AddUpdate(self, entry): | ||||
| 123 | self.batch_feed.AddUpdate(entry) | ||||
| 124 | self.FlushIfNeeded() | ||||
| 125 | |||||
| 126 | def FlushIfNeeded(self): | ||||
| 127 | if len(self.batch_feed.entry) >= 50: # could be 100 max | ||||
| 128 | self.Flush() | ||||
| 129 | |||||
| 130 | def Flush(self): | ||||
| 131 | if not len(self.batch_feed.entry): | ||||
| 132 | return | ||||
| 133 | self.client.ExecuteBatch(self.batch_feed, | ||||
| 134 | gdata.contacts.service.DEFAULT_BATCH_URL) | ||||
| 135 | self.batch_feed = gdata.contacts.ContactsFeed() | ||||
| 136 | |||||
| 137 | |||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 138 | class AddressBooker(webapp.RequestHandler): | |
| 139 | |||||
| 140 | def get(self): | ||||
| 141 | self.response.headers['Content-Type'] = 'text/html' | ||||
| 142 | |||||
| 143 | self.response.out.write("""<!DOCTYPE html><html><head> | ||||
| 144 | <title>AddressBooker: merge contacts into your Google Address Book | ||||
| 145 | </title> | ||||
| 146 | <link rel="stylesheet" type="text/css" | ||||
| 147 | href="/static/feedfetcher.css"/> | ||||
| 148 | </head><body>""") | ||||
| 149 | |||||
| 150 | self.response.out.write("""<div id="nav"><a href="/">Home</a>""") | ||||
| 151 | if users.get_current_user(): | ||||
| 152 | self.response.out.write('<a href="%s">Sign Out</a>' % ( | ||||
| 153 | users.create_logout_url('http://%s/merge/' % settings.HOST_NAME))) | ||||
| 154 | else: | ||||
| 155 | self.response.out.write('<a href="%s">Sign In</a>' % ( | ||||
| 156 | users.create_login_url('http://%s/merge/' % settings.HOST_NAME))) | ||||
| 157 | self.response.out.write('</div>') | ||||
| 158 | |||||
| 159 | |||||
| 160 | def post(self): | ||||
| 4ee0964e » | Brad Fitzpatrick | 2008-11-28 | 161 | handle = self.request.get('handle') | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 162 | if not handle: | |
| 163 | raise "Missing argument 'handle'" | ||||
| 164 | if not VALID_HANDLE.match(handle): | ||||
| 165 | raise "Bogus handle." | ||||
| 166 | |||||
| 4ee0964e » | Brad Fitzpatrick | 2008-11-28 | 167 | json = self.request.get('json') | |
| 168 | group = self.request.get('group') | ||||
| 169 | |||||
| 170 | if handle: | ||||
| 171 | post_dump = models.PostDump(key_name="handle:" + handle, | ||||
| 172 | json=json, | ||||
| 173 | group=group, | ||||
| 174 | handle=handle) | ||||
| 175 | post_dump.put() | ||||
| 176 | |||||
| 177 | contacts = simplejson.loads(json) | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 178 | ||
| 179 | self.response.out.write(template.render('now_what.html', { | ||||
| 180 | 'n_contacts': len(contacts), | ||||
| 181 | 'handle': str(post_dump.key()), | ||||
| 182 | })) | ||||
| 183 | |||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 184 | #self.response.out.write("You posted: " + pprint.pformat(contacts)); | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 185 | ||
| 186 | |||||
| 187 | class MergeView(webapp.RequestHandler): | ||||
| 188 | """View the contacts for a given handle.""" | ||||
| 189 | |||||
| 190 | def get(self): | ||||
| 191 | key = self.request.get('key') | ||||
| 192 | if not key: | ||||
| 193 | raise "Missing argument 'key'" | ||||
| 194 | post_dump = models.PostDump.get(db.Key(key)) | ||||
| 195 | if not post_dump: | ||||
| 196 | raise "State lost? Um, do it again." | ||||
| 197 | |||||
| 198 | contacts = simplejson.loads(post_dump.json) | ||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 199 | for contact in contacts: | |
| 200 | self.response.out.write("<br clear='both'><h2>%s</h2>" % contact["name"]) | ||||
| 201 | self.response.out.write("<img src='%s' style='float:left' />" % contact["img"]) | ||||
| 202 | for number in contact["numbers"]: | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 203 | obf_number = re.sub(r"\d{3}$", "<i>xxx</i>", number["number"]) | |
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 204 | self.response.out.write("<p><b>%s</b> %s</p>" % ( | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 205 | number["type"], obf_number)) | |
| 206 | |||||
| 207 | |||||
| 208 | class MergeGoogle(webapp.RequestHandler): | ||||
| 209 | """Merge contacts into Google Contacts w/ Google Contacts API.""" | ||||
| 210 | |||||
| 211 | def get(self): | ||||
| 212 | key = self.request.get('key') | ||||
| 213 | if not key: | ||||
| 214 | raise "Missing argument 'key'" | ||||
| 215 | post_dump = models.PostDump.get(db.Key(key)) | ||||
| 216 | if not post_dump: | ||||
| 217 | raise "State lost? Um, do it again." | ||||
| 218 | |||||
| f2b7da9b » | Brad Fitzpatrick | 2008-11-29 | 219 | def out(str): | |
| 220 | self.response.out.write(str) | ||||
| 221 | |||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 222 | user = users.get_current_user() | |
| 223 | logging.info("Current user: " + str(user)) | ||||
| 224 | |||||
| 225 | # We need a logged-in user for the GData.client.token_store to work. | ||||
| 226 | if not user: | ||||
| 227 | logging.info("Redirecting to sign-in."); | ||||
| 228 | sign_in_url = users.create_login_url('http://%s/merge/google?key=%s' % | ||||
| 229 | (settings.HOST_NAME, key)) | ||||
| 230 | self.redirect(sign_in_url) | ||||
| 231 | return | ||||
| 232 | |||||
| 233 | # And the subclass of the Service for the Contacts API: | ||||
| 234 | client = contactsservice.ContactsService() | ||||
| 235 | gdata.alt.appengine.run_on_appengine(client) | ||||
| 236 | |||||
| 237 | contacts = simplejson.loads(post_dump.json) | ||||
| 238 | |||||
| 239 | contacts_url = "http://www.google.com/m8/feeds/contacts/default/full" | ||||
| 240 | auth_base_url = "http://www.google.com/m8/feeds/" | ||||
| 241 | |||||
| 242 | session_token = client.token_store.find_token(auth_base_url) | ||||
| 243 | if type(session_token) == atom.http_interface.GenericToken: | ||||
| 244 | session_token = None | ||||
| 245 | |||||
| 246 | if not session_token: | ||||
| 247 | # Find the AuthSub token and upgrade it to a session token. | ||||
| 248 | auth_token = gdata.auth.extract_auth_sub_token_from_url(self.request.uri) | ||||
| 249 | if auth_token: | ||||
| 250 | session_token = client.upgrade_to_session_token(auth_token) | ||||
| 251 | client.token_store.add_token(session_token) | ||||
| 252 | # just to sanitize our URL: | ||||
| 253 | self.redirect('http://%s/merge/google?key=%s' % | ||||
| 254 | (settings.HOST_NAME, key)) | ||||
| 255 | else: | ||||
| 256 | next = self.request.uri | ||||
| 257 | auth_sub_url = client.GenerateAuthSubURL(next, auth_base_url, | ||||
| 258 | secure=False, session=True) | ||||
| 259 | self.redirect(str(auth_sub_url)) | ||||
| 260 | return | ||||
| 261 | |||||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 262 | out("<p>We're good to go. %s</p>" % str(session_token)); | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 263 | sign_out_url = users.create_logout_url('http://%s/merge/' % | |
| 264 | (settings.HOST_NAME)) | ||||
| f2b7da9b » | Brad Fitzpatrick | 2008-11-29 | 265 | out("\nOr you want to <a href='%s'>log out</a>?" % sign_out_url) | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 266 | ||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 267 | # Process Groups | |
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 268 | groups_feed = client.Get("http://www.google.com/m8/feeds/groups/default/full") | |
| f2b7da9b » | Brad Fitzpatrick | 2008-11-29 | 269 | groups_feed = gdata.contacts.GroupsFeedFromString(str(groups_feed)) | |
| 270 | group_name = {} # id -> name | ||||
| 271 | group_id = {} # name -> id | ||||
| 272 | for group in groups_feed.entry: | ||||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 273 | group_name[group.id.text] = group.content.text | |
| 274 | group_id[group.content.text] = group.id.text | ||||
| 275 | if True: | ||||
| 276 | out("<h3>Group</h3><ul>") | ||||
| 277 | out("<li>id: %s</li>" % cgi.escape(group.id.text)) | ||||
| 278 | out("<li>content: %s</li>" % cgi.escape(group.content.text)) | ||||
| 279 | out("</ul>") | ||||
| 280 | |||||
| 281 | dest_group_name = post_dump.group or "AddressBooker" | ||||
| 282 | if dest_group_name not in group_id: | ||||
| 283 | new_group = gdata.contacts.GroupEntry(title=atom.Title( | ||||
| 284 | text=dest_group_name)) | ||||
| 285 | group = client.CreateGroup(new_group) | ||||
| 286 | out("<h3>group: %s</h3>" % group.id) | ||||
| 287 | group_name[group.id] = dest_group_name | ||||
| 288 | group_id[dest_group_name] = group.id | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 289 | ||
| 290 | full_feed_url = contacts_url + "?max-results=99999" | ||||
| 291 | feed = client.Get(full_feed_url, converter=gdata.contacts.ContactsFeedFromString) | ||||
| f2b7da9b » | Brad Fitzpatrick | 2008-11-29 | 292 | ||
| f3be64f3 » | Brad Fitzpatrick | 2008-11-29 | 293 | updater = Updater(client=client); | |
| 294 | |||||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 295 | #new_entry.email.append(gdata.contacts.Email( | |
| 296 | # rel='http://schemas.google.com/g/2005#work', | ||||
| 297 | # address='TESTTEST@gmail.com')) | ||||
| 298 | #new_entry.content = atom.Content(text='Test Notes') | ||||
| 299 | |||||
| 300 | group = gdata.contacts.GroupMembershipInfo(href=unicode(group_id[dest_group_name])) | ||||
| f3be64f3 » | Brad Fitzpatrick | 2008-11-29 | 301 | ||
| f2b7da9b » | Brad Fitzpatrick | 2008-11-29 | 302 | for contact in contacts: | |
| 303 | out("<br clear='both'><h2>%s</h2>" % contact["name"]) | ||||
| 304 | out("<img src='%s' style='float:left' />" % contact["img"]) | ||||
| 305 | for number in contact["numbers"]: | ||||
| 306 | out("<p><b>%s</b> %s</p>" % (number["type"], number["number"])) | ||||
| 307 | merge_entry = FindEntryToMergeInto(contact, feed) | ||||
| 308 | if merge_entry: | ||||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 309 | out("<p><b>Action: merge into: </b> %s</p>" % cgi.escape( | |
| 310 | merge_entry.ToString().decode("utf-8"))) | ||||
| 311 | #UpdateContactEntry(merge_entry, contact) | ||||
| 312 | #updater.AddUpdate(merge_entry) | ||||
| f2b7da9b » | Brad Fitzpatrick | 2008-11-29 | 313 | else: | |
| 314 | out("<p><b>Action: new Google Contact</b>") | ||||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 315 | updater.AddInsert(NewContactEntry(contact, group=group)) | |
| 316 | |||||
| 317 | render_google_list = False | ||||
| 318 | if render_google_list: | ||||
| 319 | out("<hr />") | ||||
| 320 | for entry in feed.entry: | ||||
| 321 | if entry.title and entry.title.text: | ||||
| 322 | out('<h3>Entry Title: %s</h3>' % ( | ||||
| 323 | entry.title.text.decode('UTF-8'))) | ||||
| 324 | else: | ||||
| 325 | out("<h3>(title-less entry)</h3>"); | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 326 | ||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 327 | for phone_number in entry.phone_number: | |
| 328 | out("<p><b>Phone: (%s)</b> %s</p>" % | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 329 | (phone_number.rel, phone_number.text)) | |
| 330 | |||||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 331 | for email in entry.email: | |
| 332 | out("<p><b>Email: (%s)</b> %s</p>" % | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 333 | (email.rel, email.address)) | |
| 334 | |||||
| 15176620 » | Brad Fitzpatrick | 2008-11-29 | 335 | for group in entry.group_membership_info: | |
| 336 | out("<p><b>Group: (%s)</b> %s</p>" % | ||||
| 337 | (group.href, cgi.escape(str(group)))) | ||||
| 338 | |||||
| 339 | updater.Flush() | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 340 | ||
| 341 | |||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 342 | ||
| 343 | class Acker(webapp.RequestHandler): | ||||
| 344 | """Simulates an HTML page to prove ownership of this domain for AuthSub | ||||
| 345 | registration.""" | ||||
| 346 | |||||
| 347 | def get(self): | ||||
| 348 | self.response.headers['Content-Type'] = 'text/plain' | ||||
| 349 | self.response.out.write('This file present for AuthSub registration.') | ||||
| 350 | |||||
| 351 | |||||
| 352 | def main(): | ||||
| 04acea54 » | Brad Fitzpatrick | 2008-11-29 | 353 | application = webapp.WSGIApplication([ | |
| 354 | ('/merge/', AddressBooker), | ||||
| 355 | ('/merge/google', MergeGoogle), | ||||
| 356 | ('/merge/view', MergeView), | ||||
| 357 | ('/google72db3d6838b4c438.html', Acker), | ||||
| 358 | ], debug=True) | ||||
| 0ed12f30 » | Brad Fitzpatrick | 2008-11-28 | 359 | wsgiref.handlers.CGIHandler().run(application) | |
| 360 | |||||
| 361 | |||||
| 362 | if __name__ == '__main__': | ||||
| 363 | main() | ||||
