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