Various bug fixes and tweaks found through 0, 1, and 2
[gc-dialer] / src / gv_backend.py
index 75ec4bd..29021c2 100644 (file)
@@ -104,41 +104,6 @@ class GVDialer(object):
        the functions include login, setting up a callback number, and initalting a callback
        """
 
-       _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)
-       _validateRe = re.compile("^[0-9]{10,}$")
-       _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
-
-       _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)
-       _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
-
-       _clicktocallURL = "https://www.google.com/voice/m/sendcall"
-       _smsURL = "https://www.google.com/voice/m/sendsms"
-       _contactsURL = "https://www.google.com/voice/mobile/contacts"
-       _contactDetailURL = "https://www.google.com/voice/mobile/contact"
-
-       _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
-       _setforwardURL = "https://www.google.com//voice/m/setphone"
-       _accountNumberURL = "https://www.google.com/voice/mobile"
-       _forwardURL = "https://www.google.com/voice/mobile/phones"
-
-       _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/"
-       _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
-       _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
-
-       _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)
-       _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">(.*?)</span>""", re.MULTILINE)
-       _voicemailMessageRegex = re.compile(r"""<span class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
-
        def __init__(self, cookieFile = None):
                # Important items in this function are the setup of the browser emulation and cookie file
                self._browser = browser_emu.MozillaEmulator(1)
@@ -149,7 +114,7 @@ class GVDialer(object):
                        self._browser.cookies.load()
 
                self._token = ""
-               self._accountNum = None
+               self._accountNum = ""
                self._lastAuthed = 0.0
                self._callbackNumber = ""
                self._callbackNumbers = {}
@@ -163,7 +128,7 @@ class GVDialer(object):
                @returns If authenticated
                """
 
-               if (time.time() - self._lastAuthed) < 60 and not force:
+               if (time.time() - self._lastAuthed) < 120 and not force:
                        return True
 
                try:
@@ -176,6 +141,8 @@ class GVDialer(object):
                self._lastAuthed = time.time()
                return True
 
+       _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
+
        def login(self, username, password):
                """
                Attempt to login to grandcentral
@@ -208,6 +175,9 @@ class GVDialer(object):
 
                self.clear_caches()
 
+       _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
+       _clicktocallURL = "https://www.google.com/voice/m/sendcall"
+
        def dial(self, number):
                """
                This is the main function responsible for initating the callback
@@ -232,6 +202,8 @@ class GVDialer(object):
 
                return True
 
+       _sendSmsURL = "https://www.google.com/voice/m/sendsms"
+
        def send_sms(self, number, message):
                number = self._send_validation(number)
                try:
@@ -245,16 +217,18 @@ class GVDialer(object):
                        otherData = {
                                'Referer' : 'https://google.com/voice/m/sms',
                        }
-                       smsSuccessPage = self._browser.download(self._smsURL, smsData, None, otherData)
+                       smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
                except urllib2.URLError, e:
                        warnings.warn(traceback.format_exc())
-                       raise RuntimeError("%s is not accesible" % self._clicktocallURL)
+                       raise RuntimeError("%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):
                """
                @returns If This number be called ( syntax validation only )
@@ -301,10 +275,11 @@ class GVDialer(object):
                @returns a dictionary mapping call back numbers to descriptions
                @note These results are cached for 30 minutes.
                """
-               if time.time() - self._lastAuthed < 1800 or self.is_authed():
-                       return self._callbackNumbers
+               if not self.is_authed():
+                       return {}
+               return self._callbackNumbers
 
-               return {}
+       _setforwardURL = "https://www.google.com//voice/m/setphone"
 
        def set_callback_number(self, callbacknumber):
                """
@@ -312,33 +287,32 @@ class GVDialer(object):
                @param callbacknumber should be a proper 10 digit number
                """
                self._callbackNumber = callbacknumber
-               callbackPostData = urllib.urlencode({
-                       '_rnr_se': self._token,
-                       'phone': callbacknumber
-               })
-               try:
-                       callbackSetPage = self._browser.download(self._setforwardURL, callbackPostData)
-               except urllib2.URLError, e:
-                       warnings.warn(traceback.format_exc())
-                       raise RuntimeError("%s is not accesible" % self._setforwardURL)
 
-               # @bug This does not seem to be keeping on my tablet (but works on the
-               # desktop), or the reading isn't working too well
-               self._browser.cookies.save()
+               # Currently this isn't working out in GoogleVoice, but thats ok, we pass the callback on dial
+               #callbackPostData = urllib.urlencode({
+               #       '_rnr_se': self._token,
+               #       'phone': callbacknumber
+               #})
+               #try:
+               #       callbackSetPage = self._browser.download(self._setforwardURL, callbackPostData)
+               #       self._browser.cookies.save()
+               #except urllib2.URLError, e:
+               #       warnings.warn(traceback.format_exc())
+               #       raise RuntimeError("%s is not accesible" % self._setforwardURL)
+
                return True
 
        def get_callback_number(self):
                """
                @returns Current callback number or None
                """
-               for c in self._browser.cookies:
-                       if c.name == "gv-ph":
-                               return c.value
+               #for c in self._browser.cookies:
+               #       if c.name == "gv-ph":
+               #               return c.value
                return self._callbackNumber
 
        def get_recent(self):
                """
-               @todo Sort this stuff
                @returns Iterable of (personsName, phoneNumber, date, action)
                """
                sortedRecent = [
@@ -366,6 +340,10 @@ class GVDialer(object):
        def factory_name():
                return "Google Voice"
 
+       _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)
+       _contactsURL = "https://www.google.com/voice/mobile/contacts"
+
        def get_contacts(self):
                """
                @returns Iterable of (contact id, contact name)
@@ -379,7 +357,7 @@ class GVDialer(object):
                                        contactsPage = self._browser.download(contactsPageUrl)
                                except urllib2.URLError, e:
                                        warnings.warn(traceback.format_exc())
-                                       raise RuntimeError("%s is not accesible" % self._clicktocallURL)
+                                       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))
@@ -395,6 +373,9 @@ class GVDialer(object):
                        for contact in self.__contacts:
                                yield contact
 
+       _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
+       _contactDetailURL = "https://www.google.com/voice/mobile/contact"
+
        def get_contact_details(self, contactId):
                """
                @returns Iterable of (Phone Type, Phone Number)
@@ -403,35 +384,38 @@ class GVDialer(object):
                        detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
                except urllib2.URLError, e:
                        warnings.warn(traceback.format_exc())
-                       raise RuntimeError("%s is not accesible" % self._clicktocallURL)
+                       raise RuntimeError("%s is not accesible" % self._contactDetailURL)
 
                for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
                        phoneNumber = detail_match.group(1)
                        phoneType = saxutils.unescape(detail_match.group(2))
                        yield (phoneType, phoneNumber)
 
+       _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
+       _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
+
        def get_messages(self):
                try:
                        voicemailPage = self._browser.download(self._voicemailURL)
                except urllib2.URLError, e:
                        warnings.warn(traceback.format_exc())
                        raise RuntimeError("%s is not accesible" % self._voicemailURL)
+               voicemailHtml = self._grab_html(voicemailPage)
+               parsedVoicemail = self._parse_voicemail(voicemailHtml)
+               decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
 
                try:
                        smsPage = self._browser.download(self._smsURL)
                except urllib2.URLError, e:
                        warnings.warn(traceback.format_exc())
                        raise RuntimeError("%s is not accesible" % self._smsURL)
+               smsHtml = self._grab_html(smsPage)
+               parsedSms = self._parse_sms(smsHtml)
+               decoratedSms = self._decorate_sms(parsedSms)
 
-               voicemailHtml = self._grab_html(voicemailPage)
-               parsedVoicemail = self._parse_voicemail(voicemailHtml)
-               decoratedVoicemails = self._decorated_voicemail(parsedVoicemail)
-
-               # @todo Parse this
-               # smsHtml = self._grab_html(smsPage)
-
-               allMessages = itertools.chain(decoratedVoicemails)
+               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
 
@@ -448,6 +432,11 @@ class GVDialer(object):
                flatHtml = htmlElement.text
                return flatHtml
 
+       _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)
 
@@ -457,9 +446,10 @@ class GVDialer(object):
                self._token = tokenGroup.group(1)
 
                anGroup = self._accountNumRe.search(page)
-               if anGroup is None:
-                       raise RuntimeError("Could not extract account number from GoogleVoice")
-               self._accountNum = anGroup.group(1)
+               if anGroup is not None:
+                       self._accountNum = anGroup.group(1)
+               else:
+                       warnings.warn("Could not extract account number from GoogleVoice", UserWarning, 2)
 
                self._callbackNumbers = {}
                for match in self._callbackRe.finditer(page):
@@ -478,58 +468,76 @@ 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 url in (
-                       self._receivedCallsURL,
-                       self._missedCallsURL,
-                       self._placedCallsURL,
+               for action, url in (
+                       ("Received", self._receivedCallsURL),
+                       ("Missed", self._missedCallsURL),
+                       ("Placed", self._placedCallsURL),
                ):
                        try:
                                flatXml = self._browser.download(url)
                        except urllib2.URLError, e:
                                warnings.warn(traceback.format_exc())
-                               raise RuntimeError("%s is not accesible" % self._clicktocallURL)
-
-                       allRecentData = self._grab_json(flatXml)
-                       for recentCallData in allRecentData["messages"].itervalues():
-                               number = recentCallData["displayNumber"]
-                               exactDate = recentCallData["displayStartDateTime"]
-                               relativeDate = recentCallData["relativeStartTime"]
-                               action = ", ".join((
-                                       label.title()
-                                       for label in recentCallData["labels"]
-                                               if label.lower() != "all" and label.lower() != "inbox"
-                               ))
-                               number = saxutils.unescape(number)
-                               exactDate = saxutils.unescape(exactDate)
-                               exactDate = datetime.datetime.strptime(exactDate, "%m/%d/%y %I:%M %p")
-                               relativeDate = saxutils.unescape(relativeDate)
-                               action = saxutils.unescape(action)
-                               yield "", number, exactDate, relativeDate, action
+                               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)
+       _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
+       _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)
 
        def _parse_voicemail(self, voicemailHtml):
                splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
-               for id, messageHtml in itergroup(splitVoicemail[1:], 2):
+               for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
                        exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
-                       exactTime = exactTimeGroup.group(1) if exactTimeGroup else ""
+                       exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+                       exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
                        relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
-                       relativeTime = relativeTimeGroup.group(1) if relativeTimeGroup else ""
+                       relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
                        locationGroup = self._voicemailLocationRegex.search(messageHtml)
-                       location = locationGroup.group(1) if locationGroup else ""
+                       location = locationGroup.group(1).strip() if locationGroup else ""
+
+                       nameGroup = self._voicemailNameRegex.search(messageHtml)
+                       name = nameGroup.group(1).strip() if nameGroup else ""
                        numberGroup = self._voicemailNumberRegex.search(messageHtml)
-                       number = numberGroup.group(1) if numberGroup else ""
+                       number = numberGroup.group(1).strip() if numberGroup else ""
                        prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
-                       prettyNumber = prettyNumberGroup.group(1) if prettyNumberGroup else ""
+                       prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+
                        messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
                        messageParts = (
-                               (group.group(1), group.group(2))
+                               (group.group(1).strip(), group.group(2).strip())
                                for group in messageGroups
                        ) if messageGroups else ()
+
                        yield {
-                               "id": id,
+                               "id": messageId.strip(),
+                               "name": name,
                                "time": exactTime,
                                "relTime": relativeTime,
                                "prettyNumber": prettyNumber,
@@ -538,15 +546,22 @@ class GVDialer(object):
                                "messageParts": messageParts,
                        }
 
-       def _decorated_voicemail(self, parsedVoicemail):
+       def _decorate_voicemail(self, parsedVoicemail):
                messagePartFormat = {
                        "med1": "<i>%s</i>",
                        "med2": "%s",
                        "high": "<b>%s</b>",
                }
                for voicemailData in parsedVoicemail:
-                       exactTime = voicemailData["time"] # @todo Parse This
-                       header = "%s %s" % (voicemailData["prettyNumber"], voicemailData["location"])
+                       exactTime = voicemailData["time"]
+                       if voicemailData["name"]:
+                               header = voicemailData["name"]
+                       elif voicemailData["prettyNumber"]:
+                               header = voicemailData["prettyNumber"]
+                       elif voicemailData["location"]:
+                               header = voicemailData["location"]
+                       else:
+                               header = "Unknown"
                        message = " ".join((
                                messagePartFormat[quality] % part
                                for (quality, part) in voicemailData["messageParts"]
@@ -555,6 +570,64 @@ class GVDialer(object):
                                message = "No Transcription"
                        yield exactTime, header, voicemailData["number"], voicemailData["relTime"], message
 
+       _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)
+
+       def _parse_sms(self, smsHtml):
+               splitSms = self._seperateVoicemailsRegex.split(smsHtml)
+               for messageId, messageHtml in itergroup(splitSms[1:], 2):
+                       exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+                       exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+                       exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
+                       relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+                       relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+
+                       nameGroup = self._voicemailNameRegex.search(messageHtml)
+                       name = nameGroup.group(1).strip() if nameGroup else ""
+                       numberGroup = self._voicemailNumberRegex.search(messageHtml)
+                       number = numberGroup.group(1).strip() if numberGroup else ""
+                       prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
+                       prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+
+                       fromGroups = self._smsFromRegex.finditer(messageHtml)
+                       fromParts = (group.group(1).strip() for group in fromGroups)
+                       textGroups = self._smsTextRegex.finditer(messageHtml)
+                       textParts = (group.group(1).strip() for group in textGroups)
+                       timeGroups = self._smsTimeRegex.finditer(messageHtml)
+                       timeParts = (group.group(1).strip() for group in timeGroups)
+
+                       messageParts = itertools.izip(fromParts, textParts, timeParts)
+
+                       yield {
+                               "id": messageId.strip(),
+                               "name": name,
+                               "time": exactTime,
+                               "relTime": relativeTime,
+                               "prettyNumber": prettyNumber,
+                               "number": number,
+                               "messageParts": messageParts,
+                       }
+
+       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 test_backend(username, password):
        import pprint
@@ -562,7 +635,7 @@ def test_backend(username, password):
        print "Authenticated: ", backend.is_authed()
        print "Login?: ", backend.login(username, password)
        print "Authenticated: ", backend.is_authed()
-       print "Token: ", backend._token
+       # print "Token: ", backend._token
        print "Account: ", backend.get_account_number()
        print "Callback: ", backend.get_callback_number()
        # print "All Callback: ",