Commits

Murty Rompalli committed 1fcded8

Minor Changes and Fixed LOGIN url to work with changed google voice page

  • Participants
  • Parent commits ea708f0

Comments (0)

Files changed (15)

-This software requires pygooglevoice maintained by Arno, available here:
-https://bitbucket.org/fracai/pygooglevoice
-
 Python Google Voice
 ===================
 
 You can use the Python API or command line script to schedule calls, check for new recieved calls/sms, or even sync your recorded voicemails/calls.  
 Works for Python 2 and Python 3
 
-Full documentation is available up at http://sphinxdoc.github.com/pygooglevoice/
+API Documentation is available at http://sphinxdoc.github.com/pygooglevoice/
+
+INSTALLATION
+============
+
+1. Login to your Linux machine as yourself, a regular user (not root)
+2. Extract the tar file gvmirror.tar.gz in your home directory
+3. If you do not already have .gvoice file in your home directory, create one as follows:
+
+   cd ~/gvmirror
+   [ -f ~/.gvoice ] || cp dot.gvoice ~/.gvoice
+
+4. Edit ~/.gvoice by typing: vi ~/.gvoice
+NOTE: In this file, please put your correct google account name, password and corresponding google voice phone number for fields email, password and forwardingNumber respectively
+5. Just run the script as follows:
+~/gvmirror/mirror-murty.py
+6. Feel free to browse the downloaded files in the directory: ~/gvoice/
+7. You may run the script again or nightly through cron if you want!
+8. Each time you run the script, it will only download the new messages, not all your messages.
+
+-----------
+
+pygooglevoice package is the fork of original pygooglevoice package maintained by Arno Hautala at https://bitbucket.org/fracai/pygooglevoice
+
+-----------
+
+User agent for Android:
+Mozilla/5.0 (Linux; U; Android 2.2.1; fr-ch; A43 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
+
+-------------------------
+
+URLs located at this page:
+http://www.googlevoice.org/pages.php?title=sniffing
+
+
+https://www.google.com/voice/inbox/message/?messageId=3fe3c0f7d61173c67da60d3691e70b9a14f09bf8&v=634
+
+https://www.google.com/voice/inbox/message/?messageId=3fe3c0f7d61173c67da60d3691e70b9a14f09bf8
+
+
+------
+
+name=_rnr_se value=FjhSozhzWprabboFUQ0bVo5jWmo=
+
+User agent: default
+
+https://www.google.com/voice/m?uipref=1
+
+https://www.google.com/voice/m?uipref=1#~voice:s=conversation&i=5f6f1046d79bdc528368f07a5ca647c8efebcf91
+
+
+After setting User agent to iPhone 3.0:
+https://www.google.com/voice/m#~voice:s=inbox&l=sms
+https://www.google.com/voice/m#~voice:s=conversation&i=47e9c4c4a3ad8f7751b0d1c13e4a1b9cbe8afa4d
+
+---
+
+TO get just times:
+User agent: default
+https://www.google.com/voice#message/5f6f1046d79bdc528368f07a5ca647c8efebcf91
+https://www.google.com/voice/#message/5f6f1046d79bdc528368f07a5ca647c8efebcf91
+https://www.google.com/voice/inbox/#message/47e9c4c4a3ad8f7751b0d1c13e4a1b9cbe8afa4d
+
+All Parent SMS:
+https://www.google.com/voice/m/i/sms
+https://www.google.com/voice/m/i/sms?p=2
+etc
+
+All voicemail:
+https://www.google.com/voice/m/i/voicemail
+https://www.google.com/voice/m/i/voicemail/?p=2
+
+---
+
+Google Voice CPAN
+http://search.cpan.org/~tempire/Google-Voice-0.03/lib/Google/Voice.pm
+
+https://www.gvmax.com/
+
+https://code.google.com/p/gtalksms/
+
+---
+
+testing sms id:
+aa7d04fc9b7145e43db1f5afa9c007ca3e179eca
+
+Get Parent SMS messages in XML:
+https://www.google.com/voice/inbox/recent/sms/
+https://www.google.com/voice/inbox/recent/sms/?page=p2
+... etc
+
+https://www.google.com/voice/inbox/recent/unread/
+https://www.google.com/voice/m/i/unread
+
+Get Complete SMS chain for a given parent message in XML:
+https://www.google.com/voice/inbox/message/?messageId=ID
+https://www.google.com/voice/inbox/message/?messageId=ID&page=p2
+... etc
-TODO:
+TODO
+====
 
-1. Download contacts and current account configuration
-2. Create voicemail.html and recorded.html containinng all transcribed lines
+*  SMSAUTH in googlevoice/settings.py probably needs to be fixed!
+
+*  Download contacts and current account configuration
+
+*  Create voicemail.html and recorded.html containinng all transcribed lines
    such that clicking on transcription plays the correct mp3 file
-3. Create sms.html that shows all SMS conversations. Each conversation thread is enclosed in a rectangle.
-4. Add support to resume downloading from the first non-downloaded message, in case of interrupted download
+
+*  Create sms.html that shows all SMS conversations. Each conversation thread is enclosed in a rectangle.
+
+*  Add support to resume downloading from the first non-downloaded message, in case of interrupted download
+[auth]
+# Google Account email address (one associated w/ your Voice account)
+email=your-google-username
+
+# Raw password used or login
+password=your-password
+
+[gvoice]
+# Number to place calls from (eg, your google voice number)
+forwardingNumber=2195554201
+
+# Default phoneType for your forwardingNumber as defined below
+#  1 - Home
+#  2 - Mobile
+#  3 - Work
+#  7 - Gizmo
+phoneType=2

googlevoice/__init__.py

+"""
+This project aims to bring the power of the Google Voice API to the Python language in a simple,
+easy-to-use manner. Currently it allows you to place calls, send sms,
+download voicemails/recorded messages, and search the various folders of your Google Voice Accounts.
+You can use the Python API or command line script to schedule calls, check for new received calls/sms,
+or even sync your recorded voicemails/calls.
+Works for Python 2 and Python 3
+
+"""
+__author__ = 'Justin Quick and Joe McCall'
+__email__ = 'justquick@gmail.com, joe@mcc4ll.us',
+__copyright__ = 'Copyright 2009, Justin Quick and Joe McCall'
+__credits__ = ['Justin Quick','Joe McCall','Jacob Feisley','John Nagle']
+__license__ = 'New BSD'
+__version__ = '0.5'
+
+from voice import Voice
+from util import Phone, Message, Folder

googlevoice/__init__.pyc

Binary file added.

googlevoice/conf.py

+from ConfigParser import ConfigParser, NoOptionError
+import os
+import settings
+
+
+class Config(ConfigParser):
+    """
+    ``ConfigParser`` subclass that looks into your home folder for a file named
+    ``.gvoice`` and parses configuration data from it.
+    """
+    def __init__(self):
+        self.fname = os.path.expanduser('~/.gvoice')
+
+        if not os.path.exists(self.fname):
+            try:
+                f = open(self.fname, 'w')
+            except IOError:
+                return
+            f.write(settings.DEFAULT_CONFIG)
+            f.close()
+            
+        ConfigParser.__init__(self)
+        try:
+            self.read([self.fname])
+        except IOError:
+            return
+
+    def get(self, option, section='gvoice'):
+        try:
+            return ConfigParser.get(self, section, option).strip() or None
+        except NoOptionError:
+            return
+        
+    def set(self, option, value, section='gvoice'):
+        return ConfigParser.set(self, section, option, value)
+
+    def phoneType(self):
+        try:
+            return int(self.get('phoneType'))
+        except TypeError:
+            return
+        
+    def save(self):
+        f = open(self.fname, 'w')
+        self.write(f)
+        f.close()
+        
+
+    phoneType = property(phoneType)
+    forwardingNumber = property(lambda self: self.get('forwardingNumber'))
+    email = property(lambda self: self.get('email','auth'))
+    password = property(lambda self: self.get('password','auth'))
+    smsKey = property(lambda self: self.get('smsKey','auth'))
+    secret = property(lambda self: self.get('secret'))
+    
+config = Config()

googlevoice/conf.pyc

Binary file added.

googlevoice/settings.py

+DEFAULT_CONFIG = """
+[auth]
+# Google Account email address (one associated w/ your Voice account)
+email=
+
+# Raw password used or login
+password=
+
+# Optional 2-step authentication key (as provided by Google)
+smsKey=
+
+[gvoice]
+# Number to place calls from (eg, your google voice number)
+forwardingNumber=
+
+# Default phoneType for your forwardingNumber as defined below
+#  1 - Home
+#  2 - Mobile
+#  3 - Work
+#  7 - Gizmo
+phoneType=2
+"""
+
+TYPES = {
+     0: 'missed',
+     1: 'received',
+     2: 'voicemail',
+     4: 'recorded',
+     7: 'placed',
+    10: 'sms.received',
+    11: 'sms.sent'
+}
+
+DEBUG = False
+#LOGIN = 'https://www.google.com/accounts/ServiceLoginAuth?service=grandcentral'
+LOGIN = 'https://accounts.google.com/ServiceLoginAuth?service=grandcentral'
+#LOGIN = 'https://accounts.google.com/ServiceLogin?service=grandcentral'
+SMSAUTH = 'https://www.google.com/accounts/SmsAuth'
+FEEDS = ('inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
+        'recorded', 'placed', 'received', 'missed')
+
+BASE = 'https://www.google.com/voice/'
+LOGOUT = BASE + 'account/signout'
+INBOX = BASE + '#inbox'
+CALL = BASE + 'call/connect/'
+CANCEL = BASE + 'call/cancel/'
+DEFAULT_FORWARD = BASE + 'settings/editDefaultForwarding/'
+FORWARD = BASE + 'settings/editForwarding/'
+DELETE = BASE + 'inbox/deleteMessages/'
+ARCHIVE = BASE + 'inbox/archiveMessages/'
+MARK = BASE + 'inbox/mark/'
+STAR = BASE + 'inbox/star/'
+SMS = BASE + 'sms/send/'
+DOWNLOAD = BASE + 'media/send_voicemail/'
+BALANCE = BASE + 'settings/billingcredit/'
+
+XML_SEARCH = BASE + 'inbox/search/'
+XML_CONTACTS = BASE + 'contacts/'
+XML_RECENT = BASE + 'inbox/recent/'
+XML_MESSAGE = BASE + 'inbox/message/'
+XML_INBOX = XML_RECENT + 'inbox/'
+XML_STARRED = XML_RECENT + 'starred/'
+XML_ALL = XML_RECENT + 'all/'
+XML_SPAM = XML_RECENT + 'spam/'
+XML_TRASH = XML_RECENT + 'trash/'
+XML_VOICEMAIL = XML_RECENT + 'voicemail/'
+XML_SMS = XML_RECENT + 'sms/'
+XML_RECORDED = XML_RECENT + 'recorded/'
+XML_PLACED = XML_RECENT + 'placed/'
+XML_RECEIVED = XML_RECENT + 'received/'
+XML_MISSED = XML_RECENT + 'missed/'

googlevoice/settings.pyc

Binary file added.

googlevoice/tests.py

+from googlevoice import Voice, util
+from os import path, remove
+from unittest import TestCase, main
+
+class VoiceTest(TestCase):
+    voice = Voice()
+    voice.login()
+    outgoing = util.input('Outgoing number (blank to ignore call tests): ')
+    forwarding = None
+    if outgoing:
+        forwarding = util.input('Forwarding number [optional]: ') or None
+    
+    if outgoing:
+        def test_1call(self):
+            self.voice.call(self.outgoing, self.forwarding)
+
+        def test_sms(self):
+            self.voice.send_sms(self.outgoing, 'i sms u')
+
+        def test_2cancel(self):
+            self.voice.cancel(self.outgoing, self.forwarding)
+    
+    def test_special(self):
+        self.assert_(self.voice.special)
+        
+    def test_inbox(self):
+        self.assert_(self.voice.inbox)
+    
+    def test_balance(self):
+        self.assert_(self.voice.settings['credits'])
+        
+    def test_search(self):
+        self.assert_(len(self.voice.search('joe')))
+    
+    def test_disable_enable(self):
+        self.voice.phones[0].disable()
+        self.voice.phones[0].enable()
+    
+    def test_download(self):
+        msg = list(self.voice.voicemail.messages)[0]
+        fn = '%s.mp3' % msg.id
+        if path.isfile(fn): remove(fn)
+        self.voice.download(msg)
+        self.assert_(path.isfile(fn))
+    
+    def test_zlogout(self):
+        self.voice.logout()
+        self.assert_(self.voice.special is None)
+        
+    def test_config(self):
+        from conf import config
+        self.assert_(config.forwardingNumber)
+        self.assert_(str(config.phoneType) in '1237')
+        self.assertEqual(config.get('wtf'), None)
+        
+if __name__ == '__main__': main()

googlevoice/util.py

+import re
+from sys import stdout
+from xml.parsers.expat import ParserCreate
+from time import gmtime
+from datetime import datetime
+from pprint import pprint
+try:
+    from urllib2 import build_opener,install_opener, \
+        HTTPCookieProcessor,Request,urlopen
+    from urllib import urlencode,quote
+except ImportError:
+    from urllib.request import build_opener,install_opener, \
+        HTTPCookieProcessor,Request,urlopen
+    from urllib.parse import urlencode,quote
+try:
+    from http.cookiejar import LWPCookieJar as CookieJar
+except ImportError:
+    from cookielib import LWPCookieJar as CookieJar
+try:
+    from json import loads
+except ImportError:
+    from simplejson import loads
+try:
+    input = raw_input
+except NameError:
+    input = input
+
+sha1_re = re.compile(r'^[a-fA-F0-9]{40}$')
+
+def print_(*values, **kwargs):
+    """
+    Implementation of Python3's print function
+    
+    Prints the values to a stream, or to sys.stdout by default.
+    Optional keyword arguments:
+    
+    file: a file-like object (stream); defaults to the current sys.stdout.
+    sep:  string inserted between values, default a space.
+    end:  string appended after the last value, default a newline.
+    """
+    fo = kwargs.pop('file', stdout)
+    fo.write(kwargs.pop('sep', ' ').join(map(str, values)))
+    fo.write(kwargs.pop('end', '\n'))
+    fo.flush()
+
+def is_sha1(s):
+    """
+    Returns ``True`` if the string is a SHA1 hash
+    """
+    return bool(sha1_re.match(s))
+
+def validate_response(response):
+    """
+    Validates that the JSON response is A-OK
+    """
+    try:
+        assert 'ok' in response and response['ok']
+    except AssertionError:
+        raise ValidationError('There was a problem with GV: %s' % response)
+
+def load_and_validate(response):
+    """
+    Loads JSON data from http response then validates
+    """
+    validate_response(loads(response.read()))
+
+class ValidationError(Exception):
+    """
+    Bombs when response code back from Voice 500s
+    """
+
+class LoginError(Exception):
+    """
+    Occurs when login credentials are incorrect
+    """
+    
+class ParsingError(Exception):
+    """
+    Happens when XML feed parsing fails
+    """
+    
+class JSONError(Exception):
+    """
+    Failed JSON deserialization
+    """
+    
+class DownloadError(Exception):
+    """
+    Cannot download message, probably not in voicemail/recorded
+    """
+    
+class ForwardingError(Exception):
+    """
+    Forwarding number given was incorrect
+    """
+    
+    
+class AttrDict(dict):
+    def __getattr__(self, attr):
+        if attr in self:
+            return self[attr]
+
+class Phone(AttrDict):
+    """
+    Wrapper for phone objects used for phone specific methods
+    Attributes are:
+    
+     * id: int
+     * phoneNumber: i18n phone number
+     * formattedNumber: humanized phone number string
+     * we: data dict
+     * wd: data dict
+     * verified: bool
+     * name: strign label
+     * smsEnabled: bool
+     * scheduleSet: bool
+     * policyBitmask: int
+     * weekdayTimes: list
+     * dEPRECATEDDisabled: bool
+     * weekdayAllDay: bool
+     * telephonyVerified
+     * weekendTimes: list
+     * active: bool
+     * weekendAllDay: bool
+     * enabledForOthers: bool
+     * type: int (1 - Home, 2 - Mobile, 3 - Work, 4 - Gizmo)
+            
+    """
+    def __init__(self, voice, data):
+        self.voice = voice
+        super(Phone, self).__init__(data)
+    
+    def enable(self,):
+        """
+        Enables this phone for usage
+        """
+        return self.__call_forwarding()
+
+    def disable(self):
+        """
+        Disables this phone
+        """
+        return self.__call_forwarding('0')
+        
+    def __call_forwarding(self, enabled='1'):
+        """
+        Enables or disables this phone
+        """
+        self.voice.__validate_special_page('default_forward',
+            {'enabled':enabled, 'phoneId': self.id})
+        
+    def __str__(self):
+        return self.phoneNumber
+    
+    def __repr__(self):
+        return '<Phone %s>' % self.phoneNumber
+        
+class Message(AttrDict):
+    """
+    Wrapper for all call/sms message instances stored in Google Voice
+    Attributes are:
+    
+     * id: SHA1 identifier
+     * isTrash: bool
+     * displayStartDateTime: datetime
+     * star: bool
+     * isSpam: bool
+     * startTime: gmtime
+     * labels: list
+     * displayStartTime: time
+     * children: str
+     * note: str
+     * isRead: bool
+     * displayNumber: str
+     * relativeStartTime: str
+     * phoneNumber: str
+     * type: int
+     
+    """
+    def __init__(self, folder, id, data):
+        assert is_sha1(id), 'Message id not a SHA1 hash'
+        self.folder = folder
+        self.id = id
+        super(AttrDict, self).__init__(data)
+        self['startTime'] = gmtime(int(self['startTime'])/1000)
+        self['displayStartDateTime'] = datetime.strptime(
+                self['displayStartDateTime'], '%m/%d/%y %I:%M %p')
+        self['displayStartTime'] = self['displayStartDateTime'].time()
+    
+    def archive(self, archive=1):
+        """
+        Archive this message by removing it from the Inbox.
+        Use ``message.archive(0)`` to move it back to Inbox.
+        """
+        self.folder.voice.__messages_post('archive', self.id, archive=archive)
+    
+    def unarchive(self):
+        """
+        Unarchive this message by moving it back to the Inbox.
+        """
+        self.archive(self, archive=0)
+
+    def delete(self, trash=1):
+        """
+        Moves this message to the Trash. Use ``message.delete(0)`` to move it out of the Trash.
+        """
+        self.folder.voice.__messages_post('delete', self.id, trash=trash)
+    
+    def undelete(self):
+        """
+        Move this message out of the Trash.
+        """
+        self.delete(self, trash=0)
+
+    def star(self, star=1):
+        """
+        Star this message. Use ``message.star(0)`` to unstar it.
+        """
+        self.folder.voice.__messages_post('star', self.id, star=star)
+        
+    def mark(self, read=1):
+        """
+        Mark this message as read. Use ``message.mark(0)`` to mark it as unread.
+        """
+        self.folder.voice.__messages_post('mark', self.id, read=read)
+        
+    def download(self, adir=None, filename=None):
+        """
+        Download the message MP3 (if any). 
+        Saves files to ``adir`` (defaults to current directory). 
+        Message hashes can be found in ``self.voicemail().messages`` for example. 
+        Returns location of saved file.        
+        """
+        return self.folder.voice.download(self, adir, filename)
+
+    def __str__(self):
+        return self.id
+    
+    def __repr__(self):
+        return '<Message #%s (%s)>' % (self.id, self.phoneNumber)
+
+class Folder(AttrDict):
+    """
+    Folder wrapper for feeds from Google Voice
+    Attributes are:
+    
+     * totalSize: int (aka ``__len__``)
+     * unreadCounts: dict
+     * resultsPerPage: int
+     * messages: list of Message instances
+    """
+    def __init__(self, voice, name, data):
+        self.voice = voice
+        self.name = name
+        super(AttrDict, self).__init__(data)
+        
+    def messages(self):
+        """
+        Returns a list of all messages in this folder
+        """
+        return [Message(self, *i) for i in self['messages'].items()]
+    messages = property(messages)
+    
+    def __len__(self):
+        return self['totalSize']
+
+    def __repr__(self):
+        return '<Folder %s (%s)>' % (self.name, len(self))
+    
+class XMLParser(object):
+    """
+    XML Parser helper that can dig json and html out of the feeds. 
+    The parser takes a ``Voice`` instance, page name, and function to grab data from. 
+    Calling the parser calls the data function once, sets up the ``json`` and ``html``
+    attributes and returns a ``Folder`` instance for the given page::
+    
+        >>> o = XMLParser(voice, 'voicemail', lambda: 'some xml payload')
+        >>> o()
+        ... <Folder ...>
+        >>> o.json
+        ... 'some json payload'
+        >>> o.data
+        ... 'loaded json payload'
+        >>> o.html
+        ... 'some html payload'
+        
+    """
+    attr = None
+        
+    def start_element(self, name, attrs):
+        if name in ('json','html'):
+            self.attr = name
+    def end_element(self, name): self.attr = None
+    def char_data(self, data):
+        if self.attr and data:
+            setattr(self, self.attr, getattr(self, self.attr) + data)
+
+    def __init__(self, voice, name, datafunc):
+        self.json, self.html = '',''
+        self.datafunc = datafunc
+        self.voice = voice
+        self.name = name
+        
+    def __call__(self, terms={}):
+        self.json, self.html = '',''
+        parser = ParserCreate()
+        parser.StartElementHandler = self.start_element
+        parser.EndElementHandler = self.end_element
+        parser.CharacterDataHandler = self.char_data
+        try:
+            data = self.datafunc(terms)
+            parser.Parse(data, 1)
+        except:
+            raise ParsingError
+        return self.folder
+
+    def folder(self):
+        """
+        Returns associated ``Folder`` instance for given page (``self.name``)
+        """
+        return Folder(self.voice, self.name, self.data)        
+    folder = property(folder)
+    
+    def data(self):
+        """
+        Returns the parsed json information after calling the XMLParser
+        """
+        try:
+            return loads(self.json)
+        except:
+            raise JSONError
+    data = property(data)
+    

googlevoice/util.pyc

Binary file added.

googlevoice/voice.py

+from conf import config
+from util import *
+import settings
+import base64
+import os
+import re
+
+qpat = re.compile(r'\?')
+
+if settings.DEBUG:
+    import logging
+    logging.basicConfig()
+    log = logging.getLogger('PyGoogleVoice')
+    log.setLevel(logging.DEBUG)
+else:
+    log = None
+
+class Voice(object):
+    """
+    Main voice instance for interacting with the Google Voice service
+    Handles login/logout and most of the baser HTTP methods
+    """
+    def __init__(self):
+        install_opener(build_opener(HTTPCookieProcessor(CookieJar())))
+
+        for name in settings.FEEDS:
+            setattr(self, name, self.__get_xml_page(name))
+        
+        setattr(self, 'message', self.__get_xml_page('message'))
+        
+    ######################
+    # Some handy methods
+    ######################  
+    def special(self):
+        """
+        Returns special identifier for your session (if logged in)
+        """
+        if hasattr(self, '_special') and getattr(self, '_special'):
+            return self._special
+        try:
+            try:
+                regex = bytes("('_rnr_se':) '(.+)'", 'utf8')
+            except TypeError:
+                regex = bytes("('_rnr_se':) '(.+)'")
+        except NameError:
+            regex = r"('_rnr_se':) '(.+)'"
+        try:
+            sp = re.search(regex, urlopen(settings.INBOX).read()).group(2)
+        except AttributeError:
+            sp = None
+        self._special = sp
+        return sp
+    special = property(special)
+    
+    def login(self, email=None, passwd=None, smsKey=None):
+        """
+        Login to the service using your Google Voice account
+        Credentials will be propmpted for if not given as args or in the ``~/.gvoice`` config file
+        """
+        if hasattr(self, '_special') and getattr(self, '_special'):
+            return self
+        
+        if email is None:
+            email = config.email
+        if email is None:
+            email = input('Email address: ')
+        
+        if passwd is None:
+            passwd = config.password
+        if passwd is None:
+            from getpass import getpass
+            passwd = getpass()
+
+        from os import path
+        
+        content = self.__do_page('login').read()
+        # holy hackjob
+        galx = re.search(r"name=\"GALX\"\s+value=\"([^\"]+)\"", content).group(1)
+        result = self.__do_page('login', {'Email': email, 'Passwd': passwd, 'GALX': galx})
+        
+        if result.geturl() == getattr(settings, "SMSAUTH"):
+            content = self.__smsAuth(smsKey)
+            
+            try:
+                smsToken = re.search(r"name=\"smsToken\"\s+value=\"([^\"]+)\"", content).group(1)
+                galx = re.search(r"name=\"GALX\"\s+value=\"([^\"]+)\"", content).group(1)
+                content = self.__do_page('login', {'smsToken': smsToken, 'service': "grandcentral", 'GALX': galx})
+            except AttributeError:
+                raise LoginError
+                
+            del smsKey, smsToken, galx
+        
+        del email, passwd
+        
+        try:
+            assert self.special
+        except (AssertionError, AttributeError):
+            raise LoginError
+
+        return self
+        
+    def __smsAuth(self, smsKey=None):
+        if smsKey is None:
+            smsKey = config.smsKey
+        
+        if smsKey is None:
+            from getpass import getpass
+            smsPin = getpass("SMS PIN: ")
+            content = self.__do_page('smsauth', {'smsUserPin': smsPin}).read()
+        
+        else:
+            smsKey  = base64.b32decode(re.sub(r' ', '', smsKey), casefold=True).encode("hex")
+            content = self.__oathtoolAuth(smsKey)
+            
+            try_count = 1
+            
+            while "The code you entered didn&#39;t verify." in content and try_count < 5:
+                sleep_seconds = 10
+                try_count += 1
+                print 'invalid code, retrying after {0} seconds (attempt {1})'.format(sleep_seconds, try_count)
+                import time
+                time.sleep(sleep_seconds)
+                content = self.__oathtoolAuth(smsKey)
+        
+        del smsKey
+        
+        return content
+    
+    def __oathtoolAuth(self, smsKey):
+        import commands
+        smsPin = commands.getstatusoutput('oathtool --totp '+smsKey)[1]
+        content = self.__do_page('smsauth', {'smsUserPin': smsPin}).read()
+        del smsPin
+        return content
+        
+    
+    def logout(self):
+        """
+        Logs out an instance and makes sure it does not still have a session
+        """
+        self.__do_page('logout')
+        del self._special 
+        assert self.special == None
+        return self
+        
+    def call(self, outgoingNumber, forwardingNumber=None, phoneType=None, subscriberNumber=None):
+        """
+        Make a call to an ``outgoingNumber`` from your ``forwardingNumber`` (optional).
+        If you pass in your ``forwardingNumber``, please also pass in the correct ``phoneType``
+        """        
+        if forwardingNumber is None:
+            forwardingNumber = config.forwardingNumber
+        if phoneType is None:
+            phoneType = config.phoneType
+            
+        self.__validate_special_page('call', {
+            'outgoingNumber': outgoingNumber,
+            'forwardingNumber': forwardingNumber,
+            'subscriberNumber': subscriberNumber or 'undefined',
+            'phoneType': phoneType,
+            'remember': '1'
+        })
+        
+    __call__ = call
+    
+    def cancel(self, outgoingNumber=None, forwardingNumber=None):
+        """
+        Cancels a call matching outgoing and forwarding numbers (if given). 
+        Will raise an error if no matching call is being placed
+        """
+        self.__validate_special_page('cancel', {
+            'outgoingNumber': outgoingNumber or 'undefined',
+            'forwardingNumber': forwardingNumber or 'undefined',
+            'cancelType': 'C2C',
+        })
+
+    def phones(self):
+        """
+        Returns a list of ``Phone`` instances attached to your account.
+        """
+        return [Phone(self, data) for data in self.contacts['phones'].values()]
+    phones = property(phones)
+
+    def settings(self):
+        """
+        Dict of current Google Voice settings
+        """
+        return AttrDict(self.contacts['settings'])
+    settings = property(settings)
+    
+    def send_sms(self, phoneNumber, text):
+        """
+        Send an SMS message to a given ``phoneNumber`` with the given ``text`` message
+        """
+        self.__validate_special_page('sms', {'phoneNumber': phoneNumber, 'text': text})
+
+    def search(self, query):
+        """
+        Search your Google Voice Account history for calls, voicemails, and sms
+        Returns ``Folder`` instance containting matching messages
+        """
+        return self.__get_xml_page('search', data='?q=%s' % quote(query))()
+        
+    def archive(self, msg, archive=1):
+        """
+        Archive the specified message by removing it from the Inbox.
+        """
+        if isinstance(msg, Message):
+            msg = msg.id
+        assert is_sha1(msg), 'Message id not a SHA1 hash'
+        self.__messages_post('archive', msg, archive=archive)
+        
+    def delete(self, msg, trash=1):
+        """
+        Moves this message to the Trash. Use ``message.delete(0)`` to move it out of the Trash.
+        """
+        if isinstance(msg, Message):
+            msg = msg.id
+        assert is_sha1(msg), 'Message id not a SHA1 hash'
+        self.__messages_post('delete', msg, trash=trash)
+
+    def download(self, msg, adir=None, filename=None):
+        """
+        Download a voicemail or recorded call MP3 matching the given ``msg``
+        which can either be a ``Message`` instance, or a SHA1 identifier. 
+        Saves files to ``adir`` (defaults to current directory). 
+        Message hashes can be found in ``self.voicemail().messages`` for example. 
+        Returns location of saved file.
+        """
+        from os import path,getcwd
+        if isinstance(msg, Message):
+            msg = msg.id
+        assert is_sha1(msg), 'Message id not a SHA1 hash'
+        if adir is None:
+            adir = getcwd()
+        if filename is None:
+            filename = "%s.mp3" % msg
+        try:
+            response = self.__do_page('download', msg)
+        except:
+            raise DownloadError
+        fn = path.join(adir, filename)
+        fo = open(fn, 'wb')
+        fo.write(response.read())
+        fo.close()
+        return fn
+    
+    def contacts(self):
+        """
+        Partial data of your Google Account Contacts related to your Voice account.
+        For a more comprehensive suite of APIs, check out http://code.google.com/apis/contacts/docs/1.0/developers_guide_python.html
+        """
+        if hasattr(self, '_contacts'):
+            return self._contacts
+        self._contacts = self.__get_xml_page('contacts')()
+        return self._contacts
+    contacts = property(contacts)
+
+    ######################
+    # Helper methods
+    ######################
+
+    
+    def __do_page(self, page, data=None, headers={}, terms={}):
+        """
+        Loads a page out of the settings and pass it on to urllib Request
+        """
+        page = page.upper()
+        if isinstance(data, dict) or isinstance(data, tuple):
+            data = urlencode(data)
+        headers.update({'User-Agent': 'PyGoogleVoice/0.5'})
+        if log:
+            log.debug('%s?%s - %s' % (getattr(settings, page)[22:], data or '', headers))
+        if page in ('DOWNLOAD','XML_SEARCH'):
+            return urlopen(Request(getattr(settings, page) + data, None, headers))
+        if data:
+            headers.update({'Content-type': 'application/x-www-form-urlencoded;charset=utf-8'})
+        pageuri = getattr(settings, page)
+        if len (terms) > 0:
+            m = qpat.match(page)
+            if m:
+                pageuri += '&'
+            else:
+                pageuri += '?'
+            for i,k in enumerate(terms.keys()):
+                pageuri += k+'='+terms[k]
+                if i < len(terms)-1:
+                    pageuri += '&'
+        return urlopen(Request(pageuri, data, headers))
+
+    def __validate_special_page(self, page, data={}, **kwargs):
+        """
+        Validates a given special page for an 'ok' response
+        """
+        data.update(kwargs)
+        load_and_validate(self.__do_special_page(page, data))
+
+    _Phone__validate_special_page = __validate_special_page
+    
+    def __do_special_page(self, page, data=None, headers={}, terms={}):
+        """
+        Add self.special to request data
+        """
+        assert self.special, 'You must login before using this page'
+        if isinstance(data, tuple):
+            data += ('_rnr_se', self.special)
+        elif isinstance(data, dict):
+            data.update({'_rnr_se': self.special})
+        return self.__do_page(page, data, headers, terms)
+        
+    _Phone__do_special_page = __do_special_page
+    
+    def __get_xml_page(self, page, data=None, headers={}):
+        """
+        Return XMLParser instance generated from given page
+        """
+        return XMLParser(self, page, lambda terms={}: self.__do_special_page('XML_%s' % page.upper(), data, headers, terms).read())
+      
+    def __messages_post(self, page, *msgs, **kwargs):
+        """
+        Performs message operations, eg deleting,staring,moving
+        """
+        data = kwargs.items()
+        for msg in msgs:
+            if isinstance(msg, Message):
+                msg = msg.id
+            assert is_sha1(msg), 'Message id not a SHA1 hash'
+            data += (('messages',msg),)
+        return self.__do_special_page(page, dict(data))
+    
+    _Message__messages_post = __messages_post

googlevoice/voice.pyc

Binary file added.
 tries = 4
 
 # Download directory
-base = 'downloaded/'
-datefile = base + 'STATUS.' + download_type
+base = '~/gvoice/'
 
 from googlevoice import Voice
 from googlevoice.util import LoginError
 feeds = ['received','placed','missed','recorded','voicemail','sms']
 mp3Feeds = ['recorded','voicemail']
 
+base = os.path.expanduser(base)
+datefile = base + 'STATUS.' + download_type
+
 def fixmessage(msg,feed):
     if 'relativeStartTime' in msg:
         msg['relativeStartTime'] = ''