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