Improving login speed for new users
[gc-dialer] / src / gv_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 import browser_emu
43
44 try:
45         import simplejson
46 except ImportError:
47         simplejson = None
48
49
50 _moduleLogger = logging.getLogger("gvoice.dialer")
51
52
53 def safe_eval(s):
54         _TRUE_REGEX = re.compile("true")
55         _FALSE_REGEX = re.compile("false")
56         s = _TRUE_REGEX.sub("True", s)
57         s = _FALSE_REGEX.sub("False", s)
58         return eval(s, {}, {})
59
60
61 if simplejson is None:
62         def parse_json(flattened):
63                 return safe_eval(flattened)
64 else:
65         def parse_json(flattened):
66                 return simplejson.loads(flattened)
67
68
69 def itergroup(iterator, count, padValue = None):
70         """
71         Iterate in groups of 'count' values. If there
72         aren't enough values, the last result is padded with
73         None.
74
75         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
76         ...     print tuple(val)
77         (1, 2, 3)
78         (4, 5, 6)
79         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
80         ...     print list(val)
81         [1, 2, 3]
82         [4, 5, 6]
83         >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
84         ...     print tuple(val)
85         (1, 2, 3)
86         (4, 5, 6)
87         (7, None, None)
88         >>> for val in itergroup("123456", 3):
89         ...     print tuple(val)
90         ('1', '2', '3')
91         ('4', '5', '6')
92         >>> for val in itergroup("123456", 3):
93         ...     print repr("".join(val))
94         '123'
95         '456'
96         """
97         paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
98         nIterators = (paddedIterator, ) * count
99         return itertools.izip(*nIterators)
100
101
102 class NetworkError(RuntimeError):
103         pass
104
105
106 class GVDialer(object):
107         """
108         This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
109         the functions include login, setting up a callback number, and initalting a callback
110         """
111
112         def __init__(self, cookieFile = None):
113                 # Important items in this function are the setup of the browser emulation and cookie file
114                 self._browser = browser_emu.MozillaEmulator(1)
115                 if cookieFile is None:
116                         cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
117                 self._browser.cookies.filename = cookieFile
118                 if os.path.isfile(cookieFile):
119                         self._browser.cookies.load()
120
121                 self._token = ""
122                 self._accountNum = ""
123                 self._lastAuthed = 0.0
124                 self._callbackNumber = ""
125                 self._callbackNumbers = {}
126
127                 # Suprisingly, moving all of these from class to self sped up startup time
128
129                 self._validateRe = re.compile("^[0-9]{10,}$")
130
131                 self._forwardURL = "https://www.google.com/voice/mobile/phones"
132                 self._tokenURL = "http://www.google.com/voice/m"
133                 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
134                 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
135                 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
136                 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
137                 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
138
139                 self._gvDialingStrRe = re.compile("This may take a few seconds", re.M)
140                 self._clicktocallURL = "https://www.google.com/voice/m/sendcall"
141                 self._sendSmsURL = "https://www.google.com/voice/m/sendsms"
142
143                 self._recentCallsURL = "https://www.google.com/voice/inbox/recent/"
144                 self._placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
145                 self._receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
146                 self._missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
147
148                 self._contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
149                 self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
150                 self._contactsURL = "https://www.google.com/voice/mobile/contacts"
151                 self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
152                 self._contactDetailURL = "https://www.google.com/voice/mobile/contact"
153
154                 self._voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
155                 self._smsURL = "https://www.google.com/voice/inbox/recent/sms/"
156                 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
157                 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
158                 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
159                 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
160                 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
161                 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
162                 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
163                 self._messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
164                 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
165                 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
166                 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
167                 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
168
169         def is_authed(self, force = False):
170                 """
171                 Attempts to detect a current session
172                 @note Once logged in try not to reauth more than once a minute.
173                 @returns If authenticated
174                 """
175                 if (time.time() - self._lastAuthed) < 120 and not force:
176                         return True
177
178                 try:
179                         page = self._browser.download(self._forwardURL)
180                         self._grab_account_info(page)
181                 except Exception, e:
182                         _moduleLogger.exception(str(e))
183                         return False
184
185                 self._browser.cookies.save()
186                 self._lastAuthed = time.time()
187                 return True
188
189         def _get_token(self):
190                 try:
191                         tokenPage = self._browser.download(self._tokenURL)
192                 except urllib2.URLError, e:
193                         _moduleLogger.exception("Translating error: %s" % str(e))
194                         raise NetworkError("%s is not accesible" % self._loginURL)
195                 galxTokens = self._galxRe.search(tokenPage)
196                 if galxTokens is not None:
197                         galxToken = galxTokens.group(1)
198                 else:
199                         galxToken = ""
200                         _moduleLogger.debug("Could not grab GALX token")
201                 return galxToken
202
203         def _login(self, username, password, token):
204                 loginPostData = urllib.urlencode({
205                         'Email' : username,
206                         'Passwd' : password,
207                         'service': "grandcentral",
208                         "ltmpl": "mobile",
209                         "btmpl": "mobile",
210                         "PersistentCookie": "yes",
211                         "GALX": token,
212                         "continue": self._forwardURL,
213                 })
214
215                 try:
216                         loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
217                 except urllib2.URLError, e:
218                         _moduleLogger.exception("Translating error: %s" % str(e))
219                         raise NetworkError("%s is not accesible" % self._loginURL)
220                 return loginSuccessOrFailurePage
221
222         def login(self, username, password):
223                 """
224                 Attempt to login to GoogleVoice
225                 @returns Whether login was successful or not
226                 """
227                 self.logout()
228                 galxToken = self._get_token()
229                 loginSuccessOrFailurePage = self._login(username, password, galxToken)
230
231                 try:
232                         self._grab_account_info(loginSuccessOrFailurePage)
233                 except Exception, e:
234                         # Retry in case the redirect failed
235                         # luckily is_authed does everything we need for a retry
236                         loggedIn = self.is_authed(True)
237                         if not loggedIn:
238                                 _moduleLogger.exception(str(e))
239                                 return False
240                         _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
241
242                 self._browser.cookies.save()
243                 self._lastAuthed = time.time()
244                 return True
245
246         def logout(self):
247                 self._lastAuthed = 0.0
248                 self._browser.cookies.clear()
249                 self._browser.cookies.save()
250
251         def dial(self, number):
252                 """
253                 This is the main function responsible for initating the callback
254                 """
255                 number = self._send_validation(number)
256                 try:
257                         clickToCallData = urllib.urlencode({
258                                 "number": number,
259                                 "phone": self._callbackNumber,
260                                 "_rnr_se": self._token,
261                         })
262                         otherData = {
263                                 'Referer' : 'https://google.com/voice/m/callsms',
264                         }
265                         callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
266                 except urllib2.URLError, e:
267                         _moduleLogger.exception("Translating error: %s" % str(e))
268                         raise NetworkError("%s is not accesible" % self._clicktocallURL)
269
270                 if self._gvDialingStrRe.search(callSuccessPage) is None:
271                         raise RuntimeError("Google Voice returned an error")
272
273                 return True
274
275         def send_sms(self, number, message):
276                 number = self._send_validation(number)
277                 try:
278                         smsData = urllib.urlencode({
279                                 "number": number,
280                                 "smstext": message,
281                                 "_rnr_se": self._token,
282                                 "id": "undefined",
283                                 "c": "undefined",
284                         })
285                         otherData = {
286                                 'Referer' : 'https://google.com/voice/m/sms',
287                         }
288                         smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
289                 except urllib2.URLError, e:
290                         _moduleLogger.exception("Translating error: %s" % str(e))
291                         raise NetworkError("%s is not accesible" % self._sendSmsURL)
292
293                 return True
294
295         def is_valid_syntax(self, number):
296                 """
297                 @returns If This number be called ( syntax validation only )
298                 """
299                 return self._validateRe.match(number) is not None
300
301         def get_account_number(self):
302                 """
303                 @returns The GoogleVoice phone number
304                 """
305                 return self._accountNum
306
307         def get_callback_numbers(self):
308                 """
309                 @returns a dictionary mapping call back numbers to descriptions
310                 @note These results are cached for 30 minutes.
311                 """
312                 if not self.is_authed():
313                         return {}
314                 return self._callbackNumbers
315
316         def set_callback_number(self, callbacknumber):
317                 """
318                 Set the number that GoogleVoice calls
319                 @param callbacknumber should be a proper 10 digit number
320                 """
321                 self._callbackNumber = callbacknumber
322                 return True
323
324         def get_callback_number(self):
325                 """
326                 @returns Current callback number or None
327                 """
328                 return self._callbackNumber
329
330         def get_recent(self):
331                 """
332                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
333                 """
334                 for action, url in (
335                         ("Received", self._receivedCallsURL),
336                         ("Missed", self._missedCallsURL),
337                         ("Placed", self._placedCallsURL),
338                 ):
339                         try:
340                                 flatXml = self._browser.download(url)
341                         except urllib2.URLError, e:
342                                 _moduleLogger.exception("Translating error: %s" % str(e))
343                                 raise NetworkError("%s is not accesible" % url)
344
345                         allRecentHtml = self._grab_html(flatXml)
346                         allRecentData = self._parse_voicemail(allRecentHtml)
347                         for recentCallData in allRecentData:
348                                 recentCallData["action"] = action
349                                 yield recentCallData
350
351         def get_contacts(self):
352                 """
353                 @returns Iterable of (contact id, contact name)
354                 """
355                 contactsPagesUrls = [self._contactsURL]
356                 for contactsPageUrl in contactsPagesUrls:
357                         try:
358                                 contactsPage = self._browser.download(contactsPageUrl)
359                         except urllib2.URLError, e:
360                                 _moduleLogger.exception("Translating error: %s" % str(e))
361                                 raise NetworkError("%s is not accesible" % contactsPageUrl)
362                         for contact_match in self._contactsRe.finditer(contactsPage):
363                                 contactId = contact_match.group(1)
364                                 contactName = saxutils.unescape(contact_match.group(2))
365                                 contact = contactId, contactName
366                                 yield contact
367
368                         next_match = self._contactsNextRe.match(contactsPage)
369                         if next_match is not None:
370                                 newContactsPageUrl = self._contactsURL + next_match.group(1)
371                                 contactsPagesUrls.append(newContactsPageUrl)
372
373         def get_contact_details(self, contactId):
374                 """
375                 @returns Iterable of (Phone Type, Phone Number)
376                 """
377                 try:
378                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
379                 except urllib2.URLError, e:
380                         _moduleLogger.exception("Translating error: %s" % str(e))
381                         raise NetworkError("%s is not accesible" % self._contactDetailURL)
382
383                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
384                         phoneNumber = detail_match.group(1)
385                         phoneType = saxutils.unescape(detail_match.group(2))
386                         yield (phoneType, phoneNumber)
387
388         def get_messages(self):
389                 try:
390                         voicemailPage = self._browser.download(self._voicemailURL)
391                 except urllib2.URLError, e:
392                         _moduleLogger.exception("Translating error: %s" % str(e))
393                         raise NetworkError("%s is not accesible" % self._voicemailURL)
394                 voicemailHtml = self._grab_html(voicemailPage)
395                 voicemailJson = self._grab_json(voicemailPage)
396                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
397                 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
398                 decoratedVoicemails = self._decorate_voicemail(voicemails)
399
400                 try:
401                         smsPage = self._browser.download(self._smsURL)
402                 except urllib2.URLError, e:
403                         _moduleLogger.exception("Translating error: %s" % str(e))
404                         raise NetworkError("%s is not accesible" % self._smsURL)
405                 smsHtml = self._grab_html(smsPage)
406                 smsJson = self._grab_json(smsPage)
407                 parsedSms = self._parse_sms(smsHtml)
408                 smss = self._merge_messages(parsedSms, smsJson)
409                 decoratedSms = self._decorate_sms(smss)
410
411                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
412                 return allMessages
413
414         def clear_caches(self):
415                 pass
416
417         def get_addressbooks(self):
418                 """
419                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
420                 """
421                 yield self, "", ""
422
423         def open_addressbook(self, bookId):
424                 return self
425
426         @staticmethod
427         def contact_source_short_name(contactId):
428                 return "GV"
429
430         @staticmethod
431         def factory_name():
432                 return "Google Voice"
433
434         def _grab_json(self, flatXml):
435                 xmlTree = ElementTree.fromstring(flatXml)
436                 jsonElement = xmlTree.getchildren()[0]
437                 flatJson = jsonElement.text
438                 jsonTree = parse_json(flatJson)
439                 return jsonTree
440
441         def _grab_html(self, flatXml):
442                 xmlTree = ElementTree.fromstring(flatXml)
443                 htmlElement = xmlTree.getchildren()[1]
444                 flatHtml = htmlElement.text
445                 return flatHtml
446
447         def _grab_account_info(self, page):
448                 tokenGroup = self._tokenRe.search(page)
449                 if tokenGroup is None:
450                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
451                 self._token = tokenGroup.group(1)
452
453                 anGroup = self._accountNumRe.search(page)
454                 if anGroup is not None:
455                         self._accountNum = anGroup.group(1)
456                 else:
457                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
458
459                 self._callbackNumbers = {}
460                 for match in self._callbackRe.finditer(page):
461                         callbackNumber = match.group(2)
462                         callbackName = match.group(1)
463                         self._callbackNumbers[callbackNumber] = callbackName
464                 if len(self._callbackNumbers) == 0:
465                         _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
466
467         def _send_validation(self, number):
468                 if not self.is_valid_syntax(number):
469                         raise ValueError('Number is not valid: "%s"' % number)
470                 elif not self.is_authed():
471                         raise RuntimeError("Not Authenticated")
472
473                 if len(number) == 11 and number[0] == 1:
474                         # Strip leading 1 from 11 digit dialing
475                         number = number[1:]
476                 return number
477
478         @staticmethod
479         def _interpret_voicemail_regex(group):
480                 quality, content, number = group.group(2), group.group(3), group.group(4)
481                 if quality is not None and content is not None:
482                         return quality, content
483                 elif number is not None:
484                         return "high", number
485
486         def _parse_voicemail(self, voicemailHtml):
487                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
488                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
489                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
490                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
491                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
492                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
493                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
494                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
495                         location = locationGroup.group(1).strip() if locationGroup else ""
496
497                         nameGroup = self._voicemailNameRegex.search(messageHtml)
498                         name = nameGroup.group(1).strip() if nameGroup else ""
499                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
500                         number = numberGroup.group(1).strip() if numberGroup else ""
501                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
502                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
503                         contactIdGroup = self._messagesContactID.search(messageHtml)
504                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
505
506                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
507                         messageParts = (
508                                 self._interpret_voicemail_regex(group)
509                                 for group in messageGroups
510                         ) if messageGroups else ()
511
512                         yield {
513                                 "id": messageId.strip(),
514                                 "contactId": contactId,
515                                 "name": name,
516                                 "time": exactTime,
517                                 "relTime": relativeTime,
518                                 "prettyNumber": prettyNumber,
519                                 "number": number,
520                                 "location": location,
521                                 "messageParts": messageParts,
522                                 "type": "Voicemail",
523                         }
524
525         def _decorate_voicemail(self, parsedVoicemails):
526                 messagePartFormat = {
527                         "med1": "<i>%s</i>",
528                         "med2": "%s",
529                         "high": "<b>%s</b>",
530                 }
531                 for voicemailData in parsedVoicemails:
532                         message = " ".join((
533                                 messagePartFormat[quality] % part
534                                 for (quality, part) in voicemailData["messageParts"]
535                         )).strip()
536                         if not message:
537                                 message = "No Transcription"
538                         whoFrom = voicemailData["name"]
539                         when = voicemailData["time"]
540                         voicemailData["messageParts"] = ((whoFrom, message, when), )
541                         yield voicemailData
542
543         def _parse_sms(self, smsHtml):
544                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
545                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
546                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
547                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
548                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
549                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
550                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
551
552                         nameGroup = self._voicemailNameRegex.search(messageHtml)
553                         name = nameGroup.group(1).strip() if nameGroup else ""
554                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
555                         number = numberGroup.group(1).strip() if numberGroup else ""
556                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
557                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
558                         contactIdGroup = self._messagesContactID.search(messageHtml)
559                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
560
561                         fromGroups = self._smsFromRegex.finditer(messageHtml)
562                         fromParts = (group.group(1).strip() for group in fromGroups)
563                         textGroups = self._smsTextRegex.finditer(messageHtml)
564                         textParts = (group.group(1).strip() for group in textGroups)
565                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
566                         timeParts = (group.group(1).strip() for group in timeGroups)
567
568                         messageParts = itertools.izip(fromParts, textParts, timeParts)
569
570                         yield {
571                                 "id": messageId.strip(),
572                                 "contactId": contactId,
573                                 "name": name,
574                                 "time": exactTime,
575                                 "relTime": relativeTime,
576                                 "prettyNumber": prettyNumber,
577                                 "number": number,
578                                 "location": "",
579                                 "messageParts": messageParts,
580                                 "type": "Texts",
581                         }
582
583         def _decorate_sms(self, parsedTexts):
584                 return parsedTexts
585
586         @staticmethod
587         def _merge_messages(parsedMessages, json):
588                 for message in parsedMessages:
589                         id = message["id"]
590                         jsonItem = json["messages"][id]
591                         message["isRead"] = jsonItem["isRead"]
592                         message["isSpam"] = jsonItem["isSpam"]
593                         message["isTrash"] = jsonItem["isTrash"]
594                         message["isArchived"] = "inbox" not in jsonItem["labels"]
595                         yield message
596
597
598 def set_sane_callback(backend):
599         """
600         Try to set a sane default callback number on these preferences
601         1) 1747 numbers ( Gizmo )
602         2) anything with gizmo in the name
603         3) anything with computer in the name
604         4) the first value
605         """
606         numbers = backend.get_callback_numbers()
607
608         priorityOrderedCriteria = [
609                 ("1747", None),
610                 (None, "gizmo"),
611                 (None, "computer"),
612                 (None, "sip"),
613                 (None, None),
614         ]
615
616         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
617                 for number, description in numbers.iteritems():
618                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
619                                 continue
620                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
621                                 continue
622                         backend.set_callback_number(number)
623                         return
624
625
626 def sort_messages(allMessages):
627         sortableAllMessages = [
628                 (message["time"], message)
629                 for message in allMessages
630         ]
631         sortableAllMessages.sort(reverse=True)
632         return (
633                 message
634                 for (exactTime, message) in sortableAllMessages
635         )
636
637
638 def decorate_recent(recentCallData):
639         """
640         @returns (personsName, phoneNumber, date, action)
641         """
642         contactId = recentCallData["contactId"]
643         if recentCallData["name"]:
644                 header = recentCallData["name"]
645         elif recentCallData["prettyNumber"]:
646                 header = recentCallData["prettyNumber"]
647         elif recentCallData["location"]:
648                 header = recentCallData["location"]
649         else:
650                 header = "Unknown"
651
652         number = recentCallData["number"]
653         relTime = recentCallData["relTime"]
654         action = recentCallData["action"]
655         return contactId, header, number, relTime, action
656
657
658 def decorate_message(messageData):
659         contactId = messageData["contactId"]
660         exactTime = messageData["time"]
661         if messageData["name"]:
662                 header = messageData["name"]
663         elif messageData["prettyNumber"]:
664                 header = messageData["prettyNumber"]
665         else:
666                 header = "Unknown"
667         number = messageData["number"]
668         relativeTime = messageData["relTime"]
669
670         messageParts = list(messageData["messageParts"])
671         if len(messageParts) == 0:
672                 messages = ("No Transcription", )
673         elif len(messageParts) == 1:
674                 messages = (messageParts[0][1], )
675         else:
676                 messages = [
677                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
678                         for messagePart in messageParts
679                 ]
680
681         decoratedResults = contactId, header, number, relativeTime, messages
682         return decoratedResults
683
684
685 def test_backend(username, password):
686         backend = GVDialer()
687         print "Authenticated: ", backend.is_authed()
688         if not backend.is_authed():
689                 print "Login?: ", backend.login(username, password)
690         print "Authenticated: ", backend.is_authed()
691
692         print "Token: ", backend._token
693         #print "Account: ", backend.get_account_number()
694         #print "Callback: ", backend.get_callback_number()
695         #print "All Callback: ",
696         import pprint
697         #pprint.pprint(backend.get_callback_numbers())
698
699         #print "Recent: "
700         #for data in backend.get_recent():
701         #       pprint.pprint(data)
702         #for data in sort_messages(backend.get_recent()):
703         #       pprint.pprint(decorate_recent(data))
704         #pprint.pprint(list(backend.get_recent()))
705
706         #print "Contacts: ",
707         #for contact in backend.get_contacts():
708         #       print contact
709         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
710
711         print "Messages: ",
712         for message in backend.get_messages():
713                 message["messageParts"] = list(message["messageParts"])
714                 pprint.pprint(message)
715         #for message in sort_messages(backend.get_messages()):
716         #       pprint.pprint(decorate_message(message))
717
718         return backend
719
720
721 def grab_debug_info(username, password):
722         cookieFile = os.path.join(".", "raw_cookies.txt")
723         try:
724                 os.remove(cookieFile)
725         except OSError:
726                 pass
727
728         backend = GVDialer(cookieFile)
729         browser = backend._browser
730
731         _TEST_WEBPAGES = [
732                 ("forward", backend._forwardURL),
733                 ("token", backend._tokenURL),
734                 ("login", backend._loginURL),
735                 ("contacts", backend._contactsURL),
736
737                 ("voicemail", backend._voicemailURL),
738                 ("sms", backend._smsURL),
739
740                 ("recent", backend._recentCallsURL),
741                 ("placed", backend._placedCallsURL),
742                 ("recieved", backend._receivedCallsURL),
743                 ("missed", backend._missedCallsURL),
744         ]
745
746         # Get Pages
747         print "Grabbing pre-login pages"
748         for name, url in _TEST_WEBPAGES:
749                 try:
750                         page = browser.download(url)
751                 except StandardError, e:
752                         print e.message
753                         continue
754                 print "\tWriting to file"
755                 with open("not_loggedin_%s.txt" % name, "w") as f:
756                         f.write(page)
757
758         # Login
759         print "Attempting login"
760         galxToken = backend._get_token()
761         loginSuccessOrFailurePage = backend._login(username, password, galxToken)
762         with open("loggingin.txt", "w") as f:
763                 print "\tWriting to file"
764                 f.write(loginSuccessOrFailurePage)
765         try:
766                 backend._grab_account_info(loginSuccessOrFailurePage)
767         except Exception:
768                 # Retry in case the redirect failed
769                 # luckily is_authed does everything we need for a retry
770                 loggedIn = backend.is_authed(True)
771                 if not loggedIn:
772                         raise
773
774         # Get Pages
775         print "Grabbing post-login pages"
776         for name, url in _TEST_WEBPAGES:
777                 try:
778                         page = browser.download(url)
779                 except StandardError, e:
780                         print e.message
781                         continue
782                 print "\tWriting to file"
783                 with open("loggedin_%s.txt" % name, "w") as f:
784                         f.write(page)
785
786         # Cookies
787         browser.cookies.save()
788         print "\tWriting cookies to file"
789         with open("cookies.txt", "w") as f:
790                 f.writelines(
791                         "%s: %s\n" % (c.name, c.value)
792                         for c in browser.cookies
793                 )
794
795
796 if __name__ == "__main__":
797         import sys
798         logging.basicConfig(level=logging.DEBUG)
799         #test_backend(sys.argv[1], sys.argv[2])
800         grab_debug_info(sys.argv[1], sys.argv[2])