http://posttopic.com/topic/google-voice-add-on-development
"""
+from __future__ import with_statement
import os
import re
import datetime
import itertools
import logging
-from xml.sax import saxutils
+import inspect
from xml.etree import ElementTree
-import browser_emu
-
try:
- import simplejson
+ import simplejson as _simplejson
+ simplejson = _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):
- s = _TRUE_REGEX.sub("True", s)
- s = _FALSE_REGEX.sub("False", s)
- return eval(s, {}, {})
+class NetworkError(RuntimeError):
+ pass
-if simplejson is None:
- def parse_json(flattened):
- return safe_eval(flattened)
-else:
- def parse_json(flattened):
- return simplejson.loads(flattened)
+class MessageText(object):
+ ACCURACY_LOW = "med1"
+ ACCURACY_MEDIUM = "med2"
+ ACCURACY_HIGH = "high"
-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.
+ def __init__(self):
+ self.accuracy = None
+ self.text = 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)
+ def __str__(self):
+ return self.text
+ def to_dict(self):
+ return to_dict(self)
+
+ def __eq__(self, other):
+ return self.accuracy == other.accuracy and self.text == other.text
+
+
+class Message(object):
+
+ def __init__(self):
+ self.whoFrom = None
+ self.body = None
+ self.when = None
+
+ def __str__(self):
+ return "%s (%s): %s" % (
+ self.whoFrom,
+ self.when,
+ "".join(str(part) for part in self.body)
+ )
+
+ def to_dict(self):
+ selfDict = to_dict(self)
+ selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
+ return selfDict
+
+ def __eq__(self, other):
+ return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
-class NetworkError(RuntimeError):
- pass
+
+class Conversation(object):
+
+ TYPE_VOICEMAIL = "Voicemail"
+ TYPE_SMS = "SMS"
+
+ def __init__(self):
+ self.type = None
+ self.id = None
+ self.contactId = None
+ self.name = None
+ self.location = None
+ self.prettyNumber = None
+ self.number = None
+
+ self.time = None
+ self.relTime = None
+ self.messages = None
+ self.isRead = None
+ self.isSpam = None
+ self.isTrash = None
+ self.isArchived = None
+
+ def __cmp__(self, other):
+ cmpValue = cmp(self.contactId, other.contactId)
+ if cmpValue != 0:
+ return cmpValue
+
+ cmpValue = cmp(self.time, other.time)
+ if cmpValue != 0:
+ return cmpValue
+
+ cmpValue = cmp(self.id, other.id)
+ if cmpValue != 0:
+ return cmpValue
+
+ def to_dict(self):
+ selfDict = to_dict(self)
+ selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
+ return selfDict
class GVoiceBackend(object):
the functions include login, setting up a callback number, and initalting a callback
"""
+ PHONE_TYPE_HOME = 1
+ PHONE_TYPE_MOBILE = 2
+ PHONE_TYPE_WORK = 3
+ PHONE_TYPE_GIZMO = 7
+
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)
- if cookieFile is None:
- 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._loadedFromCookies = self._browser.load_cookies(cookieFile)
self._token = ""
self._accountNum = ""
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._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
+
+ 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"
+ self._sendSmsURL = SECURE_URL_BASE + "sms/send"
+
+ self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
+ self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
+ self._setDndURL = "https://www.google.com/voice/m/savednd"
+
+ self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
+ self._markAsReadURL = SECURE_URL_BASE + "m/mark"
+ self._archiveMessageURL = SECURE_URL_BASE + "m/archive"
+
+ self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
+ 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._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
+
+ self.XML_FEEDS = (
+ 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
+ 'recorded', 'placed', 'received', 'missed'
+ )
+ self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
+ self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
+ self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
+ self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
+ 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._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"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
+ self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
+ self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
+ self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
+
+ self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
+ self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
+ self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
+ self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
+ self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
+ self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
+ self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
+ self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
+ self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
+ self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
+ self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+
+ def is_quick_login_possible(self):
+ """
+ @returns True then is_authed 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
"""
- if (time.time() - self._lastAuthed) < 120 and not force:
+ isRecentledAuthed = (time.time() - self._lastAuthed) < 120
+ isPreviouslyAuthed = self._token is not None
+ if isRecentledAuthed and isPreviouslyAuthed and not force:
return True
try:
- page = self._browser.download(self._forwardURL)
+ page = self._get_page(self._forwardURL)
self._grab_account_info(page)
except Exception, e:
_moduleLogger.exception(str(e))
return False
- self._browser.cookies.save()
+ self._browser.save_cookies()
self._lastAuthed = time.time()
return True
- _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
+ def _get_token(self):
+ tokenPage = self._get_page(self._tokenURL)
- def login(self, username, password):
- """
- Attempt to login to GoogleVoice
- @returns Whether login was successful or not
- """
- loginPostData = urllib.urlencode({
+ galxTokens = self._galxRe.search(tokenPage)
+ if galxTokens is not None:
+ galxToken = galxTokens.group(1)
+ else:
+ galxToken = ""
+ _moduleLogger.debug("Could not grab GALX token")
+ return galxToken
+
+ def _login(self, username, password, token):
+ loginData = {
'Email' : username,
'Passwd' : password,
'service': "grandcentral",
"ltmpl": "mobile",
"btmpl": "mobile",
"PersistentCookie": "yes",
+ "GALX": token,
"continue": self._forwardURL,
- })
+ }
- try:
- loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
- except urllib2.URLError, e:
- _moduleLogger.exception("Translating error: %s" % str(e))
- raise NetworkError("%s is not accesible" % self._loginURL)
+ loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
+ 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
-
- self._browser.cookies.save()
+ # 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.save_cookies()
self._lastAuthed = time.time()
return True
def logout(self):
+ self._browser.clear_cookies()
+ self._browser.save_cookies()
+ self._token = None
self._lastAuthed = 0.0
- 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 is_dnd(self):
+ isDndPage = self._get_page(self._isDndURL)
+
+ dndGroup = self._isDndRe.search(isDndPage)
+ if dndGroup is None:
+ return False
+ dndStatus = dndGroup.group(1)
+ isDnd = True if dndStatus.strip().lower() == "true" else False
+ return isDnd
+
+ def set_dnd(self, doNotDisturb):
+ dndPostData = {
+ "doNotDisturb": 1 if doNotDisturb else 0,
+ }
+
+ dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
- def dial(self, number):
+ def call(self, outgoingNumber):
"""
This is the main function responsible for initating the callback
"""
- number = self._send_validation(number)
- try:
- 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:
- _moduleLogger.exception("Translating error: %s" % str(e))
- raise NetworkError("%s is not accesible" % self._clicktocallURL)
-
- if self._gvDialingStrRe.search(callSuccessPage) is None:
- raise RuntimeError("Google Voice returned an error")
+ outgoingNumber = self._send_validation(outgoingNumber)
+ subscriberNumber = None
+ phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
+
+ callData = {
+ 'outgoingNumber': outgoingNumber,
+ 'forwardingNumber': self._callbackNumber,
+ 'subscriberNumber': subscriberNumber or 'undefined',
+ 'phoneType': str(phoneType),
+ 'remember': '1',
+ }
+ _moduleLogger.info("%r" % callData)
+ page = self._get_page_with_token(
+ self._callUrl,
+ callData,
+ )
+ self._parse_with_validation(page)
return True
- _sendSmsURL = "https://www.google.com/voice/m/sendsms"
+ def cancel(self, outgoingNumber=None):
+ """
+ Cancels a call matching outgoing and forwarding numbers (if given).
+ Will raise an error if no matching call is being placed
+ """
+ page = self._get_page_with_token(
+ self._callCancelURL,
+ {
+ 'outgoingNumber': outgoingNumber or 'undefined',
+ 'forwardingNumber': self._callbackNumber or 'undefined',
+ 'cancelType': 'C2C',
+ },
+ )
+ self._parse_with_validation(page)
+
+ def send_sms(self, phoneNumber, message):
+ phoneNumber = self._send_validation(phoneNumber)
+ page = self._get_page_with_token(
+ self._sendSmsURL,
+ {
+ 'phoneNumber': phoneNumber,
+ 'text': message
+ },
+ )
+ self._parse_with_validation(page)
+
+ def search(self, query):
+ """
+ Search your Google Voice Account history for calls, voicemails, and sms
+ Returns ``Folder`` instance containting matching messages
+ """
+ page = self._get_page(
+ self._XML_SEARCH_URL,
+ {"q": query},
+ )
+ json, html = extract_payload(page)
+ return json
- 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:
- _moduleLogger.exception("Translating error: %s" % str(e))
- raise NetworkError("%s is not accesible" % self._sendSmsURL)
+ def get_feed(self, feed):
+ actualFeed = "_XML_%s_URL" % feed.upper()
+ feedUrl = getattr(self, actualFeed)
- return True
+ page = self._get_page(feedUrl)
+ json, html = extract_payload(page)
- _validateRe = re.compile("^[0-9]{10,}$")
+ return json
+
+ def download(self, messageId, adir):
+ """
+ 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.
+ """
+ page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
+ fn = os.path.join(adir, '%s.mp3' % messageId)
+ with open(fn, 'wb') as fo:
+ fo.write(page)
+ return fn
def is_valid_syntax(self, number):
"""
return {}
return self._callbackNumbers
- _setforwardURL = "https://www.google.com//voice/m/setphone"
-
def set_callback_number(self, callbacknumber):
"""
Set the number that GoogleVoice calls
@param callbacknumber should be a proper 10 digit number
"""
self._callbackNumber = callbacknumber
+ _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
return True
def get_callback_number(self):
"""
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)
"""
for action, url in (
- ("Received", self._receivedCallsURL),
- ("Missed", self._missedCallsURL),
- ("Placed", self._placedCallsURL),
+ ("Received", self._XML_RECEIVED_URL),
+ ("Missed", self._XML_MISSED_URL),
+ ("Placed", self._XML_PLACED_URL),
):
- try:
- flatXml = self._browser.download(url)
- except urllib2.URLError, e:
- _moduleLogger.exception("Translating error: %s" % str(e))
- raise NetworkError("%s is not accesible" % url)
+ flatXml = self._get_page(url)
allRecentHtml = self._grab_html(flatXml)
- allRecentData = self._parse_voicemail(allRecentHtml)
+ allRecentData = self._parse_history(allRecentHtml)
for recentCallData in allRecentData:
recentCallData["action"] = action
yield recentCallData
- _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)
- _contactsURL = "https://www.google.com/voice/mobile/contacts"
-
def get_contacts(self):
"""
@returns Iterable of (contact id, contact name)
"""
- contactsPagesUrls = [self._contactsURL]
- for contactsPageUrl in contactsPagesUrls:
- try:
- contactsPage = self._browser.download(contactsPageUrl)
- except urllib2.URLError, e:
- _moduleLogger.exception("Translating error: %s" % str(e))
- raise NetworkError("%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))
- contact = contactId, contactName
- yield contact
-
- next_match = self._contactsNextRe.match(contactsPage)
- if next_match is not None:
- newContactsPageUrl = self._contactsURL + next_match.group(1)
- contactsPagesUrls.append(newContactsPageUrl)
-
- _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
- _contactDetailURL = "https://www.google.com/voice/mobile/contact"
-
- def get_contact_details(self, contactId):
- """
- @returns Iterable of (Phone Type, Phone Number)
- """
- try:
- detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
- except urllib2.URLError, e:
- _moduleLogger.exception("Translating error: %s" % str(e))
- raise NetworkError("%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)
-
- _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:
- _moduleLogger.exception("Translating error: %s" % str(e))
- raise NetworkError("%s is not accesible" % self._voicemailURL)
+ 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))
+ for contactId, contactDetails in accountData["contacts"].iteritems():
+ # A zero contact id is the catch all for unknown contacts
+ if contactId != "0":
+ yield contactId, contactDetails
+
+ def get_conversations(self):
+ voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
voicemailHtml = self._grab_html(voicemailPage)
+ voicemailJson = self._grab_json(voicemailPage)
parsedVoicemail = self._parse_voicemail(voicemailHtml)
- decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
+ voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
- try:
- smsPage = self._browser.download(self._smsURL)
- except urllib2.URLError, e:
- _moduleLogger.exception("Translating error: %s" % str(e))
- raise NetworkError("%s is not accesible" % self._smsURL)
+ smsPage = self._get_page(self._XML_SMS_URL)
smsHtml = self._grab_html(smsPage)
+ smsJson = self._grab_json(smsPage)
parsedSms = self._parse_sms(smsHtml)
- decoratedSms = self._decorate_sms(parsedSms)
+ smss = self._merge_conversation_sources(parsedSms, smsJson)
+
+ allConversations = itertools.chain(voicemails, smss)
+ return allConversations
+
+ def mark_message(self, messageId, asRead):
+ postData = {
+ "read": 1 if asRead else 0,
+ "id": messageId,
+ }
- allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
- return allMessages
+ markPage = self._get_page(self._markAsReadURL, postData)
+
+ def archive_message(self, messageId):
+ postData = {
+ "id": messageId,
+ }
+
+ markPage = self._get_page(self._archiveMessageURL, postData)
def _grab_json(self, flatXml):
xmlTree = ElementTree.fromstring(flatXml)
flatHtml = htmlElement.text
return flatHtml
- _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
- _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
- _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\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:
callbackNumber = match.group(2)
callbackName = match.group(1)
self._callbackNumbers[callbackNumber] = callbackName
+ if len(self._callbackNumbers) == 0:
+ _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
def _send_validation(self, number):
if not self.is_valid_syntax(number):
number = number[1:]
return number
- _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
- _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
- _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
- _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
- _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
- _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
- _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
- _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
- #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
- #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
- _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
-
- @staticmethod
- def _interpret_voicemail_regex(group):
- quality, content, number = group.group(2), group.group(3), group.group(4)
- if quality is not None and content is not None:
- return quality, content
- elif number is not None:
- return "high", number
-
- def _parse_voicemail(self, voicemailHtml):
- splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
+ def _parse_history(self, historyHtml):
+ splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
number = numberGroup.group(1).strip() if numberGroup else ""
prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
- contactIdGroup = self._messagesContactID.search(messageHtml)
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
- messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
- messageParts = (
- self._interpret_voicemail_regex(group)
- for group in messageGroups
- ) if messageGroups else ()
-
yield {
"id": messageId.strip(),
"contactId": contactId,
"prettyNumber": prettyNumber,
"number": number,
"location": location,
- "messageParts": messageParts,
}
- def _decorate_voicemail(self, parsedVoicemails):
- messagePartFormat = {
- "med1": "<i>%s</i>",
- "med2": "%s",
- "high": "<b>%s</b>",
- }
- for voicemailData in parsedVoicemails:
- message = " ".join((
- messagePartFormat[quality] % part
- for (quality, part) in voicemailData["messageParts"]
- )).strip()
- if not message:
- message = "No Transcription"
- whoFrom = voicemailData["name"]
- when = voicemailData["time"]
- voicemailData["messageParts"] = ((whoFrom, message, when), )
- yield voicemailData
-
- _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
- _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
- _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+ @staticmethod
+ def _interpret_voicemail_regex(group):
+ quality, content, number = group.group(2), group.group(3), group.group(4)
+ text = MessageText()
+ if quality is not None and content is not None:
+ text.accuracy = quality
+ text.text = content
+ return text
+ elif number is not None:
+ text.accuracy = MessageText.ACCURACY_HIGH
+ text.text = number
+ return text
+
+ def _parse_voicemail(self, voicemailHtml):
+ splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
+ for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
+ conv = Conversation()
+ conv.type = Conversation.TYPE_VOICEMAIL
+ conv.id = messageId.strip()
+
+ exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+ exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+ conv.time = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p")
+ relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+ conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ locationGroup = self._voicemailLocationRegex.search(messageHtml)
+ conv.location = locationGroup.group(1).strip() if locationGroup else ""
+
+ nameGroup = self._voicemailNameRegex.search(messageHtml)
+ conv.name = nameGroup.group(1).strip() if nameGroup else ""
+ numberGroup = self._voicemailNumberRegex.search(messageHtml)
+ conv.number = numberGroup.group(1).strip() if numberGroup else ""
+ prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
+ conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
+ conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+
+ messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
+ messageParts = [
+ self._interpret_voicemail_regex(group)
+ for group in messageGroups
+ ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
+ message = Message()
+ message.body = messageParts
+ message.whoFrom = conv.name
+ message.when = conv.time.strftime("%I:%M %p")
+ conv.messages = (message, )
+
+ yield conv
+
+ @staticmethod
+ def _interpret_sms_message_parts(fromPart, textPart, timePart):
+ text = MessageText()
+ text.accuracy = MessageText.ACCURACY_MEDIUM
+ text.text = textPart
+
+ message = Message()
+ message.body = (text, )
+ message.whoFrom = fromPart
+ message.when = timePart
+
+ return message
def _parse_sms(self, smsHtml):
splitSms = self._seperateVoicemailsRegex.split(smsHtml)
for messageId, messageHtml in itergroup(splitSms[1:], 2):
+ conv = Conversation()
+ conv.type = Conversation.TYPE_SMS
+ conv.id = messageId.strip()
+
exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
- exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
- exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
+ exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+ conv.time = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p")
relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
- relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+ conv.location = ""
nameGroup = self._voicemailNameRegex.search(messageHtml)
- name = nameGroup.group(1).strip() if nameGroup else ""
+ conv.name = nameGroup.group(1).strip() if nameGroup else ""
numberGroup = self._voicemailNumberRegex.search(messageHtml)
- number = numberGroup.group(1).strip() if numberGroup else ""
+ conv.number = numberGroup.group(1).strip() if numberGroup else ""
prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
- prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
- contactIdGroup = self._messagesContactID.search(messageHtml)
- contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+ conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+ contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
+ conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
fromGroups = self._smsFromRegex.finditer(messageHtml)
fromParts = (group.group(1).strip() for group in fromGroups)
timeParts = (group.group(1).strip() for group in timeGroups)
messageParts = itertools.izip(fromParts, textParts, timeParts)
+ messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
+ conv.messages = messages
- yield {
- "id": messageId.strip(),
- "contactId": contactId,
- "name": name,
- "time": exactTime,
- "relTime": relativeTime,
- "prettyNumber": prettyNumber,
- "number": number,
- "location": "",
- "messageParts": messageParts,
- }
+ yield conv
- def _decorate_sms(self, parsedTexts):
- return parsedTexts
+ @staticmethod
+ def _merge_conversation_sources(parsedMessages, json):
+ for message in parsedMessages:
+ jsonItem = json["messages"][message.id]
+ message.isRead = jsonItem["isRead"]
+ message.isSpam = jsonItem["isSpam"]
+ message.isTrash = jsonItem["isTrash"]
+ message.isArchived = "inbox" not in jsonItem["labels"]
+ yield message
+
+ def _get_page(self, url, data = None, refererUrl = None):
+ headers = {}
+ if refererUrl is not None:
+ headers["Referer"] = refererUrl
+
+ encodedData = urllib.urlencode(data) if data is not None else None
+
+ try:
+ page = self._browser.download(url, encodedData, None, headers)
+ except urllib2.URLError, e:
+ _moduleLogger.error("Translating error: %s" % str(e))
+ raise NetworkError("%s is not accesible" % url)
+
+ return page
+
+ def _get_page_with_token(self, url, data = None, refererUrl = None):
+ if data is None:
+ data = {}
+ data['_rnr_se'] = self._token
+
+ page = self._get_page(url, data, refererUrl)
+
+ return page
+
+ def _parse_with_validation(self, page):
+ json = parse_json(page)
+ validate_response(json)
+ return json
+
+
+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)
+
+
+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, {}, {})
+
+
+def _fake_parse_json(flattened):
+ return safe_eval(flattened)
+
+
+def _actual_parse_json(flattened):
+ return simplejson.loads(flattened)
+
+
+if simplejson is None:
+ parse_json = _fake_parse_json
+else:
+ parse_json = _actual_parse_json
+
+
+def extract_payload(flatXml):
+ xmlTree = ElementTree.fromstring(flatXml)
+
+ jsonElement = xmlTree.getchildren()[0]
+ flatJson = jsonElement.text
+ jsonTree = parse_json(flatJson)
+
+ htmlElement = xmlTree.getchildren()[1]
+ flatHtml = htmlElement.text
+
+ return jsonTree, flatHtml
+
+
+def validate_response(response):
+ """
+ Validates that the JSON response is A-OK
+ """
+ try:
+ assert 'ok' in response and 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"):
+ return GVoiceBackend.PHONE_TYPE_GIZMO
+ else:
+ return GVoiceBackend.PHONE_TYPE_MOBILE
def set_sane_callback(backend):
return
-def sort_messages(allMessages):
- sortableAllMessages = [
- (message["time"], message)
- for message in allMessages
- ]
- sortableAllMessages.sort(reverse=True)
- return (
- message
- for (exactTime, message) in sortableAllMessages
- )
+def _is_not_special(name):
+ return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
-def decorate_recent(recentCallData):
- """
- @returns (personsName, phoneNumber, date, action)
- """
- if recentCallData["name"]:
- header = recentCallData["name"]
- elif recentCallData["prettyNumber"]:
- header = recentCallData["prettyNumber"]
- elif recentCallData["location"]:
- header = recentCallData["location"]
- else:
- header = "Unknown"
+def to_dict(obj):
+ members = inspect.getmembers(obj)
+ return dict((name, value) for (name, value) in members if _is_not_special(name))
- number = recentCallData["number"]
- relTime = recentCallData["relTime"]
- action = recentCallData["action"]
- return header, number, relTime, action
+def grab_debug_info(username, password):
+ cookieFile = os.path.join(".", "raw_cookies.txt")
+ try:
+ os.remove(cookieFile)
+ except OSError:
+ pass
-def decorate_message(messageData):
- exactTime = messageData["time"]
- if messageData["name"]:
- header = messageData["name"]
- elif messageData["prettyNumber"]:
- header = messageData["prettyNumber"]
- else:
- header = "Unknown"
- number = messageData["number"]
- relativeTime = messageData["relTime"]
-
- messageParts = list(messageData["messageParts"])
- if len(messageParts) == 0:
- messages = ("No Transcription", )
- elif len(messageParts) == 1:
- messages = (messageParts[0][1], )
- else:
- messages = [
- "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
- for messagePart in messageParts
- ]
-
- decoratedResults = header, number, relativeTime, messages
- return decoratedResults
-
-
-def test_backend(username, password):
- backend = GVoiceBackend()
- print "Authenticated: ", backend.is_authed()
- if not 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: ",
- #import pprint
- #pprint.pprint(backend.get_callback_numbers())
-
- #print "Recent: "
- #for data in backend.get_recent():
- # pprint.pprint(data)
- #for data in sort_messages(backend.get_recent()):
- # pprint.pprint(decorate_recent(data))
- #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])))
-
- #print "Messages: ",
- #for message in backend.get_messages():
- # pprint.pprint(message)
- #for message in sort_messages(backend.get_messages()):
- # pprint.pprint(decorate_message(message))
-
- return backend
+ backend = GVoiceBackend(cookieFile)
+ 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),
-if __name__ == "__main__":
+ ("voicemail", backend._XML_VOICEMAIL_URL),
+ ("sms", backend._XML_SMS_URL),
+
+ ("recent", backend._XML_RECENT_URL),
+ ("placed", backend._XML_PLACED_URL),
+ ("recieved", backend._XML_RECEIVED_URL),
+ ("missed", backend._XML_MISSED_URL),
+ ]
+
+ # 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 str(e)
+ continue
+ print "\tWriting to file"
+ with open("loggedin_%s.txt" % name, "w") as f:
+ f.write(page)
+
+ # Cookies
+ browser.save_cookies()
+ 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
+ )
+
+
+def main():
import sys
logging.basicConfig(level=logging.DEBUG)
- test_backend(sys.argv[1], sys.argv[2])
+ args = sys.argv
+ if 3 <= len(args):
+ username = args[1]
+ password = args[2]
+
+ grab_debug_info(username, password)
+
+
+if __name__ == "__main__":
+ main()