4b6007b65c976598f52eb36c5992851249c784d3
[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 NetworkError(RuntimeError):
102         pass
103
104
105 class GVDialer(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 clear_caches(self):
379                 pass
380
381         def get_addressbooks(self):
382                 """
383                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
384                 """
385                 yield self, "", ""
386
387         def open_addressbook(self, bookId):
388                 return self
389
390         @staticmethod
391         def contact_source_short_name(contactId):
392                 return "GV"
393
394         @staticmethod
395         def factory_name():
396                 return "Google Voice"
397
398         def _grab_json(self, flatXml):
399                 xmlTree = ElementTree.fromstring(flatXml)
400                 jsonElement = xmlTree.getchildren()[0]
401                 flatJson = jsonElement.text
402                 jsonTree = parse_json(flatJson)
403                 return jsonTree
404
405         def _grab_html(self, flatXml):
406                 xmlTree = ElementTree.fromstring(flatXml)
407                 htmlElement = xmlTree.getchildren()[1]
408                 flatHtml = htmlElement.text
409                 return flatHtml
410
411         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
412         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
413         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
414         _forwardURL = "https://www.google.com/voice/mobile/phones"
415
416         def _grab_account_info(self, page):
417                 tokenGroup = self._tokenRe.search(page)
418                 if tokenGroup is None:
419                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
420                 self._token = tokenGroup.group(1)
421
422                 anGroup = self._accountNumRe.search(page)
423                 if anGroup is not None:
424                         self._accountNum = anGroup.group(1)
425                 else:
426                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
427
428                 self._callbackNumbers = {}
429                 for match in self._callbackRe.finditer(page):
430                         callbackNumber = match.group(2)
431                         callbackName = match.group(1)
432                         self._callbackNumbers[callbackNumber] = callbackName
433
434         def _send_validation(self, number):
435                 if not self.is_valid_syntax(number):
436                         raise ValueError('Number is not valid: "%s"' % number)
437                 elif not self.is_authed():
438                         raise RuntimeError("Not Authenticated")
439
440                 if len(number) == 11 and number[0] == 1:
441                         # Strip leading 1 from 11 digit dialing
442                         number = number[1:]
443                 return number
444
445         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
446         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
447         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
448         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
449         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
450         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
451         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
452         _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
453         #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
454         #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
455         _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
456
457         @staticmethod
458         def _interpret_voicemail_regex(group):
459                 quality, content, number = group.group(2), group.group(3), group.group(4)
460                 if quality is not None and content is not None:
461                         return quality, content
462                 elif number is not None:
463                         return "high", number
464
465         def _parse_voicemail(self, voicemailHtml):
466                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
467                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
468                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
469                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
470                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
471                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
472                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
473                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
474                         location = locationGroup.group(1).strip() if locationGroup else ""
475
476                         nameGroup = self._voicemailNameRegex.search(messageHtml)
477                         name = nameGroup.group(1).strip() if nameGroup else ""
478                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
479                         number = numberGroup.group(1).strip() if numberGroup else ""
480                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
481                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
482                         contactIdGroup = self._messagesContactID.search(messageHtml)
483                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
484
485                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
486                         messageParts = (
487                                 self._interpret_voicemail_regex(group)
488                                 for group in messageGroups
489                         ) if messageGroups else ()
490
491                         yield {
492                                 "id": messageId.strip(),
493                                 "contactId": contactId,
494                                 "name": name,
495                                 "time": exactTime,
496                                 "relTime": relativeTime,
497                                 "prettyNumber": prettyNumber,
498                                 "number": number,
499                                 "location": location,
500                                 "messageParts": messageParts,
501                         }
502
503         def _decorate_voicemail(self, parsedVoicemails):
504                 messagePartFormat = {
505                         "med1": "<i>%s</i>",
506                         "med2": "%s",
507                         "high": "<b>%s</b>",
508                 }
509                 for voicemailData in parsedVoicemails:
510                         message = " ".join((
511                                 messagePartFormat[quality] % part
512                                 for (quality, part) in voicemailData["messageParts"]
513                         )).strip()
514                         if not message:
515                                 message = "No Transcription"
516                         whoFrom = voicemailData["name"]
517                         when = voicemailData["time"]
518                         voicemailData["messageParts"] = ((whoFrom, message, when), )
519                         yield voicemailData
520
521         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
522         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
523         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
524
525         def _parse_sms(self, smsHtml):
526                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
527                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
528                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
529                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
530                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
531                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
532                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
533
534                         nameGroup = self._voicemailNameRegex.search(messageHtml)
535                         name = nameGroup.group(1).strip() if nameGroup else ""
536                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
537                         number = numberGroup.group(1).strip() if numberGroup else ""
538                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
539                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
540                         contactIdGroup = self._messagesContactID.search(messageHtml)
541                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
542
543                         fromGroups = self._smsFromRegex.finditer(messageHtml)
544                         fromParts = (group.group(1).strip() for group in fromGroups)
545                         textGroups = self._smsTextRegex.finditer(messageHtml)
546                         textParts = (group.group(1).strip() for group in textGroups)
547                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
548                         timeParts = (group.group(1).strip() for group in timeGroups)
549
550                         messageParts = itertools.izip(fromParts, textParts, timeParts)
551
552                         yield {
553                                 "id": messageId.strip(),
554                                 "contactId": contactId,
555                                 "name": name,
556                                 "time": exactTime,
557                                 "relTime": relativeTime,
558                                 "prettyNumber": prettyNumber,
559                                 "number": number,
560                                 "location": "",
561                                 "messageParts": messageParts,
562                         }
563
564         def _decorate_sms(self, parsedTexts):
565                 return parsedTexts
566
567
568 def set_sane_callback(backend):
569         """
570         Try to set a sane default callback number on these preferences
571         1) 1747 numbers ( Gizmo )
572         2) anything with gizmo in the name
573         3) anything with computer in the name
574         4) the first value
575         """
576         numbers = backend.get_callback_numbers()
577
578         priorityOrderedCriteria = [
579                 ("1747", None),
580                 (None, "gizmo"),
581                 (None, "computer"),
582                 (None, "sip"),
583                 (None, None),
584         ]
585
586         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
587                 for number, description in numbers.iteritems():
588                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
589                                 continue
590                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
591                                 continue
592                         backend.set_callback_number(number)
593                         return
594
595
596 def sort_messages(allMessages):
597         sortableAllMessages = [
598                 (message["time"], message)
599                 for message in allMessages
600         ]
601         sortableAllMessages.sort(reverse=True)
602         return (
603                 message
604                 for (exactTime, message) in sortableAllMessages
605         )
606
607
608 def decorate_recent(recentCallData):
609         """
610         @returns (personsName, phoneNumber, date, action)
611         """
612         if recentCallData["name"]:
613                 header = recentCallData["name"]
614         elif recentCallData["prettyNumber"]:
615                 header = recentCallData["prettyNumber"]
616         elif recentCallData["location"]:
617                 header = recentCallData["location"]
618         else:
619                 header = "Unknown"
620
621         number = recentCallData["number"]
622         relTime = recentCallData["relTime"]
623         action = recentCallData["action"]
624         return header, number, relTime, action
625
626
627 def decorate_message(messageData):
628         exactTime = messageData["time"]
629         if messageData["name"]:
630                 header = messageData["name"]
631         elif messageData["prettyNumber"]:
632                 header = messageData["prettyNumber"]
633         else:
634                 header = "Unknown"
635         number = messageData["number"]
636         relativeTime = messageData["relTime"]
637
638         messageParts = list(messageData["messageParts"])
639         if len(messageParts) == 0:
640                 messages = ("No Transcription", )
641         elif len(messageParts) == 1:
642                 messages = (messageParts[0][1], )
643         else:
644                 messages = [
645                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
646                         for messagePart in messageParts
647                 ]
648
649         decoratedResults = header, number, relativeTime, messages
650         return decoratedResults
651
652
653 def test_backend(username, password):
654         backend = GVDialer()
655         print "Authenticated: ", backend.is_authed()
656         if not backend.is_authed():
657                 print "Login?: ", backend.login(username, password)
658         print "Authenticated: ", backend.is_authed()
659
660         #print "Token: ", backend._token
661         #print "Account: ", backend.get_account_number()
662         #print "Callback: ", backend.get_callback_number()
663         #print "All Callback: ",
664         #import pprint
665         #pprint.pprint(backend.get_callback_numbers())
666
667         #print "Recent: "
668         #for data in backend.get_recent():
669         #       pprint.pprint(data)
670         #for data in sort_messages(backend.get_recent()):
671         #       pprint.pprint(decorate_recent(data))
672         #pprint.pprint(list(backend.get_recent()))
673
674         #print "Contacts: ",
675         #for contact in backend.get_contacts():
676         #       print contact
677         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
678
679         #print "Messages: ",
680         #for message in backend.get_messages():
681         #       pprint.pprint(message)
682         #for message in sort_messages(backend.get_messages()):
683         #       pprint.pprint(decorate_message(message))
684
685         return backend
686
687
688 if __name__ == "__main__":
689         import sys
690         logging.basicConfig(level=logging.DEBUG)
691         test_backend(sys.argv[1], sys.argv[2])