00001 import re 00002 from sys import stdout 00003 from xml.parsers.expat import ParserCreate 00004 from time import gmtime 00005 from datetime import datetime 00006 from pprint import pprint 00007 try: 00008 from urllib2 import build_opener,install_opener, \ 00009 HTTPCookieProcessor,Request,urlopen 00010 from urllib import urlencode,quote 00011 except ImportError: 00012 from urllib.request import build_opener,install_opener, \ 00013 HTTPCookieProcessor,Request,urlopen 00014 from urllib.parse import urlencode,quote 00015 try: 00016 from http.cookiejar import LWPCookieJar as CookieJar 00017 except ImportError: 00018 from cookielib import LWPCookieJar as CookieJar 00019 try: 00020 from json import loads 00021 except ImportError: 00022 from simplejson import loads 00023 try: 00024 input = raw_input 00025 except NameError: 00026 input = input 00027 00028 sha1_re = re.compile(r'^[a-fA-F0-9]{40}$') 00029 00030 def print_(*values, **kwargs): 00031 """ 00032 Implementation of Python3's print function 00033 00034 Prints the values to a stream, or to sys.stdout by default. 00035 Optional keyword arguments: 00036 00037 file: a file-like object (stream); defaults to the current sys.stdout. 00038 sep: string inserted between values, default a space. 00039 end: string appended after the last value, default a newline. 00040 """ 00041 fo = kwargs.pop('file', stdout) 00042 fo.write(kwargs.pop('sep', ' ').join(map(str, values))) 00043 fo.write(kwargs.pop('end', '\n')) 00044 fo.flush() 00045 00046 def is_sha1(s): 00047 """ 00048 Returns ``True`` if the string is a SHA1 hash 00049 """ 00050 return bool(sha1_re.match(s)) 00051 00052 def validate_response(response): 00053 """ 00054 Validates that the JSON response is A-OK 00055 """ 00056 try: 00057 assert 'ok' in response and response['ok'] 00058 except AssertionError: 00059 raise ValidationError('There was a problem with GV: %s' % response) 00060 00061 def load_and_validate(response): 00062 """ 00063 Loads JSON data from http response then validates 00064 """ 00065 validate_response(loads(response.read())) 00066 00067 class ValidationError(Exception): 00068 """ 00069 Bombs when response code back from Voice 500s 00070 """ 00071 00072 class LoginError(Exception): 00073 """ 00074 Occurs when login credentials are incorrect 00075 """ 00076 00077 class ParsingError(Exception): 00078 """ 00079 Happens when XML feed parsing fails 00080 """ 00081 00082 class JSONError(Exception): 00083 """ 00084 Failed JSON deserialization 00085 """ 00086 00087 class DownloadError(Exception): 00088 """ 00089 Cannot download message, probably not in voicemail/recorded 00090 """ 00091 00092 class ForwardingError(Exception): 00093 """ 00094 Forwarding number given was incorrect 00095 """ 00096 00097 00098 class AttrDict(dict): 00099 def __getattr__(self, attr): 00100 if attr in self: 00101 return self[attr] 00102 00103 class Phone(AttrDict): 00104 """ 00105 Wrapper for phone objects used for phone specific methods 00106 Attributes are: 00107 00108 * id: int 00109 * phoneNumber: i18n phone number 00110 * formattedNumber: humanized phone number string 00111 * we: data dict 00112 * wd: data dict 00113 * verified: bool 00114 * name: strign label 00115 * smsEnabled: bool 00116 * scheduleSet: bool 00117 * policyBitmask: int 00118 * weekdayTimes: list 00119 * dEPRECATEDDisabled: bool 00120 * weekdayAllDay: bool 00121 * telephonyVerified 00122 * weekendTimes: list 00123 * active: bool 00124 * weekendAllDay: bool 00125 * enabledForOthers: bool 00126 * type: int (1 - Home, 2 - Mobile, 3 - Work, 4 - Gizmo) 00127 00128 """ 00129 def __init__(self, voice, data): 00130 self.voice = voice 00131 super(Phone, self).__init__(data) 00132 00133 def enable(self,): 00134 """ 00135 Enables this phone for usage 00136 """ 00137 return self.__call_forwarding() 00138 00139 def disable(self): 00140 """ 00141 Disables this phone 00142 """ 00143 return self.__call_forwarding('0') 00144 00145 def __call_forwarding(self, enabled='1'): 00146 """ 00147 Enables or disables this phone 00148 """ 00149 self.voice.__validate_special_page('default_forward', 00150 {'enabled':enabled, 'phoneId': self.id}) 00151 00152 def __str__(self): 00153 return self.phoneNumber 00154 00155 def __repr__(self): 00156 return '<Phone %s>' % self.phoneNumber 00157 00158 class Message(AttrDict): 00159 """ 00160 Wrapper for all call/sms message instances stored in Google Voice 00161 Attributes are: 00162 00163 * id: SHA1 identifier 00164 * isTrash: bool 00165 * displayStartDateTime: datetime 00166 * star: bool 00167 * isSpam: bool 00168 * startTime: gmtime 00169 * labels: list 00170 * displayStartTime: time 00171 * children: str 00172 * note: str 00173 * isRead: bool 00174 * displayNumber: str 00175 * relativeStartTime: str 00176 * phoneNumber: str 00177 * type: int 00178 00179 """ 00180 def __init__(self, folder, id, data): 00181 assert is_sha1(id), 'Message id not a SHA1 hash' 00182 self.folder = folder 00183 self.id = id 00184 super(AttrDict, self).__init__(data) 00185 self['startTime'] = gmtime(int(self['startTime'])/1000) 00186 self['displayStartDateTime'] = datetime.strptime( 00187 self['displayStartDateTime'], '%m/%d/%y %I:%M %p') 00188 self['displayStartTime'] = self['displayStartDateTime'].time() 00189 00190 def delete(self, trash=1): 00191 """ 00192 Moves this message to the Trash. Use ``message.delete(0)`` to move it out of the Trash. 00193 """ 00194 self.folder.voice.__messages_post('delete', self.id, trash=trash) 00195 00196 def star(self, star=1): 00197 """ 00198 Star this message. Use ``message.star(0)`` to unstar it. 00199 """ 00200 self.folder.voice.__messages_post('star', self.id, star=star) 00201 00202 def mark(self, read=1): 00203 """ 00204 Mark this message as read. Use ``message.mark(0)`` to mark it as unread. 00205 """ 00206 self.folder.voice.__messages_post('mark', self.id, read=read) 00207 00208 def download(self, adir=None): 00209 """ 00210 Download the message MP3 (if any). 00211 Saves files to ``adir`` (defaults to current directory). 00212 Message hashes can be found in ``self.voicemail().messages`` for example. 00213 Returns location of saved file. 00214 """ 00215 return self.folder.voice.download(self, adir) 00216 00217 def __str__(self): 00218 return self.id 00219 00220 def __repr__(self): 00221 return '<Message #%s (%s)>' % (self.id, self.phoneNumber) 00222 00223 class Folder(AttrDict): 00224 """ 00225 Folder wrapper for feeds from Google Voice 00226 Attributes are: 00227 00228 * totalSize: int (aka ``__len__``) 00229 * unreadCounts: dict 00230 * resultsPerPage: int 00231 * messages: list of Message instances 00232 """ 00233 def __init__(self, voice, name, data): 00234 self.voice = voice 00235 self.name = name 00236 super(AttrDict, self).__init__(data) 00237 00238 def messages(self): 00239 """ 00240 Returns a list of all messages in this folder 00241 """ 00242 return [Message(self, *i) for i in self['messages'].items()] 00243 messages = property(messages) 00244 00245 def __len__(self): 00246 return self['totalSize'] 00247 00248 def __repr__(self): 00249 return '<Folder %s (%s)>' % (self.name, len(self)) 00250 00251 class XMLParser(object): 00252 """ 00253 XML Parser helper that can dig json and html out of the feeds. 00254 The parser takes a ``Voice`` instance, page name, and function to grab data from. 00255 Calling the parser calls the data function once, sets up the ``json`` and ``html`` 00256 attributes and returns a ``Folder`` instance for the given page:: 00257 00258 >>> o = XMLParser(voice, 'voicemail', lambda: 'some xml payload') 00259 >>> o() 00260 ... <Folder ...> 00261 >>> o.json 00262 ... 'some json payload' 00263 >>> o.data 00264 ... 'loaded json payload' 00265 >>> o.html 00266 ... 'some html payload' 00267 00268 """ 00269 attr = None 00270 00271 def start_element(self, name, attrs): 00272 if name in ('json','html'): 00273 self.attr = name 00274 def end_element(self, name): self.attr = None 00275 def char_data(self, data): 00276 if self.attr and data: 00277 setattr(self, self.attr, getattr(self, self.attr) + data) 00278 00279 def __init__(self, voice, name, datafunc): 00280 self.json, self.html = '','' 00281 self.datafunc = datafunc 00282 self.voice = voice 00283 self.name = name 00284 00285 def __call__(self): 00286 self.json, self.html = '','' 00287 parser = ParserCreate() 00288 parser.StartElementHandler = self.start_element 00289 parser.EndElementHandler = self.end_element 00290 parser.CharacterDataHandler = self.char_data 00291 try: 00292 data = self.datafunc() 00293 parser.Parse(data, 1) 00294 except: 00295 raise ParsingError 00296 return self.folder 00297 00298 def folder(self): 00299 """ 00300 Returns associated ``Folder`` instance for given page (``self.name``) 00301 """ 00302 return Folder(self.voice, self.name, self.data) 00303 folder = property(folder) 00304 00305 def data(self): 00306 """ 00307 Returns the parsed json information after calling the XMLParser 00308 """ 00309 try: 00310 return loads(self.json) 00311 except: 00312 raise JSONError 00313 data = property(data) 00314