# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""
-Grandcentral backend code
+Google Voice backend code
+
+Resources
+ http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
+ http://posttopic.com/topic/google-voice-add-on-development
"""
import urllib2
import time
import warnings
+import traceback
+
+from xml.etree import ElementTree
from browser_emu import MozillaEmulator
-import socket
+try:
+ import simplejson
+except ImportError:
+ simplejson = None
+
+
+_TRUE_REGEX = re.compile("true")
+_FALSE_REGEX = re.compile("false")
+
+def safe_eval(s):
+ s = _TRUE_REGEX.sub("True", s)
+ s = _FALSE_REGEX.sub("False", s)
+ return eval(s, {}, {})
-socket.setdefaulttimeout(5)
+if simplejson is None:
+ def parse_json(flattened):
+ return safe_eval(flattened)
+else:
+ def parse_json(flattened):
+ return simplejson.loads(flattened)
-class GCDialer(object):
+
+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
"""
- _gcDialingStrRe = re.compile("This may take a few seconds", re.M)
- _accessTokenRe = re.compile(r"""<input type="hidden" name="a_t" [^>]*value="(.*)"/>""")
- _isLoginPageRe = re.compile(r"""<form method="post" action="https://www.grandcentral.com/mobile/account/login">""")
- _callbackRe = re.compile(r"""name="default_number" value="(\d+)" />\s+(.*)\s$""", re.M)
- _accountNumRe = re.compile(r"""<img src="/images/mobile/inbox_logo.gif" alt="GrandCentral" />\s*(.{14})\s* """, re.M)
- _inboxRe = re.compile(r"""<td>.*?(voicemail|received|missed|call return).*?</td>\s+<td>\s+<font size="2">\s+(.*?)\s+ \| \s+<a href="/mobile/contacts/.*?">(.*?)\s?</a>\s+<br/>\s+(.*?)\s?<a href=""", re.S)
- _contactsRe = re.compile(r"""<a href="/mobile/contacts/detail/(\d+)">(.*?)</a>""", re.S)
- _contactsNextRe = re.compile(r""".*<a href="/mobile/contacts(\?page=\d+)">Next</a>""", re.S)
- _contactDetailGroupRe = re.compile(r"""Group:\s*(\w*)""", re.S)
- _contactDetailPhoneRe = re.compile(r"""(\w+):[0-9\-\(\) \t]*?<a href="/mobile/calls/click_to_call\?destno=(\d+).*?">call</a>""", re.S)
-
+ _isNotLoginPageRe = re.compile(r"""I cannot access my account""")
+ _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
+ _accountNumRe = re.compile(r"""<b class="ms2">(.{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"
+ _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"
- _forwardselectURL = "http://www.grandcentral.com/mobile/settings/forwarding_select"
- _loginURL = "https://www.grandcentral.com/mobile/account/login"
- _setforwardURL = "http://www.grandcentral.com/mobile/settings/set_forwarding?from=settings"
- _clicktocallURL = "http://www.grandcentral.com/mobile/calls/click_to_call?a_t=%s&destno=%s"
- _inboxallURL = "http://www.grandcentral.com/mobile/messages/inbox?types=all"
- _contactsURL = "http://www.grandcentral.com/mobile/contacts"
- _contactDetailURL = "http://www.grandcentral.com/mobile/contacts/detail"
+ _inboxURL = "https://www.google.com/voice/inbox/"
+ _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 = MozillaEmulator(None, 0)
if cookieFile is None:
- cookieFile = os.path.join(os.path.expanduser("~"), ".gc_cookies.txt")
+ cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
self._browser.cookies.filename = cookieFile
if os.path.isfile(cookieFile):
self._browser.cookies.load()
- self._accessToken = None
self._accountNum = None
- self._callbackNumbers = {}
self._lastAuthed = 0.0
+ self._token = ""
+ self._callbackNumber = ""
+ self._callbackNumbers = {}
self.__contacts = None
def is_authed(self, force = False):
"""
- Attempts to detect a current session and pull the auth token ( a_t ) from the page.
+ Attempts to detect a current session
@note Once logged in try not to reauth more than once a minute.
@returns If authenticated
"""
return True
try:
- forwardSelectionPage = self._browser.download(GCDialer._forwardselectURL)
+ inboxPage = self._browser.download(self._inboxURL)
except urllib2.URLError, e:
- raise RuntimeError("%s is not accesible" % GCDialer._clicktocallURL)
+ warnings.warn(traceback.format_exc())
+ raise RuntimeError("%s is not accesible" % self._inboxURL)
self._browser.cookies.save()
- if GCDialer._isLoginPageRe.search(forwardSelectionPage) is None:
- self._grab_token(forwardSelectionPage)
- self._lastAuthed = time.time()
- return True
+ if self._isNotLoginPageRe.search(inboxPage) is not None:
+ return False
- return False
+ self._grab_account_info()
+ self._lastAuthed = time.time()
+ return True
def login(self, username, password):
"""
if self.is_authed():
return True
- loginPostData = urllib.urlencode( {'username' : username , 'password' : password } )
+ loginPostData = urllib.urlencode({
+ 'Email' : username,
+ 'Passwd' : password,
+ 'service': "grandcentral",
+ })
try:
- loginSuccessOrFailurePage = self._browser.download(GCDialer._loginURL, loginPostData)
+ loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
except urllib2.URLError, e:
- raise RuntimeError("%s is not accesible" % GCDialer._clicktocallURL)
+ warnings.warn(traceback.format_exc())
+ raise RuntimeError("%s is not accesible" % self._loginURL)
return self.is_authed()
"""
This is the main function responsible for initating the callback
"""
- # If the number is not valid throw exception
if not self.is_valid_syntax(number):
- raise ValueError('number is not valid')
-
- # No point if we don't have the magic cookie
- if not self.is_authed():
+ raise ValueError('Number is not valid: "%s"' % number)
+ elif not self.is_authed():
raise RuntimeError("Not Authenticated")
- # Strip leading 1 from 11 digit dialing
if len(number) == 11 and number[0] == 1:
+ # Strip leading 1 from 11 digit dialing
number = number[1:]
try:
- callSuccessPage = self._browser.download(
- GCDialer._clicktocallURL % (self._accessToken, number),
- None,
- {'Referer' : 'http://www.grandcentral.com/mobile/messages'}
- )
+ clickToCallData = urllib.urlencode({
+ "number": number,
+ "phone": self._callbackNumber,
+ "_rnr_se": self._token,
+ })
+ otherData = {
+ 'Referer' : 'https://google.com/voice/m/callsms',
+ }
+ callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
except urllib2.URLError, e:
- raise RuntimeError("%s is not accesible" % GCDialer._clicktocallURL)
+ warnings.warn(traceback.format_exc())
+ raise RuntimeError("%s is not accesible" % self._clicktocallURL)
- if GCDialer._gcDialingStrRe.search(callSuccessPage) is None:
- raise RuntimeError("Grand Central returned an error")
+ if self._gvDialingStrRe.search(callSuccessPage) is None:
+ raise RuntimeError("Google Voice returned an error")
return True
numbers = self.get_callback_numbers()
for number, description in numbers.iteritems():
- if not re.compile(r"""1747""").match(number) is None:
+ if re.compile(r"""1747""").match(number) is not None:
self.set_callback_number(number)
return
for number, description in numbers.iteritems():
- if not re.compile(r"""gizmo""", re.I).search(description) is None:
+ if re.compile(r"""gizmo""", re.I).search(description) is not None:
self.set_callback_number(number)
return
for number, description in numbers.iteritems():
- if not re.compile(r"""computer""", re.I).search(description) is None:
+ if re.compile(r"""computer""", re.I).search(description) is not None:
self.set_callback_number(number)
return
Set the number that grandcental calls
@param callbacknumber should be a proper 10 digit number
"""
+ self._callbackNumber = callbacknumber
callbackPostData = urllib.urlencode({
- 'a_t': self._accessToken,
- 'default_number': callbacknumber
+ '_rnr_se': self._token,
+ 'phone': callbacknumber
})
try:
- callbackSetPage = self._browser.download(GCDialer._setforwardURL, callbackPostData)
+ callbackSetPage = self._browser.download(self._setforwardURL, callbackPostData)
except urllib2.URLError, e:
- raise RuntimeError("%s is not accesible" % GCDialer._clicktocallURL)
+ warnings.warn(traceback.format_exc())
+ raise RuntimeError("%s is not accesible" % self._setforwardURL)
self._browser.cookies.save()
return True
"""
@returns Current callback number or None
"""
- for c in self._browser.cookies:
- if c.name == "pda_forwarding_number":
- return c.value
- return None
+ return self._callbackNumber
def get_recent(self):
"""
@returns Iterable of (personsName, phoneNumber, date, action)
"""
- try:
- recentCallsPage = self._browser.download(GCDialer._inboxallURL)
- except urllib2.URLError, e:
- raise RuntimeError("%s is not accesible" % GCDialer._clicktocallURL)
-
- for match in self._inboxRe.finditer(recentCallsPage):
- phoneNumber = match.group(4)
- action = match.group(1)
- date = match.group(2)
- personsName = match.group(3)
- yield 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"
+ ))
+ yield "", number, date, action
def get_addressbooks(self):
"""
@staticmethod
def contact_source_short_name(contactId):
- return "GC"
+ return "GV"
@staticmethod
def factory_name():
- return "Grand Central"
+ return "Google Voice"
def get_contacts(self):
"""
if self.__contacts is None:
self.__contacts = []
- contactsPagesUrls = [GCDialer._contactsURL]
+ contactsPagesUrls = [self._contactsURL]
for contactsPageUrl in contactsPagesUrls:
try:
contactsPage = self._browser.download(contactsPageUrl)
except urllib2.URLError, e:
- raise RuntimeError("%s is not accesible" % GCDialer._clicktocallURL)
+ warnings.warn(traceback.format_exc())
+ raise RuntimeError("%s is not accesible" % self._clicktocallURL)
for contact_match in self._contactsRe.finditer(contactsPage):
contactId = contact_match.group(1)
contactName = contact_match.group(2)
@returns Iterable of (Phone Type, Phone Number)
"""
try:
- detailPage = self._browser.download(GCDialer._contactDetailURL + '/' + contactId)
+ detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
except urllib2.URLError, e:
- raise RuntimeError("%s is not accesible" % GCDialer._clicktocallURL)
+ warnings.warn(traceback.format_exc())
+ raise RuntimeError("%s is not accesible" % self._clicktocallURL)
for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
- phoneType = detail_match.group(1)
- phoneNumber = detail_match.group(2)
+ phoneNumber = detail_match.group(1)
+ phoneType = detail_match.group(2)
yield (phoneType, phoneNumber)
- def _grab_token(self, data):
- "Pull the magic cookie from the datastream"
- atGroup = GCDialer._accessTokenRe.search(data)
- self._accessToken = atGroup.group(1)
-
- anGroup = GCDialer._accountNumRe.search(data)
+ def _grab_json(self, url):
+ flatXml = self._browser.download(url)
+ xmlTree = ElementTree.fromstring(flatXml)
+ jsonElement = xmlTree.getchildren()[0]
+ flatJson = jsonElement.text
+ jsonTree = parse_json(flatJson)
+ return jsonTree
+
+ def _grab_account_info(self, accountNumberPage = None):
+ if accountNumberPage is None:
+ accountNumberPage = self._browser.download(self._accountNumberURL)
+
+ tokenGroup = self._tokenRe.search(accountNumberPage)
+ if tokenGroup is None:
+ raise RuntimeError("Could not extract authentication token from GrandCentral")
+ self._token = tokenGroup.group(1)
+
+ anGroup = self._accountNumRe.search(accountNumberPage)
+ if atGroup is None:
+ raise RuntimeError("Could not extract account number from GrandCentral")
self._accountNum = anGroup.group(1)
+ callbackPage = self._browser.download(self._forwardURL)
self._callbackNumbers = {}
- for match in GCDialer._callbackRe.finditer(data):
- self._callbackNumbers[match.group(1)] = match.group(2)
+ for match in self._callbackRe.finditer(callbackPage):
+ self._callbackNumbers[match.group(2)] = match.group(1)
+
+ if len(self._callbackNumber) == 0:
+ self.set_sane_callback()
+
+
+def test_backend(username, password):
+ import pprint
+ backend = GVDialer()
+ print "Authenticated: ", backend.is_authed()
+ print "Login?: ", backend.login(username, password)
+ print "Authenticated: ", backend.is_authed()
+ 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 "Recent: ",
+ # pprint.pprint(list(backend.get_recent()))
+ # print "Contacts: ",
+ # for contact in backend.get_contacts():
+ # print contact
+ # pprint.pprint(list(backend.get_contact_details(contact[0])))
+
+ return backend