Some cleanup
authorepage <eopage@byu.net>
Thu, 12 Nov 2009 02:33:18 +0000 (02:33 +0000)
committerepage <eopage@byu.net>
Thu, 12 Nov 2009 02:33:18 +0000 (02:33 +0000)
git-svn-id: file:///svnroot/gc-dialer/trunk@570 c39d3808-3fe2-4d86-a59f-b7f623ee9f21

14 files changed:
src/alarm_notify.py
src/backends/__init__.py [new file with mode: 0644]
src/backends/browser_emu.py [new file with mode: 0644]
src/backends/file_backend.py [new file with mode: 0644]
src/backends/gv_backend.py [new file with mode: 0644]
src/backends/merge_backend.py [new file with mode: 0644]
src/backends/null_backend.py [new file with mode: 0644]
src/browser_emu.py [deleted file]
src/dc_glade.py
src/file_backend.py [deleted file]
src/gv_backend.py [deleted file]
src/gv_views.py
src/merge_backend.py [deleted file]
src/null_backend.py [deleted file]

index 1920a59..d71b030 100755 (executable)
@@ -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 (file)
index 0000000..e69de29
diff --git a/src/backends/browser_emu.py b/src/backends/browser_emu.py
new file mode 100644 (file)
index 0000000..056f204
--- /dev/null
@@ -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 (file)
index 0000000..b373561
--- /dev/null
@@ -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 (file)
index 0000000..bc98467
--- /dev/null
@@ -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"""<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._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"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
+               self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
+               self._contactsURL = "https://www.google.com/voice/mobile/contacts"
+               self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", 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*<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._messagesContactID = 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_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": "<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
+
+       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 = [
+                       "<b>%s</b>: %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 (file)
index 0000000..476a616
--- /dev/null
@@ -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 (file)
index 0000000..c07f724
--- /dev/null
@@ -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 (file)
index 056f204..0000000
+++ /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
index ead1bdf..edb2a5a 100755 (executable)
@@ -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 (file)
index b373561..0000000
+++ /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 (file)
index bc98467..0000000
+++ /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"""<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._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"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
-               self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
-               self._contactsURL = "https://www.google.com/voice/mobile/contacts"
-               self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", 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*<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._messagesContactID = 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_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": "<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
-
-       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 = [
-                       "<b>%s</b>: %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])
index 98f4d88..cf9a733 100644 (file)
@@ -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 (file)
index 476a616..0000000
+++ /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 (file)
index c07f724..0000000
+++ /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 []