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
250 'outgoingNumber': outgoingNumber,
251 'forwardingNumber': self._callbackNumber,
252 'subscriberNumber': subscriberNumber or 'undefined',
253 'phoneType': str(phoneType),
256 _moduleLogger.info("%r" % callData)
258 page = self._get_page_with_token(
262 _moduleLogger.info(page)
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 contactsPagesUrls = [self._contactsURL]
384 for contactsPageUrl in contactsPagesUrls:
385 contactsPage = self._get_page(contactsPageUrl)
386 for contact_match in self._contactsRe.finditer(contactsPage):
387 contactId = contact_match.group(1)
388 contactName = saxutils.unescape(contact_match.group(2))
389 contact = contactId, contactName
392 next_match = self._contactsNextRe.match(contactsPage)
393 if next_match is not None:
394 newContactsPageUrl = self._contactsURL + next_match.group(1)
395 contactsPagesUrls.append(newContactsPageUrl)
397 def get_contact_details(self, contactId):
399 @returns Iterable of (Phone Type, Phone Number)
401 detailPage = self._get_page(self._contactDetailURL + '/' + contactId)
403 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
404 phoneNumber = detail_match.group(1)
405 phoneType = saxutils.unescape(detail_match.group(2))
406 yield (phoneType, phoneNumber)
408 def get_messages(self):
409 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
410 voicemailHtml = self._grab_html(voicemailPage)
411 voicemailJson = self._grab_json(voicemailPage)
412 parsedVoicemail = self._parse_voicemail(voicemailHtml)
413 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
414 decoratedVoicemails = self._decorate_voicemail(voicemails)
416 smsPage = self._get_page(self._XML_SMS_URL)
417 smsHtml = self._grab_html(smsPage)
418 smsJson = self._grab_json(smsPage)
419 parsedSms = self._parse_sms(smsHtml)
420 smss = self._merge_messages(parsedSms, smsJson)
421 decoratedSms = self._decorate_sms(smss)
423 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
426 def _grab_json(self, flatXml):
427 xmlTree = ElementTree.fromstring(flatXml)
428 jsonElement = xmlTree.getchildren()[0]
429 flatJson = jsonElement.text
430 jsonTree = parse_json(flatJson)
433 def _grab_html(self, flatXml):
434 xmlTree = ElementTree.fromstring(flatXml)
435 htmlElement = xmlTree.getchildren()[1]
436 flatHtml = htmlElement.text
439 def _grab_account_info(self, page):
440 tokenGroup = self._tokenRe.search(page)
441 if tokenGroup is None:
442 raise RuntimeError("Could not extract authentication token from GoogleVoice")
443 self._token = tokenGroup.group(1)
445 anGroup = self._accountNumRe.search(page)
446 if anGroup is not None:
447 self._accountNum = anGroup.group(1)
449 _moduleLogger.debug("Could not extract account number from GoogleVoice")
451 self._callbackNumbers = {}
452 for match in self._callbackRe.finditer(page):
453 callbackNumber = match.group(2)
454 callbackName = match.group(1)
455 self._callbackNumbers[callbackNumber] = callbackName
456 if len(self._callbackNumbers) == 0:
457 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
459 def _send_validation(self, number):
460 if not self.is_valid_syntax(number):
461 raise ValueError('Number is not valid: "%s"' % number)
462 elif not self.is_authed():
463 raise RuntimeError("Not Authenticated")
465 if len(number) == 11 and number[0] == 1:
466 # Strip leading 1 from 11 digit dialing
471 def _interpret_voicemail_regex(group):
472 quality, content, number = group.group(2), group.group(3), group.group(4)
473 if quality is not None and content is not None:
474 return quality, content
475 elif number is not None:
476 return "high", number
478 def _parse_voicemail(self, voicemailHtml):
479 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
480 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
481 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
482 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
483 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
484 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
485 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
486 locationGroup = self._voicemailLocationRegex.search(messageHtml)
487 location = locationGroup.group(1).strip() if locationGroup else ""
489 nameGroup = self._voicemailNameRegex.search(messageHtml)
490 name = nameGroup.group(1).strip() if nameGroup else ""
491 numberGroup = self._voicemailNumberRegex.search(messageHtml)
492 number = numberGroup.group(1).strip() if numberGroup else ""
493 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
494 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
495 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
496 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
498 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
500 self._interpret_voicemail_regex(group)
501 for group in messageGroups
502 ) if messageGroups else ()
505 "id": messageId.strip(),
506 "contactId": contactId,
509 "relTime": relativeTime,
510 "prettyNumber": prettyNumber,
512 "location": location,
513 "messageParts": messageParts,
517 def _decorate_voicemail(self, parsedVoicemails):
518 messagePartFormat = {
523 for voicemailData in parsedVoicemails:
525 messagePartFormat[quality] % part
526 for (quality, part) in voicemailData["messageParts"]
529 message = "No Transcription"
530 whoFrom = voicemailData["name"]
531 when = voicemailData["time"]
532 voicemailData["messageParts"] = ((whoFrom, message, when), )
535 def _parse_sms(self, smsHtml):
536 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
537 for messageId, messageHtml in itergroup(splitSms[1:], 2):
538 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
539 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
540 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
541 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
542 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
544 nameGroup = self._voicemailNameRegex.search(messageHtml)
545 name = nameGroup.group(1).strip() if nameGroup else ""
546 numberGroup = self._voicemailNumberRegex.search(messageHtml)
547 number = numberGroup.group(1).strip() if numberGroup else ""
548 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
549 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
550 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
551 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
553 fromGroups = self._smsFromRegex.finditer(messageHtml)
554 fromParts = (group.group(1).strip() for group in fromGroups)
555 textGroups = self._smsTextRegex.finditer(messageHtml)
556 textParts = (group.group(1).strip() for group in textGroups)
557 timeGroups = self._smsTimeRegex.finditer(messageHtml)
558 timeParts = (group.group(1).strip() for group in timeGroups)
560 messageParts = itertools.izip(fromParts, textParts, timeParts)
563 "id": messageId.strip(),
564 "contactId": contactId,
567 "relTime": relativeTime,
568 "prettyNumber": prettyNumber,
571 "messageParts": messageParts,
575 def _decorate_sms(self, parsedTexts):
579 def _merge_messages(parsedMessages, json):
580 for message in parsedMessages:
582 jsonItem = json["messages"][id]
583 message["isRead"] = jsonItem["isRead"]
584 message["isSpam"] = jsonItem["isSpam"]
585 message["isTrash"] = jsonItem["isTrash"]
586 message["isArchived"] = "inbox" not in jsonItem["labels"]
589 def _get_page(self, url, data = None, refererUrl = None):
591 if refererUrl is not None:
592 headers["Referer"] = refererUrl
594 encodedData = urllib.urlencode(data) if data is not None else None
597 page = self._browser.download(url, encodedData, None, headers)
598 except urllib2.URLError, e:
599 _moduleLogger.error("Translating error: %s" % str(e))
600 raise NetworkError("%s is not accesible" % url)
604 def _get_page_with_token(self, url, data = None, refererUrl = None):
607 data['_rnr_se'] = self._token
609 page = self._get_page(url, data, refererUrl)
613 def _parse_with_validation(self, page):
614 json = parse_json(page)
615 validate_response(json)
619 def itergroup(iterator, count, padValue = None):
621 Iterate in groups of 'count' values. If there
622 aren't enough values, the last result is padded with
625 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
629 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
633 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
638 >>> for val in itergroup("123456", 3):
642 >>> for val in itergroup("123456", 3):
643 ... print repr("".join(val))
647 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
648 nIterators = (paddedIterator, ) * count
649 return itertools.izip(*nIterators)
653 _TRUE_REGEX = re.compile("true")
654 _FALSE_REGEX = re.compile("false")
655 s = _TRUE_REGEX.sub("True", s)
656 s = _FALSE_REGEX.sub("False", s)
657 return eval(s, {}, {})
660 def _fake_parse_json(flattened):
661 return safe_eval(flattened)
664 def _actual_parse_json(flattened):
665 return simplejson.loads(flattened)
668 if simplejson is None:
669 parse_json = _fake_parse_json
671 parse_json = _actual_parse_json
674 def extract_payload(flatXml):
675 xmlTree = ElementTree.fromstring(flatXml)
677 jsonElement = xmlTree.getchildren()[0]
678 flatJson = jsonElement.text
679 jsonTree = parse_json(flatJson)
681 htmlElement = xmlTree.getchildren()[1]
682 flatHtml = htmlElement.text
684 return jsonTree, flatHtml
687 def validate_response(response):
689 Validates that the JSON response is A-OK
692 assert 'ok' in response and response['ok']
693 except AssertionError:
694 raise RuntimeError('There was a problem with GV: %s' % response)
697 def guess_phone_type(number):
698 if number.startswith("747") or number.startswith("1747"):
699 return GVoiceBackend.PHONE_TYPE_GIZMO
701 return GVoiceBackend.PHONE_TYPE_MOBILE
704 def set_sane_callback(backend):
706 Try to set a sane default callback number on these preferences
707 1) 1747 numbers ( Gizmo )
708 2) anything with gizmo in the name
709 3) anything with computer in the name
712 numbers = backend.get_callback_numbers()
714 priorityOrderedCriteria = [
722 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
723 for number, description in numbers.iteritems():
724 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
726 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
728 backend.set_callback_number(number)
732 def sort_messages(allMessages):
733 sortableAllMessages = [
734 (message["time"], message)
735 for message in allMessages
737 sortableAllMessages.sort(reverse=True)
740 for (exactTime, message) in sortableAllMessages
744 def decorate_recent(recentCallData):
746 @returns (personsName, phoneNumber, date, action)
748 contactId = recentCallData["contactId"]
749 if recentCallData["name"]:
750 header = recentCallData["name"]
751 elif recentCallData["prettyNumber"]:
752 header = recentCallData["prettyNumber"]
753 elif recentCallData["location"]:
754 header = recentCallData["location"]
758 number = recentCallData["number"]
759 relTime = recentCallData["relTime"]
760 action = recentCallData["action"]
761 return contactId, header, number, relTime, action
764 def decorate_message(messageData):
765 contactId = messageData["contactId"]
766 exactTime = messageData["time"]
767 if messageData["name"]:
768 header = messageData["name"]
769 elif messageData["prettyNumber"]:
770 header = messageData["prettyNumber"]
773 number = messageData["number"]
774 relativeTime = messageData["relTime"]
776 messageParts = list(messageData["messageParts"])
777 if len(messageParts) == 0:
778 messages = ("No Transcription", )
779 elif len(messageParts) == 1:
780 messages = (messageParts[0][1], )
783 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
784 for messagePart in messageParts
787 decoratedResults = contactId, header, number, relativeTime, messages
788 return decoratedResults
791 def test_backend(username, password):
792 backend = GVoiceBackend()
793 print "Authenticated: ", backend.is_authed()
794 if not backend.is_authed():
795 print "Login?: ", backend.login(username, password)
796 print "Authenticated: ", backend.is_authed()
797 print "Is Dnd: ", backend.is_dnd()
798 #print "Setting Dnd", backend.set_dnd(True)
799 #print "Is Dnd: ", backend.is_dnd()
800 #print "Setting Dnd", backend.set_dnd(False)
801 #print "Is Dnd: ", backend.is_dnd()
803 #print "Token: ", backend._token
804 #print "Account: ", backend.get_account_number()
805 #print "Callback: ", backend.get_callback_number()
806 #print "All Callback: ",
808 #pprint.pprint(backend.get_callback_numbers())
811 #for data in backend.get_recent():
812 # pprint.pprint(data)
813 #for data in sort_messages(backend.get_recent()):
814 # pprint.pprint(decorate_recent(data))
815 #pprint.pprint(list(backend.get_recent()))
818 #for contact in backend.get_contacts():
820 # pprint.pprint(list(backend.get_contact_details(contact[0])))
823 #for message in backend.get_messages():
824 # pprint.pprint(message)
825 #for message in sort_messages(backend.get_messages()):
826 # pprint.pprint(decorate_message(message))
831 def grab_debug_info(username, password):
832 cookieFile = os.path.join(".", "raw_cookies.txt")
834 os.remove(cookieFile)
838 backend = GVoiceBackend(cookieFile)
839 browser = backend._browser
842 ("forward", backend._forwardURL),
843 ("token", backend._tokenURL),
844 ("login", backend._loginURL),
845 ("isdnd", backend._isDndURL),
846 ("contacts", backend._contactsURL),
848 ("account", backend._XML_ACCOUNT_URL),
849 ("voicemail", backend._XML_VOICEMAIL_URL),
850 ("sms", backend._XML_SMS_URL),
852 ("recent", backend._XML_RECENT_URL),
853 ("placed", backend._XML_PLACED_URL),
854 ("recieved", backend._XML_RECEIVED_URL),
855 ("missed", backend._XML_MISSED_URL),
859 print "Grabbing pre-login pages"
860 for name, url in _TEST_WEBPAGES:
862 page = browser.download(url)
863 except StandardError, e:
866 print "\tWriting to file"
867 with open("not_loggedin_%s.txt" % name, "w") as f:
871 print "Attempting login"
872 galxToken = backend._get_token()
873 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
874 with open("loggingin.txt", "w") as f:
875 print "\tWriting to file"
876 f.write(loginSuccessOrFailurePage)
878 backend._grab_account_info(loginSuccessOrFailurePage)
880 # Retry in case the redirect failed
881 # luckily is_authed does everything we need for a retry
882 loggedIn = backend.is_authed(True)
887 print "Grabbing post-login pages"
888 for name, url in _TEST_WEBPAGES:
890 page = browser.download(url)
891 except StandardError, e:
894 print "\tWriting to file"
895 with open("loggedin_%s.txt" % name, "w") as f:
899 browser.cookies.save()
900 print "\tWriting cookies to file"
901 with open("cookies.txt", "w") as f:
903 "%s: %s\n" % (c.name, c.value)
904 for c in browser.cookies
908 if __name__ == "__main__":
910 logging.basicConfig(level=logging.DEBUG)
912 grab_debug_info(sys.argv[1], sys.argv[2])
914 test_backend(sys.argv[1], sys.argv[2])