9bada991aac332f53e5621d9dd603ae828af3bff
[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
44 except ImportError:
45         simplejson = None
46
47 import browser_emu
48
49
50 _moduleLogger = logging.getLogger("gvoice.backend")
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 GVoiceBackend(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 _grab_json(self, flatXml):
415                 xmlTree = ElementTree.fromstring(flatXml)
416                 jsonElement = xmlTree.getchildren()[0]
417                 flatJson = jsonElement.text
418                 jsonTree = parse_json(flatJson)
419                 return jsonTree
420
421         def _grab_html(self, flatXml):
422                 xmlTree = ElementTree.fromstring(flatXml)
423                 htmlElement = xmlTree.getchildren()[1]
424                 flatHtml = htmlElement.text
425                 return flatHtml
426
427         def _grab_account_info(self, page):
428                 tokenGroup = self._tokenRe.search(page)
429                 if tokenGroup is None:
430                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
431                 self._token = tokenGroup.group(1)
432
433                 anGroup = self._accountNumRe.search(page)
434                 if anGroup is not None:
435                         self._accountNum = anGroup.group(1)
436                 else:
437                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
438
439                 self._callbackNumbers = {}
440                 for match in self._callbackRe.finditer(page):
441                         callbackNumber = match.group(2)
442                         callbackName = match.group(1)
443                         self._callbackNumbers[callbackNumber] = callbackName
444                 if len(self._callbackNumbers) == 0:
445                         _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
446
447         def _send_validation(self, number):
448                 if not self.is_valid_syntax(number):
449                         raise ValueError('Number is not valid: "%s"' % number)
450                 elif not self.is_authed():
451                         raise RuntimeError("Not Authenticated")
452
453                 if len(number) == 11 and number[0] == 1:
454                         # Strip leading 1 from 11 digit dialing
455                         number = number[1:]
456                 return number
457
458         @staticmethod
459         def _interpret_voicemail_regex(group):
460                 quality, content, number = group.group(2), group.group(3), group.group(4)
461                 if quality is not None and content is not None:
462                         return quality, content
463                 elif number is not None:
464                         return "high", number
465
466         def _parse_voicemail(self, voicemailHtml):
467                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
468                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
469                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
470                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
471                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
472                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
473                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
474                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
475                         location = locationGroup.group(1).strip() if locationGroup else ""
476
477                         nameGroup = self._voicemailNameRegex.search(messageHtml)
478                         name = nameGroup.group(1).strip() if nameGroup else ""
479                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
480                         number = numberGroup.group(1).strip() if numberGroup else ""
481                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
482                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
483                         contactIdGroup = self._messagesContactID.search(messageHtml)
484                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
485
486                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
487                         messageParts = (
488                                 self._interpret_voicemail_regex(group)
489                                 for group in messageGroups
490                         ) if messageGroups else ()
491
492                         yield {
493                                 "id": messageId.strip(),
494                                 "contactId": contactId,
495                                 "name": name,
496                                 "time": exactTime,
497                                 "relTime": relativeTime,
498                                 "prettyNumber": prettyNumber,
499                                 "number": number,
500                                 "location": location,
501                                 "messageParts": messageParts,
502                                 "type": "Voicemail",
503                         }
504
505         def _decorate_voicemail(self, parsedVoicemails):
506                 messagePartFormat = {
507                         "med1": "<i>%s</i>",
508                         "med2": "%s",
509                         "high": "<b>%s</b>",
510                 }
511                 for voicemailData in parsedVoicemails:
512                         message = " ".join((
513                                 messagePartFormat[quality] % part
514                                 for (quality, part) in voicemailData["messageParts"]
515                         )).strip()
516                         if not message:
517                                 message = "No Transcription"
518                         whoFrom = voicemailData["name"]
519                         when = voicemailData["time"]
520                         voicemailData["messageParts"] = ((whoFrom, message, when), )
521                         yield voicemailData
522
523         def _parse_sms(self, smsHtml):
524                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
525                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
526                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
527                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
528                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
529                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
530                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
531
532                         nameGroup = self._voicemailNameRegex.search(messageHtml)
533                         name = nameGroup.group(1).strip() if nameGroup else ""
534                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
535                         number = numberGroup.group(1).strip() if numberGroup else ""
536                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
537                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
538                         contactIdGroup = self._messagesContactID.search(messageHtml)
539                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
540
541                         fromGroups = self._smsFromRegex.finditer(messageHtml)
542                         fromParts = (group.group(1).strip() for group in fromGroups)
543                         textGroups = self._smsTextRegex.finditer(messageHtml)
544                         textParts = (group.group(1).strip() for group in textGroups)
545                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
546                         timeParts = (group.group(1).strip() for group in timeGroups)
547
548                         messageParts = itertools.izip(fromParts, textParts, timeParts)
549
550                         yield {
551                                 "id": messageId.strip(),
552                                 "contactId": contactId,
553                                 "name": name,
554                                 "time": exactTime,
555                                 "relTime": relativeTime,
556                                 "prettyNumber": prettyNumber,
557                                 "number": number,
558                                 "location": "",
559                                 "messageParts": messageParts,
560                                 "type": "Texts",
561                         }
562
563         def _decorate_sms(self, parsedTexts):
564                 return parsedTexts
565
566         @staticmethod
567         def _merge_messages(parsedMessages, json):
568                 for message in parsedMessages:
569                         id = message["id"]
570                         jsonItem = json["messages"][id]
571                         message["isRead"] = jsonItem["isRead"]
572                         message["isSpam"] = jsonItem["isSpam"]
573                         message["isTrash"] = jsonItem["isTrash"]
574                         message["isArchived"] = "inbox" not in jsonItem["labels"]
575                         yield message
576
577
578 def set_sane_callback(backend):
579         """
580         Try to set a sane default callback number on these preferences
581         1) 1747 numbers ( Gizmo )
582         2) anything with gizmo in the name
583         3) anything with computer in the name
584         4) the first value
585         """
586         numbers = backend.get_callback_numbers()
587
588         priorityOrderedCriteria = [
589                 ("1747", None),
590                 (None, "gizmo"),
591                 (None, "computer"),
592                 (None, "sip"),
593                 (None, None),
594         ]
595
596         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
597                 for number, description in numbers.iteritems():
598                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
599                                 continue
600                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
601                                 continue
602                         backend.set_callback_number(number)
603                         return
604
605
606 def sort_messages(allMessages):
607         sortableAllMessages = [
608                 (message["time"], message)
609                 for message in allMessages
610         ]
611         sortableAllMessages.sort(reverse=True)
612         return (
613                 message
614                 for (exactTime, message) in sortableAllMessages
615         )
616
617
618 def decorate_recent(recentCallData):
619         """
620         @returns (personsName, phoneNumber, date, action)
621         """
622         contactId = recentCallData["contactId"]
623         if recentCallData["name"]:
624                 header = recentCallData["name"]
625         elif recentCallData["prettyNumber"]:
626                 header = recentCallData["prettyNumber"]
627         elif recentCallData["location"]:
628                 header = recentCallData["location"]
629         else:
630                 header = "Unknown"
631
632         number = recentCallData["number"]
633         relTime = recentCallData["relTime"]
634         action = recentCallData["action"]
635         return contactId, header, number, relTime, action
636
637
638 def decorate_message(messageData):
639         contactId = messageData["contactId"]
640         exactTime = messageData["time"]
641         if messageData["name"]:
642                 header = messageData["name"]
643         elif messageData["prettyNumber"]:
644                 header = messageData["prettyNumber"]
645         else:
646                 header = "Unknown"
647         number = messageData["number"]
648         relativeTime = messageData["relTime"]
649
650         messageParts = list(messageData["messageParts"])
651         if len(messageParts) == 0:
652                 messages = ("No Transcription", )
653         elif len(messageParts) == 1:
654                 messages = (messageParts[0][1], )
655         else:
656                 messages = [
657                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
658                         for messagePart in messageParts
659                 ]
660
661         decoratedResults = contactId, header, number, relativeTime, messages
662         return decoratedResults
663
664
665 def test_backend(username, password):
666         backend = GVoiceBackend()
667         print "Authenticated: ", backend.is_authed()
668         if not backend.is_authed():
669                 print "Login?: ", backend.login(username, password)
670         print "Authenticated: ", backend.is_authed()
671
672         #print "Token: ", backend._token
673         #print "Account: ", backend.get_account_number()
674         #print "Callback: ", backend.get_callback_number()
675         #print "All Callback: ",
676         #import pprint
677         #pprint.pprint(backend.get_callback_numbers())
678
679         #print "Recent: "
680         #for data in backend.get_recent():
681         #       pprint.pprint(data)
682         #for data in sort_messages(backend.get_recent()):
683         #       pprint.pprint(decorate_recent(data))
684         #pprint.pprint(list(backend.get_recent()))
685
686         #print "Contacts: ",
687         #for contact in backend.get_contacts():
688         #       print contact
689         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
690
691         #print "Messages: ",
692         #for message in backend.get_messages():
693         #       pprint.pprint(message)
694         #for message in sort_messages(backend.get_messages()):
695         #       pprint.pprint(decorate_message(message))
696
697         return backend
698
699
700 def grab_debug_info(username, password):
701         cookieFile = os.path.join(".", "raw_cookies.txt")
702         try:
703                 os.remove(cookieFile)
704         except OSError:
705                 pass
706
707         backend = GVoiceBackend(cookieFile)
708         browser = backend._browser
709
710         _TEST_WEBPAGES = [
711                 ("forward", backend._forwardURL),
712                 ("token", backend._tokenURL),
713                 ("login", backend._loginURL),
714                 ("contacts", backend._contactsURL),
715
716                 ("voicemail", backend._voicemailURL),
717                 ("sms", backend._smsURL),
718
719                 ("recent", backend._recentCallsURL),
720                 ("placed", backend._placedCallsURL),
721                 ("recieved", backend._receivedCallsURL),
722                 ("missed", backend._missedCallsURL),
723         ]
724
725         # Get Pages
726         print "Grabbing pre-login pages"
727         for name, url in _TEST_WEBPAGES:
728                 try:
729                         page = browser.download(url)
730                 except StandardError, e:
731                         print e.message
732                         continue
733                 print "\tWriting to file"
734                 with open("not_loggedin_%s.txt" % name, "w") as f:
735                         f.write(page)
736
737         # Login
738         print "Attempting login"
739         galxToken = backend._get_token()
740         loginSuccessOrFailurePage = backend._login(username, password, galxToken)
741         with open("loggingin.txt", "w") as f:
742                 print "\tWriting to file"
743                 f.write(loginSuccessOrFailurePage)
744         try:
745                 backend._grab_account_info(loginSuccessOrFailurePage)
746         except Exception:
747                 # Retry in case the redirect failed
748                 # luckily is_authed does everything we need for a retry
749                 loggedIn = backend.is_authed(True)
750                 if not loggedIn:
751                         raise
752
753         # Get Pages
754         print "Grabbing post-login pages"
755         for name, url in _TEST_WEBPAGES:
756                 try:
757                         page = browser.download(url)
758                 except StandardError, e:
759                         print e.message
760                         continue
761                 print "\tWriting to file"
762                 with open("loggedin_%s.txt" % name, "w") as f:
763                         f.write(page)
764
765         # Cookies
766         browser.cookies.save()
767         print "\tWriting cookies to file"
768         with open("cookies.txt", "w") as f:
769                 f.writelines(
770                         "%s: %s\n" % (c.name, c.value)
771                         for c in browser.cookies
772                 )
773
774
775 if __name__ == "__main__":
776         import sys
777         logging.basicConfig(level=logging.DEBUG)
778         #test_backend(sys.argv[1], sys.argv[2])
779         grab_debug_info(sys.argv[1], sys.argv[2])