Message type selection stuff
[gc-dialer] / src / gv_backend.py
index 6aab60e..3f28b29 100644 (file)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 
 """
-DialCentral - Front end for Google's Grand Central service.
+DialCentral - Front end for Google's GoogleVoice service.
 Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
 
 This library is free software; you can redistribute it and/or
@@ -25,6 +25,7 @@ Resources
        http://posttopic.com/topic/google-voice-add-on-development
 """
 
+from __future__ import with_statement
 
 import os
 import re
@@ -46,6 +47,7 @@ except ImportError:
        simplejson = None
 
 
+_moduleLogger = logging.getLogger("gvoice.dialer")
 _TRUE_REGEX = re.compile("true")
 _FALSE_REGEX = re.compile("false")
 
@@ -97,9 +99,13 @@ def itergroup(iterator, count, padValue = None):
        return itertools.izip(*nIterators)
 
 
+class NetworkError(RuntimeError):
+       pass
+
+
 class GVDialer(object):
        """
-       This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
+       This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
        the functions include login, setting up a callback number, and initalting a callback
        """
 
@@ -118,7 +124,7 @@ class GVDialer(object):
                self._callbackNumber = ""
                self._callbackNumbers = {}
 
-               self.__contacts = None
+       _forwardURL = "https://www.google.com/voice/mobile/phones"
 
        def is_authed(self, force = False):
                """
@@ -126,30 +132,39 @@ class GVDialer(object):
                @note Once logged in try not to reauth more than once a minute.
                @returns If authenticated
                """
-
                if (time.time() - self._lastAuthed) < 120 and not force:
                        return True
 
                try:
-                       self._grab_account_info()
-               except StandardError, e:
-                       logging.exception(str(e))
+                       page = self._browser.download(self._forwardURL)
+                       self._grab_account_info(page)
+               except Exception, e:
+                       _moduleLogger.exception(str(e))
                        return False
 
                self._browser.cookies.save()
                self._lastAuthed = time.time()
                return True
 
+       _tokenURL = "http://www.google.com/voice/m"
        _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
+       _galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
 
-       def login(self, username, password):
-               """
-               Attempt to login to grandcentral
-               @returns Whether login was successful or not
-               """
-               if self.is_authed():
-                       return True
+       def _get_token(self):
+               try:
+                       tokenPage = self._browser.download(self._tokenURL)
+               except urllib2.URLError, e:
+                       _moduleLogger.exception("Translating error: %s" % str(e))
+                       raise NetworkError("%s is not accesible" % self._loginURL)
+               galxTokens = self._galxRe.search(tokenPage)
+               if galxTokens is not None:
+                       galxToken = galxTokens.group(1)
+               else:
+                       galxToken = ""
+                       _moduleLogger.debug("Could not grab GALX token")
+               return galxToken
 
+       def _login(self, username, password, token):
                loginPostData = urllib.urlencode({
                        'Email' : username,
                        'Passwd' : password,
@@ -157,23 +172,46 @@ class GVDialer(object):
                        "ltmpl": "mobile",
                        "btmpl": "mobile",
                        "PersistentCookie": "yes",
+                       "GALX": token,
+                       "continue": self._forwardURL,
                })
 
                try:
                        loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
                except urllib2.URLError, e:
-                       logging.exception(str(e))
-                       raise RuntimeError("%s is not accesible" % self._loginURL)
+                       _moduleLogger.exception("Translating error: %s" % str(e))
+                       raise NetworkError("%s is not accesible" % self._loginURL)
+               return loginSuccessOrFailurePage
+
+       def login(self, username, password):
+               """
+               Attempt to login to GoogleVoice
+               @returns Whether login was successful or not
+               """
+               self.logout()
+               galxToken = self._get_token()
+               loginSuccessOrFailurePage = self._login(username, password, galxToken)
 
-               return self.is_authed()
+               try:
+                       self._grab_account_info(loginSuccessOrFailurePage)
+               except Exception, e:
+                       # Retry in case the redirect failed
+                       # luckily is_authed does everything we need for a retry
+                       loggedIn = self.is_authed(True)
+                       if not loggedIn:
+                               _moduleLogger.exception(str(e))
+                               return False
+                       _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
+
+               self._browser.cookies.save()
+               self._lastAuthed = time.time()
+               return True
 
        def logout(self):
                self._lastAuthed = 0.0
                self._browser.cookies.clear()
                self._browser.cookies.save()
 
-               self.clear_caches()
-
        _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
        _clicktocallURL = "https://www.google.com/voice/m/sendcall"
 
@@ -193,8 +231,8 @@ class GVDialer(object):
                        }
                        callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
                except urllib2.URLError, e:
-                       logging.exception(str(e))
-                       raise RuntimeError("%s is not accesible" % self._clicktocallURL)
+                       _moduleLogger.exception("Translating error: %s" % str(e))
+                       raise NetworkError("%s is not accesible" % self._clicktocallURL)
 
                if self._gvDialingStrRe.search(callSuccessPage) is None:
                        raise RuntimeError("Google Voice returned an error")
@@ -218,14 +256,11 @@ class GVDialer(object):
                        }
                        smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
                except urllib2.URLError, e:
-                       logging.exception(str(e))
-                       raise RuntimeError("%s is not accesible" % self._sendSmsURL)
+                       _moduleLogger.exception("Translating error: %s" % str(e))
+                       raise NetworkError("%s is not accesible" % self._sendSmsURL)
 
                return True
 
-       def clear_caches(self):
-               self.__contacts = None
-
        _validateRe = re.compile("^[0-9]{10,}$")
 
        def is_valid_syntax(self, number):
@@ -236,39 +271,10 @@ class GVDialer(object):
 
        def get_account_number(self):
                """
-               @returns The grand central phone number
+               @returns The GoogleVoice phone number
                """
                return self._accountNum
 
-       def set_sane_callback(self):
-               """
-               Try to set a sane default callback number on these preferences
-               1) 1747 numbers ( Gizmo )
-               2) anything with gizmo in the name
-               3) anything with computer in the name
-               4) the first value
-               """
-               numbers = self.get_callback_numbers()
-
-               for number, description in numbers.iteritems():
-                       if re.compile(r"""1747""").match(number) is not None:
-                               self.set_callback_number(number)
-                               return
-
-               for number, description in numbers.iteritems():
-                       if re.compile(r"""gizmo""", re.I).search(description) is not None:
-                               self.set_callback_number(number)
-                               return
-
-               for number, description in numbers.iteritems():
-                       if re.compile(r"""computer""", re.I).search(description) is not None:
-                               self.set_callback_number(number)
-                               return
-
-               for number, description in numbers.iteritems():
-                       self.set_callback_number(number)
-                       return
-
        def get_callback_numbers(self):
                """
                @returns a dictionary mapping call back numbers to descriptions
@@ -278,11 +284,9 @@ class GVDialer(object):
                        return {}
                return self._callbackNumbers
 
-       _setforwardURL = "https://www.google.com//voice/m/setphone"
-
        def set_callback_number(self, callbacknumber):
                """
-               Set the number that grandcental calls
+               Set the number that GoogleVoice calls
                @param callbacknumber should be a proper 10 digit number
                """
                self._callbackNumber = callbacknumber
@@ -294,34 +298,31 @@ class GVDialer(object):
                """
                return self._callbackNumber
 
-       def get_recent(self):
-               """
-               @returns Iterable of (personsName, phoneNumber, date, action)
-               """
-               sortedRecent = [
-                       (exactDate, name, number, relativeDate, action)
-                       for (name, number, exactDate, relativeDate, action) in self._get_recent()
-               ]
-               sortedRecent.sort(reverse = True)
-               for exactDate, name, number, relativeDate, action in sortedRecent:
-                       yield name, number, relativeDate, action
+       _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
+       _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
+       _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
+       _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
 
-       def get_addressbooks(self):
+       def get_recent(self):
                """
-               @returns Iterable of (Address Book Factory, Book Id, Book Name)
+               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
                """
-               yield self, "", ""
-
-       def open_addressbook(self, bookId):
-               return self
-
-       @staticmethod
-       def contact_source_short_name(contactId):
-               return "GV"
+               for action, url in (
+                       ("Received", self._receivedCallsURL),
+                       ("Missed", self._missedCallsURL),
+                       ("Placed", self._placedCallsURL),
+               ):
+                       try:
+                               flatXml = self._browser.download(url)
+                       except urllib2.URLError, e:
+                               _moduleLogger.exception("Translating error: %s" % str(e))
+                               raise NetworkError("%s is not accesible" % url)
 
-       @staticmethod
-       def factory_name():
-               return "Google Voice"
+                       allRecentHtml = self._grab_html(flatXml)
+                       allRecentData = self._parse_voicemail(allRecentHtml)
+                       for recentCallData in allRecentData:
+                               recentCallData["action"] = action
+                               yield recentCallData
 
        _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
        _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
@@ -331,31 +332,24 @@ class GVDialer(object):
                """
                @returns Iterable of (contact id, contact name)
                """
-               if self.__contacts is None:
-                       self.__contacts = []
-
-                       contactsPagesUrls = [self._contactsURL]
-                       for contactsPageUrl in contactsPagesUrls:
-                               try:
-                                       contactsPage = self._browser.download(contactsPageUrl)
-                               except urllib2.URLError, e:
-                                       logging.exception(str(e))
-                                       raise RuntimeError("%s is not accesible" % contactsPageUrl)
-                               for contact_match in self._contactsRe.finditer(contactsPage):
-                                       contactId = contact_match.group(1)
-                                       contactName = saxutils.unescape(contact_match.group(2))
-                                       contact = contactId, contactName
-                                       self.__contacts.append(contact)
-                                       yield contact
-
-                               next_match = self._contactsNextRe.match(contactsPage)
-                               if next_match is not None:
-                                       newContactsPageUrl = self._contactsURL + next_match.group(1)
-                                       contactsPagesUrls.append(newContactsPageUrl)
-               else:
-                       for contact in self.__contacts:
+               contactsPagesUrls = [self._contactsURL]
+               for contactsPageUrl in contactsPagesUrls:
+                       try:
+                               contactsPage = self._browser.download(contactsPageUrl)
+                       except urllib2.URLError, e:
+                               _moduleLogger.exception("Translating error: %s" % str(e))
+                               raise NetworkError("%s is not accesible" % contactsPageUrl)
+                       for contact_match in self._contactsRe.finditer(contactsPage):
+                               contactId = contact_match.group(1)
+                               contactName = saxutils.unescape(contact_match.group(2))
+                               contact = contactId, contactName
                                yield contact
 
+                       next_match = self._contactsNextRe.match(contactsPage)
+                       if next_match is not None:
+                               newContactsPageUrl = self._contactsURL + next_match.group(1)
+                               contactsPagesUrls.append(newContactsPageUrl)
+
        _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
        _contactDetailURL = "https://www.google.com/voice/mobile/contact"
 
@@ -366,8 +360,8 @@ class GVDialer(object):
                try:
                        detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
                except urllib2.URLError, e:
-                       logging.exception(str(e))
-                       raise RuntimeError("%s is not accesible" % self._contactDetailURL)
+                       _moduleLogger.exception("Translating error: %s" % str(e))
+                       raise NetworkError("%s is not accesible" % self._contactDetailURL)
 
                for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
                        phoneNumber = detail_match.group(1)
@@ -377,30 +371,61 @@ class GVDialer(object):
        _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
        _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
 
+       @staticmethod
+       def _merge_messages(parsedMessages, json):
+               for message in parsedMessages:
+                       id = message["id"]
+                       jsonItem = json["messages"][id]
+                       message["isRead"] = jsonItem["isRead"]
+                       message["isSpam"] = jsonItem["isSpam"]
+                       message["isTrash"] = jsonItem["isTrash"]
+                       yield message
+
        def get_messages(self):
                try:
                        voicemailPage = self._browser.download(self._voicemailURL)
                except urllib2.URLError, e:
-                       logging.exception(str(e))
-                       raise RuntimeError("%s is not accesible" % self._voicemailURL)
+                       _moduleLogger.exception("Translating error: %s" % str(e))
+                       raise NetworkError("%s is not accesible" % self._voicemailURL)
                voicemailHtml = self._grab_html(voicemailPage)
+               voicemailJson = self._grab_json(voicemailPage)
                parsedVoicemail = self._parse_voicemail(voicemailHtml)
-               decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
+               voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
+               decoratedVoicemails = self._decorate_voicemail(voicemails)
 
                try:
                        smsPage = self._browser.download(self._smsURL)
                except urllib2.URLError, e:
-                       logging.exception(str(e))
-                       raise RuntimeError("%s is not accesible" % self._smsURL)
+                       _moduleLogger.exception("Translating error: %s" % str(e))
+                       raise NetworkError("%s is not accesible" % self._smsURL)
                smsHtml = self._grab_html(smsPage)
+               smsJson = self._grab_json(smsPage)
                parsedSms = self._parse_sms(smsHtml)
-               decoratedSms = self._decorate_sms(parsedSms)
+               smss = self._merge_messages(parsedSms, smsJson)
+               decoratedSms = self._decorate_sms(smss)
 
                allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
-               sortedMessages = list(allMessages)
-               sortedMessages.sort(reverse=True)
-               for exactDate, header, number, relativeDate, message in sortedMessages:
-                       yield header, number, relativeDate, message
+               return allMessages
+
+       def clear_caches(self):
+               pass
+
+       def get_addressbooks(self):
+               """
+               @returns Iterable of (Address Book Factory, Book Id, Book Name)
+               """
+               yield self, "", ""
+
+       def open_addressbook(self, bookId):
+               return self
+
+       @staticmethod
+       def contact_source_short_name(contactId):
+               return "GV"
+
+       @staticmethod
+       def factory_name():
+               return "Google Voice"
 
        def _grab_json(self, flatXml):
                xmlTree = ElementTree.fromstring(flatXml)
@@ -418,11 +443,8 @@ class GVDialer(object):
        _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
        _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
        _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
-       _forwardURL = "https://www.google.com/voice/mobile/phones"
-
-       def _grab_account_info(self):
-               page = self._browser.download(self._forwardURL)
 
+       def _grab_account_info(self, page):
                tokenGroup = self._tokenRe.search(page)
                if tokenGroup is None:
                        raise RuntimeError("Could not extract authentication token from GoogleVoice")
@@ -432,13 +454,15 @@ class GVDialer(object):
                if anGroup is not None:
                        self._accountNum = anGroup.group(1)
                else:
-                       logging.debug("Could not extract account number from GoogleVoice")
+                       _moduleLogger.debug("Could not extract account number from GoogleVoice")
 
                self._callbackNumbers = {}
                for match in self._callbackRe.finditer(page):
                        callbackNumber = match.group(2)
                        callbackName = match.group(1)
                        self._callbackNumbers[callbackNumber] = callbackName
+               if len(self._callbackNumbers) == 0:
+                       _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
 
        def _send_validation(self, number):
                if not self.is_valid_syntax(number):
@@ -451,40 +475,6 @@ class GVDialer(object):
                        number = number[1:]
                return number
 
-       _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
-       _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
-       _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
-       _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
-
-       def _get_recent(self):
-               """
-               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
-               """
-               for action, url in (
-                       ("Received", self._receivedCallsURL),
-                       ("Missed", self._missedCallsURL),
-                       ("Placed", self._placedCallsURL),
-               ):
-                       try:
-                               flatXml = self._browser.download(url)
-                       except urllib2.URLError, e:
-                               logging.exception(str(e))
-                               raise RuntimeError("%s is not accesible" % url)
-
-                       allRecentHtml = self._grab_html(flatXml)
-                       allRecentData = self._parse_voicemail(allRecentHtml)
-                       for recentCallData in allRecentData:
-                               exactTime = recentCallData["time"]
-                               if recentCallData["name"]:
-                                       header = recentCallData["name"]
-                               elif recentCallData["prettyNumber"]:
-                                       header = recentCallData["prettyNumber"]
-                               elif recentCallData["location"]:
-                                       header = recentCallData["location"]
-                               else:
-                                       header = "Unknown"
-                               yield header, recentCallData["number"], exactTime, recentCallData["relTime"], action
-
        _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
        _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
        _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
@@ -492,7 +482,18 @@ class GVDialer(object):
        _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
        _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
        _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
-       _voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
+       _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
+       #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
+       #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
+       _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
+
+       @staticmethod
+       def _interpret_voicemail_regex(group):
+               quality, content, number = group.group(2), group.group(3), group.group(4)
+               if quality is not None and content is not None:
+                       return quality, content
+               elif number is not None:
+                       return "high", number
 
        def _parse_voicemail(self, voicemailHtml):
                splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
@@ -511,15 +512,18 @@ class GVDialer(object):
                        number = numberGroup.group(1).strip() if numberGroup else ""
                        prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
                        prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+                       contactIdGroup = self._messagesContactID.search(messageHtml)
+                       contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
 
                        messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
                        messageParts = (
-                               (group.group(1).strip(), group.group(2).strip())
+                               self._interpret_voicemail_regex(group)
                                for group in messageGroups
                        ) if messageGroups else ()
 
                        yield {
                                "id": messageId.strip(),
+                               "contactId": contactId,
                                "name": name,
                                "time": exactTime,
                                "relTime": relativeTime,
@@ -527,35 +531,30 @@ class GVDialer(object):
                                "number": number,
                                "location": location,
                                "messageParts": messageParts,
+                               "type": "Voicemail",
                        }
 
-       def _decorate_voicemail(self, parsedVoicemail):
+       def _decorate_voicemail(self, parsedVoicemails):
                messagePartFormat = {
                        "med1": "<i>%s</i>",
                        "med2": "%s",
                        "high": "<b>%s</b>",
                }
-               for voicemailData in parsedVoicemail:
-                       exactTime = voicemailData["time"]
-                       if voicemailData["name"]:
-                               header = voicemailData["name"]
-                       elif voicemailData["prettyNumber"]:
-                               header = voicemailData["prettyNumber"]
-                       elif voicemailData["location"]:
-                               header = voicemailData["location"]
-                       else:
-                               header = "Unknown"
+               for voicemailData in parsedVoicemails:
                        message = " ".join((
                                messagePartFormat[quality] % part
                                for (quality, part) in voicemailData["messageParts"]
                        )).strip()
                        if not message:
                                message = "No Transcription"
-                       yield exactTime, header, voicemailData["number"], voicemailData["relTime"], message
+                       whoFrom = voicemailData["name"]
+                       when = voicemailData["time"]
+                       voicemailData["messageParts"] = ((whoFrom, message, when), )
+                       yield voicemailData
 
        _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
-       _smsTextRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
-       _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+       _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+       _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
 
        def _parse_sms(self, smsHtml):
                splitSms = self._seperateVoicemailsRegex.split(smsHtml)
@@ -572,6 +571,8 @@ class GVDialer(object):
                        number = numberGroup.group(1).strip() if numberGroup else ""
                        prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
                        prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+                       contactIdGroup = self._messagesContactID.search(messageHtml)
+                       contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
 
                        fromGroups = self._smsFromRegex.finditer(messageHtml)
                        fromParts = (group.group(1).strip() for group in fromGroups)
@@ -584,50 +585,222 @@ class GVDialer(object):
 
                        yield {
                                "id": messageId.strip(),
+                               "contactId": contactId,
                                "name": name,
                                "time": exactTime,
                                "relTime": relativeTime,
                                "prettyNumber": prettyNumber,
                                "number": number,
+                               "location": "",
                                "messageParts": messageParts,
+                               "type": "Texts",
                        }
 
-       def _decorate_sms(self, parsedSms):
-               for messageData in parsedSms:
-                       exactTime = messageData["time"]
-                       if messageData["name"]:
-                               header = messageData["name"]
-                       elif messageData["prettyNumber"]:
-                               header = messageData["prettyNumber"]
-                       else:
-                               header = "Unknown"
-                       number = messageData["number"]
-                       relativeTime = messageData["relTime"]
-                       message = "\n".join((
-                               "<b>%s</b>: %s" % (messagePart[0], messagePart[-1])
-                               for messagePart in messageData["messageParts"]
-                       ))
-                       if not message:
-                               message = "No Transcription"
-                       yield exactTime, header, number, relativeTime, message
+       def _decorate_sms(self, parsedTexts):
+               return parsedTexts
+
+
+def set_sane_callback(backend):
+       """
+       Try to set a sane default callback number on these preferences
+       1) 1747 numbers ( Gizmo )
+       2) anything with gizmo in the name
+       3) anything with computer in the name
+       4) the first value
+       """
+       numbers = backend.get_callback_numbers()
+
+       priorityOrderedCriteria = [
+               ("1747", None),
+               (None, "gizmo"),
+               (None, "computer"),
+               (None, "sip"),
+               (None, None),
+       ]
+
+       for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
+               for number, description in numbers.iteritems():
+                       if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
+                               continue
+                       if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
+                               continue
+                       backend.set_callback_number(number)
+                       return
+
+
+def sort_messages(allMessages):
+       sortableAllMessages = [
+               (message["time"], message)
+               for message in allMessages
+       ]
+       sortableAllMessages.sort(reverse=True)
+       return (
+               message
+               for (exactTime, message) in sortableAllMessages
+       )
+
+
+def decorate_recent(recentCallData):
+       """
+       @returns (personsName, phoneNumber, date, action)
+       """
+       contactId = recentCallData["contactId"]
+       if recentCallData["name"]:
+               header = recentCallData["name"]
+       elif recentCallData["prettyNumber"]:
+               header = recentCallData["prettyNumber"]
+       elif recentCallData["location"]:
+               header = recentCallData["location"]
+       else:
+               header = "Unknown"
+
+       number = recentCallData["number"]
+       relTime = recentCallData["relTime"]
+       action = recentCallData["action"]
+       return contactId, header, number, relTime, action
+
+
+def decorate_message(messageData):
+       contactId = messageData["contactId"]
+       exactTime = messageData["time"]
+       if messageData["name"]:
+               header = messageData["name"]
+       elif messageData["prettyNumber"]:
+               header = messageData["prettyNumber"]
+       else:
+               header = "Unknown"
+       number = messageData["number"]
+       relativeTime = messageData["relTime"]
+
+       messageParts = list(messageData["messageParts"])
+       if len(messageParts) == 0:
+               messages = ("No Transcription", )
+       elif len(messageParts) == 1:
+               messages = (messageParts[0][1], )
+       else:
+               messages = [
+                       "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
+                       for messagePart in messageParts
+               ]
+
+       decoratedResults = contactId, header, number, relativeTime, messages
+       return decoratedResults
 
 
 def test_backend(username, password):
        backend = GVDialer()
        print "Authenticated: ", backend.is_authed()
-       print "Login?: ", backend.login(username, password)
+       if not backend.is_authed():
+               print "Login?: ", backend.login(username, password)
        print "Authenticated: ", backend.is_authed()
-       # print "Token: ", backend._token
-       print "Account: ", backend.get_account_number()
-       print "Callback: ", backend.get_callback_number()
-       # print "All Callback: ",
-       #import pprint
-       # pprint.pprint(backend.get_callback_numbers())
-       # print "Recent: ",
-       # pprint.pprint(list(backend.get_recent()))
-       # print "Contacts: ",
-       # for contact in backend.get_contacts():
+
+       print "Token: ", backend._token
+       #print "Account: ", backend.get_account_number()
+       #print "Callback: ", backend.get_callback_number()
+       #print "All Callback: ",
+       import pprint
+       #pprint.pprint(backend.get_callback_numbers())
+
+       #print "Recent: "
+       #for data in backend.get_recent():
+       #       pprint.pprint(data)
+       #for data in sort_messages(backend.get_recent()):
+       #       pprint.pprint(decorate_recent(data))
+       #pprint.pprint(list(backend.get_recent()))
+
+       #print "Contacts: ",
+       #for contact in backend.get_contacts():
        #       print contact
        #       pprint.pprint(list(backend.get_contact_details(contact[0])))
 
+       print "Messages: ",
+       for message in backend.get_messages():
+               message["messageParts"] = list(message["messageParts"])
+               pprint.pprint(message)
+       #for message in sort_messages(backend.get_messages()):
+       #       pprint.pprint(decorate_message(message))
+
        return backend
+
+
+_TEST_WEBPAGES = [
+       ("forward", GVDialer._forwardURL),
+       ("token", GVDialer._tokenURL),
+       ("login", GVDialer._loginURL),
+       ("contacts", GVDialer._contactsURL),
+
+       ("voicemail", GVDialer._voicemailURL),
+       ("sms", GVDialer._smsURL),
+
+       ("recent", GVDialer._recentCallsURL),
+       ("placed", GVDialer._placedCallsURL),
+       ("recieved", GVDialer._receivedCallsURL),
+       ("missed", GVDialer._missedCallsURL),
+]
+
+
+def grab_debug_info(username, password):
+       cookieFile = os.path.join(".", "raw_cookies.txt")
+       try:
+               os.remove(cookieFile)
+       except OSError:
+               pass
+
+       backend = GVDialer(cookieFile)
+       browser = backend._browser
+
+       # Get Pages
+       print "Grabbing pre-login pages"
+       for name, url in _TEST_WEBPAGES:
+               try:
+                       page = browser.download(url)
+               except StandardError, e:
+                       print e.message
+                       continue
+               print "\tWriting to file"
+               with open("not_loggedin_%s.txt" % name, "w") as f:
+                       f.write(page)
+
+       # Login
+       print "Attempting login"
+       galxToken = backend._get_token()
+       loginSuccessOrFailurePage = backend._login(username, password, galxToken)
+       with open("loggingin.txt", "w") as f:
+               print "\tWriting to file"
+               f.write(loginSuccessOrFailurePage)
+       try:
+               backend._grab_account_info(loginSuccessOrFailurePage)
+       except Exception:
+               # Retry in case the redirect failed
+               # luckily is_authed does everything we need for a retry
+               loggedIn = backend.is_authed(True)
+               if not loggedIn:
+                       raise
+
+       # Get Pages
+       print "Grabbing post-login pages"
+       for name, url in _TEST_WEBPAGES:
+               try:
+                       page = browser.download(url)
+               except StandardError, e:
+                       print e.message
+                       continue
+               print "\tWriting to file"
+               with open("loggedin_%s.txt" % name, "w") as f:
+                       f.write(page)
+
+       # Cookies
+       browser.cookies.save()
+       print "\tWriting cookies to file"
+       with open("cookies.txt", "w") as f:
+               f.writelines(
+                       "%s: %s\n" % (c.name, c.value)
+                       for c in browser.cookies
+               )
+
+
+if __name__ == "__main__":
+       import sys
+       logging.basicConfig(level=logging.DEBUG)
+       #test_backend(sys.argv[1], sys.argv[2])
+       grab_debug_info(sys.argv[1], sys.argv[2])