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