4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 Google Voice backend code
24 http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
25 http://posttopic.com/topic/google-voice-add-on-development
36 from xml.sax import saxutils
38 from xml.etree import ElementTree
48 _TRUE_REGEX = re.compile("true")
49 _FALSE_REGEX = re.compile("false")
53 s = _TRUE_REGEX.sub("True", s)
54 s = _FALSE_REGEX.sub("False", s)
55 return eval(s, {}, {})
58 if simplejson is None:
59 def parse_json(flattened):
60 return safe_eval(flattened)
62 def parse_json(flattened):
63 return simplejson.loads(flattened)
66 class GVDialer(object):
68 This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
69 the functions include login, setting up a callback number, and initalting a callback
72 _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
73 _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
74 _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
75 _validateRe = re.compile("^[0-9]{10,}$")
76 _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
78 _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
79 _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
80 _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
82 _clicktocallURL = "https://www.google.com/voice/m/sendcall"
83 _contactsURL = "https://www.google.com/voice/mobile/contacts"
84 _contactDetailURL = "https://www.google.com/voice/mobile/contact"
86 _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
87 _setforwardURL = "https://www.google.com//voice/m/setphone"
88 _accountNumberURL = "https://www.google.com/voice/mobile"
89 _forwardURL = "https://www.google.com/voice/mobile/phones"
91 _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
92 _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
93 _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
94 _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
95 _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
96 _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
98 def __init__(self, cookieFile = None):
99 # Important items in this function are the setup of the browser emulation and cookie file
100 self._browser = browser_emu.MozillaEmulator(1)
101 if cookieFile is None:
102 cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
103 self._browser.cookies.filename = cookieFile
104 if os.path.isfile(cookieFile):
105 self._browser.cookies.load()
108 self._accountNum = None
109 self._lastAuthed = 0.0
110 self._callbackNumber = ""
111 self._callbackNumbers = {}
113 self.__contacts = None
115 def is_authed(self, force = False):
117 Attempts to detect a current session
118 @note Once logged in try not to reauth more than once a minute.
119 @returns If authenticated
122 if (time.time() - self._lastAuthed) < 60 and not force:
126 self._grab_account_info()
127 except StandardError, e:
128 warnings.warn(traceback.format_exc())
131 self._browser.cookies.save()
132 self._lastAuthed = time.time()
135 def login(self, username, password):
137 Attempt to login to grandcentral
138 @returns Whether login was successful or not
143 loginPostData = urllib.urlencode({
146 'service': "grandcentral",
149 "PersistentCookie": "yes",
153 loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
154 except urllib2.URLError, e:
155 warnings.warn(traceback.format_exc())
156 raise RuntimeError("%s is not accesible" % self._loginURL)
158 return self.is_authed()
161 self._lastAuthed = 0.0
162 self._browser.cookies.clear()
163 self._browser.cookies.save()
167 def dial(self, number):
169 This is the main function responsible for initating the callback
171 if not self.is_valid_syntax(number):
172 raise ValueError('Number is not valid: "%s"' % number)
173 elif not self.is_authed():
174 raise RuntimeError("Not Authenticated")
176 if len(number) == 11 and number[0] == 1:
177 # Strip leading 1 from 11 digit dialing
181 clickToCallData = urllib.urlencode({
183 "phone": self._callbackNumber,
184 "_rnr_se": self._token,
187 'Referer' : 'https://google.com/voice/m/callsms',
189 callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
190 except urllib2.URLError, e:
191 warnings.warn(traceback.format_exc())
192 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
194 if self._gvDialingStrRe.search(callSuccessPage) is None:
195 raise RuntimeError("Google Voice returned an error")
199 def clear_caches(self):
200 self.__contacts = None
202 def is_valid_syntax(self, number):
204 @returns If This number be called ( syntax validation only )
206 return self._validateRe.match(number) is not None
208 def get_account_number(self):
210 @returns The grand central phone number
212 return self._accountNum
214 def set_sane_callback(self):
216 Try to set a sane default callback number on these preferences
217 1) 1747 numbers ( Gizmo )
218 2) anything with gizmo in the name
219 3) anything with computer in the name
222 numbers = self.get_callback_numbers()
224 for number, description in numbers.iteritems():
225 if re.compile(r"""1747""").match(number) is not None:
226 self.set_callback_number(number)
229 for number, description in numbers.iteritems():
230 if re.compile(r"""gizmo""", re.I).search(description) is not None:
231 self.set_callback_number(number)
234 for number, description in numbers.iteritems():
235 if re.compile(r"""computer""", re.I).search(description) is not None:
236 self.set_callback_number(number)
239 for number, description in numbers.iteritems():
240 self.set_callback_number(number)
243 def get_callback_numbers(self):
245 @returns a dictionary mapping call back numbers to descriptions
246 @note These results are cached for 30 minutes.
248 if time.time() - self._lastAuthed < 1800 or self.is_authed():
249 return self._callbackNumbers
253 def set_callback_number(self, callbacknumber):
255 Set the number that grandcental calls
256 @param callbacknumber should be a proper 10 digit number
258 self._callbackNumber = callbacknumber
259 callbackPostData = urllib.urlencode({
260 '_rnr_se': self._token,
261 'phone': callbacknumber
264 callbackSetPage = self._browser.download(self._setforwardURL, callbackPostData)
265 except urllib2.URLError, e:
266 warnings.warn(traceback.format_exc())
267 raise RuntimeError("%s is not accesible" % self._setforwardURL)
269 # @bug This does not seem to be keeping on my tablet (but works on the
270 # desktop), or the reading isn't working too well
271 self._browser.cookies.save()
274 def get_callback_number(self):
276 @returns Current callback number or None
278 for c in self._browser.cookies:
279 if c.name == "gv-ph":
281 return self._callbackNumber
283 def get_recent(self):
285 @returns Iterable of (personsName, phoneNumber, date, action)
288 self._receivedCallsURL,
289 self._missedCallsURL,
290 self._placedCallsURL,
293 flatXml = self._browser.download(url)
294 except urllib2.URLError, e:
295 warnings.warn(traceback.format_exc())
296 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
298 allRecentData = self._grab_json(flatXml)
299 for recentCallData in allRecentData["messages"].itervalues():
300 number = recentCallData["displayNumber"]
301 date = recentCallData["relativeStartTime"]
304 for label in recentCallData["labels"]
305 if label.lower() != "all" and label.lower() != "inbox"
307 number = saxutils.unescape(number)
308 date = saxutils.unescape(date)
309 action = saxutils.unescape(action)
310 yield "", number, date, action
312 def get_addressbooks(self):
314 @returns Iterable of (Address Book Factory, Book Id, Book Name)
318 def open_addressbook(self, bookId):
322 def contact_source_short_name(contactId):
327 return "Google Voice"
329 def get_contacts(self):
331 @returns Iterable of (contact id, contact name)
333 if self.__contacts is None:
336 contactsPagesUrls = [self._contactsURL]
337 for contactsPageUrl in contactsPagesUrls:
339 contactsPage = self._browser.download(contactsPageUrl)
340 except urllib2.URLError, e:
341 warnings.warn(traceback.format_exc())
342 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
343 for contact_match in self._contactsRe.finditer(contactsPage):
344 contactId = contact_match.group(1)
345 contactName = saxutils.unescape(contact_match.group(2))
346 contact = contactId, contactName
347 self.__contacts.append(contact)
350 next_match = self._contactsNextRe.match(contactsPage)
351 if next_match is not None:
352 newContactsPageUrl = self._contactsURL + next_match.group(1)
353 contactsPagesUrls.append(newContactsPageUrl)
355 for contact in self.__contacts:
358 def get_contact_details(self, contactId):
360 @returns Iterable of (Phone Type, Phone Number)
363 detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
364 except urllib2.URLError, e:
365 warnings.warn(traceback.format_exc())
366 raise RuntimeError("%s is not accesible" % self._clicktocallURL)
368 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
369 phoneNumber = detail_match.group(1)
370 phoneType = saxutils.unescape(detail_match.group(2))
371 yield (phoneType, phoneNumber)
373 def get_messages(self):
375 voicemailPage = self._browser.download(self._voicemailURL)
376 except urllib2.URLError, e:
377 warnings.warn(traceback.format_exc())
378 raise RuntimeError("%s is not accesible" % self._voicemailURL)
381 smsPage = self._browser.download(self._smsURL)
382 except urllib2.URLError, e:
383 warnings.warn(traceback.format_exc())
384 raise RuntimeError("%s is not accesible" % self._smsURL)
386 voicemailHtml = self._grab_html(voicemailPage)
387 smsHtml = self._grab_html(smsPage)
397 def _grab_json(self, flatXml):
398 xmlTree = ElementTree.fromstring(flatXml)
399 jsonElement = xmlTree.getchildren()[0]
400 flatJson = jsonElement.text
401 jsonTree = parse_json(flatJson)
404 def _grab_html(self, flatXml):
405 xmlTree = ElementTree.fromstring(flatXml)
406 htmlElement = xmlTree.getchildren()[1]
407 flatHtml = htmlElement.text
410 def _grab_account_info(self):
411 page = self._browser.download(self._forwardURL)
413 tokenGroup = self._tokenRe.search(page)
414 if tokenGroup is None:
415 raise RuntimeError("Could not extract authentication token from GoogleVoice")
416 self._token = tokenGroup.group(1)
418 anGroup = self._accountNumRe.search(page)
420 raise RuntimeError("Could not extract account number from GoogleVoice")
421 self._accountNum = anGroup.group(1)
423 self._callbackNumbers = {}
424 for match in self._callbackRe.finditer(page):
425 callbackNumber = match.group(2)
426 callbackName = match.group(1)
427 self._callbackNumbers[callbackNumber] = callbackName
430 def test_backend(username, password):
433 print "Authenticated: ", backend.is_authed()
434 print "Login?: ", backend.login(username, password)
435 print "Authenticated: ", backend.is_authed()
436 print "Token: ", backend._token
437 print "Account: ", backend.get_account_number()
438 print "Callback: ", backend.get_callback_number()
439 print "All Callback: ",
440 pprint.pprint(backend.get_callback_numbers())
442 # pprint.pprint(list(backend.get_recent()))
443 # print "Contacts: ",
444 # for contact in backend.get_contacts():
446 # pprint.pprint(list(backend.get_contact_details(contact[0])))