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