41d66a88e5bbb1e2d55eab05050878318643bd07
[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                 if galxTokens is not None:
162                         galxToken = galxTokens.group(1)
163                 else:
164                         galxToken = ""
165                         _moduleLogger.debug("Could not grab GALX token")
166
167                 loginPostData = urllib.urlencode({
168                         'Email' : username,
169                         'Passwd' : password,
170                         'service': "grandcentral",
171                         "ltmpl": "mobile",
172                         "btmpl": "mobile",
173                         "PersistentCookie": "yes",
174                         "GALX": galxToken,
175                         "continue": self._forwardURL,
176                 })
177
178                 try:
179                         loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
180                 except urllib2.URLError, e:
181                         _moduleLogger.exception("Translating error: %s" % str(e))
182                         raise NetworkError("%s is not accesible" % self._loginURL)
183
184                 try:
185                         self._grab_account_info(loginSuccessOrFailurePage)
186                 except Exception, e:
187                         _moduleLogger.exception(str(e))
188                         return False
189
190                 self._browser.cookies.save()
191                 self._lastAuthed = time.time()
192                 return True
193
194         def logout(self):
195                 self._lastAuthed = 0.0
196                 self._browser.cookies.clear()
197                 self._browser.cookies.save()
198
199         _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
200         _clicktocallURL = "https://www.google.com/voice/m/sendcall"
201
202         def dial(self, number):
203                 """
204                 This is the main function responsible for initating the callback
205                 """
206                 number = self._send_validation(number)
207                 try:
208                         clickToCallData = urllib.urlencode({
209                                 "number": number,
210                                 "phone": self._callbackNumber,
211                                 "_rnr_se": self._token,
212                         })
213                         otherData = {
214                                 'Referer' : 'https://google.com/voice/m/callsms',
215                         }
216                         callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
217                 except urllib2.URLError, e:
218                         _moduleLogger.exception("Translating error: %s" % str(e))
219                         raise NetworkError("%s is not accesible" % self._clicktocallURL)
220
221                 if self._gvDialingStrRe.search(callSuccessPage) is None:
222                         raise RuntimeError("Google Voice returned an error")
223
224                 return True
225
226         _sendSmsURL = "https://www.google.com/voice/m/sendsms"
227
228         def send_sms(self, number, message):
229                 number = self._send_validation(number)
230                 try:
231                         smsData = urllib.urlencode({
232                                 "number": number,
233                                 "smstext": message,
234                                 "_rnr_se": self._token,
235                                 "id": "undefined",
236                                 "c": "undefined",
237                         })
238                         otherData = {
239                                 'Referer' : 'https://google.com/voice/m/sms',
240                         }
241                         smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
242                 except urllib2.URLError, e:
243                         _moduleLogger.exception("Translating error: %s" % str(e))
244                         raise NetworkError("%s is not accesible" % self._sendSmsURL)
245
246                 return True
247
248         _validateRe = re.compile("^[0-9]{10,}$")
249
250         def is_valid_syntax(self, number):
251                 """
252                 @returns If This number be called ( syntax validation only )
253                 """
254                 return self._validateRe.match(number) is not None
255
256         def get_account_number(self):
257                 """
258                 @returns The GoogleVoice phone number
259                 """
260                 return self._accountNum
261
262         def get_callback_numbers(self):
263                 """
264                 @returns a dictionary mapping call back numbers to descriptions
265                 @note These results are cached for 30 minutes.
266                 """
267                 if not self.is_authed():
268                         return {}
269                 return self._callbackNumbers
270
271         _setforwardURL = "https://www.google.com//voice/m/setphone"
272
273         def set_callback_number(self, callbacknumber):
274                 """
275                 Set the number that GoogleVoice calls
276                 @param callbacknumber should be a proper 10 digit number
277                 """
278                 self._callbackNumber = callbacknumber
279                 return True
280
281         def get_callback_number(self):
282                 """
283                 @returns Current callback number or None
284                 """
285                 return self._callbackNumber
286
287         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
288         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
289         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
290         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
291
292         def get_recent(self):
293                 """
294                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
295                 """
296                 for action, url in (
297                         ("Received", self._receivedCallsURL),
298                         ("Missed", self._missedCallsURL),
299                         ("Placed", self._placedCallsURL),
300                 ):
301                         try:
302                                 flatXml = self._browser.download(url)
303                         except urllib2.URLError, e:
304                                 _moduleLogger.exception("Translating error: %s" % str(e))
305                                 raise NetworkError("%s is not accesible" % url)
306
307                         allRecentHtml = self._grab_html(flatXml)
308                         allRecentData = self._parse_voicemail(allRecentHtml)
309                         for recentCallData in allRecentData:
310                                 recentCallData["action"] = action
311                                 yield recentCallData
312
313         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
314         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
315         _contactsURL = "https://www.google.com/voice/mobile/contacts"
316
317         def get_contacts(self):
318                 """
319                 @returns Iterable of (contact id, contact name)
320                 """
321                 contactsPagesUrls = [self._contactsURL]
322                 for contactsPageUrl in contactsPagesUrls:
323                         try:
324                                 contactsPage = self._browser.download(contactsPageUrl)
325                         except urllib2.URLError, e:
326                                 _moduleLogger.exception("Translating error: %s" % str(e))
327                                 raise NetworkError("%s is not accesible" % contactsPageUrl)
328                         for contact_match in self._contactsRe.finditer(contactsPage):
329                                 contactId = contact_match.group(1)
330                                 contactName = saxutils.unescape(contact_match.group(2))
331                                 contact = contactId, contactName
332                                 yield contact
333
334                         next_match = self._contactsNextRe.match(contactsPage)
335                         if next_match is not None:
336                                 newContactsPageUrl = self._contactsURL + next_match.group(1)
337                                 contactsPagesUrls.append(newContactsPageUrl)
338
339         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
340         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
341
342         def get_contact_details(self, contactId):
343                 """
344                 @returns Iterable of (Phone Type, Phone Number)
345                 """
346                 try:
347                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
348                 except urllib2.URLError, e:
349                         _moduleLogger.exception("Translating error: %s" % str(e))
350                         raise NetworkError("%s is not accesible" % self._contactDetailURL)
351
352                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
353                         phoneNumber = detail_match.group(1)
354                         phoneType = saxutils.unescape(detail_match.group(2))
355                         yield (phoneType, phoneNumber)
356
357         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
358         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
359
360         def get_messages(self):
361                 try:
362                         voicemailPage = self._browser.download(self._voicemailURL)
363                 except urllib2.URLError, e:
364                         _moduleLogger.exception("Translating error: %s" % str(e))
365                         raise NetworkError("%s is not accesible" % self._voicemailURL)
366                 voicemailHtml = self._grab_html(voicemailPage)
367                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
368                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
369
370                 try:
371                         smsPage = self._browser.download(self._smsURL)
372                 except urllib2.URLError, e:
373                         _moduleLogger.exception("Translating error: %s" % str(e))
374                         raise NetworkError("%s is not accesible" % self._smsURL)
375                 smsHtml = self._grab_html(smsPage)
376                 parsedSms = self._parse_sms(smsHtml)
377                 decoratedSms = self._decorate_sms(parsedSms)
378
379                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
380                 return allMessages
381
382         def _grab_json(self, flatXml):
383                 xmlTree = ElementTree.fromstring(flatXml)
384                 jsonElement = xmlTree.getchildren()[0]
385                 flatJson = jsonElement.text
386                 jsonTree = parse_json(flatJson)
387                 return jsonTree
388
389         def _grab_html(self, flatXml):
390                 xmlTree = ElementTree.fromstring(flatXml)
391                 htmlElement = xmlTree.getchildren()[1]
392                 flatHtml = htmlElement.text
393                 return flatHtml
394
395         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
396         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
397         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
398         _forwardURL = "https://www.google.com/voice/mobile/phones"
399
400         def _grab_account_info(self, page):
401                 tokenGroup = self._tokenRe.search(page)
402                 if tokenGroup is None:
403                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
404                 self._token = tokenGroup.group(1)
405
406                 anGroup = self._accountNumRe.search(page)
407                 if anGroup is not None:
408                         self._accountNum = anGroup.group(1)
409                 else:
410                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
411
412                 self._callbackNumbers = {}
413                 for match in self._callbackRe.finditer(page):
414                         callbackNumber = match.group(2)
415                         callbackName = match.group(1)
416                         self._callbackNumbers[callbackNumber] = callbackName
417                 if len(self._callbackNumbers) == 0:
418                         _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
419
420         def _send_validation(self, number):
421                 if not self.is_valid_syntax(number):
422                         raise ValueError('Number is not valid: "%s"' % number)
423                 elif not self.is_authed():
424                         raise RuntimeError("Not Authenticated")
425
426                 if len(number) == 11 and number[0] == 1:
427                         # Strip leading 1 from 11 digit dialing
428                         number = number[1:]
429                 return number
430
431         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
432         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
433         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
434         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
435         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
436         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
437         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
438         _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
439         #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
440         #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
441         _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
442
443         @staticmethod
444         def _interpret_voicemail_regex(group):
445                 quality, content, number = group.group(2), group.group(3), group.group(4)
446                 if quality is not None and content is not None:
447                         return quality, content
448                 elif number is not None:
449                         return "high", number
450
451         def _parse_voicemail(self, voicemailHtml):
452                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
453                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
454                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
455                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
456                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
457                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
458                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
459                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
460                         location = locationGroup.group(1).strip() if locationGroup else ""
461
462                         nameGroup = self._voicemailNameRegex.search(messageHtml)
463                         name = nameGroup.group(1).strip() if nameGroup else ""
464                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
465                         number = numberGroup.group(1).strip() if numberGroup else ""
466                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
467                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
468                         contactIdGroup = self._messagesContactID.search(messageHtml)
469                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
470
471                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
472                         messageParts = (
473                                 self._interpret_voicemail_regex(group)
474                                 for group in messageGroups
475                         ) if messageGroups else ()
476
477                         yield {
478                                 "id": messageId.strip(),
479                                 "contactId": contactId,
480                                 "name": name,
481                                 "time": exactTime,
482                                 "relTime": relativeTime,
483                                 "prettyNumber": prettyNumber,
484                                 "number": number,
485                                 "location": location,
486                                 "messageParts": messageParts,
487                         }
488
489         def _decorate_voicemail(self, parsedVoicemails):
490                 messagePartFormat = {
491                         "med1": "<i>%s</i>",
492                         "med2": "%s",
493                         "high": "<b>%s</b>",
494                 }
495                 for voicemailData in parsedVoicemails:
496                         message = " ".join((
497                                 messagePartFormat[quality] % part
498                                 for (quality, part) in voicemailData["messageParts"]
499                         )).strip()
500                         if not message:
501                                 message = "No Transcription"
502                         whoFrom = voicemailData["name"]
503                         when = voicemailData["time"]
504                         voicemailData["messageParts"] = ((whoFrom, message, when), )
505                         yield voicemailData
506
507         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
508         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
509         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
510
511         def _parse_sms(self, smsHtml):
512                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
513                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
514                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
515                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
516                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
517                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
518                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
519
520                         nameGroup = self._voicemailNameRegex.search(messageHtml)
521                         name = nameGroup.group(1).strip() if nameGroup else ""
522                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
523                         number = numberGroup.group(1).strip() if numberGroup else ""
524                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
525                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
526                         contactIdGroup = self._messagesContactID.search(messageHtml)
527                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
528
529                         fromGroups = self._smsFromRegex.finditer(messageHtml)
530                         fromParts = (group.group(1).strip() for group in fromGroups)
531                         textGroups = self._smsTextRegex.finditer(messageHtml)
532                         textParts = (group.group(1).strip() for group in textGroups)
533                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
534                         timeParts = (group.group(1).strip() for group in timeGroups)
535
536                         messageParts = itertools.izip(fromParts, textParts, timeParts)
537
538                         yield {
539                                 "id": messageId.strip(),
540                                 "contactId": contactId,
541                                 "name": name,
542                                 "time": exactTime,
543                                 "relTime": relativeTime,
544                                 "prettyNumber": prettyNumber,
545                                 "number": number,
546                                 "location": "",
547                                 "messageParts": messageParts,
548                         }
549
550         def _decorate_sms(self, parsedTexts):
551                 return parsedTexts
552
553
554 def set_sane_callback(backend):
555         """
556         Try to set a sane default callback number on these preferences
557         1) 1747 numbers ( Gizmo )
558         2) anything with gizmo in the name
559         3) anything with computer in the name
560         4) the first value
561         """
562         numbers = backend.get_callback_numbers()
563
564         priorityOrderedCriteria = [
565                 ("1747", None),
566                 (None, "gizmo"),
567                 (None, "computer"),
568                 (None, "sip"),
569                 (None, None),
570         ]
571
572         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
573                 for number, description in numbers.iteritems():
574                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
575                                 continue
576                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
577                                 continue
578                         backend.set_callback_number(number)
579                         return
580
581
582 def sort_messages(allMessages):
583         sortableAllMessages = [
584                 (message["time"], message)
585                 for message in allMessages
586         ]
587         sortableAllMessages.sort(reverse=True)
588         return (
589                 message
590                 for (exactTime, message) in sortableAllMessages
591         )
592
593
594 def decorate_recent(recentCallData):
595         """
596         @returns (personsName, phoneNumber, date, action)
597         """
598         contactId = recentCallData["contactId"]
599         if recentCallData["name"]:
600                 header = recentCallData["name"]
601         elif recentCallData["prettyNumber"]:
602                 header = recentCallData["prettyNumber"]
603         elif recentCallData["location"]:
604                 header = recentCallData["location"]
605         else:
606                 header = "Unknown"
607
608         number = recentCallData["number"]
609         relTime = recentCallData["relTime"]
610         action = recentCallData["action"]
611         return contactId, header, number, relTime, action
612
613
614 def decorate_message(messageData):
615         contactId = messageData["contactId"]
616         exactTime = messageData["time"]
617         if messageData["name"]:
618                 header = messageData["name"]
619         elif messageData["prettyNumber"]:
620                 header = messageData["prettyNumber"]
621         else:
622                 header = "Unknown"
623         number = messageData["number"]
624         relativeTime = messageData["relTime"]
625
626         messageParts = list(messageData["messageParts"])
627         if len(messageParts) == 0:
628                 messages = ("No Transcription", )
629         elif len(messageParts) == 1:
630                 messages = (messageParts[0][1], )
631         else:
632                 messages = [
633                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
634                         for messagePart in messageParts
635                 ]
636
637         decoratedResults = contactId, header, number, relativeTime, messages
638         return decoratedResults
639
640
641 def test_backend(username, password):
642         backend = GVoiceBackend()
643         print "Authenticated: ", backend.is_authed()
644         if not backend.is_authed():
645                 print "Login?: ", backend.login(username, password)
646         print "Authenticated: ", backend.is_authed()
647
648         #print "Token: ", backend._token
649         #print "Account: ", backend.get_account_number()
650         #print "Callback: ", backend.get_callback_number()
651         #print "All Callback: ",
652         #import pprint
653         #pprint.pprint(backend.get_callback_numbers())
654
655         #print "Recent: "
656         #for data in backend.get_recent():
657         #       pprint.pprint(data)
658         #for data in sort_messages(backend.get_recent()):
659         #       pprint.pprint(decorate_recent(data))
660         #pprint.pprint(list(backend.get_recent()))
661
662         #print "Contacts: ",
663         #for contact in backend.get_contacts():
664         #       print contact
665         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
666
667         #print "Messages: ",
668         #for message in backend.get_messages():
669         #       pprint.pprint(message)
670         #for message in sort_messages(backend.get_messages()):
671         #       pprint.pprint(decorate_message(message))
672
673         return backend
674
675
676 if __name__ == "__main__":
677         import sys
678         logging.basicConfig(level=logging.DEBUG)
679         test_backend(sys.argv[1], sys.argv[2])