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.dialer")
51 _TRUE_REGEX = re.compile("true")
52 _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 GVDialer(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 _forwardURL = "https://www.google.com/voice/mobile/phones"
129 def is_authed(self, force = False):
131 Attempts to detect a current session
132 @note Once logged in try not to reauth more than once a minute.
133 @returns If authenticated
135 if (time.time() - self._lastAuthed) < 120 and not force:
139 page = self._browser.download(self._forwardURL)
140 self._grab_account_info(page)
142 _moduleLogger.exception(str(e))
145 self._browser.cookies.save()
146 self._lastAuthed = time.time()
149 _tokenURL = "http://www.google.com/voice/m"
150 _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
151 _galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
153 def _get_token(self):
155 tokenPage = self._browser.download(self._tokenURL)
156 except urllib2.URLError, e:
157 _moduleLogger.exception("Translating error: %s" % str(e))
158 raise NetworkError("%s is not accesible" % self._loginURL)
159 galxTokens = self._galxRe.search(tokenPage)
160 if galxTokens is not None:
161 galxToken = galxTokens.group(1)
164 _moduleLogger.debug("Could not grab GALX token")
167 def _login(self, username, password, token):
168 loginPostData = urllib.urlencode({
171 'service': "grandcentral",
174 "PersistentCookie": "yes",
176 "continue": self._forwardURL,
180 loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
181 except urllib2.URLError, e:
182 _moduleLogger.exception("Translating error: %s" % str(e))
183 raise NetworkError("%s is not accesible" % self._loginURL)
184 return loginSuccessOrFailurePage
186 def login(self, username, password):
188 Attempt to login to GoogleVoice
189 @returns Whether login was successful or not
192 galxToken = self._get_token()
193 loginSuccessOrFailurePage = self._login(username, password, galxToken)
196 self._grab_account_info(loginSuccessOrFailurePage)
198 _moduleLogger.exception(str(e))
201 self._browser.cookies.save()
202 self._lastAuthed = time.time()
206 self._lastAuthed = 0.0
207 self._browser.cookies.clear()
208 self._browser.cookies.save()
210 _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
211 _clicktocallURL = "https://www.google.com/voice/m/sendcall"
213 def dial(self, number):
215 This is the main function responsible for initating the callback
217 number = self._send_validation(number)
219 clickToCallData = urllib.urlencode({
221 "phone": self._callbackNumber,
222 "_rnr_se": self._token,
225 'Referer' : 'https://google.com/voice/m/callsms',
227 callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
228 except urllib2.URLError, e:
229 _moduleLogger.exception("Translating error: %s" % str(e))
230 raise NetworkError("%s is not accesible" % self._clicktocallURL)
232 if self._gvDialingStrRe.search(callSuccessPage) is None:
233 raise RuntimeError("Google Voice returned an error")
237 _sendSmsURL = "https://www.google.com/voice/m/sendsms"
239 def send_sms(self, number, message):
240 number = self._send_validation(number)
242 smsData = urllib.urlencode({
245 "_rnr_se": self._token,
250 'Referer' : 'https://google.com/voice/m/sms',
252 smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
253 except urllib2.URLError, e:
254 _moduleLogger.exception("Translating error: %s" % str(e))
255 raise NetworkError("%s is not accesible" % self._sendSmsURL)
259 _validateRe = re.compile("^[0-9]{10,}$")
261 def is_valid_syntax(self, number):
263 @returns If This number be called ( syntax validation only )
265 return self._validateRe.match(number) is not None
267 def get_account_number(self):
269 @returns The GoogleVoice phone number
271 return self._accountNum
273 def get_callback_numbers(self):
275 @returns a dictionary mapping call back numbers to descriptions
276 @note These results are cached for 30 minutes.
278 if not self.is_authed():
280 return self._callbackNumbers
282 def set_callback_number(self, callbacknumber):
284 Set the number that GoogleVoice calls
285 @param callbacknumber should be a proper 10 digit number
287 self._callbackNumber = callbacknumber
290 def get_callback_number(self):
292 @returns Current callback number or None
294 return self._callbackNumber
296 _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
297 _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
298 _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
299 _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
301 def get_recent(self):
303 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
306 ("Received", self._receivedCallsURL),
307 ("Missed", self._missedCallsURL),
308 ("Placed", self._placedCallsURL),
311 flatXml = self._browser.download(url)
312 except urllib2.URLError, e:
313 _moduleLogger.exception("Translating error: %s" % str(e))
314 raise NetworkError("%s is not accesible" % url)
316 allRecentHtml = self._grab_html(flatXml)
317 allRecentData = self._parse_voicemail(allRecentHtml)
318 for recentCallData in allRecentData:
319 recentCallData["action"] = action
322 _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
323 _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
324 _contactsURL = "https://www.google.com/voice/mobile/contacts"
326 def get_contacts(self):
328 @returns Iterable of (contact id, contact name)
330 contactsPagesUrls = [self._contactsURL]
331 for contactsPageUrl in contactsPagesUrls:
333 contactsPage = self._browser.download(contactsPageUrl)
334 except urllib2.URLError, e:
335 _moduleLogger.exception("Translating error: %s" % str(e))
336 raise NetworkError("%s is not accesible" % contactsPageUrl)
337 for contact_match in self._contactsRe.finditer(contactsPage):
338 contactId = contact_match.group(1)
339 contactName = saxutils.unescape(contact_match.group(2))
340 contact = contactId, contactName
343 next_match = self._contactsNextRe.match(contactsPage)
344 if next_match is not None:
345 newContactsPageUrl = self._contactsURL + next_match.group(1)
346 contactsPagesUrls.append(newContactsPageUrl)
348 _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
349 _contactDetailURL = "https://www.google.com/voice/mobile/contact"
351 def get_contact_details(self, contactId):
353 @returns Iterable of (Phone Type, Phone Number)
356 detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
357 except urllib2.URLError, e:
358 _moduleLogger.exception("Translating error: %s" % str(e))
359 raise NetworkError("%s is not accesible" % self._contactDetailURL)
361 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
362 phoneNumber = detail_match.group(1)
363 phoneType = saxutils.unescape(detail_match.group(2))
364 yield (phoneType, phoneNumber)
366 _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
367 _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
369 def get_messages(self):
371 voicemailPage = self._browser.download(self._voicemailURL)
372 except urllib2.URLError, e:
373 _moduleLogger.exception("Translating error: %s" % str(e))
374 raise NetworkError("%s is not accesible" % self._voicemailURL)
375 voicemailHtml = self._grab_html(voicemailPage)
376 parsedVoicemail = self._parse_voicemail(voicemailHtml)
377 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
380 smsPage = self._browser.download(self._smsURL)
381 except urllib2.URLError, e:
382 _moduleLogger.exception("Translating error: %s" % str(e))
383 raise NetworkError("%s is not accesible" % self._smsURL)
384 smsHtml = self._grab_html(smsPage)
385 parsedSms = self._parse_sms(smsHtml)
386 decoratedSms = self._decorate_sms(parsedSms)
388 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
391 def clear_caches(self):
394 def get_addressbooks(self):
396 @returns Iterable of (Address Book Factory, Book Id, Book Name)
400 def open_addressbook(self, bookId):
404 def contact_source_short_name(contactId):
409 return "Google Voice"
411 def _grab_json(self, flatXml):
412 xmlTree = ElementTree.fromstring(flatXml)
413 jsonElement = xmlTree.getchildren()[0]
414 flatJson = jsonElement.text
415 jsonTree = parse_json(flatJson)
418 def _grab_html(self, flatXml):
419 xmlTree = ElementTree.fromstring(flatXml)
420 htmlElement = xmlTree.getchildren()[1]
421 flatHtml = htmlElement.text
424 _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
425 _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
426 _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
428 def _grab_account_info(self, page):
429 tokenGroup = self._tokenRe.search(page)
430 if tokenGroup is None:
431 raise RuntimeError("Could not extract authentication token from GoogleVoice")
432 self._token = tokenGroup.group(1)
434 anGroup = self._accountNumRe.search(page)
435 if anGroup is not None:
436 self._accountNum = anGroup.group(1)
438 _moduleLogger.debug("Could not extract account number from GoogleVoice")
440 self._callbackNumbers = {}
441 for match in self._callbackRe.finditer(page):
442 callbackNumber = match.group(2)
443 callbackName = match.group(1)
444 self._callbackNumbers[callbackNumber] = callbackName
445 if len(self._callbackNumbers) == 0:
446 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
448 def _send_validation(self, number):
449 if not self.is_valid_syntax(number):
450 raise ValueError('Number is not valid: "%s"' % number)
451 elif not self.is_authed():
452 raise RuntimeError("Not Authenticated")
454 if len(number) == 11 and number[0] == 1:
455 # Strip leading 1 from 11 digit dialing
459 _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
460 _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
461 _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
462 _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
463 _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
464 _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
465 _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
466 _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
467 #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
468 #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
469 _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
472 def _interpret_voicemail_regex(group):
473 quality, content, number = group.group(2), group.group(3), group.group(4)
474 if quality is not None and content is not None:
475 return quality, content
476 elif number is not None:
477 return "high", number
479 def _parse_voicemail(self, voicemailHtml):
480 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
481 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
482 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
483 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
484 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
485 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
486 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
487 locationGroup = self._voicemailLocationRegex.search(messageHtml)
488 location = locationGroup.group(1).strip() if locationGroup else ""
490 nameGroup = self._voicemailNameRegex.search(messageHtml)
491 name = nameGroup.group(1).strip() if nameGroup else ""
492 numberGroup = self._voicemailNumberRegex.search(messageHtml)
493 number = numberGroup.group(1).strip() if numberGroup else ""
494 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
495 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
496 contactIdGroup = self._messagesContactID.search(messageHtml)
497 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
499 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
501 self._interpret_voicemail_regex(group)
502 for group in messageGroups
503 ) if messageGroups else ()
506 "id": messageId.strip(),
507 "contactId": contactId,
510 "relTime": relativeTime,
511 "prettyNumber": prettyNumber,
513 "location": location,
514 "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 _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
536 _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
537 _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
539 def _parse_sms(self, smsHtml):
540 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
541 for messageId, messageHtml in itergroup(splitSms[1:], 2):
542 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
543 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
544 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
545 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
546 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
548 nameGroup = self._voicemailNameRegex.search(messageHtml)
549 name = nameGroup.group(1).strip() if nameGroup else ""
550 numberGroup = self._voicemailNumberRegex.search(messageHtml)
551 number = numberGroup.group(1).strip() if numberGroup else ""
552 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
553 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
554 contactIdGroup = self._messagesContactID.search(messageHtml)
555 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
557 fromGroups = self._smsFromRegex.finditer(messageHtml)
558 fromParts = (group.group(1).strip() for group in fromGroups)
559 textGroups = self._smsTextRegex.finditer(messageHtml)
560 textParts = (group.group(1).strip() for group in textGroups)
561 timeGroups = self._smsTimeRegex.finditer(messageHtml)
562 timeParts = (group.group(1).strip() for group in timeGroups)
564 messageParts = itertools.izip(fromParts, textParts, timeParts)
567 "id": messageId.strip(),
568 "contactId": contactId,
571 "relTime": relativeTime,
572 "prettyNumber": prettyNumber,
575 "messageParts": messageParts,
578 def _decorate_sms(self, parsedTexts):
582 def set_sane_callback(backend):
584 Try to set a sane default callback number on these preferences
585 1) 1747 numbers ( Gizmo )
586 2) anything with gizmo in the name
587 3) anything with computer in the name
590 numbers = backend.get_callback_numbers()
592 priorityOrderedCriteria = [
600 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
601 for number, description in numbers.iteritems():
602 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
604 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
606 backend.set_callback_number(number)
610 def sort_messages(allMessages):
611 sortableAllMessages = [
612 (message["time"], message)
613 for message in allMessages
615 sortableAllMessages.sort(reverse=True)
618 for (exactTime, message) in sortableAllMessages
622 def decorate_recent(recentCallData):
624 @returns (personsName, phoneNumber, date, action)
626 contactId = recentCallData["contactId"]
627 if recentCallData["name"]:
628 header = recentCallData["name"]
629 elif recentCallData["prettyNumber"]:
630 header = recentCallData["prettyNumber"]
631 elif recentCallData["location"]:
632 header = recentCallData["location"]
636 number = recentCallData["number"]
637 relTime = recentCallData["relTime"]
638 action = recentCallData["action"]
639 return contactId, header, number, relTime, action
642 def decorate_message(messageData):
643 contactId = messageData["contactId"]
644 exactTime = messageData["time"]
645 if messageData["name"]:
646 header = messageData["name"]
647 elif messageData["prettyNumber"]:
648 header = messageData["prettyNumber"]
651 number = messageData["number"]
652 relativeTime = messageData["relTime"]
654 messageParts = list(messageData["messageParts"])
655 if len(messageParts) == 0:
656 messages = ("No Transcription", )
657 elif len(messageParts) == 1:
658 messages = (messageParts[0][1], )
661 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
662 for messagePart in messageParts
665 decoratedResults = contactId, header, number, relativeTime, messages
666 return decoratedResults
669 def test_backend(username, password):
671 print "Authenticated: ", backend.is_authed()
672 if not backend.is_authed():
673 print "Login?: ", backend.login(username, password)
674 print "Authenticated: ", backend.is_authed()
676 print "Token: ", backend._token
677 #print "Account: ", backend.get_account_number()
678 #print "Callback: ", backend.get_callback_number()
679 #print "All Callback: ",
681 #pprint.pprint(backend.get_callback_numbers())
684 #for data in backend.get_recent():
685 # pprint.pprint(data)
686 #for data in sort_messages(backend.get_recent()):
687 # pprint.pprint(decorate_recent(data))
688 #pprint.pprint(list(backend.get_recent()))
691 #for contact in backend.get_contacts():
693 # pprint.pprint(list(backend.get_contact_details(contact[0])))
696 for message in backend.get_messages():
697 message["messageParts"] = list(message["messageParts"])
698 pprint.pprint(message)
699 #for message in sort_messages(backend.get_messages()):
700 # pprint.pprint(decorate_message(message))
706 ("forward", GVDialer._forwardURL),
707 ("token", GVDialer._tokenURL),
708 ("login", GVDialer._loginURL),
709 ("contacts", GVDialer._contactsURL),
711 ("voicemail", GVDialer._voicemailURL),
712 ("sms", GVDialer._smsURL),
714 ("recent", GVDialer._recentCallsURL),
715 ("placed", GVDialer._placedCallsURL),
716 ("recieved", GVDialer._receivedCallsURL),
717 ("missed", GVDialer._missedCallsURL),
721 def grab_debug_info(username, password):
722 cookieFile = os.path.join(".", "raw_cookies.txt")
724 os.remove(cookieFile)
728 backend = GVDialer(cookieFile)
729 browser = backend._browser
732 print "Grabbing pre-login pages"
733 for name, url in _TEST_WEBPAGES:
735 page = browser.download(url)
736 except StandardError, e:
739 print "\tWriting to file"
740 with open("not_loggedin_%s.txt" % name, "w") as f:
744 print "Attempting login"
745 galxToken = backend._get_token()
746 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
747 with open("loggingin.txt", "w") as f:
748 print "\tWriting to file"
749 f.write(loginSuccessOrFailurePage)
751 backend._grab_account_info(loginSuccessOrFailurePage)
753 # Retry in case the redirect failed
754 # luckily is_authed does everything we need for a retry
755 loggedIn = backend.is_authed(True)
760 print "Grabbing post-login pages"
761 for name, url in _TEST_WEBPAGES:
763 page = browser.download(url)
764 except StandardError, e:
767 print "\tWriting to file"
768 with open("loggedin_%s.txt" % name, "w") as f:
772 browser.cookies.save()
773 print "\tWriting cookies to file"
774 with open("cookies.txt", "w") as f:
776 "%s: %s\n" % (c.name, c.value)
777 for c in browser.cookies
781 if __name__ == "__main__":
783 logging.basicConfig(level=logging.DEBUG)
784 #test_backend(sys.argv[1], sys.argv[2])
785 grab_debug_info(sys.argv[1], sys.argv[2])