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