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