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 + "contacts/"
102 self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
103 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
106 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
107 'recorded', 'placed', 'received', 'missed'
109 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
110 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
111 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
112 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
113 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
114 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
115 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
116 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
117 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
118 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
119 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
121 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
122 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
123 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
124 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
126 self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
127 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
128 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
129 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
130 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
131 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
132 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
133 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
134 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
135 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
136 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
137 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
138 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
140 def is_quick_login_possible(self):
142 @returns True then is_authed might be enough to login, else full login is required
144 return self._loadedFromCookies or 0.0 < self._lastAuthed
146 def is_authed(self, force = False):
148 Attempts to detect a current session
149 @note Once logged in try not to reauth more than once a minute.
150 @returns If authenticated
152 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
153 isPreviouslyAuthed = self._token is not None
154 if isRecentledAuthed and isPreviouslyAuthed and not force:
158 page = self._get_page(self._forwardURL)
159 self._grab_account_info(page)
161 _moduleLogger.exception(str(e))
164 self._browser.save_cookies()
165 self._lastAuthed = time.time()
168 def _get_token(self):
169 tokenPage = self._get_page(self._tokenURL)
171 galxTokens = self._galxRe.search(tokenPage)
172 if galxTokens is not None:
173 galxToken = galxTokens.group(1)
176 _moduleLogger.debug("Could not grab GALX token")
179 def _login(self, username, password, token):
183 'service': "grandcentral",
186 "PersistentCookie": "yes",
188 "continue": self._forwardURL,
191 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
192 return loginSuccessOrFailurePage
194 def login(self, username, password):
196 Attempt to login to GoogleVoice
197 @returns Whether login was successful or not
200 galxToken = self._get_token()
201 loginSuccessOrFailurePage = self._login(username, password, galxToken)
204 self._grab_account_info(loginSuccessOrFailurePage)
206 # Retry in case the redirect failed
207 # luckily is_authed does everything we need for a retry
208 loggedIn = self.is_authed(True)
210 _moduleLogger.exception(str(e))
212 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
214 self._browser.save_cookies()
215 self._lastAuthed = time.time()
219 self._browser.clear_cookies()
220 self._browser.save_cookies()
222 self._lastAuthed = 0.0
225 isDndPage = self._get_page(self._isDndURL)
227 dndGroup = self._isDndRe.search(isDndPage)
230 dndStatus = dndGroup.group(1)
231 isDnd = True if dndStatus.strip().lower() == "true" else False
234 def set_dnd(self, doNotDisturb):
236 "doNotDisturb": 1 if doNotDisturb else 0,
237 "_rnr_se": self._token,
240 dndPage = self._get_page(self._setDndURL, dndPostData)
242 def call(self, outgoingNumber):
244 This is the main function responsible for initating the callback
246 outgoingNumber = self._send_validation(outgoingNumber)
247 subscriberNumber = None
248 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
251 'outgoingNumber': outgoingNumber,
252 'forwardingNumber': self._callbackNumber,
253 'subscriberNumber': subscriberNumber or 'undefined',
254 'phoneType': str(phoneType),
257 _moduleLogger.info("%r" % callData)
259 page = self._get_page_with_token(
263 self._parse_with_validation(page)
266 def cancel(self, outgoingNumber=None):
268 Cancels a call matching outgoing and forwarding numbers (if given).
269 Will raise an error if no matching call is being placed
271 page = self._get_page_with_token(
274 'outgoingNumber': outgoingNumber or 'undefined',
275 'forwardingNumber': self._callbackNumber or 'undefined',
279 self._parse_with_validation(page)
281 def send_sms(self, phoneNumber, message):
282 phoneNumber = self._send_validation(phoneNumber)
283 page = self._get_page_with_token(
286 'phoneNumber': phoneNumber,
290 self._parse_with_validation(page)
292 def search(self, query):
294 Search your Google Voice Account history for calls, voicemails, and sms
295 Returns ``Folder`` instance containting matching messages
297 page = self._get_page(
298 self._XML_SEARCH_URL,
301 json, html = extract_payload(page)
304 def get_feed(self, feed):
305 actualFeed = "_XML_%s_URL" % feed.upper()
306 feedUrl = getattr(self, actualFeed)
308 page = self._get_page(feedUrl)
309 json, html = extract_payload(page)
313 def download(self, messageId, adir):
315 Download a voicemail or recorded call MP3 matching the given ``msg``
316 which can either be a ``Message`` instance, or a SHA1 identifier.
317 Saves files to ``adir`` (defaults to current directory).
318 Message hashes can be found in ``self.voicemail().messages`` for example.
319 Returns location of saved file.
321 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
322 fn = os.path.join(adir, '%s.mp3' % messageId)
323 with open(fn, 'wb') as fo:
327 def is_valid_syntax(self, number):
329 @returns If This number be called ( syntax validation only )
331 return self._validateRe.match(number) is not None
333 def get_account_number(self):
335 @returns The GoogleVoice phone number
337 return self._accountNum
339 def get_callback_numbers(self):
341 @returns a dictionary mapping call back numbers to descriptions
342 @note These results are cached for 30 minutes.
344 if not self.is_authed():
346 return self._callbackNumbers
348 def set_callback_number(self, callbacknumber):
350 Set the number that GoogleVoice calls
351 @param callbacknumber should be a proper 10 digit number
353 self._callbackNumber = callbacknumber
356 def get_callback_number(self):
358 @returns Current callback number or None
360 return self._callbackNumber
362 def get_recent(self):
364 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
367 ("Received", self._XML_RECEIVED_URL),
368 ("Missed", self._XML_MISSED_URL),
369 ("Placed", self._XML_PLACED_URL),
371 flatXml = self._get_page(url)
373 allRecentHtml = self._grab_html(flatXml)
374 allRecentData = self._parse_voicemail(allRecentHtml)
375 for recentCallData in allRecentData:
376 recentCallData["action"] = action
379 def get_contacts(self):
381 @returns Iterable of (contact id, contact name)
383 page = self._get_page(self._XML_CONTACTS_URL)
384 contactsBody = self._contactsBodyRe.search(page)
385 if contactsBody is None:
386 raise RuntimeError("Could not extract contact information")
387 accountData = _fake_parse_json(contactsBody.group(1))
388 for contactId, contactDetails in accountData["contacts"].iteritems():
389 yield contactId, contactDetails
391 def get_messages(self):
392 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
393 voicemailHtml = self._grab_html(voicemailPage)
394 voicemailJson = self._grab_json(voicemailPage)
395 parsedVoicemail = self._parse_voicemail(voicemailHtml)
396 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
397 decoratedVoicemails = self._decorate_voicemail(voicemails)
399 smsPage = self._get_page(self._XML_SMS_URL)
400 smsHtml = self._grab_html(smsPage)
401 smsJson = self._grab_json(smsPage)
402 parsedSms = self._parse_sms(smsHtml)
403 smss = self._merge_messages(parsedSms, smsJson)
404 decoratedSms = self._decorate_sms(smss)
406 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
409 def _grab_json(self, flatXml):
410 xmlTree = ElementTree.fromstring(flatXml)
411 jsonElement = xmlTree.getchildren()[0]
412 flatJson = jsonElement.text
413 jsonTree = parse_json(flatJson)
416 def _grab_html(self, flatXml):
417 xmlTree = ElementTree.fromstring(flatXml)
418 htmlElement = xmlTree.getchildren()[1]
419 flatHtml = htmlElement.text
422 def _grab_account_info(self, page):
423 tokenGroup = self._tokenRe.search(page)
424 if tokenGroup is None:
425 raise RuntimeError("Could not extract authentication token from GoogleVoice")
426 self._token = tokenGroup.group(1)
428 anGroup = self._accountNumRe.search(page)
429 if anGroup is not None:
430 self._accountNum = anGroup.group(1)
432 _moduleLogger.debug("Could not extract account number from GoogleVoice")
434 self._callbackNumbers = {}
435 for match in self._callbackRe.finditer(page):
436 callbackNumber = match.group(2)
437 callbackName = match.group(1)
438 self._callbackNumbers[callbackNumber] = callbackName
439 if len(self._callbackNumbers) == 0:
440 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
442 def _send_validation(self, number):
443 if not self.is_valid_syntax(number):
444 raise ValueError('Number is not valid: "%s"' % number)
445 elif not self.is_authed():
446 raise RuntimeError("Not Authenticated")
448 if len(number) == 11 and number[0] == 1:
449 # Strip leading 1 from 11 digit dialing
454 def _interpret_voicemail_regex(group):
455 quality, content, number = group.group(2), group.group(3), group.group(4)
456 if quality is not None and content is not None:
457 return quality, content
458 elif number is not None:
459 return "high", number
461 def _parse_voicemail(self, voicemailHtml):
462 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
463 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
464 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
465 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
466 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
467 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
468 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
469 locationGroup = self._voicemailLocationRegex.search(messageHtml)
470 location = locationGroup.group(1).strip() if locationGroup else ""
472 nameGroup = self._voicemailNameRegex.search(messageHtml)
473 name = nameGroup.group(1).strip() if nameGroup else ""
474 numberGroup = self._voicemailNumberRegex.search(messageHtml)
475 number = numberGroup.group(1).strip() if numberGroup else ""
476 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
477 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
478 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
479 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
481 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
483 self._interpret_voicemail_regex(group)
484 for group in messageGroups
485 ) if messageGroups else ()
488 "id": messageId.strip(),
489 "contactId": contactId,
492 "relTime": relativeTime,
493 "prettyNumber": prettyNumber,
495 "location": location,
496 "messageParts": messageParts,
500 def _decorate_voicemail(self, parsedVoicemails):
501 messagePartFormat = {
506 for voicemailData in parsedVoicemails:
508 messagePartFormat[quality] % part
509 for (quality, part) in voicemailData["messageParts"]
512 message = "No Transcription"
513 whoFrom = voicemailData["name"]
514 when = voicemailData["time"]
515 voicemailData["messageParts"] = ((whoFrom, message, when), )
518 def _parse_sms(self, smsHtml):
519 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
520 for messageId, messageHtml in itergroup(splitSms[1:], 2):
521 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
522 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
523 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
524 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
525 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
527 nameGroup = self._voicemailNameRegex.search(messageHtml)
528 name = nameGroup.group(1).strip() if nameGroup else ""
529 numberGroup = self._voicemailNumberRegex.search(messageHtml)
530 number = numberGroup.group(1).strip() if numberGroup else ""
531 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
532 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
533 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
534 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
536 fromGroups = self._smsFromRegex.finditer(messageHtml)
537 fromParts = (group.group(1).strip() for group in fromGroups)
538 textGroups = self._smsTextRegex.finditer(messageHtml)
539 textParts = (group.group(1).strip() for group in textGroups)
540 timeGroups = self._smsTimeRegex.finditer(messageHtml)
541 timeParts = (group.group(1).strip() for group in timeGroups)
543 messageParts = itertools.izip(fromParts, textParts, timeParts)
546 "id": messageId.strip(),
547 "contactId": contactId,
550 "relTime": relativeTime,
551 "prettyNumber": prettyNumber,
554 "messageParts": messageParts,
558 def _decorate_sms(self, parsedTexts):
562 def _merge_messages(parsedMessages, json):
563 for message in parsedMessages:
565 jsonItem = json["messages"][id]
566 message["isRead"] = jsonItem["isRead"]
567 message["isSpam"] = jsonItem["isSpam"]
568 message["isTrash"] = jsonItem["isTrash"]
569 message["isArchived"] = "inbox" not in jsonItem["labels"]
572 def _get_page(self, url, data = None, refererUrl = None):
574 if refererUrl is not None:
575 headers["Referer"] = refererUrl
577 encodedData = urllib.urlencode(data) if data is not None else None
580 page = self._browser.download(url, encodedData, None, headers)
581 except urllib2.URLError, e:
582 _moduleLogger.error("Translating error: %s" % str(e))
583 raise NetworkError("%s is not accesible" % url)
587 def _get_page_with_token(self, url, data = None, refererUrl = None):
590 data['_rnr_se'] = self._token
592 page = self._get_page(url, data, refererUrl)
596 def _parse_with_validation(self, page):
597 json = parse_json(page)
598 validate_response(json)
602 def itergroup(iterator, count, padValue = None):
604 Iterate in groups of 'count' values. If there
605 aren't enough values, the last result is padded with
608 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
612 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
616 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
621 >>> for val in itergroup("123456", 3):
625 >>> for val in itergroup("123456", 3):
626 ... print repr("".join(val))
630 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
631 nIterators = (paddedIterator, ) * count
632 return itertools.izip(*nIterators)
636 _TRUE_REGEX = re.compile("true")
637 _FALSE_REGEX = re.compile("false")
638 s = _TRUE_REGEX.sub("True", s)
639 s = _FALSE_REGEX.sub("False", s)
640 return eval(s, {}, {})
643 def _fake_parse_json(flattened):
644 return safe_eval(flattened)
647 def _actual_parse_json(flattened):
648 return simplejson.loads(flattened)
651 if simplejson is None:
652 parse_json = _fake_parse_json
654 parse_json = _actual_parse_json
657 def extract_payload(flatXml):
658 xmlTree = ElementTree.fromstring(flatXml)
660 jsonElement = xmlTree.getchildren()[0]
661 flatJson = jsonElement.text
662 jsonTree = parse_json(flatJson)
664 htmlElement = xmlTree.getchildren()[1]
665 flatHtml = htmlElement.text
667 return jsonTree, flatHtml
670 def validate_response(response):
672 Validates that the JSON response is A-OK
675 assert 'ok' in response and response['ok']
676 except AssertionError:
677 raise RuntimeError('There was a problem with GV: %s' % response)
680 def guess_phone_type(number):
681 if number.startswith("747") or number.startswith("1747"):
682 return GVoiceBackend.PHONE_TYPE_GIZMO
684 return GVoiceBackend.PHONE_TYPE_MOBILE
687 def set_sane_callback(backend):
689 Try to set a sane default callback number on these preferences
690 1) 1747 numbers ( Gizmo )
691 2) anything with gizmo in the name
692 3) anything with computer in the name
695 numbers = backend.get_callback_numbers()
697 priorityOrderedCriteria = [
705 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
706 for number, description in numbers.iteritems():
707 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
709 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
711 backend.set_callback_number(number)
715 def sort_messages(allMessages):
716 sortableAllMessages = [
717 (message["time"], message)
718 for message in allMessages
720 sortableAllMessages.sort(reverse=True)
723 for (exactTime, message) in sortableAllMessages
727 def decorate_recent(recentCallData):
729 @returns (personsName, phoneNumber, date, action)
731 contactId = recentCallData["contactId"]
732 if recentCallData["name"]:
733 header = recentCallData["name"]
734 elif recentCallData["prettyNumber"]:
735 header = recentCallData["prettyNumber"]
736 elif recentCallData["location"]:
737 header = recentCallData["location"]
741 number = recentCallData["number"]
742 relTime = recentCallData["relTime"]
743 action = recentCallData["action"]
744 return contactId, header, number, relTime, action
747 def decorate_message(messageData):
748 contactId = messageData["contactId"]
749 exactTime = messageData["time"]
750 if messageData["name"]:
751 header = messageData["name"]
752 elif messageData["prettyNumber"]:
753 header = messageData["prettyNumber"]
756 number = messageData["number"]
757 relativeTime = messageData["relTime"]
759 messageParts = list(messageData["messageParts"])
760 if len(messageParts) == 0:
761 messages = ("No Transcription", )
762 elif len(messageParts) == 1:
763 messages = (messageParts[0][1], )
766 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
767 for messagePart in messageParts
770 decoratedResults = contactId, header, number, relativeTime, messages
771 return decoratedResults
774 def test_backend(username, password):
775 backend = GVoiceBackend()
776 print "Authenticated: ", backend.is_authed()
777 if not backend.is_authed():
778 print "Login?: ", backend.login(username, password)
779 print "Authenticated: ", backend.is_authed()
780 #print "Is Dnd: ", backend.is_dnd()
781 #print "Setting Dnd", backend.set_dnd(True)
782 #print "Is Dnd: ", backend.is_dnd()
783 #print "Setting Dnd", backend.set_dnd(False)
784 #print "Is Dnd: ", backend.is_dnd()
786 #print "Token: ", backend._token
787 #print "Account: ", backend.get_account_number()
788 #print "Callback: ", backend.get_callback_number()
789 #print "All Callback: ",
791 #pprint.pprint(backend.get_callback_numbers())
794 #for data in backend.get_recent():
795 # pprint.pprint(data)
796 #for data in sort_messages(backend.get_recent()):
797 # pprint.pprint(decorate_recent(data))
798 #pprint.pprint(list(backend.get_recent()))
801 for contact in backend.get_contacts():
802 pprint.pprint(contact)
805 #for message in backend.get_messages():
806 # pprint.pprint(message)
807 #for message in sort_messages(backend.get_messages()):
808 # pprint.pprint(decorate_message(message))
813 def grab_debug_info(username, password):
814 cookieFile = os.path.join(".", "raw_cookies.txt")
816 os.remove(cookieFile)
820 backend = GVoiceBackend(cookieFile)
821 browser = backend._browser
824 ("forward", backend._forwardURL),
825 ("token", backend._tokenURL),
826 ("login", backend._loginURL),
827 ("isdnd", backend._isDndURL),
828 ("account", backend._XML_ACCOUNT_URL),
829 ("contacts", backend._XML_CONTACTS_URL),
831 ("voicemail", backend._XML_VOICEMAIL_URL),
832 ("sms", backend._XML_SMS_URL),
834 ("recent", backend._XML_RECENT_URL),
835 ("placed", backend._XML_PLACED_URL),
836 ("recieved", backend._XML_RECEIVED_URL),
837 ("missed", backend._XML_MISSED_URL),
841 print "Grabbing pre-login pages"
842 for name, url in _TEST_WEBPAGES:
844 page = browser.download(url)
845 except StandardError, e:
848 print "\tWriting to file"
849 with open("not_loggedin_%s.txt" % name, "w") as f:
853 print "Attempting login"
854 galxToken = backend._get_token()
855 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
856 with open("loggingin.txt", "w") as f:
857 print "\tWriting to file"
858 f.write(loginSuccessOrFailurePage)
860 backend._grab_account_info(loginSuccessOrFailurePage)
862 # Retry in case the redirect failed
863 # luckily is_authed does everything we need for a retry
864 loggedIn = backend.is_authed(True)
869 print "Grabbing post-login pages"
870 for name, url in _TEST_WEBPAGES:
872 page = browser.download(url)
873 except StandardError, e:
876 print "\tWriting to file"
877 with open("loggedin_%s.txt" % name, "w") as f:
881 browser.save_cookies()
882 print "\tWriting cookies to file"
883 with open("cookies.txt", "w") as f:
885 "%s: %s\n" % (c.name, c.value)
886 for c in browser._cookies
890 if __name__ == "__main__":
892 logging.basicConfig(level=logging.DEBUG)
894 grab_debug_info(sys.argv[1], sys.argv[2])
896 test_backend(sys.argv[1], sys.argv[2])