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_quick_login_possible(self):
147 @returns True then is_authed might be enough to login, else full login is required
149 return self._loadedFromCookies or 0.0 < self._lastAuthed
151 def is_authed(self, force = False):
153 Attempts to detect a current session
154 @note Once logged in try not to reauth more than once a minute.
155 @returns If authenticated
157 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
158 isPreviouslyAuthed = self._token is not None
159 if isRecentledAuthed and isPreviouslyAuthed and not force:
163 page = self._get_page(self._forwardURL)
164 self._grab_account_info(page)
166 _moduleLogger.exception(str(e))
169 self._browser.save_cookies()
170 self._lastAuthed = time.time()
173 def _get_token(self):
174 tokenPage = self._get_page(self._tokenURL)
176 galxTokens = self._galxRe.search(tokenPage)
177 if galxTokens is not None:
178 galxToken = galxTokens.group(1)
181 _moduleLogger.debug("Could not grab GALX token")
184 def _login(self, username, password, token):
188 'service': "grandcentral",
191 "PersistentCookie": "yes",
193 "continue": self._forwardURL,
196 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
197 return loginSuccessOrFailurePage
199 def login(self, username, password):
201 Attempt to login to GoogleVoice
202 @returns Whether login was successful or not
205 galxToken = self._get_token()
206 loginSuccessOrFailurePage = self._login(username, password, galxToken)
209 self._grab_account_info(loginSuccessOrFailurePage)
211 # Retry in case the redirect failed
212 # luckily is_authed does everything we need for a retry
213 loggedIn = self.is_authed(True)
215 _moduleLogger.exception(str(e))
217 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
219 self._browser.save_cookies()
220 self._lastAuthed = time.time()
224 self._browser.clear_cookies()
225 self._browser.save_cookies()
227 self._lastAuthed = 0.0
230 isDndPage = self._get_page(self._isDndURL)
232 dndGroup = self._isDndRe.search(isDndPage)
235 dndStatus = dndGroup.group(1)
236 isDnd = True if dndStatus.strip().lower() == "true" else False
239 def set_dnd(self, doNotDisturb):
241 "doNotDisturb": 1 if doNotDisturb else 0,
242 "_rnr_se": self._token,
245 dndPage = self._get_page(self._setDndURL, dndPostData)
247 def call(self, outgoingNumber):
249 This is the main function responsible for initating the callback
251 outgoingNumber = self._send_validation(outgoingNumber)
252 subscriberNumber = None
253 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
256 'outgoingNumber': outgoingNumber,
257 'forwardingNumber': self._callbackNumber,
258 'subscriberNumber': subscriberNumber or 'undefined',
259 'phoneType': str(phoneType),
262 _moduleLogger.info("%r" % callData)
264 page = self._get_page_with_token(
268 self._parse_with_validation(page)
271 def cancel(self, outgoingNumber=None):
273 Cancels a call matching outgoing and forwarding numbers (if given).
274 Will raise an error if no matching call is being placed
276 page = self._get_page_with_token(
279 'outgoingNumber': outgoingNumber or 'undefined',
280 'forwardingNumber': self._callbackNumber or 'undefined',
284 self._parse_with_validation(page)
286 def send_sms(self, phoneNumber, message):
287 phoneNumber = self._send_validation(phoneNumber)
288 page = self._get_page_with_token(
291 'phoneNumber': phoneNumber,
295 self._parse_with_validation(page)
297 def search(self, query):
299 Search your Google Voice Account history for calls, voicemails, and sms
300 Returns ``Folder`` instance containting matching messages
302 page = self._get_page(
303 self._XML_SEARCH_URL,
306 json, html = extract_payload(page)
309 def get_feed(self, feed):
310 actualFeed = "_XML_%s_URL" % feed.upper()
311 feedUrl = getattr(self, actualFeed)
313 page = self._get_page(feedUrl)
314 json, html = extract_payload(page)
318 def download(self, messageId, adir):
320 Download a voicemail or recorded call MP3 matching the given ``msg``
321 which can either be a ``Message`` instance, or a SHA1 identifier.
322 Saves files to ``adir`` (defaults to current directory).
323 Message hashes can be found in ``self.voicemail().messages`` for example.
324 Returns location of saved file.
326 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
327 fn = os.path.join(adir, '%s.mp3' % messageId)
328 with open(fn, 'wb') as fo:
332 def is_valid_syntax(self, number):
334 @returns If This number be called ( syntax validation only )
336 return self._validateRe.match(number) is not None
338 def get_account_number(self):
340 @returns The GoogleVoice phone number
342 return self._accountNum
344 def get_callback_numbers(self):
346 @returns a dictionary mapping call back numbers to descriptions
347 @note These results are cached for 30 minutes.
349 if not self.is_authed():
351 return self._callbackNumbers
353 def set_callback_number(self, callbacknumber):
355 Set the number that GoogleVoice calls
356 @param callbacknumber should be a proper 10 digit number
358 self._callbackNumber = callbacknumber
361 def get_callback_number(self):
363 @returns Current callback number or None
365 return self._callbackNumber
367 def get_recent(self):
369 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
372 ("Received", self._XML_RECEIVED_URL),
373 ("Missed", self._XML_MISSED_URL),
374 ("Placed", self._XML_PLACED_URL),
376 flatXml = self._get_page(url)
378 allRecentHtml = self._grab_html(flatXml)
379 allRecentData = self._parse_voicemail(allRecentHtml)
380 for recentCallData in allRecentData:
381 recentCallData["action"] = action
384 def get_contacts(self):
386 @returns Iterable of (contact id, contact name)
388 contactsPagesUrls = [self._contactsURL]
389 for contactsPageUrl in contactsPagesUrls:
390 contactsPage = self._get_page(contactsPageUrl)
391 for contact_match in self._contactsRe.finditer(contactsPage):
392 contactId = contact_match.group(1)
393 contactName = saxutils.unescape(contact_match.group(2))
394 contact = contactId, contactName
397 next_match = self._contactsNextRe.match(contactsPage)
398 if next_match is not None:
399 newContactsPageUrl = self._contactsURL + next_match.group(1)
400 contactsPagesUrls.append(newContactsPageUrl)
402 def get_contact_details(self, contactId):
404 @returns Iterable of (Phone Type, Phone Number)
406 detailPage = self._get_page(self._contactDetailURL + '/' + contactId)
408 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
409 phoneNumber = detail_match.group(1)
410 phoneType = saxutils.unescape(detail_match.group(2))
411 yield (phoneType, phoneNumber)
413 def get_messages(self):
414 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
415 voicemailHtml = self._grab_html(voicemailPage)
416 voicemailJson = self._grab_json(voicemailPage)
417 parsedVoicemail = self._parse_voicemail(voicemailHtml)
418 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
419 decoratedVoicemails = self._decorate_voicemail(voicemails)
421 smsPage = self._get_page(self._XML_SMS_URL)
422 smsHtml = self._grab_html(smsPage)
423 smsJson = self._grab_json(smsPage)
424 parsedSms = self._parse_sms(smsHtml)
425 smss = self._merge_messages(parsedSms, smsJson)
426 decoratedSms = self._decorate_sms(smss)
428 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
431 def _grab_json(self, flatXml):
432 xmlTree = ElementTree.fromstring(flatXml)
433 jsonElement = xmlTree.getchildren()[0]
434 flatJson = jsonElement.text
435 jsonTree = parse_json(flatJson)
438 def _grab_html(self, flatXml):
439 xmlTree = ElementTree.fromstring(flatXml)
440 htmlElement = xmlTree.getchildren()[1]
441 flatHtml = htmlElement.text
444 def _grab_account_info(self, page):
445 tokenGroup = self._tokenRe.search(page)
446 if tokenGroup is None:
447 raise RuntimeError("Could not extract authentication token from GoogleVoice")
448 self._token = tokenGroup.group(1)
450 anGroup = self._accountNumRe.search(page)
451 if anGroup is not None:
452 self._accountNum = anGroup.group(1)
454 _moduleLogger.debug("Could not extract account number from GoogleVoice")
456 self._callbackNumbers = {}
457 for match in self._callbackRe.finditer(page):
458 callbackNumber = match.group(2)
459 callbackName = match.group(1)
460 self._callbackNumbers[callbackNumber] = callbackName
461 if len(self._callbackNumbers) == 0:
462 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
464 def _send_validation(self, number):
465 if not self.is_valid_syntax(number):
466 raise ValueError('Number is not valid: "%s"' % number)
467 elif not self.is_authed():
468 raise RuntimeError("Not Authenticated")
470 if len(number) == 11 and number[0] == 1:
471 # Strip leading 1 from 11 digit dialing
476 def _interpret_voicemail_regex(group):
477 quality, content, number = group.group(2), group.group(3), group.group(4)
478 if quality is not None and content is not None:
479 return quality, content
480 elif number is not None:
481 return "high", number
483 def _parse_voicemail(self, voicemailHtml):
484 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
485 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
486 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
487 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
488 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
489 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
490 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
491 locationGroup = self._voicemailLocationRegex.search(messageHtml)
492 location = locationGroup.group(1).strip() if locationGroup else ""
494 nameGroup = self._voicemailNameRegex.search(messageHtml)
495 name = nameGroup.group(1).strip() if nameGroup else ""
496 numberGroup = self._voicemailNumberRegex.search(messageHtml)
497 number = numberGroup.group(1).strip() if numberGroup else ""
498 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
499 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
500 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
501 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
503 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
505 self._interpret_voicemail_regex(group)
506 for group in messageGroups
507 ) if messageGroups else ()
510 "id": messageId.strip(),
511 "contactId": contactId,
514 "relTime": relativeTime,
515 "prettyNumber": prettyNumber,
517 "location": location,
518 "messageParts": messageParts,
522 def _decorate_voicemail(self, parsedVoicemails):
523 messagePartFormat = {
528 for voicemailData in parsedVoicemails:
530 messagePartFormat[quality] % part
531 for (quality, part) in voicemailData["messageParts"]
534 message = "No Transcription"
535 whoFrom = voicemailData["name"]
536 when = voicemailData["time"]
537 voicemailData["messageParts"] = ((whoFrom, message, when), )
540 def _parse_sms(self, smsHtml):
541 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
542 for messageId, messageHtml in itergroup(splitSms[1:], 2):
543 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
544 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
545 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
546 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
547 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
549 nameGroup = self._voicemailNameRegex.search(messageHtml)
550 name = nameGroup.group(1).strip() if nameGroup else ""
551 numberGroup = self._voicemailNumberRegex.search(messageHtml)
552 number = numberGroup.group(1).strip() if numberGroup else ""
553 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
554 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
555 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
556 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
558 fromGroups = self._smsFromRegex.finditer(messageHtml)
559 fromParts = (group.group(1).strip() for group in fromGroups)
560 textGroups = self._smsTextRegex.finditer(messageHtml)
561 textParts = (group.group(1).strip() for group in textGroups)
562 timeGroups = self._smsTimeRegex.finditer(messageHtml)
563 timeParts = (group.group(1).strip() for group in timeGroups)
565 messageParts = itertools.izip(fromParts, textParts, timeParts)
568 "id": messageId.strip(),
569 "contactId": contactId,
572 "relTime": relativeTime,
573 "prettyNumber": prettyNumber,
576 "messageParts": messageParts,
580 def _decorate_sms(self, parsedTexts):
584 def _merge_messages(parsedMessages, json):
585 for message in parsedMessages:
587 jsonItem = json["messages"][id]
588 message["isRead"] = jsonItem["isRead"]
589 message["isSpam"] = jsonItem["isSpam"]
590 message["isTrash"] = jsonItem["isTrash"]
591 message["isArchived"] = "inbox" not in jsonItem["labels"]
594 def _get_page(self, url, data = None, refererUrl = None):
596 if refererUrl is not None:
597 headers["Referer"] = refererUrl
599 encodedData = urllib.urlencode(data) if data is not None else None
602 page = self._browser.download(url, encodedData, None, headers)
603 except urllib2.URLError, e:
604 _moduleLogger.error("Translating error: %s" % str(e))
605 raise NetworkError("%s is not accesible" % url)
609 def _get_page_with_token(self, url, data = None, refererUrl = None):
612 data['_rnr_se'] = self._token
614 page = self._get_page(url, data, refererUrl)
618 def _parse_with_validation(self, page):
619 json = parse_json(page)
620 validate_response(json)
624 def itergroup(iterator, count, padValue = None):
626 Iterate in groups of 'count' values. If there
627 aren't enough values, the last result is padded with
630 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
634 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
638 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
643 >>> for val in itergroup("123456", 3):
647 >>> for val in itergroup("123456", 3):
648 ... print repr("".join(val))
652 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
653 nIterators = (paddedIterator, ) * count
654 return itertools.izip(*nIterators)
658 _TRUE_REGEX = re.compile("true")
659 _FALSE_REGEX = re.compile("false")
660 s = _TRUE_REGEX.sub("True", s)
661 s = _FALSE_REGEX.sub("False", s)
662 return eval(s, {}, {})
665 def _fake_parse_json(flattened):
666 return safe_eval(flattened)
669 def _actual_parse_json(flattened):
670 return simplejson.loads(flattened)
673 if simplejson is None:
674 parse_json = _fake_parse_json
676 parse_json = _actual_parse_json
679 def extract_payload(flatXml):
680 xmlTree = ElementTree.fromstring(flatXml)
682 jsonElement = xmlTree.getchildren()[0]
683 flatJson = jsonElement.text
684 jsonTree = parse_json(flatJson)
686 htmlElement = xmlTree.getchildren()[1]
687 flatHtml = htmlElement.text
689 return jsonTree, flatHtml
692 def validate_response(response):
694 Validates that the JSON response is A-OK
697 assert 'ok' in response and response['ok']
698 except AssertionError:
699 raise RuntimeError('There was a problem with GV: %s' % response)
702 def guess_phone_type(number):
703 if number.startswith("747") or number.startswith("1747"):
704 return GVoiceBackend.PHONE_TYPE_GIZMO
706 return GVoiceBackend.PHONE_TYPE_MOBILE
709 def set_sane_callback(backend):
711 Try to set a sane default callback number on these preferences
712 1) 1747 numbers ( Gizmo )
713 2) anything with gizmo in the name
714 3) anything with computer in the name
717 numbers = backend.get_callback_numbers()
719 priorityOrderedCriteria = [
727 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
728 for number, description in numbers.iteritems():
729 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
731 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
733 backend.set_callback_number(number)
737 def sort_messages(allMessages):
738 sortableAllMessages = [
739 (message["time"], message)
740 for message in allMessages
742 sortableAllMessages.sort(reverse=True)
745 for (exactTime, message) in sortableAllMessages
749 def decorate_recent(recentCallData):
751 @returns (personsName, phoneNumber, date, action)
753 contactId = recentCallData["contactId"]
754 if recentCallData["name"]:
755 header = recentCallData["name"]
756 elif recentCallData["prettyNumber"]:
757 header = recentCallData["prettyNumber"]
758 elif recentCallData["location"]:
759 header = recentCallData["location"]
763 number = recentCallData["number"]
764 relTime = recentCallData["relTime"]
765 action = recentCallData["action"]
766 return contactId, header, number, relTime, action
769 def decorate_message(messageData):
770 contactId = messageData["contactId"]
771 exactTime = messageData["time"]
772 if messageData["name"]:
773 header = messageData["name"]
774 elif messageData["prettyNumber"]:
775 header = messageData["prettyNumber"]
778 number = messageData["number"]
779 relativeTime = messageData["relTime"]
781 messageParts = list(messageData["messageParts"])
782 if len(messageParts) == 0:
783 messages = ("No Transcription", )
784 elif len(messageParts) == 1:
785 messages = (messageParts[0][1], )
788 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
789 for messagePart in messageParts
792 decoratedResults = contactId, header, number, relativeTime, messages
793 return decoratedResults
796 def test_backend(username, password):
797 backend = GVoiceBackend()
798 print "Authenticated: ", backend.is_authed()
799 if not backend.is_authed():
800 print "Login?: ", backend.login(username, password)
801 print "Authenticated: ", backend.is_authed()
802 print "Is Dnd: ", backend.is_dnd()
803 #print "Setting Dnd", backend.set_dnd(True)
804 #print "Is Dnd: ", backend.is_dnd()
805 #print "Setting Dnd", backend.set_dnd(False)
806 #print "Is Dnd: ", backend.is_dnd()
808 #print "Token: ", backend._token
809 #print "Account: ", backend.get_account_number()
810 #print "Callback: ", backend.get_callback_number()
811 #print "All Callback: ",
813 #pprint.pprint(backend.get_callback_numbers())
816 #for data in backend.get_recent():
817 # pprint.pprint(data)
818 #for data in sort_messages(backend.get_recent()):
819 # pprint.pprint(decorate_recent(data))
820 #pprint.pprint(list(backend.get_recent()))
823 #for contact in backend.get_contacts():
825 # pprint.pprint(list(backend.get_contact_details(contact[0])))
828 #for message in backend.get_messages():
829 # pprint.pprint(message)
830 #for message in sort_messages(backend.get_messages()):
831 # pprint.pprint(decorate_message(message))
836 def grab_debug_info(username, password):
837 cookieFile = os.path.join(".", "raw_cookies.txt")
839 os.remove(cookieFile)
843 backend = GVoiceBackend(cookieFile)
844 browser = backend._browser
847 ("forward", backend._forwardURL),
848 ("token", backend._tokenURL),
849 ("login", backend._loginURL),
850 ("isdnd", backend._isDndURL),
851 ("contacts", backend._contactsURL),
853 ("account", backend._XML_ACCOUNT_URL),
854 ("voicemail", backend._XML_VOICEMAIL_URL),
855 ("sms", backend._XML_SMS_URL),
857 ("recent", backend._XML_RECENT_URL),
858 ("placed", backend._XML_PLACED_URL),
859 ("recieved", backend._XML_RECEIVED_URL),
860 ("missed", backend._XML_MISSED_URL),
864 print "Grabbing pre-login pages"
865 for name, url in _TEST_WEBPAGES:
867 page = browser.download(url)
868 except StandardError, e:
871 print "\tWriting to file"
872 with open("not_loggedin_%s.txt" % name, "w") as f:
876 print "Attempting login"
877 galxToken = backend._get_token()
878 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
879 with open("loggingin.txt", "w") as f:
880 print "\tWriting to file"
881 f.write(loginSuccessOrFailurePage)
883 backend._grab_account_info(loginSuccessOrFailurePage)
885 # Retry in case the redirect failed
886 # luckily is_authed does everything we need for a retry
887 loggedIn = backend.is_authed(True)
892 print "Grabbing post-login pages"
893 for name, url in _TEST_WEBPAGES:
895 page = browser.download(url)
896 except StandardError, e:
899 print "\tWriting to file"
900 with open("loggedin_%s.txt" % name, "w") as f:
904 browser.cookies.save()
905 print "\tWriting cookies to file"
906 with open("cookies.txt", "w") as f:
908 "%s: %s\n" % (c.name, c.value)
909 for c in browser.cookies
913 if __name__ == "__main__":
915 logging.basicConfig(level=logging.DEBUG)
917 grab_debug_info(sys.argv[1], sys.argv[2])
919 test_backend(sys.argv[1], sys.argv[2])