Lots more bug fixes
[theonering] / src / gvoice / backend.py
1 #!/usr/bin/python
2
3 """
4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 Google Voice backend code
22
23 Resources
24         http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
25         http://posttopic.com/topic/google-voice-add-on-development
26 """
27
28 from __future__ import with_statement
29
30 import os
31 import re
32 import urllib
33 import urllib2
34 import time
35 import datetime
36 import itertools
37 import logging
38 from xml.sax import saxutils
39
40 from xml.etree import ElementTree
41
42 try:
43         import simplejson as _simplejson
44         simplejson = _simplejson
45 except ImportError:
46         simplejson = None
47
48 import browser_emu
49
50
51 _moduleLogger = logging.getLogger("gvoice.backend")
52
53
54 class NetworkError(RuntimeError):
55         pass
56
57
58 class GVoiceBackend(object):
59         """
60         This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
61         the functions include login, setting up a callback number, and initalting a callback
62         """
63
64         PHONE_TYPE_HOME = 1
65         PHONE_TYPE_MOBILE = 2
66         PHONE_TYPE_WORK = 3
67         PHONE_TYPE_GIZMO = 7
68
69         def __init__(self, cookieFile = None):
70                 # Important items in this function are the setup of the browser emulation and cookie file
71                 self._browser = browser_emu.MozillaEmulator(1)
72                 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
73
74                 self._token = ""
75                 self._accountNum = ""
76                 self._lastAuthed = 0.0
77                 self._callbackNumber = ""
78                 self._callbackNumbers = {}
79
80                 # Suprisingly, moving all of these from class to self sped up startup time
81
82                 self._validateRe = re.compile("^[0-9]{10,}$")
83
84                 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
85
86                 SECURE_URL_BASE = "https://www.google.com/voice/"
87                 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
88                 self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
89                 self._tokenURL = SECURE_URL_BASE + "m"
90                 self._callUrl = SECURE_URL_BASE + "call/connect"
91                 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
92                 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
93
94                 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
95                 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
96                 self._setDndURL = "https://www.google.com/voice/m/savednd"
97
98                 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
99
100                 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
101                 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
102                 self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
103                 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
104
105                 self.XML_FEEDS = (
106                         'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
107                         'recorded', 'placed', 'received', 'missed'
108                 )
109                 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
110                 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
111                 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
112                 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
113                 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
114                 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
115                 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
116                 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
117                 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
118                 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
119                 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
120
121                 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
122                 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
123                 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
124                 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
125
126                 self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
127                 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
128                 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
129                 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
130                 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
131                 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
132                 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
133                 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
134                 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
135                 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
136                 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
137                 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
138                 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
139
140         def is_quick_login_possible(self):
141                 """
142                 @returns True then is_authed might be enough to login, else full login is required
143                 """
144                 return self._loadedFromCookies or 0.0 < self._lastAuthed
145
146         def is_authed(self, force = False):
147                 """
148                 Attempts to detect a current session
149                 @note Once logged in try not to reauth more than once a minute.
150                 @returns If authenticated
151                 """
152                 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
153                 isPreviouslyAuthed = self._token is not None
154                 if isRecentledAuthed and isPreviouslyAuthed and not force:
155                         return True
156
157                 try:
158                         page = self._get_page(self._forwardURL)
159                         self._grab_account_info(page)
160                 except Exception, e:
161                         _moduleLogger.exception(str(e))
162                         return False
163
164                 self._browser.save_cookies()
165                 self._lastAuthed = time.time()
166                 return True
167
168         def _get_token(self):
169                 tokenPage = self._get_page(self._tokenURL)
170
171                 galxTokens = self._galxRe.search(tokenPage)
172                 if galxTokens is not None:
173                         galxToken = galxTokens.group(1)
174                 else:
175                         galxToken = ""
176                         _moduleLogger.debug("Could not grab GALX token")
177                 return galxToken
178
179         def _login(self, username, password, token):
180                 loginData = {
181                         'Email' : username,
182                         'Passwd' : password,
183                         'service': "grandcentral",
184                         "ltmpl": "mobile",
185                         "btmpl": "mobile",
186                         "PersistentCookie": "yes",
187                         "GALX": token,
188                         "continue": self._forwardURL,
189                 }
190
191                 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
192                 return loginSuccessOrFailurePage
193
194         def login(self, username, password):
195                 """
196                 Attempt to login to GoogleVoice
197                 @returns Whether login was successful or not
198                 """
199                 self.logout()
200                 galxToken = self._get_token()
201                 loginSuccessOrFailurePage = self._login(username, password, galxToken)
202
203                 try:
204                         self._grab_account_info(loginSuccessOrFailurePage)
205                 except Exception, e:
206                         # Retry in case the redirect failed
207                         # luckily is_authed does everything we need for a retry
208                         loggedIn = self.is_authed(True)
209                         if not loggedIn:
210                                 _moduleLogger.exception(str(e))
211                                 return False
212                         _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
213
214                 self._browser.save_cookies()
215                 self._lastAuthed = time.time()
216                 return True
217
218         def logout(self):
219                 self._browser.clear_cookies()
220                 self._browser.save_cookies()
221                 self._token = None
222                 self._lastAuthed = 0.0
223
224         def is_dnd(self):
225                 isDndPage = self._get_page(self._isDndURL)
226
227                 dndGroup = self._isDndRe.search(isDndPage)
228                 if dndGroup is None:
229                         return False
230                 dndStatus = dndGroup.group(1)
231                 isDnd = True if dndStatus.strip().lower() == "true" else False
232                 return isDnd
233
234         def set_dnd(self, doNotDisturb):
235                 dndPostData = {
236                         "doNotDisturb": 1 if doNotDisturb else 0,
237                         "_rnr_se": self._token,
238                 }
239
240                 dndPage = self._get_page(self._setDndURL, dndPostData)
241
242         def call(self, outgoingNumber):
243                 """
244                 This is the main function responsible for initating the callback
245                 """
246                 outgoingNumber = self._send_validation(outgoingNumber)
247                 subscriberNumber = None
248                 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
249
250                 callData = {
251                                 'outgoingNumber': outgoingNumber,
252                                 'forwardingNumber': self._callbackNumber,
253                                 'subscriberNumber': subscriberNumber or 'undefined',
254                                 'phoneType': str(phoneType),
255                                 'remember': '1',
256                 }
257                 _moduleLogger.info("%r" % callData)
258
259                 page = self._get_page_with_token(
260                         self._callUrl,
261                         callData,
262                 )
263                 self._parse_with_validation(page)
264                 return True
265
266         def cancel(self, outgoingNumber=None):
267                 """
268                 Cancels a call matching outgoing and forwarding numbers (if given). 
269                 Will raise an error if no matching call is being placed
270                 """
271                 page = self._get_page_with_token(
272                         self._callCancelURL,
273                         {
274                         'outgoingNumber': outgoingNumber or 'undefined',
275                         'forwardingNumber': self._callbackNumber or 'undefined',
276                         'cancelType': 'C2C',
277                         },
278                 )
279                 self._parse_with_validation(page)
280
281         def send_sms(self, phoneNumber, message):
282                 phoneNumber = self._send_validation(phoneNumber)
283                 page = self._get_page_with_token(
284                         self._sendSmsURL,
285                         {
286                                 'phoneNumber': phoneNumber,
287                                 'text': message
288                         },
289                 )
290                 self._parse_with_validation(page)
291
292         def search(self, query):
293                 """
294                 Search your Google Voice Account history for calls, voicemails, and sms
295                 Returns ``Folder`` instance containting matching messages
296                 """
297                 page = self._get_page(
298                         self._XML_SEARCH_URL,
299                         {"q": query},
300                 )
301                 json, html = extract_payload(page)
302                 return json
303
304         def get_feed(self, feed):
305                 actualFeed = "_XML_%s_URL" % feed.upper()
306                 feedUrl = getattr(self, actualFeed)
307
308                 page = self._get_page(feedUrl)
309                 json, html = extract_payload(page)
310
311                 return json
312
313         def download(self, messageId, adir):
314                 """
315                 Download a voicemail or recorded call MP3 matching the given ``msg``
316                 which can either be a ``Message`` instance, or a SHA1 identifier. 
317                 Saves files to ``adir`` (defaults to current directory). 
318                 Message hashes can be found in ``self.voicemail().messages`` for example. 
319                 Returns location of saved file.
320                 """
321                 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
322                 fn = os.path.join(adir, '%s.mp3' % messageId)
323                 with open(fn, 'wb') as fo:
324                         fo.write(page)
325                 return fn
326
327         def is_valid_syntax(self, number):
328                 """
329                 @returns If This number be called ( syntax validation only )
330                 """
331                 return self._validateRe.match(number) is not None
332
333         def get_account_number(self):
334                 """
335                 @returns The GoogleVoice phone number
336                 """
337                 return self._accountNum
338
339         def get_callback_numbers(self):
340                 """
341                 @returns a dictionary mapping call back numbers to descriptions
342                 @note These results are cached for 30 minutes.
343                 """
344                 if not self.is_authed():
345                         return {}
346                 return self._callbackNumbers
347
348         def set_callback_number(self, callbacknumber):
349                 """
350                 Set the number that GoogleVoice calls
351                 @param callbacknumber should be a proper 10 digit number
352                 """
353                 self._callbackNumber = callbacknumber
354                 return True
355
356         def get_callback_number(self):
357                 """
358                 @returns Current callback number or None
359                 """
360                 return self._callbackNumber
361
362         def get_recent(self):
363                 """
364                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
365                 """
366                 for action, url in (
367                         ("Received", self._XML_RECEIVED_URL),
368                         ("Missed", self._XML_MISSED_URL),
369                         ("Placed", self._XML_PLACED_URL),
370                 ):
371                         flatXml = self._get_page(url)
372
373                         allRecentHtml = self._grab_html(flatXml)
374                         allRecentData = self._parse_voicemail(allRecentHtml)
375                         for recentCallData in allRecentData:
376                                 recentCallData["action"] = action
377                                 yield recentCallData
378
379         def get_contacts(self):
380                 """
381                 @returns Iterable of (contact id, contact name)
382                 """
383                 page = self._get_page(self._XML_CONTACTS_URL)
384                 contactsBody = self._contactsBodyRe.search(page)
385                 if contactsBody is None:
386                         raise RuntimeError("Could not extract contact information")
387                 accountData = _fake_parse_json(contactsBody.group(1))
388                 for contactId, contactDetails in accountData["contacts"].iteritems():
389                         yield contactId, contactDetails
390
391         def get_messages(self):
392                 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
393                 voicemailHtml = self._grab_html(voicemailPage)
394                 voicemailJson = self._grab_json(voicemailPage)
395                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
396                 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
397                 decoratedVoicemails = self._decorate_voicemail(voicemails)
398
399                 smsPage = self._get_page(self._XML_SMS_URL)
400                 smsHtml = self._grab_html(smsPage)
401                 smsJson = self._grab_json(smsPage)
402                 parsedSms = self._parse_sms(smsHtml)
403                 smss = self._merge_messages(parsedSms, smsJson)
404                 decoratedSms = self._decorate_sms(smss)
405
406                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
407                 return allMessages
408
409         def _grab_json(self, flatXml):
410                 xmlTree = ElementTree.fromstring(flatXml)
411                 jsonElement = xmlTree.getchildren()[0]
412                 flatJson = jsonElement.text
413                 jsonTree = parse_json(flatJson)
414                 return jsonTree
415
416         def _grab_html(self, flatXml):
417                 xmlTree = ElementTree.fromstring(flatXml)
418                 htmlElement = xmlTree.getchildren()[1]
419                 flatHtml = htmlElement.text
420                 return flatHtml
421
422         def _grab_account_info(self, page):
423                 tokenGroup = self._tokenRe.search(page)
424                 if tokenGroup is None:
425                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
426                 self._token = tokenGroup.group(1)
427
428                 anGroup = self._accountNumRe.search(page)
429                 if anGroup is not None:
430                         self._accountNum = anGroup.group(1)
431                 else:
432                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
433
434                 self._callbackNumbers = {}
435                 for match in self._callbackRe.finditer(page):
436                         callbackNumber = match.group(2)
437                         callbackName = match.group(1)
438                         self._callbackNumbers[callbackNumber] = callbackName
439                 if len(self._callbackNumbers) == 0:
440                         _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
441
442         def _send_validation(self, number):
443                 if not self.is_valid_syntax(number):
444                         raise ValueError('Number is not valid: "%s"' % number)
445                 elif not self.is_authed():
446                         raise RuntimeError("Not Authenticated")
447
448                 if len(number) == 11 and number[0] == 1:
449                         # Strip leading 1 from 11 digit dialing
450                         number = number[1:]
451                 return number
452
453         @staticmethod
454         def _interpret_voicemail_regex(group):
455                 quality, content, number = group.group(2), group.group(3), group.group(4)
456                 if quality is not None and content is not None:
457                         return quality, content
458                 elif number is not None:
459                         return "high", number
460
461         def _parse_voicemail(self, voicemailHtml):
462                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
463                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
464                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
465                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
466                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
467                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
468                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
469                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
470                         location = locationGroup.group(1).strip() if locationGroup else ""
471
472                         nameGroup = self._voicemailNameRegex.search(messageHtml)
473                         name = nameGroup.group(1).strip() if nameGroup else ""
474                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
475                         number = numberGroup.group(1).strip() if numberGroup else ""
476                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
477                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
478                         contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
479                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
480
481                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
482                         messageParts = (
483                                 self._interpret_voicemail_regex(group)
484                                 for group in messageGroups
485                         ) if messageGroups else ()
486
487                         yield {
488                                 "id": messageId.strip(),
489                                 "contactId": contactId,
490                                 "name": name,
491                                 "time": exactTime,
492                                 "relTime": relativeTime,
493                                 "prettyNumber": prettyNumber,
494                                 "number": number,
495                                 "location": location,
496                                 "messageParts": messageParts,
497                                 "type": "Voicemail",
498                         }
499
500         def _decorate_voicemail(self, parsedVoicemails):
501                 messagePartFormat = {
502                         "med1": "<i>%s</i>",
503                         "med2": "%s",
504                         "high": "<b>%s</b>",
505                 }
506                 for voicemailData in parsedVoicemails:
507                         message = " ".join((
508                                 messagePartFormat[quality] % part
509                                 for (quality, part) in voicemailData["messageParts"]
510                         )).strip()
511                         if not message:
512                                 message = "No Transcription"
513                         whoFrom = voicemailData["name"]
514                         when = voicemailData["time"]
515                         voicemailData["messageParts"] = ((whoFrom, message, when), )
516                         yield voicemailData
517
518         def _parse_sms(self, smsHtml):
519                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
520                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
521                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
522                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
523                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
524                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
525                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
526
527                         nameGroup = self._voicemailNameRegex.search(messageHtml)
528                         name = nameGroup.group(1).strip() if nameGroup else ""
529                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
530                         number = numberGroup.group(1).strip() if numberGroup else ""
531                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
532                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
533                         contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
534                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
535
536                         fromGroups = self._smsFromRegex.finditer(messageHtml)
537                         fromParts = (group.group(1).strip() for group in fromGroups)
538                         textGroups = self._smsTextRegex.finditer(messageHtml)
539                         textParts = (group.group(1).strip() for group in textGroups)
540                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
541                         timeParts = (group.group(1).strip() for group in timeGroups)
542
543                         messageParts = itertools.izip(fromParts, textParts, timeParts)
544
545                         yield {
546                                 "id": messageId.strip(),
547                                 "contactId": contactId,
548                                 "name": name,
549                                 "time": exactTime,
550                                 "relTime": relativeTime,
551                                 "prettyNumber": prettyNumber,
552                                 "number": number,
553                                 "location": "",
554                                 "messageParts": messageParts,
555                                 "type": "Texts",
556                         }
557
558         def _decorate_sms(self, parsedTexts):
559                 return parsedTexts
560
561         @staticmethod
562         def _merge_messages(parsedMessages, json):
563                 for message in parsedMessages:
564                         id = message["id"]
565                         jsonItem = json["messages"][id]
566                         message["isRead"] = jsonItem["isRead"]
567                         message["isSpam"] = jsonItem["isSpam"]
568                         message["isTrash"] = jsonItem["isTrash"]
569                         message["isArchived"] = "inbox" not in jsonItem["labels"]
570                         yield message
571
572         def _get_page(self, url, data = None, refererUrl = None):
573                 headers = {}
574                 if refererUrl is not None:
575                         headers["Referer"] = refererUrl
576
577                 encodedData = urllib.urlencode(data) if data is not None else None
578
579                 try:
580                         page = self._browser.download(url, encodedData, None, headers)
581                 except urllib2.URLError, e:
582                         _moduleLogger.error("Translating error: %s" % str(e))
583                         raise NetworkError("%s is not accesible" % url)
584
585                 return page
586
587         def _get_page_with_token(self, url, data = None, refererUrl = None):
588                 if data is None:
589                         data = {}
590                 data['_rnr_se'] = self._token
591
592                 page = self._get_page(url, data, refererUrl)
593
594                 return page
595
596         def _parse_with_validation(self, page):
597                 json = parse_json(page)
598                 validate_response(json)
599                 return json
600
601
602 def itergroup(iterator, count, padValue = None):
603         """
604         Iterate in groups of 'count' values. If there
605         aren't enough values, the last result is padded with
606         None.
607
608         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
609         ...     print tuple(val)
610         (1, 2, 3)
611         (4, 5, 6)
612         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
613         ...     print list(val)
614         [1, 2, 3]
615         [4, 5, 6]
616         >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
617         ...     print tuple(val)
618         (1, 2, 3)
619         (4, 5, 6)
620         (7, None, None)
621         >>> for val in itergroup("123456", 3):
622         ...     print tuple(val)
623         ('1', '2', '3')
624         ('4', '5', '6')
625         >>> for val in itergroup("123456", 3):
626         ...     print repr("".join(val))
627         '123'
628         '456'
629         """
630         paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
631         nIterators = (paddedIterator, ) * count
632         return itertools.izip(*nIterators)
633
634
635 def safe_eval(s):
636         _TRUE_REGEX = re.compile("true")
637         _FALSE_REGEX = re.compile("false")
638         s = _TRUE_REGEX.sub("True", s)
639         s = _FALSE_REGEX.sub("False", s)
640         return eval(s, {}, {})
641
642
643 def _fake_parse_json(flattened):
644         return safe_eval(flattened)
645
646
647 def _actual_parse_json(flattened):
648         return simplejson.loads(flattened)
649
650
651 if simplejson is None:
652         parse_json = _fake_parse_json
653 else:
654         parse_json = _actual_parse_json
655
656
657 def extract_payload(flatXml):
658         xmlTree = ElementTree.fromstring(flatXml)
659
660         jsonElement = xmlTree.getchildren()[0]
661         flatJson = jsonElement.text
662         jsonTree = parse_json(flatJson)
663
664         htmlElement = xmlTree.getchildren()[1]
665         flatHtml = htmlElement.text
666
667         return jsonTree, flatHtml
668
669
670 def validate_response(response):
671         """
672         Validates that the JSON response is A-OK
673         """
674         try:
675                 assert 'ok' in response and response['ok']
676         except AssertionError:
677                 raise RuntimeError('There was a problem with GV: %s' % response)
678
679
680 def guess_phone_type(number):
681         if number.startswith("747") or number.startswith("1747"):
682                 return GVoiceBackend.PHONE_TYPE_GIZMO
683         else:
684                 return GVoiceBackend.PHONE_TYPE_MOBILE
685
686
687 def set_sane_callback(backend):
688         """
689         Try to set a sane default callback number on these preferences
690         1) 1747 numbers ( Gizmo )
691         2) anything with gizmo in the name
692         3) anything with computer in the name
693         4) the first value
694         """
695         numbers = backend.get_callback_numbers()
696
697         priorityOrderedCriteria = [
698                 ("1747", None),
699                 (None, "gizmo"),
700                 (None, "computer"),
701                 (None, "sip"),
702                 (None, None),
703         ]
704
705         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
706                 for number, description in numbers.iteritems():
707                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
708                                 continue
709                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
710                                 continue
711                         backend.set_callback_number(number)
712                         return
713
714
715 def sort_messages(allMessages):
716         sortableAllMessages = [
717                 (message["time"], message)
718                 for message in allMessages
719         ]
720         sortableAllMessages.sort(reverse=True)
721         return (
722                 message
723                 for (exactTime, message) in sortableAllMessages
724         )
725
726
727 def decorate_recent(recentCallData):
728         """
729         @returns (personsName, phoneNumber, date, action)
730         """
731         contactId = recentCallData["contactId"]
732         if recentCallData["name"]:
733                 header = recentCallData["name"]
734         elif recentCallData["prettyNumber"]:
735                 header = recentCallData["prettyNumber"]
736         elif recentCallData["location"]:
737                 header = recentCallData["location"]
738         else:
739                 header = "Unknown"
740
741         number = recentCallData["number"]
742         relTime = recentCallData["relTime"]
743         action = recentCallData["action"]
744         return contactId, header, number, relTime, action
745
746
747 def decorate_message(messageData):
748         contactId = messageData["contactId"]
749         exactTime = messageData["time"]
750         if messageData["name"]:
751                 header = messageData["name"]
752         elif messageData["prettyNumber"]:
753                 header = messageData["prettyNumber"]
754         else:
755                 header = "Unknown"
756         number = messageData["number"]
757         relativeTime = messageData["relTime"]
758
759         messageParts = list(messageData["messageParts"])
760         if len(messageParts) == 0:
761                 messages = ("No Transcription", )
762         elif len(messageParts) == 1:
763                 messages = (messageParts[0][1], )
764         else:
765                 messages = [
766                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
767                         for messagePart in messageParts
768                 ]
769
770         decoratedResults = contactId, header, number, relativeTime, messages
771         return decoratedResults
772
773
774 def test_backend(username, password):
775         backend = GVoiceBackend()
776         print "Authenticated: ", backend.is_authed()
777         if not backend.is_authed():
778                 print "Login?: ", backend.login(username, password)
779         print "Authenticated: ", backend.is_authed()
780         #print "Is Dnd: ", backend.is_dnd()
781         #print "Setting Dnd", backend.set_dnd(True)
782         #print "Is Dnd: ", backend.is_dnd()
783         #print "Setting Dnd", backend.set_dnd(False)
784         #print "Is Dnd: ", backend.is_dnd()
785
786         #print "Token: ", backend._token
787         #print "Account: ", backend.get_account_number()
788         #print "Callback: ", backend.get_callback_number()
789         #print "All Callback: ",
790         import pprint
791         #pprint.pprint(backend.get_callback_numbers())
792
793         #print "Recent: "
794         #for data in backend.get_recent():
795         #       pprint.pprint(data)
796         #for data in sort_messages(backend.get_recent()):
797         #       pprint.pprint(decorate_recent(data))
798         #pprint.pprint(list(backend.get_recent()))
799
800         print "Contacts: ",
801         for contact in backend.get_contacts():
802                 pprint.pprint(contact)
803
804         #print "Messages: ",
805         #for message in backend.get_messages():
806         #       pprint.pprint(message)
807         #for message in sort_messages(backend.get_messages()):
808         #       pprint.pprint(decorate_message(message))
809
810         return backend
811
812
813 def grab_debug_info(username, password):
814         cookieFile = os.path.join(".", "raw_cookies.txt")
815         try:
816                 os.remove(cookieFile)
817         except OSError:
818                 pass
819
820         backend = GVoiceBackend(cookieFile)
821         browser = backend._browser
822
823         _TEST_WEBPAGES = [
824                 ("forward", backend._forwardURL),
825                 ("token", backend._tokenURL),
826                 ("login", backend._loginURL),
827                 ("isdnd", backend._isDndURL),
828                 ("account", backend._XML_ACCOUNT_URL),
829                 ("contacts", backend._XML_CONTACTS_URL),
830
831                 ("voicemail", backend._XML_VOICEMAIL_URL),
832                 ("sms", backend._XML_SMS_URL),
833
834                 ("recent", backend._XML_RECENT_URL),
835                 ("placed", backend._XML_PLACED_URL),
836                 ("recieved", backend._XML_RECEIVED_URL),
837                 ("missed", backend._XML_MISSED_URL),
838         ]
839
840         # Get Pages
841         print "Grabbing pre-login pages"
842         for name, url in _TEST_WEBPAGES:
843                 try:
844                         page = browser.download(url)
845                 except StandardError, e:
846                         print e.message
847                         continue
848                 print "\tWriting to file"
849                 with open("not_loggedin_%s.txt" % name, "w") as f:
850                         f.write(page)
851
852         # Login
853         print "Attempting login"
854         galxToken = backend._get_token()
855         loginSuccessOrFailurePage = backend._login(username, password, galxToken)
856         with open("loggingin.txt", "w") as f:
857                 print "\tWriting to file"
858                 f.write(loginSuccessOrFailurePage)
859         try:
860                 backend._grab_account_info(loginSuccessOrFailurePage)
861         except Exception:
862                 # Retry in case the redirect failed
863                 # luckily is_authed does everything we need for a retry
864                 loggedIn = backend.is_authed(True)
865                 if not loggedIn:
866                         raise
867
868         # Get Pages
869         print "Grabbing post-login pages"
870         for name, url in _TEST_WEBPAGES:
871                 try:
872                         page = browser.download(url)
873                 except StandardError, e:
874                         print str(e)
875                         continue
876                 print "\tWriting to file"
877                 with open("loggedin_%s.txt" % name, "w") as f:
878                         f.write(page)
879
880         # Cookies
881         browser.save_cookies()
882         print "\tWriting cookies to file"
883         with open("cookies.txt", "w") as f:
884                 f.writelines(
885                         "%s: %s\n" % (c.name, c.value)
886                         for c in browser._cookies
887                 )
888
889
890 if __name__ == "__main__":
891         import sys
892         logging.basicConfig(level=logging.DEBUG)
893         if True:
894                 grab_debug_info(sys.argv[1], sys.argv[2])
895         else:
896                 test_backend(sys.argv[1], sys.argv[2])