X-Git-Url: http://git.maemo.org/git/?a=blobdiff_plain;f=src%2Fgv_backend.py;h=44adba15d75eec3f21e084bf1d57fa6780b2ec6f;hb=e562e0258a25db172f35ebd354d08164e61b4d58;hp=5040ad6440ec80b20c9c8a585790cf0984eacc7d;hpb=0fa550ea4bef7d41c611932071f755a95134661b;p=gc-dialer diff --git a/src/gv_backend.py b/src/gv_backend.py index 5040ad6..44adba1 100644 --- a/src/gv_backend.py +++ b/src/gv_backend.py @@ -1,23 +1,23 @@ #!/usr/bin/python -# DialCentral - Front end for Google's Grand Central service. -# Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - """ +DialCentral - Front end for Google's Grand Central service. +Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Google Voice backend code Resources @@ -31,6 +31,8 @@ import re import urllib import urllib2 import time +import datetime +import itertools import warnings import traceback from xml.sax import saxutils @@ -63,36 +65,45 @@ else: return simplejson.loads(flattened) +def itergroup(iterator, count, padValue = None): + """ + Iterate in groups of 'count' values. If there + aren't enough values, the last result is padded with + None. + + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3): + ... print list(val) + [1, 2, 3] + [4, 5, 6] + >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3): + ... print tuple(val) + (1, 2, 3) + (4, 5, 6) + (7, None, None) + >>> for val in itergroup("123456", 3): + ... print tuple(val) + ('1', '2', '3') + ('4', '5', '6') + >>> for val in itergroup("123456", 3): + ... print repr("".join(val)) + '123' + '456' + """ + paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1)) + nIterators = (paddedIterator, ) * count + return itertools.izip(*nIterators) + + class GVDialer(object): """ This class encapsulates all of the knowledge necessary to interace with the grandcentral servers the functions include login, setting up a callback number, and initalting a callback """ - _tokenRe = re.compile(r"""""") - _accountNumRe = re.compile(r"""(.{14})""") - _callbackRe = re.compile(r"""\s+(.*?):\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"""(.*?)""", re.S) - _contactsNextRe = re.compile(r""".*Next.*?""", re.S) - _contactDetailPhoneRe = re.compile(r"""([0-9\-\(\) \t]+?)\((\w+)\)""", re.S) - - _clicktocallURL = "https://www.google.com/voice/m/sendcall" - _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/" - 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) @@ -130,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 @@ -162,19 +175,14 @@ 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 """ - if not self.is_valid_syntax(number): - raise ValueError('Number is not valid: "%s"' % number) - elif not self.is_authed(): - raise RuntimeError("Not Authenticated") - - if len(number) == 11 and number[0] == 1: - # Strip leading 1 from 11 digit dialing - number = number[1:] - + number = self._send_validation(number) try: clickToCallData = urllib.urlencode({ "number": number, @@ -194,9 +202,33 @@ 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: + smsData = urllib.urlencode({ + "number": number, + "smstext": message, + "_rnr_se": self._token, + "id": "undefined", + "c": "undefined", + }) + otherData = { + 'Referer' : 'https://google.com/voice/m/sms', + } + 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._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 ) @@ -248,6 +280,8 @@ class GVDialer(object): return {} + _setforwardURL = "https://www.google.com//voice/m/setphone" + def set_callback_number(self, callbacknumber): """ Set the number that grandcental calls @@ -280,31 +314,16 @@ class GVDialer(object): def get_recent(self): """ + @todo Sort this stuff @returns Iterable of (personsName, phoneNumber, date, action) """ - for url in ( - self._receivedCallsURL, - self._missedCallsURL, - self._placedCallsURL, - ): - try: - allRecentData = self._grab_json(url) - except urllib2.URLError, e: - warnings.warn(traceback.format_exc()) - raise RuntimeError("%s is not accesible" % self._clicktocallURL) - - for recentCallData in allRecentData["messages"].itervalues(): - number = recentCallData["displayNumber"] - date = recentCallData["relativeStartTime"] - action = ", ".join(( - label.title() - for label in recentCallData["labels"] - if label.lower() != "all" and label.lower() != "inbox" - )) - number = saxutils.unescape(number) - date = saxutils.unescape(date) - action = saxutils.unescape(action) - yield "", number, 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 def get_addressbooks(self): """ @@ -323,6 +342,10 @@ class GVDialer(object): def factory_name(): return "Google Voice" + _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,7 +359,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)) @@ -352,6 +375,9 @@ class GVDialer(object): for contact in self.__contacts: yield contact + _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) @@ -360,21 +386,58 @@ 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) - def _grab_json(self, url): - flatXml = self._browser.download(url) + _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) + + allMessages = itertools.chain(decoratedVoicemails, decoratedSms) + sortedMessages = list(allMessages) + for exactDate, header, number, relativeDate, message in sortedMessages: + yield header, number, relativeDate, message + + def _grab_json(self, flatXml): xmlTree = ElementTree.fromstring(flatXml) jsonElement = xmlTree.getchildren()[0] flatJson = jsonElement.text jsonTree = parse_json(flatJson) return jsonTree + def _grab_html(self, flatXml): + xmlTree = ElementTree.fromstring(flatXml) + htmlElement = xmlTree.getchildren()[1] + 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 = self._browser.download(self._forwardURL) @@ -394,6 +457,159 @@ class GVDialer(object): callbackName = match.group(1) self._callbackNumbers[callbackNumber] = callbackName + 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") + + if len(number) == 11 and number[0] == 1: + # Strip leading 1 from 11 digit dialing + 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, + ): + try: + flatXml = self._browser.download(url) + except urllib2.URLError, e: + warnings.warn(traceback.format_exc()) + raise RuntimeError("%s is not accesible" % url) + + 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 + + _seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) + _exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + _relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + _voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) + _prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) + _voicemailLocationRegex = re.compile(r"""(.*?)""", re.MULTILINE) + _voicemailMessageRegex = re.compile(r"""(.*?)""", re.MULTILINE) + + def _parse_voicemail(self, voicemailHtml): + splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml) + for id, messageHtml in itergroup(splitVoicemail[1:], 2): + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + locationGroup = self._voicemailLocationRegex.search(messageHtml) + location = locationGroup.group(1).strip() if locationGroup 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 "" + + messageGroups = self._voicemailMessageRegex.finditer(messageHtml) + messageParts = ( + (group.group(1).strip(), group.group(2).strip()) + for group in messageGroups + ) if messageGroups else () + + yield { + "id": id.strip(), + "time": exactTime, + "relTime": relativeTime, + "prettyNumber": prettyNumber, + "number": number, + "location": location, + "messageParts": messageParts, + } + + def _decorate_voicemail(self, parsedVoicemail): + messagePartFormat = { + "med1": "%s", + "med2": "%s", + "high": "%s", + } + for voicemailData in parsedVoicemail: + exactTime = voicemailData["time"] # @todo Parse This + header = "%s %s" % (voicemailData["prettyNumber"], voicemailData["location"]) + 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 + + _smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + _smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + _smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + + def _parse_sms(self, smsHtml): + splitSms = self._seperateVoicemailsRegex.split(smsHtml) + for id, messageHtml in itergroup(splitSms[1:], 2): + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup 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": id.strip(), + "time": exactTime, + "relTime": relativeTime, + "prettyNumber": prettyNumber, + "number": number, + "messageParts": messageParts, + } + + def _decorate_sms(self, parsedSms): + for messageData in parsedSms: + exactTime = messageData["time"] # @todo Parse This + header = "%s" % (messageData["prettyNumber"]) + number = messageData["number"] + relativeTime = messageData["relTime"] + message = "\n".join(( + "%s (%s): %s" % messagePart + for messagePart in messageData["messageParts"] + )) + if not message: + message = "No Transcription" + yield exactTime, header, number, relativeTime, message + def test_backend(username, password): import pprint @@ -404,8 +620,8 @@ def test_backend(username, password): print "Token: ", backend._token print "Account: ", backend.get_account_number() print "Callback: ", backend.get_callback_number() - print "All Callback: ", - pprint.pprint(backend.get_callback_numbers()) + # print "All Callback: ", + # pprint.pprint(backend.get_callback_numbers()) # print "Recent: ", # pprint.pprint(list(backend.get_recent())) # print "Contacts: ",