X-Git-Url: http://vcs.maemo.org/git/?a=blobdiff_plain;f=src%2Fbackends%2Fgvoice%2Fgvoice.py;h=f73600e1bc59f76f928ddb418378500fd5d8f350;hb=0ba0b153f9787ab64daead531eb0a71d1ecfbe14;hp=354e3e304430d6039e6836188171dfc7bb16ee47;hpb=175e3375296a2fb0d3faa4ef8f89a014b55ba5ec;p=gc-dialer diff --git a/src/backends/gvoice/gvoice.py b/src/backends/gvoice/gvoice.py index 354e3e3..f73600e 100755 --- a/src/backends/gvoice/gvoice.py +++ b/src/backends/gvoice/gvoice.py @@ -87,7 +87,7 @@ class Message(object): return "%s (%s): %s" % ( self.whoFrom, self.when, - "".join(str(part) for part in self.body) + "".join(unicode(part) for part in self.body) ) def to_dict(self): @@ -170,7 +170,6 @@ class GVoiceBackend(object): SECURE_URL_BASE = "https://www.google.com/voice/" SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/" - self._forwardURL = SECURE_MOBILE_URL_BASE + "phones" self._tokenURL = SECURE_URL_BASE + "m" self._callUrl = SECURE_URL_BASE + "call/connect" self._callCancelURL = SECURE_URL_BASE + "call/cancel" @@ -188,6 +187,8 @@ class GVoiceBackend(object): self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/" # HACK really this redirects to the main pge and we are grabbing some javascript self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact" + self._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export" + self._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user" self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/" self.XML_FEEDS = ( @@ -201,17 +202,15 @@ class GVoiceBackend(object): self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash" self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/" self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/" + self._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/" + self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread" self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/" self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/" self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/" self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/" 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._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL) self._seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) self._exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) self._relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) @@ -227,32 +226,21 @@ class GVoiceBackend(object): def is_quick_login_possible(self): """ - @returns True then is_authed might be enough to login, else full login is required + @returns True then refresh_account_info might be enough to login, else full login is required """ return self._loadedFromCookies or 0.0 < self._lastAuthed - def is_authed(self, force = False): - """ - Attempts to detect a current session - @note Once logged in try not to reauth more than once a minute. - @returns If authenticated - @blocks - """ - isRecentledAuthed = (time.time() - self._lastAuthed) < 120 - isPreviouslyAuthed = self._token is not None - if isRecentledAuthed and isPreviouslyAuthed and not force: - return True - + def refresh_account_info(self): try: - page = self._get_page(self._forwardURL) - self._grab_account_info(page) + page = self._get_page(self._JSON_CONTACTS_URL) + accountData = self._grab_account_info(page) except Exception, e: _moduleLogger.exception(str(e)) - return False + return None self._browser.save_cookies() self._lastAuthed = time.time() - return True + return accountData def _get_token(self): tokenPage = self._get_page(self._tokenURL) @@ -274,7 +262,7 @@ class GVoiceBackend(object): "btmpl": "mobile", "PersistentCookie": "yes", "GALX": token, - "continue": self._forwardURL, + "continue": self._JSON_CONTACTS_URL, } loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData) @@ -291,19 +279,19 @@ class GVoiceBackend(object): loginSuccessOrFailurePage = self._login(username, password, galxToken) try: - self._grab_account_info(loginSuccessOrFailurePage) + accountData = 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: + # luckily refresh_account_info does everything we need for a retry + accountData = self.refresh_account_info() + if accountData is None: _moduleLogger.exception(str(e)) - return False + return None _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this") self._browser.save_cookies() self._lastAuthed = time.time() - return True + return accountData def persist(self): self._browser.save_cookies() @@ -396,7 +384,7 @@ class GVoiceBackend(object): self._sendSmsURL, { 'phoneNumber': flattenedPhoneNumbers, - 'text': message + 'text': unicode(message).encode("utf-8"), }, ) self._parse_with_validation(page) @@ -426,20 +414,21 @@ class GVoiceBackend(object): return json - def download(self, messageId, adir): + def recording_url(self, messageId): + url = self._downloadVoicemailURL+messageId + return url + + def download(self, messageId, targetPath): """ Download a voicemail or recorded call MP3 matching the given ``msg`` which can either be a ``Message`` instance, or a SHA1 identifier. - Saves files to ``adir`` (defaults to current directory). Message hashes can be found in ``self.voicemail().messages`` for example. @returns location of saved file. @blocks """ - page = self._get_page(self._downloadVoicemailURL, {"id": messageId}) - fn = os.path.join(adir, '%s.mp3' % messageId) - with open(fn, 'wb') as fo: + page = self._get_page(self.recording_url(messageId)) + with open(targetPath, 'wb') as fo: fo.write(page) - return fn def is_valid_syntax(self, number): """ @@ -458,8 +447,6 @@ class GVoiceBackend(object): @returns a dictionary mapping call back numbers to descriptions @note These results are cached for 30 minutes. """ - if not self.is_authed(): - return {} return self._callbackNumbers def set_callback_number(self, callbacknumber): @@ -482,37 +469,25 @@ class GVoiceBackend(object): @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) @blocks """ - for action, url in ( - ("Received", self._XML_RECEIVED_URL), - ("Missed", self._XML_MISSED_URL), - ("Placed", self._XML_PLACED_URL), - ): - flatXml = self._get_page(url) - - allRecentHtml = self._grab_html(flatXml) - allRecentData = self._parse_history(allRecentHtml) - for recentCallData in allRecentData: - recentCallData["action"] = action - yield recentCallData + recentPages = [ + (action, self._get_page(url)) + for action, url in ( + ("Received", self._XML_RECEIVED_URL), + ("Missed", self._XML_MISSED_URL), + ("Placed", self._XML_PLACED_URL), + ) + ] + return self._parse_recent(recentPages) - def get_contacts(self): - """ - @returns Iterable of (contact id, contact name) - @blocks - """ - page = self._get_page(self._XML_CONTACTS_URL) - contactsBody = self._contactsBodyRe.search(page) - if contactsBody is None: - raise RuntimeError("Could not extract contact information") - accountData = _fake_parse_json(contactsBody.group(1)) - if accountData is None: - return - for contactId, contactDetails in accountData["contacts"].iteritems(): - # A zero contact id is the catch all for unknown contacts - if contactId != "0": - if "name" in contactDetails: - contactDetails["name"] = unescape(contactDetails["name"]) - yield contactId, contactDetails + def get_csv_contacts(self): + data = { + "groupToExport": "mine", + "exportType": "ALL", + "out": "OUTLOOK_CSV", + } + encodedData = urllib.urlencode(data) + contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData) + return contacts def get_voicemails(self): """ @@ -540,6 +515,12 @@ class GVoiceBackend(object): smss = self._merge_conversation_sources(parsedSms, smsJson) return smss + def get_unread_counts(self): + countPage = self._get_page(self._JSON_SMS_COUNT_URL) + counts = parse_json(countPage) + counts = counts["unreadCounts"] + return counts + def mark_message(self, messageId, asRead): """ @blocks @@ -575,32 +556,28 @@ class GVoiceBackend(object): return flatHtml def _grab_account_info(self, page): - tokenGroup = self._tokenRe.search(page) - if tokenGroup is None: - raise RuntimeError("Could not extract authentication token from GoogleVoice") - self._token = tokenGroup.group(1) - - anGroup = self._accountNumRe.search(page) - if anGroup is not None: - self._accountNum = anGroup.group(1) - else: - _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 + accountData = parse_json(page) + self._token = accountData["r"] + self._accountNum = accountData["number"]["raw"] + for callback in accountData["phones"].itervalues(): + self._callbackNumbers[callback["phoneNumber"]] = callback["name"] if len(self._callbackNumbers) == 0: _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page) + return accountData def _send_validation(self, number): if not self.is_valid_syntax(number): raise ValueError('Number is not valid: "%s"' % number) - elif not self.is_authed(): - raise RuntimeError("Not Authenticated") return number + def _parse_recent(self, recentPages): + for action, flatXml in recentPages: + allRecentHtml = self._grab_html(flatXml) + allRecentData = self._parse_history(allRecentHtml) + for recentCallData in allRecentData: + recentCallData["action"] = action + yield recentCallData + def _parse_history(self, historyHtml): splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml) for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): @@ -677,7 +654,11 @@ class GVoiceBackend(object): message = Message() message.body = messageParts message.whoFrom = conv.name - message.when = conv.time.strftime("%I:%M %p") + try: + message.when = conv.time.strftime("%I:%M %p") + except ValueError: + _moduleLogger.exception("Confusing time provided: %r" % conv.time) + message.when = "Unknown" conv.messages = (message, ) yield conv @@ -767,9 +748,27 @@ class GVoiceBackend(object): def _parse_with_validation(self, page): json = parse_json(page) - validate_response(json) + self._validate_response(json) return json + def _validate_response(self, response): + """ + Validates that the JSON response is A-OK + """ + try: + assert response is not None, "Response not provided" + assert 'ok' in response, "Response lacks status" + assert response['ok'], "Response not good" + except AssertionError: + try: + if response["data"]["code"] == 20: + raise RuntimeError( +"""Ambiguous error 20 returned by Google Voice. +Please verify you have configured your callback number (currently "%s"). If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber) + except KeyError: + pass + raise RuntimeError('There was a problem with GV: %s' % response) + _UNESCAPE_ENTITIES = { """: '"', @@ -871,18 +870,6 @@ def extract_payload(flatXml): return jsonTree, flatHtml -def validate_response(response): - """ - Validates that the JSON response is A-OK - """ - try: - assert response is not None - assert 'ok' in response - assert response['ok'] - except AssertionError: - raise RuntimeError('There was a problem with GV: %s' % response) - - def guess_phone_type(number): if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"): return GVoiceBackend.PHONE_TYPE_GIZMO @@ -958,15 +945,18 @@ def grab_debug_info(username, password): browser = backend._browser _TEST_WEBPAGES = [ - ("forward", backend._forwardURL), ("token", backend._tokenURL), ("login", backend._loginURL), ("isdnd", backend._isDndURL), ("account", backend._XML_ACCOUNT_URL), - ("contacts", backend._XML_CONTACTS_URL), + ("html_contacts", backend._XML_CONTACTS_URL), + ("contacts", backend._JSON_CONTACTS_URL), + ("csv", backend._CSV_CONTACTS_URL), ("voicemail", backend._XML_VOICEMAIL_URL), - ("sms", backend._XML_SMS_URL), + ("html_sms", backend._XML_SMS_URL), + ("sms", backend._JSON_SMS_URL), + ("count", backend._JSON_SMS_COUNT_URL), ("recent", backend._XML_RECENT_URL), ("placed", backend._XML_PLACED_URL), @@ -997,8 +987,8 @@ def grab_debug_info(username, password): 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) + # luckily refresh_account_info does everything we need for a retry + loggedIn = backend.refresh_account_info() is not None if not loggedIn: raise @@ -1024,6 +1014,21 @@ def grab_debug_info(username, password): ) +def grab_voicemails(username, password): + cookieFile = os.path.join(".", "raw_cookies.txt") + try: + os.remove(cookieFile) + except OSError: + pass + + backend = GVoiceBackend(cookieFile) + backend.login(username, password) + voicemails = list(backend.get_voicemails()) + for voicemail in voicemails: + print voicemail.id + backend.download(voicemail.id, ".") + + def main(): import sys logging.basicConfig(level=logging.DEBUG) @@ -1033,6 +1038,7 @@ def main(): password = args[2] grab_debug_info(username, password) + grab_voicemails(username, password) if __name__ == "__main__":