Fixing the title under maemo
[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) < 120 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 not self.is_authed():
292                         return {}
293                 return self._callbackNumbers
294
295         _setforwardURL = "https://www.google.com//voice/m/setphone"
296
297         def set_callback_number(self, callbacknumber):
298                 """
299                 Set the number that grandcental calls
300                 @param callbacknumber should be a proper 10 digit number
301                 """
302                 self._callbackNumber = callbacknumber
303
304                 # Currently this isn't working out in GoogleVoice, but thats ok, we pass the callback on dial
305                 #callbackPostData = urllib.urlencode({
306                 #       '_rnr_se': self._token,
307                 #       'phone': callbacknumber
308                 #})
309                 #try:
310                 #       callbackSetPage = self._browser.download(self._setforwardURL, callbackPostData)
311                 #       self._browser.cookies.save()
312                 #except urllib2.URLError, e:
313                 #       warnings.warn(traceback.format_exc())
314                 #       raise RuntimeError("%s is not accesible" % self._setforwardURL)
315
316                 return True
317
318         def get_callback_number(self):
319                 """
320                 @returns Current callback number or None
321                 """
322                 #for c in self._browser.cookies:
323                 #       if c.name == "gv-ph":
324                 #               return c.value
325                 return self._callbackNumber
326
327         def get_recent(self):
328                 """
329                 @returns Iterable of (personsName, phoneNumber, date, action)
330                 """
331                 sortedRecent = [
332                         (exactDate, name, number, relativeDate, action)
333                         for (name, number, exactDate, relativeDate, action) in self._get_recent()
334                 ]
335                 sortedRecent.sort(reverse = True)
336                 for exactDate, name, number, relativeDate, action in sortedRecent:
337                         relativeDate = abbrev_relative_date(relativeDate)
338                         yield name, number, relativeDate, action
339
340         def get_addressbooks(self):
341                 """
342                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
343                 """
344                 yield self, "", ""
345
346         def open_addressbook(self, bookId):
347                 return self
348
349         @staticmethod
350         def contact_source_short_name(contactId):
351                 return "GV"
352
353         @staticmethod
354         def factory_name():
355                 return "Google Voice"
356
357         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
358         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
359         _contactsURL = "https://www.google.com/voice/mobile/contacts"
360
361         def get_contacts(self):
362                 """
363                 @returns Iterable of (contact id, contact name)
364                 """
365                 if self.__contacts is None:
366                         self.__contacts = []
367
368                         contactsPagesUrls = [self._contactsURL]
369                         for contactsPageUrl in contactsPagesUrls:
370                                 try:
371                                         contactsPage = self._browser.download(contactsPageUrl)
372                                 except urllib2.URLError, e:
373                                         warnings.warn(traceback.format_exc())
374                                         raise RuntimeError("%s is not accesible" % contactsPageUrl)
375                                 for contact_match in self._contactsRe.finditer(contactsPage):
376                                         contactId = contact_match.group(1)
377                                         contactName = saxutils.unescape(contact_match.group(2))
378                                         contact = contactId, contactName
379                                         self.__contacts.append(contact)
380                                         yield contact
381
382                                 next_match = self._contactsNextRe.match(contactsPage)
383                                 if next_match is not None:
384                                         newContactsPageUrl = self._contactsURL + next_match.group(1)
385                                         contactsPagesUrls.append(newContactsPageUrl)
386                 else:
387                         for contact in self.__contacts:
388                                 yield contact
389
390         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
391         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
392
393         def get_contact_details(self, contactId):
394                 """
395                 @returns Iterable of (Phone Type, Phone Number)
396                 """
397                 try:
398                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
399                 except urllib2.URLError, e:
400                         warnings.warn(traceback.format_exc())
401                         raise RuntimeError("%s is not accesible" % self._contactDetailURL)
402
403                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
404                         phoneNumber = detail_match.group(1)
405                         phoneType = saxutils.unescape(detail_match.group(2))
406                         yield (phoneType, phoneNumber)
407
408         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
409         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
410
411         def get_messages(self):
412                 try:
413                         voicemailPage = self._browser.download(self._voicemailURL)
414                 except urllib2.URLError, e:
415                         warnings.warn(traceback.format_exc())
416                         raise RuntimeError("%s is not accesible" % self._voicemailURL)
417                 voicemailHtml = self._grab_html(voicemailPage)
418                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
419                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
420
421                 try:
422                         smsPage = self._browser.download(self._smsURL)
423                 except urllib2.URLError, e:
424                         warnings.warn(traceback.format_exc())
425                         raise RuntimeError("%s is not accesible" % self._smsURL)
426                 smsHtml = self._grab_html(smsPage)
427                 parsedSms = self._parse_sms(smsHtml)
428                 decoratedSms = self._decorate_sms(parsedSms)
429
430                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
431                 sortedMessages = list(allMessages)
432                 sortedMessages.sort(reverse=True)
433                 for exactDate, header, number, relativeDate, message in sortedMessages:
434                         relativeDate = abbrev_relative_date(relativeDate)
435                         yield header, number, relativeDate, message
436
437         def _grab_json(self, flatXml):
438                 xmlTree = ElementTree.fromstring(flatXml)
439                 jsonElement = xmlTree.getchildren()[0]
440                 flatJson = jsonElement.text
441                 jsonTree = parse_json(flatJson)
442                 return jsonTree
443
444         def _grab_html(self, flatXml):
445                 xmlTree = ElementTree.fromstring(flatXml)
446                 htmlElement = xmlTree.getchildren()[1]
447                 flatHtml = htmlElement.text
448                 return flatHtml
449
450         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
451         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
452         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
453         _forwardURL = "https://www.google.com/voice/mobile/phones"
454
455         def _grab_account_info(self):
456                 page = self._browser.download(self._forwardURL)
457
458                 tokenGroup = self._tokenRe.search(page)
459                 if tokenGroup is None:
460                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
461                 self._token = tokenGroup.group(1)
462
463                 anGroup = self._accountNumRe.search(page)
464                 if anGroup is not None:
465                         self._accountNum = anGroup.group(1)
466                 else:
467                         warnings.warn("Could not extract account number from GoogleVoice", UserWarning, 2)
468
469                 self._callbackNumbers = {}
470                 for match in self._callbackRe.finditer(page):
471                         callbackNumber = match.group(2)
472                         callbackName = match.group(1)
473                         self._callbackNumbers[callbackNumber] = callbackName
474
475         def _send_validation(self, number):
476                 if not self.is_valid_syntax(number):
477                         raise ValueError('Number is not valid: "%s"' % number)
478                 elif not self.is_authed():
479                         raise RuntimeError("Not Authenticated")
480
481                 if len(number) == 11 and number[0] == 1:
482                         # Strip leading 1 from 11 digit dialing
483                         number = number[1:]
484                 return number
485
486         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
487         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
488         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
489         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
490
491         def _get_recent(self):
492                 """
493                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
494                 """
495                 for action, url in (
496                         ("Recieved", self._receivedCallsURL),
497                         ("Missed", self._missedCallsURL),
498                         ("Placed", self._placedCallsURL),
499                 ):
500                         try:
501                                 flatXml = self._browser.download(url)
502                         except urllib2.URLError, e:
503                                 warnings.warn(traceback.format_exc())
504                                 raise RuntimeError("%s is not accesible" % url)
505
506                         allRecentHtml = self._grab_html(flatXml)
507                         allRecentData = self._parse_voicemail(allRecentHtml)
508                         for recentCallData in allRecentData:
509                                 exactTime = recentCallData["time"]
510                                 if recentCallData["name"]:
511                                         header = recentCallData["name"]
512                                 elif recentCallData["prettyNumber"]:
513                                         header = recentCallData["prettyNumber"]
514                                 elif recentCallData["location"]:
515                                         header = recentCallData["location"]
516                                 else:
517                                         header = "Unknown"
518                                 yield header, recentCallData["number"], exactTime, recentCallData["relTime"], action
519
520         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
521         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
522         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
523         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
524         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
525         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
526         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
527         _voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
528
529         def _parse_voicemail(self, voicemailHtml):
530                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
531                 for messageId, messageHtml in itergroup(splitVoicemail[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                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
538                         location = locationGroup.group(1).strip() if locationGroup else ""
539
540                         nameGroup = self._voicemailNameRegex.search(messageHtml)
541                         name = nameGroup.group(1).strip() if nameGroup else ""
542                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
543                         number = numberGroup.group(1).strip() if numberGroup else ""
544                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
545                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
546
547                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
548                         messageParts = (
549                                 (group.group(1).strip(), group.group(2).strip())
550                                 for group in messageGroups
551                         ) if messageGroups else ()
552
553                         yield {
554                                 "id": messageId.strip(),
555                                 "name": name,
556                                 "time": exactTime,
557                                 "relTime": relativeTime,
558                                 "prettyNumber": prettyNumber,
559                                 "number": number,
560                                 "location": location,
561                                 "messageParts": messageParts,
562                         }
563
564         def _decorate_voicemail(self, parsedVoicemail):
565                 messagePartFormat = {
566                         "med1": "<i>%s</i>",
567                         "med2": "%s",
568                         "high": "<b>%s</b>",
569                 }
570                 for voicemailData in parsedVoicemail:
571                         exactTime = voicemailData["time"]
572                         if voicemailData["name"]:
573                                 header = voicemailData["name"]
574                         elif voicemailData["prettyNumber"]:
575                                 header = voicemailData["prettyNumber"]
576                         elif voicemailData["location"]:
577                                 header = voicemailData["location"]
578                         else:
579                                 header = "Unknown"
580                         message = " ".join((
581                                 messagePartFormat[quality] % part
582                                 for (quality, part) in voicemailData["messageParts"]
583                         )).strip()
584                         if not message:
585                                 message = "No Transcription"
586                         yield exactTime, header, voicemailData["number"], voicemailData["relTime"], message
587
588         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
589         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
590         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
591
592         def _parse_sms(self, smsHtml):
593                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
594                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
595                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
596                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
597                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
598                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
599                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
600
601                         nameGroup = self._voicemailNameRegex.search(messageHtml)
602                         name = nameGroup.group(1).strip() if nameGroup else ""
603                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
604                         number = numberGroup.group(1).strip() if numberGroup else ""
605                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
606                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
607
608                         fromGroups = self._smsFromRegex.finditer(messageHtml)
609                         fromParts = (group.group(1).strip() for group in fromGroups)
610                         textGroups = self._smsTextRegex.finditer(messageHtml)
611                         textParts = (group.group(1).strip() for group in textGroups)
612                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
613                         timeParts = (group.group(1).strip() for group in timeGroups)
614
615                         messageParts = itertools.izip(fromParts, textParts, timeParts)
616
617                         yield {
618                                 "id": messageId.strip(),
619                                 "name": name,
620                                 "time": exactTime,
621                                 "relTime": relativeTime,
622                                 "prettyNumber": prettyNumber,
623                                 "number": number,
624                                 "messageParts": messageParts,
625                         }
626
627         def _decorate_sms(self, parsedSms):
628                 for messageData in parsedSms:
629                         exactTime = messageData["time"]
630                         if messageData["name"]:
631                                 header = messageData["name"]
632                         elif messageData["prettyNumber"]:
633                                 header = messageData["prettyNumber"]
634                         else:
635                                 header = "Unknown"
636                         number = messageData["number"]
637                         relativeTime = messageData["relTime"]
638                         message = "\n".join((
639                                 "<b>%s</b>: %s" % (messagePart[0], messagePart[-1])
640                                 for messagePart in messageData["messageParts"]
641                         ))
642                         if not message:
643                                 message = "No Transcription"
644                         yield exactTime, header, number, relativeTime, message
645
646
647 def test_backend(username, password):
648         import pprint
649         backend = GVDialer()
650         print "Authenticated: ", backend.is_authed()
651         print "Login?: ", backend.login(username, password)
652         print "Authenticated: ", backend.is_authed()
653         # print "Token: ", backend._token
654         print "Account: ", backend.get_account_number()
655         print "Callback: ", backend.get_callback_number()
656         # print "All Callback: ",
657         # pprint.pprint(backend.get_callback_numbers())
658         # print "Recent: ",
659         # pprint.pprint(list(backend.get_recent()))
660         # print "Contacts: ",
661         # for contact in backend.get_contacts():
662         #       print contact
663         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
664
665         return backend