4 DialCentral - Front end for Google's GoogleVoice 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
28 from __future__ import with_statement
38 from xml.sax import saxutils
40 from xml.etree import ElementTree
43 import simplejson as _simplejson
44 simplejson = _simplejson
51 _moduleLogger = logging.getLogger("gvoice.backend")
54 class NetworkError(RuntimeError):
58 class GVoiceBackend(object):
60 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
61 the functions include login, setting up a callback number, and initalting a callback
69 def __init__(self, cookieFile = None):
70 # Important items in this function are the setup of the browser emulation and cookie file
71 self._browser = browser_emu.MozillaEmulator(1)
72 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
76 self._lastAuthed = 0.0
77 self._callbackNumber = ""
78 self._callbackNumbers = {}
80 # Suprisingly, moving all of these from class to self sped up startup time
82 self._validateRe = re.compile("^[0-9]{10,}$")
84 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
86 SECURE_URL_BASE = "https://www.google.com/voice/"
87 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
88 self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
89 self._tokenURL = SECURE_URL_BASE + "m"
90 self._callUrl = SECURE_URL_BASE + "call/connect"
91 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
92 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
94 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
95 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
96 self._setDndURL = "https://www.google.com/voice/m/savednd"
98 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
100 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
101 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "inbox/contacts/"
102 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
105 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
106 'recorded', 'placed', 'received', 'missed'
108 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
109 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
110 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
111 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
112 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
113 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
114 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
115 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
116 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
117 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
118 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
120 self._contactsURL = SECURE_MOBILE_URL_BASE + "contacts"
121 self._contactDetailURL = SECURE_MOBILE_URL_BASE + "contact"
123 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
124 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
125 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
126 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
128 self._contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
129 self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
130 self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
132 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
133 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
134 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
135 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
136 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
137 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
138 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
139 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
140 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
141 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
142 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
143 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
145 def is_authed(self, force = False):
147 Attempts to detect a current session
148 @note Once logged in try not to reauth more than once a minute.
149 @returns If authenticated
151 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
152 isPreviouslyAuthed = self._token is not None
153 if isRecentledAuthed and isPreviouslyAuthed and not force:
157 page = self._get_page(self._forwardURL)
158 self._grab_account_info(page)
160 _moduleLogger.exception(str(e))
163 self._browser.save_cookies()
164 self._lastAuthed = time.time()
167 def _get_token(self):
168 tokenPage = self._get_page(self._tokenURL)
170 galxTokens = self._galxRe.search(tokenPage)
171 if galxTokens is not None:
172 galxToken = galxTokens.group(1)
175 _moduleLogger.debug("Could not grab GALX token")
178 def _login(self, username, password, token):
182 'service': "grandcentral",
185 "PersistentCookie": "yes",
187 "continue": self._forwardURL,
190 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
191 return loginSuccessOrFailurePage
193 def login(self, username, password):
195 Attempt to login to GoogleVoice
196 @returns Whether login was successful or not
199 galxToken = self._get_token()
200 loginSuccessOrFailurePage = self._login(username, password, galxToken)
203 self._grab_account_info(loginSuccessOrFailurePage)
205 # Retry in case the redirect failed
206 # luckily is_authed does everything we need for a retry
207 loggedIn = self.is_authed(True)
209 _moduleLogger.exception(str(e))
211 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
213 self._browser.save_cookies()
214 self._lastAuthed = time.time()
218 self._browser.clear_cookies()
219 self._browser.save_cookies()
221 self._lastAuthed = 0.0
224 isDndPage = self._get_page(self._isDndURL)
226 dndGroup = self._isDndRe.search(isDndPage)
229 dndStatus = dndGroup.group(1)
230 isDnd = True if dndStatus.strip().lower() == "true" else False
233 def set_dnd(self, doNotDisturb):
235 "doNotDisturb": 1 if doNotDisturb else 0,
236 "_rnr_se": self._token,
239 dndPage = self._get_page(self._setDndURL, dndPostData)
241 def call(self, outgoingNumber):
243 This is the main function responsible for initating the callback
245 outgoingNumber = self._send_validation(outgoingNumber)
246 subscriberNumber = None
247 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
249 page = self._get_page_with_token(
252 'outgoingNumber': outgoingNumber,
253 'forwardingNumber': self._callbackNumber,
254 'subscriberNumber': subscriberNumber or 'undefined',
255 'phoneType': phoneType,
259 self._parse_with_validation(page)
262 def cancel(self, outgoingNumber=None):
264 Cancels a call matching outgoing and forwarding numbers (if given).
265 Will raise an error if no matching call is being placed
267 page = self._get_page_with_token(
270 'outgoingNumber': outgoingNumber or 'undefined',
271 'forwardingNumber': self._callbackNumber or 'undefined',
275 self._parse_with_validation(page)
277 def send_sms(self, phoneNumber, message):
278 phoneNumber = self._send_validation(phoneNumber)
279 page = self._get_page_with_token(
282 'phoneNumber': phoneNumber,
286 self._parse_with_validation(page)
288 def search(self, query):
290 Search your Google Voice Account history for calls, voicemails, and sms
291 Returns ``Folder`` instance containting matching messages
293 page = self._get_page(
294 self._XML_SEARCH_URL,
297 json, html = extract_payload(page)
300 def get_feed(self, feed):
301 actualFeed = "_XML_%s_URL" % feed.upper()
302 feedUrl = getattr(self, actualFeed)
304 page = self._get_page(feedUrl)
305 json, html = extract_payload(page)
309 def download(self, messageId, adir):
311 Download a voicemail or recorded call MP3 matching the given ``msg``
312 which can either be a ``Message`` instance, or a SHA1 identifier.
313 Saves files to ``adir`` (defaults to current directory).
314 Message hashes can be found in ``self.voicemail().messages`` for example.
315 Returns location of saved file.
317 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
318 fn = os.path.join(adir, '%s.mp3' % messageId)
319 with open(fn, 'wb') as fo:
323 def is_valid_syntax(self, number):
325 @returns If This number be called ( syntax validation only )
327 return self._validateRe.match(number) is not None
329 def get_account_number(self):
331 @returns The GoogleVoice phone number
333 return self._accountNum
335 def get_callback_numbers(self):
337 @returns a dictionary mapping call back numbers to descriptions
338 @note These results are cached for 30 minutes.
340 if not self.is_authed():
342 return self._callbackNumbers
344 def set_callback_number(self, callbacknumber):
346 Set the number that GoogleVoice calls
347 @param callbacknumber should be a proper 10 digit number
349 self._callbackNumber = callbacknumber
352 def get_callback_number(self):
354 @returns Current callback number or None
356 return self._callbackNumber
358 def get_recent(self):
360 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
363 ("Received", self._XML_RECEIVED_URL),
364 ("Missed", self._XML_MISSED_URL),
365 ("Placed", self._XML_PLACED_URL),
367 flatXml = self._get_page(url)
369 allRecentHtml = self._grab_html(flatXml)
370 allRecentData = self._parse_voicemail(allRecentHtml)
371 for recentCallData in allRecentData:
372 recentCallData["action"] = action
375 def get_contacts(self):
377 @returns Iterable of (contact id, contact name)
379 contactsPagesUrls = [self._contactsURL]
380 for contactsPageUrl in contactsPagesUrls:
381 contactsPage = self._get_page(contactsPageUrl)
382 for contact_match in self._contactsRe.finditer(contactsPage):
383 contactId = contact_match.group(1)
384 contactName = saxutils.unescape(contact_match.group(2))
385 contact = contactId, contactName
388 next_match = self._contactsNextRe.match(contactsPage)
389 if next_match is not None:
390 newContactsPageUrl = self._contactsURL + next_match.group(1)
391 contactsPagesUrls.append(newContactsPageUrl)
393 def get_contact_details(self, contactId):
395 @returns Iterable of (Phone Type, Phone Number)
397 detailPage = self._get_page(self._contactDetailURL + '/' + contactId)
399 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
400 phoneNumber = detail_match.group(1)
401 phoneType = saxutils.unescape(detail_match.group(2))
402 yield (phoneType, phoneNumber)
404 def get_messages(self):
405 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
406 voicemailHtml = self._grab_html(voicemailPage)
407 voicemailJson = self._grab_json(voicemailPage)
408 parsedVoicemail = self._parse_voicemail(voicemailHtml)
409 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
410 decoratedVoicemails = self._decorate_voicemail(voicemails)
412 smsPage = self._get_page(self._XML_SMS_URL)
413 smsHtml = self._grab_html(smsPage)
414 smsJson = self._grab_json(smsPage)
415 parsedSms = self._parse_sms(smsHtml)
416 smss = self._merge_messages(parsedSms, smsJson)
417 decoratedSms = self._decorate_sms(smss)
419 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
422 def _grab_json(self, flatXml):
423 xmlTree = ElementTree.fromstring(flatXml)
424 jsonElement = xmlTree.getchildren()[0]
425 flatJson = jsonElement.text
426 jsonTree = parse_json(flatJson)
429 def _grab_html(self, flatXml):
430 xmlTree = ElementTree.fromstring(flatXml)
431 htmlElement = xmlTree.getchildren()[1]
432 flatHtml = htmlElement.text
435 def _grab_account_info(self, page):
436 tokenGroup = self._tokenRe.search(page)
437 if tokenGroup is None:
438 raise RuntimeError("Could not extract authentication token from GoogleVoice")
439 self._token = tokenGroup.group(1)
441 anGroup = self._accountNumRe.search(page)
442 if anGroup is not None:
443 self._accountNum = anGroup.group(1)
445 _moduleLogger.debug("Could not extract account number from GoogleVoice")
447 self._callbackNumbers = {}
448 for match in self._callbackRe.finditer(page):
449 callbackNumber = match.group(2)
450 callbackName = match.group(1)
451 self._callbackNumbers[callbackNumber] = callbackName
452 if len(self._callbackNumbers) == 0:
453 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
455 def _send_validation(self, number):
456 if not self.is_valid_syntax(number):
457 raise ValueError('Number is not valid: "%s"' % number)
458 elif not self.is_authed():
459 raise RuntimeError("Not Authenticated")
461 if len(number) == 11 and number[0] == 1:
462 # Strip leading 1 from 11 digit dialing
467 def _interpret_voicemail_regex(group):
468 quality, content, number = group.group(2), group.group(3), group.group(4)
469 if quality is not None and content is not None:
470 return quality, content
471 elif number is not None:
472 return "high", number
474 def _parse_voicemail(self, voicemailHtml):
475 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
476 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
477 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
478 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
479 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
480 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
481 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
482 locationGroup = self._voicemailLocationRegex.search(messageHtml)
483 location = locationGroup.group(1).strip() if locationGroup else ""
485 nameGroup = self._voicemailNameRegex.search(messageHtml)
486 name = nameGroup.group(1).strip() if nameGroup else ""
487 numberGroup = self._voicemailNumberRegex.search(messageHtml)
488 number = numberGroup.group(1).strip() if numberGroup else ""
489 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
490 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
491 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
492 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
494 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
496 self._interpret_voicemail_regex(group)
497 for group in messageGroups
498 ) if messageGroups else ()
501 "id": messageId.strip(),
502 "contactId": contactId,
505 "relTime": relativeTime,
506 "prettyNumber": prettyNumber,
508 "location": location,
509 "messageParts": messageParts,
513 def _decorate_voicemail(self, parsedVoicemails):
514 messagePartFormat = {
519 for voicemailData in parsedVoicemails:
521 messagePartFormat[quality] % part
522 for (quality, part) in voicemailData["messageParts"]
525 message = "No Transcription"
526 whoFrom = voicemailData["name"]
527 when = voicemailData["time"]
528 voicemailData["messageParts"] = ((whoFrom, message, when), )
531 def _parse_sms(self, smsHtml):
532 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
533 for messageId, messageHtml in itergroup(splitSms[1:], 2):
534 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
535 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
536 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
537 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
538 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
540 nameGroup = self._voicemailNameRegex.search(messageHtml)
541 name = nameGroup.group(1).strip() if nameGroup else ""
542 numberGroup = self._voicemailNumberRegex.search(messageHtml)
543 number = numberGroup.group(1).strip() if numberGroup else ""
544 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
545 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
546 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
547 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
549 fromGroups = self._smsFromRegex.finditer(messageHtml)
550 fromParts = (group.group(1).strip() for group in fromGroups)
551 textGroups = self._smsTextRegex.finditer(messageHtml)
552 textParts = (group.group(1).strip() for group in textGroups)
553 timeGroups = self._smsTimeRegex.finditer(messageHtml)
554 timeParts = (group.group(1).strip() for group in timeGroups)
556 messageParts = itertools.izip(fromParts, textParts, timeParts)
559 "id": messageId.strip(),
560 "contactId": contactId,
563 "relTime": relativeTime,
564 "prettyNumber": prettyNumber,
567 "messageParts": messageParts,
571 def _decorate_sms(self, parsedTexts):
575 def _merge_messages(parsedMessages, json):
576 for message in parsedMessages:
578 jsonItem = json["messages"][id]
579 message["isRead"] = jsonItem["isRead"]
580 message["isSpam"] = jsonItem["isSpam"]
581 message["isTrash"] = jsonItem["isTrash"]
582 message["isArchived"] = "inbox" not in jsonItem["labels"]
585 def _get_page(self, url, data = None, refererUrl = None):
587 if refererUrl is not None:
588 headers["Referer"] = refererUrl
590 encodedData = urllib.urlencode(data) if data is not None else None
593 page = self._browser.download(url, encodedData, None, headers)
594 except urllib2.URLError, e:
595 _moduleLogger.error("Translating error: %s" % str(e))
596 raise NetworkError("%s is not accesible" % url)
600 def _get_page_with_token(self, url, data = None, refererUrl = None):
603 data['_rnr_se'] = self._token
605 page = self._get_page(url, data, refererUrl)
609 def _parse_with_validation(self, page):
610 json, html = extract_payload(page)
611 validate_response(json)
615 def itergroup(iterator, count, padValue = None):
617 Iterate in groups of 'count' values. If there
618 aren't enough values, the last result is padded with
621 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
625 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
629 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
634 >>> for val in itergroup("123456", 3):
638 >>> for val in itergroup("123456", 3):
639 ... print repr("".join(val))
643 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
644 nIterators = (paddedIterator, ) * count
645 return itertools.izip(*nIterators)
649 _TRUE_REGEX = re.compile("true")
650 _FALSE_REGEX = re.compile("false")
651 s = _TRUE_REGEX.sub("True", s)
652 s = _FALSE_REGEX.sub("False", s)
653 return eval(s, {}, {})
656 def _fake_parse_json(flattened):
657 return safe_eval(flattened)
660 def _actual_parse_json(flattened):
661 return simplejson.loads(flattened)
664 if simplejson is None:
665 parse_json = _fake_parse_json
667 parse_json = _actual_parse_json
670 def extract_payload(flatXml):
671 xmlTree = ElementTree.fromstring(flatXml)
673 jsonElement = xmlTree.getchildren()[0]
674 flatJson = jsonElement.text
675 jsonTree = parse_json(flatJson)
677 htmlElement = xmlTree.getchildren()[1]
678 flatHtml = htmlElement.text
680 return jsonTree, flatHtml
683 def validate_response(response):
685 Validates that the JSON response is A-OK
688 assert 'ok' in response and response['ok']
689 except AssertionError:
690 raise RuntimeError('There was a problem with GV: %s' % response)
693 def guess_phone_type(number):
694 if number.startswith("747") or number.startswith("1747"):
695 return GVDialer.PHONE_TYPE_GIZMO
697 return GVDialer.PHONE_TYPE_MOBILE
700 def set_sane_callback(backend):
702 Try to set a sane default callback number on these preferences
703 1) 1747 numbers ( Gizmo )
704 2) anything with gizmo in the name
705 3) anything with computer in the name
708 numbers = backend.get_callback_numbers()
710 priorityOrderedCriteria = [
718 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
719 for number, description in numbers.iteritems():
720 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
722 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
724 backend.set_callback_number(number)
728 def sort_messages(allMessages):
729 sortableAllMessages = [
730 (message["time"], message)
731 for message in allMessages
733 sortableAllMessages.sort(reverse=True)
736 for (exactTime, message) in sortableAllMessages
740 def decorate_recent(recentCallData):
742 @returns (personsName, phoneNumber, date, action)
744 contactId = recentCallData["contactId"]
745 if recentCallData["name"]:
746 header = recentCallData["name"]
747 elif recentCallData["prettyNumber"]:
748 header = recentCallData["prettyNumber"]
749 elif recentCallData["location"]:
750 header = recentCallData["location"]
754 number = recentCallData["number"]
755 relTime = recentCallData["relTime"]
756 action = recentCallData["action"]
757 return contactId, header, number, relTime, action
760 def decorate_message(messageData):
761 contactId = messageData["contactId"]
762 exactTime = messageData["time"]
763 if messageData["name"]:
764 header = messageData["name"]
765 elif messageData["prettyNumber"]:
766 header = messageData["prettyNumber"]
769 number = messageData["number"]
770 relativeTime = messageData["relTime"]
772 messageParts = list(messageData["messageParts"])
773 if len(messageParts) == 0:
774 messages = ("No Transcription", )
775 elif len(messageParts) == 1:
776 messages = (messageParts[0][1], )
779 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
780 for messagePart in messageParts
783 decoratedResults = contactId, header, number, relativeTime, messages
784 return decoratedResults
787 def test_backend(username, password):
788 backend = GVoiceBackend()
789 print "Authenticated: ", backend.is_authed()
790 if not backend.is_authed():
791 print "Login?: ", backend.login(username, password)
792 print "Authenticated: ", backend.is_authed()
793 print "Is Dnd: ", backend.is_dnd()
794 #print "Setting Dnd", backend.set_dnd(True)
795 #print "Is Dnd: ", backend.is_dnd()
796 #print "Setting Dnd", backend.set_dnd(False)
797 #print "Is Dnd: ", backend.is_dnd()
799 #print "Token: ", backend._token
800 #print "Account: ", backend.get_account_number()
801 #print "Callback: ", backend.get_callback_number()
802 #print "All Callback: ",
804 #pprint.pprint(backend.get_callback_numbers())
807 #for data in backend.get_recent():
808 # pprint.pprint(data)
809 #for data in sort_messages(backend.get_recent()):
810 # pprint.pprint(decorate_recent(data))
811 #pprint.pprint(list(backend.get_recent()))
814 #for contact in backend.get_contacts():
816 # pprint.pprint(list(backend.get_contact_details(contact[0])))
819 #for message in backend.get_messages():
820 # pprint.pprint(message)
821 #for message in sort_messages(backend.get_messages()):
822 # pprint.pprint(decorate_message(message))
827 def grab_debug_info(username, password):
828 cookieFile = os.path.join(".", "raw_cookies.txt")
830 os.remove(cookieFile)
834 backend = GVoiceBackend(cookieFile)
835 browser = backend._browser
838 ("forward", backend._forwardURL),
839 ("token", backend._tokenURL),
840 ("login", backend._loginURL),
841 ("isdnd", backend._isDndURL),
842 ("contacts", backend._contactsURL),
844 ("account", backend._XML_ACCOUNT_URL),
845 ("voicemail", backend._XML_VOICEMAIL_URL),
846 ("sms", backend._XML_SMS_URL),
848 ("recent", backend._XML_RECENT_URL),
849 ("placed", backend._XML_PLACED_URL),
850 ("recieved", backend._XML_RECEIVED_URL),
851 ("missed", backend._XML_MISSED_URL),
855 print "Grabbing pre-login pages"
856 for name, url in _TEST_WEBPAGES:
858 page = browser.download(url)
859 except StandardError, e:
862 print "\tWriting to file"
863 with open("not_loggedin_%s.txt" % name, "w") as f:
867 print "Attempting login"
868 galxToken = backend._get_token()
869 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
870 with open("loggingin.txt", "w") as f:
871 print "\tWriting to file"
872 f.write(loginSuccessOrFailurePage)
874 backend._grab_account_info(loginSuccessOrFailurePage)
876 # Retry in case the redirect failed
877 # luckily is_authed does everything we need for a retry
878 loggedIn = backend.is_authed(True)
883 print "Grabbing post-login pages"
884 for name, url in _TEST_WEBPAGES:
886 page = browser.download(url)
887 except StandardError, e:
890 print "\tWriting to file"
891 with open("loggedin_%s.txt" % name, "w") as f:
895 browser.cookies.save()
896 print "\tWriting cookies to file"
897 with open("cookies.txt", "w") as f:
899 "%s: %s\n" % (c.name, c.value)
900 for c in browser.cookies
904 if __name__ == "__main__":
906 logging.basicConfig(level=logging.DEBUG)
908 grab_debug_info(sys.argv[1], sys.argv[2])
910 test_backend(sys.argv[1], sys.argv[2])