From 773901ab6004ada2aac231635694b9ec5534e30c Mon Sep 17 00:00:00 2001 From: epage Date: Thu, 12 Nov 2009 02:33:18 +0000 Subject: [PATCH] Some cleanup git-svn-id: file:///svnroot/gc-dialer/trunk@570 c39d3808-3fe2-4d86-a59f-b7f623ee9f21 --- src/alarm_notify.py | 2 +- src/backends/browser_emu.py | 169 +++++++++ src/backends/file_backend.py | 171 +++++++++ src/backends/gv_backend.py | 800 +++++++++++++++++++++++++++++++++++++++++ src/backends/merge_backend.py | 153 ++++++++ src/backends/null_backend.py | 134 +++++++ src/browser_emu.py | 169 --------- src/dc_glade.py | 8 +- src/file_backend.py | 171 --------- src/gv_backend.py | 800 ----------------------------------------- src/gv_views.py | 4 +- src/merge_backend.py | 153 -------- src/null_backend.py | 134 ------- 13 files changed, 1434 insertions(+), 1434 deletions(-) create mode 100644 src/backends/__init__.py create mode 100644 src/backends/browser_emu.py create mode 100644 src/backends/file_backend.py create mode 100644 src/backends/gv_backend.py create mode 100644 src/backends/merge_backend.py create mode 100644 src/backends/null_backend.py delete mode 100644 src/browser_emu.py delete mode 100644 src/file_backend.py delete mode 100644 src/gv_backend.py delete mode 100644 src/merge_backend.py delete mode 100644 src/null_backend.py diff --git a/src/alarm_notify.py b/src/alarm_notify.py index 1920a59..d71b030 100755 --- a/src/alarm_notify.py +++ b/src/alarm_notify.py @@ -6,7 +6,7 @@ import ConfigParser import pprint import constants -import gv_backend +from backends import gv_backend def get_missed(backend): diff --git a/src/backends/__init__.py b/src/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backends/browser_emu.py b/src/backends/browser_emu.py new file mode 100644 index 0000000..056f204 --- /dev/null +++ b/src/backends/browser_emu.py @@ -0,0 +1,169 @@ +""" +@author: Laszlo Nagy +@copyright: (c) 2005 by Szoftver Messias Bt. +@licence: BSD style + +Objects of the MozillaEmulator class can emulate a browser that is capable of: + + - cookie management + - configurable user agent string + - GET and POST + - multipart POST (send files) + - receive content into file + +I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it: + + 1. Use firefox + 2. Install and open the livehttpheaders plugin + 3. Use the website manually with firefox + 4. Check the GET and POST requests in the livehttpheaders capture window + 5. Create an instance of the above class and send the same GET and POST requests to the server. + +Optional steps: + + - You can change user agent string in the build_opened method + - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files +""" + +import urllib2 +import cookielib +import logging + +import socket + + +_moduleLogger = logging.getLogger("browser_emu") +socket.setdefaulttimeout(10) + + +class MozillaEmulator(object): + + def __init__(self, trycount = 1): + """Create a new MozillaEmulator object. + + @param trycount: The download() method will retry the operation if it fails. You can specify -1 for infinite retrying. + A value of 0 means no retrying. A value of 1 means one retry. etc.""" + self.cookies = cookielib.LWPCookieJar() + self.debug = False + self.trycount = trycount + + def download(self, url, + postdata = None, extraheaders = None, forbidRedirect = False, + trycount = None, only_head = False, + ): + """Download an URL with GET or POST methods. + + @param postdata: It can be a string that will be POST-ed to the URL. + When None is given, the method will be GET instead. + @param extraheaders: You can add/modify HTTP headers with a dict here. + @param forbidRedirect: Set this flag if you do not want to handle + HTTP 301 and 302 redirects. + @param trycount: Specify the maximum number of retries here. + 0 means no retry on error. Using -1 means infinite retring. + None means the default value (that is self.trycount). + @param only_head: Create the openerdirector and return it. In other + words, this will not retrieve any content except HTTP headers. + + @return: The raw HTML page data + """ + _moduleLogger.warning("Performing download of %s" % url) + + if extraheaders is None: + extraheaders = {} + if trycount is None: + trycount = self.trycount + cnt = 0 + + while True: + try: + req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect) + openerdirector = u.open(req) + if self.debug: + _moduleLogger.info("%r - %r" % (req.get_method(), url)) + _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg)) + _moduleLogger.info("%r" % (openerdirector.headers)) + self.cookies.extract_cookies(openerdirector, req) + if only_head: + return openerdirector + + return self._read(openerdirector, trycount) + except urllib2.URLError: + cnt += 1 + if (-1 < trycount) and (trycount < cnt): + raise + + # Retry :-) + _moduleLogger.info("MozillaEmulator: urllib2.URLError, retryting %d" % cnt) + + def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False): + if extraheaders is None: + extraheaders = {} + + txheaders = { + 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png', + 'Accept-Language': 'en,en-us;q=0.5', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + } + for key, value in extraheaders.iteritems(): + txheaders[key] = value + req = urllib2.Request(url, postdata, txheaders) + self.cookies.add_cookie_header(req) + if forbidRedirect: + redirector = HTTPNoRedirector() + #_moduleLogger.info("Redirection disabled") + else: + redirector = urllib2.HTTPRedirectHandler() + #_moduleLogger.info("Redirection enabled") + + http_handler = urllib2.HTTPHandler(debuglevel=self.debug) + https_handler = urllib2.HTTPSHandler(debuglevel=self.debug) + + u = urllib2.build_opener( + http_handler, + https_handler, + urllib2.HTTPCookieProcessor(self.cookies), + redirector + ) + u.addheaders = [( + 'User-Agent', + 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4' + )] + if not postdata is None: + req.add_data(postdata) + return (req, u) + + def _read(self, openerdirector, trycount): + chunks = [] + + chunk = openerdirector.read() + chunks.append(chunk) + #while chunk and cnt < trycount: + # time.sleep(1) + # cnt += 1 + # chunk = openerdirector.read() + # chunks.append(chunk) + + data = "".join(chunks) + + if "Content-Length" in openerdirector.info(): + assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % ( + openerdirector.info()["Content-Length"], + len(data), + ) + + return data + + +class HTTPNoRedirector(urllib2.HTTPRedirectHandler): + """This is a custom http redirect handler that FORBIDS redirection.""" + + def http_error_302(self, req, fp, code, msg, headers): + e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) + if e.code in (301, 302): + if 'location' in headers: + newurl = headers.getheaders('location')[0] + elif 'uri' in headers: + newurl = headers.getheaders('uri')[0] + e.newurl = newurl + _moduleLogger.info("New url: %s" % e.newurl) + raise e diff --git a/src/backends/file_backend.py b/src/backends/file_backend.py new file mode 100644 index 0000000..b373561 --- /dev/null +++ b/src/backends/file_backend.py @@ -0,0 +1,171 @@ +#!/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 + +Filesystem backend for contact support +""" + + +import os +import re +import csv + + +class CsvAddressBook(object): + """ + Currently supported file format + @li Has the first line as a header + @li Escapes with quotes + @li Comma as delimiter + @li Column 0 is name, column 1 is number + """ + + _nameRe = re.compile("name", re.IGNORECASE) + _phoneRe = re.compile("phone", re.IGNORECASE) + _mobileRe = re.compile("mobile", re.IGNORECASE) + + def __init__(self, csvPath): + self.__csvPath = csvPath + self.__contacts = list( + self.read_csv(csvPath) + ) + + @classmethod + def read_csv(cls, csvPath): + try: + csvReader = iter(csv.reader(open(csvPath, "rU"))) + except IOError, e: + if e.errno != 2: + raise + return + + header = csvReader.next() + nameColumn, phoneColumns = cls._guess_columns(header) + + yieldCount = 0 + for row in csvReader: + contactDetails = [] + for (phoneType, phoneColumn) in phoneColumns: + try: + if len(row[phoneColumn]) == 0: + continue + contactDetails.append((phoneType, row[phoneColumn])) + except IndexError: + pass + if len(contactDetails) != 0: + yield str(yieldCount), row[nameColumn], contactDetails + yieldCount += 1 + + @classmethod + def _guess_columns(cls, row): + names = [] + phones = [] + for i, item in enumerate(row): + if cls._nameRe.search(item) is not None: + names.append((item, i)) + elif cls._phoneRe.search(item) is not None: + phones.append((item, i)) + elif cls._mobileRe.search(item) is not None: + phones.append((item, i)) + if len(names) == 0: + names.append(("Name", 0)) + if len(phones) == 0: + phones.append(("Phone", 1)) + + return names[0][1], phones + + def clear_caches(self): + pass + + @staticmethod + def factory_name(): + return "csv" + + @staticmethod + def contact_source_short_name(contactId): + return "csv" + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + for contact in self.__contacts: + yield contact[0:2] + + def get_contact_details(self, contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + contactId = int(contactId) + return iter(self.__contacts[contactId][2]) + + +class FilesystemAddressBookFactory(object): + + FILETYPE_SUPPORT = { + "csv": CsvAddressBook, + } + + def __init__(self, path): + self.__path = path + + def clear_caches(self): + pass + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + for root, dirs, filenames in os.walk(self.__path): + for filename in filenames: + try: + name, ext = filename.rsplit(".", 1) + except ValueError: + continue + + if ext in self.FILETYPE_SUPPORT: + yield self, os.path.join(root, filename), name + + def open_addressbook(self, bookId): + name, ext = bookId.rsplit(".", 1) + assert ext in self.FILETYPE_SUPPORT, "Unsupported file extension %s" % ext + return self.FILETYPE_SUPPORT[ext](bookId) + + @staticmethod + def factory_name(): + return "File" + + +def print_filebooks(contactPath = None): + """ + Included here for debugging. + + Either insert it into the code or launch python with the "-i" flag + """ + if contactPath is None: + contactPath = os.path.join(os.path.expanduser("~"), ".dialcentral", "contacts") + + abf = FilesystemAddressBookFactory(contactPath) + for book in abf.get_addressbooks(): + ab = abf.open_addressbook(book[1]) + print book + for contact in ab.get_contacts(): + print "\t", contact + for details in ab.get_contact_details(contact[0]): + print "\t\t", details diff --git a/src/backends/gv_backend.py b/src/backends/gv_backend.py new file mode 100644 index 0000000..bc98467 --- /dev/null +++ b/src/backends/gv_backend.py @@ -0,0 +1,800 @@ +#!/usr/bin/python + +""" +DialCentral - Front end for Google's GoogleVoice 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 + http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ + http://posttopic.com/topic/google-voice-add-on-development +""" + +from __future__ import with_statement + +import os +import re +import urllib +import urllib2 +import time +import datetime +import itertools +import logging +from xml.sax import saxutils + +from xml.etree import ElementTree + +try: + import simplejson +except ImportError: + simplejson = None + +import browser_emu + + +_moduleLogger = logging.getLogger("gvoice.dialer") + + +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, {}, {}) + + +if simplejson is None: + def parse_json(flattened): + return safe_eval(flattened) +else: + def parse_json(flattened): + 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 NetworkError(RuntimeError): + pass + + +class GVDialer(object): + """ + This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers + the functions include login, setting up a callback number, and initalting a callback + """ + + 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._token = "" + self._accountNum = "" + self._lastAuthed = 0.0 + 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._forwardURL = "https://www.google.com/voice/mobile/phones" + self._tokenURL = "http://www.google.com/voice/m" + self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth" + 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._gvDialingStrRe = re.compile("This may take a few seconds", re.M) + self._clicktocallURL = "https://www.google.com/voice/m/sendcall" + self._sendSmsURL = "https://www.google.com/voice/m/sendsms" + + self._recentCallsURL = "https://www.google.com/voice/inbox/recent/" + self._placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/" + self._receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/" + self._missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/" + + self._contactsRe = re.compile(r"""(.*?)""", re.S) + self._contactsNextRe = re.compile(r""".*Next.*?""", re.S) + self._contactsURL = "https://www.google.com/voice/mobile/contacts" + self._contactDetailPhoneRe = re.compile(r"""([0-9+\-\(\) \t]+?)\((\w+)\)""", re.S) + self._contactDetailURL = "https://www.google.com/voice/mobile/contact" + + self._voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/" + self._smsURL = "https://www.google.com/voice/inbox/recent/sms/" + self._seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) + self._exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) + self._prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) + self._voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) + self._messagesContactID = re.compile(r""".*?\s*?(.*?)""", re.MULTILINE) + self._voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) + self._smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + self._smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) + + 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: + return True + + try: + page = self._browser.download(self._forwardURL) + self._grab_account_info(page) + except Exception, e: + _moduleLogger.exception(str(e)) + return False + + self._browser.cookies.save() + self._lastAuthed = time.time() + return True + + def _get_token(self): + try: + tokenPage = self._browser.download(self._tokenURL) + except urllib2.URLError, e: + _moduleLogger.exception("Translating error: %s" % str(e)) + raise NetworkError("%s is not accesible" % self._loginURL) + 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): + loginPostData = urllib.urlencode({ + '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) + 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: + # 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.cookies.save() + self._lastAuthed = time.time() + return True + + def logout(self): + self._lastAuthed = 0.0 + self._browser.cookies.clear() + self._browser.cookies.save() + + def dial(self, number): + """ + 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") + + return True + + 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) + + return True + + def is_valid_syntax(self, number): + """ + @returns If This number be called ( syntax validation only ) + """ + return self._validateRe.match(number) is not None + + def get_account_number(self): + """ + @returns The GoogleVoice phone number + """ + return self._accountNum + + def get_callback_numbers(self): + """ + @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): + """ + Set the number that GoogleVoice calls + @param callbacknumber should be a proper 10 digit number + """ + self._callbackNumber = callbacknumber + return True + + def get_callback_number(self): + """ + @returns Current callback number or None + """ + return self._callbackNumber + + 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), + ): + try: + flatXml = self._browser.download(url) + except urllib2.URLError, e: + _moduleLogger.exception("Translating error: %s" % str(e)) + raise NetworkError("%s is not accesible" % url) + + allRecentHtml = self._grab_html(flatXml) + allRecentData = self._parse_voicemail(allRecentHtml) + for recentCallData in allRecentData: + recentCallData["action"] = action + yield recentCallData + + 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) + + 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) + + 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) + voicemailHtml = self._grab_html(voicemailPage) + voicemailJson = self._grab_json(voicemailPage) + parsedVoicemail = self._parse_voicemail(voicemailHtml) + voicemails = self._merge_messages(parsedVoicemail, voicemailJson) + decoratedVoicemails = self._decorate_voicemail(voicemails) + + 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) + smsHtml = self._grab_html(smsPage) + smsJson = self._grab_json(smsPage) + parsedSms = self._parse_sms(smsHtml) + smss = self._merge_messages(parsedSms, smsJson) + decoratedSms = self._decorate_sms(smss) + + allMessages = itertools.chain(decoratedVoicemails, decoratedSms) + return allMessages + + def clear_caches(self): + pass + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "" + + def open_addressbook(self, bookId): + return self + + @staticmethod + def contact_source_short_name(contactId): + return "GV" + + @staticmethod + def factory_name(): + return "Google Voice" + + 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 + + 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 + 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): + 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 + + @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) + for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p") + 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 "" + + nameGroup = self._voicemailNameRegex.search(messageHtml) + name = nameGroup.group(1).strip() if nameGroup 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 "" + contactIdGroup = self._messagesContactID.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, + "name": name, + "time": exactTime, + "relTime": relativeTime, + "prettyNumber": prettyNumber, + "number": number, + "location": location, + "messageParts": messageParts, + "type": "Voicemail", + } + + def _decorate_voicemail(self, parsedVoicemails): + messagePartFormat = { + "med1": "%s", + "med2": "%s", + "high": "%s", + } + 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 + + def _parse_sms(self, smsHtml): + splitSms = self._seperateVoicemailsRegex.split(smsHtml) + for messageId, messageHtml in itergroup(splitSms[1:], 2): + exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) + exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" + exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p") + relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) + relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" + + nameGroup = self._voicemailNameRegex.search(messageHtml) + name = nameGroup.group(1).strip() if nameGroup 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 "" + contactIdGroup = self._messagesContactID.search(messageHtml) + contactId = contactIdGroup.group(1).strip() if contactIdGroup 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": messageId.strip(), + "contactId": contactId, + "name": name, + "time": exactTime, + "relTime": relativeTime, + "prettyNumber": prettyNumber, + "number": number, + "location": "", + "messageParts": messageParts, + "type": "Texts", + } + + def _decorate_sms(self, parsedTexts): + return parsedTexts + + @staticmethod + def _merge_messages(parsedMessages, json): + for message in parsedMessages: + id = message["id"] + jsonItem = json["messages"][id] + message["isRead"] = jsonItem["isRead"] + message["isSpam"] = jsonItem["isSpam"] + message["isTrash"] = jsonItem["isTrash"] + message["isArchived"] = "inbox" not in jsonItem["labels"] + yield message + + +def set_sane_callback(backend): + """ + Try to set a sane default callback number on these preferences + 1) 1747 numbers ( Gizmo ) + 2) anything with gizmo in the name + 3) anything with computer in the name + 4) the first value + """ + numbers = backend.get_callback_numbers() + + priorityOrderedCriteria = [ + ("1747", None), + (None, "gizmo"), + (None, "computer"), + (None, "sip"), + (None, None), + ] + + for numberCriteria, descriptionCriteria in priorityOrderedCriteria: + for number, description in numbers.iteritems(): + if numberCriteria is not None and re.compile(numberCriteria).match(number) is None: + continue + if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None: + continue + backend.set_callback_number(number) + 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 decorate_recent(recentCallData): + """ + @returns (personsName, phoneNumber, date, action) + """ + contactId = recentCallData["contactId"] + if recentCallData["name"]: + header = recentCallData["name"] + elif recentCallData["prettyNumber"]: + header = recentCallData["prettyNumber"] + elif recentCallData["location"]: + header = recentCallData["location"] + else: + header = "Unknown" + + number = recentCallData["number"] + relTime = recentCallData["relTime"] + action = recentCallData["action"] + return contactId, header, number, relTime, action + + +def decorate_message(messageData): + contactId = messageData["contactId"] + 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 = [ + "%s: %s" % (messagePart[0], messagePart[1]) + for messagePart in messageParts + ] + + decoratedResults = contactId, header, number, relativeTime, messages + return decoratedResults + + +def test_backend(username, password): + backend = GVDialer() + 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(): + message["messageParts"] = list(message["messageParts"]) + pprint.pprint(message) + #for message in sort_messages(backend.get_messages()): + # pprint.pprint(decorate_message(message)) + + return backend + + +def grab_debug_info(username, password): + cookieFile = os.path.join(".", "raw_cookies.txt") + try: + os.remove(cookieFile) + except OSError: + pass + + backend = GVDialer(cookieFile) + browser = backend._browser + + _TEST_WEBPAGES = [ + ("forward", backend._forwardURL), + ("token", backend._tokenURL), + ("login", backend._loginURL), + ("contacts", backend._contactsURL), + + ("voicemail", backend._voicemailURL), + ("sms", backend._smsURL), + + ("recent", backend._recentCallsURL), + ("placed", backend._placedCallsURL), + ("recieved", backend._receivedCallsURL), + ("missed", backend._missedCallsURL), + ] + + # 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 e.message + continue + print "\tWriting to file" + with open("loggedin_%s.txt" % name, "w") as f: + f.write(page) + + # Cookies + browser.cookies.save() + 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 + ) + + +if __name__ == "__main__": + import sys + logging.basicConfig(level=logging.DEBUG) + #test_backend(sys.argv[1], sys.argv[2]) + grab_debug_info(sys.argv[1], sys.argv[2]) diff --git a/src/backends/merge_backend.py b/src/backends/merge_backend.py new file mode 100644 index 0000000..476a616 --- /dev/null +++ b/src/backends/merge_backend.py @@ -0,0 +1,153 @@ +import logging + + +_moduleLogger = logging.getLogger("merge_backend") + + +class MergedAddressBook(object): + """ + Merger of all addressbooks + """ + + def __init__(self, addressbookFactories, sorter = None): + self.__addressbookFactories = addressbookFactories + self.__addressbooks = None + self.__sort_contacts = sorter if sorter is not None else self.null_sorter + + def clear_caches(self): + self.__addressbooks = None + for factory in self.__addressbookFactories: + factory.clear_caches() + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "" + + def open_addressbook(self, bookId): + return self + + def contact_source_short_name(self, contactId): + if self.__addressbooks is None: + return "" + bookIndex, originalId = contactId.split("-", 1) + return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId) + + @staticmethod + def factory_name(): + return "All Contacts" + + def get_contacts(self): + """ + @returns Iterable of (contact id, contact name) + """ + if self.__addressbooks is None: + self.__addressbooks = list( + factory.open_addressbook(id) + for factory in self.__addressbookFactories + for (f, id, name) in factory.get_addressbooks() + ) + contacts = ( + ("-".join([str(bookIndex), contactId]), contactName) + for (bookIndex, addressbook) in enumerate(self.__addressbooks) + for (contactId, contactName) in addressbook.get_contacts() + ) + sortedContacts = self.__sort_contacts(contacts) + return sortedContacts + + def get_contact_details(self, contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + if self.__addressbooks is None: + return [] + bookIndex, originalId = contactId.split("-", 1) + return self.__addressbooks[int(bookIndex)].get_contact_details(originalId) + + @staticmethod + def null_sorter(contacts): + """ + Good for speed/low memory + """ + return contacts + + @staticmethod + def basic_firtname_sorter(contacts): + """ + Expects names in "First Last" format + """ + contactsWithKey = [ + (contactName.rsplit(" ", 1)[0], (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @staticmethod + def basic_lastname_sorter(contacts): + """ + Expects names in "First Last" format + """ + contactsWithKey = [ + (contactName.rsplit(" ", 1)[-1], (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @staticmethod + def reversed_firtname_sorter(contacts): + """ + Expects names in "Last, First" format + """ + contactsWithKey = [ + (contactName.split(", ", 1)[-1], (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @staticmethod + def reversed_lastname_sorter(contacts): + """ + Expects names in "Last, First" format + """ + contactsWithKey = [ + (contactName.split(", ", 1)[0], (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @staticmethod + def guess_firstname(name): + if ", " in name: + return name.split(", ", 1)[-1] + else: + return name.rsplit(" ", 1)[0] + + @staticmethod + def guess_lastname(name): + if ", " in name: + return name.split(", ", 1)[0] + else: + return name.rsplit(" ", 1)[-1] + + @classmethod + def advanced_firstname_sorter(cls, contacts): + contactsWithKey = [ + (cls.guess_firstname(contactName), (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) + + @classmethod + def advanced_lastname_sorter(cls, contacts): + contactsWithKey = [ + (cls.guess_lastname(contactName), (contactId, contactName)) + for (contactId, contactName) in contacts + ] + contactsWithKey.sort() + return (contactData for (lastName, contactData) in contactsWithKey) diff --git a/src/backends/null_backend.py b/src/backends/null_backend.py new file mode 100644 index 0000000..c07f724 --- /dev/null +++ b/src/backends/null_backend.py @@ -0,0 +1,134 @@ +#!/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 +""" + + +class NullDialer(object): + + def __init__(self): + pass + + def is_authed(self, force = False): + return False + + def login(self, username, password): + return self.is_authed() + + def logout(self): + self.clear_caches() + + def dial(self, number): + return True + + def send_sms(self, number, message): + raise NotImplementedError("SMS Is Not Supported") + + def clear_caches(self): + pass + + def is_valid_syntax(self, number): + """ + @returns If This number be called ( syntax validation only ) + """ + return False + + def get_account_number(self): + """ + @returns The grand central phone number + """ + return "" + + def set_sane_callback(self): + pass + + def get_callback_numbers(self): + return {} + + def set_callback_number(self, callbacknumber): + return True + + def get_callback_number(self): + return "" + + def get_recent(self): + return () + + def get_addressbooks(self): + return () + + def open_addressbook(self, bookId): + return self + + @staticmethod + def contact_source_short_name(contactId): + return "ERROR" + + @staticmethod + def factory_name(): + return "ERROR" + + def get_contacts(self): + return () + + def get_contact_details(self, contactId): + return () + + def get_messages(self): + return () + + +class NullAddressBook(object): + """ + Minimal example of both an addressbook factory and an addressbook + """ + + def clear_caches(self): + pass + + def get_addressbooks(self): + """ + @returns Iterable of (Address Book Factory, Book Id, Book Name) + """ + yield self, "", "None" + + def open_addressbook(self, bookId): + return self + + @staticmethod + def contact_source_short_name(contactId): + return "" + + @staticmethod + def factory_name(): + return "" + + @staticmethod + def get_contacts(): + """ + @returns Iterable of (contact id, contact name) + """ + return [] + + @staticmethod + def get_contact_details(contactId): + """ + @returns Iterable of (Phone Type, Phone Number) + """ + return [] diff --git a/src/browser_emu.py b/src/browser_emu.py deleted file mode 100644 index 056f204..0000000 --- a/src/browser_emu.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -@author: Laszlo Nagy -@copyright: (c) 2005 by Szoftver Messias Bt. -@licence: BSD style - -Objects of the MozillaEmulator class can emulate a browser that is capable of: - - - cookie management - - configurable user agent string - - GET and POST - - multipart POST (send files) - - receive content into file - -I have seen many requests on the python mailing list about how to emulate a browser. I'm using this class for years now, without any problems. This is how you can use it: - - 1. Use firefox - 2. Install and open the livehttpheaders plugin - 3. Use the website manually with firefox - 4. Check the GET and POST requests in the livehttpheaders capture window - 5. Create an instance of the above class and send the same GET and POST requests to the server. - -Optional steps: - - - You can change user agent string in the build_opened method - - The "encode_multipart_formdata" function can be used alone to create POST data from a list of field values and files -""" - -import urllib2 -import cookielib -import logging - -import socket - - -_moduleLogger = logging.getLogger("browser_emu") -socket.setdefaulttimeout(10) - - -class MozillaEmulator(object): - - def __init__(self, trycount = 1): - """Create a new MozillaEmulator object. - - @param trycount: The download() method will retry the operation if it fails. You can specify -1 for infinite retrying. - A value of 0 means no retrying. A value of 1 means one retry. etc.""" - self.cookies = cookielib.LWPCookieJar() - self.debug = False - self.trycount = trycount - - def download(self, url, - postdata = None, extraheaders = None, forbidRedirect = False, - trycount = None, only_head = False, - ): - """Download an URL with GET or POST methods. - - @param postdata: It can be a string that will be POST-ed to the URL. - When None is given, the method will be GET instead. - @param extraheaders: You can add/modify HTTP headers with a dict here. - @param forbidRedirect: Set this flag if you do not want to handle - HTTP 301 and 302 redirects. - @param trycount: Specify the maximum number of retries here. - 0 means no retry on error. Using -1 means infinite retring. - None means the default value (that is self.trycount). - @param only_head: Create the openerdirector and return it. In other - words, this will not retrieve any content except HTTP headers. - - @return: The raw HTML page data - """ - _moduleLogger.warning("Performing download of %s" % url) - - if extraheaders is None: - extraheaders = {} - if trycount is None: - trycount = self.trycount - cnt = 0 - - while True: - try: - req, u = self._build_opener(url, postdata, extraheaders, forbidRedirect) - openerdirector = u.open(req) - if self.debug: - _moduleLogger.info("%r - %r" % (req.get_method(), url)) - _moduleLogger.info("%r - %r" % (openerdirector.code, openerdirector.msg)) - _moduleLogger.info("%r" % (openerdirector.headers)) - self.cookies.extract_cookies(openerdirector, req) - if only_head: - return openerdirector - - return self._read(openerdirector, trycount) - except urllib2.URLError: - cnt += 1 - if (-1 < trycount) and (trycount < cnt): - raise - - # Retry :-) - _moduleLogger.info("MozillaEmulator: urllib2.URLError, retryting %d" % cnt) - - def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False): - if extraheaders is None: - extraheaders = {} - - txheaders = { - 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png', - 'Accept-Language': 'en,en-us;q=0.5', - 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', - } - for key, value in extraheaders.iteritems(): - txheaders[key] = value - req = urllib2.Request(url, postdata, txheaders) - self.cookies.add_cookie_header(req) - if forbidRedirect: - redirector = HTTPNoRedirector() - #_moduleLogger.info("Redirection disabled") - else: - redirector = urllib2.HTTPRedirectHandler() - #_moduleLogger.info("Redirection enabled") - - http_handler = urllib2.HTTPHandler(debuglevel=self.debug) - https_handler = urllib2.HTTPSHandler(debuglevel=self.debug) - - u = urllib2.build_opener( - http_handler, - https_handler, - urllib2.HTTPCookieProcessor(self.cookies), - redirector - ) - u.addheaders = [( - 'User-Agent', - 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4' - )] - if not postdata is None: - req.add_data(postdata) - return (req, u) - - def _read(self, openerdirector, trycount): - chunks = [] - - chunk = openerdirector.read() - chunks.append(chunk) - #while chunk and cnt < trycount: - # time.sleep(1) - # cnt += 1 - # chunk = openerdirector.read() - # chunks.append(chunk) - - data = "".join(chunks) - - if "Content-Length" in openerdirector.info(): - assert len(data) == int(openerdirector.info()["Content-Length"]), "The packet header promised %s of data but only was able to read %s of data" % ( - openerdirector.info()["Content-Length"], - len(data), - ) - - return data - - -class HTTPNoRedirector(urllib2.HTTPRedirectHandler): - """This is a custom http redirect handler that FORBIDS redirection.""" - - def http_error_302(self, req, fp, code, msg, headers): - e = urllib2.HTTPError(req.get_full_url(), code, msg, headers, fp) - if e.code in (301, 302): - if 'location' in headers: - newurl = headers.getheaders('location')[0] - elif 'uri' in headers: - newurl = headers.getheaders('uri')[0] - e.newurl = newurl - _moduleLogger.info("New url: %s" % e.newurl) - raise e diff --git a/src/dc_glade.py b/src/dc_glade.py index ead1bdf..edb2a5a 100755 --- a/src/dc_glade.py +++ b/src/dc_glade.py @@ -186,7 +186,7 @@ class Dialcentral(object): """ # Barebones UI handlers try: - import null_backend + from backends import null_backend import null_views self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()} @@ -261,10 +261,10 @@ class Dialcentral(object): # Setup costly backends try: - import gv_backend - import file_backend + from backends import gv_backend + from backends import file_backend import gv_views - import merge_backend + from backends import merge_backend try: os.makedirs(constants._data_path_) diff --git a/src/file_backend.py b/src/file_backend.py deleted file mode 100644 index b373561..0000000 --- a/src/file_backend.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/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 - -Filesystem backend for contact support -""" - - -import os -import re -import csv - - -class CsvAddressBook(object): - """ - Currently supported file format - @li Has the first line as a header - @li Escapes with quotes - @li Comma as delimiter - @li Column 0 is name, column 1 is number - """ - - _nameRe = re.compile("name", re.IGNORECASE) - _phoneRe = re.compile("phone", re.IGNORECASE) - _mobileRe = re.compile("mobile", re.IGNORECASE) - - def __init__(self, csvPath): - self.__csvPath = csvPath - self.__contacts = list( - self.read_csv(csvPath) - ) - - @classmethod - def read_csv(cls, csvPath): - try: - csvReader = iter(csv.reader(open(csvPath, "rU"))) - except IOError, e: - if e.errno != 2: - raise - return - - header = csvReader.next() - nameColumn, phoneColumns = cls._guess_columns(header) - - yieldCount = 0 - for row in csvReader: - contactDetails = [] - for (phoneType, phoneColumn) in phoneColumns: - try: - if len(row[phoneColumn]) == 0: - continue - contactDetails.append((phoneType, row[phoneColumn])) - except IndexError: - pass - if len(contactDetails) != 0: - yield str(yieldCount), row[nameColumn], contactDetails - yieldCount += 1 - - @classmethod - def _guess_columns(cls, row): - names = [] - phones = [] - for i, item in enumerate(row): - if cls._nameRe.search(item) is not None: - names.append((item, i)) - elif cls._phoneRe.search(item) is not None: - phones.append((item, i)) - elif cls._mobileRe.search(item) is not None: - phones.append((item, i)) - if len(names) == 0: - names.append(("Name", 0)) - if len(phones) == 0: - phones.append(("Phone", 1)) - - return names[0][1], phones - - def clear_caches(self): - pass - - @staticmethod - def factory_name(): - return "csv" - - @staticmethod - def contact_source_short_name(contactId): - return "csv" - - def get_contacts(self): - """ - @returns Iterable of (contact id, contact name) - """ - for contact in self.__contacts: - yield contact[0:2] - - def get_contact_details(self, contactId): - """ - @returns Iterable of (Phone Type, Phone Number) - """ - contactId = int(contactId) - return iter(self.__contacts[contactId][2]) - - -class FilesystemAddressBookFactory(object): - - FILETYPE_SUPPORT = { - "csv": CsvAddressBook, - } - - def __init__(self, path): - self.__path = path - - def clear_caches(self): - pass - - def get_addressbooks(self): - """ - @returns Iterable of (Address Book Factory, Book Id, Book Name) - """ - for root, dirs, filenames in os.walk(self.__path): - for filename in filenames: - try: - name, ext = filename.rsplit(".", 1) - except ValueError: - continue - - if ext in self.FILETYPE_SUPPORT: - yield self, os.path.join(root, filename), name - - def open_addressbook(self, bookId): - name, ext = bookId.rsplit(".", 1) - assert ext in self.FILETYPE_SUPPORT, "Unsupported file extension %s" % ext - return self.FILETYPE_SUPPORT[ext](bookId) - - @staticmethod - def factory_name(): - return "File" - - -def print_filebooks(contactPath = None): - """ - Included here for debugging. - - Either insert it into the code or launch python with the "-i" flag - """ - if contactPath is None: - contactPath = os.path.join(os.path.expanduser("~"), ".dialcentral", "contacts") - - abf = FilesystemAddressBookFactory(contactPath) - for book in abf.get_addressbooks(): - ab = abf.open_addressbook(book[1]) - print book - for contact in ab.get_contacts(): - print "\t", contact - for details in ab.get_contact_details(contact[0]): - print "\t\t", details diff --git a/src/gv_backend.py b/src/gv_backend.py deleted file mode 100644 index bc98467..0000000 --- a/src/gv_backend.py +++ /dev/null @@ -1,800 +0,0 @@ -#!/usr/bin/python - -""" -DialCentral - Front end for Google's GoogleVoice 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 - http://thatsmith.com/2009/03/google-voice-addon-for-firefox/ - http://posttopic.com/topic/google-voice-add-on-development -""" - -from __future__ import with_statement - -import os -import re -import urllib -import urllib2 -import time -import datetime -import itertools -import logging -from xml.sax import saxutils - -from xml.etree import ElementTree - -try: - import simplejson -except ImportError: - simplejson = None - -import browser_emu - - -_moduleLogger = logging.getLogger("gvoice.dialer") - - -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, {}, {}) - - -if simplejson is None: - def parse_json(flattened): - return safe_eval(flattened) -else: - def parse_json(flattened): - 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 NetworkError(RuntimeError): - pass - - -class GVDialer(object): - """ - This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers - the functions include login, setting up a callback number, and initalting a callback - """ - - 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._token = "" - self._accountNum = "" - self._lastAuthed = 0.0 - 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._forwardURL = "https://www.google.com/voice/mobile/phones" - self._tokenURL = "http://www.google.com/voice/m" - self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth" - 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._gvDialingStrRe = re.compile("This may take a few seconds", re.M) - self._clicktocallURL = "https://www.google.com/voice/m/sendcall" - self._sendSmsURL = "https://www.google.com/voice/m/sendsms" - - self._recentCallsURL = "https://www.google.com/voice/inbox/recent/" - self._placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/" - self._receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/" - self._missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/" - - self._contactsRe = re.compile(r"""(.*?)""", re.S) - self._contactsNextRe = re.compile(r""".*Next.*?""", re.S) - self._contactsURL = "https://www.google.com/voice/mobile/contacts" - self._contactDetailPhoneRe = re.compile(r"""([0-9+\-\(\) \t]+?)\((\w+)\)""", re.S) - self._contactDetailURL = "https://www.google.com/voice/mobile/contact" - - self._voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/" - self._smsURL = "https://www.google.com/voice/inbox/recent/sms/" - self._seperateVoicemailsRegex = re.compile(r"""^\s*
""", re.MULTILINE | re.DOTALL) - self._exactVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._relativeVoicemailTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._voicemailNameRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._voicemailNumberRegex = re.compile(r"""""", re.MULTILINE) - self._prettyVoicemailNumberRegex = re.compile(r"""(.*?)""", re.MULTILINE) - self._voicemailLocationRegex = re.compile(r""".*?(.*?)""", re.MULTILINE) - self._messagesContactID = re.compile(r""".*?\s*?(.*?)""", re.MULTILINE) - self._voicemailMessageRegex = re.compile(r"""((.*?)|(.*?))""", re.MULTILINE) - self._smsFromRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._smsTimeRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - self._smsTextRegex = re.compile(r"""(.*?)""", re.MULTILINE | re.DOTALL) - - 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: - return True - - try: - page = self._browser.download(self._forwardURL) - self._grab_account_info(page) - except Exception, e: - _moduleLogger.exception(str(e)) - return False - - self._browser.cookies.save() - self._lastAuthed = time.time() - return True - - def _get_token(self): - try: - tokenPage = self._browser.download(self._tokenURL) - except urllib2.URLError, e: - _moduleLogger.exception("Translating error: %s" % str(e)) - raise NetworkError("%s is not accesible" % self._loginURL) - 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): - loginPostData = urllib.urlencode({ - '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) - 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: - # 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.cookies.save() - self._lastAuthed = time.time() - return True - - def logout(self): - self._lastAuthed = 0.0 - self._browser.cookies.clear() - self._browser.cookies.save() - - def dial(self, number): - """ - 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") - - return True - - 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) - - return True - - def is_valid_syntax(self, number): - """ - @returns If This number be called ( syntax validation only ) - """ - return self._validateRe.match(number) is not None - - def get_account_number(self): - """ - @returns The GoogleVoice phone number - """ - return self._accountNum - - def get_callback_numbers(self): - """ - @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): - """ - Set the number that GoogleVoice calls - @param callbacknumber should be a proper 10 digit number - """ - self._callbackNumber = callbacknumber - return True - - def get_callback_number(self): - """ - @returns Current callback number or None - """ - return self._callbackNumber - - 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), - ): - try: - flatXml = self._browser.download(url) - except urllib2.URLError, e: - _moduleLogger.exception("Translating error: %s" % str(e)) - raise NetworkError("%s is not accesible" % url) - - allRecentHtml = self._grab_html(flatXml) - allRecentData = self._parse_voicemail(allRecentHtml) - for recentCallData in allRecentData: - recentCallData["action"] = action - yield recentCallData - - 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) - - 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) - - 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) - voicemailHtml = self._grab_html(voicemailPage) - voicemailJson = self._grab_json(voicemailPage) - parsedVoicemail = self._parse_voicemail(voicemailHtml) - voicemails = self._merge_messages(parsedVoicemail, voicemailJson) - decoratedVoicemails = self._decorate_voicemail(voicemails) - - 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) - smsHtml = self._grab_html(smsPage) - smsJson = self._grab_json(smsPage) - parsedSms = self._parse_sms(smsHtml) - smss = self._merge_messages(parsedSms, smsJson) - decoratedSms = self._decorate_sms(smss) - - allMessages = itertools.chain(decoratedVoicemails, decoratedSms) - return allMessages - - def clear_caches(self): - pass - - def get_addressbooks(self): - """ - @returns Iterable of (Address Book Factory, Book Id, Book Name) - """ - yield self, "", "" - - def open_addressbook(self, bookId): - return self - - @staticmethod - def contact_source_short_name(contactId): - return "GV" - - @staticmethod - def factory_name(): - return "Google Voice" - - 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 - - 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 - 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): - 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 - - @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) - for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): - exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) - exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p") - 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 "" - - nameGroup = self._voicemailNameRegex.search(messageHtml) - name = nameGroup.group(1).strip() if nameGroup 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 "" - contactIdGroup = self._messagesContactID.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, - "name": name, - "time": exactTime, - "relTime": relativeTime, - "prettyNumber": prettyNumber, - "number": number, - "location": location, - "messageParts": messageParts, - "type": "Voicemail", - } - - def _decorate_voicemail(self, parsedVoicemails): - messagePartFormat = { - "med1": "%s", - "med2": "%s", - "high": "%s", - } - 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 - - def _parse_sms(self, smsHtml): - splitSms = self._seperateVoicemailsRegex.split(smsHtml) - for messageId, messageHtml in itergroup(splitSms[1:], 2): - exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) - exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p") - relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) - relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" - - nameGroup = self._voicemailNameRegex.search(messageHtml) - name = nameGroup.group(1).strip() if nameGroup 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 "" - contactIdGroup = self._messagesContactID.search(messageHtml) - contactId = contactIdGroup.group(1).strip() if contactIdGroup 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": messageId.strip(), - "contactId": contactId, - "name": name, - "time": exactTime, - "relTime": relativeTime, - "prettyNumber": prettyNumber, - "number": number, - "location": "", - "messageParts": messageParts, - "type": "Texts", - } - - def _decorate_sms(self, parsedTexts): - return parsedTexts - - @staticmethod - def _merge_messages(parsedMessages, json): - for message in parsedMessages: - id = message["id"] - jsonItem = json["messages"][id] - message["isRead"] = jsonItem["isRead"] - message["isSpam"] = jsonItem["isSpam"] - message["isTrash"] = jsonItem["isTrash"] - message["isArchived"] = "inbox" not in jsonItem["labels"] - yield message - - -def set_sane_callback(backend): - """ - Try to set a sane default callback number on these preferences - 1) 1747 numbers ( Gizmo ) - 2) anything with gizmo in the name - 3) anything with computer in the name - 4) the first value - """ - numbers = backend.get_callback_numbers() - - priorityOrderedCriteria = [ - ("1747", None), - (None, "gizmo"), - (None, "computer"), - (None, "sip"), - (None, None), - ] - - for numberCriteria, descriptionCriteria in priorityOrderedCriteria: - for number, description in numbers.iteritems(): - if numberCriteria is not None and re.compile(numberCriteria).match(number) is None: - continue - if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None: - continue - backend.set_callback_number(number) - 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 decorate_recent(recentCallData): - """ - @returns (personsName, phoneNumber, date, action) - """ - contactId = recentCallData["contactId"] - if recentCallData["name"]: - header = recentCallData["name"] - elif recentCallData["prettyNumber"]: - header = recentCallData["prettyNumber"] - elif recentCallData["location"]: - header = recentCallData["location"] - else: - header = "Unknown" - - number = recentCallData["number"] - relTime = recentCallData["relTime"] - action = recentCallData["action"] - return contactId, header, number, relTime, action - - -def decorate_message(messageData): - contactId = messageData["contactId"] - 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 = [ - "%s: %s" % (messagePart[0], messagePart[1]) - for messagePart in messageParts - ] - - decoratedResults = contactId, header, number, relativeTime, messages - return decoratedResults - - -def test_backend(username, password): - backend = GVDialer() - 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(): - message["messageParts"] = list(message["messageParts"]) - pprint.pprint(message) - #for message in sort_messages(backend.get_messages()): - # pprint.pprint(decorate_message(message)) - - return backend - - -def grab_debug_info(username, password): - cookieFile = os.path.join(".", "raw_cookies.txt") - try: - os.remove(cookieFile) - except OSError: - pass - - backend = GVDialer(cookieFile) - browser = backend._browser - - _TEST_WEBPAGES = [ - ("forward", backend._forwardURL), - ("token", backend._tokenURL), - ("login", backend._loginURL), - ("contacts", backend._contactsURL), - - ("voicemail", backend._voicemailURL), - ("sms", backend._smsURL), - - ("recent", backend._recentCallsURL), - ("placed", backend._placedCallsURL), - ("recieved", backend._receivedCallsURL), - ("missed", backend._missedCallsURL), - ] - - # 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 e.message - continue - print "\tWriting to file" - with open("loggedin_%s.txt" % name, "w") as f: - f.write(page) - - # Cookies - browser.cookies.save() - 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 - ) - - -if __name__ == "__main__": - import sys - logging.basicConfig(level=logging.DEBUG) - #test_backend(sys.argv[1], sys.argv[2]) - grab_debug_info(sys.argv[1], sys.argv[2]) diff --git a/src/gv_views.py b/src/gv_views.py index 98f4d88..cf9a733 100644 --- a/src/gv_views.py +++ b/src/gv_views.py @@ -34,8 +34,8 @@ import gtk import gtk_toolbox import hildonize -import gv_backend -import null_backend +from backends import gv_backend +from backends import null_backend _moduleLogger = logging.getLogger("gv_views") diff --git a/src/merge_backend.py b/src/merge_backend.py deleted file mode 100644 index 476a616..0000000 --- a/src/merge_backend.py +++ /dev/null @@ -1,153 +0,0 @@ -import logging - - -_moduleLogger = logging.getLogger("merge_backend") - - -class MergedAddressBook(object): - """ - Merger of all addressbooks - """ - - def __init__(self, addressbookFactories, sorter = None): - self.__addressbookFactories = addressbookFactories - self.__addressbooks = None - self.__sort_contacts = sorter if sorter is not None else self.null_sorter - - def clear_caches(self): - self.__addressbooks = None - for factory in self.__addressbookFactories: - factory.clear_caches() - - def get_addressbooks(self): - """ - @returns Iterable of (Address Book Factory, Book Id, Book Name) - """ - yield self, "", "" - - def open_addressbook(self, bookId): - return self - - def contact_source_short_name(self, contactId): - if self.__addressbooks is None: - return "" - bookIndex, originalId = contactId.split("-", 1) - return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId) - - @staticmethod - def factory_name(): - return "All Contacts" - - def get_contacts(self): - """ - @returns Iterable of (contact id, contact name) - """ - if self.__addressbooks is None: - self.__addressbooks = list( - factory.open_addressbook(id) - for factory in self.__addressbookFactories - for (f, id, name) in factory.get_addressbooks() - ) - contacts = ( - ("-".join([str(bookIndex), contactId]), contactName) - for (bookIndex, addressbook) in enumerate(self.__addressbooks) - for (contactId, contactName) in addressbook.get_contacts() - ) - sortedContacts = self.__sort_contacts(contacts) - return sortedContacts - - def get_contact_details(self, contactId): - """ - @returns Iterable of (Phone Type, Phone Number) - """ - if self.__addressbooks is None: - return [] - bookIndex, originalId = contactId.split("-", 1) - return self.__addressbooks[int(bookIndex)].get_contact_details(originalId) - - @staticmethod - def null_sorter(contacts): - """ - Good for speed/low memory - """ - return contacts - - @staticmethod - def basic_firtname_sorter(contacts): - """ - Expects names in "First Last" format - """ - contactsWithKey = [ - (contactName.rsplit(" ", 1)[0], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @staticmethod - def basic_lastname_sorter(contacts): - """ - Expects names in "First Last" format - """ - contactsWithKey = [ - (contactName.rsplit(" ", 1)[-1], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @staticmethod - def reversed_firtname_sorter(contacts): - """ - Expects names in "Last, First" format - """ - contactsWithKey = [ - (contactName.split(", ", 1)[-1], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @staticmethod - def reversed_lastname_sorter(contacts): - """ - Expects names in "Last, First" format - """ - contactsWithKey = [ - (contactName.split(", ", 1)[0], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @staticmethod - def guess_firstname(name): - if ", " in name: - return name.split(", ", 1)[-1] - else: - return name.rsplit(" ", 1)[0] - - @staticmethod - def guess_lastname(name): - if ", " in name: - return name.split(", ", 1)[0] - else: - return name.rsplit(" ", 1)[-1] - - @classmethod - def advanced_firstname_sorter(cls, contacts): - contactsWithKey = [ - (cls.guess_firstname(contactName), (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @classmethod - def advanced_lastname_sorter(cls, contacts): - contactsWithKey = [ - (cls.guess_lastname(contactName), (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) diff --git a/src/null_backend.py b/src/null_backend.py deleted file mode 100644 index c07f724..0000000 --- a/src/null_backend.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/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 -""" - - -class NullDialer(object): - - def __init__(self): - pass - - def is_authed(self, force = False): - return False - - def login(self, username, password): - return self.is_authed() - - def logout(self): - self.clear_caches() - - def dial(self, number): - return True - - def send_sms(self, number, message): - raise NotImplementedError("SMS Is Not Supported") - - def clear_caches(self): - pass - - def is_valid_syntax(self, number): - """ - @returns If This number be called ( syntax validation only ) - """ - return False - - def get_account_number(self): - """ - @returns The grand central phone number - """ - return "" - - def set_sane_callback(self): - pass - - def get_callback_numbers(self): - return {} - - def set_callback_number(self, callbacknumber): - return True - - def get_callback_number(self): - return "" - - def get_recent(self): - return () - - def get_addressbooks(self): - return () - - def open_addressbook(self, bookId): - return self - - @staticmethod - def contact_source_short_name(contactId): - return "ERROR" - - @staticmethod - def factory_name(): - return "ERROR" - - def get_contacts(self): - return () - - def get_contact_details(self, contactId): - return () - - def get_messages(self): - return () - - -class NullAddressBook(object): - """ - Minimal example of both an addressbook factory and an addressbook - """ - - def clear_caches(self): - pass - - def get_addressbooks(self): - """ - @returns Iterable of (Address Book Factory, Book Id, Book Name) - """ - yield self, "", "None" - - def open_addressbook(self, bookId): - return self - - @staticmethod - def contact_source_short_name(contactId): - return "" - - @staticmethod - def factory_name(): - return "" - - @staticmethod - def get_contacts(): - """ - @returns Iterable of (contact id, contact name) - """ - return [] - - @staticmethod - def get_contact_details(contactId): - """ - @returns Iterable of (Phone Type, Phone Number) - """ - return [] -- 1.7.9.5