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
50 _moduleLogger = logging.getLogger("gvoice.backend")
54 _TRUE_REGEX = re.compile("true")
55 _FALSE_REGEX = re.compile("false")
56 s = _TRUE_REGEX.sub("True", s)
57 s = _FALSE_REGEX.sub("False", s)
58 return eval(s, {}, {})
61 if simplejson is None:
62 def parse_json(flattened):
63 return safe_eval(flattened)
65 def parse_json(flattened):
66 return simplejson.loads(flattened)
69 def itergroup(iterator, count, padValue = None):
71 Iterate in groups of 'count' values. If there
72 aren't enough values, the last result is padded with
75 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
79 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
83 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
88 >>> for val in itergroup("123456", 3):
92 >>> for val in itergroup("123456", 3):
93 ... print repr("".join(val))
97 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
98 nIterators = (paddedIterator, ) * count
99 return itertools.izip(*nIterators)
102 class NetworkError(RuntimeError):
106 class GVoiceBackend(object):
108 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
109 the functions include login, setting up a callback number, and initalting a callback
112 def __init__(self, cookieFile = None):
113 # Important items in this function are the setup of the browser emulation and cookie file
114 self._browser = browser_emu.MozillaEmulator(1)
115 if cookieFile is None:
116 cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
117 self._browser.cookies.filename = cookieFile
118 if os.path.isfile(cookieFile):
119 self._browser.cookies.load()
122 self._accountNum = ""
123 self._lastAuthed = 0.0
124 self._callbackNumber = ""
125 self._callbackNumbers = {}
127 # Suprisingly, moving all of these from class to self sped up startup time
129 self._validateRe = re.compile("^[0-9]{10,}$")
131 self._forwardURL = "https://www.google.com/voice/mobile/phones"
132 self._tokenURL = "http://www.google.com/voice/m"
133 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
134 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
135 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
136 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
137 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
139 self._gvDialingStrRe = re.compile("This may take a few seconds", re.M)
140 self._clicktocallURL = "https://www.google.com/voice/m/sendcall"
141 self._sendSmsURL = "https://www.google.com/voice/m/sendsms"
143 self._recentCallsURL = "https://www.google.com/voice/inbox/recent/"
144 self._placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
145 self._receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
146 self._missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
148 self._contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
149 self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
150 self._contactsURL = "https://www.google.com/voice/mobile/contacts"
151 self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
152 self._contactDetailURL = "https://www.google.com/voice/mobile/contact"
154 self._voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
155 self._smsURL = "https://www.google.com/voice/inbox/recent/sms/"
156 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
157 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
158 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
159 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
160 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
161 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
162 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
163 self._messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
164 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
165 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
166 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
167 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
169 def is_authed(self, force = False):
171 Attempts to detect a current session
172 @note Once logged in try not to reauth more than once a minute.
173 @returns If authenticated
175 if (time.time() - self._lastAuthed) < 120 and not force:
179 page = self._browser.download(self._forwardURL)
180 self._grab_account_info(page)
182 _moduleLogger.exception(str(e))
185 self._browser.cookies.save()
186 self._lastAuthed = time.time()
189 def _get_token(self):
191 tokenPage = self._browser.download(self._tokenURL)
192 except urllib2.URLError, e:
193 _moduleLogger.exception("Translating error: %s" % str(e))
194 raise NetworkError("%s is not accesible" % self._loginURL)
195 galxTokens = self._galxRe.search(tokenPage)
196 if galxTokens is not None:
197 galxToken = galxTokens.group(1)
200 _moduleLogger.debug("Could not grab GALX token")
203 def _login(self, username, password, token):
204 loginPostData = urllib.urlencode({
207 'service': "grandcentral",
210 "PersistentCookie": "yes",
212 "continue": self._forwardURL,
216 loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
217 except urllib2.URLError, e:
218 _moduleLogger.exception("Translating error: %s" % str(e))
219 raise NetworkError("%s is not accesible" % self._loginURL)
220 return loginSuccessOrFailurePage
222 def login(self, username, password):
224 Attempt to login to GoogleVoice
225 @returns Whether login was successful or not
228 galxToken = self._get_token()
229 loginSuccessOrFailurePage = self._login(username, password, galxToken)
232 self._grab_account_info(loginSuccessOrFailurePage)
234 # Retry in case the redirect failed
235 # luckily is_authed does everything we need for a retry
236 loggedIn = self.is_authed(True)
238 _moduleLogger.exception(str(e))
240 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
242 self._browser.cookies.save()
243 self._lastAuthed = time.time()
247 self._lastAuthed = 0.0
248 self._browser.cookies.clear()
249 self._browser.cookies.save()
251 def dial(self, number):
253 This is the main function responsible for initating the callback
255 number = self._send_validation(number)
257 clickToCallData = urllib.urlencode({
259 "phone": self._callbackNumber,
260 "_rnr_se": self._token,
263 'Referer' : 'https://google.com/voice/m/callsms',
265 callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
266 except urllib2.URLError, e:
267 _moduleLogger.exception("Translating error: %s" % str(e))
268 raise NetworkError("%s is not accesible" % self._clicktocallURL)
270 if self._gvDialingStrRe.search(callSuccessPage) is None:
271 raise RuntimeError("Google Voice returned an error")
275 def send_sms(self, number, message):
276 number = self._send_validation(number)
278 smsData = urllib.urlencode({
281 "_rnr_se": self._token,
286 'Referer' : 'https://google.com/voice/m/sms',
288 smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
289 except urllib2.URLError, e:
290 _moduleLogger.exception("Translating error: %s" % str(e))
291 raise NetworkError("%s is not accesible" % self._sendSmsURL)
295 def is_valid_syntax(self, number):
297 @returns If This number be called ( syntax validation only )
299 return self._validateRe.match(number) is not None
301 def get_account_number(self):
303 @returns The GoogleVoice phone number
305 return self._accountNum
307 def get_callback_numbers(self):
309 @returns a dictionary mapping call back numbers to descriptions
310 @note These results are cached for 30 minutes.
312 if not self.is_authed():
314 return self._callbackNumbers
316 def set_callback_number(self, callbacknumber):
318 Set the number that GoogleVoice calls
319 @param callbacknumber should be a proper 10 digit number
321 self._callbackNumber = callbacknumber
324 def get_callback_number(self):
326 @returns Current callback number or None
328 return self._callbackNumber
330 def get_recent(self):
332 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
335 ("Received", self._receivedCallsURL),
336 ("Missed", self._missedCallsURL),
337 ("Placed", self._placedCallsURL),
340 flatXml = self._browser.download(url)
341 except urllib2.URLError, e:
342 _moduleLogger.exception("Translating error: %s" % str(e))
343 raise NetworkError("%s is not accesible" % url)
345 allRecentHtml = self._grab_html(flatXml)
346 allRecentData = self._parse_voicemail(allRecentHtml)
347 for recentCallData in allRecentData:
348 recentCallData["action"] = action
351 def get_contacts(self):
353 @returns Iterable of (contact id, contact name)
355 contactsPagesUrls = [self._contactsURL]
356 for contactsPageUrl in contactsPagesUrls:
358 contactsPage = self._browser.download(contactsPageUrl)
359 except urllib2.URLError, e:
360 _moduleLogger.exception("Translating error: %s" % str(e))
361 raise NetworkError("%s is not accesible" % contactsPageUrl)
362 for contact_match in self._contactsRe.finditer(contactsPage):
363 contactId = contact_match.group(1)
364 contactName = saxutils.unescape(contact_match.group(2))
365 contact = contactId, contactName
368 next_match = self._contactsNextRe.match(contactsPage)
369 if next_match is not None:
370 newContactsPageUrl = self._contactsURL + next_match.group(1)
371 contactsPagesUrls.append(newContactsPageUrl)
373 def get_contact_details(self, contactId):
375 @returns Iterable of (Phone Type, Phone Number)
378 detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
379 except urllib2.URLError, e:
380 _moduleLogger.exception("Translating error: %s" % str(e))
381 raise NetworkError("%s is not accesible" % self._contactDetailURL)
383 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
384 phoneNumber = detail_match.group(1)
385 phoneType = saxutils.unescape(detail_match.group(2))
386 yield (phoneType, phoneNumber)
388 def get_messages(self):
390 voicemailPage = self._browser.download(self._voicemailURL)
391 except urllib2.URLError, e:
392 _moduleLogger.exception("Translating error: %s" % str(e))
393 raise NetworkError("%s is not accesible" % self._voicemailURL)
394 voicemailHtml = self._grab_html(voicemailPage)
395 voicemailJson = self._grab_json(voicemailPage)
396 parsedVoicemail = self._parse_voicemail(voicemailHtml)
397 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
398 decoratedVoicemails = self._decorate_voicemail(voicemails)
401 smsPage = self._browser.download(self._smsURL)
402 except urllib2.URLError, e:
403 _moduleLogger.exception("Translating error: %s" % str(e))
404 raise NetworkError("%s is not accesible" % self._smsURL)
405 smsHtml = self._grab_html(smsPage)
406 smsJson = self._grab_json(smsPage)
407 parsedSms = self._parse_sms(smsHtml)
408 smss = self._merge_messages(parsedSms, smsJson)
409 decoratedSms = self._decorate_sms(smss)
411 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
414 def _grab_json(self, flatXml):
415 xmlTree = ElementTree.fromstring(flatXml)
416 jsonElement = xmlTree.getchildren()[0]
417 flatJson = jsonElement.text
418 jsonTree = parse_json(flatJson)
421 def _grab_html(self, flatXml):
422 xmlTree = ElementTree.fromstring(flatXml)
423 htmlElement = xmlTree.getchildren()[1]
424 flatHtml = htmlElement.text
427 def _grab_account_info(self, page):
428 tokenGroup = self._tokenRe.search(page)
429 if tokenGroup is None:
430 raise RuntimeError("Could not extract authentication token from GoogleVoice")
431 self._token = tokenGroup.group(1)
433 anGroup = self._accountNumRe.search(page)
434 if anGroup is not None:
435 self._accountNum = anGroup.group(1)
437 _moduleLogger.debug("Could not extract account number from GoogleVoice")
439 self._callbackNumbers = {}
440 for match in self._callbackRe.finditer(page):
441 callbackNumber = match.group(2)
442 callbackName = match.group(1)
443 self._callbackNumbers[callbackNumber] = callbackName
444 if len(self._callbackNumbers) == 0:
445 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
447 def _send_validation(self, number):
448 if not self.is_valid_syntax(number):
449 raise ValueError('Number is not valid: "%s"' % number)
450 elif not self.is_authed():
451 raise RuntimeError("Not Authenticated")
453 if len(number) == 11 and number[0] == 1:
454 # Strip leading 1 from 11 digit dialing
459 def _interpret_voicemail_regex(group):
460 quality, content, number = group.group(2), group.group(3), group.group(4)
461 if quality is not None and content is not None:
462 return quality, content
463 elif number is not None:
464 return "high", number
466 def _parse_voicemail(self, voicemailHtml):
467 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
468 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
469 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
470 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
471 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
472 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
473 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
474 locationGroup = self._voicemailLocationRegex.search(messageHtml)
475 location = locationGroup.group(1).strip() if locationGroup else ""
477 nameGroup = self._voicemailNameRegex.search(messageHtml)
478 name = nameGroup.group(1).strip() if nameGroup else ""
479 numberGroup = self._voicemailNumberRegex.search(messageHtml)
480 number = numberGroup.group(1).strip() if numberGroup else ""
481 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
482 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
483 contactIdGroup = self._messagesContactID.search(messageHtml)
484 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
486 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
488 self._interpret_voicemail_regex(group)
489 for group in messageGroups
490 ) if messageGroups else ()
493 "id": messageId.strip(),
494 "contactId": contactId,
497 "relTime": relativeTime,
498 "prettyNumber": prettyNumber,
500 "location": location,
501 "messageParts": messageParts,
505 def _decorate_voicemail(self, parsedVoicemails):
506 messagePartFormat = {
511 for voicemailData in parsedVoicemails:
513 messagePartFormat[quality] % part
514 for (quality, part) in voicemailData["messageParts"]
517 message = "No Transcription"
518 whoFrom = voicemailData["name"]
519 when = voicemailData["time"]
520 voicemailData["messageParts"] = ((whoFrom, message, when), )
523 def _parse_sms(self, smsHtml):
524 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
525 for messageId, messageHtml in itergroup(splitSms[1:], 2):
526 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
527 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
528 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
529 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
530 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
532 nameGroup = self._voicemailNameRegex.search(messageHtml)
533 name = nameGroup.group(1).strip() if nameGroup else ""
534 numberGroup = self._voicemailNumberRegex.search(messageHtml)
535 number = numberGroup.group(1).strip() if numberGroup else ""
536 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
537 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
538 contactIdGroup = self._messagesContactID.search(messageHtml)
539 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
541 fromGroups = self._smsFromRegex.finditer(messageHtml)
542 fromParts = (group.group(1).strip() for group in fromGroups)
543 textGroups = self._smsTextRegex.finditer(messageHtml)
544 textParts = (group.group(1).strip() for group in textGroups)
545 timeGroups = self._smsTimeRegex.finditer(messageHtml)
546 timeParts = (group.group(1).strip() for group in timeGroups)
548 messageParts = itertools.izip(fromParts, textParts, timeParts)
551 "id": messageId.strip(),
552 "contactId": contactId,
555 "relTime": relativeTime,
556 "prettyNumber": prettyNumber,
559 "messageParts": messageParts,
563 def _decorate_sms(self, parsedTexts):
567 def _merge_messages(parsedMessages, json):
568 for message in parsedMessages:
570 jsonItem = json["messages"][id]
571 message["isRead"] = jsonItem["isRead"]
572 message["isSpam"] = jsonItem["isSpam"]
573 message["isTrash"] = jsonItem["isTrash"]
574 message["isArchived"] = "inbox" not in jsonItem["labels"]
578 def set_sane_callback(backend):
580 Try to set a sane default callback number on these preferences
581 1) 1747 numbers ( Gizmo )
582 2) anything with gizmo in the name
583 3) anything with computer in the name
586 numbers = backend.get_callback_numbers()
588 priorityOrderedCriteria = [
596 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
597 for number, description in numbers.iteritems():
598 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
600 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
602 backend.set_callback_number(number)
606 def sort_messages(allMessages):
607 sortableAllMessages = [
608 (message["time"], message)
609 for message in allMessages
611 sortableAllMessages.sort(reverse=True)
614 for (exactTime, message) in sortableAllMessages
618 def decorate_recent(recentCallData):
620 @returns (personsName, phoneNumber, date, action)
622 contactId = recentCallData["contactId"]
623 if recentCallData["name"]:
624 header = recentCallData["name"]
625 elif recentCallData["prettyNumber"]:
626 header = recentCallData["prettyNumber"]
627 elif recentCallData["location"]:
628 header = recentCallData["location"]
632 number = recentCallData["number"]
633 relTime = recentCallData["relTime"]
634 action = recentCallData["action"]
635 return contactId, header, number, relTime, action
638 def decorate_message(messageData):
639 contactId = messageData["contactId"]
640 exactTime = messageData["time"]
641 if messageData["name"]:
642 header = messageData["name"]
643 elif messageData["prettyNumber"]:
644 header = messageData["prettyNumber"]
647 number = messageData["number"]
648 relativeTime = messageData["relTime"]
650 messageParts = list(messageData["messageParts"])
651 if len(messageParts) == 0:
652 messages = ("No Transcription", )
653 elif len(messageParts) == 1:
654 messages = (messageParts[0][1], )
657 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
658 for messagePart in messageParts
661 decoratedResults = contactId, header, number, relativeTime, messages
662 return decoratedResults
665 def test_backend(username, password):
666 backend = GVoiceBackend()
667 print "Authenticated: ", backend.is_authed()
668 if not backend.is_authed():
669 print "Login?: ", backend.login(username, password)
670 print "Authenticated: ", backend.is_authed()
672 #print "Token: ", backend._token
673 #print "Account: ", backend.get_account_number()
674 #print "Callback: ", backend.get_callback_number()
675 #print "All Callback: ",
677 #pprint.pprint(backend.get_callback_numbers())
680 #for data in backend.get_recent():
681 # pprint.pprint(data)
682 #for data in sort_messages(backend.get_recent()):
683 # pprint.pprint(decorate_recent(data))
684 #pprint.pprint(list(backend.get_recent()))
687 #for contact in backend.get_contacts():
689 # pprint.pprint(list(backend.get_contact_details(contact[0])))
692 #for message in backend.get_messages():
693 # pprint.pprint(message)
694 #for message in sort_messages(backend.get_messages()):
695 # pprint.pprint(decorate_message(message))
700 def grab_debug_info(username, password):
701 cookieFile = os.path.join(".", "raw_cookies.txt")
703 os.remove(cookieFile)
707 backend = GVoiceBackend(cookieFile)
708 browser = backend._browser
711 ("forward", backend._forwardURL),
712 ("token", backend._tokenURL),
713 ("login", backend._loginURL),
714 ("contacts", backend._contactsURL),
716 ("voicemail", backend._voicemailURL),
717 ("sms", backend._smsURL),
719 ("recent", backend._recentCallsURL),
720 ("placed", backend._placedCallsURL),
721 ("recieved", backend._receivedCallsURL),
722 ("missed", backend._missedCallsURL),
726 print "Grabbing pre-login pages"
727 for name, url in _TEST_WEBPAGES:
729 page = browser.download(url)
730 except StandardError, e:
733 print "\tWriting to file"
734 with open("not_loggedin_%s.txt" % name, "w") as f:
738 print "Attempting login"
739 galxToken = backend._get_token()
740 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
741 with open("loggingin.txt", "w") as f:
742 print "\tWriting to file"
743 f.write(loginSuccessOrFailurePage)
745 backend._grab_account_info(loginSuccessOrFailurePage)
747 # Retry in case the redirect failed
748 # luckily is_authed does everything we need for a retry
749 loggedIn = backend.is_authed(True)
754 print "Grabbing post-login pages"
755 for name, url in _TEST_WEBPAGES:
757 page = browser.download(url)
758 except StandardError, e:
761 print "\tWriting to file"
762 with open("loggedin_%s.txt" % name, "w") as f:
766 browser.cookies.save()
767 print "\tWriting cookies to file"
768 with open("cookies.txt", "w") as f:
770 "%s: %s\n" % (c.name, c.value)
771 for c in browser.cookies
775 if __name__ == "__main__":
777 logging.basicConfig(level=logging.DEBUG)
778 #test_backend(sys.argv[1], sys.argv[2])
779 grab_debug_info(sys.argv[1], sys.argv[2])