Starting to generalize the gv backend
[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 get_callback_numbers(self):
245                 """
246                 @returns a dictionary mapping call back numbers to descriptions
247                 @note These results are cached for 30 minutes.
248                 """
249                 if not self.is_authed():
250                         return {}
251                 return self._callbackNumbers
252
253         _setforwardURL = "https://www.google.com//voice/m/setphone"
254
255         def set_callback_number(self, callbacknumber):
256                 """
257                 Set the number that GoogleVoice calls
258                 @param callbacknumber should be a proper 10 digit number
259                 """
260                 self._callbackNumber = callbacknumber
261                 return True
262
263         def get_callback_number(self):
264                 """
265                 @returns Current callback number or None
266                 """
267                 return self._callbackNumber
268
269         def get_recent(self):
270                 """
271                 @returns Iterable of (personsName, phoneNumber, date, action)
272                 """
273                 sortedRecent = [
274                         (exactDate, name, number, relativeDate, action)
275                         for (name, number, exactDate, relativeDate, action) in self._get_recent()
276                 ]
277                 sortedRecent.sort(reverse = True)
278                 for exactDate, name, number, relativeDate, action in sortedRecent:
279                         yield name, number, relativeDate, action
280
281         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
282         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
283         _contactsURL = "https://www.google.com/voice/mobile/contacts"
284
285         def get_contacts(self):
286                 """
287                 @returns Iterable of (contact id, contact name)
288                 """
289                 if self.__contacts is None:
290                         self.__contacts = []
291
292                         contactsPagesUrls = [self._contactsURL]
293                         for contactsPageUrl in contactsPagesUrls:
294                                 try:
295                                         contactsPage = self._browser.download(contactsPageUrl)
296                                 except urllib2.URLError, e:
297                                         _moduleLogger.exception(str(e))
298                                         raise RuntimeError("%s is not accesible" % contactsPageUrl)
299                                 for contact_match in self._contactsRe.finditer(contactsPage):
300                                         contactId = contact_match.group(1)
301                                         contactName = saxutils.unescape(contact_match.group(2))
302                                         contact = contactId, contactName
303                                         self.__contacts.append(contact)
304                                         yield contact
305
306                                 next_match = self._contactsNextRe.match(contactsPage)
307                                 if next_match is not None:
308                                         newContactsPageUrl = self._contactsURL + next_match.group(1)
309                                         contactsPagesUrls.append(newContactsPageUrl)
310                 else:
311                         for contact in self.__contacts:
312                                 yield contact
313
314         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
315         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
316
317         def get_contact_details(self, contactId):
318                 """
319                 @returns Iterable of (Phone Type, Phone Number)
320                 """
321                 try:
322                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
323                 except urllib2.URLError, e:
324                         _moduleLogger.exception(str(e))
325                         raise RuntimeError("%s is not accesible" % self._contactDetailURL)
326
327                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
328                         phoneNumber = detail_match.group(1)
329                         phoneType = saxutils.unescape(detail_match.group(2))
330                         yield (phoneType, phoneNumber)
331
332         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
333         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
334
335         def get_messages(self):
336                 try:
337                         voicemailPage = self._browser.download(self._voicemailURL)
338                 except urllib2.URLError, e:
339                         _moduleLogger.exception(str(e))
340                         raise RuntimeError("%s is not accesible" % self._voicemailURL)
341                 voicemailHtml = self._grab_html(voicemailPage)
342                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
343                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
344
345                 try:
346                         smsPage = self._browser.download(self._smsURL)
347                 except urllib2.URLError, e:
348                         _moduleLogger.exception(str(e))
349                         raise RuntimeError("%s is not accesible" % self._smsURL)
350                 smsHtml = self._grab_html(smsPage)
351                 parsedSms = self._parse_sms(smsHtml)
352                 decoratedSms = self._decorate_sms(parsedSms)
353
354                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
355                 return allMessages
356
357         def _grab_json(self, flatXml):
358                 xmlTree = ElementTree.fromstring(flatXml)
359                 jsonElement = xmlTree.getchildren()[0]
360                 flatJson = jsonElement.text
361                 jsonTree = parse_json(flatJson)
362                 return jsonTree
363
364         def _grab_html(self, flatXml):
365                 xmlTree = ElementTree.fromstring(flatXml)
366                 htmlElement = xmlTree.getchildren()[1]
367                 flatHtml = htmlElement.text
368                 return flatHtml
369
370         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
371         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
372         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
373         _forwardURL = "https://www.google.com/voice/mobile/phones"
374
375         def _grab_account_info(self):
376                 page = self._browser.download(self._forwardURL)
377
378                 tokenGroup = self._tokenRe.search(page)
379                 if tokenGroup is None:
380                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
381                 self._token = tokenGroup.group(1)
382
383                 anGroup = self._accountNumRe.search(page)
384                 if anGroup is not None:
385                         self._accountNum = anGroup.group(1)
386                 else:
387                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
388
389                 self._callbackNumbers = {}
390                 for match in self._callbackRe.finditer(page):
391                         callbackNumber = match.group(2)
392                         callbackName = match.group(1)
393                         self._callbackNumbers[callbackNumber] = callbackName
394
395         def _send_validation(self, number):
396                 if not self.is_valid_syntax(number):
397                         raise ValueError('Number is not valid: "%s"' % number)
398                 elif not self.is_authed():
399                         raise RuntimeError("Not Authenticated")
400
401                 if len(number) == 11 and number[0] == 1:
402                         # Strip leading 1 from 11 digit dialing
403                         number = number[1:]
404                 return number
405
406         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
407         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
408         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
409         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
410
411         def _get_recent(self):
412                 """
413                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
414                 """
415                 for action, url in (
416                         ("Received", self._receivedCallsURL),
417                         ("Missed", self._missedCallsURL),
418                         ("Placed", self._placedCallsURL),
419                 ):
420                         try:
421                                 flatXml = self._browser.download(url)
422                         except urllib2.URLError, e:
423                                 _moduleLogger.exception(str(e))
424                                 raise RuntimeError("%s is not accesible" % url)
425
426                         allRecentHtml = self._grab_html(flatXml)
427                         allRecentData = self._parse_voicemail(allRecentHtml)
428                         for recentCallData in allRecentData:
429                                 exactTime = recentCallData["time"]
430                                 if recentCallData["name"]:
431                                         header = recentCallData["name"]
432                                 elif recentCallData["prettyNumber"]:
433                                         header = recentCallData["prettyNumber"]
434                                 elif recentCallData["location"]:
435                                         header = recentCallData["location"]
436                                 else:
437                                         header = "Unknown"
438                                 yield header, recentCallData["number"], exactTime, recentCallData["relTime"], action
439
440         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
441         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
442         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
443         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
444         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
445         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
446         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
447         _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
448         #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
449         #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
450         _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
451
452         @staticmethod
453         def _interpret_voicemail_regex(group):
454                 quality, content, number = group.group(2), group.group(3), group.group(4)
455                 if quality is not None and content is not None:
456                         return quality, content
457                 elif number is not None:
458                         return "high", number
459
460         def _parse_voicemail(self, voicemailHtml):
461                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
462                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
463                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
464                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
465                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
466                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
467                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
468                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
469                         location = locationGroup.group(1).strip() if locationGroup else ""
470
471                         nameGroup = self._voicemailNameRegex.search(messageHtml)
472                         name = nameGroup.group(1).strip() if nameGroup else ""
473                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
474                         number = numberGroup.group(1).strip() if numberGroup else ""
475                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
476                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
477                         contactIdGroup = self._messagesContactID.search(messageHtml)
478                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else number
479
480                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
481                         messageParts = (
482                                 self._interpret_voicemail_regex(group)
483                                 for group in messageGroups
484                         ) if messageGroups else ()
485
486                         yield {
487                                 "id": messageId.strip(),
488                                 "contactId": contactId,
489                                 "name": name,
490                                 "time": exactTime,
491                                 "relTime": relativeTime,
492                                 "prettyNumber": prettyNumber,
493                                 "number": number,
494                                 "location": location,
495                                 "messageParts": messageParts,
496                         }
497
498         def _decorate_voicemail(self, parsedVoicemails):
499                 messagePartFormat = {
500                         "med1": "<i>%s</i>",
501                         "med2": "%s",
502                         "high": "<b>%s</b>",
503                 }
504                 for voicemailData in parsedVoicemails:
505                         message = " ".join((
506                                 messagePartFormat[quality] % part
507                                 for (quality, part) in voicemailData["messageParts"]
508                         )).strip()
509                         if not message:
510                                 message = "No Transcription"
511                         whoFrom = voicemailData["name"]
512                         when = voicemailData["time"]
513                         voicemailData["messageParts"] = ((whoFrom, message, when), )
514                         yield voicemailData
515
516         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
517         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
518         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
519
520         def _parse_sms(self, smsHtml):
521                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
522                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
523                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
524                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
525                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
526                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
527                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
528
529                         nameGroup = self._voicemailNameRegex.search(messageHtml)
530                         name = nameGroup.group(1).strip() if nameGroup else ""
531                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
532                         number = numberGroup.group(1).strip() if numberGroup else ""
533                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
534                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
535                         contactIdGroup = self._messagesContactID.search(messageHtml)
536                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else number
537
538                         fromGroups = self._smsFromRegex.finditer(messageHtml)
539                         fromParts = (group.group(1).strip() for group in fromGroups)
540                         textGroups = self._smsTextRegex.finditer(messageHtml)
541                         textParts = (group.group(1).strip() for group in textGroups)
542                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
543                         timeParts = (group.group(1).strip() for group in timeGroups)
544
545                         messageParts = itertools.izip(fromParts, textParts, timeParts)
546
547                         yield {
548                                 "id": messageId.strip(),
549                                 "contactId": contactId,
550                                 "name": name,
551                                 "time": exactTime,
552                                 "relTime": relativeTime,
553                                 "prettyNumber": prettyNumber,
554                                 "number": number,
555                                 "location": "",
556                                 "messageParts": messageParts,
557                         }
558
559         def _decorate_sms(self, parsedTexts):
560                 return parsedTexts
561
562
563 def set_sane_callback(backend):
564         """
565         Try to set a sane default callback number on these preferences
566         1) 1747 numbers ( Gizmo )
567         2) anything with gizmo in the name
568         3) anything with computer in the name
569         4) the first value
570         """
571         numbers = backend.get_callback_numbers()
572
573         priorityOrderedCriteria = [
574                 ("1747", None),
575                 (None, "gizmo"),
576                 (None, "computer"),
577                 (None, "sip"),
578                 (None, None),
579         ]
580
581         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
582                 for number, description in numbers.iteritems():
583                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
584                                 continue
585                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
586                                 continue
587                         backend.set_callback_number(number)
588                         return
589
590
591 def sort_messages(allMessages):
592         sortableAllMessages = [
593                 (message["time"], message)
594                 for message in allMessages
595         ]
596         sortableAllMessages.sort(reverse=True)
597         return (
598                 message
599                 for (exactTime, message) in sortableAllMessages
600         )
601
602
603 def decorate_message(messageData):
604         exactTime = messageData["time"]
605         if messageData["name"]:
606                 header = messageData["name"]
607         elif messageData["prettyNumber"]:
608                 header = messageData["prettyNumber"]
609         else:
610                 header = "Unknown"
611         number = messageData["number"]
612         relativeTime = messageData["relTime"]
613
614         messageParts = list(messageData["messageParts"])
615         if len(messageParts) == 0:
616                 messages = ("No Transcription", )
617         elif len(messageParts) == 1:
618                 messages = (messageParts[0][1], )
619         else:
620                 messages = [
621                         "<b>%s</b>: %s" % (messagePart[0], messagePart[-1])
622                         for messagePart in messageParts
623                 ]
624
625         decoratedResults = header, number, relativeTime, messages
626         return decoratedResults
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         for message in sort_messages(backend.get_messages()):
649           pprint.pprint(decorate_message(message))
650
651         return backend
652
653
654 if __name__ == "__main__":
655         import sys
656         logging.basicConfig(level=logging.DEBUG)
657         test_backend(sys.argv[1], sys.argv[2])