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 # HACK really this redirects to the main pge and we are grabbing some javascript
103 self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
104 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
107 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
108 'recorded', 'placed', 'received', 'missed'
110 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
111 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
112 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
113 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
114 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
115 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
116 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
117 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
118 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
119 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
120 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
122 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
123 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
124 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
125 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
127 self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
128 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
129 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
130 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
131 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
132 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
133 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
134 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
135 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
136 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
137 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
138 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
139 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
141 def is_quick_login_possible(self):
143 @returns True then is_authed might be enough to login, else full login is required
145 return self._loadedFromCookies or 0.0 < self._lastAuthed
147 def is_authed(self, force = False):
149 Attempts to detect a current session
150 @note Once logged in try not to reauth more than once a minute.
151 @returns If authenticated
153 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
154 isPreviouslyAuthed = self._token is not None
155 if isRecentledAuthed and isPreviouslyAuthed and not force:
159 page = self._get_page(self._forwardURL)
160 self._grab_account_info(page)
162 _moduleLogger.exception(str(e))
165 self._browser.save_cookies()
166 self._lastAuthed = time.time()
169 def _get_token(self):
170 tokenPage = self._get_page(self._tokenURL)
172 galxTokens = self._galxRe.search(tokenPage)
173 if galxTokens is not None:
174 galxToken = galxTokens.group(1)
177 _moduleLogger.debug("Could not grab GALX token")
180 def _login(self, username, password, token):
184 'service': "grandcentral",
187 "PersistentCookie": "yes",
189 "continue": self._forwardURL,
192 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
193 return loginSuccessOrFailurePage
195 def login(self, username, password):
197 Attempt to login to GoogleVoice
198 @returns Whether login was successful or not
201 galxToken = self._get_token()
202 loginSuccessOrFailurePage = self._login(username, password, galxToken)
205 self._grab_account_info(loginSuccessOrFailurePage)
207 # Retry in case the redirect failed
208 # luckily is_authed does everything we need for a retry
209 loggedIn = self.is_authed(True)
211 _moduleLogger.exception(str(e))
213 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
215 self._browser.save_cookies()
216 self._lastAuthed = time.time()
220 self._browser.clear_cookies()
221 self._browser.save_cookies()
223 self._lastAuthed = 0.0
226 isDndPage = self._get_page(self._isDndURL)
228 dndGroup = self._isDndRe.search(isDndPage)
231 dndStatus = dndGroup.group(1)
232 isDnd = True if dndStatus.strip().lower() == "true" else False
235 def set_dnd(self, doNotDisturb):
237 "doNotDisturb": 1 if doNotDisturb else 0,
238 "_rnr_se": self._token,
241 dndPage = self._get_page(self._setDndURL, dndPostData)
243 def call(self, outgoingNumber):
245 This is the main function responsible for initating the callback
247 outgoingNumber = self._send_validation(outgoingNumber)
248 subscriberNumber = None
249 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
252 'outgoingNumber': outgoingNumber,
253 'forwardingNumber': self._callbackNumber,
254 'subscriberNumber': subscriberNumber or 'undefined',
255 'phoneType': str(phoneType),
258 _moduleLogger.info("%r" % callData)
260 page = self._get_page_with_token(
264 self._parse_with_validation(page)
267 def cancel(self, outgoingNumber=None):
269 Cancels a call matching outgoing and forwarding numbers (if given).
270 Will raise an error if no matching call is being placed
272 page = self._get_page_with_token(
275 'outgoingNumber': outgoingNumber or 'undefined',
276 'forwardingNumber': self._callbackNumber or 'undefined',
280 self._parse_with_validation(page)
282 def send_sms(self, phoneNumber, message):
283 phoneNumber = self._send_validation(phoneNumber)
284 page = self._get_page_with_token(
287 'phoneNumber': phoneNumber,
291 self._parse_with_validation(page)
293 def search(self, query):
295 Search your Google Voice Account history for calls, voicemails, and sms
296 Returns ``Folder`` instance containting matching messages
298 page = self._get_page(
299 self._XML_SEARCH_URL,
302 json, html = extract_payload(page)
305 def get_feed(self, feed):
306 actualFeed = "_XML_%s_URL" % feed.upper()
307 feedUrl = getattr(self, actualFeed)
309 page = self._get_page(feedUrl)
310 json, html = extract_payload(page)
314 def download(self, messageId, adir):
316 Download a voicemail or recorded call MP3 matching the given ``msg``
317 which can either be a ``Message`` instance, or a SHA1 identifier.
318 Saves files to ``adir`` (defaults to current directory).
319 Message hashes can be found in ``self.voicemail().messages`` for example.
320 Returns location of saved file.
322 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
323 fn = os.path.join(adir, '%s.mp3' % messageId)
324 with open(fn, 'wb') as fo:
328 def is_valid_syntax(self, number):
330 @returns If This number be called ( syntax validation only )
332 return self._validateRe.match(number) is not None
334 def get_account_number(self):
336 @returns The GoogleVoice phone number
338 return self._accountNum
340 def get_callback_numbers(self):
342 @returns a dictionary mapping call back numbers to descriptions
343 @note These results are cached for 30 minutes.
345 if not self.is_authed():
347 return self._callbackNumbers
349 def set_callback_number(self, callbacknumber):
351 Set the number that GoogleVoice calls
352 @param callbacknumber should be a proper 10 digit number
354 self._callbackNumber = callbacknumber
357 def get_callback_number(self):
359 @returns Current callback number or None
361 return self._callbackNumber
363 def get_recent(self):
365 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
368 ("Received", self._XML_RECEIVED_URL),
369 ("Missed", self._XML_MISSED_URL),
370 ("Placed", self._XML_PLACED_URL),
372 flatXml = self._get_page(url)
374 allRecentHtml = self._grab_html(flatXml)
375 allRecentData = self._parse_voicemail(allRecentHtml)
376 for recentCallData in allRecentData:
377 recentCallData["action"] = action
380 def get_contacts(self):
382 @returns Iterable of (contact id, contact name)
384 page = self._get_page(self._XML_CONTACTS_URL)
385 contactsBody = self._contactsBodyRe.search(page)
386 if contactsBody is None:
387 raise RuntimeError("Could not extract contact information")
388 accountData = _fake_parse_json(contactsBody.group(1))
389 for contactId, contactDetails in accountData["contacts"].iteritems():
390 # A zero contact id is the catch all for unknown contacts
392 yield contactId, contactDetails
394 def get_messages(self):
395 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
396 voicemailHtml = self._grab_html(voicemailPage)
397 voicemailJson = self._grab_json(voicemailPage)
398 parsedVoicemail = self._parse_voicemail(voicemailHtml)
399 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
400 decoratedVoicemails = self._decorate_voicemail(voicemails)
402 smsPage = self._get_page(self._XML_SMS_URL)
403 smsHtml = self._grab_html(smsPage)
404 smsJson = self._grab_json(smsPage)
405 parsedSms = self._parse_sms(smsHtml)
406 smss = self._merge_messages(parsedSms, smsJson)
407 decoratedSms = self._decorate_sms(smss)
409 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
412 def _grab_json(self, flatXml):
413 xmlTree = ElementTree.fromstring(flatXml)
414 jsonElement = xmlTree.getchildren()[0]
415 flatJson = jsonElement.text
416 jsonTree = parse_json(flatJson)
419 def _grab_html(self, flatXml):
420 xmlTree = ElementTree.fromstring(flatXml)
421 htmlElement = xmlTree.getchildren()[1]
422 flatHtml = htmlElement.text
425 def _grab_account_info(self, page):
426 tokenGroup = self._tokenRe.search(page)
427 if tokenGroup is None:
428 raise RuntimeError("Could not extract authentication token from GoogleVoice")
429 self._token = tokenGroup.group(1)
431 anGroup = self._accountNumRe.search(page)
432 if anGroup is not None:
433 self._accountNum = anGroup.group(1)
435 _moduleLogger.debug("Could not extract account number from GoogleVoice")
437 self._callbackNumbers = {}
438 for match in self._callbackRe.finditer(page):
439 callbackNumber = match.group(2)
440 callbackName = match.group(1)
441 self._callbackNumbers[callbackNumber] = callbackName
442 if len(self._callbackNumbers) == 0:
443 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
445 def _send_validation(self, number):
446 if not self.is_valid_syntax(number):
447 raise ValueError('Number is not valid: "%s"' % number)
448 elif not self.is_authed():
449 raise RuntimeError("Not Authenticated")
451 if len(number) == 11 and number[0] == 1:
452 # Strip leading 1 from 11 digit dialing
457 def _interpret_voicemail_regex(group):
458 quality, content, number = group.group(2), group.group(3), group.group(4)
459 if quality is not None and content is not None:
460 return quality, content
461 elif number is not None:
462 return "high", number
464 def _parse_voicemail(self, voicemailHtml):
465 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
466 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
467 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
468 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
469 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
470 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
471 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
472 locationGroup = self._voicemailLocationRegex.search(messageHtml)
473 location = locationGroup.group(1).strip() if locationGroup else ""
475 nameGroup = self._voicemailNameRegex.search(messageHtml)
476 name = nameGroup.group(1).strip() if nameGroup else ""
477 numberGroup = self._voicemailNumberRegex.search(messageHtml)
478 number = numberGroup.group(1).strip() if numberGroup else ""
479 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
480 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
481 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
482 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
484 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
486 self._interpret_voicemail_regex(group)
487 for group in messageGroups
488 ) if messageGroups else ()
491 "id": messageId.strip(),
492 "contactId": contactId,
495 "relTime": relativeTime,
496 "prettyNumber": prettyNumber,
498 "location": location,
499 "messageParts": messageParts,
503 def _decorate_voicemail(self, parsedVoicemails):
504 messagePartFormat = {
509 for voicemailData in parsedVoicemails:
511 messagePartFormat[quality] % part
512 for (quality, part) in voicemailData["messageParts"]
515 message = "No Transcription"
516 whoFrom = voicemailData["name"]
517 when = voicemailData["time"]
518 voicemailData["messageParts"] = ((whoFrom, message, when), )
521 def _parse_sms(self, smsHtml):
522 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
523 for messageId, messageHtml in itergroup(splitSms[1:], 2):
524 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
525 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
526 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
527 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
528 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
530 nameGroup = self._voicemailNameRegex.search(messageHtml)
531 name = nameGroup.group(1).strip() if nameGroup else ""
532 numberGroup = self._voicemailNumberRegex.search(messageHtml)
533 number = numberGroup.group(1).strip() if numberGroup else ""
534 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
535 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
536 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
537 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
539 fromGroups = self._smsFromRegex.finditer(messageHtml)
540 fromParts = (group.group(1).strip() for group in fromGroups)
541 textGroups = self._smsTextRegex.finditer(messageHtml)
542 textParts = (group.group(1).strip() for group in textGroups)
543 timeGroups = self._smsTimeRegex.finditer(messageHtml)
544 timeParts = (group.group(1).strip() for group in timeGroups)
546 messageParts = itertools.izip(fromParts, textParts, timeParts)
549 "id": messageId.strip(),
550 "contactId": contactId,
553 "relTime": relativeTime,
554 "prettyNumber": prettyNumber,
557 "messageParts": messageParts,
561 def _decorate_sms(self, parsedTexts):
565 def _merge_messages(parsedMessages, json):
566 for message in parsedMessages:
568 jsonItem = json["messages"][id]
569 message["isRead"] = jsonItem["isRead"]
570 message["isSpam"] = jsonItem["isSpam"]
571 message["isTrash"] = jsonItem["isTrash"]
572 message["isArchived"] = "inbox" not in jsonItem["labels"]
575 def _get_page(self, url, data = None, refererUrl = None):
577 if refererUrl is not None:
578 headers["Referer"] = refererUrl
580 encodedData = urllib.urlencode(data) if data is not None else None
583 page = self._browser.download(url, encodedData, None, headers)
584 except urllib2.URLError, e:
585 _moduleLogger.error("Translating error: %s" % str(e))
586 raise NetworkError("%s is not accesible" % url)
590 def _get_page_with_token(self, url, data = None, refererUrl = None):
593 data['_rnr_se'] = self._token
595 page = self._get_page(url, data, refererUrl)
599 def _parse_with_validation(self, page):
600 json = parse_json(page)
601 validate_response(json)
605 def itergroup(iterator, count, padValue = None):
607 Iterate in groups of 'count' values. If there
608 aren't enough values, the last result is padded with
611 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
615 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
619 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
624 >>> for val in itergroup("123456", 3):
628 >>> for val in itergroup("123456", 3):
629 ... print repr("".join(val))
633 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
634 nIterators = (paddedIterator, ) * count
635 return itertools.izip(*nIterators)
639 _TRUE_REGEX = re.compile("true")
640 _FALSE_REGEX = re.compile("false")
641 s = _TRUE_REGEX.sub("True", s)
642 s = _FALSE_REGEX.sub("False", s)
643 return eval(s, {}, {})
646 def _fake_parse_json(flattened):
647 return safe_eval(flattened)
650 def _actual_parse_json(flattened):
651 return simplejson.loads(flattened)
654 if simplejson is None:
655 parse_json = _fake_parse_json
657 parse_json = _actual_parse_json
660 def extract_payload(flatXml):
661 xmlTree = ElementTree.fromstring(flatXml)
663 jsonElement = xmlTree.getchildren()[0]
664 flatJson = jsonElement.text
665 jsonTree = parse_json(flatJson)
667 htmlElement = xmlTree.getchildren()[1]
668 flatHtml = htmlElement.text
670 return jsonTree, flatHtml
673 def validate_response(response):
675 Validates that the JSON response is A-OK
678 assert 'ok' in response and response['ok']
679 except AssertionError:
680 raise RuntimeError('There was a problem with GV: %s' % response)
683 def guess_phone_type(number):
684 if number.startswith("747") or number.startswith("1747"):
685 return GVoiceBackend.PHONE_TYPE_GIZMO
687 return GVoiceBackend.PHONE_TYPE_MOBILE
690 def set_sane_callback(backend):
692 Try to set a sane default callback number on these preferences
693 1) 1747 numbers ( Gizmo )
694 2) anything with gizmo in the name
695 3) anything with computer in the name
698 numbers = backend.get_callback_numbers()
700 priorityOrderedCriteria = [
708 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
709 for number, description in numbers.iteritems():
710 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
712 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
714 backend.set_callback_number(number)
718 def sort_messages(allMessages):
719 sortableAllMessages = [
720 (message["time"], message)
721 for message in allMessages
723 sortableAllMessages.sort(reverse=True)
726 for (exactTime, message) in sortableAllMessages
730 def decorate_recent(recentCallData):
732 @returns (personsName, phoneNumber, date, action)
734 contactId = recentCallData["contactId"]
735 if recentCallData["name"]:
736 header = recentCallData["name"]
737 elif recentCallData["prettyNumber"]:
738 header = recentCallData["prettyNumber"]
739 elif recentCallData["location"]:
740 header = recentCallData["location"]
744 number = recentCallData["number"]
745 relTime = recentCallData["relTime"]
746 action = recentCallData["action"]
747 return contactId, header, number, relTime, action
750 def decorate_message(messageData):
751 contactId = messageData["contactId"]
752 exactTime = messageData["time"]
753 if messageData["name"]:
754 header = messageData["name"]
755 elif messageData["prettyNumber"]:
756 header = messageData["prettyNumber"]
759 number = messageData["number"]
760 relativeTime = messageData["relTime"]
762 messageParts = list(messageData["messageParts"])
763 if len(messageParts) == 0:
764 messages = ("No Transcription", )
765 elif len(messageParts) == 1:
766 messages = (messageParts[0][1], )
769 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
770 for messagePart in messageParts
773 decoratedResults = contactId, header, number, relativeTime, messages
774 return decoratedResults
777 def test_backend(username, password):
778 backend = GVoiceBackend()
779 print "Authenticated: ", backend.is_authed()
780 if not backend.is_authed():
781 print "Login?: ", backend.login(username, password)
782 print "Authenticated: ", backend.is_authed()
783 #print "Is Dnd: ", backend.is_dnd()
784 #print "Setting Dnd", backend.set_dnd(True)
785 #print "Is Dnd: ", backend.is_dnd()
786 #print "Setting Dnd", backend.set_dnd(False)
787 #print "Is Dnd: ", backend.is_dnd()
789 #print "Token: ", backend._token
790 #print "Account: ", backend.get_account_number()
791 #print "Callback: ", backend.get_callback_number()
792 #print "All Callback: ",
794 #pprint.pprint(backend.get_callback_numbers())
797 #for data in backend.get_recent():
798 # pprint.pprint(data)
799 #for data in sort_messages(backend.get_recent()):
800 # pprint.pprint(decorate_recent(data))
801 #pprint.pprint(list(backend.get_recent()))
804 for contact in backend.get_contacts():
805 pprint.pprint(contact)
808 #for message in backend.get_messages():
809 # pprint.pprint(message)
810 #for message in sort_messages(backend.get_messages()):
811 # pprint.pprint(decorate_message(message))
816 def grab_debug_info(username, password):
817 cookieFile = os.path.join(".", "raw_cookies.txt")
819 os.remove(cookieFile)
823 backend = GVoiceBackend(cookieFile)
824 browser = backend._browser
827 ("forward", backend._forwardURL),
828 ("token", backend._tokenURL),
829 ("login", backend._loginURL),
830 ("isdnd", backend._isDndURL),
831 ("account", backend._XML_ACCOUNT_URL),
832 ("contacts", backend._XML_CONTACTS_URL),
834 ("voicemail", backend._XML_VOICEMAIL_URL),
835 ("sms", backend._XML_SMS_URL),
837 ("recent", backend._XML_RECENT_URL),
838 ("placed", backend._XML_PLACED_URL),
839 ("recieved", backend._XML_RECEIVED_URL),
840 ("missed", backend._XML_MISSED_URL),
844 print "Grabbing pre-login pages"
845 for name, url in _TEST_WEBPAGES:
847 page = browser.download(url)
848 except StandardError, e:
851 print "\tWriting to file"
852 with open("not_loggedin_%s.txt" % name, "w") as f:
856 print "Attempting login"
857 galxToken = backend._get_token()
858 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
859 with open("loggingin.txt", "w") as f:
860 print "\tWriting to file"
861 f.write(loginSuccessOrFailurePage)
863 backend._grab_account_info(loginSuccessOrFailurePage)
865 # Retry in case the redirect failed
866 # luckily is_authed does everything we need for a retry
867 loggedIn = backend.is_authed(True)
872 print "Grabbing post-login pages"
873 for name, url in _TEST_WEBPAGES:
875 page = browser.download(url)
876 except StandardError, e:
879 print "\tWriting to file"
880 with open("loggedin_%s.txt" % name, "w") as f:
884 browser.save_cookies()
885 print "\tWriting cookies to file"
886 with open("cookies.txt", "w") as f:
888 "%s: %s\n" % (c.name, c.value)
889 for c in browser._cookies
893 if __name__ == "__main__":
895 logging.basicConfig(level=logging.DEBUG)
897 grab_debug_info(sys.argv[1], sys.argv[2])
899 test_backend(sys.argv[1], sys.argv[2])