Version bump to 1.0, description fixes, and adding of names to the recent tab for...
[gc-dialer] / src / gv_backend.py
1 #!/usr/bin/python
2
3 """
4 DialCentral - Front end for Google's Grand Central 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 warnings
37 import traceback
38 from xml.sax import saxutils
39
40 from xml.etree import ElementTree
41
42 import browser_emu
43
44 try:
45         import simplejson
46 except ImportError:
47         simplejson = None
48
49
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 def abbrev_relative_date(date):
102         """
103         >>> abbrev_relative_date("42 hours ago")
104         '42 h'
105         >>> abbrev_relative_date("2 days ago")
106         '2 d'
107         >>> abbrev_relative_date("4 weeks ago")
108         '4 w'
109         """
110         parts = date.split(" ")
111         return "%s %s" % (parts[0], parts[1][0])
112
113
114 class GVDialer(object):
115         """
116         This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
117         the functions include login, setting up a callback number, and initalting a callback
118         """
119
120         def __init__(self, cookieFile = None):
121                 # Important items in this function are the setup of the browser emulation and cookie file
122                 self._browser = browser_emu.MozillaEmulator(1)
123                 if cookieFile is None:
124                         cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
125                 self._browser.cookies.filename = cookieFile
126                 if os.path.isfile(cookieFile):
127                         self._browser.cookies.load()
128
129                 self._token = ""
130                 self._accountNum = ""
131                 self._lastAuthed = 0.0
132                 self._callbackNumber = ""
133                 self._callbackNumbers = {}
134
135                 self.__contacts = None
136
137         def is_authed(self, force = False):
138                 """
139                 Attempts to detect a current session
140                 @note Once logged in try not to reauth more than once a minute.
141                 @returns If authenticated
142                 """
143
144                 if (time.time() - self._lastAuthed) < 60 and not force:
145                         return True
146
147                 try:
148                         self._grab_account_info()
149                 except StandardError, e:
150                         warnings.warn(traceback.format_exc())
151                         return False
152
153                 self._browser.cookies.save()
154                 self._lastAuthed = time.time()
155                 return True
156
157         _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
158
159         def login(self, username, password):
160                 """
161                 Attempt to login to grandcentral
162                 @returns Whether login was successful or not
163                 """
164                 if self.is_authed():
165                         return True
166
167                 loginPostData = urllib.urlencode({
168                         'Email' : username,
169                         'Passwd' : password,
170                         'service': "grandcentral",
171                         "ltmpl": "mobile",
172                         "btmpl": "mobile",
173                         "PersistentCookie": "yes",
174                 })
175
176                 try:
177                         loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
178                 except urllib2.URLError, e:
179                         warnings.warn(traceback.format_exc())
180                         raise RuntimeError("%s is not accesible" % self._loginURL)
181
182                 return self.is_authed()
183
184         def logout(self):
185                 self._lastAuthed = 0.0
186                 self._browser.cookies.clear()
187                 self._browser.cookies.save()
188
189                 self.clear_caches()
190
191         _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
192         _clicktocallURL = "https://www.google.com/voice/m/sendcall"
193
194         def dial(self, number):
195                 """
196                 This is the main function responsible for initating the callback
197                 """
198                 number = self._send_validation(number)
199                 try:
200                         clickToCallData = urllib.urlencode({
201                                 "number": number,
202                                 "phone": self._callbackNumber,
203                                 "_rnr_se": self._token,
204                         })
205                         otherData = {
206                                 'Referer' : 'https://google.com/voice/m/callsms',
207                         }
208                         callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
209                 except urllib2.URLError, e:
210                         warnings.warn(traceback.format_exc())
211                         raise RuntimeError("%s is not accesible" % self._clicktocallURL)
212
213                 if self._gvDialingStrRe.search(callSuccessPage) is None:
214                         raise RuntimeError("Google Voice returned an error")
215
216                 return True
217
218         _sendSmsURL = "https://www.google.com/voice/m/sendsms"
219
220         def send_sms(self, number, message):
221                 number = self._send_validation(number)
222                 try:
223                         smsData = urllib.urlencode({
224                                 "number": number,
225                                 "smstext": message,
226                                 "_rnr_se": self._token,
227                                 "id": "undefined",
228                                 "c": "undefined",
229                         })
230                         otherData = {
231                                 'Referer' : 'https://google.com/voice/m/sms',
232                         }
233                         smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
234                 except urllib2.URLError, e:
235                         warnings.warn(traceback.format_exc())
236                         raise RuntimeError("%s is not accesible" % self._sendSmsURL)
237
238                 return True
239
240         def clear_caches(self):
241                 self.__contacts = None
242
243         _validateRe = re.compile("^[0-9]{10,}$")
244
245         def is_valid_syntax(self, number):
246                 """
247                 @returns If This number be called ( syntax validation only )
248                 """
249                 return self._validateRe.match(number) is not None
250
251         def get_account_number(self):
252                 """
253                 @returns The grand central phone number
254                 """
255                 return self._accountNum
256
257         def set_sane_callback(self):
258                 """
259                 Try to set a sane default callback number on these preferences
260                 1) 1747 numbers ( Gizmo )
261                 2) anything with gizmo in the name
262                 3) anything with computer in the name
263                 4) the first value
264                 """
265                 numbers = self.get_callback_numbers()
266
267                 for number, description in numbers.iteritems():
268                         if re.compile(r"""1747""").match(number) is not None:
269                                 self.set_callback_number(number)
270                                 return
271
272                 for number, description in numbers.iteritems():
273                         if re.compile(r"""gizmo""", re.I).search(description) is not None:
274                                 self.set_callback_number(number)
275                                 return
276
277                 for number, description in numbers.iteritems():
278                         if re.compile(r"""computer""", re.I).search(description) is not None:
279                                 self.set_callback_number(number)
280                                 return
281
282                 for number, description in numbers.iteritems():
283                         self.set_callback_number(number)
284                         return
285
286         def get_callback_numbers(self):
287                 """
288                 @returns a dictionary mapping call back numbers to descriptions
289                 @note These results are cached for 30 minutes.
290                 """
291                 if time.time() - self._lastAuthed < 1800 or self.is_authed():
292                         return self._callbackNumbers
293
294                 return {}
295
296         _setforwardURL = "https://www.google.com//voice/m/setphone"
297
298         def set_callback_number(self, callbacknumber):
299                 """
300                 Set the number that grandcental calls
301                 @param callbacknumber should be a proper 10 digit number
302                 """
303                 self._callbackNumber = callbacknumber
304                 callbackPostData = urllib.urlencode({
305                         '_rnr_se': self._token,
306                         'phone': callbacknumber
307                 })
308                 try:
309                         callbackSetPage = self._browser.download(self._setforwardURL, callbackPostData)
310                 except urllib2.URLError, e:
311                         warnings.warn(traceback.format_exc())
312                         raise RuntimeError("%s is not accesible" % self._setforwardURL)
313
314                 self._browser.cookies.save()
315                 return True
316
317         def get_callback_number(self):
318                 """
319                 @returns Current callback number or None
320                 """
321                 for c in self._browser.cookies:
322                         if c.name == "gv-ph":
323                                 return c.value
324                 return self._callbackNumber
325
326         def get_recent(self):
327                 """
328                 @returns Iterable of (personsName, phoneNumber, date, action)
329                 """
330                 sortedRecent = [
331                         (exactDate, name, number, relativeDate, action)
332                         for (name, number, exactDate, relativeDate, action) in self._get_recent()
333                 ]
334                 sortedRecent.sort(reverse = True)
335                 for exactDate, name, number, relativeDate, action in sortedRecent:
336                         relativeDate = abbrev_relative_date(relativeDate)
337                         yield name, number, relativeDate, action
338
339         def get_addressbooks(self):
340                 """
341                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
342                 """
343                 yield self, "", ""
344
345         def open_addressbook(self, bookId):
346                 return self
347
348         @staticmethod
349         def contact_source_short_name(contactId):
350                 return "GV"
351
352         @staticmethod
353         def factory_name():
354                 return "Google Voice"
355
356         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
357         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
358         _contactsURL = "https://www.google.com/voice/mobile/contacts"
359
360         def get_contacts(self):
361                 """
362                 @returns Iterable of (contact id, contact name)
363                 """
364                 if self.__contacts is None:
365                         self.__contacts = []
366
367                         contactsPagesUrls = [self._contactsURL]
368                         for contactsPageUrl in contactsPagesUrls:
369                                 try:
370                                         contactsPage = self._browser.download(contactsPageUrl)
371                                 except urllib2.URLError, e:
372                                         warnings.warn(traceback.format_exc())
373                                         raise RuntimeError("%s is not accesible" % contactsPageUrl)
374                                 for contact_match in self._contactsRe.finditer(contactsPage):
375                                         contactId = contact_match.group(1)
376                                         contactName = saxutils.unescape(contact_match.group(2))
377                                         contact = contactId, contactName
378                                         self.__contacts.append(contact)
379                                         yield contact
380
381                                 next_match = self._contactsNextRe.match(contactsPage)
382                                 if next_match is not None:
383                                         newContactsPageUrl = self._contactsURL + next_match.group(1)
384                                         contactsPagesUrls.append(newContactsPageUrl)
385                 else:
386                         for contact in self.__contacts:
387                                 yield contact
388
389         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
390         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
391
392         def get_contact_details(self, contactId):
393                 """
394                 @returns Iterable of (Phone Type, Phone Number)
395                 """
396                 try:
397                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
398                 except urllib2.URLError, e:
399                         warnings.warn(traceback.format_exc())
400                         raise RuntimeError("%s is not accesible" % self._contactDetailURL)
401
402                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
403                         phoneNumber = detail_match.group(1)
404                         phoneType = saxutils.unescape(detail_match.group(2))
405                         yield (phoneType, phoneNumber)
406
407         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
408         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
409
410         def get_messages(self):
411                 try:
412                         voicemailPage = self._browser.download(self._voicemailURL)
413                 except urllib2.URLError, e:
414                         warnings.warn(traceback.format_exc())
415                         raise RuntimeError("%s is not accesible" % self._voicemailURL)
416                 voicemailHtml = self._grab_html(voicemailPage)
417                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
418                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
419
420                 try:
421                         smsPage = self._browser.download(self._smsURL)
422                 except urllib2.URLError, e:
423                         warnings.warn(traceback.format_exc())
424                         raise RuntimeError("%s is not accesible" % self._smsURL)
425                 smsHtml = self._grab_html(smsPage)
426                 parsedSms = self._parse_sms(smsHtml)
427                 decoratedSms = self._decorate_sms(parsedSms)
428
429                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
430                 sortedMessages = list(allMessages)
431                 sortedMessages.sort(reverse=True)
432                 for exactDate, header, number, relativeDate, message in sortedMessages:
433                         relativeDate = abbrev_relative_date(relativeDate)
434                         yield header, number, relativeDate, message
435
436         def _grab_json(self, flatXml):
437                 xmlTree = ElementTree.fromstring(flatXml)
438                 jsonElement = xmlTree.getchildren()[0]
439                 flatJson = jsonElement.text
440                 jsonTree = parse_json(flatJson)
441                 return jsonTree
442
443         def _grab_html(self, flatXml):
444                 xmlTree = ElementTree.fromstring(flatXml)
445                 htmlElement = xmlTree.getchildren()[1]
446                 flatHtml = htmlElement.text
447                 return flatHtml
448
449         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
450         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
451         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
452         _forwardURL = "https://www.google.com/voice/mobile/phones"
453
454         def _grab_account_info(self):
455                 page = self._browser.download(self._forwardURL)
456
457                 tokenGroup = self._tokenRe.search(page)
458                 if tokenGroup is None:
459                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
460                 self._token = tokenGroup.group(1)
461
462                 anGroup = self._accountNumRe.search(page)
463                 if anGroup is not None:
464                         self._accountNum = anGroup.group(1)
465                 else:
466                         warnings.warn("Could not extract account number from GoogleVoice", UserWarning, 2)
467
468                 self._callbackNumbers = {}
469                 for match in self._callbackRe.finditer(page):
470                         callbackNumber = match.group(2)
471                         callbackName = match.group(1)
472                         self._callbackNumbers[callbackNumber] = callbackName
473
474         def _send_validation(self, number):
475                 if not self.is_valid_syntax(number):
476                         raise ValueError('Number is not valid: "%s"' % number)
477                 elif not self.is_authed():
478                         raise RuntimeError("Not Authenticated")
479
480                 if len(number) == 11 and number[0] == 1:
481                         # Strip leading 1 from 11 digit dialing
482                         number = number[1:]
483                 return number
484
485         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
486         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
487         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
488         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
489
490         def _get_recent(self):
491                 """
492                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
493                 """
494                 for action, url in (
495                         ("Recieved", self._receivedCallsURL),
496                         ("Missed", self._missedCallsURL),
497                         ("Placed", self._placedCallsURL),
498                 ):
499                         try:
500                                 flatXml = self._browser.download(url)
501                         except urllib2.URLError, e:
502                                 warnings.warn(traceback.format_exc())
503                                 raise RuntimeError("%s is not accesible" % url)
504
505                         allRecentHtml = self._grab_html(flatXml)
506                         allRecentData = self._parse_voicemail(allRecentHtml)
507                         for recentCallData in allRecentData:
508                                 exactTime = recentCallData["time"]
509                                 if recentCallData["name"]:
510                                         header = recentCallData["name"]
511                                 elif recentCallData["prettyNumber"]:
512                                         header = recentCallData["prettyNumber"]
513                                 elif recentCallData["location"]:
514                                         header = recentCallData["location"]
515                                 else:
516                                         header = "Unknown"
517                                 yield header, recentCallData["number"], exactTime, recentCallData["relTime"], action
518
519         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class="gc-message.*?">""", re.MULTILINE | re.DOTALL)
520         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
521         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
522         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
523         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
524         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
525         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
526         _voicemailMessageRegex = re.compile(r"""<span class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
527
528         def _parse_voicemail(self, voicemailHtml):
529                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
530                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
531                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
532                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
533                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
534                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
535                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
536                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
537                         location = locationGroup.group(1).strip() if locationGroup else ""
538
539                         nameGroup = self._voicemailNameRegex.search(messageHtml)
540                         name = nameGroup.group(1).strip() if nameGroup else ""
541                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
542                         number = numberGroup.group(1).strip() if numberGroup else ""
543                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
544                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
545
546                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
547                         messageParts = (
548                                 (group.group(1).strip(), group.group(2).strip())
549                                 for group in messageGroups
550                         ) if messageGroups else ()
551
552                         yield {
553                                 "id": messageId.strip(),
554                                 "name": name,
555                                 "time": exactTime,
556                                 "relTime": relativeTime,
557                                 "prettyNumber": prettyNumber,
558                                 "number": number,
559                                 "location": location,
560                                 "messageParts": messageParts,
561                         }
562
563         def _decorate_voicemail(self, parsedVoicemail):
564                 messagePartFormat = {
565                         "med1": "<i>%s</i>",
566                         "med2": "%s",
567                         "high": "<b>%s</b>",
568                 }
569                 for voicemailData in parsedVoicemail:
570                         exactTime = voicemailData["time"]
571                         if voicemailData["name"]:
572                                 header = voicemailData["name"]
573                         elif voicemailData["prettyNumber"]:
574                                 header = voicemailData["prettyNumber"]
575                         elif voicemailData["location"]:
576                                 header = voicemailData["location"]
577                         else:
578                                 header = "Unknown"
579                         message = " ".join((
580                                 messagePartFormat[quality] % part
581                                 for (quality, part) in voicemailData["messageParts"]
582                         )).strip()
583                         if not message:
584                                 message = "No Transcription"
585                         yield exactTime, header, voicemailData["number"], voicemailData["relTime"], message
586
587         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
588         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
589         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
590
591         def _parse_sms(self, smsHtml):
592                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
593                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
594                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
595                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
596                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
597                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
598                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
599
600                         nameGroup = self._voicemailNameRegex.search(messageHtml)
601                         name = nameGroup.group(1).strip() if nameGroup else ""
602                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
603                         number = numberGroup.group(1).strip() if numberGroup else ""
604                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
605                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
606
607                         fromGroups = self._smsFromRegex.finditer(messageHtml)
608                         fromParts = (group.group(1).strip() for group in fromGroups)
609                         textGroups = self._smsTextRegex.finditer(messageHtml)
610                         textParts = (group.group(1).strip() for group in textGroups)
611                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
612                         timeParts = (group.group(1).strip() for group in timeGroups)
613
614                         messageParts = itertools.izip(fromParts, textParts, timeParts)
615
616                         yield {
617                                 "id": messageId.strip(),
618                                 "name": name,
619                                 "time": exactTime,
620                                 "relTime": relativeTime,
621                                 "prettyNumber": prettyNumber,
622                                 "number": number,
623                                 "messageParts": messageParts,
624                         }
625
626         def _decorate_sms(self, parsedSms):
627                 for messageData in parsedSms:
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                         message = "\n".join((
638                                 "<b>%s</b>: %s" % (messagePart[0], messagePart[-1])
639                                 for messagePart in messageData["messageParts"]
640                         ))
641                         if not message:
642                                 message = "No Transcription"
643                         yield exactTime, header, number, relativeTime, message
644
645
646 def test_backend(username, password):
647         import pprint
648         backend = GVDialer()
649         print "Authenticated: ", backend.is_authed()
650         print "Login?: ", backend.login(username, password)
651         print "Authenticated: ", backend.is_authed()
652         # print "Token: ", backend._token
653         print "Account: ", backend.get_account_number()
654         print "Callback: ", backend.get_callback_number()
655         # print "All Callback: ",
656         # pprint.pprint(backend.get_callback_numbers())
657         # print "Recent: ",
658         # pprint.pprint(list(backend.get_recent()))
659         # print "Contacts: ",
660         # for contact in backend.get_contacts():
661         #       print contact
662         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
663
664         return backend