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