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
39 from xml.sax import saxutils
41 from xml.etree import ElementTree
51 _moduleLogger = logging.getLogger("gvoice.dialer")
55 _TRUE_REGEX = re.compile("true")
56 _FALSE_REGEX = re.compile("false")
57 s = _TRUE_REGEX.sub("True", s)
58 s = _FALSE_REGEX.sub("False", s)
59 return eval(s, {}, {})
62 if simplejson is None:
63 def parse_json(flattened):
64 return safe_eval(flattened)
66 def parse_json(flattened):
67 return simplejson.loads(flattened)
70 def itergroup(iterator, count, padValue = None):
72 Iterate in groups of 'count' values. If there
73 aren't enough values, the last result is padded with
76 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
80 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
84 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
89 >>> for val in itergroup("123456", 3):
93 >>> for val in itergroup("123456", 3):
94 ... print repr("".join(val))
98 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
99 nIterators = (paddedIterator, ) * count
100 return itertools.izip(*nIterators)
103 class NetworkError(RuntimeError):
107 class GVDialer(object):
109 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
110 the functions include login, setting up a callback number, and initalting a callback
113 def __init__(self, cookieFile = None):
114 # Important items in this function are the setup of the browser emulation and cookie file
115 self._browser = browser_emu.MozillaEmulator(1)
116 if cookieFile is None:
117 cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
118 self._browser.cookies.filename = cookieFile
120 self._browser.cookies.load()
121 self._loadedFromCookies = True
122 except cookielib.LoadError:
123 _moduleLogger.exception("Bad cookie file")
124 self._loadedFromCookies = False
126 _moduleLogger.exception("No cookie file")
127 self._loadedFromCookies = False
129 self._loadedFromCookies = False
132 self._accountNum = ""
133 self._lastAuthed = 0.0
134 self._callbackNumber = ""
135 self._callbackNumbers = {}
137 # Suprisingly, moving all of these from class to self sped up startup time
139 self._validateRe = re.compile("^[0-9]{10,}$")
141 self._forwardURL = "https://www.google.com/voice/mobile/phones"
142 self._tokenURL = "http://www.google.com/voice/m"
143 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
144 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
145 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
146 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
147 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
149 self._gvDialingStrRe = re.compile("This may take a few seconds", re.M)
150 self._clicktocallURL = "https://www.google.com/voice/m/sendcall"
151 self._sendSmsURL = "https://www.google.com/voice/m/sendsms"
153 self._recentCallsURL = "https://www.google.com/voice/inbox/recent/"
154 self._placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
155 self._receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
156 self._missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
158 self._contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
159 self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
160 self._contactsURL = "https://www.google.com/voice/mobile/contacts"
161 self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
162 self._contactDetailURL = "https://www.google.com/voice/mobile/contact"
164 self._voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
165 self._smsURL = "https://www.google.com/voice/inbox/recent/sms/"
166 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
167 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
168 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
169 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
170 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
171 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
172 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
173 self._messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
174 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
175 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
176 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
177 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
179 def is_quick_login_possible(self):
181 @returns True then is_authed might be enough to login, else full login is required
183 return self._loadedFromCookies or 0.0 < self._lastAuthed
185 def is_authed(self, force = False):
187 Attempts to detect a current session
188 @note Once logged in try not to reauth more than once a minute.
189 @returns If authenticated
191 if (time.time() - self._lastAuthed) < 120 and not force:
195 page = self._browser.download(self._forwardURL)
196 self._grab_account_info(page)
198 _moduleLogger.exception(str(e))
201 self._browser.cookies.save()
202 self._lastAuthed = time.time()
205 def _get_token(self):
207 tokenPage = self._browser.download(self._tokenURL)
208 except urllib2.URLError, e:
209 _moduleLogger.exception("Translating error: %s" % str(e))
210 raise NetworkError("%s is not accesible" % self._loginURL)
211 galxTokens = self._galxRe.search(tokenPage)
212 if galxTokens is not None:
213 galxToken = galxTokens.group(1)
216 _moduleLogger.debug("Could not grab GALX token")
219 def _login(self, username, password, token):
220 loginPostData = urllib.urlencode({
223 'service': "grandcentral",
226 "PersistentCookie": "yes",
228 "continue": self._forwardURL,
232 loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
233 except urllib2.URLError, e:
234 _moduleLogger.exception("Translating error: %s" % str(e))
235 raise NetworkError("%s is not accesible" % self._loginURL)
236 return loginSuccessOrFailurePage
238 def login(self, username, password):
240 Attempt to login to GoogleVoice
241 @returns Whether login was successful or not
244 galxToken = self._get_token()
245 loginSuccessOrFailurePage = self._login(username, password, galxToken)
248 self._grab_account_info(loginSuccessOrFailurePage)
250 # Retry in case the redirect failed
251 # luckily is_authed does everything we need for a retry
252 loggedIn = self.is_authed(True)
254 _moduleLogger.exception(str(e))
256 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
258 self._browser.cookies.save()
259 self._lastAuthed = time.time()
263 self._lastAuthed = 0.0
264 self._browser.cookies.clear()
265 self._browser.cookies.save()
267 def dial(self, number):
269 This is the main function responsible for initating the callback
271 number = self._send_validation(number)
273 clickToCallData = urllib.urlencode({
275 "phone": self._callbackNumber,
276 "_rnr_se": self._token,
279 'Referer' : 'https://google.com/voice/m/callsms',
281 callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
282 except urllib2.URLError, e:
283 _moduleLogger.exception("Translating error: %s" % str(e))
284 raise NetworkError("%s is not accesible" % self._clicktocallURL)
286 if self._gvDialingStrRe.search(callSuccessPage) is None:
287 raise RuntimeError("Google Voice returned an error")
291 def send_sms(self, number, message):
292 number = self._send_validation(number)
294 smsData = urllib.urlencode({
297 "_rnr_se": self._token,
302 'Referer' : 'https://google.com/voice/m/sms',
304 smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
305 except urllib2.URLError, e:
306 _moduleLogger.exception("Translating error: %s" % str(e))
307 raise NetworkError("%s is not accesible" % self._sendSmsURL)
311 def is_valid_syntax(self, number):
313 @returns If This number be called ( syntax validation only )
315 return self._validateRe.match(number) is not None
317 def get_account_number(self):
319 @returns The GoogleVoice phone number
321 return self._accountNum
323 def get_callback_numbers(self):
325 @returns a dictionary mapping call back numbers to descriptions
326 @note These results are cached for 30 minutes.
328 if not self.is_authed():
330 return self._callbackNumbers
332 def set_callback_number(self, callbacknumber):
334 Set the number that GoogleVoice calls
335 @param callbacknumber should be a proper 10 digit number
337 self._callbackNumber = callbacknumber
340 def get_callback_number(self):
342 @returns Current callback number or None
344 return self._callbackNumber
346 def get_recent(self):
348 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
351 ("Received", self._receivedCallsURL),
352 ("Missed", self._missedCallsURL),
353 ("Placed", self._placedCallsURL),
356 flatXml = self._browser.download(url)
357 except urllib2.URLError, e:
358 _moduleLogger.exception("Translating error: %s" % str(e))
359 raise NetworkError("%s is not accesible" % url)
361 allRecentHtml = self._grab_html(flatXml)
362 allRecentData = self._parse_voicemail(allRecentHtml)
363 for recentCallData in allRecentData:
364 recentCallData["action"] = action
367 def get_contacts(self):
369 @returns Iterable of (contact id, contact name)
371 contactsPagesUrls = [self._contactsURL]
372 for contactsPageUrl in contactsPagesUrls:
374 contactsPage = self._browser.download(contactsPageUrl)
375 except urllib2.URLError, e:
376 _moduleLogger.exception("Translating error: %s" % str(e))
377 raise NetworkError("%s is not accesible" % contactsPageUrl)
378 for contact_match in self._contactsRe.finditer(contactsPage):
379 contactId = contact_match.group(1)
380 contactName = saxutils.unescape(contact_match.group(2))
381 contact = contactId, contactName
384 next_match = self._contactsNextRe.match(contactsPage)
385 if next_match is not None:
386 newContactsPageUrl = self._contactsURL + next_match.group(1)
387 contactsPagesUrls.append(newContactsPageUrl)
389 def get_contact_details(self, contactId):
391 @returns Iterable of (Phone Type, Phone Number)
394 detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
395 except urllib2.URLError, e:
396 _moduleLogger.exception("Translating error: %s" % str(e))
397 raise NetworkError("%s is not accesible" % self._contactDetailURL)
399 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
400 phoneNumber = detail_match.group(1)
401 phoneType = saxutils.unescape(detail_match.group(2))
402 yield (phoneType, phoneNumber)
404 def get_messages(self):
406 voicemailPage = self._browser.download(self._voicemailURL)
407 except urllib2.URLError, e:
408 _moduleLogger.exception("Translating error: %s" % str(e))
409 raise NetworkError("%s is not accesible" % self._voicemailURL)
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)
417 smsPage = self._browser.download(self._smsURL)
418 except urllib2.URLError, e:
419 _moduleLogger.exception("Translating error: %s" % str(e))
420 raise NetworkError("%s is not accesible" % self._smsURL)
421 smsHtml = self._grab_html(smsPage)
422 smsJson = self._grab_json(smsPage)
423 parsedSms = self._parse_sms(smsHtml)
424 smss = self._merge_messages(parsedSms, smsJson)
425 decoratedSms = self._decorate_sms(smss)
427 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
430 def clear_caches(self):
433 def get_addressbooks(self):
435 @returns Iterable of (Address Book Factory, Book Id, Book Name)
439 def open_addressbook(self, bookId):
443 def contact_source_short_name(contactId):
448 return "Google Voice"
450 def _grab_json(self, flatXml):
451 xmlTree = ElementTree.fromstring(flatXml)
452 jsonElement = xmlTree.getchildren()[0]
453 flatJson = jsonElement.text
454 jsonTree = parse_json(flatJson)
457 def _grab_html(self, flatXml):
458 xmlTree = ElementTree.fromstring(flatXml)
459 htmlElement = xmlTree.getchildren()[1]
460 flatHtml = htmlElement.text
463 def _grab_account_info(self, page):
464 tokenGroup = self._tokenRe.search(page)
465 if tokenGroup is None:
466 raise RuntimeError("Could not extract authentication token from GoogleVoice")
467 self._token = tokenGroup.group(1)
469 anGroup = self._accountNumRe.search(page)
470 if anGroup is not None:
471 self._accountNum = anGroup.group(1)
473 _moduleLogger.debug("Could not extract account number from GoogleVoice")
475 self._callbackNumbers = {}
476 for match in self._callbackRe.finditer(page):
477 callbackNumber = match.group(2)
478 callbackName = match.group(1)
479 self._callbackNumbers[callbackNumber] = callbackName
480 if len(self._callbackNumbers) == 0:
481 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
483 def _send_validation(self, number):
484 if not self.is_valid_syntax(number):
485 raise ValueError('Number is not valid: "%s"' % number)
486 elif not self.is_authed():
487 raise RuntimeError("Not Authenticated")
489 if len(number) == 11 and number[0] == 1:
490 # Strip leading 1 from 11 digit dialing
495 def _interpret_voicemail_regex(group):
496 quality, content, number = group.group(2), group.group(3), group.group(4)
497 if quality is not None and content is not None:
498 return quality, content
499 elif number is not None:
500 return "high", number
502 def _parse_voicemail(self, voicemailHtml):
503 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
504 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
505 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
506 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
507 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
508 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
509 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
510 locationGroup = self._voicemailLocationRegex.search(messageHtml)
511 location = locationGroup.group(1).strip() if locationGroup else ""
513 nameGroup = self._voicemailNameRegex.search(messageHtml)
514 name = nameGroup.group(1).strip() if nameGroup else ""
515 numberGroup = self._voicemailNumberRegex.search(messageHtml)
516 number = numberGroup.group(1).strip() if numberGroup else ""
517 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
518 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
519 contactIdGroup = self._messagesContactID.search(messageHtml)
520 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
522 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
524 self._interpret_voicemail_regex(group)
525 for group in messageGroups
526 ) if messageGroups else ()
529 "id": messageId.strip(),
530 "contactId": contactId,
533 "relTime": relativeTime,
534 "prettyNumber": prettyNumber,
536 "location": location,
537 "messageParts": messageParts,
541 def _decorate_voicemail(self, parsedVoicemails):
542 messagePartFormat = {
547 for voicemailData in parsedVoicemails:
549 messagePartFormat[quality] % part
550 for (quality, part) in voicemailData["messageParts"]
553 message = "No Transcription"
554 whoFrom = voicemailData["name"]
555 when = voicemailData["time"]
556 voicemailData["messageParts"] = ((whoFrom, message, when), )
559 def _parse_sms(self, smsHtml):
560 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
561 for messageId, messageHtml in itergroup(splitSms[1:], 2):
562 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
563 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
564 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
565 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
566 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
568 nameGroup = self._voicemailNameRegex.search(messageHtml)
569 name = nameGroup.group(1).strip() if nameGroup else ""
570 numberGroup = self._voicemailNumberRegex.search(messageHtml)
571 number = numberGroup.group(1).strip() if numberGroup else ""
572 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
573 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
574 contactIdGroup = self._messagesContactID.search(messageHtml)
575 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
577 fromGroups = self._smsFromRegex.finditer(messageHtml)
578 fromParts = (group.group(1).strip() for group in fromGroups)
579 textGroups = self._smsTextRegex.finditer(messageHtml)
580 textParts = (group.group(1).strip() for group in textGroups)
581 timeGroups = self._smsTimeRegex.finditer(messageHtml)
582 timeParts = (group.group(1).strip() for group in timeGroups)
584 messageParts = itertools.izip(fromParts, textParts, timeParts)
587 "id": messageId.strip(),
588 "contactId": contactId,
591 "relTime": relativeTime,
592 "prettyNumber": prettyNumber,
595 "messageParts": messageParts,
599 def _decorate_sms(self, parsedTexts):
603 def _merge_messages(parsedMessages, json):
604 for message in parsedMessages:
606 jsonItem = json["messages"][id]
607 message["isRead"] = jsonItem["isRead"]
608 message["isSpam"] = jsonItem["isSpam"]
609 message["isTrash"] = jsonItem["isTrash"]
610 message["isArchived"] = "inbox" not in jsonItem["labels"]
614 def set_sane_callback(backend):
616 Try to set a sane default callback number on these preferences
617 1) 1747 numbers ( Gizmo )
618 2) anything with gizmo in the name
619 3) anything with computer in the name
622 numbers = backend.get_callback_numbers()
624 priorityOrderedCriteria = [
632 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
633 for number, description in numbers.iteritems():
634 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
636 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
638 backend.set_callback_number(number)
642 def sort_messages(allMessages):
643 sortableAllMessages = [
644 (message["time"], message)
645 for message in allMessages
647 sortableAllMessages.sort(reverse=True)
650 for (exactTime, message) in sortableAllMessages
654 def decorate_recent(recentCallData):
656 @returns (personsName, phoneNumber, date, action)
658 contactId = recentCallData["contactId"]
659 if recentCallData["name"]:
660 header = recentCallData["name"]
661 elif recentCallData["prettyNumber"]:
662 header = recentCallData["prettyNumber"]
663 elif recentCallData["location"]:
664 header = recentCallData["location"]
668 number = recentCallData["number"]
669 relTime = recentCallData["relTime"]
670 action = recentCallData["action"]
671 return contactId, header, number, relTime, action
674 def decorate_message(messageData):
675 contactId = messageData["contactId"]
676 exactTime = messageData["time"]
677 if messageData["name"]:
678 header = messageData["name"]
679 elif messageData["prettyNumber"]:
680 header = messageData["prettyNumber"]
683 number = messageData["number"]
684 relativeTime = messageData["relTime"]
686 messageParts = list(messageData["messageParts"])
687 if len(messageParts) == 0:
688 messages = ("No Transcription", )
689 elif len(messageParts) == 1:
690 messages = (messageParts[0][1], )
693 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
694 for messagePart in messageParts
697 decoratedResults = contactId, header, number, relativeTime, messages
698 return decoratedResults
701 def test_backend(username, password):
703 print "Authenticated: ", backend.is_authed()
704 if not backend.is_authed():
705 print "Login?: ", backend.login(username, password)
706 print "Authenticated: ", backend.is_authed()
708 print "Token: ", backend._token
709 #print "Account: ", backend.get_account_number()
710 #print "Callback: ", backend.get_callback_number()
711 #print "All Callback: ",
713 #pprint.pprint(backend.get_callback_numbers())
716 #for data in backend.get_recent():
717 # pprint.pprint(data)
718 #for data in sort_messages(backend.get_recent()):
719 # pprint.pprint(decorate_recent(data))
720 #pprint.pprint(list(backend.get_recent()))
723 #for contact in backend.get_contacts():
725 # pprint.pprint(list(backend.get_contact_details(contact[0])))
728 #for message in backend.get_messages():
729 # message["messageParts"] = list(message["messageParts"])
730 # pprint.pprint(message)
731 #for message in sort_messages(backend.get_messages()):
732 # pprint.pprint(decorate_message(message))
737 def grab_debug_info(username, password):
738 cookieFile = os.path.join(".", "raw_cookies.txt")
740 os.remove(cookieFile)
744 backend = GVDialer(cookieFile)
745 browser = backend._browser
748 ("forward", backend._forwardURL),
749 ("token", backend._tokenURL),
750 ("login", backend._loginURL),
751 ("contacts", backend._contactsURL),
753 ("voicemail", backend._voicemailURL),
754 ("sms", backend._smsURL),
756 ("recent", backend._recentCallsURL),
757 ("placed", backend._placedCallsURL),
758 ("recieved", backend._receivedCallsURL),
759 ("missed", backend._missedCallsURL),
763 print "Grabbing pre-login pages"
764 for name, url in _TEST_WEBPAGES:
766 page = browser.download(url)
767 except StandardError, e:
770 print "\tWriting to file"
771 with open("not_loggedin_%s.txt" % name, "w") as f:
775 print "Attempting login"
776 galxToken = backend._get_token()
777 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
778 with open("loggingin.txt", "w") as f:
779 print "\tWriting to file"
780 f.write(loginSuccessOrFailurePage)
782 backend._grab_account_info(loginSuccessOrFailurePage)
784 # Retry in case the redirect failed
785 # luckily is_authed does everything we need for a retry
786 loggedIn = backend.is_authed(True)
791 print "Grabbing post-login pages"
792 for name, url in _TEST_WEBPAGES:
794 page = browser.download(url)
795 except StandardError, e:
798 print "\tWriting to file"
799 with open("loggedin_%s.txt" % name, "w") as f:
803 browser.cookies.save()
804 print "\tWriting cookies to file"
805 with open("cookies.txt", "w") as f:
807 "%s: %s\n" % (c.name, c.value)
808 for c in browser.cookies
812 if __name__ == "__main__":
814 logging.basicConfig(level=logging.DEBUG)
816 grab_debug_info(sys.argv[1], sys.argv[2])
818 test_backend(sys.argv[1], sys.argv[2])