X-Git-Url: http://git.maemo.org/git/?p=theonering;a=blobdiff_plain;f=src%2Fgvoice%2Fbackend.py;h=9bada991aac332f53e5621d9dd603ae828af3bff;hp=41d66a88e5bbb1e2d55eab05050878318643bd07;hb=58f796243ce76a2629f169b943d2e0f89a877999;hpb=1e6c9bca48542c422f117ddfd4a4e119af6086cc;ds=sidebyside diff --git a/src/gvoice/backend.py b/src/gvoice/backend.py index 41d66a8..9bada99 100755 --- a/src/gvoice/backend.py +++ b/src/gvoice/backend.py @@ -25,6 +25,7 @@ Resources http://posttopic.com/topic/google-voice-add-on-development """ +from __future__ import with_statement import os import re @@ -38,20 +39,20 @@ from xml.sax import saxutils from xml.etree import ElementTree -import browser_emu - try: import simplejson except ImportError: simplejson = None +import browser_emu + _moduleLogger = logging.getLogger("gvoice.backend") -_TRUE_REGEX = re.compile("true") -_FALSE_REGEX = re.compile("false") def safe_eval(s): + _TRUE_REGEX = re.compile("true") + _FALSE_REGEX = re.compile("false") s = _TRUE_REGEX.sub("True", s) s = _FALSE_REGEX.sub("False", s) return eval(s, {}, {}) @@ -123,6 +124,48 @@ class GVoiceBackend(object): self._callbackNumber = "" self._callbackNumbers = {} + # Suprisingly, moving all of these from class to self sped up startup time + + self._validateRe = re.compile("^[0-9]{10,}$") + + self._forwardURL = "https://www.google.com/voice/mobile/phones" + self._tokenURL = "http://www.google.com/voice/m" + self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth" + self._galxRe = re.compile(r"""""", re.MULTILINE | re.DOTALL) + self._tokenRe = re.compile(r"""""") + self._accountNumRe = re.compile(r"""(.{14})""") + self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)\s*$""", re.M) + + self._gvDialingStrRe = re.compile("This may take a few seconds", re.M) + self._clicktocallURL = "https://www.google.com/voice/m/sendcall" + self._sendSmsURL = "https://www.google.com/voice/m/sendsms" + + self._recentCallsURL = "https://www.google.com/voice/inbox/recent/" + self._placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/" + self._receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/" + self._missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/" + + self._contactsRe = re.compile(r"""(.*?)""", re.S) + self._contactsNextRe = re.compile(r""".*Next.*?""", re.S) + self._contactsURL = "https://www.google.com/voice/mobile/contacts" + self._contactDetailPhoneRe = re.compile(r"""([0-9+\-\(\) \t]+?)\((\w+)\)""", re.S) + self._contactDetailURL = "https://www.google.com/voice/mobile/contact" + + self._voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/" + self._smsURL = "https://www.google.com/voice/inbox/recent/sms/" + self._seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) + self._exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) + self._prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) + self._messagesContactID = re.compile(r""".*?\s*?(.*?)""", re.MULTILINE) + self._voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) + self._smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + def is_authed(self, force = False): """ Attempts to detect a current session @@ -143,15 +186,7 @@ class GVoiceBackend(object): self._lastAuthed = time.time() return True - _tokenURL = "http://www.google.com/voice/m" - _loginURL = "https://www.google.com/accounts/ServiceLoginAuth" - _galxRe = re.compile(r"""""", re.MULTILINE | re.DOTALL) - - def login(self, username, password): - """ - Attempt to login to GoogleVoice - @returns Whether login was successful or not - """ + def _get_token(self): try: tokenPage = self._browser.download(self._tokenURL) except urllib2.URLError, e: @@ -163,7 +198,9 @@ class GVoiceBackend(object): else: galxToken = "" _moduleLogger.debug("Could not grab GALX token") + return galxToken + def _login(self, username, password, token): loginPostData = urllib.urlencode({ 'Email' : username, 'Passwd' : password, @@ -171,7 +208,7 @@ class GVoiceBackend(object): "ltmpl": "mobile", "btmpl": "mobile", "PersistentCookie": "yes", - "GALX": galxToken, + "GALX": token, "continue": self._forwardURL, }) @@ -180,12 +217,27 @@ class GVoiceBackend(object): except urllib2.URLError, e: _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) try: self._grab_account_info(loginSuccessOrFailurePage) except Exception, e: - _moduleLogger.exception(str(e)) - return False + # 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() @@ -196,9 +248,6 @@ class GVoiceBackend(object): self._browser.cookies.clear() self._browser.cookies.save() - _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 @@ -223,8 +272,6 @@ class GVoiceBackend(object): return True - _sendSmsURL = "https://www.google.com/voice/m/sendsms" - def send_sms(self, number, message): number = self._send_validation(number) try: @@ -245,8 +292,6 @@ class GVoiceBackend(object): return True - _validateRe = re.compile("^[0-9]{10,}$") - def is_valid_syntax(self, number): """ @returns If This number be called ( syntax validation only ) @@ -268,8 +313,6 @@ class GVoiceBackend(object): return {} return self._callbackNumbers - _setforwardURL = "https://www.google.com//voice/m/setphone" - def set_callback_number(self, callbacknumber): """ Set the number that GoogleVoice calls @@ -284,11 +327,6 @@ class GVoiceBackend(object): """ return self._callbackNumber - _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) @@ -310,10 +348,6 @@ class GVoiceBackend(object): recentCallData["action"] = action yield recentCallData - _contactsRe = re.compile(r"""(.*?)""", re.S) - _contactsNextRe = re.compile(r""".*Next.*?""", re.S) - _contactsURL = "https://www.google.com/voice/mobile/contacts" - def get_contacts(self): """ @returns Iterable of (contact id, contact name) @@ -336,9 +370,6 @@ class GVoiceBackend(object): newContactsPageUrl = self._contactsURL + next_match.group(1) contactsPagesUrls.append(newContactsPageUrl) - _contactDetailPhoneRe = re.compile(r"""([0-9+\-\(\) \t]+?)\((\w+)\)""", re.S) - _contactDetailURL = "https://www.google.com/voice/mobile/contact" - def get_contact_details(self, contactId): """ @returns Iterable of (Phone Type, Phone Number) @@ -354,9 +385,6 @@ class GVoiceBackend(object): 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) @@ -364,8 +392,10 @@ class GVoiceBackend(object): _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) @@ -373,8 +403,10 @@ class GVoiceBackend(object): _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) return allMessages @@ -392,11 +424,6 @@ class GVoiceBackend(object): flatHtml = htmlElement.text return flatHtml - _tokenRe = re.compile(r"""""") - _accountNumRe = re.compile(r"""(.{14})
""") - _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)\s*$""", re.M) - _forwardURL = "https://www.google.com/voice/mobile/phones" - def _grab_account_info(self, page): tokenGroup = self._tokenRe.search(page) if tokenGroup is None: @@ -428,18 +455,6 @@ class GVoiceBackend(object): number = number[1:] return number - _seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) - _exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) - _relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) - _voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - _voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) - _prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) - _voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) - _messagesContactID = re.compile(r""".*?\s*?(.*?)""", re.MULTILINE) - #_voicemailMessageRegex = re.compile(r"""(.*?)""", re.MULTILINE) - #_voicemailMessageRegex = re.compile(r"""(.*?)""", re.MULTILINE) - _voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) - @staticmethod def _interpret_voicemail_regex(group): quality, content, number = group.group(2), group.group(3), group.group(4) @@ -484,6 +499,7 @@ class GVoiceBackend(object): "number": number, "location": location, "messageParts": messageParts, + "type": "Voicemail", } def _decorate_voicemail(self, parsedVoicemails): @@ -504,10 +520,6 @@ class GVoiceBackend(object): voicemailData["messageParts"] = ((whoFrom, message, when), ) yield voicemailData - _smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - _smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - _smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - def _parse_sms(self, smsHtml): splitSms = self._seperateVoicemailsRegex.split(smsHtml) for messageId, messageHtml in itergroup(splitSms[1:], 2): @@ -545,11 +557,23 @@ class GVoiceBackend(object): "number": number, "location": "", "messageParts": messageParts, + "type": "Texts", } def _decorate_sms(self, parsedTexts): return parsedTexts + @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"] + message["isArchived"] = "inbox" not in jsonItem["labels"] + yield message + def set_sane_callback(backend): """ @@ -673,7 +697,83 @@ def test_backend(username, password): return backend +def grab_debug_info(username, password): + cookieFile = os.path.join(".", "raw_cookies.txt") + try: + os.remove(cookieFile) + except OSError: + pass + + backend = GVoiceBackend(cookieFile) + browser = backend._browser + + _TEST_WEBPAGES = [ + ("forward", backend._forwardURL), + ("token", backend._tokenURL), + ("login", backend._loginURL), + ("contacts", backend._contactsURL), + + ("voicemail", backend._voicemailURL), + ("sms", backend._smsURL), + + ("recent", backend._recentCallsURL), + ("placed", backend._placedCallsURL), + ("recieved", backend._receivedCallsURL), + ("missed", backend._missedCallsURL), + ] + + # 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]) + #test_backend(sys.argv[1], sys.argv[2]) + grab_debug_info(sys.argv[1], sys.argv[2])