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