X-Git-Url: http://git.maemo.org/git/?a=blobdiff_plain;f=src%2Fgv_backend.py;h=44adba15d75eec3f21e084bf1d57fa6780b2ec6f;hb=e562e0258a25db172f35ebd354d08164e61b4d58;hp=c92412efaa9c340fe286af0c0de9cf54bb096479;hpb=54a02afe3b7124a13ce342901c37be130022c580;p=gc-dialer
diff --git a/src/gv_backend.py b/src/gv_backend.py
index c92412e..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,24 +386,60 @@ 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)
- print page
tokenGroup = self._tokenRe.search(page)
if tokenGroup is None:
@@ -395,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*