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