Fixing a bug with sending SMS
[gc-dialer] / src / gv_backend.py
1 #!/usr/bin/python
2
3 """
4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 Google Voice backend code
22
23 Resources
24         http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
25         http://posttopic.com/topic/google-voice-add-on-development
26 """
27
28
29 import os
30 import re
31 import urllib
32 import urllib2
33 import time
34 import datetime
35 import itertools
36 import logging
37 from xml.sax import saxutils
38
39 from xml.etree import ElementTree
40
41 import browser_emu
42
43 try:
44         import simplejson
45 except ImportError:
46         simplejson = None
47
48
49 _moduleLogger = logging.getLogger("gvoice.dialer")
50 _TRUE_REGEX = re.compile("true")
51 _FALSE_REGEX = re.compile("false")
52
53
54 def safe_eval(s):
55         s = _TRUE_REGEX.sub("True", s)
56         s = _FALSE_REGEX.sub("False", s)
57         return eval(s, {}, {})
58
59
60 if simplejson is None:
61         def parse_json(flattened):
62                 return safe_eval(flattened)
63 else:
64         def parse_json(flattened):
65                 return simplejson.loads(flattened)
66
67
68 def itergroup(iterator, count, padValue = None):
69         """
70         Iterate in groups of 'count' values. If there
71         aren't enough values, the last result is padded with
72         None.
73
74         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
75         ...     print tuple(val)
76         (1, 2, 3)
77         (4, 5, 6)
78         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
79         ...     print list(val)
80         [1, 2, 3]
81         [4, 5, 6]
82         >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
83         ...     print tuple(val)
84         (1, 2, 3)
85         (4, 5, 6)
86         (7, None, None)
87         >>> for val in itergroup("123456", 3):
88         ...     print tuple(val)
89         ('1', '2', '3')
90         ('4', '5', '6')
91         >>> for val in itergroup("123456", 3):
92         ...     print repr("".join(val))
93         '123'
94         '456'
95         """
96         paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
97         nIterators = (paddedIterator, ) * count
98         return itertools.izip(*nIterators)
99
100
101 class NetworkError(RuntimeError):
102         pass
103
104
105 class GVDialer(object):
106         """
107         This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
108         the functions include login, setting up a callback number, and initalting a callback
109         """
110
111         def __init__(self, cookieFile = None):
112                 # Important items in this function are the setup of the browser emulation and cookie file
113                 self._browser = browser_emu.MozillaEmulator(1)
114                 if cookieFile is None:
115                         cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
116                 self._browser.cookies.filename = cookieFile
117                 if os.path.isfile(cookieFile):
118                         self._browser.cookies.load()
119
120                 self._token = ""
121                 self._accountNum = ""
122                 self._lastAuthed = 0.0
123                 self._callbackNumber = ""
124                 self._callbackNumbers = {}
125
126         def is_authed(self, force = False):
127                 """
128                 Attempts to detect a current session
129                 @note Once logged in try not to reauth more than once a minute.
130                 @returns If authenticated
131                 """
132                 if (time.time() - self._lastAuthed) < 120 and not force:
133                         return True
134
135                 try:
136                         page = self._browser.download(self._forwardURL)
137                         self._grab_account_info(page)
138                 except Exception, e:
139                         _moduleLogger.exception(str(e))
140                         return False
141
142                 self._browser.cookies.save()
143                 self._lastAuthed = time.time()
144                 return True
145
146         _tokenURL = "http://www.google.com/voice/m"
147         _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
148         _galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
149
150         def login(self, username, password):
151                 """
152                 Attempt to login to GoogleVoice
153                 @returns Whether login was successful or not
154                 """
155                 try:
156                         tokenPage = self._browser.download(self._tokenURL)
157                 except urllib2.URLError, e:
158                         _moduleLogger.exception("Translating error: %s" % str(e))
159                         raise NetworkError("%s is not accesible" % self._loginURL)
160                 galxTokens = self._galxRe.search(tokenPage)
161                 if galxTokens is not None:
162                         galxToken = galxTokens.group(1)
163                 else:
164                         galxToken = ""
165                         _moduleLogger.debug("Could not grab GALX token")
166
167                 loginPostData = urllib.urlencode({
168                         'Email' : username,
169                         'Passwd' : password,
170                         'service': "grandcentral",
171                         "ltmpl": "mobile",
172                         "btmpl": "mobile",
173                         "PersistentCookie": "yes",
174                         "GALX": galxToken,
175                         "continue": self._forwardURL,
176                 })
177
178                 try:
179                         loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
180                 except urllib2.URLError, e:
181                         _moduleLogger.exception("Translating error: %s" % str(e))
182                         raise NetworkError("%s is not accesible" % self._loginURL)
183
184                 try:
185                         self._grab_account_info(loginSuccessOrFailurePage)
186                 except Exception, e:
187                         _moduleLogger.exception(str(e))
188                         return False
189
190                 self._browser.cookies.save()
191                 self._lastAuthed = time.time()
192                 return True
193
194         def logout(self):
195                 self._lastAuthed = 0.0
196                 self._browser.cookies.clear()
197                 self._browser.cookies.save()
198
199         _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
200         _clicktocallURL = "https://www.google.com/voice/m/sendcall"
201
202         def dial(self, number):
203                 """
204                 This is the main function responsible for initating the callback
205                 """
206                 number = self._send_validation(number)
207                 try:
208                         clickToCallData = urllib.urlencode({
209                                 "number": number,
210                                 "phone": self._callbackNumber,
211                                 "_rnr_se": self._token,
212                         })
213                         otherData = {
214                                 'Referer' : 'https://google.com/voice/m/callsms',
215                         }
216                         callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
217                 except urllib2.URLError, e:
218                         _moduleLogger.exception("Translating error: %s" % str(e))
219                         raise NetworkError("%s is not accesible" % self._clicktocallURL)
220
221                 if self._gvDialingStrRe.search(callSuccessPage) is None:
222                         raise RuntimeError("Google Voice returned an error")
223
224                 return True
225
226         _sendSmsURL = "https://www.google.com/voice/m/sendsms"
227
228         def send_sms(self, number, message):
229                 number = self._send_validation(number)
230                 try:
231                         smsData = urllib.urlencode({
232                                 "number": number,
233                                 "smstext": message,
234                                 "_rnr_se": self._token,
235                                 "id": "undefined",
236                                 "c": "undefined",
237                         })
238                         otherData = {
239                                 'Referer' : 'https://google.com/voice/m/sms',
240                         }
241                         smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
242                 except urllib2.URLError, e:
243                         _moduleLogger.exception("Translating error: %s" % str(e))
244                         raise NetworkError("%s is not accesible" % self._sendSmsURL)
245
246                 return True
247
248         _validateRe = re.compile("^[0-9]{10,}$")
249
250         def is_valid_syntax(self, number):
251                 """
252                 @returns If This number be called ( syntax validation only )
253                 """
254                 return self._validateRe.match(number) is not None
255
256         def get_account_number(self):
257                 """
258                 @returns The GoogleVoice phone number
259                 """
260                 return self._accountNum
261
262         def get_callback_numbers(self):
263                 """
264                 @returns a dictionary mapping call back numbers to descriptions
265                 @note These results are cached for 30 minutes.
266                 """
267                 if not self.is_authed():
268                         return {}
269                 return self._callbackNumbers
270
271         _setforwardURL = "https://www.google.com//voice/m/setphone"
272
273         def set_callback_number(self, callbacknumber):
274                 """
275                 Set the number that GoogleVoice calls
276                 @param callbacknumber should be a proper 10 digit number
277                 """
278                 self._callbackNumber = callbacknumber
279                 return True
280
281         def get_callback_number(self):
282                 """
283                 @returns Current callback number or None
284                 """
285                 return self._callbackNumber
286
287         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
288         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
289         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
290         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
291
292         def get_recent(self):
293                 """
294                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
295                 """
296                 for action, url in (
297                         ("Received", self._receivedCallsURL),
298                         ("Missed", self._missedCallsURL),
299                         ("Placed", self._placedCallsURL),
300                 ):
301                         try:
302                                 flatXml = self._browser.download(url)
303                         except urllib2.URLError, e:
304                                 _moduleLogger.exception("Translating error: %s" % str(e))
305                                 raise NetworkError("%s is not accesible" % url)
306
307                         allRecentHtml = self._grab_html(flatXml)
308                         allRecentData = self._parse_voicemail(allRecentHtml)
309                         for recentCallData in allRecentData:
310                                 recentCallData["action"] = action
311                                 yield recentCallData
312
313         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
314         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
315         _contactsURL = "https://www.google.com/voice/mobile/contacts"
316
317         def get_contacts(self):
318                 """
319                 @returns Iterable of (contact id, contact name)
320                 """
321                 contactsPagesUrls = [self._contactsURL]
322                 for contactsPageUrl in contactsPagesUrls:
323                         try:
324                                 contactsPage = self._browser.download(contactsPageUrl)
325                         except urllib2.URLError, e:
326                                 _moduleLogger.exception("Translating error: %s" % str(e))
327                                 raise NetworkError("%s is not accesible" % contactsPageUrl)
328                         for contact_match in self._contactsRe.finditer(contactsPage):
329                                 contactId = contact_match.group(1)
330                                 contactName = saxutils.unescape(contact_match.group(2))
331                                 contact = contactId, contactName
332                                 yield contact
333
334                         next_match = self._contactsNextRe.match(contactsPage)
335                         if next_match is not None:
336                                 newContactsPageUrl = self._contactsURL + next_match.group(1)
337                                 contactsPagesUrls.append(newContactsPageUrl)
338
339         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
340         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
341
342         def get_contact_details(self, contactId):
343                 """
344                 @returns Iterable of (Phone Type, Phone Number)
345                 """
346                 try:
347                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
348                 except urllib2.URLError, e:
349                         _moduleLogger.exception("Translating error: %s" % str(e))
350                         raise NetworkError("%s is not accesible" % self._contactDetailURL)
351
352                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
353                         phoneNumber = detail_match.group(1)
354                         phoneType = saxutils.unescape(detail_match.group(2))
355                         yield (phoneType, phoneNumber)
356
357         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
358         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
359
360         def get_messages(self):
361                 try:
362                         voicemailPage = self._browser.download(self._voicemailURL)
363                 except urllib2.URLError, e:
364                         _moduleLogger.exception("Translating error: %s" % str(e))
365                         raise NetworkError("%s is not accesible" % self._voicemailURL)
366                 voicemailHtml = self._grab_html(voicemailPage)
367                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
368                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
369
370                 try:
371                         smsPage = self._browser.download(self._smsURL)
372                 except urllib2.URLError, e:
373                         _moduleLogger.exception("Translating error: %s" % str(e))
374                         raise NetworkError("%s is not accesible" % self._smsURL)
375                 smsHtml = self._grab_html(smsPage)
376                 parsedSms = self._parse_sms(smsHtml)
377                 decoratedSms = self._decorate_sms(parsedSms)
378
379                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
380                 return allMessages
381
382         def clear_caches(self):
383                 pass
384
385         def get_addressbooks(self):
386                 """
387                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
388                 """
389                 yield self, "", ""
390
391         def open_addressbook(self, bookId):
392                 return self
393
394         @staticmethod
395         def contact_source_short_name(contactId):
396                 return "GV"
397
398         @staticmethod
399         def factory_name():
400                 return "Google Voice"
401
402         def _grab_json(self, flatXml):
403                 xmlTree = ElementTree.fromstring(flatXml)
404                 jsonElement = xmlTree.getchildren()[0]
405                 flatJson = jsonElement.text
406                 jsonTree = parse_json(flatJson)
407                 return jsonTree
408
409         def _grab_html(self, flatXml):
410                 xmlTree = ElementTree.fromstring(flatXml)
411                 htmlElement = xmlTree.getchildren()[1]
412                 flatHtml = htmlElement.text
413                 return flatHtml
414
415         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
416         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
417         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
418         _forwardURL = "https://www.google.com/voice/mobile/phones"
419
420         def _grab_account_info(self, page):
421                 tokenGroup = self._tokenRe.search(page)
422                 if tokenGroup is None:
423                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
424                 self._token = tokenGroup.group(1)
425
426                 anGroup = self._accountNumRe.search(page)
427                 if anGroup is not None:
428                         self._accountNum = anGroup.group(1)
429                 else:
430                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
431
432                 self._callbackNumbers = {}
433                 for match in self._callbackRe.finditer(page):
434                         callbackNumber = match.group(2)
435                         callbackName = match.group(1)
436                         self._callbackNumbers[callbackNumber] = callbackName
437                 if len(self._callbackNumbers) == 0:
438                         _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
439
440         def _send_validation(self, number):
441                 if not self.is_valid_syntax(number):
442                         raise ValueError('Number is not valid: "%s"' % number)
443                 elif not self.is_authed():
444                         raise RuntimeError("Not Authenticated")
445
446                 if len(number) == 11 and number[0] == 1:
447                         # Strip leading 1 from 11 digit dialing
448                         number = number[1:]
449                 return number
450
451         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
452         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
453         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
454         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
455         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
456         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
457         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
458         _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
459         #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
460         #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
461         _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
462
463         @staticmethod
464         def _interpret_voicemail_regex(group):
465                 quality, content, number = group.group(2), group.group(3), group.group(4)
466                 if quality is not None and content is not None:
467                         return quality, content
468                 elif number is not None:
469                         return "high", number
470
471         def _parse_voicemail(self, voicemailHtml):
472                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
473                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
474                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
475                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
476                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
477                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
478                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
479                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
480                         location = locationGroup.group(1).strip() if locationGroup else ""
481
482                         nameGroup = self._voicemailNameRegex.search(messageHtml)
483                         name = nameGroup.group(1).strip() if nameGroup else ""
484                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
485                         number = numberGroup.group(1).strip() if numberGroup else ""
486                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
487                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
488                         contactIdGroup = self._messagesContactID.search(messageHtml)
489                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
490
491                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
492                         messageParts = (
493                                 self._interpret_voicemail_regex(group)
494                                 for group in messageGroups
495                         ) if messageGroups else ()
496
497                         yield {
498                                 "id": messageId.strip(),
499                                 "contactId": contactId,
500                                 "name": name,
501                                 "time": exactTime,
502                                 "relTime": relativeTime,
503                                 "prettyNumber": prettyNumber,
504                                 "number": number,
505                                 "location": location,
506                                 "messageParts": messageParts,
507                         }
508
509         def _decorate_voicemail(self, parsedVoicemails):
510                 messagePartFormat = {
511                         "med1": "<i>%s</i>",
512                         "med2": "%s",
513                         "high": "<b>%s</b>",
514                 }
515                 for voicemailData in parsedVoicemails:
516                         message = " ".join((
517                                 messagePartFormat[quality] % part
518                                 for (quality, part) in voicemailData["messageParts"]
519                         )).strip()
520                         if not message:
521                                 message = "No Transcription"
522                         whoFrom = voicemailData["name"]
523                         when = voicemailData["time"]
524                         voicemailData["messageParts"] = ((whoFrom, message, when), )
525                         yield voicemailData
526
527         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
528         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
529         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
530
531         def _parse_sms(self, smsHtml):
532                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
533                 for messageId, messageHtml in itergroup(splitSms[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
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                         contactIdGroup = self._messagesContactID.search(messageHtml)
547                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
548
549                         fromGroups = self._smsFromRegex.finditer(messageHtml)
550                         fromParts = (group.group(1).strip() for group in fromGroups)
551                         textGroups = self._smsTextRegex.finditer(messageHtml)
552                         textParts = (group.group(1).strip() for group in textGroups)
553                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
554                         timeParts = (group.group(1).strip() for group in timeGroups)
555
556                         messageParts = itertools.izip(fromParts, textParts, timeParts)
557
558                         yield {
559                                 "id": messageId.strip(),
560                                 "contactId": contactId,
561                                 "name": name,
562                                 "time": exactTime,
563                                 "relTime": relativeTime,
564                                 "prettyNumber": prettyNumber,
565                                 "number": number,
566                                 "location": "",
567                                 "messageParts": messageParts,
568                         }
569
570         def _decorate_sms(self, parsedTexts):
571                 return parsedTexts
572
573
574 def set_sane_callback(backend):
575         """
576         Try to set a sane default callback number on these preferences
577         1) 1747 numbers ( Gizmo )
578         2) anything with gizmo in the name
579         3) anything with computer in the name
580         4) the first value
581         """
582         numbers = backend.get_callback_numbers()
583
584         priorityOrderedCriteria = [
585                 ("1747", None),
586                 (None, "gizmo"),
587                 (None, "computer"),
588                 (None, "sip"),
589                 (None, None),
590         ]
591
592         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
593                 for number, description in numbers.iteritems():
594                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
595                                 continue
596                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
597                                 continue
598                         backend.set_callback_number(number)
599                         return
600
601
602 def sort_messages(allMessages):
603         sortableAllMessages = [
604                 (message["time"], message)
605                 for message in allMessages
606         ]
607         sortableAllMessages.sort(reverse=True)
608         return (
609                 message
610                 for (exactTime, message) in sortableAllMessages
611         )
612
613
614 def decorate_recent(recentCallData):
615         """
616         @returns (personsName, phoneNumber, date, action)
617         """
618         contactId = recentCallData["contactId"]
619         if recentCallData["name"]:
620                 header = recentCallData["name"]
621         elif recentCallData["prettyNumber"]:
622                 header = recentCallData["prettyNumber"]
623         elif recentCallData["location"]:
624                 header = recentCallData["location"]
625         else:
626                 header = "Unknown"
627
628         number = recentCallData["number"]
629         relTime = recentCallData["relTime"]
630         action = recentCallData["action"]
631         return contactId, header, number, relTime, action
632
633
634 def decorate_message(messageData):
635         contactId = messageData["contactId"]
636         exactTime = messageData["time"]
637         if messageData["name"]:
638                 header = messageData["name"]
639         elif messageData["prettyNumber"]:
640                 header = messageData["prettyNumber"]
641         else:
642                 header = "Unknown"
643         number = messageData["number"]
644         relativeTime = messageData["relTime"]
645
646         messageParts = list(messageData["messageParts"])
647         if len(messageParts) == 0:
648                 messages = ("No Transcription", )
649         elif len(messageParts) == 1:
650                 messages = (messageParts[0][1], )
651         else:
652                 messages = [
653                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
654                         for messagePart in messageParts
655                 ]
656
657         decoratedResults = contactId, header, number, relativeTime, messages
658         return decoratedResults
659
660
661 def test_backend(username, password):
662         backend = GVDialer()
663         print "Authenticated: ", backend.is_authed()
664         if not backend.is_authed():
665                 print "Login?: ", backend.login(username, password)
666         print "Authenticated: ", backend.is_authed()
667
668         #print "Token: ", backend._token
669         #print "Account: ", backend.get_account_number()
670         #print "Callback: ", backend.get_callback_number()
671         #print "All Callback: ",
672         import pprint
673         #pprint.pprint(backend.get_callback_numbers())
674
675         #print "Recent: "
676         #for data in backend.get_recent():
677         #       pprint.pprint(data)
678         #for data in sort_messages(backend.get_recent()):
679         #       pprint.pprint(decorate_recent(data))
680         #pprint.pprint(list(backend.get_recent()))
681
682         #print "Contacts: ",
683         #for contact in backend.get_contacts():
684         #       print contact
685         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
686
687         print "Messages: ",
688         for message in backend.get_messages():
689                 message["messageParts"] = list(message["messageParts"])
690                 pprint.pprint(message)
691         #for message in sort_messages(backend.get_messages()):
692         #       pprint.pprint(decorate_message(message))
693
694         return backend
695
696
697 if __name__ == "__main__":
698         import sys
699         logging.basicConfig(level=logging.DEBUG)
700         test_backend(sys.argv[1], sys.argv[2])