3daf6c6a3c277745474609cd50db8722881a94d4
[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 url in (
495                         self._receivedCallsURL,
496                         self._missedCallsURL,
497                         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                         allRecentData = self._grab_json(flatXml)
506                         for recentCallData in allRecentData["messages"].itervalues():
507                                 number = recentCallData["displayNumber"]
508                                 exactDate = recentCallData["displayStartDateTime"]
509                                 relativeDate = recentCallData["relativeStartTime"]
510                                 action = ", ".join((
511                                         label.title()
512                                         for label in recentCallData["labels"]
513                                                 if label.lower() != "all" and label.lower() != "inbox"
514                                 ))
515                                 number = saxutils.unescape(number)
516                                 exactDate = saxutils.unescape(exactDate)
517                                 exactDate = datetime.datetime.strptime(exactDate, "%m/%d/%y %I:%M %p")
518                                 relativeDate = saxutils.unescape(relativeDate)
519                                 action = saxutils.unescape(action)
520                                 yield "", number, exactDate, relativeDate, action
521
522         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class="gc-message.*?">""", re.MULTILINE | re.DOTALL)
523         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
524         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
525         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
526         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
527         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
528         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
529         _voicemailMessageRegex = re.compile(r"""<span class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
530
531         def _parse_voicemail(self, voicemailHtml):
532                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
533                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
534                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
535                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
536                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
537                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
538                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
539                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
540                         location = locationGroup.group(1).strip() if locationGroup else ""
541
542                         nameGroup = self._voicemailNameRegex.search(messageHtml)
543                         name = nameGroup.group(1).strip() if nameGroup else ""
544                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
545                         number = numberGroup.group(1).strip() if numberGroup else ""
546                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
547                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
548
549                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
550                         messageParts = (
551                                 (group.group(1).strip(), group.group(2).strip())
552                                 for group in messageGroups
553                         ) if messageGroups else ()
554
555                         yield {
556                                 "id": messageId.strip(),
557                                 "name": name,
558                                 "time": exactTime,
559                                 "relTime": relativeTime,
560                                 "prettyNumber": prettyNumber,
561                                 "number": number,
562                                 "location": location,
563                                 "messageParts": messageParts,
564                         }
565
566         def _decorate_voicemail(self, parsedVoicemail):
567                 messagePartFormat = {
568                         "med1": "<i>%s</i>",
569                         "med2": "%s",
570                         "high": "<b>%s</b>",
571                 }
572                 for voicemailData in parsedVoicemail:
573                         exactTime = voicemailData["time"]
574                         if voicemailData["name"]:
575                                 header = voicemailData["name"]
576                         elif voicemailData["prettyNumber"]:
577                                 header = voicemailData["prettyNumber"]
578                         elif voicemailData["location"]:
579                                 header = voicemailData["location"]
580                         else:
581                                 header = "Unknown"
582                         message = " ".join((
583                                 messagePartFormat[quality] % part
584                                 for (quality, part) in voicemailData["messageParts"]
585                         )).strip()
586                         if not message:
587                                 message = "No Transcription"
588                         yield exactTime, header, voicemailData["number"], voicemailData["relTime"], message
589
590         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
591         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
592         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
593
594         def _parse_sms(self, smsHtml):
595                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
596                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
597                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
598                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
599                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
600                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
601                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
602
603                         nameGroup = self._voicemailNameRegex.search(messageHtml)
604                         name = nameGroup.group(1).strip() if nameGroup else ""
605                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
606                         number = numberGroup.group(1).strip() if numberGroup else ""
607                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
608                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
609
610                         fromGroups = self._smsFromRegex.finditer(messageHtml)
611                         fromParts = (group.group(1).strip() for group in fromGroups)
612                         textGroups = self._smsTextRegex.finditer(messageHtml)
613                         textParts = (group.group(1).strip() for group in textGroups)
614                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
615                         timeParts = (group.group(1).strip() for group in timeGroups)
616
617                         messageParts = itertools.izip(fromParts, textParts, timeParts)
618
619                         yield {
620                                 "id": messageId.strip(),
621                                 "name": name,
622                                 "time": exactTime,
623                                 "relTime": relativeTime,
624                                 "prettyNumber": prettyNumber,
625                                 "number": number,
626                                 "messageParts": messageParts,
627                         }
628
629         def _decorate_sms(self, parsedSms):
630                 for messageData in parsedSms:
631                         exactTime = messageData["time"]
632                         if messageData["name"]:
633                                 header = messageData["name"]
634                         elif messageData["prettyNumber"]:
635                                 header = messageData["prettyNumber"]
636                         else:
637                                 header = "Unknown"
638                         number = messageData["number"]
639                         relativeTime = messageData["relTime"]
640                         message = "\n".join((
641                                 "<b>%s</b>: %s" % (messagePart[0], messagePart[-1])
642                                 for messagePart in messageData["messageParts"]
643                         ))
644                         if not message:
645                                 message = "No Transcription"
646                         yield exactTime, header, number, relativeTime, message
647
648
649 def test_backend(username, password):
650         import pprint
651         backend = GVDialer()
652         print "Authenticated: ", backend.is_authed()
653         print "Login?: ", backend.login(username, password)
654         print "Authenticated: ", backend.is_authed()
655         # print "Token: ", backend._token
656         print "Account: ", backend.get_account_number()
657         print "Callback: ", backend.get_callback_number()
658         # print "All Callback: ",
659         # pprint.pprint(backend.get_callback_numbers())
660         # print "Recent: ",
661         # pprint.pprint(list(backend.get_recent()))
662         # print "Contacts: ",
663         # for contact in backend.get_contacts():
664         #       print contact
665         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
666
667         return backend