Slowly transforming things into the shape needed to be ready to get things working
[theonering] / src / gvoice / backend.py
1 #!/usr/bin/python
2
3 """
4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 Google Voice backend code
22
23 Resources
24         http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
25         http://posttopic.com/topic/google-voice-add-on-development
26 """
27
28
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.backend")
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 GVoiceBackend(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         _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
147
148         def login(self, username, password):
149                 """
150                 Attempt to login to GoogleVoice
151                 @returns Whether login was successful or not
152                 """
153                 loginPostData = urllib.urlencode({
154                         'Email' : username,
155                         'Passwd' : password,
156                         'service': "grandcentral",
157                         "ltmpl": "mobile",
158                         "btmpl": "mobile",
159                         "PersistentCookie": "yes",
160                         "continue": self._forwardURL,
161                 })
162
163                 try:
164                         loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
165                 except urllib2.URLError, e:
166                         _moduleLogger.exception("Translating error: %s" % str(e))
167                         raise NetworkError("%s is not accesible" % self._loginURL)
168
169                 try:
170                         self._grab_account_info(loginSuccessOrFailurePage)
171                 except Exception, e:
172                         _moduleLogger.exception(str(e))
173                         return False
174
175                 self._browser.cookies.save()
176                 self._lastAuthed = time.time()
177                 return True
178
179         def logout(self):
180                 self._lastAuthed = 0.0
181                 self._browser.cookies.clear()
182                 self._browser.cookies.save()
183
184         _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
185         _clicktocallURL = "https://www.google.com/voice/m/sendcall"
186
187         def dial(self, number):
188                 """
189                 This is the main function responsible for initating the callback
190                 """
191                 number = self._send_validation(number)
192                 try:
193                         clickToCallData = urllib.urlencode({
194                                 "number": number,
195                                 "phone": self._callbackNumber,
196                                 "_rnr_se": self._token,
197                         })
198                         otherData = {
199                                 'Referer' : 'https://google.com/voice/m/callsms',
200                         }
201                         callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
202                 except urllib2.URLError, e:
203                         _moduleLogger.exception("Translating error: %s" % str(e))
204                         raise NetworkError("%s is not accesible" % self._clicktocallURL)
205
206                 if self._gvDialingStrRe.search(callSuccessPage) is None:
207                         raise RuntimeError("Google Voice returned an error")
208
209                 return True
210
211         _sendSmsURL = "https://www.google.com/voice/m/sendsms"
212
213         def send_sms(self, number, message):
214                 number = self._send_validation(number)
215                 try:
216                         smsData = urllib.urlencode({
217                                 "number": number,
218                                 "smstext": message,
219                                 "_rnr_se": self._token,
220                                 "id": "undefined",
221                                 "c": "undefined",
222                         })
223                         otherData = {
224                                 'Referer' : 'https://google.com/voice/m/sms',
225                         }
226                         smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
227                 except urllib2.URLError, e:
228                         _moduleLogger.exception("Translating error: %s" % str(e))
229                         raise NetworkError("%s is not accesible" % self._sendSmsURL)
230
231                 return True
232
233         _validateRe = re.compile("^[0-9]{10,}$")
234
235         def is_valid_syntax(self, number):
236                 """
237                 @returns If This number be called ( syntax validation only )
238                 """
239                 return self._validateRe.match(number) is not None
240
241         def get_account_number(self):
242                 """
243                 @returns The GoogleVoice phone number
244                 """
245                 return self._accountNum
246
247         def get_callback_numbers(self):
248                 """
249                 @returns a dictionary mapping call back numbers to descriptions
250                 @note These results are cached for 30 minutes.
251                 """
252                 if not self.is_authed():
253                         return {}
254                 return self._callbackNumbers
255
256         _setforwardURL = "https://www.google.com//voice/m/setphone"
257
258         def set_callback_number(self, callbacknumber):
259                 """
260                 Set the number that GoogleVoice calls
261                 @param callbacknumber should be a proper 10 digit number
262                 """
263                 self._callbackNumber = callbacknumber
264                 return True
265
266         def get_callback_number(self):
267                 """
268                 @returns Current callback number or None
269                 """
270                 return self._callbackNumber
271
272         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
273         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
274         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
275         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
276
277         def get_recent(self):
278                 """
279                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
280                 """
281                 for action, url in (
282                         ("Received", self._receivedCallsURL),
283                         ("Missed", self._missedCallsURL),
284                         ("Placed", self._placedCallsURL),
285                 ):
286                         try:
287                                 flatXml = self._browser.download(url)
288                         except urllib2.URLError, e:
289                                 _moduleLogger.exception("Translating error: %s" % str(e))
290                                 raise NetworkError("%s is not accesible" % url)
291
292                         allRecentHtml = self._grab_html(flatXml)
293                         allRecentData = self._parse_voicemail(allRecentHtml)
294                         for recentCallData in allRecentData:
295                                 recentCallData["action"] = action
296                                 yield recentCallData
297
298         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
299         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
300         _contactsURL = "https://www.google.com/voice/mobile/contacts"
301
302         def get_contacts(self):
303                 """
304                 @returns Iterable of (contact id, contact name)
305                 """
306                 contactsPagesUrls = [self._contactsURL]
307                 for contactsPageUrl in contactsPagesUrls:
308                         try:
309                                 contactsPage = self._browser.download(contactsPageUrl)
310                         except urllib2.URLError, e:
311                                 _moduleLogger.exception("Translating error: %s" % str(e))
312                                 raise NetworkError("%s is not accesible" % contactsPageUrl)
313                         for contact_match in self._contactsRe.finditer(contactsPage):
314                                 contactId = contact_match.group(1)
315                                 contactName = saxutils.unescape(contact_match.group(2))
316                                 contact = contactId, contactName
317                                 yield contact
318
319                         next_match = self._contactsNextRe.match(contactsPage)
320                         if next_match is not None:
321                                 newContactsPageUrl = self._contactsURL + next_match.group(1)
322                                 contactsPagesUrls.append(newContactsPageUrl)
323
324         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
325         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
326
327         def get_contact_details(self, contactId):
328                 """
329                 @returns Iterable of (Phone Type, Phone Number)
330                 """
331                 try:
332                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
333                 except urllib2.URLError, e:
334                         _moduleLogger.exception("Translating error: %s" % str(e))
335                         raise NetworkError("%s is not accesible" % self._contactDetailURL)
336
337                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
338                         phoneNumber = detail_match.group(1)
339                         phoneType = saxutils.unescape(detail_match.group(2))
340                         yield (phoneType, phoneNumber)
341
342         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
343         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
344
345         def get_messages(self):
346                 try:
347                         voicemailPage = self._browser.download(self._voicemailURL)
348                 except urllib2.URLError, e:
349                         _moduleLogger.exception("Translating error: %s" % str(e))
350                         raise NetworkError("%s is not accesible" % self._voicemailURL)
351                 voicemailHtml = self._grab_html(voicemailPage)
352                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
353                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
354
355                 try:
356                         smsPage = self._browser.download(self._smsURL)
357                 except urllib2.URLError, e:
358                         _moduleLogger.exception("Translating error: %s" % str(e))
359                         raise NetworkError("%s is not accesible" % self._smsURL)
360                 smsHtml = self._grab_html(smsPage)
361                 parsedSms = self._parse_sms(smsHtml)
362                 decoratedSms = self._decorate_sms(parsedSms)
363
364                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
365                 return allMessages
366
367         def _grab_json(self, flatXml):
368                 xmlTree = ElementTree.fromstring(flatXml)
369                 jsonElement = xmlTree.getchildren()[0]
370                 flatJson = jsonElement.text
371                 jsonTree = parse_json(flatJson)
372                 return jsonTree
373
374         def _grab_html(self, flatXml):
375                 xmlTree = ElementTree.fromstring(flatXml)
376                 htmlElement = xmlTree.getchildren()[1]
377                 flatHtml = htmlElement.text
378                 return flatHtml
379
380         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
381         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
382         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
383         _forwardURL = "https://www.google.com/voice/mobile/phones"
384
385         def _grab_account_info(self, page):
386                 tokenGroup = self._tokenRe.search(page)
387                 if tokenGroup is None:
388                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
389                 self._token = tokenGroup.group(1)
390
391                 anGroup = self._accountNumRe.search(page)
392                 if anGroup is not None:
393                         self._accountNum = anGroup.group(1)
394                 else:
395                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
396
397                 self._callbackNumbers = {}
398                 for match in self._callbackRe.finditer(page):
399                         callbackNumber = match.group(2)
400                         callbackName = match.group(1)
401                         self._callbackNumbers[callbackNumber] = callbackName
402
403         def _send_validation(self, number):
404                 if not self.is_valid_syntax(number):
405                         raise ValueError('Number is not valid: "%s"' % number)
406                 elif not self.is_authed():
407                         raise RuntimeError("Not Authenticated")
408
409                 if len(number) == 11 and number[0] == 1:
410                         # Strip leading 1 from 11 digit dialing
411                         number = number[1:]
412                 return number
413
414         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
415         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
416         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
417         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
418         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
419         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
420         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
421         _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
422         #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
423         #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
424         _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
425
426         @staticmethod
427         def _interpret_voicemail_regex(group):
428                 quality, content, number = group.group(2), group.group(3), group.group(4)
429                 if quality is not None and content is not None:
430                         return quality, content
431                 elif number is not None:
432                         return "high", number
433
434         def _parse_voicemail(self, voicemailHtml):
435                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
436                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
437                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
438                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
439                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
440                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
441                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
442                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
443                         location = locationGroup.group(1).strip() if locationGroup else ""
444
445                         nameGroup = self._voicemailNameRegex.search(messageHtml)
446                         name = nameGroup.group(1).strip() if nameGroup else ""
447                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
448                         number = numberGroup.group(1).strip() if numberGroup else ""
449                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
450                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
451                         contactIdGroup = self._messagesContactID.search(messageHtml)
452                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
453
454                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
455                         messageParts = (
456                                 self._interpret_voicemail_regex(group)
457                                 for group in messageGroups
458                         ) if messageGroups else ()
459
460                         yield {
461                                 "id": messageId.strip(),
462                                 "contactId": contactId,
463                                 "name": name,
464                                 "time": exactTime,
465                                 "relTime": relativeTime,
466                                 "prettyNumber": prettyNumber,
467                                 "number": number,
468                                 "location": location,
469                                 "messageParts": messageParts,
470                         }
471
472         def _decorate_voicemail(self, parsedVoicemails):
473                 messagePartFormat = {
474                         "med1": "<i>%s</i>",
475                         "med2": "%s",
476                         "high": "<b>%s</b>",
477                 }
478                 for voicemailData in parsedVoicemails:
479                         message = " ".join((
480                                 messagePartFormat[quality] % part
481                                 for (quality, part) in voicemailData["messageParts"]
482                         )).strip()
483                         if not message:
484                                 message = "No Transcription"
485                         whoFrom = voicemailData["name"]
486                         when = voicemailData["time"]
487                         voicemailData["messageParts"] = ((whoFrom, message, when), )
488                         yield voicemailData
489
490         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
491         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
492         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
493
494         def _parse_sms(self, smsHtml):
495                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
496                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
497                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
498                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
499                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
500                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
501                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
502
503                         nameGroup = self._voicemailNameRegex.search(messageHtml)
504                         name = nameGroup.group(1).strip() if nameGroup else ""
505                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
506                         number = numberGroup.group(1).strip() if numberGroup else ""
507                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
508                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
509                         contactIdGroup = self._messagesContactID.search(messageHtml)
510                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
511
512                         fromGroups = self._smsFromRegex.finditer(messageHtml)
513                         fromParts = (group.group(1).strip() for group in fromGroups)
514                         textGroups = self._smsTextRegex.finditer(messageHtml)
515                         textParts = (group.group(1).strip() for group in textGroups)
516                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
517                         timeParts = (group.group(1).strip() for group in timeGroups)
518
519                         messageParts = itertools.izip(fromParts, textParts, timeParts)
520
521                         yield {
522                                 "id": messageId.strip(),
523                                 "contactId": contactId,
524                                 "name": name,
525                                 "time": exactTime,
526                                 "relTime": relativeTime,
527                                 "prettyNumber": prettyNumber,
528                                 "number": number,
529                                 "location": "",
530                                 "messageParts": messageParts,
531                         }
532
533         def _decorate_sms(self, parsedTexts):
534                 return parsedTexts
535
536
537 def set_sane_callback(backend):
538         """
539         Try to set a sane default callback number on these preferences
540         1) 1747 numbers ( Gizmo )
541         2) anything with gizmo in the name
542         3) anything with computer in the name
543         4) the first value
544         """
545         numbers = backend.get_callback_numbers()
546
547         priorityOrderedCriteria = [
548                 ("1747", None),
549                 (None, "gizmo"),
550                 (None, "computer"),
551                 (None, "sip"),
552                 (None, None),
553         ]
554
555         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
556                 for number, description in numbers.iteritems():
557                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
558                                 continue
559                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
560                                 continue
561                         backend.set_callback_number(number)
562                         return
563
564
565 def sort_messages(allMessages):
566         sortableAllMessages = [
567                 (message["time"], message)
568                 for message in allMessages
569         ]
570         sortableAllMessages.sort(reverse=True)
571         return (
572                 message
573                 for (exactTime, message) in sortableAllMessages
574         )
575
576
577 def decorate_recent(recentCallData):
578         """
579         @returns (personsName, phoneNumber, date, action)
580         """
581         if recentCallData["name"]:
582                 header = recentCallData["name"]
583         elif recentCallData["prettyNumber"]:
584                 header = recentCallData["prettyNumber"]
585         elif recentCallData["location"]:
586                 header = recentCallData["location"]
587         else:
588                 header = "Unknown"
589
590         number = recentCallData["number"]
591         relTime = recentCallData["relTime"]
592         action = recentCallData["action"]
593         return header, number, relTime, action
594
595
596 def decorate_message(messageData):
597         exactTime = messageData["time"]
598         if messageData["name"]:
599                 header = messageData["name"]
600         elif messageData["prettyNumber"]:
601                 header = messageData["prettyNumber"]
602         else:
603                 header = "Unknown"
604         number = messageData["number"]
605         relativeTime = messageData["relTime"]
606
607         messageParts = list(messageData["messageParts"])
608         if len(messageParts) == 0:
609                 messages = ("No Transcription", )
610         elif len(messageParts) == 1:
611                 messages = (messageParts[0][1], )
612         else:
613                 messages = [
614                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
615                         for messagePart in messageParts
616                 ]
617
618         decoratedResults = header, number, relativeTime, messages
619         return decoratedResults
620
621
622 def test_backend(username, password):
623         backend = GVoiceBackend()
624         print "Authenticated: ", backend.is_authed()
625         if not backend.is_authed():
626                 print "Login?: ", backend.login(username, password)
627         print "Authenticated: ", backend.is_authed()
628
629         #print "Token: ", backend._token
630         #print "Account: ", backend.get_account_number()
631         #print "Callback: ", backend.get_callback_number()
632         #print "All Callback: ",
633         #import pprint
634         #pprint.pprint(backend.get_callback_numbers())
635
636         #print "Recent: "
637         #for data in backend.get_recent():
638         #       pprint.pprint(data)
639         #for data in sort_messages(backend.get_recent()):
640         #       pprint.pprint(decorate_recent(data))
641         #pprint.pprint(list(backend.get_recent()))
642
643         #print "Contacts: ",
644         #for contact in backend.get_contacts():
645         #       print contact
646         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
647
648         #print "Messages: ",
649         #for message in backend.get_messages():
650         #       pprint.pprint(message)
651         #for message in sort_messages(backend.get_messages()):
652         #       pprint.pprint(decorate_message(message))
653
654         return backend
655
656
657 if __name__ == "__main__":
658         import sys
659         logging.basicConfig(level=logging.DEBUG)
660         test_backend(sys.argv[1], sys.argv[2])