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